官方教程:Getting started with ASP.NET Core MVC and Entity Framework Core using Visual Studio
第一课:入门
Entity Framework Core NuGet 软件包
安装数据库提供程序(以 SQL Server 为例):其它数据库提供程序
Install-Package Microsoft.EntityFrameworkCore.SqlServer
该软件包依赖 Microsoft.EntityFrameworkCore 和 Microsoft.EntityFrameworkCore.Relational
新建数据模型
文件夹 Models 存放实体类
默认以 ID 或 类名ID 为主键列,并且是标识(即自动递增),以下代码可以取消标识,允许自定义 ID 值
[DatabaseGenerated(DatabaseGeneratedOption.None)]
导航属性:对于数据库来说就是外键,对一定义为实体类,对多定义为 ICollection<T>
字段类型可以是枚举 enum
创建数据库上下文
在 Data 文件夹中创建一个名为 SchoolContext.cs 的类,继承于 DbContext
使用依赖注入注册上下文
修改 Startup.cs 的 ConfigureServices 方法
在 appsettings.json 中配置数据库连接字符串
添加代码以使用测试数据初始化数据库
在 Data 文件夹中创建一个名为 DbInitializer.cs 的类
修改 Startup.cs 的 Configure 方法
新建一个控制器和视图
在 Controllers 文件夹上右键,添加
MVC 依赖项选择 Minimal Dependencies 即可
会自动安装 NuGet 包:Microsoft.EntityFrameworkCore.Design 和 Microsoft.EntityFrameworkCore.SqlServer.Design
公约
表名优先来自 DbSet 属性名,其次是类名
实体属性名将映射为列名
实体属性名 ID 或 类名ID 将默认被设为主键
如果属性被命名,它将当作外键(例如,导航属性 Student 对应的实体 Student 的主键是 ID,所以 StudentID 是外键);
公约可以被覆盖,例如,可以指定表名,设置列名,并将任何属性设置为主键或外键。
异步代码
第二课:新建、读取、更新和删除操作(CRUD)
自定义详细信息页面
Include() // 加载导航属性
ThenInclude() // 加载导航属性的导航属性
AsNoTracking() // 使该实体在当前上下文生命周期中不更新
SingleOrDefaultAsync() // 异步检索单个实体
路由数据
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a>
<a asp-action="Edit" asp-route-studentID="@item.ID">Edit</a>
生成为
<a href="/Students/Edit/6">Edit</a>
<a href="/Students/Edit?studentID=6">Edit</a>
改进“新建页面”
在 HttpPost 的 Create 方法中,因为 ID 是自动递增的,所以可以从 Bind 中删除
[ValidateAntiForgeryToken] 搭配 if (ModelState.IsValid),用于实现验证,防止 CSRF 攻击
改进“删除页面”
在 HttpGet 的 Delete 方法中增加参数用于传递报错内容
在 HttpPost 的 Delete 方法中删除失败则 RedirectToAction 到 Delete 页面并带上报错参数
用”初始化和附加“的方法实现删除
Student studentToDelete = new Student() { ID = id };
_context.Entry(studentToDelete).State = EntityState.Deleted;
实例化要删除的实体(仅指定主键值),并将状态设置为 Deleted,即可在 SaveChanges 时实现删除,从而避免了删除必须先读取的情况。
处理交易
当 SaveChanges 时,EF 自动确保所有更改都成功或都失败,如果首先完成一些更改,然后发生错误,那么这些更改将自动回滚。
无追踪查询
什么情况下适用 AsNoTracking 方法:
在上下文中不需要更新任何实体,并且不需要加载导航属性(若需要导航属性,可以预先 Include 或稍后单独查询)
检索大量数据时,仅一小部分将被更新(稍后单独查询)
当前检索的实体需要在稍后以不同的目的附加(_context.Entry(*).State)
第三课:排序,过滤,分布和分组
添加列排序链接到学生索引页
将“搜索框”添加到“学生索引”页面
将分页功能添加到“学生索引”页面
PaginatedList 类的使用
创建一个显示学生统计信息的关于页面
创建视图模型:在 Models 文件夹中创建一个 SchoolViewModels 文件夹,添加一个类文件 EnrollmentDateGroup.cs
修改 HomeController 添加上下文
使用 group by 进行分组查询
第四课:迁移
用于迁移的 NuGet 软件包
要使用迁移,可以使用包管理器控制台(PMC)或命令行界面(CLI)。教程
CLI 的另一种打开方式:在项目上右键 - 在文件资源管理器中打开文件夹,在地址栏上键入 cmd 回车
删除数据库的 CLI 命令
dotnet ef database drop
创建初始迁移
dotnet ef migrations add InitialCreate
创建数据库的代码位于 Migrations 文件夹中,Up 方法用于创建,Down 方法用于回滚
这时只生成了创建数据库的代码,并没有真正的创建数据库
多次创建的迁移会按时间戳的次序展示在 Migrations 文件夹中,数据库中记录了最后一次迁移的版本
检查数据模型快照
迁移会在 Migrations/SchoolContextModelSnapshot.cs 中创建当前数据库模式的快照,因此 EF Core 不必与数据库进行交互以创建迁移。
要删除最后一次迁移,执行:
dotnet ef migrations remove
将迁移应用于数据库
dotnet ef database update
执行完成会创建数据库结构,但表中的初始数据将在第一次运行时被添加。
上次应用迁移之后创建的一个或多个新迁移,将在当前应用迁移时按顺序全部执行。
第五课:创建复杂的数据模型
通过使用属性自定义数据模型
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
DataType 用于指定比数据库固有类型更具体的数据类型,上例指定为日期,而不是日期和时间
DateType 支持:Date, Time, PhoneNumber, Currency, EmailAddress 等等
DateType 支持传达数据的语义到 HTML5
DisplayFormat 用于指定格式
ApplyFormatInEditMode 指定是否当值显示在文本框中进行编辑时也应用此格式
[StringLength(50)]
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
[RegularExpression(@"^[A-Z]+[a-zA-Z''-'\s]*$")]
StringLength 设置数据库中最大长度,并为客户端和服务端提供验证
StringLength 可以指定最小长度,但该值对数据库没有影响
StringLength 不会阻止用户输入空格,可以使用 RegularExpression 来限制
缩短最大长度会导致迁移时发出警告,为了保证顺利迁移,请先更改数据或重新选择合适的最大长度
[Column("FirstName")]
public string FirstMidName { get; set; }
上例将属性 FirstMidName 映射到数据库列名 FirstName
[Required]
必需属性,不能为空的类型将自动处理必填字段,无需重复指定。
[Display(Name = "Last Name")]
显示属性,指定用于显示的名称
计算属性,只有一个 get 访问器,不会在数据库中生成列,例如:
public string FullName
{
get
{
return LastName + ", " + FirstMidName;
}
}
可以将多个属性放在一行中:
[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
在一对一的关系中,副表的主键名称不以副表实体 classnameID 命名,而以主表实体 classnameID 命名时,用 Key 属性来指定主键:
[Key]
或者需要以除 classnameID 或 ID 外的其它属性为主键,那么可以用 Key
[Range(0, 5)]
指定数值范围
当有相关实体的导航属性时,EF 不要求在数据模型中添加外键属性。详情见教程
[DatabaseGenerated(DatabaseGeneratedOption.None)]
主键值由用户提供,而不是由数据库生成。
[Column(TypeName = "money")]
public decimal Budget { get; set; }
通常不需要列映射,此例将 decimal 类型的属性 Budget 映射为 money 类型的字段 Budget。
部门可能有负责人也可能没有,负责人必定是教师。因此 InstructorID 属性为教师实体的外键,并且可空,导航属性是被命名为 Administrator 的 Instructor 实体。
public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }
通常,EF 允许对不可空的外键进行级联删除以久多对多关系。
例如,如果将 Department.InstructorID 属性定义为不可空,则 EF 将配置级联删除规则,以在删除部门时删除教师。如果我们想保留教师,但业务逻辑要求该 InstructorID 属性确实不可为空,则必须使用以下语句来禁用关系中的级联删除:
modelBuilder.Entity<Department>()
.HasOne(d => d.Administrator)
.WithMany()
.OnDelete(DeleteBehavior.Restrict)
补充,现在设计业务逻辑时会尽量避免删除操作,而使用禁用来代替,以在数据库中永久保留记录。
[DisplayFormat(NullDisplayText = "No grade")]
NullDisplayText 指定当值为 null 时的显示内容
联合主键
在多对多关系(教师到课程)的连接表(CourseAssignment)中,两个外键都不可为空,并且一同唯一标识表的每一行,因此不需要单独的主键,将 InstructorID 和 CourseID 作为一个联合主键。
定义联合主键不能通过使用属性来完成,唯一方法是使用 Fluent API,修改 Data/SchoolContext.cs 的 OnModelCreating 方法:
modelBuilder.Entity<CourseAssignment>().ToTable("CourseAssignment");
modelBuilder.Entity<CourseAssignment>()
.HasKey(c => new { c.CourseID, c.InstructorID });
Fluent API 替代属性
protected override void OnModelCreating(ModelBuilder modelBuilder){
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.IsRequired();
}
在本教程中,您只使用 Fluent API 进行数据库映射,而不能使用属性。您还可以使用 Fluent API 来指定可以使用属性执行的大多数格式化、验证和映射规则。一些属性(如 MinimumLength)不能应用 Fluent API,因为它不会更改数据库模式,它只适用于客户端和服务端验证。
一些开发人员喜欢专门使用 Fluent API,以保持实体类“干净”。当然可以混合使用属性和 Fluent API,但推荐只选择一种方式。如果混合使用,那么 Fluent API 会覆盖属性。
添加迁移
dotnet ef migrations add ComplexDataModel
如果在此时尝试运行命令 database update(不要这样做),您将收到以下错误:
ALTER TABLE语句与FOREIGN KEY约束“FK_dbo.Course_dbo.Department_DepartmentID”冲突。冲突发生在数据库“ContosoUniversity”,表“dbo.Department”,“DepartmentID”列。
这是因为在课程 Course 表增加了一个 DepartmentID 外键,而且是不可空的,又由于课程表中已经存在数据,所以无法 AddColumn。
我们可以创建一个临时部门,然后将它的 DepartmentID 值赋给 Course.DepartmentID 作为默认值。
打开 _ComplexDataModel.cs 文件,注释将 DepartmentID 添加到课程表的代码行,注释之,并在创建 Department 表的代码之后添加创建默认部门和添加 DepartmentID 的代码,详见教程。
第六课:读取相关数据
预先加载、显式加载和延时加载
预先加载:当实体被读取时,相关数据随之被检索。即在单个连接查询中读取所有所需数据。通过 Include 和 ThenInclude 方法实现。
显式加载:当实体首次读取时,不检索相关数据。如果需要再检索相关数据。显式加载会导致多次数据库查询。
延时加载:当实体首次读取时,不检索相关数据。在首次尝试访问导航属性时,将自动检索该导航属性所需的数据。
* 从定义上看“显示加载”和“延时加载”比较类似,详细区别会在学完本课后有具体的了解。
创建一个显示部门名称的课程页面
在课程列表中,每个课程都显示部门名称,因此使用预先加载。
在控制器中使用 Include 和 AsNoTracking
在视图中使用 @Html.DisplayFor(modelItem => item.Department.Name)
创建一个显示课程和报名的教师页面
为教师页面创建一个视图模型 InstructorIndexData.cs
在控制器中多次使用 Include 和 ThenInclude 来预先加载相关数据
第一个 Include 加载办公室(一对零或一)
第二个 Include 加载课程(多对多)和报名课程的学生(多对多)
第三个 Include 加载课程(多对多)和课程所在部门(多对一)
本例中“课程的学生”和“课程的部门”是各自级联 ThenInclude 加载的,不能合并到同一个 Include 的课程中,因为 ThenInclude 不能读取多个分支的导航属性,每个分支都必须使用 Include 从教师实体调用。
在视图中给选择的教师添加 class 样式类
显式加载
去掉 Include 和 AsNoTracking,然后在需要的时候附加相关的实体(上下文.Entry())来实现显式加载,以减少首次读取实体时的数据量。
await 上下文.Entry(单个实体).Collection(x=>x.导航属性).LoadAsync(); // 一对多
await 上下文.Entry(单个实体).Reference(x=>x.导航属性).LoadAsync(); // 一对一
第七课:更新相关数据
课程必须与部门关联,在新建和编辑课程时提供部门的下拉列表。
ViewBag.DepartmentID = new SelectList(departmentsQuery.AsNoTracking(), "DepartmentID", "Name", selectedDepartment);
<select asp-for="DepartmentID" class="form-control" asp-items="ViewBag.DepartmentID">
<option value="">-- Select Department --</option>
</select>
SelectList 为下拉列表创建一个集合,允许指定默认选中项,并传递到 ViewBag。
将 .AsNoTracking() 添加到详细信息和删除方法
“一对零或一”关系:(本例是教师和办公室分配,抑或是文章和正文、学生和档案)
删除教师的办公室分配时,如果原先具有值,则删除办公室分配;
输入教师的办公室分配时,如果原先为空,则创建一个办公室分配;
更改教师的办公室分配时,更改现在有办公室分配值。
如何处理:
在 HttpGet 的 Edit 方法中,Include 办公室实体,并 AsNoTracking
在 HttpPost 的 Edit 方法中,移除除 id 以外的参数(为了不与 HttpGet 的 Edit 冲突,改名为 EditPost,并 ActionName("Edit"))
if (await TryUpdateModelAsync<Instructor>(
instructorToUpdate,
"",
i => i.FirstMidName, i => i.LastName, i => i.HireDate, i => i.OfficeAssignment))
使用模型绑定器的值更新检索到的教师实体。TryUpdateModel 支持更新实体的导航属性。
我们可以直接设置实体的导航属性为 null,以便相关的行将被删除。
第八课:处理并发冲突
悲观并发,即锁定,性能差。
乐观并发:
[Timestamp]
public byte[] RowVersion { get; set; }
添加一个 timestamp 类型的 RowVersion 字段
详见教程
第九课:继承
第十课:进阶专题
调用“返回实体”的查询
为了防止 SQL 注入,使用参数化查询:
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
string query = "SELECT * FROM Department WHERE DepartmentID = {0}";
var department = await _context.Departments
.FromSql(query, id)
.Include(d => d.Administrator)
.AsNoTracking()
.SingleOrDefaultAsync();
if (department == null)
{
return NotFound();
}
return View(department);
}
调用“返回其他类型”的查询
编写 SQL,而非 LINQ:
public async Task<ActionResult> About()
{
List<EnrollmentDateGroup> groups = new List<EnrollmentDateGroup>();
var conn = _context.Database.GetDbConnection();
try
{
await conn.OpenAsync();
using (var command = conn.CreateCommand())
{
string query = "SELECT EnrollmentDate, COUNT(*) AS StudentCount "
+ "FROM Person "
+ "WHERE Discriminator = 'Student' "
+ "GROUP BY EnrollmentDate";
command.CommandText = query;
DbDataReader reader = await command.ExecuteReaderAsync();
if (reader.HasRows)
{
while (await reader.ReadAsync())
{
var row = new EnrollmentDateGroup { EnrollmentDate = reader.GetDateTime(0), StudentCount = reader.GetInt32(1) };
groups.Add(row);
}
}
reader.Dispose();
}
}
finally
{
conn.Close();
}
return View(groups);
}
调用“更新”查询(批量更新)
使用参数化查询批量更新数据,返回受影响行数:
ViewData["RowsAffected"] =
await _context.Database.ExecuteSqlCommandAsync(
"UPDATE Course SET Credits = Credits * {0}",
parameters: multiplier);
检查发送到数据库的 SQL
有时,可以看到发送到数据库的实际 SQL 查询很有帮助。
第一步:设置一个断点
第二步:调试模式运行程序
第三步:在“输出”窗口可以看到 SQL 查询语句
自动变化检测
EF 通过将实体的当前值与原始值进行比较来确定实体如何更改。
一些方法会导致自动更改检测:
DbContext.SaveChages
DbContext.Entry
ChangeTracker.Entries
如果您正在跟踪大量实体,并且您在循环中多次调用上述方法之一,则可以通过临时关闭 AutoDetectChangesEnabled 属性,能显著提升性能。
_context.ChangeTracker.AutoDetectChangesEnabled = false;
从现有数据库反向工程
常见错误
问:ContosoUniversity.dll 由另一个进程使用
答:在 IIS Express 中停止站点。
问:迁移在 Up 和 Down 方法中没有代码
原因:在运行migrations add
命令时有未保存的更改
答:运行migrations remove
命令,保存代码更改并重新运行migrations add
命令。
本文适用于 EF Core,并非 EF 6.x 及以下。
在本演练中,您将构建一个使用 Entity Framework 执行基本数据访问的 ASP.NET Core MVC 应用程序。您将使用逆向工程基于现有数据库创建实体框架模型。
创建一个新项目
打开 Visual Studio 2017
文件 -> 新建 -> 项目...
从左侧菜单中选择 已安装 -> 模板 -> Visual C# -> Web
选择 ASP.NET Core Web应用程序(.NET Core)项目模板
输入项目名称,然后单击确定
在 ASP.NET Core 1.1 选择 Web 应用程序
确保“身份验证”设置为“不进行身份验证”
点击确定
安装 Entity Framework
要使用 EF Core,请安装目标数据库提供程序的软件包。本演练使用 SQL Server。有关可用提供程序的列表,请参阅 Database Providers(从打开页面的左侧菜单选择相应的数据库)。
工具 -> NuGet 包管理器 -> 管理解决方案的 NuGet 程序包
在“浏览”选项卡中搜索并安装 Microsoft.EntityFrameworkCore.SqlServer
我们将使用一些 Entity Framework 命令从数据库创建模型。所以我们也将安装工具包。
安装 Microsoft.EntityFrameworkCore.SqlServer.Design
安装 Microsoft.EntityFrameworkCore.Tools
模型的反向工程
现在是根据现有数据库创建 EF 模型的时候了。
工具 -> NuGet 包管理器 -> 程序包管理器控制台
运行
Scaffold-DbContext "数据库连接字符串" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models
数据库连接字符串可以从 服务器资源管理器 -> 数据连接,在要连接的数据库上右击属性来获得。
如果您收到以下错误说明,请关闭并重新打开 Visual Studio 后重试。
The term 'Scaffold-DbContext' is not recognized as the name of a cmdlet
逆向工程过程基于现有数据库的模式创建实体类(表名.cs)和派生上下文(库名Context.cs)。
实体类是简单的 C# 对象,表示要查询和保存的数据。
上下文表示与数据库的会话,并允许您查询和保存实体类的实例。
使用依赖注入注册上下文
依赖注入的概念是ASP.NET Core的核心。服务(例如数据库 Context)在应用程序启动期间使用依赖注入进行注册。然后,需要这些服务的组件(例如您的 MVC 控制器)通过构造函数参数或属性提供这些服务。
删除内联上下文配置
打开 Models\库名Context.cs
删除方法 OnConfiguring(...)
添加以下构造函数,这将允许通过依赖注入将配置传递到上下文中
public 库名Context(DbContextOptions<库名Context> options) : base(options) { }
在 Startup.cs 中注册和配置上下文
为了使我们的 MVC 控制器使用数据库Context,我们将它注册为一个服务。
打开 Startup.cs
使用 using 语句在文件的开头添加以下内容
using 项目命名空间.Models; using Microsoft.EntityFrameworkCore;
现在我们可以使用 AddDbContext(...)方法将其注册为服务。
找到 ConfigureServices(...)方法
添加以下代码以将上下文注册为服务
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { // Add framework services. services.AddMvc(); var connection = @"连接字符串"; services.AddDbContext<库名Context>(options => options.UseSqlServer(connection)); }
在实际应用程序中,通常将连接字符串放在配置文件中。为了简单起见,我们在代码中定义它。查看如何将连接字符串配置到 appsettings.json 文件中
创建一个控制器
接下来,我们将在我们的项目中启用基架。
右键单击解决方案资源管理器中的 Controllers 文件夹,然后选择添加 -> 控制器...
选择 Full Dependencies(完全依赖),然后单击添加
您可以忽略打开的
ScaffoldingReadMe.txt
文件中的指令
现在基架已启用,我们可以为数据表实体构建控制器。
右键单击解决方案资源管理器中的 Controllers 文件夹,然后选择添加 -> 控制器...
选择“视图使用 Entity Framework 的 MVC 控制器”并单击“添加”
设置模型类和数据上下文类
单击添加
运行应用程序
您现在可以运行应用程序以查看它的操作。
调试 -> 开始执行(不调试)
应用程序将在 Web 浏览器中构建和打开
导航到相应的路径
/控制器名
单击 Create New
填写表单并单击 Create
EF Core for MySQL
截至 Visual Studio 2017 正式版发布(2017年3月7日),MySQL for Visual Studio(支持)尚不支持 Visual Studio 2017,因此 Visual Studio 2017 的服务器资源管理器不能连接数据源为 MySQL 的数据库,也无法创建和更新可视化模型(.edmx)
NuGet 包 MySql.Data.EntityFrameworkCore 还没有找到 Database First 的 PM 命令,因此不能由 MySQL 数据库生成实体类和上下文
综上,要连接 MySQL 只能用 VS2015,要生成 MySQL 实体类和上下文就不能用 EF Core,暂时使用 EF 6.x 代替(创建 ASP.NET MVC 项目)
EF Core 1.1 未实现
EF Core 1.1 未实现:可视化模型,以查看基于代码的模型的图形表示。
EF Core 1.1 未实现:用于逆向工程的 Visual Studio 向导,允许您在从现有数据库创建模型时可视化地配置连接,选择表等。
EF Core 1.1 未实现:从数据库更新模型,允许从数据库以前反向工程的模型使用对模式所做的更改进行刷新。
补充
如果我们在创建新项目时选择 ASP.NET Core Web应用程序(.NET Framework)项目模板,那么完全按照 Core 的方式来开始(比如选择 NuGet 包的版本),而发布必须是 .NET Framework 环境,而非跨平台。
参考
程序设计规范:
【推荐】上传的文件直接保存到文件存储服务(如阿里云 OSS),这样即使被上传了后门 shell,对网站服务器也不会有影响。
否则必须通过文件头来确定文件类型,检查文件十六进制的文件头和文件尾是否合法,并检查文件流中是否包含 php、evel 等字符。
不可直接使用客户端文件名来保存文件,特别是后缀名/扩展名。应生成随机文件名,并通过检验文件头来确定文件类型。必须由程序指定保存目录。
使用 OSS 的应直接上传,不要在 ECS 上临时存放或备份。如必须存放的,应按上述规范操作。
服务器安全设置
CentOS + nginx + PHP:
全站文件取消属性中的“执行”权限(chmod),因为这个“执行”与运行 PHP 无关。而需要上传文件的“目录”需要“执行”权限,原因是需要往该目录创建文件。
仅需要写入的目录或文件设置“写入”权限。如上传图片目录、ThinkPHP 的 Runtime 目录。
凡可写目录或文件均不允许运行 PHP / PY 等 除需要被直接访问的 PHP / PY 文件,其它动态文件均不允许被访问到,在 nginx 的配置文件中添加项,参:https://xoyozo.net/Blog/Details/nginx-location-if,若全站使用统一的入口文件访问,那么设置仅该文件允许运行 PHP 即可。通过 IO 方式被其它文件包含的文件,无需运行 PHP 权限。(“deny all”对 include 起作用,但对 IO 不起作用,因此 Runtime 目录可以继续为 ThinkPHP 提供缓存服务。)这一步非常有用。
使用与 nginx 网站用户不同的用户来部署网站文件,如宝塔面板 PHP 使用 www 用户,那么就使用 root 或其它新用户来上传文件,否则将导致全站目录和文件可写。有条件的建议不同网站使用不同的用户,可防止一个网站被入侵后导致其它网站文件或磁盘上的其它文件被泄露的风险(2022年10月2日从宝塔官方社区获悉,宝塔面板暂不支持使用非 www 用户创建并运行网站)。
Windows Server + IIS + ASP.NET:
配置每个磁盘的安全属性,拒绝“IIS_IUSRS”这个用户组的所有权限。只要设置驱动器即可,子文件夹和文件会自动继承。若运行 .NET Framework 项目,需要设置 C:\Windows\Microsoft.NET\Framework\v*.*.*****\Temporary ASP.NET Files\ 目录可修改写入权限,.NET Core 项目不需要此设置。
为每个网站创建一个新用户,仅隶属于“IIS_IUSRS”。网站根目录安全属性添加该用户,权限选择“读取”。(已测取消“读取与执行”不影响 PHP,“列出文件夹内容”视业务需求开启,建议关闭)。仅需要上传文件的目录或文件设置“修改”、“写入”权限。(修改对应修改文件,写入对应上传文件)
IIS 网站中设置“物理路径凭据”以及应用程序池的“标识”。
IIS 中设置写入目录的“处理程序映射”无脚本。
了解使用 Visual Studio 创建的模板项目
启动 VS,从菜单项打开:文件 - 新建 - 项目,在已安装的模板中选择 .NET Core,选择 ASP.NET Core Web Application(.NET Core),给项目取个名称,确定。选择 Web 应用程序,确定。
· global.json: 你可以在这里放置解决方案的配置信息和工程之间的引用。作用类似于 *.sln 文件。
· Program.cs: 这个文件包含了 ASP.NET Core 应用的 Main 方法,负责配置和启动应用程序。
· src 文件夹: 包含组成你应用程序的全部项目代码。
· wwwroot: 你的静态文件将被放置在这个文件夹,它们都将作为资源直接提供给客户端,包含 HTML,CSS 和 JavaScript 文件。可以在 project.json 内重置。
· project.json: 包含项目设置。在 ASP.NET Core 中,你可以通过使用 NuGet 程序包管理工具(NPM)添加 NuGet 包或者编辑这个文件来管理从属。
· project.lock.json: 由 project.json 生成。
· Startup.cs 这个主要放置你 ASP.NET Core 的 stratup 和 configuration 代码。ConfigureServices 方法定义了你应用程序使用的服务,Configure 方法用来定义组成请求管道的中间件。
一些备注:
ASP.NET Core 项目中不能创建 Web 窗体页面(.aspx)
发布后的网站根目录是项目中的 /wwwroot 目录
路由在 Startup 的 Configure 方法中配置
在 ASP.NET Core 或 ASP.NET 5 中部署百度编辑器请跳转此文。
本文记录百度编辑器 ASP.NET 版的部署过程,对其它语言版本也有一定的参考价值。
【2020.02.21 重新整理】
下载
从 GitHub 下载最新发布版本:https://github.com/fex-team/ueditor/releases
按编码分有 gbk 和 utf8 两种版本,按服务端编程语言分有 asp、jsp、net、php 四种版本,按需下载。
目录介绍
以 v1.4.3.3 utf8-net 为例,
客户端部署
本例将上述所有目录和文件拷贝到网站目录 /libs/ueditor/ 下。
当然也可以引用 CDN 静态资源,但会遇到诸多跨域问题,不建议。
在内容编辑页面引入:
<script src="/libs/ueditor/ueditor.config.js"></script>
<script src="/libs/ueditor/ueditor.all.min.js"></script>
在内容显示页面引入:
<script src="/libs/ueditor/ueditor.parse.min.js"></script>
如需修改编辑器资源文件根路径,参 ueditor.config.js 文件内顶部文件。(一般不需要单独设置)
如果使用 CDN,那么在初始化 UE 实例的时候应配置 serverUrl 值(即 controller.ashx 所在路径)。
客户端配置
初始化 UE 实例:
var ue = UE.getEditor('tb_content', {
// serverUrl: '/libs/ueditor/net/controller.ashx', // 指定服务端接收文件路径
initialFrameWidth: '100%'
});
其它参数见官方文档,或 ueditor.config.js 文件。
服务端部署
net 目录是 ASP.NET 版的服务端程序,用来实现接收上传的文件等功能。
本例中在网站中的位置是 /libs/ueditor/net/。如果改动了位置,那么在初始化 UE 的时候也应该配置 serverUrl 值。
这是一个完整的 VS 项目,可以单独部署为一个网站。其中:
net/config.json 服务端配置文件
net/controller.ashx 文件上传入口
net/App_Code/CrawlerHandler.cs 远程抓图动作
net/App_Code/ListFileManager.cs 文件管理动作
net/App_Code/UploadHandler.cs 上传动作
该目录不需要转换为应用程序。
服务端配置
根据 config.json 中 *PathFormat 的默认配置,一般地,上传的图片会保存在 controller.ashx 文件所在目录(即本例中的 /libs/ueditor/)的 upload 目录中:
/libs/ueditor/upload/image/
原因是 UploadHandler.cs 中 Server.MapPath 的参数是由 *PathFormat 决定的。
以修改 config.json 中的 imagePathFormat 为例:
原值:"imagePathFormat": "upload/image/{yyyy}{mm}{dd}/{time}{rand:6}"
改为:"imagePathFormat": "/upload/ueditor/{yyyy}{mm}{dd}/{time}{rand:6}"
以“/”开始的路径在 Server.MapPath 时会定位到网站根目录。
此处不能以“~/”开始,因为最终在客户端显示的图片路径是 imageUrlPrefix + imagePathFormat,若其中包含符号“~”就无法正确显示。
在该配置文件中查找所有 PathFormat,按相同的规则修改。
说到客户端的图片路径,我们只要将
原值:"imageUrlPrefix": "/ueditor/net/"
改为:"imageUrlPrefix": ""
即可返回客户端正确的 URL。
当然也要同步修改 scrawlUrlPrefix、snapscreenUrlPrefix、catcherUrlPrefix、videoUrlPrefix、fileUrlPrefix。
特殊情况,在复制包含图片的网页内容的操作中,若图片地址带“?”等符号,会出现无法保存到磁盘的情况,需要修改以下代码:
打开 CrawlerHandler.cs 文件,找到
ServerUrl = PathFormatter.Format(Path.GetFileName(this.SourceUrl), Config.GetString("catcherPathFormat"));
替换成:
ServerUrl = PathFormatter.Format(Path.GetFileName(SourceUrl.Contains("?") ? SourceUrl.Substring(0, SourceUrl.IndexOf("?")) : SourceUrl), Config.GetString("catcherPathFormat"));
如果你将图片保存到第三方图库,那么 imageUrlPrefix 值设为相应的域名即可,如:
改为:"imageUrlPrefix": "//cdn.***.com"
然后在 UploadHandler.cs 文件(用于文件上传)中找到
File.WriteAllBytes(localPath, uploadFileBytes);
在其下方插入上传到第三方图库的代码,以阿里云 OSS 为例:
// 上传到 OSS
client.PutObject(bucketName, savePath.Substring(1), localPath);
在 CrawlerHandler.cs 文件(无程抓图上传)中找到
File.WriteAllBytes(savePath, bytes);
在其下方插入上传到第三方图库的代码,以阿里云 OSS 为例:
// 上传到 OSS
client.PutObject(bucketName, ServerUrl.Substring(1), savePath);
最后有还有两个以 UrlPrefix 结尾的参数名 imageManagerUrlPrefix 和 fileManagerUrlPrefix 分别是用来列出上传目录中的图片和文件的,
对应的操作是在编辑器上的“多图上传”功能的“在线管理”,和“附件”功能的“在线附件”。
最终列出的图片路径是由 imageManagerUrlPrefix + imageManagerListPath + 图片 URL 组成的,那么:
"imageManagerListPath": "/upload/ueditor/image",
"imageManagerUrlPrefix": "",
以及:
"fileManagerListPath": "/upload/ueditor/file",
"fileManagerUrlPrefix": "",
即可。
如果是上传到第三方图库的,且图库上的文件与本地副本是一致的,那么将 imageManagerUrlPrefix 和 fileManagerUrlPrefix 设置为图库域名,
服务端仍然以 imageManagerListPath 指定的路径来查找本地文件(非图库),但客户端显示图库的文件 URL。
因此,如果文件仅存放在图库上,本地没有副本的情况就无法使用该功能了。
综上,所有的 *UrlPrefix 应该设为一致。
另外记得配置不希望被远程抓图的域名,参数 catcherLocalDomain。
服务端授权
现在来判断一下只有登录用户才允许上传。
首先打开服务端的统一入口文件 controller.ashx,
继承类“IHttpHandler”改为“IHttpHandler, System.Web.SessionState.IRequiresSessionState”,即同时继承两个类,以便可使用 Session,
找到“switch”,其上插入:
if (用户未登录) { throw new System.Exception("请登录后再试"); }
即用户已登录或 action 为获取 config 才进入 switch。然后,
else
{
action = new NotAllowedHandler(context);
}
这里的 NotAllowedHandler 是参照 NotSupportedHandler 创建的,提示语 state 可以是“登录后才能进行此操作。”
上传目录权限设置
上传目录(即本例中的 /upload/ueditor/ 目录)应设置允许写入和禁止执行。
基本用法
设置内容:
ue.setContent("Hello world.");
获取内容:
var a = ue.getContent();
更多用法见官方文档:http://fex.baidu.com/ueditor/#api-common
其它事宜
配置上传附件的文件格式
找到文件:config.json,更改“上传文件配置”的 fileAllowFiles 项,
同时在 Web 服务器上允许这些格式的文件可访问权限。以 IIS 为例,在“MIME 类型”模块中添加扩展名。
遇到从客户端(......)中检测到有潜在危险的 Request.Form 值。请参考此文
另外,对于不支持上传 .webp 类型的图片的问题,可以作以下修改:
config.json 中搜索“".bmp"”,替换为“".bmp", ".webp"”
IIS 中选中对应网站或直接选中服务器名,打开“MIME 类型”,添加,文件扩展名为“.webp”,MIME 类型为“image/webp”
最后,为了在内容展示页面看到跟编辑器中相同的效果,请参照官方文档引用 uParse
若有插入代码,再引用:
<link href="/lib/ueditor/utf8-net/third-party/SyntaxHighlighter/shCoreDefault.css" rel="stylesheet" />
<script src="/lib/ueditor/utf8-net/third-party/SyntaxHighlighter/shCore.js"></script>
其它插件雷同。
若对编辑器的尺寸有要求,在初始化时设置即可:
var ue = UE.getEditor('tb_content', {
initialFrameWidth: '100%',
initialFrameHeight: 320
});
推荐参考更新的文章:.NET 5 / ASP.NET Core / ASP.NET Frameworks 从纯真 IP 数据库(QQWry.Dat)中查询 IP 归属地
纯真IP数据库官方下载地址:http://www.cz88.net/
之前我写过将 QQWry.Dat 转为 Access 格式后,在 ASP.NET 中通过简单算法去查询,缺点是 IP 数据库体积变大。现在有了更简单的方法:
1,下载 IPLocation.dll (备用地址),将它放在网站的 Bin 目录下,将 QQWry.Dat 放在网站的任何位置(一般为 App_Data 目录)。刷新解决方案管理器。
2,在使用的页面加入引用
using IPLocation;
或者在 web.config 文件中,节点 pages -> controls 添加
<add tagPrefix="IPLocation" namespace="IPLocation" assembly="IPLocation"/>
3,调用方法:
Label1.Text = IPLocation.IPLocation.IPLocate(Server.MapPath("~/App_Data/QQWry.Dat"), TextBox1.Text.Trim());
方法 IPLocate() 参数一为 QQWry.Dat 路径,参数二为 IP 地址,返回 IP 归属地。
够简单吧,补充一点,在 VS2003 需要在项目中添加 IPLocation.dll 的引用。