用ASP.NET Core MVC 和 EF Core 构建Web应用 (一)
系统必备
- .NET Core 2.0.0 SDK 或更高版本。
- 已安装 ASP.NET 和 Web 开发工作负载的 Visual Studio 2017 15.3 版或更高版本。
创建Web应用程序
打开 Visual Studio 并创建一个新 ASP.NET Core C# web 项目名为”ContosoUniversity”。
-
从文件菜单上,选择新建 > 项目。
-
从左窗格中,选择已安装 > Visual C# > Web。
-
选择“ASP.NET Core Web 应用程序”项目模板。
-
输入ContosoUniversity作为名称,然后单击确定。
-
在 “新建 ASP.NET Core Web 应用程序” 对话框,选择ASP.NET Core 2.1和 Web 应用程序 (模型-视图-控制器)模板。
-
注意:本教程需要安装 ASP.NET Core 2.0 和 EF Core 2.0 或更高版本。
-
请确保身份验证设置为不进行身份验,单击“确定”。
修改页面菜单布局和主页
打开Views/Shared/_Layout.cshtml并进行以下更改:
-
将文件中的”ContosoUniversity”更改为”Contoso University”。 需要更改三个地方。
-
添加菜单项Students,Courses,Instructors,和Department,并删除Contact菜单项。
1 <li><a asp-area="" asp-controller="Students" asp-action="Index">Students</a></li> 2 <li><a asp-area="" asp-controller="Courses" asp-action="Index">Courses</a></li> 3 <li><a asp-area="" asp-controller="Instructors" asp-action="Index">Instructors</a></li> 4 <li><a asp-area="" asp-controller="Departments" asp-action="Index">Departments</a></li>
在Views/Home/Index.cshtml,将文件的内容替换为以下代码以将有关 ASP.NET 和 MVC 的内容替换为有关此应用程序的内容:
@{ ViewData["Title"] = "Home Page"; } <div class="jumbotron"> <h1>Contoso University</h1> </div> <div class="row"> <div class="col-md-4"> <h2>Welcome to Contoso University</h2> <p> Contoso University is a sample application that demonstrates how to use Entity Framework Core in an ASP.NET Core MVC web application. </p> </div> <div class="col-md-4"> <h2>Build it from scratch</h2> <p>You can build the application by following the steps in a series of tutorials.</p> <p><a class="btn btn-default" href="https://docs.asp.net/en/latest/data/ef-mvc/intro.html">See the tutorial »</a></p> </div> <div class="col-md-4"> <h2>Download it</h2> <p>You can download the completed project from GitHub.</p> <p><a class="btn btn-default" href="https://github.com/aspnet/Docs/tree/master/aspnetcore/data/ef-mvc/intro/samples/cu-final">See project source code »</a></p> </div> </div>
按 CTRL + F5 来运行该项目或从菜单选择调试 > 开始执行不调试。 你会看到首页和将通过这个教程创建的页对应的选项卡。
数据访问部分使用 EF Core
若要为项目添加 EF Core 支持,需要安装相应的数据库驱动包。 本教程使用 SQL Server,相关驱动包Microsoft.EntityFrameworkCore.SqlServer。 该包包含在Microsoft.AspNetCore.All 包中,因此不需要手动安装。此包和其依赖项 (Microsoft.EntityFrameworkCore
和Microsoft.EntityFrameworkCore.Relational
) 一起提供 EF 的运行时支持。
创建数据模型
创建 Contoso 大学应用程序的实体类,从以下三个实体类开始。
Student
和Enrollment
实体之间是一对多的关系,Course
和Enrollment
实体之间也是一个对多的关系。 换而言之,一名学生可以修读任意数量的课程, 并且某一课程可以被任意数量的学生修读。
在Models文件夹中,创建一个名为Student.cs的类文件并且将模板代码替换为以下代码。
1 using System; 2 using System.Collections.Generic; 3 4 namespace ContosoUniversity.Models 5 { 6 public class Student 7 { 8 public int ID { get; set; } 9 public string LastName { get; set; } 10 public string FirstMidName { get; set; } 11 public DateTime EnrollmentDate { get; set; } 12 13 public ICollection<Enrollment> Enrollments { get; set; } 14 } 15 }
ID
属性将成为对应于此类的数据库表中的主键。 默认情况下,EF 将会将名为ID
或classnameID
的属性解析为主键。
Enrollments
属性是导航属性。 导航属性中包含与此实体相关的其他实体。 在这个案例下,Student entity
中的Enrollments
属性会保留所有与Student
实体相关的Enrollment
。 换而言之,如果在数据库中有两行描述同一个学生的修读情况 (两行的 StudentID 值相同,而且 StudentID 作为外键和某位学生的主键值相同),Student
实体的Enrollments
导航属性将包含那两个Enrollment
实体。
如果导航属性可以具有多个实体 (如多对多或一对多关系),那么导航属性的类型必须是可以添加、 删除和更新条目的容器,如ICollection<T>
。 你可以指定ICollection<T>
或实现该接口类型,如List<T>
或HashSet<T>
。 如果指定ICollection<T>
,EF在默认情况下创建HashSet<T>
集合。
在Models文件夹中,创建Enrollment.cs并且用以下代码替换现有代码:
1 namespace ContosoUniversity.Models 2 { 3 public enum Grade 4 { 5 A, B, C, D, F 6 } 7 8 public class Enrollment 9 { 10 public int EnrollmentID { get; set; } 11 public int CourseID { get; set; } 12 public int StudentID { get; set; } 13 public Grade? Grade { get; set; } 14 15 public Course Course { get; set; } 16 public Student Student { get; set; } 17 } 18 }
EnrollmentID
属性将被设为主键; 此实体使用classnameID
模式而不是如Student
实体那样直接使用ID
。 通常情况下,你选择一个主键模式,并在你的数据模型自始至终使用这种模式。 在这里,使用了两种不同的模式只是为了说明你可以使用任一模式来指定主键。
Grade
属性是enum
。 Grade
声明类型后的?
表示Grade
属性可以为 null。 评级为 null 和评级为零是有区别的 –null 意味着评级未知或者尚未分配。
StudentID
属性是一个外键,Student
是与其且对应的导航属性。 Enrollment
实体与一个Student
实体相关联,因此该属性只包含单个Student
实体 (与前面所看到的Student.Enrollments
导航属性不同后,Student
中可以容纳多个Enrollment
实体)。
CourseID
属性是一个外键,Course
是与其对应的导航属性。 Enrollment
实体与一个Course
实体相关联。
如果一个属性名为<导航属性名><主键属性名>
,Entity Framework 就会将这个属性解析为外键属性(例如,Student
实体的主键是ID
,Student
是Enrollment
的导航属性所以Enrollment
实体中StudentID
会被解析为外键)。 此外还可以将需要解析为外键的属性命名为<主键属性名>
(例如,CourseID
由于Course
实体的主键所以CourseID
也被解析为外键)。
在Models文件夹中,创建Course.cs并且用以下代码替换现有代码:
1 using System.Collections.Generic; 2 using System.ComponentModel.DataAnnotations.Schema; 3 4 namespace ContosoUniversity.Models 5 { 6 public class Course 7 { 8 [DatabaseGenerated(DatabaseGeneratedOption.None)] 9 public int CourseID { get; set; } 10 public string Title { get; set; } 11 public int Credits { get; set; } 12 13 public ICollection<Enrollment> Enrollments { get; set; } 14 } 15 }
Enrollments
属性是导航属性。 一个Course
实体可以与任意数量的Enrollment
实体相关。
创建数据库上下文
使得给定的数据模型与 Entity Framework 功能相协调的主类是数据库上下文类。 可以通过继承 Microsoft.EntityFrameworkCore.DbContext
类的方式创建此类。 在该类中你可以指定数据模型中包含哪些实体。 你还可以定义某些 Entity Framework 行为。 在此项目中将数据库上下文类命名为SchoolContext
。
在项目文件夹中,创建名为的文件夹Data。在Data文件夹创建名为SchoolContext.cs的类文件,并将模板代码替换为以下代码:
1 using ContosoUniversity.Models; 2 using Microsoft.EntityFrameworkCore; 3 4 namespace ContosoUniversity.Data 5 { 6 public class SchoolContext : DbContext 7 { 8 public SchoolContext(DbContextOptions<SchoolContext> options) : base(options) 9 { 10 } 11 12 public DbSet<Course> Courses { get; set; } 13 public DbSet<Enrollment> Enrollments { get; set; } 14 public DbSet<Student> Students { get; set; } 15 } 16 }
此代码将为每个实体集创建DbSet
属性。 在 Entity Framework 中,实体集通常与数据表相对应,具体实体与表中的行相对应。
在这里可以省略DbSet<Enrollment>
和DbSet<Course>
语句,实现的功能没有任何改变。 Entity Framework 会隐式包含这两个实体因为Student
实体引用了Enrollment
实体、Enrollment
实体引用了Course
实体。
当数据库创建完成后, EF 创建一系列数据表,表名默认和DbSet
属性名相同。 集合属性的名称一般使用复数形式,但不同的开发人员的命名习惯可能不一样,开发人员根据自己的情况确定是否使用复数形式。在最后一个 DbSet 属性之后添加以下代码,对 DbContext 指定单数的表明来覆盖默认的表名。
1 protected override void OnModelCreating(ModelBuilder modelBuilder) 2 { 3 modelBuilder.Entity<Course>().ToTable("Course"); 4 modelBuilder.Entity<Enrollment>().ToTable("Enrollment"); 5 modelBuilder.Entity<Student>().ToTable("Student"); 6 }
用依赖注入注册上下文
ASP.NET Core 默认实现依赖注入。在应用程序启动过程通过依赖注入注册相关服务 (例如 EF 数据库上下文)。 需要这些服务的组件 (如 MVC 控制器) 可以通过向构造函数添加相关参数来获得对应服务。
若要将SchoolContext
注册为一种服务,打开Startup.cs,并将以下代码添加到ConfigureServices
方法中。
1 public void ConfigureServices(IServiceCollection services) 2 { 3 services.AddDbContext<SchoolContext>(options => 4 options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); 5 6 services.AddMvc(); 7 }
通过调用DbContextOptionsBuilder
中的一个方法将数据库连接字符串在配置文件中的名称传递给上下文对象。 进行本地开发时, ASP.NET Core 配置系统在appsettings.json文件中读取数据库连接字符串。
添加using
语句引用ContosoUniversity.Data
和Microsoft.EntityFrameworkCore
命名空间,然后生成项目。
打开appsettings.json文件并添加连接字符串,如下所示。
1 { 2 "ConnectionStrings": { 3 "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=ContosoUniversity1;Trusted_Connection=True;MultipleActiveResultSets=true" 4 }, 5 "Logging": { 6 "IncludeScopes": false, 7 "LogLevel": { 8 "Default": "Warning" 9 } 10 } 11 }
数据库连接字符串指定使用 SQL Server LocalDB 数据库。 LocalDB 是 SQL Server Express 数据库引擎的轻量级版本,用于应用程序开发,不在生产环境中使用。 LocalDB 作为按需启动并在用户模式下运行的轻量级数据库没有复杂的配置。 默认情况下, LocalDB 在C:/Users/<user>
目录下创建.mdf数据库文件。
用测试数据初始化数据库
Entity Framework 已经为你创建了一个空数据库。在本部分中编写一个方法用于向数据库填充测试数据,该方法会在数据库创建完成之后执行。
此处将使用EnsureCreated
方法来自动创建数据库。 在Data文件夹中,创建名为的新类文件DbInitializer.cs并且将模板代码替换为以下代码,使得在需要时能创建数据库并向其填充测试数据。
1 using ContosoUniversity.Models; 2 using System; 3 using System.Linq; 4 5 namespace ContosoUniversity.Data 6 { 7 public static class DbInitializer 8 { 9 public static void Initialize(SchoolContext context) 10 { 11 context.Database.EnsureCreated(); 12 13 // Look for any students. 14 if (context.Students.Any()) 15 { 16 return; // DB has been seeded 17 } 18 19 var students = new Student[] 20 { 21 new Student{FirstMidName="Carson",LastName="Alexander",EnrollmentDate=DateTime.Parse("2005-09-01")}, 22 new Student{FirstMidName="Meredith",LastName="Alonso",EnrollmentDate=DateTime.Parse("2002-09-01")}, 23 new Student{FirstMidName="Arturo",LastName="Anand",EnrollmentDate=DateTime.Parse("2003-09-01")}, 24 new Student{FirstMidName="Gytis",LastName="Barzdukas",EnrollmentDate=DateTime.Parse("2002-09-01")}, 25 new Student{FirstMidName="Yan",LastName="Li",EnrollmentDate=DateTime.Parse("2002-09-01")}, 26 new Student{FirstMidName="Peggy",LastName="Justice",EnrollmentDate=DateTime.Parse("2001-09-01")}, 27 new Student{FirstMidName="Laura",LastName="Norman",EnrollmentDate=DateTime.Parse("2003-09-01")}, 28 new Student{FirstMidName="Nino",LastName="Olivetto",EnrollmentDate=DateTime.Parse("2005-09-01")} 29 }; 30 foreach (Student s in students) 31 { 32 context.Students.Add(s); 33 } 34 context.SaveChanges(); 35 36 var courses = new Course[] 37 { 38 new Course{CourseID=1050,Title="Chemistry",Credits=3}, 39 new Course{CourseID=4022,Title="Microeconomics",Credits=3}, 40 new Course{CourseID=4041,Title="Macroeconomics",Credits=3}, 41 new Course{CourseID=1045,Title="Calculus",Credits=4}, 42 new Course{CourseID=3141,Title="Trigonometry",Credits=4}, 43 new Course{CourseID=2021,Title="Composition",Credits=3}, 44 new Course{CourseID=2042,Title="Literature",Credits=4} 45 }; 46 foreach (Course c in courses) 47 { 48 context.Courses.Add(c); 49 } 50 context.SaveChanges(); 51 52 var enrollments = new Enrollment[] 53 { 54 new Enrollment{StudentID=1,CourseID=1050,Grade=Grade.A}, 55 new Enrollment{StudentID=1,CourseID=4022,Grade=Grade.C}, 56 new Enrollment{StudentID=1,CourseID=4041,Grade=Grade.B}, 57 new Enrollment{StudentID=2,CourseID=1045,Grade=Grade.B}, 58 new Enrollment{StudentID=2,CourseID=3141,Grade=Grade.F}, 59 new Enrollment{StudentID=2,CourseID=2021,Grade=Grade.F}, 60 new Enrollment{StudentID=3,CourseID=1050}, 61 new Enrollment{StudentID=4,CourseID=1050}, 62 new Enrollment{StudentID=4,CourseID=4022,Grade=Grade.F}, 63 new Enrollment{StudentID=5,CourseID=4041,Grade=Grade.C}, 64 new Enrollment{StudentID=6,CourseID=1045}, 65 new Enrollment{StudentID=7,CourseID=3141,Grade=Grade.A}, 66 }; 67 foreach (Enrollment e in enrollments) 68 { 69 context.Enrollments.Add(e); 70 } 71 context.SaveChanges(); 72 } 73 } 74 }
这段代码首先检查是否有学生数据在数据库中,如果没有的话,就可以假定数据库是新建的,然后使用测试数据进行填充。代码中使用数组存放测试数据而不是使用List<T>
集合是为了优化性能。
在Program.cs,修改Main
方法,使得在应用程序启动时能执行以下操作:
- 从依赖注入容器中获取数据库上下文实例。
- 调用 seed 方法,将上下文传递给它。
- Seed 方法完成此操作时释放上下文。
1 using ContosoUniversity.Data; 2 using Microsoft.AspNetCore; 3 using Microsoft.AspNetCore.Hosting; 4 using Microsoft.Extensions.DependencyInjection; 5 using Microsoft.Extensions.Logging; 6 using System; 7 8 namespace ContosoUniversity 9 { 10 public class Program 11 { 12 public static void Main(string[] args) 13 { 14 var host = CreateWebHostBuilder(args).Build(); 15 using (var scope = host.Services.CreateScope()) 16 { 17 var services = scope.ServiceProvider; 18 try 19 { 20 var context = services.GetRequiredService<SchoolContext>(); 21 DbInitializer.Initialize(context); 22 } 23 catch (Exception ex) 24 { 25 var logger = services.GetRequiredService<ILogger<Program>>(); 26 logger.LogError(ex, "An error occurred while seeding the database."); 27 } 28 } 29 host.Run(); 30 } 31 32 public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 33 WebHost.CreateDefaultBuilder(args) 34 .UseStartup<Startup>(); 35 } 36 }
现在首次运行该应用程序,创建数据库并使用测试数据作为种子数据。 每当你更改你的数据模型,你可以删除数据库、 更新你的 Initialize 方法,然后使用上述方式更新新数据库。 在之后的教程中,你将了解如何在数据模型更改时,只需修改数据库而无需删除重建数据库。
创建控制器和视图
用 Visual Studio 中的基架引擎添加一个 MVC 控制器和使用 EF 来查询和保存数据的视图。
CRUD 操作方法和视图的自动创建被称为基架。 基架与代码生成不同,基架的代码是一个起点,您可以修改基架以满足自己需求,而你通常无需修改生成的代码。 当你需要自定义生成代码时,你使用一部分类或需求发生变化时重新生成代码。
- 右键单击解决方案资源管理器中的Controllers文件夹选择添加 > 新搭建的基架项目。
-
在添加基架的对话框中:
-
选择视图使用 Entity Framework 的 MVC 控制器
-
单击添加
-
-
在添加控制器对话框中:
-
在模型类选择Student
-
在数据上下文类选择SchoolContext
-
使用StudentsController作为默认名字
-
单击添加
-
-
当你单击添加后,Visual Studio 基架引擎创建StudentsController.cs文件和一组对应于控制器的视图 (.cshtml文件) 。
(如果你之前手动创建数据库上下文,基架引擎还可以自动创建。 你可以在添加控制器对话框中单击右侧的加号框数据上下文类来指定在一个新上下文类。然后,Visual Studio 将创建你的DbContext
,控制器和视图类。)
你会注意到控制器采用SchoolContext
作为构造函数参数。
public class StudentsController : Controller { private readonly SchoolContext _context; public StudentsController(SchoolContext context) { _context = context; }
ASP.NET 依赖注入机制将会处理传递一个SchoolContext
实例到控制器。
控制器包含Index
操作方法用于显示数据库中的所有学生。 该方法从学生实体集中获取学生列表,学生实体集则是通过读取数据库上下文实例中的Students
属性获得:
public async Task<IActionResult> Index() { return View(await _context.Students.ToListAsync()); }
Views/Students/Index.cshtml视图使用table标签显示此列表:
1 @model IEnumerable<ContosoUniversity.Models.Student> 2 3 @{ 4 ViewData["Title"] = "Index"; 5 } 6 7 <h2>Index</h2> 8 9 <p> 10 <a asp-action="Create">Create New</a> 11 </p> 12 <table class="table"> 13 <thead> 14 <tr> 15 <th> 16 @Html.DisplayNameFor(model => model.LastName) 17 </th> 18 <th> 19 @Html.DisplayNameFor(model => model.FirstMidName) 20 </th> 21 <th> 22 @Html.DisplayNameFor(model => model.EnrollmentDate) 23 </th> 24 <th></th> 25 </tr> 26 </thead> 27 <tbody> 28 @foreach (var item in Model) { 29 <tr> 30 <td> 31 @Html.DisplayFor(modelItem => item.LastName) 32 </td> 33 <td> 34 @Html.DisplayFor(modelItem => item.FirstMidName) 35 </td> 36 <td> 37 @Html.DisplayFor(modelItem => item.EnrollmentDate) 38 </td> 39 <td> 40 <a asp-action="Edit" asp-route-id="@item.ID">Edit</a> | 41 <a asp-action="Details" asp-route-id="@item.ID">Details</a> | 42 <a asp-action="Delete" asp-route-id="@item.ID">Delete</a> 43 </td> 44 </tr> 45 } 46 </tbody> 47 </table>
按 CTRL + F5 来运行该项目或从菜单选择调试 > 开始执行(不调试)。
单击学生选项卡以查看DbInitializer.Initialize
插入的测试的数据。 你将看到Student
选项卡链接在页的顶部或在单击右上角后的导航图标中,具体显示在哪里取决于浏览器窗口宽度。
约定
由于 Entity Framwork 有一定的约束条件,你只需要按规则编写很少的代码就能够创建一个完整的数据库,
-
DbSet
类型的属性用作表名。 实体未被DbSet
属性引用,实体类名称用作表名称。 -
实体属性名称用于列名称。
-
ID 或 classnameID 命名的实体属性被识别为主键属性。
-
如果属性名为 <导航属性名> <主键名>将被解释为外键属性 (例如,
StudentID
对应Student
导航属性,Student
实体的主键是ID
,所以StudentID
被解释为外键属性). 此外也可以将外键属性命名为 <主键属性名> (例如,EnrollmentID
,由于Enrollment
实体的主键是EnrollmentID
,因此被解释为外键)。
约定行为可以被重写。 例如,设置列名称和将任何属性设置为主键或外键。
异步代码
异步编程是 ASP.NET Core 和 EF Core 的默认模式。
Web 服务器的可用线程是有限的,而在高负载情况下的可能所有线程都被占用。 当发生这种情况的时候,服务器就无法处理新请求,直到线程被释放。 使用同步代码时,可能会出现多个线程被占用但不能执行任何操作的情况,因为它们正在等待 I/O 完成。 使用异步代码时,当进程正在等待 I/O 完成,服务器可以将其线程释放用于处理其他请求。 因此,异步代码使得服务器更有效地使用资源,并且该服务器可以无延迟地处理更多流量。
异步代码在运行时,会引入的少量开销,在低流量时对性能的影响可以忽略不计,但在针对高流量情况下潜在的性能提升是可观的。
在下面的代码中,async
关键字,Task<T>
返回值,await
关键字,和ToListAsync
方法使代码异步执行。
public async Task<IActionResult> Index() { return View(await _context.Students.ToListAsync()); }
-
async
关键字用于告知编译器该方法主体将生成回调并自动创建Task<IActionResult>
返回对象。 -
返回类型
Task<IActionResult>
表示正在进行的工作返回的结果为IActionResult
类型。 -
await
关键字会使得编译器将方法拆分为两个部分。 第一部分是以异步方式结束已启动的操作。 第二部分是当操作完成时注入调用回调方法的地方。 -
ToListAsync
是由ToList
方法的的异步扩展版本。
你使用 Entity Framework 编写异步代码时的一些注意事项:
-
只有导致查询或发送数据库命令的语句才能以异步方式执行。 包括
ToListAsync
,SingleOrDefaultAsync
,和SaveChangesAsync
。 不包括,操作IQueryable
的语句,如var students = context.Students.Where(s => s.LastName == "Lilo")
。 -
EF 上下文是线程不安全的: 请勿尝试并行执行多个操作。 当调用异步 EF 方法时,始终使用
await
关键字。 -
如果你想要利用异步代码的性能优势,请确保你所使用的任何库和包在它们调用导致 Entity Framework 数据库查询方法时也使用异步。
总结
现已创建了一个使用 Entity Framework Core 和 SQL Server Express LocalDB 来存储和显示数据的简单应用程序。 在下一篇中,将介绍如何执行基本的 CRUD (创建、 读取、 更新、 删除) 操作。