第七章 SportsStore: 模型的扩展
在本章中,我将扩展SportsStore应用程序的数据模型,不再只是一个Product类。我将定义一个单独的Category类替换Product类中用一个字符串属性来做分类,并解释如何在创建数据之后访问数据。我还添加了Order订单,这是任何在线商店的重要组成部分。
7.1 准备工作
在本章中,我将继续使用SportsStore项目。我把创建和编辑产品对象的过程合并到一个视图中。如清单7-1所示。我在Home控制器中合并了添加或更新产品对象的操作方法,并删除了执行批量更新的操作(Action方法)。
小贴士:您可以从本书的GitHub知识库中下载本章的SportsStore项目和其他章节的项目:
https://github.com/apress/pro-ef-core-2-for-asp.net-core-mvc.
1 //Listing 7-1. Consolidating Actions in the HomeController.cs File in the Controllers Folder 2 using Microsoft.AspNetCore.Mvc; 3 using SportsStore.Models; 4 namespace SportsStore.Controllers 5 { 6 public class HomeController : Controller 7 { 8 private IRepository repository; 9 public HomeController(IRepository repo) => repository = repo; 10 public IActionResult Index() 11 { 12 return View(repository.Products); 13 } 14 public IActionResult UpdateProduct(long key) 15 {//请求UpdateProduct视图 16 return View(key == 0 ? new Product() : repository.GetProduct(key)); 17 } 18 19 [HttpPost] 20 public IActionResult UpdateProduct(Product product) 21 {//HTTP POST方法,请求修改或者创建Product对象 22 if (product.Id == 0) 23 { 24 repository.AddProduct(product); 25 } 26 else 27 { 28 repository.UpdateProduct(product); 29 } 30 return RedirectToAction(nameof(Index)); 31 } 32 33 [HttpPost] 34 public IActionResult Delete(Product product) 35 { 36 repository.Delete(product); 37 return RedirectToAction(nameof(Index)); 38 } 39 } 40 }
合并操作依赖于long属性的值,以确定用户是希望修改现有对象还是创建新对象。在清单7-2中,我更新了Index视图以反映控制器中的更改。
1 Listing 7-2. Reflecting Controller Changes in the Index.cshtml File in the Views/Home Folder 2 @model IEnumerable<Product> 3 <h3 class="p-2 bg-primary text-white text-center">Products</h3> 4 <div class="container-fluid mt-3"> 5 <div class="row"> 6 <div class="col-1 font-weight-bold">Id</div> 7 <div class="col font-weight-bold">Name</div> 8 <div class="col font-weight-bold">Category</div> 9 <div class="col font-weight-bold text-right">Purchase Price</div> 10 <div class="col font-weight-bold text-right">Retail Price</div> 11 <div class="col"></div> 12 </div> 13 @foreach (Product p in Model) 14 { 15 <div class="row p-2"> 16 <div class="col-1">@p.Id</div> 17 <div class="col">@p.Name</div> 18 <div class="col">@p.Category</div> 19 <div class="col text-right">@p.PurchasePrice</div> 20 <div class="col text-right">@p.RetailPrice</div> 21 <div class="col"> 22 <form asp-action="Delete" method="post"> 23 <a asp-action="UpdateProduct" asp-route-key="@p.Id" 24 class="btn btn-outline-primary"> 25 Edit 26 </a> 27 <input type="hidden" name="Id" value="@p.Id" /> 28 <button type="submit" class="btn btn-outline-danger"> 29 Delete 30 </button> 31 </form> 32 </div> 33 </div> 34 } 35 <div class="text-center p-2"> 36 <a asp-action="UpdateProduct" asp-route-key="0" 37 class="btn btn-primary">Add</a> 38 </div> 39 </div>
在SportsStore项目文件夹中运行清单7-3所示的命令来删除和重新创建数据库,这将有助于确保您从示例中获得预期的结果。
Listing 7-3. Deleting and Re-creating the Database
dotnet ef database drop --force
dotnet ef database update
使用dotnet run启动应用程序并导航到http://localhost:5000;您将看到如图7-1所示的内容。不要向数据库添加任何数据,因为下一节将使用新的迁移更新数据库,如果现在添加数据,后面将产生异常。

7.2 创建数据模型关联
目前,创建的每个Product对象都有一个Category属性,该值是一个字符串。在实际的项目中,输入手误会给产品指定意外的Category。为了避免这类问题,可以使用关联来规范应用程序的数据,这可以减少重复并确保一致性,我将在下面的小节中演示这一点。
7.2.1 添加数据模型Categories 类
下面创建一个新的数据模型类。我添加了一个Category.cs文件。并使用它来定义如清单7-4所示的类。
1 //Listing 7-4. The Contents of the Category.cs File in the Models Folder 2 namespace SportsStore.Models { 3 public class Category { 4 public long Id { get; set; } 5 public string Name { get; set; } 6 public string Description { get; set; } 7 } 8 }
Category类表示产品的一个分类。Id属性是主键属性,当在数据库中创建和存储新分类时,用户要提供名称和描述属性。
7.2.2 创建关联
下一步是在两个数据模型类(Category与Product)之间创建关系,这是通过向其中一个类添加属性来完成的。在任何数据关系中,其中一个类称为有依赖的实体,属性就是添加到这个类中的。要确定哪一个类是有依赖的实体,请自问哪些类不可能存在(这个就是有依赖的类),如果没有某类;哪些类可以独立存在。在SportsStore应用程序中,一个Category 能够独立存在(有没有Product无所谓),但是每个Product对象都得有一个Category分类——这意味着,Product类是有依赖的实体类。在清单7-5中,我向Product类添加了两个属性,它们创建了与Category类的关系。
译者述:有依赖的实体类,对应于那些有外键列的表。
小贴士:如果此时搞不懂如何分辨出有依赖的实体,不要担心。我将在第14章中更详细地讨论这个主题,随着您对EF Core有了更多的经验,您将对这个概念更加熟悉。
1 //Listing 7-5. Adding Relationship Properties in the Product.cs File in the Models Folder 2 namespace SportsStore.Models 3 { 4 public class Product 5 { 6 public long Id { get; set; } 7 public string Name { get; set; } 8 //public string Category { get; set; } 9 public decimal PurchasePrice { get; set; } 10 public decimal RetailPrice { get; set; } 11 public long CategoryId { get; set; } 12 public Category Category { get; set; } 13 } 14 }
我添加的第一个属性称为CategoryId,它是Product类的外键属性,存储的是Category表的主键值。EF Core通过外键属性跟踪Product对象与Category对象的关联。外键属性的名称由类名和主键属性名组成,这就是我如何得到CategoryId的。
第二个属性替换现有的Category属性,它是导航属性。该属性指向一个Category对象,其主键Id与CategoryId的值保持一致,这使得处理数据库中的数据更加自然。
7.2.3 修改上下文,创建仓储
为了访问Category对象,我在数据库上下文类中添加了一个DbSet<T>属性,如清单7-6所示。
1 //Listing 7-6. Adding a Property in the DataContext.cs File in the Models Folder 2 using Microsoft.EntityFrameworkCore; 3 namespace SportsStore.Models 4 { 5 public class DataContext : DbContext 6 { 7 public DataContext(DbContextOptions<DataContext> opts) : base(opts) {} 8 public DbSet<Product> Products { get; set; } 9 public DbSet<Category> Categories { get; set; } 10 } 11 }
新属性遵循与现有属性相同的模式:它是一个带有get和set访问器的public属性,它返回DbSet<T>,其中T是我想存储在数据库中的类。
在扩展数据模型时,可以向现有仓储添加成员或创建新仓储,为应用程序的其余部分提供对新数据类型的访问。对于SportsStore应用程序,我将创建一个单独的仓储来演示它是如何实现的。我添加了一个类文件CategoryRepository.cs。并在其中定义接口和实现类,如清单7-7所示。
1 //Listing 7-7. The Contents of the CategoryRepository.cs File in the Models Folder 2 using System.Collections.Generic; 3 namespace SportsStore.Models 4 { 5 public interface ICategoryRepository 6 { 7 IEnumerable<Category> Categories { get; } 8 9 void AddCategory(Category category); 10 void UpdateCategory(Category category); 11 void DeleteCategory(Category category); 12 } 13 public class CategoryRepository : ICategoryRepository 14 { 15 private DataContext context; 16 public CategoryRepository(DataContext ctx) => context = ctx; 17 public IEnumerable<Category> Categories => context.Categories; 18 public void AddCategory(Category category) 19 { 20 context.Categories.Add(category); 21 context.SaveChanges(); 22 } 23 public void UpdateCategory(Category category) 24 { 25 context.Categories.Update(category); 26 context.SaveChanges(); 27 } 28 public void DeleteCategory(Category category) 29 { 30 context.Categories.Remove(category); 31 context.SaveChanges(); 32 } 33 } 34 }
我在这个文件中定义了仓储接口和实现类,并使用最简单的方法执行更新,而不依赖于更改侦测功能。在清单7-8中,我在Startup类中注册绑定仓储接口及其实现,以便与依赖项注入特性一起使用。
1 //Listing 7-8. Registering a Repository in the Startup.cs File in the SportsStore Folder 2 using System; 3 using System.Collections.Generic; 4 using System.Linq; 5 using System.Threading.Tasks; 6 using Microsoft.AspNetCore.Builder; 7 using Microsoft.AspNetCore.Hosting; 8 using Microsoft.AspNetCore.Http; 9 using Microsoft.Extensions.DependencyInjection; 10 using SportsStore.Models; 11 using Microsoft.EntityFrameworkCore; 12 using Microsoft.Extensions.Configuration; 13 namespace SportsStore 14 { 15 public class Startup 16 { 17 public Startup(IConfiguration config) => Configuration = config; 18 public IConfiguration Configuration { get; } 19 public void ConfigureServices(IServiceCollection services) 20 { 21 services.AddMvc(); 22 services.AddTransient<IRepository, DataRepository>(); 23 services.AddTransient<ICategoryRepository, CategoryRepository>(); 24 string conString = Configuration["ConnectionStrings:DefaultConnection"]; 25 services.AddDbContext<DataContext>(options => 26 options.UseSqlServer(conString)); 27 } 28 public void Configure(IApplicationBuilder app, IHostingEnvironment env) 29 { 30 app.UseDeveloperExceptionPage(); 31 app.UseStatusCodePages(); 32 app.UseStaticFiles(); 33 app.UseMvcWithDefaultRoute(); 34 } 35 } 36 }
7.2.4 创建和应用迁移
只有更新数据库以匹配数据模型中的更改之后,实体框架核心才能存储Category 对象。要更新数据库,必须创建新的迁移并将其应用到数据库,这是通过在SportsStore项目文件夹中运行清单7-9所示的命令来完成的。
小贴士:如果在运行dotnet ef database update 命令时出现异常,那么可能的原因是在运行清单7-3中的命令后向数据库添加了产品数据。再次运行清单7-3中的命令,数据库将被重置并更新到清单7-9中创建的迁移。
1 Listing 7-9. Creating and Applying a Database Migration 2 dotnet ef migrations add Categories 3 dotnet ef database update
第一个命令创建一个名为Categories的新迁移,它将包含准备数据库存储新对象所需的命令。第二个命令执行这些命令来更新数据库。
7.2.5 创建控制器和视图
我已经在Product和Category类之间创建了一个关联(外键属性&导航属性),这意味着每个产品都必须与一个Category对象相关联。通过这种关系,可以为用户提供管理数据库中Category对象的途径。我添加了一个类文件CategoriesController.cs到Controllers文件夹,并使用它创建如清单7-10所示的控制器。
小贴士:关联的另一种方案是可选关联,其中Product对象可以与Category对象相关联,也可以不关联。我将在第2部分详细解释如何创建这两种关系。
1 //Listing 7-10. The Contents of the CategoriesController.cs File in the Controllers Folder 2 using Microsoft.AspNetCore.Mvc; 3 using SportsStore.Models; 4 namespace SportsStore.Controllers 5 { 6 public class CategoriesController : Controller 7 { 8 private ICategoryRepository repository; 9 public CategoriesController(ICategoryRepository repo) => repository = repo; 10 public IActionResult Index() => View(repository.Categories); 11 [HttpPost] 12 public IActionResult AddCategory(Category category) 13 { 14 repository.AddCategory(category); 15 return RedirectToAction(nameof(Index)); 16 } 17 public IActionResult EditCategory(long id) 18 { 19 ViewBag.EditId = id; 20 return View("Index", repository.Categories); 21 } 22 [HttpPost] 23 public IActionResult UpdateCategory(Category category) 24 { 25 repository.UpdateCategory(category); 26 return RedirectToAction(nameof(Index)); 27 } 28 [HttpPost] 29 public IActionResult DeleteCategory(Category category) 30 { 31 repository.DeleteCategory(category); 32 return RedirectToAction(nameof(Index)); 33 } 34 } 35 }
Categories控制器的构造函数接收一个Category仓储,该仓储用来访问Category数据。并定义了从数据库查询、创建、更新和删除Category对象的操作(Action方法)。为了向控制器提供视图,我在Views/Categories文件夹下添加了一个Index.cshtml 文件,内容如如清单7-11所示。
1 Listing 7-11. The Contents of the Index.cshtml File in the Views/Categories Folder 2 @model IEnumerable<Category> 3 <h3 class="p-2 bg-primary text-white text-center">Categories</h3> 4 <div class="container-fluid mt-3"> 5 <div class="row"> 6 <div class="col-1 font-weight-bold">Id</div> 7 <div class="col font-weight-bold">Name</div> 8 <div class="col font-weight-bold">Description</div> 9 <div class="col-3"></div> 10 </div> 11 @if (ViewBag.EditId == null) 12 { 13 <form asp-action="AddCategory" method="post"> 14 @Html.Partial("CategoryEditor", new Category()) 15 </form> 16 } 17 @foreach (Category c in Model) 18 { 19 @if (c.Id == ViewBag.EditId) 20 { 21 <form asp-action="UpdateCategory" method="post"> 22 <input type="hidden" name="Id" value="@c.Id" /> 23 @Html.Partial("CategoryEditor", c) 24 </form> 25 } 26 else 27 { 28 <div class="row p-2"> 29 <div class="col-1">@c.Id</div> 30 <div class="col">@c.Name</div> 31 <div class="col">@c.Description</div> 32 <div class="col-3"> 33 <form asp-action="DeleteCategory" method="post"> 34 <input type="hidden" name="Id" value="@c.Id" /> 35 <a asp-action="EditCategory" asp-route-id="@c.Id" 36 class="btn btn-outline-primary">Edit</a> 37 <button type="submit" class="btn btn-outline-danger"> 38 Delete 39 </button> 40 </form> 41 </div> 42 </div> 43 } 44 } 45 </div>
这个视图提供了管理Category的一体式界面。并将创建和编辑对象丢给一个分部视图。为了创建分部视图,我添加了一个CategoryEditor.cshtml文件到Views/Category文件夹,并添加了如清单7-12所示的内容。
1 Listing 7-12. The Contents of the CategoryEditor.cshtml File in the Views/Categories Folder 2 @model Category 3 <div class="row p-2"> 4 <div class="col-1"></div> 5 <div class="col"> 6 <input asp-for="Name" class="form-control" /> 7 </div> 8 <div class="col"> 9 <input asp-for="Description" class="form-control" /> 10 </div> 11 <div class="col-3"> 12 @if (Model.Id == 0) 13 { 14 <button type="submit" class="btn btn-primary">Add</button> 15 } 16 else 17 { 18 <button type="submit" class="btn btn-outline-primary">Save</button> 19 <a asp-action="Index" class="btn btn-outline-secondary">Cancel</a> 20 } 21 </div> 22 </div>
为了便于在应用程序中切换,我将清单7-13中所示的元素添加到共享布局中。
1 Listing 7-13. Adding Elements in the _Layout.cshtml File in the Views/Shared Folder 2 <!DOCTYPE html> 3 <html> 4 <head> 5 <meta name="viewport" content="width=device-width" /> 6 <title>SportsStore</title> 7 <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" /> 8 <style> 9 .placeholder { 10 visibility: collapse; 11 display: none 12 } 13 14 .placeholder:only-child { 15 visibility: visible; 16 display: flex 17 } 18 </style> 19 </head> 20 <body> 21 <div class="container-fluid"> 22 <div class="row p-2"> 23 <div class="col-2"> 24 <a asp-controller="Home" asp-action="Index" 25 class="@GetClassForButton("Home")"> 26 Products 27 </a> 28 <a asp-controller="Categories" asp-action="Index" 29 class="@GetClassForButton("Categories")"> 30 Categories 31 </a> 32 </div> 33 <div class="col"> 34 @RenderBody() 35 </div> 36 </div> 37 </div> 38 </body> 39 </html> 40 @functions 41 { 42 string GetClassForButton(string controller) 43 { 44 return "btn btn-block " + (ViewContext.RouteData.Values["controller"] 45 as string == controller ? "btn-primary" : "btn-outline-primary"); 46 } 47 }
我添加了一些按钮,这些按钮通过一个简单的内联函数选择产品和类别控制器,该函数使用Bootstrap CSS
样式突出显示当前显示的控制器的按钮。
注意:我不经常使用内置razor函数,因为我更喜欢将所有的c#代码放在类文件中。但是在这种情况下,该函数的优点是保持示例的简洁,并且它只与视图中的内容相关,并且比创建视图组件更容易。
7.2.6 将Categories 数据输入到数据库
在完成数据关联时,使用一些数据是很有帮助的。使用dotnet run启动应用程序,单击Categories按钮,并使用表7-1中的值在HTML表单中创建Category。

当您添加了所有这三个Category之后,您应该会看到如图7-2所示的内容。

7.3 使用数据关联
必须更新应用程序中处理产品对象的部分,以反映数据库中的新关系(Product与Category)。这个过程包括两部分:在查询数据库时包含类别数据,在创建或编辑产品时允许用户选择类别。
7.3.1 处理相关数据
EF Core会忽略关联,除非您显式地在查询中include它们。这意味着导航属性(如Product类定义的Category属性)默认为null。Include扩展方法告诉EF Core使用相关数据填充导航属性,并在表示SQL查询的IQueryable<T>对象上调用该Include方法。在清单7-14中,我使用Include方法在产品仓储执行的查询中include相关的类别对象。
1 //Listing 7-14. Including Related Data in the DataRepository.cs File in the Models Folder 2 using System.Collections.Generic; 3 using System.Linq; 4 using Microsoft.EntityFrameworkCore; 5 namespace SportsStore.Models 6 { 7 public class DataRepository : IRepository 8 { 9 private DataContext context; 10 public DataRepository(DataContext ctx) => context = ctx; 11 public IEnumerable<Product> Products => context.Products 12 .Include(p => p.Category).ToArray(); 13 public Product GetProduct(long key) => context.Products 14 .Include(p => p.Category).First(p => p.Id == key); 15 16 public void AddProduct(Product product) 17 { 18 context.Products.Add(product); 19 context.SaveChanges(); 20 } 21 public void UpdateProduct(Product product) 22 { 23 Product p = context.Products.Find(product.Id); 24 p.Name = product.Name; 25 //p.Category = product.Category; 26 p.PurchasePrice = product.PurchasePrice; 27 p.RetailPrice = product.RetailPrice; 28 p.CategoryId = product.CategoryId; 29 context.SaveChanges(); 30 } 31 public void UpdateAll(Product[] products) 32 { 33 Dictionary<long, Product> data = products.ToDictionary(p => p.Id); 34 IEnumerable<Product> baseline = 35 context.Products.Where(p => data.Keys.Contains(p.Id)); 36 foreach (Product databaseProduct in baseline) 37 { 38 Product requestProduct = data[databaseProduct.Id]; 39 databaseProduct.Name = requestProduct.Name; 40 databaseProduct.Category = requestProduct.Category; 41 databaseProduct.PurchasePrice = requestProduct.PurchasePrice; 42 databaseProduct.RetailPrice = requestProduct.RetailPrice; 43 } 44 context.SaveChanges(); 45 } 46 public void Delete(Product product) 47 { 48 context.Products.Remove(product); 49 context.SaveChanges(); 50 } 51 } 52 }
Include方法在Microsoft.EntityFrameworkCore命名空间中定义,它接受lambda表达式,在该lambda中选择一个导航属性(您希望EF Core把它include到查询中的那个导航属性)。我在GetProduct方法中使用的Find方法不能与Include方法一起使用,所以我将它替换为First方法,可以达到相同的效果。这些更改的结果是EF Core将为Products属性和GetProduct方法创建的产品对象,填充其Product.Category 导航属性。
请注意我对UpdateProduct方法所做的更改。首先,我直接查询baseline基线数据,而不是通过GetProduct方法,因为我不想在执行更新时加载关联数据。其次,我注释掉了Category属性的赋值语句,并添加了一个CategoryId属性的赋值语句。要让EF Core更新数据库中两个对象之间的关联,你要做的全部工作就是对外键属性赋值(或者更改赋值)。
译者述:你更改外键属性CategoryId的值(是个整数),Update到数据库之后就是修改了Product表的CategoryId外键值。下次查询该Product记录,得到的Category导航属性,自然引用新指定的Category对象。
7.3.2 为Product选择Category分类
在清单7-15中,我更新了Home控制器,使其能够通过仓储访问Category 数据,并将数据传递给视图。这将允许视图在编辑或创建产品对象时从完整的Category集合中进行选择。
1 //Listing 7-15. Using Category Data in the HomeController.cs File in the Controllers Folder 2 using Microsoft.AspNetCore.Mvc; 3 using SportsStore.Models; 4 namespace SportsStore.Controllers 5 { 6 public class HomeController : Controller 7 { 8 private IRepository repository; 9 private ICategoryRepository catRepository; 10 11 public HomeController(IRepository repo, ICategoryRepository catRepo) 12 { 13 repository = repo; 14 catRepository = catRepo; 15 } 16 public IActionResult Index() 17 { 18 return View(repository.Products); 19 } 20 21 public IActionResult UpdateProduct(long key) 22 { 23 ViewBag.Categories = catRepository.Categories; 24 return View(key == 0 ? new Product() : repository.GetProduct(key)); 25 } 26 27 [HttpPost] 28 public IActionResult UpdateProduct(Product product) 29 { 30 if (product.Id == 0) 31 { 32 repository.AddProduct(product); 33 } 34 else 35 { 36 repository.UpdateProduct(product); 37 } 38 return RedirectToAction(nameof(Index)); 39 } 40 41 [HttpPost] 42 public IActionResult Delete(Product product) 43 { 44 repository.Delete(product); 45 return RedirectToAction(nameof(Index)); 46 } 47 } 48 }
为了允许用户在创建或编辑产品时选择一个类别,我在UpdateProduct视图中添加了一个select元素,如清单7-16所示。
1 Listing 7-16. Displaying Categories in the UpdateProduct.html File in the Views/Home Folder 2 @model Product 3 <h3 class="p-2 bg-primary text-white text-center">Update Product</h3> 4 <form asp-action="UpdateProduct" method="post"> 5 <div class="form-group"> 6 <label asp-for="Id"></label> 7 <input asp-for="Id" class="form-control" readonly /> 8 </div> 9 <div class="form-group"> 10 <label asp-for="Name"></label> 11 <input asp-for="Name" class="form-control" /> 12 </div> 13 <div class="form-group"> 14 <label asp-for="Category"></label> 15 <select class="form-control" asp-for="CategoryId"> 16 @if (Model.Id == 0) 17 { 18 <option disabled selected>Choose Category</option> 19 } 20 @foreach (Category c in ViewBag.Categories) 21 { 22 <option selected=@(Model.Category?.Id == c.Id) 23 value="@c.Id"> 24 @c.Name 25 </option> 26 } 27 </select> 28 </div> 29 <div class="form-group"> 30 <label asp-for="PurchasePrice"></label> 31 <input asp-for="PurchasePrice" class="form-control" /> 32 </div> 33 <div class="form-group"> 34 <label asp-for="RetailPrice"></label> 35 <input asp-for="RetailPrice" class="form-control" /> 36 </div> 37 <div class="text-center"> 38 <button class="btn btn-primary" type="submit">Save</button> 39 <a asp-action="Index" class="btn btn-secondary">Cancel</a> 40 </div> 41 </form>
如果视图用于创建新产品对象,我引入一个占位符option元素;如果正在编辑现有对象,则使用Razor表达式应用selected 特性。
剩下的就是更新Index视图,使其遵循导航属性,为每个产品对象显示选中分类的名称,如清单7-17所示。
1 Listing 7-17. Following a Navigation Property in the Index.cshtml File in the Views/Home Folder 2 @model IEnumerable<Product> 3 <h3 class="p-2 bg-primary text-white text-center">Products</h3> 4 <div class="container-fluid mt-3"> 5 <div class="row"> 6 <div class="col-1 font-weight-bold">Id</div> 7 <div class="col font-weight-bold">Name</div> 8 <div class="col font-weight-bold">Category</div> 9 <div class="col font-weight-bold text-right">Purchase Price</div> 10 <div class="col font-weight-bold text-right">Retail Price</div> 11 <div class="col"></div> 12 </div> 13 @foreach (Product p in Model) 14 { 15 <div class="row p-2"> 16 <div class="col-1">@p.Id</div> 17 <div class="col">@p.Name</div> 18 <div class="col">@p.Category.Name</div> 19 <div class="col text-right">@p.PurchasePrice</div> 20 <div class="col text-right">@p.RetailPrice</div> 21 <div class="col"> 22 <form asp-action="Delete" method="post"> 23 <a asp-action="UpdateProduct" asp-route-key="@p.Id" 24 class="btn btn-outline-primary"> 25 Edit 26 </a> 27 <input type="hidden" name="Id" value="@p.Id" /> 28 <button type="submit" class="btn btn-outline-danger"> 29 Delete 30 </button> 31 </form> 32 </div> 33 </div> 34 } 35 <div class="text-center p-2"> 36 <a asp-action="UpdateProduct" asp-route-key="0" 37 class="btn btn-primary">Add</a> 38 </div> 39 </div>
7.3.3 创建和编辑带有类别的产品
使用dotnet run启动应用程序,导航到http://localhost:5000,单击Add按钮,在form表单输入表7-2所示的数据,创建产品对象。在创建每个对象时,使用select元素从列表中选择类别。

在创建每个对象时,Index操作(Action方法)会被执行来显示结果,这将导致实体框架核心去数据库查询Product数据及其关联的Category对象。通过检查应用程序生成的日志消息,您可以看到如何将其转换为SQL语句,如下所示:
1 ... 2 SELECT [p].[Id], [p].[CategoryId], [p].[Name], [p].[PurchasePrice], 3 [p].[RetailPrice], [p.Category].[Id], [p.Category].[Description], 4 [p.Category].[Name] 5 FROM [Products] AS [p] 6 INNER JOIN [Categories] AS [p.Category] ON [p].[CategoryId] = [p.Category].[Id] 7 ...
为了给每个Product对象创建Category对象,实体框架核心使用外键来查询Category的数据,并使用inner join语句来连接产品和类别表的数据。
创建三个产品对象之后,单击Categories,单击Watersports 类别的编辑按钮,并将名称字段的值更改为Aquatics 。单击Save按钮,再单击Products。您将看到编辑类别中的两个产品对象都显示了新名称,如图7-3所示。
小心:如果删除类别对象,则与之相关联的产品对象也全被删除,这是关联的默认配置。我将在第22章中解释它的工作原理和其他配置选项。

7.4 添加Order订单类
为了演示更复杂的关联,我将添加创建和存储订单的功能,并使用它们来表示客户选择的产品。在下面的小节中,我将添加更多的实体类扩展数据模型,更新数据库,并添加一个控制器来管理新数据。
7.4.1 创建数据模型Order类
我首先添加了一个名为Order.cs的文件。并使用它来定义清单7-18所示的类。
1 //Listing 7-18. The Contents of the Order.cs File in the Models Folder 2 using System.Collections.Generic; 3 namespace SportsStore.Models 4 { 5 public class Order 6 { 7 public long Id { get; set; } 8 public string CustomerName { get; set; } 9 public string Address { get; set; } 10 public string State { get; set; } 11 public string ZipCode { get; set; } 12 public bool Shipped { get; set; } 13 public IEnumerable<OrderLine> Lines { get; set; } 14 } 15 }
Order类具有客户名和地址以及产品是否已发货的属性。还有一个导航属性,它提供了对相关OrderLine对象的访问,这些对象将表示一个单独的产品选择。为了创建这个类,我在Models文件夹中添加了一个名为OrderLine.cs的文件。代码如清单7-19所示。
1 //Listing 7-19. The Contents of the OrderLine.cs File in the Models Folder 2 namespace SportsStore.Models 3 { 4 public class OrderLine 5 { 6 public long Id { get; set; } 7 public long ProductId { get; set; } 8 public Product Product { get; set; } 9 public int Quantity { get; set; } 10 public long OrderId { get; set; } 11 public Order Order { get; set; } 12 } 13 }
每个OrderLine对象都与订单和产品相关,并具有一个属性,该属性指示客户需要多少个该产品。为了方便访问订单数据,我将清单7-20中所示的属性添加到上下文类中。
1 //Listing 7-20. Adding Properties in the DataContext.cs File in the Models Folder 2 using Microsoft.EntityFrameworkCore; 3 namespace SportsStore.Models 4 { 5 public class DataContext : DbContext 6 { 7 public DataContext(DbContextOptions<DataContext> opts) : base(opts) { } 8 public DbSet<Product> Products { get; set; } 9 public DbSet<Category> Categories { get; set; } 10 public DbSet<Order> Orders { get; set; } 11 public DbSet<OrderLine> OrderLines { get; set; } 12 } 13 }
7.4.2 创建仓储,准备数据库
为了向应用程序的其余部分提供对新数据的一致访问,我添加了一个名为IOrdersRepository.cs的文件,并定义如清单7-21所示的接口。
1 //Listing 7-21. The Contents of the IOrdersRepository.cs File in the Models Folder 2 using System.Collections.Generic; 3 namespace SportsStore.Models 4 { 5 public interface IOrdersRepository 6 { 7 IEnumerable<Order> Orders { get; } 8 Order GetOrder(long key); 9 void AddOrder(Order order); 10 void UpdateOrder(Order order); 11 void DeleteOrder(Order order); 12 } 13 }
接下来,我添加了一个名为OrdersRepository.cs的文件。并使用它创建实现类,如清单7-22所示。
1 Listing 7-22. The Contents of the OrdersRepository.cs File in the Models Folder 2 using Microsoft.EntityFrameworkCore; 3 using System.Collections.Generic; 4 using System.Linq; 5 namespace SportsStore.Models 6 { 7 public class OrdersRepository : IOrdersRepository 8 { 9 private DataContext context; 10 public OrdersRepository(DataContext ctx) => context = ctx; 11 public IEnumerable<Order> Orders => context.Orders 12 .Include(o => o.Lines).ThenInclude(l => l.Product); 13 public Order GetOrder(long key) => context.Orders 14 .Include(o => o.Lines).First(o => o.Id == key); 15 public void AddOrder(Order order) 16 { 17 context.Orders.Add(order); 18 context.SaveChanges(); 19 } 20 public void UpdateOrder(Order order) 21 { 22 context.Orders.Update(order); 23 context.SaveChanges(); 24 } 25 public void DeleteOrder(Order order) 26 { 27 context.Orders.Remove(order); 28 context.SaveChanges(); 29 } 30 } 31 }
存储库实现遵循其他存储库建立的模式,为了简单起见,放弃使用更改侦测功能。请注意Include和ThenInclude方法的使用,它们用于数据模型间导航,并向查询添加相关数据——我将在第14-16章中详细描述这一过程。
在清单7-23中,我向Startup类添加了一条语句,这样依赖项注入系统就可以使用瞬态的OrderRepository对象,注入到IOrderRepository接口的依赖项。
1 //Listing 7-23. Configuring Dependency Injection in the Startup.cs File in the SportsStore Folder 2 using System; 3 using System.Collections.Generic; 4 using System.Linq; 5 using System.Threading.Tasks; 6 using Microsoft.AspNetCore.Builder; 7 using Microsoft.AspNetCore.Hosting; 8 using Microsoft.AspNetCore.Http; 9 using Microsoft.Extensions.DependencyInjection; 10 using SportsStore.Models; 11 using Microsoft.EntityFrameworkCore; 12 using Microsoft.Extensions.Configuration; 13 namespace SportsStore 14 { 15 public class Startup 16 { 17 public Startup(IConfiguration config) => Configuration = config; 18 public IConfiguration Configuration { get; } 19 public void ConfigureServices(IServiceCollection services) 20 { 21 services.AddMvc(); 22 services.AddTransient<IRepository, DataRepository>(); 23 services.AddTransient<ICategoryRepository, CategoryRepository>(); 24 services.AddTransient<IOrdersRepository, OrdersRepository>(); 25 string conString = Configuration["ConnectionStrings:DefaultConnection"]; 26 services.AddDbContext<DataContext>(options => 27 options.UseSqlServer(conString)); 28 } 29 public void Configure(IApplicationBuilder app, IHostingEnvironment env) 30 { 31 app.UseDeveloperExceptionPage(); 32 app.UseStatusCodePages(); 33 app.UseStaticFiles(); 34 app.UseMvcWithDefaultRoute(); 35 } 36 } 37 }
在SportsStore项目文件夹中运行清单7-24所示的命令,通过创建和应用EF Core的migration迁移,来准备数据库,存储新的数据模型类。
Listing 7-24. Creating and Applying a New Database Migration
dotnet ef migrations add Orders
dotnet ef database update
7.4.3 创建控制器和视图
所有的EF Core基本工作都已经就绪,可以处理Order对象了。下一步是添加MVC功能,允许创建和管理实例。我添加了一个OrdersController.cs类文件到Controllers文件夹,并使用它来定义如清单7-25所示的控制器。 我暂时没写AddOrUpdateOrder方法的代码,当其他特性就绪时,我将完成该方法。
1 //Listing 7-25. The Contents of the OrdersController.cs File in the Controllers Folder 2 using Microsoft.AspNetCore.Mvc; 3 using SportsStore.Models; 4 using System.Collections.Generic; 5 using System.Linq; 6 namespace SportsStore.Controllers 7 { 8 public class OrdersController : Controller 9 { 10 private IRepository productRepository; 11 private IOrdersRepository ordersRepository; 12 public OrdersController(IRepository productRepo, 13 IOrdersRepository orderRepo) 14 { 15 productRepository = productRepo; 16 ordersRepository = orderRepo; 17 } 18 public IActionResult Index() => View(ordersRepository.Orders); 19 public IActionResult EditOrder(long id) 20 { 21 var products = productRepository.Products; 22 Order order = id == 0 ? new Order() : ordersRepository.GetOrder(id); 23 IDictionary<long, OrderLine> linesMap 24 = order.Lines?.ToDictionary(l => l.ProductId) //?.操作符,不为null时执行后面的操作 25 ?? new Dictionary<long, OrderLine>(); 26 ViewBag.Lines = products.Select(p => linesMap.ContainsKey(p.Id) 27 ? linesMap[p.Id] 28 : new OrderLine { Product = p, ProductId = p.Id, Quantity = 0 }); 29 return View(order); 30 } 31 [HttpPost] 32 public IActionResult AddOrUpdateOrder(Order order) 33 { 34 // ...action method to be completed... 35 return RedirectToAction(nameof(Index)); 36 } 37 [HttpPost] 38 public IActionResult DeleteOrder(Order order) 39 { 40 ordersRepository.DeleteOrder(order); 41 return RedirectToAction(nameof(Index)); 42 } 43 } 44 }
EditOrder操作(Action)方法中的LINQ语句可能看起来比较复杂,但是它们准备了OrderLine数据,以便每个产品都对应一个OrderLine 对象,即使之前没有针对该产品的选择。
对于新订单,这意味着ViewBag.Lines 属性将被一系列OrderLine对象填充,与数据库中的每个产品对应,Id和Quantity属性设置为0。当对象存储到数据库中时,零Id值将表明这是一个新对象,数据库服务器将分配一个新的唯一主键值。
对于已存在的订单,ViewBag.Lines属性将被数据库中读取的OrderLine对象填充,为剩余产品填充Id属性为零的额外对象。
这种结构利用了高级的方式,将ASP.NET Core MVC和EF Core结合在一起,简化了更新数据库的过程,您将在完成示例的其余部分时看到这一点。
下一步是创建一个视图,该视图将列出数据库中的所有对象。我创建了Views/Orders文件夹,并向其添加了一个名为Index.cshtml 的文件。内容如清单7-26所示。
1 //Listing 7-26. The Contents of the Index.cshtml File in the Views/Orders Folder 2 @model IEnumerable<Order> 3 <h3 class="p-2 bg-primary text-white text-center">Orders</h3> 4 <div class="container-fluid mt-3"> 5 <div class="row"> 6 <div class="col-1 font-weight-bold">Id</div> 7 <div class="col font-weight-bold">Name</div> 8 <div class="col font-weight-bold">Zip</div> 9 <div class="col font-weight-bold">Total</div> 10 <div class="col font-weight-bold">Profit</div> 11 <div class="col-1 font-weight-bold">Status</div> 12 <div class="col-3"></div> 13 </div> 14 <div> 15 <div class="row placeholder p-2"> 16 <div class="col-12 text-center"> 17 <h5>No Orders</h5> 18 </div> 19 </div> 20 @foreach (Order o in Model) 21 { 22 <div class="row p-2"> 23 <div class="col-1">@o.Id</div> 24 <div class="col">@o.CustomerName</div> 25 <div class="col">@o.ZipCode</div> 26 <div class="col"> 27 @o.Lines.Sum(l => l.Quantity * l.Product.RetailPrice) 28 </div> 29 <div class="col"> 30 @o.Lines.Sum(l => l.Quantity * (l.Product.RetailPrice - l.Product.PurchasePrice)) 31 </div> 32 <div class="col-1">@(o.Shipped ? "Shipped" : "Pending")</div> 33 <div class="col-3 text-right"> 34 <form asp-action="DeleteOrder" method="post"> 35 <input type="hidden" name="Id" value="@o.Id" /> 36 <a asp-action="EditOrder" asp-route-id="@o.Id" 37 class="btn btn-outline-primary">Edit</a> 38 <button type="submit" class="btn btn-outline-danger"> 39 Delete 40 </button> 41 </form> 42 </div> 43 </div> 44 } 45 </div> 46 </div> 47 <div class="text-center"> 48 <a asp-action="EditOrder" class="btn btn-primary">Create</a> 49 </div>
这个视图显示数据库中Order对象的摘要,并显示所订购产品的总价格和将获得的利润。有创建新订单和编辑、删除现有订单的按钮。
为了提供创建或编辑订单的视图,我添加了一个名为EditOrder.cshtml文件到Views/Orders文件夹,并添加了如清单7-27所示的内容。
1 Listing 7-27. The Contents of the EditOrder.cshtml File in the Views/Orders Folder 2 @model Order 3 <h3 class="p-2 bg-primary text-white text-center">Create/Update Order</h3> 4 <form asp-action="AddOrUpdateOrder" method="post"> 5 <div class="form-group"> 6 <label asp-for="Id"></label> 7 <input asp-for="Id" class="form-control" readonly /> 8 </div> 9 <div class="form-group"> 10 <label asp-for="CustomerName"></label> 11 <input asp-for="CustomerName" class="form-control" /> 12 </div> 13 <div class="form-group"> 14 <label asp-for="Address"></label> 15 <input asp-for="Address" class="form-control" /> 16 </div> 17 <div class="form-group"> 18 <label asp-for="State"></label> 19 <input asp-for="State" class="form-control" /> 20 </div> 21 <div class="form-group"> 22 <label asp-for="ZipCode"></label> 23 <input asp-for="ZipCode" class="form-control" /> 24 </div> 25 <div class="form-check"> 26 <label class="form-check-label"> 27 <input type="checkbox" asp-for="Shipped" class="form-check-input" /> 28 Shipped 29 </label> 30 </div> 31 <h6 class="mt-1 p-2 bg-primary text-white text-center">Products Ordered</h6> 32 <div class="container-fluid"> 33 <div class="row"> 34 <div class="col font-weight-bold">Product</div> 35 <div class="col font-weight-bold">Category</div> 36 <div class="col font-weight-bold">Quantity</div> 37 </div> 38 @{ int counter = 0; } 39 @foreach (OrderLine line in ViewBag.Lines) 40 { 41 <input type="hidden" name="lines[@counter].Id" value="@line.Id" /> 42 <input type="hidden" name="lines[@counter].ProductId" 43 value="@line.ProductId" /> 44 <input type="hidden" name="lines[@counter].OrderId" value="@Model.Id" /> 45 <div class="row mt-1"> 46 <div class="col">@line.Product.Name</div> 47 <div class="col">@line.Product.Category.Name</div> 48 <div class="col"> 49 <input type="number" name="lines[@counter].Quantity" 50 value="@line.Quantity" /> 51 </div> 52 </div> 53 counter++; 54 } 55 </div> 56 <div class="text-center m-2"> 57 <button type="submit" class="btn btn-primary">Save</button> 58 <a asp-action="Index" class="btn btn-secondary">Cancel</a> 59 </div> 60 </form>
该视图为用户提供一个form表单,其中包含为Order类属性创建的input元素,以及为数据库中每个产品对象创建的input元素,在编辑现有对象时,将使用选定的数量填充这些对象。
为了使新特性更容易访问,我将清单7-28中所示的元素添加到所有视图共享的布局中。
1 Listing 7-28. Adding an Element in the _Layout.cshtml File in the Views/Shared Folder 2 <!DOCTYPE html> 3 <html> 4 <head> 5 <meta name="viewport" content="width=device-width" /> 6 <title>SportsStore</title> 7 <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" /> 8 <style> 9 .placeholder { 10 visibility: collapse; 11 display: none 12 } 13 14 .placeholder:only-child { 15 visibility: visible; 16 display: flex 17 } 18 </style> 19 </head> 20 <body> 21 <div class="container-fluid"> 22 <div class="row p-2"> 23 <div class="col-2"> 24 <a asp-controller="Home" asp-action="Index" 25 class="@GetClassForButton("Home")"> 26 Products 27 </a> 28 <a asp-controller="Categories" asp-action="Index" 29 class="@GetClassForButton("Categories")"> 30 Categories 31 </a> 32 <a asp-controller="Orders" asp-action="Index" 33 class="@GetClassForButton("Orders")"> 34 Orders 35 </a> 36 </div> 37 <div class="col"> 38 @RenderBody() 39 </div> 40 </div> 41 </div> 42 </body> 43 </html> 44 @functions 45 { 46 string GetClassForButton(string controller) 47 { 48 return "btn btn-block " + (ViewContext.RouteData.Values["controller"] 49 as string == controller ? "btn-primary" : "btn-outline-primary"); 50 } 51 }
使用dotnet run启动应用程序,导航到http://localhost:5000,单击Orders按钮,然后单击Create。您将看到一个空form表单,其中包含数据库中所有产品的元素。因为这是一个新的订单,所以所有的Quantity字段都是零,如图7-4所示。

7.4.4 存储Order数据
当您单击Save按钮时,没有存储任何数据,因为我没有把AddOrUpdateOrder方法写完整,见清单7-25。为了完成控制器,我将清单7-29所示的代码添加到action方法中。
1 //Listing 7-29. Storing Data in the OrdersController.cs File in the Controllers Folder 2 ... 3 [HttpPost] 4 public IActionResult AddOrUpdateOrder(Order order) 5 { 6 order.Lines = order.Lines 7 .Where(l => l.Id > 0 || (l.Id == 0 && l.Quantity > 0)).ToArray(); 8 if (order.Id == 0) 9 { 10 ordersRepository.AddOrder(order); 11 } 12 else 13 { 14 ordersRepository.UpdateOrder(order); 15 } 16 return RedirectToAction(nameof(Index)); 17 } 18 ...
我在action方法中使用的代码,依赖于一个有用的实体框架核心特性:当我将一个Order对象传递给AddOrder或UpdateOrder存储库方法,实体框架核心将不仅存储Order对象,而且存储它的相关OrderLine对象。这似乎并不重要,但它简化了一个过程,否则将需要一系列精心协调的更新语句。
要查看生成的SQL命令,请使用dotnet run启动应用程序,导航到http://localhost:5000/orders,单击Create按钮,并填写表单。无论您为客户使用什么细节,请确保输入表7-3中所示产品的数量。

单击Save按钮时,您将在应用程序生成的日志消息中看到几个SQL命令。第一个存储Order对象,第二个获取为主键分配的值。
1 ... 2 INSERT INTO [Orders] ([Address], [CustomerName], [Shipped], [State], [ZipCode]) 3 VALUES (@p0, @p1, @p2, @p3, @p4); 4 SELECT [Id] 5 FROM [Orders] 6 WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity(); 7 ...
然后EF Core使用Order对象的主键来存储OrderLine对象,如下所示:
1 ... 2 DECLARE @inserted0 TABLE ([Id] bigint, [_Position] [int]); 3 MERGE [OrderLines] USING ( 4 VALUES (@p5, @p6, @p7, 0), 5 (@p8, @p9, @p10, 1)) AS i ([OrderId], [ProductId], [Quantity], _Position) ON 1=0 6 WHEN NOT MATCHED THEN 7 INSERT ([OrderId], [ProductId], [Quantity]) 8 VALUES (i.[OrderId], i.[ProductId], i.[Quantity]) 9 OUTPUT INSERTED.[Id], i._Position 10 INTO @inserted0; 11 ...
最后,Entity Framework Core查询数据库,获取分配给OrderLine对象的主键,如下所示:
1 ... 2 SELECT [t].[Id] FROM [OrderLines] t 3 INNER JOIN @inserted0 i ON ([t].[Id] = [i].[Id]) 4 ORDER BY [i].[_Position]; 5 ...
如果您不能完全理解SQL命令,也不必担心。需要注意的是这个EF Core负责存储相关数据并自动生成SQL命令。关于清单7-29中的代码,需要注意的最后一点是:
... order.Lines = order.Lines .Where(l => l.Id > 0 || (l.Id == 0 && l.Quantity > 0)).ToArray(); ...
除了已经存储在数据库中的对象,此语句排除了没有为其选择数量的OrderLine对象。这可以确保订单中没有的OrderLine对象不存到数据库,但是允许对以前存储的数据进行更新。
保存数据时,将显示订单的摘要,如图7-5所示。

7.5 常见错误和解决方案
用于创建和处理相关数据的特性可能比较笨拙,在下面的部分中,我描述常见的问题并解释如何解决它们。
7.5.1 修改表与外键冲突
The “ALTER TABLE conflicted with the FOREIGN KEY” Exception
当您将迁移应用到数据库,库中如果有的已有数据不符合新的约束时,通常会出现此异常。例如,对于SportsStore应用程序,如果您在应用迁移(为了增加一个Category类)之前已经在数据库中创建了产品对象(数据),您就会看到这个异常。数据库中的现有数据不符合外键关系,因此数据库更新将失败。
解决此问题的最简单方法是删除并重新创建数据库,这将删除库中的任何数据。然而,在生产系统中,这不是一种合适的方法,在应用迁移之前,必须仔细修改数据。
7.5.2 更新与外键冲突
The “UPDATE Conflicted with the FOREIGN KEY” Exception
当您试图使用不匹配数据库外键约束的数据存储新对象或更新现有对象时,将发生此异常。最可能的原因是省略了关联数据的外键值。例如,在SportsStore应用程序上下文中,如果您试图存储或更新产品对象,而不指定CategoryId属性的值,则会出现此异常。如果收到此异常,请确保HTML表单对实体类的每个外键属性写一个input元素。(让用户选择关联数据,指定外键值)
7.5.3 属性表达式x => x.<name> 无效
The “The Property Expression ‘x => x.<name>’ is Not Valid” Exception
当您忘记向导航属性添加get和set访问器,然后使用Include方法选择它时,就会出现此异常。省略get和set访问器将创建字段而不是属性,并且Include方法不能在查询中跟随它。要解决这个问题,可以添加get和set访问器来创建属性。您可能还必须重新创建和应用定义关联的迁移。
7.5.4 导航属性未实现ICollection<OrderLine>
The “Type of Navigation Property <name> Does Not Implement
ICollection<OrderLine> ” Exception
EF Core对导航属性的数据类型很敏感,当您在存储数据之前对导航属性集合执行LINQ查询时,就会出现这种异常,就像我在文章中所做的那样,见SportsStore应用程序的清单7-29。要修复此问题,请在您的LINQ查询结果上调用ToArray方法生成一个数组。(数组和List实现了ICollection<T>接口 )
7.5.5 属性xxx不是实体类xx的导航属性
The “The Property <name> is Not a Navigation Property of Entity
Type <name>” Exception
当您使用Include方法为关联数据选择EF Core无法跟随的属性时,将发生此异常。这个问题最常见的原因是选择了外键属性,而不是与其配对的导航属性。在SportsStore应用程序中,如果使用Include方法从OrderLine对象中选择了ProductId外键属性,而非Product导航属性,就会看到这个错误。
7.5.6 无效的对象名xxx
The “Invalid Object Name <name>” Exception
当您创建了一个迁移,扩展了数据模型的外键关联,但忘记将其应用于数据库时,通常会出现此异常。使用dotnet ef database update 命令将你项目中的迁移应用到数据库,请参阅第13章,其中详细解释了迁移的工作方式。
7.5.7 对象被删除而不是更新
Objects Are Deleted Instead of Being Updated
如果您发现试图更新一个对象实际上导致它被删除,那么可能的原因是:您正在加载关联数据,然后将导航属性设置为null,当获取用于更改侦测的baseline基线数据时。
例如,在SportsStore应用程序中,想看到这种行为你可以这么干:通过查询baseline基线产品对象,使用Include方法加载关联Category对象,然后在调用SaveChanges方法之前将Category属性设置为null。
这种组合的Action方法导致EF Core悄悄地删除您原打算更新的对象。要解决此问题,请不要加载关联数据,也不要将导航属性设置为null。
7.5.8 关联数据的类名显示在视图中
The Class Name for Related Data Is Displayed in a View
这个问题是由Razor表达式导致的,该表达式选择了导航属性的值,该导航属性返回关联的对象。Razor然后调用ToString方法,该方法返回类名。若要在视图中包含来自关联数据的数据值,请选择一个关联对象的属性,比如应该使用@Category.Name,而不能仅写@Category。
7.6 本章小结
在本章中,为了SportsStore数据模型,我添加了新类、创建了(外键)关联。我解释了如何查询关联数据,如何执行更新,以及如何解决这些功能可能导致的最常见问题。在下一章中,我将向您展示如何适配应用程序的MVC和EF Core部分,用于处理大量数据。
浙公网安备 33010602011771号