Pro Entity Framework Core 2 for ASP.NET Core MVC 翻译

第 4 章 运动商店:一个真实的(数据)应用程序

作者:Adam Freeman
翻译:陈广
日期:2018-12-6


本章,我开始了创建一个更为现实的项目的进程,以演示 ASP.NET Core MVC 及 Entity Framework Core 是如何在一起工作的。项目将是比较简单但现实的,聚焦于最常用的实体框架核心功能。该应用程序将是我在许多书籍中使用的运动商店应用程序的一个变体,它将更多地关注数据和数据存储。

本章,我创建一个简单的自包含的 ASP.NET Core MVC 应用程序。在下一章,我增加 Entity Framework Core 并在数据库中存储应用程序数据。在接下来的章节中,我将添加更多的数据操作,扩展数据模型,并支持客户功能,以及向您演示如何扩展应用程序。在所有的运动商店章节中,我将在不同章节中分别描述关键的特性,并提供了更多的细节。从第5章开始,当我将 Entity Framework Core 添加到项目中时,我还描述了您可能遇到的最常见的问题,并解释了如何解决这些问题。

注意:本书的焦点在于 Entity Framework Core 以及它在 MVC 应用程序中是如何使用的。我省略了一些运动商店的功能,如管理认证功能,并扩展了其它功能,如数据模型。

创建项目

要创建运动商店项目,启动 Visual Studio 并在【文件】菜单中选择【新建】➤【项目】。选择【ASP.NET Core Web 应用程序】项目模板,将名称设置为 SportsStore,单击【浏览】按钮选择一个方便的位置存储项目,如图4-1所示。

提示:您可以在本书的 GitHub 仓库中下载此项目,https://github.com/apress/pro-ef-core-2-for-asp.net-core-mvc

图4-1 创建示例项目

单击【确定】按钮继续设置项目,确保选中窗体顶部的【.NET Core】和【ASP.NET Core 2.1】,并单击【空】模板,如图4-2所示。Visual Studio 包含在项目中设置 ASP.NET Core MVC 和 Entity Framework Core 的模板,但结果隐藏了一些有用的细节。我从最基本的 ASP.NET Core 项目开始,然后在接下来的章节中逐步构建它,这样您就可以看到不同的组件是如何一起工作的。

图4-2 配置 ASP.NET Core 项目

单击【确定】按钮,Visual Studio 将创建一个具有基本配置 SportsStore 项目,它设置了 ASP.NET Core 但没有配置 MVC 框架或 Entity Framework Core。

配置 MVC 框架

下一步是为 MVC 框架添加基础配置,以便我可以添加控制器和视图来处理 HTTP 请求。我将 MVC 中间件添加至 ASP.NET Core 请求管道,如清单4-1所示。

清单 4-1:SportsStore 文件夹下的 Startup.cs 文件,配置 MVC

using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;

namespace SportsStore
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

这些代码使用默认路由架构配置 MVC 框架,添加对静态文件的支持,并配置了错误页以便对开发者提供有用的细节。

添加模型

此应用程序的模型将以产品列表为基础。我创建了 Models 文件夹并向其中添加了名为 Product.cs 的类文件,代码如清单4-2所示。

清单 4-2:Models 文件夹下的 Product.cs 文件的内容

namespace SportsStore.Models
{
    public class Product
    {
        public string Name { get; set; }
        public string Category { get; set; }
        public decimal PurchasePrice { get; set; }
        public decimal RetailPrice { get; set; }
    }
}

这是我在书中通常使用的 Product 类的一个变体,以便我能够更好地演示有用的 Entity Framework Core 特性。

添加存储库

为了提供对应用程序中数据的一致访问,我喜欢使用存储库模式,其中的接口定义用于访问数据的属性和方法,并使用实现类来处理数据存储机制。使用存储库模式的优点是,更易于对应用程序的 MVC 部分进行单元测试,对应用程序的其余部分将隐藏如何存储数据的详细信息。

提示:对于大多数项目来说,使用存储库是一个好主意,但它并不是使用 Entity Framework Core 的要求。例如,在第3部分中,大多数示例没有存储库,因为有许多复杂的代码更改,而且我不希望通过多个类和接口重复进行更改。如果您不相信存储库模式的好处,请不要担心,因为以后总是可以添加存储库模式,尽管需要进行一些重构。

要创建存储库接口,我向 Models 文件夹添加了一个名为 IRepository.cs 的文件,并添加了如清单4-3所示的代码。

清单 4-3:Models 文件夹下的 IRepository.cs 文件的内容

using System.Collections.Generic;

namespace SportsStore.Models
{
    public interface IRepository
    {
        IEnumerable<Product> Products { get; }
        void AddProduct(Product product);
    }
}

Products属性将提供对应用程序所知的所有产品的只读访问。AddProduct方法将用于添加新产品。

对于本章,我将把模型对象存储在内存中,然后在第 5 章中用 Entity Framework Core 替换模型对象。我在 Models 文件夹中添加了一个名为 DataRepository.cs 的类,并添加了清单4-4所示的代码。

清单 4-4:Models 文件夹下的 DataRepository.cs 文件的内容

using System.Collections.Generic;

namespace SportsStore.Models
{
    public class DataRepository : IRepository
    {
        private List<Product> data = new List<Product>();

        public IEnumerable<Product> Products => data;

        public void AddProduct(Product product)
        {
            this.data.Add(product);
        }
    }
}

DataRepository类实现了IRepository接口,并使用一个List来跟踪Product对象,这意味着一旦应用程序停止或重新启动,数据就会丢失。我在第3章中介绍了一个持久存储库,但在介绍 Entity Framework Core 之前,这足以让项目的 ASP.NET Core MVC 部分工作。

我将清单4-5中所示的语句添加到Startup类中,以便将DataRepository类注册为用于依赖于IRepository接口的实现。

清单 4-5:SportsStore 文件夹下的 Startup.cs 文件,配置依赖注入

using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using SportsStore.Models;

namespace SportsStore
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            services.AddSingleton<IRepository, DataRepository>();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

清单4-5中的语句使用AddSingleton方法注册了DataRepository类,这意味着在第一次解析IRepository接口上的依赖时将创建单个对象,然后将其用于所有后续依赖项。

添加控制器和视图

示例应用程序的重点将是产品对象的管理,因为这最大限度地演示了不同的数据特性。我需要一个控制器来接收 HTTP 请求并将它们转换为对Product对象的操作,所以我创建了 Controllers 文件夹,向它添加了一个名为HomeController.cs的代码文件,并使用它来定义如清单4-6所示的控制器。

清单 4-6:Controllers 文件夹下的 HomeController.cs 文件的内容

using Microsoft.AspNetCore.Mvc;
using SportsStore.Models;

namespace SportsStore.Controllers
{
    public class HomeController : Controller
    {
        private IRepository repository;

        public HomeController(IRepository repo) => repository = repo;

        public IActionResult Index() => View(repository.Products);

        [HttpPost]
        public IActionResult AddProduct(Product product)
        {
            repository.AddProduct(product);
            return RedirectToAction(nameof(Index));
        }
    }
}

Index action 将Product对象集合从存储库传递到视图,然后向用户显示数据列表。AddProduct方法基于 HTTP POST 请求所接收的数据存储新的Product对象。

接下来,我创建了 Views/Home 文件夹并向其添加了一个名为 Index.cshtml 的文件,内容如清单4-7所示。此视图将显示应用程序的Product数据,并允许用户创建新对象。

清单 4-7:Views/Home 文件夹下的 Index.cshtml 文件的内容

@model IEnumerable<Product>

<h3 class="p-2 bg-primary text-white text-center">Products</h3>

<div class="container-fluid mt-3">
    <div class="row">
        <div class="col font-weight-bold">Name</div>
        <div class="col font-weight-bold">Category</div>
        <div class="col font-weight-bold text-right">Purchase Price</div>
        <div class="col font-weight-bold text-right">Retail Price</div>
        <div class="col"></div>
    </div>
    <form asp-action="AddProduct" method="post">
        <div class="row">
            <div class="col"><input name="Name" class="form-control" /></div>
            <div class="col"><input name="Category" class="form-control" /></div>
            <div class="col">
                <input name="PurchasePrice" class="form-control" />
            </div>
            <div class="col">
                <input name="RetailPrice" class="form-control" />
            </div>
            <div class="col">
                <button type="submit" class="btn btn-primary">Add</button>
            </div>
        </div>
    </form>
    @if (Model.Count() == 0)
    {
        <div class="row">
            <div class="col text-center p-2">No Data</div>
        </div>
    }
    else
    {
        @foreach (Product p in Model)
        {
            <div class="row p-2">
                <div class="col">@p.Name</div>
                <div class="col">@p.Category</div>
                <div class="col text-right">@p.PurchasePrice</div>
                <div class="col text-right">@p.RetailPrice</div>
                <div class="col&quo