企业实体框架核心2

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/mzl87/article/details/86514577

目录

介绍

背景

架构:整体结构

先决条件

技能

软件

使用代码

第01章——数据库

第02章——核心项目

实体层

数据层

存储库与DbHelper与数据访问对象的对比

工作单元怎么样?

存储过程与LINQ查询

业务层

控制器与服务与业务对象

业务层:处理与业务相关的方面

第03章——将所有代码放在一起

得到所有

通过key获取

第04章——Mocker

第05章——Web API

ViewModel与Request

第06章——Web API的帮助页面

第07章——Web API的单元测试

第08章——Web API的集成测试


GitHub存储库

介绍

为应用程序设计一个企业架构是一个很大的挑战,在这一点上有一个共同的问题:根据我们公司选择的技术,按照最佳实践解决这个问题的最佳方法是什么。

本教程使用.Net Core,因此我们将使用Entity Framework Core,但这些概念适用于其他技术,如Dapper或其他ORM

实际上,我们将在本文中介绍企业架构师设计的常见要求。

本教程中提供的示例数据库代表在线商店。

因为我们正在使用Entity Framework CoreASP.NET Core使用内存数据库提供程序进行单元测试使用Test Web Server进行集成测试。

所有测试(单元测试和集成测试)都是用xUnit框架编写的。

背景

根据我的经验,应用程序的企业架构应该具有以下层:

  1. 实体层:包含实体(POCO
  2. 数据层:包含与数据库访问相关的对象
  3. 业务层:包含与业务相关的定义和验证
  4. 外部服务层(可选):包含外部服务的调用(ASMXWCFRESTful
  5. 通用层:包含层的公共对象(例如LoggersMappersExtensions
  6. 测试(QA:包含后端测试(单元和集成)
  7. 表示层:这是UI
  8. UI测试(QA:包含前端的自动测试

架构:整体结构

数据库

SQL Server

数据库

实体层

POCO

后端

数据层

DbContext,配置,约定,数据约定和存储库

业务层

服务,约定,数据约定,异常和记录器

外部服务层

ASMXWCFRESTful

通用层

记录器,映射器,扩展

表示层

UI框架(AngularJS | ReactJS | Vue.js |其他)

前端

用户

 

 

先决条件

技能

在继续之前,请记住我们需要掌握以下技能才能理解本教程:

  • OOP(面向对象编程)
  • AOP(面向切面​​编程)
  • ORM(对象关系映射)
  • 设计模式:领域驱动设计,存储库&工作单元以及IoC

软件

  • .Net Core
  • Visual Studio 2017
  • SQL Server实例(本地或远程)
  • SQL Server Management Studio

使用代码

01章——数据库

查看示例数据库以了解体系结构中的每个组件。在这个数据库中有4个架构:DboHumanResourcesWarehouseSales

每个模式代表商店公司的一个部门,请记住这一点,因为所有代码都是按照这个方面设计的此时此代码仅实现ProductionSales架构的功能。

所有表都有一个包含一列的主键,并且包含用于创建,最新更新和并发令牌的列。

架构

名称

dbo

ChangeLog

dbo

ChangeLogExclusion

dbo

Country

dbo

CountryCurrency

dbo

Currency

dbo

EventLog

HumanResources

Employee

HumanResources

EmployeeAddress

HumanResources

EmployeeEmail

Sales

Customer

Sales

OrderDetail

Sales

OrderHeader

Sales

OrderStatus

Sales

PaymentMethod

Sales

Shipper

Warehouse

Location

Warehouse

Product

Warehouse

ProductCategory

Warehouse

ProductInventory

您可以在此链接中找到数据库脚本OnLine Store GitHub上的数据库脚本

请记住:这是一个示例数据库,仅用于演示概念。

02章——核心项目

核心项目代表解决方案的核心,在本教程中核心项目包括实体,数据和业务层。

我们正在使用.NET Core,命名约定是.NET命名约定,因此定义命名约定表以显示如何在代码中设置名称非常有用,如下所示:

识别码

案例

例子

命名空间

PascalCase

Store

PascalCase

Product

接口

前缀+ PascalCase

ISalesRepository

方法

PascalCase中的动词+ PascalCase中的名词

GetProducts

异步方法

PascalCase中的动词+ PascalCase中的名词 + Async 后缀

GetOrdersAsync

属性

PascalCase

Description

参数

camelCase

connectionString

此约定很重要,因为它定义了体系结构的命名准则。

这是Store.Core项目的结构:

  1. EntityLayer
  2. DataLayer
  3. DataLayer\Configurations
  4. BusinessLayer
  5. BusinessLayer\Contracts
  6. BusinessLayer\Requests
  7. BusinessLayer\Responses

Entitylayer内部,我们将放置所有实体,在此上下文中,实体表示一个表示数据库中的表或视图的类,有时实体被命名为POCO(普通的旧公共语言运行时对象),而不是只具有属性、不包含方法或其他事物(事件)的类根据wkempf反馈,有必要明确POCOPOCO可以有方法和事件以及其他成员,但在POCO中添加这些成员并不常见。

DataLayer里面,我们将放置DbContext,因为它是DataLayer中的通用类。

对于DataLayerContracts,我们将放置代表操作目录的所有接口,我们专注于模式,我们将为每个模式创建一个接口并为默认模式(dboStore契约。

对于DataLayerDataContracts,我们将为Contracts命名空间中的返回值放置所有对象定义,现在该目录包含OrderInfo类定义。

对于DataLayerMapping,我们将所有对象定义与数据库的映射类相关联。

对于DataLayerRepositories,我们将放置Contracts定义的实现。

一个储存库包括与以下一个架构操作,所以我们有4个仓库:DboRepositoryHumanResourcesRepositoryProductionRepositorySalesRepository

EntityLayerDataLayer\Mapping之内,我们将为每个模式创建一个目录。

BusinessLayer内部,我们将创建服务的接口和实现,在这种情况下,服务将根据用例(或类似的东西)包含方法,并且这些方法必须执行验证并处理与业务相关的异常。

对于BusinessLayer\Responses,我们将创建响应:singlelistpaged来表示服务的结果。

我们将检查代码以理解这些概念,但是每个级别的审查将使用一个对象,因为剩余的代码是相似的。

实体层

OrderHeader 类:

using System;
using System.Collections.ObjectModel;
using OnLineStore.Core.EntityLayer.Dbo;
using OnLineStore.Core.EntityLayer.HumanResources;

namespace OnLineStore.Core.EntityLayer.Sales
{
    public class OrderHeader : IAuditableEntity
    {
        public OrderHeader()
        {
        }

        public OrderHeader(long? orderHeaderID)
        {
            OrderHeaderID = orderHeaderID;
        }

        public long? OrderHeaderID { get; set; }

        public short? OrderStatusID { get; set; }

        public DateTime? OrderDate { get; set; }

        public int? CustomerID { get; set; }

        public int? EmployeeID { get; set; }

        public int? ShipperID { get; set; }

        public decimal? Total { get; set; }

        public short? CurrencyID { get; set; }

        public Guid? PaymentMethodID { get; set; }

        public int? DetailsCount { get; set; }

        public long? ReferenceOrderID { get; set; }

        public string Comments { get; set; }

        public string CreationUser { get; set; }

        public DateTime? CreationDateTime { get; set; }

        public string LastUpdateUser { get; set; }

        public DateTime? LastUpdateDateTime { get; set; }

        public byte[] Timestamp { get; set; }

        public virtual OrderStatus OrderStatusFk { get; set; }

        public virtual Customer CustomerFk { get; set; }

        public virtual Employee EmployeeFk { get; set; }

        public virtual Shipper ShipperFk { get; set; }

        public virtual Currency CurrencyFk { get; set; }

        public virtual PaymentMethod PaymentMethodFk { get; set; }

        public virtual Collection<OrderDetail> OrderDetails { get; set; } = new Collection<OrderDetail>();
    }
}

请看一下POCO,我们使用可空类型而不是本机类型,因为如果属性有值或者没有,可以很容易地评估可空类型,这与数据库模型更相似。

EntityLayer有两个接口:IEntityIAuditEntityIEntity代表了我们的应用程序的所有实体,而IAuditEntity表示允许保存审计信息的所有实体:创建和最新更新作为特殊点,如果我们有视图映射,那些类没有实现IAuditEntity因为视图不允许添加,更新和删除操作。

数据层

对于此源代码,存储库的实现是按功能而不是泛型存储库如果我们需要实现特定的操作,泛型存储库需要创建派生的存储库。我更喜欢按功能部署的存储库,因为不需要创建派生对象(接口和类),但按功能的存储库将包含大量操作,因为它是功能中所有操作的占位符。

本文的示例数据库在数据库中包含4个模式,因此我们将拥有4个存储库,此实现提供了概念的分离。

我们在本教程中使用EF Core,因此我们需要一个DbContext和允许映射类和数据库对象(表和视图)的对象。

存储库与DbHelper与数据访问对象的对比

这个问题与命名对象有关,几年前我用DataAccessObject作为包含数据库操作(selectinsertupdatedelete等)的类的后缀。其他开发人员用DbHelper作为后缀来表示这种对象,在我开始使用EF时,我了解了存储库设计模式,因此从我的角度来看,我更喜欢使用Repository后缀来命名包含数据库操作的对象。

OnLineStoreDbContext 类:

using Microsoft.EntityFrameworkCore;
using OnLineStore.Core.DataLayer.Configurations;
using OnLineStore.Core.DataLayer.Configurations.Dbo;
using OnLineStore.Core.DataLayer.Configurations.HumanResources;
using OnLineStore.Core.DataLayer.Configurations.Warehouse;
using OnLineStore.Core.DataLayer.Configurations.Sales;
using OnLineStore.Core.EntityLayer.Dbo;
using OnLineStore.Core.EntityLayer.HumanResources;
using OnLineStore.Core.EntityLayer.Warehouse;
using OnLineStore.Core.EntityLayer.Sales;

namespace OnLineStore.Core.DataLayer
{
    public class OnLineStoreDbContext : DbContext
    {
        public OnLineStoreDbContext(DbContextOptions<OnLineStoreDbContext> options)
            : base(options)
        {
        }

        public DbSet<ChangeLog> ChangeLogs { get; set; }

        public DbSet<ChangeLogExclusion> ChangeLogExclusions { get; set; }

        public DbSet<CountryCurrency> CountryCurrencies { get; set; }

        public DbSet<Country> Countries { get; set; }

        public DbSet<Currency> Currencies { get; set; }

        public DbSet<EventLog> EventLogs { get; set; }

        public DbSet<Employee> Employees { get; set; }

        public DbSet<EmployeeAddress> EmployeeAddresses { get; set; }

        public DbSet<EmployeeEmail> EmployeeEmails { get; set; }

        public DbSet<ProductCategory> ProductCategories { get; set; }

        public DbSet<ProductInventory> ProductInventories { get; set; }

        public DbSet<Product> Products { get; set; }

        public DbSet<EntityLayer.Warehouse.Location> Warehouses { get; set; }

        public DbSet<Customer> Customers { get; set; }

        public DbSet<OrderDetail> OrderDetails { get; set; }

        public DbSet<OrderHeader> Orders { get; set; }

        public DbSet<OrderStatus> OrderStatuses { get; set; }

        public DbSet<OrderSummary> OrderSummaries { get; set; }

        public DbSet<PaymentMethod> PaymentMethods { get; set; }

        public DbSet<Shipper> Shippers { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // Apply all configurations

            modelBuilder
                .ApplyConfiguration(new ChangeLogConfiguration())
                .ApplyConfiguration(new ChangeLogExclusionConfiguration())
                .ApplyConfiguration(new CountryCurrencyConfiguration())
                .ApplyConfiguration(new CountryConfiguration())
                .ApplyConfiguration(new CurrencyConfiguration())
                .ApplyConfiguration(new EventLogConfiguration())
                ;

            modelBuilder
                .ApplyConfiguration(new EmployeeConfiguration())
                .ApplyConfiguration(new EmployeeAddressConfiguration())
                .ApplyConfiguration(new EmployeeEmailConfiguration())
                ;

            modelBuilder
                .ApplyConfiguration(new ProductCategoryConfiguration())
                .ApplyConfiguration(new ProductInventoryConfiguration())
                .ApplyConfiguration(new ProductConfiguration())
                .ApplyConfiguration(new LocationConfiguration())
                ;

            modelBuilder
                .ApplyConfiguration(new CustomerConfiguration())
                .ApplyConfiguration(new OrderDetailConfiguration())
                .ApplyConfiguration(new OrderHeaderConfiguration())
                .ApplyConfiguration(new OrderStatusConfiguration())
                .ApplyConfiguration(new OrderSummaryConfiguration())
                .ApplyConfiguration(new PaymentMethodConfiguration())
                .ApplyConfiguration(new ShipperConfiguration())
                ;

            base.OnModelCreating(modelBuilder);
        }
    }
}

OrderHeaderConfiguration 类:

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using OnLineStore.Core.EntityLayer.Sales;

namespace OnLineStore.Core.DataLayer.Configurations.Sales
{
    public class OrderHeaderConfiguration : IEntityTypeConfiguration<OrderHeader>
    {
        public void Configure(EntityTypeBuilder<OrderHeader> builder)
        {
            // Mapping for table
            builder.ToTable("OrderHeader", "Sales");

            // Set key for entity
            builder.HasKey(p => p.OrderHeaderID);

            // Set identity for entity (auto increment)
            builder.Property(p => p.OrderHeaderID).UseSqlServerIdentityColumn();

            // Set mapping for columns
            builder.Property(p => p.OrderStatusID).HasColumnType("smallint").IsRequired();
            builder.Property(p => p.OrderDate).HasColumnType("datetime").IsRequired();
            builder.Property(p => p.CustomerID).HasColumnType("int").IsRequired();
            builder.Property(p => p.EmployeeID).HasColumnType("int");
            builder.Property(p => p.ShipperID).HasColumnType("int");
            builder.Property(p => p.Total).HasColumnType("decimal(12, 4)").IsRequired();
            builder.Property(p => p.CurrencyID).HasColumnType("smallint");
            builder.Property(p => p.PaymentMethodID).HasColumnType("uniqueidentifier");
            builder.Property(p => p.DetailsCount).HasColumnType("int").IsRequired();
            builder.Property(p => p.ReferenceOrderID).HasColumnType("bigint");
            builder.Property(p => p.Comments).HasColumnType("varchar(max)");
            builder.Property(p => p.CreationUser).HasColumnType("varchar(25)").IsRequired();
            builder.Property(p => p.CreationDateTime).HasColumnType("datetime").IsRequired();
            builder.Property(p => p.LastUpdateUser).HasColumnType("varchar(25)");
            builder.Property(p => p.LastUpdateDateTime).HasColumnType("datetime");

            // Set concurrency token for entity
            builder.Property(p => p.Timestamp).ValueGeneratedOnAddOrUpdate().IsConcurrencyToken();

            // Add configuration for foreign keys
            builder
                .HasOne(p => p.OrderStatusFk)
                .WithMany(b => b.Orders)
                .HasForeignKey(p => p.OrderStatusID);

            builder
                .HasOne(p => p.CustomerFk)
                .WithMany(b => b.Orders)
                .HasForeignKey(p => p.CustomerID);

            builder
                .HasOne(p => p.ShipperFk)
                .WithMany(b => b.Orders)
                .HasForeignKey(p => p.ShipperID);
        }
    }
}

工作单元怎么样?

EF 6.x中通常创建一个存储库类和工作类单元:存储库提供的数据库访问操作而工作单元提供了保存数据库更改的操作但是在EF Core中,通常的做法是只拥有存储库而没有工作单元无论如何,对于这个代码,我们在Repository类中添加了两个方法:CommitChangesCommitChangesAsync,因此,为了确保在存储库中所有数据内部编写的方法都调用CommitChanges或者CommitChangesAsync,并且在这种设计中,我们有两个定义在我们的体系结构中工作。

对于此版本的DbContext,我们将动态使用DbSet,而不是在DbContext中声明DbSet属性。我认为这更多是关于架构师偏好,我喜欢动态使用DbSet因为我不担心添加所有的DbSetsDbContext中,但如果您认为在DbContext中使用声明DbSet属性更准确,则此样式将会更改。

异步操作怎么样?在这篇文章的先前版本中,我说我们将在最后一级实现异步操作:REST API,但我错了,因为.NET Core更多的是关于异步编程,所以最好的决定是使用EF Core提供的异步方式处理所有数据库操作。

我们可以看一看Repository类,有两种方法:AddUpdate,在这个例子中Order类有审计属性:CreationUser, CreationDateTime, LastUpdateUserLastUpdateDateTime同时Order类实现IAuditEntity的接口,该接口是用于设置审计属性的值。

对于本文的当前版本,我们将省略服务层,但在某些情况下,有一个层包含外部服务的连接(ASMXWCFRESTful)。

存储过程与LINQ查询

在数据层中,有一个非常有趣的点:我们如何使用存储过程?对于当前版本的EF Core,不支持存储过程,所以我们不能以本机方式使用它们,DbSet里面有一个方法来执行查询同时对于存储过程有效但是不返回结果集合(columns),我们可以添加一些扩展方法并添加包来使用经典的ADO.NET,所以在这种情况下我们需要处理动态创建对象来表示存储过程的结果那讲得通?如果我们使用名称为GetOrdersByMonth的过程并且该过程返回一个包含7列的选中集合,为了以相同的方式处理所有结果,我们需要定义对象来表示那些结果,对象必须根据我们在DataLayerDataContracts命名空间内部的命名约定进行定义。

在企业环境中,常见的讨论是关于LINQ查询或存储过程。根据我的经验,我认为解决这个问题的最佳方法是:与架构师和数据库管理员一起审查设计惯例现在,在异步模式中使用LINQ查询而不是存储过程,但有时一些公司有限制约定并且不允许使用LINQ查询,因此需要使用存储过程,我们需要使我们的架构灵活,因为我们不要不对开发者经理说业务逻辑将被重写,因为Entity Framework Core不允许调用存储过程

我们可以看到,直到现在,假设我们有EF Core的扩展方法来调用存储过程和数据契约来表示存储过程调用的结果,我们在哪里放置这些方法?最好使用相同的约定,因此我们将在契约和存储库中添加这些方法只是要清楚,如果我们有一个名为Sales.GetCustomerOrdersHistoryHumanResources.DisableEmployee的存储程序我们必须在SalesHumanResources存储库中放置方法。

需要明确的是:远离存储过程!

以前的概念对数据库中的视图应用相同的方式。此外,我们只需要检查存储库是否允许对视图进行添加,更新和删除操作。

更改跟踪Repository类内部有一个名称为GetChanges的方法,该方法通过ChangeTrackerDbContext获取所有更改并返回所有更改,因此这些值保存在CommitChanges方法中的ChangeLog表中。您可以使用业务对象更新一个现有实体,稍后您可以检查您的ChangeLog表:

ChangeLogID ClassName    PropertyName   Key  OriginalValue          CurrentValue           UserName   ChangeDate
----------- ------------ -------------- ---- ---------------------- ---------------------- ---------- -----------------------
1           Employee     FirstName      1    John                   John III               admin      2017-02-19 21:49:51.347
2           Employee     MiddleName     1                           Smith III              admin      2017-02-19 21:49:51.347
3           Employee     LastName       1    Doe                    Doe III                admin      2017-02-19 21:49:51.347

(3 row(s) affected)

我们可以看到实体中所做的所有更改都将保存在此表中,作为未来的改进,我们需要为此更改日志添加排除项。在本教程中,我们正在使用SQL Server,因为我知道有一种方法可以从数据库端启用更改跟踪,但是在这篇文章中,我向您展示了如何从后端实现此功能如果此功能位于后端或数据库端,则由您的领导决定。在时间轴中,我们可以在此表中检查实体中的所有更改,某些实体具有审计属性,但这些属性仅反映创建和上次更新的用户和日期,但不提供有关数据更改方式的完整详细信息。

业务层

控制器服务业务对象

在这一点上有一个共同的问题,我们必须如何命名代表业务操作的对象:对于本文的第一个版本,我将此对象命名为BusinessObject,这可能会让一些开发人员感到困惑,一些开发人员不会将此命名为业务对象因为Web API中的控制器代表业务逻辑,但是Service是开发人员使用的是另一个名称,因此从我的观点来看,将Service作为这个对象的后缀会更清楚。如果我们有一个在控制器中实现业务逻辑的Web API,我们可以省略服务,但如果有业务层,那么拥有服务更有用,这些类必须实现逻辑业务,控制器必须调用服务的方法。

业务层:处理与业务相关的方面

  1. 记录:我们需要一个记录器对象,这意味着一个在文本文件、数据库、电子邮件等上记录我们体系结构中所有事件的对象我们可以创建自己的记录器实现或选择现有的日志。我们已经使用Microsoft.Extensions.Logging包添加了日志记录,这样我们就可以使用.NET Core中的默认日志系统了,我们可以使用另一种日志机制,但此时我们将使用这个记录器,在控制器和业务对象的每个方法中,有一个像这样的代码行:Logger?.LogInformation("{0} has been invoked", nameof(GetOrdersAsync));,这样我们确保调用logger ,如果它是一个有效的实例,并使用nameof运算符来检索成员的名称而不使用神奇的字符串,之后我们将添加代码将所有日志保存到数据库中。
  2. 业务异常:处理向用户发送消息的最佳方式是使用自定义异常,在业务层内,我们将添加异常定义以表示体系结构中的所有句柄错误。
  3. 事务:正如我们在Sales业务对象内部所看到的,我们已经实现了事务来处理数据库中的多个更改CreateOrderAsync方法内部,我们从存储库调用方法,在存储库内部我们没有任何事务,因为服务是负责事务处理的过程,我们还添加了逻辑来处理与自定义消息业务相关的异常,因为我们需要提供友好的消息给最终用户。
  4. 有一个CloneOrderAsync方法,这种方法提供现有订单的副本,这是ERP的常见要求,因为创建新订单更容易,但添加一些修改而不是创建整个订单。有些情况下,销售代理创建新订单,但从详细信息中删除1或者2行,或者加12个详细信息,无论如何,绝不允许前段开发人员在UI中添加此逻辑,API必须提供此功能。
  5. SalesRepository中的GetCreateOrderRequestAsync方法提供创建订单所需的信息,来自外键的信息:产品和其他。使用此方法,我们提供了一个包含外键列表的模型,这样我们就可以减少前端的工作,从而知道如何创建创建订单操作。

Service 类:

using Microsoft.Extensions.Logging;
using OnLineStore.Core.BusinessLayer.Contracts;
using OnLineStore.Core.DataLayer;

namespace OnLineStore.Core.BusinessLayer
{
    public abstract class Service : IService
    {
        protected bool Disposed;
        protected ILogger Logger;
        protected IUserInfo UserInfo;

        public Service(ILogger logger, IUserInfo userInfo, OnLineStoreDbContext dbContext)
        {
            Logger = logger;
            UserInfo = userInfo;
            DbContext = dbContext;
        }

        public void Dispose()
        {
            if (!Disposed)
            {
                DbContext?.Dispose();

                Disposed = true;
            }
        }

        public OnLineStoreDbContext DbContext { get; }
    }
}

SalesService 类:

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using OnLineStore.Core.BusinessLayer.Contracts;
using OnLineStore.Core.BusinessLayer.Requests;
using OnLineStore.Core.BusinessLayer.Responses;
using OnLineStore.Core.DataLayer;
using OnLineStore.Core.DataLayer.Repositories;
using OnLineStore.Core.DataLayer.Sales;
using OnLineStore.Core.DataLayer.Warehouse;
using OnLineStore.Core.EntityLayer.Dbo;
using OnLineStore.Core.EntityLayer.Sales;
using OnLineStore.Core.EntityLayer.Warehouse;

namespace OnLineStore.Core.BusinessLayer
{
    public class SalesService : Service, ISalesService
    {
        public SalesService(ILogger<SalesService> logger, IUserInfo userInfo, OnLineStoreDbContext dbContext)
            : base(logger, userInfo, dbContext)
        {
        }

        public async Task<IPagedResponse<Customer>> GetCustomersAsync(int pageSize = 10, int pageNumber = 1)
        {
            Logger?.LogDebug("{0} has been invoked", nameof(GetCustomersAsync));

            var response = new PagedResponse<Customer>();

            try
            {
                // Get query
                var query = DbContext.Customers;

                // Set information for paging
                response.PageSize = pageSize;
                response.PageNumber = pageNumber;
                response.ItemsCount = await query.CountAsync();

                // Retrieve items, set model for response
                response.Model = await query
                    .Paging(pageSize, pageNumber)
                    .ToListAsync();
            }
            catch (Exception ex)
            {
                response.SetError(Logger, nameof(GetCustomersAsync), ex);
            }

            return response;
        }

        public async Task<IPagedResponse<Shipper>> GetShippersAsync(int pageSize = 10, int pageNumber = 1)
        {
            Logger?.LogDebug("{0} has been invoked", nameof(GetShippersAsync));

            var response = new PagedResponse<Shipper>();

            try
            {
                // Get query
                var query = DbContext.Shippers;

                // Set information for paging
                response.PageSize = pageSize;
                response.PageNumber = pageNumber;
                response.ItemsCount = await query.CountAsync();

                // Retrieve items, set model for response
                response.Model = await query
                    .Paging(pageSize, pageNumber)
                    .ToListAsync();
            }
            catch (Exception ex)
            {
                response.SetError(Logger, nameof(GetShippersAsync), ex);
            }

            return response;
        }

        public async Task<IPagedResponse<Currency>> GetCurrenciesAsync(int pageSize = 10, int pageNumber = 1)
        {
            Logger?.LogDebug("{0} has been invoked", nameof(GetCurrenciesAsync));

            var response = new PagedResponse<Currency>();

            try
            {
                // Get query
                var query = DbContext.Currencies;

                // Set information for paging
                response.PageSize = pageSize;
                response.PageNumber = pageNumber;
                response.ItemsCount = await query.CountAsync();

                // Retrieve items, set model for response
                response.Model = await query
                    .Paging(pageSize, pageNumber)
                    .ToListAsync();
            }
            catch (Exception ex)
            {
                response.SetError(Logger, nameof(GetCurrenciesAsync), ex);
            }

            return response;
        }

        public async Task<IPagedResponse<PaymentMethod>> GetPaymentMethodsAsync(int pageSize = 10, int pageNumber = 1)
        {
            Logger?.LogDebug("{0} has been invoked", nameof(GetPaymentMethodsAsync));

            var response = new PagedResponse<PaymentMethod>();

            try
            {
                // Get query
                var query = DbContext.PaymentMethods;

                // Set information for paging
                response.PageSize = pageSize;
                response.PageNumber = pageNumber;
                response.ItemsCount = await query.CountAsync();

                // Retrieve items, set model for response
                response.Model = await query
                    .Paging(pageSize, pageNumber)
                    .ToListAsync();
            }
            catch (Exception ex)
            {
                response.SetError(Logger, nameof(GetPaymentMethodsAsync), ex);
            }

            return response;
        }

        public async Task<IPagedResponse<OrderInfo>> GetOrdersAsync(int pageSize = 10, int pageNumber = 1, short? orderStatusID = null, int? customerID = null, int? employeeID = null, int? shipperID = null, short? currencyID = null, Guid? paymentMethodID = null)
        {
            Logger?.LogDebug("{0} has been invoked", nameof(GetOrdersAsync));

            var response = new PagedResponse<OrderInfo>();

            try
            {
                // Get query
                var query = DbContext.GetOrders(orderStatusID, customerID, employeeID, shipperID, currencyID, paymentMethodID);

                // Set information for paging
                response.PageSize = pageSize;
                response.PageNumber = pageNumber;
                response.ItemsCount = await query.CountAsync();

                // Retrieve items, set model for response
                response.Model = await query
                    .Paging(pageSize, pageNumber)
                    .ToListAsync();

                response.Message = string.Format("Page {0} of {1}, Total of rows: {2}", response.PageNumber, response.PageCount, response.ItemsCount);

                Logger?.LogInformation(response.Message);
            }
            catch (Exception ex)
            {
                response.SetError(Logger, nameof(GetOrdersAsync), ex);
            }

            return response;
        }

        public async Task<ISingleResponse<OrderHeader>> GetOrderAsync(long id)
        {
            Logger?.LogDebug("{0} has been invoked", nameof(GetOrderAsync));

            var response = new SingleResponse<OrderHeader>();

            try
            {
                // Retrieve order by id
                response.Model = await DbContext.GetOrderAsync(new OrderHeader(id));
            }
            catch (Exception ex)
            {
                response.SetError(Logger, nameof(GetOrderAsync), ex);
            }

            return response;
        }

        public async Task<ISingleResponse<CreateOrderRequest>> GetCreateOrderRequestAsync()
        {
            Logger?.LogDebug("{0} has been invoked", nameof(GetCreateOrderRequestAsync));

            var response = new SingleResponse<CreateOrderRequest>();

            try
            {
                // Retrieve products list
                response.Model.Products = await DbContext.GetProducts().ToListAsync();

                // Retrieve customers list
                response.Model.Customers = await DbContext.Customers.ToListAsync();
            }
            catch (Exception ex)
            {
                response.SetError(Logger, nameof(GetCreateOrderRequestAsync), ex);
            }

            return response;
        }

        public async Task<ISingleResponse<OrderHeader>> CreateOrderAsync(OrderHeader header, OrderDetail[] details)
        {
            Logger?.LogDebug("{0} has been invoked", nameof(CreateOrderAsync));

            var response = new SingleResponse<OrderHeader>();

            // Begin transaction
            using (var transaction = await DbContext.Database.BeginTransactionAsync())
            {
                try
                {
                    // todo: Retrieve available warehouse to dispatch products
                    var warehouses = await DbContext.Warehouses.ToListAsync();

                    foreach (var detail in details)
                    {
                        // Retrieve product by id
                        var product = await DbContext.GetProductAsync(new Product(detail.ProductID));

                        // Throw exception if product no exists
                        if (product == null)
                            throw new NonExistingProductException(string.Format(SalesDisplays.NonExistingProductExceptionMessage, detail.ProductID));

                        // Throw exception if product is discontinued
                        if (product.Discontinued == true)
                            throw new AddOrderWithDiscontinuedProductException(string.Format(SalesDisplays.AddOrderWithDiscontinuedProductExceptionMessage, product.ProductID));

                        // Throw exception if quantity for product is invalid
                        if (detail.Quantity <= 0)
                            throw new InvalidQuantityException(string.Format(SalesDisplays.InvalidQuantityExceptionMessage, product.ProductID));

                        // Set values for detail
                        detail.ProductName = product.ProductName;
                        detail.UnitPrice = product.UnitPrice;
                        detail.Total = product.UnitPrice * detail.Quantity;
                    }

                    // Set default values for order header
                    if (!header.OrderDate.HasValue)
                        header.OrderDate = DateTime.Now;

                    header.OrderStatusID = 100;

                    // Calculate total for order header from order's details
                    header.Total = details.Sum(item => item.Total);
                    header.DetailsCount = details.Count();

                    // Save order header
                    DbContext.Add(header, UserInfo);

                    await DbContext.SaveChangesAsync();

                    foreach (var detail in details)
                    {
                        // Set order id for order detail
                        detail.OrderHeaderID = header.OrderHeaderID;

                        // Add order detail
                        DbContext.Add(detail, UserInfo);

                        await DbContext.SaveChangesAsync();

                        // Create product inventory instance
                        var productInventory = new ProductInventory
                        {
                            ProductID = detail.ProductID,
                            LocationID = warehouses.First().LocationID,
                            OrderDetailID = detail.OrderDetailID,
                            Quantity = detail.Quantity * -1,
                            CreationDateTime = DateTime.Now,
                            CreationUser = header.CreationUser
                        };

                        // Save product inventory
                        DbContext.Add(productInventory);
                    }

                    await DbContext.SaveChangesAsync();

                    response.Model = header;

                    // Commit transaction
                    transaction.Commit();

                    Logger.LogInformation(SalesDisplays.CreateOrderMessage);
                }
                catch (Exception ex)
                {
                    response.SetError(Logger, nameof(CreateOrderAsync), ex);
                }
            }

            return response;
        }

        public async Task<ISingleResponse<OrderHeader>> CloneOrderAsync(long id)
        {
            Logger?.LogDebug("{0} has been invoked", nameof(CloneOrderAsync));

            var response = new SingleResponse<OrderHeader>();

            try
            {
                // Retrieve order by id
                var entity = await DbContext.GetOrderAsync(new OrderHeader(id));

                if (entity != null)
                {
                    // Create a new instance for order and set values from existing order
                    response.Model = new OrderHeader
                    {
                        OrderHeaderID = entity.OrderHeaderID,
                        OrderDate = entity.OrderDate,
                        CustomerID = entity.CustomerID,
                        EmployeeID = entity.EmployeeID,
                        ShipperID = entity.ShipperID,
                        Total = entity.Total,
                        Comments = entity.Comments
                    };

                    if (entity.OrderDetails != null && entity.OrderDetails.Count > 0)
                    {
                        foreach (var detail in entity.OrderDetails)
                        {
                            // Add order detail clone to collection
                            response.Model.OrderDetails.Add(new OrderDetail
                            {
                                ProductID = detail.ProductID,
                                ProductName = detail.ProductName,
                                UnitPrice = detail.UnitPrice,
                                Quantity = detail.Quantity,
                                Total = detail.Total
                            });
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                response.SetError(Logger, nameof(CloneOrderAsync), ex);
            }

            return response;
        }

        public async Task<IResponse> RemoveOrderAsync(long id)
        {
            Logger?.LogDebug("{0} has been invoked", nameof(RemoveOrderAsync));

            var response = new Response();

            try
            {
                // Retrieve order by id
                var entity = await DbContext.GetOrderAsync(new OrderHeader(id));

                if (entity != null)
                {
                    // Restrict remove operation for orders with details
                    if (entity.OrderDetails.Count > 0)
                        throw new ForeignKeyDependencyException(string.Format(SalesDisplays.RemoveOrderExceptionMessage, id));

                    // Delete order
                    DbContext.Remove(entity);

                    await DbContext.SaveChangesAsync();

                    Logger?.LogInformation(SalesDisplays.DeleteOrderMessage);
                }
            }
            catch (Exception ex)
            {
                response.SetError(Logger, nameof(RemoveOrderAsync), ex);
            }

            return response;
        }
    }
}

BusinessLayer中,最好是有代表错误的自定义异常,而不是向客户机发送简单的字符串消息,显然,自定义异常必须有一个消息,但在Logger中会有一个关于自定义异常的引用。对于此体系结构,这些是自定义异常:

业务异常

名称

描述

AddOrderWithDiscontinuedProductException

表示添加已停产产品的订单的异常

ForeignKeyDependencyException

表示删除包含详细信息行的订单的异常

DuplicatedProductNameException

表示添加具有现有名称的产品的异常

NonExistingProductException

表示使用非现有产品添加订单的异常

03章——将所有代码放在一起

我们需要在OnModelCreating方法中创建一个OnLineStoreDbContext实例,该实例与SQL Server一起使用,所有配置都应用于ModelBuilder实例。

稍后,将使用OnLineStoreDbContext的有效实例创建SalesService的实例,以获取对服务操作的访问权限。

得到所有

这是我们如何检索订单列表的示例:

// Create logger instance
var logger = LoggerMocker.GetLogger<ISalesService>();

// Create application user
var userInfo = new UserInfo("admin");

// Create options for DbContext
var options = new DbContextOptionsBuilder<OnLineStoreDbContext>()
    .UseSqlServer("YourConnectionStringHere")
    .Options;

// Create instance of business object
// Set logger, application user and context for database
using (var service = new SalesService(logger, userInfo, new OnLineStoreDbContext(options)))
{
    // Declare parameters and set values for paging
	var pageSize = 10;
	var pageNumber = 1;

    // Get response from business object
	var response = await service.GetOrdersAsync(pageSize, pageNumber);

	// Validate if there was an error
	var valid = !response.DidError;
}

我们可以看到,GetOrdersAsync方法SalesServiceSales.Order表中检索行作为通用列表。

通过key获取

这是我们如何通过密钥检索实体的示例:

// Create logger instance
var logger = LoggerMocker.GetLogger<ISalesService>();

// Create application user
var userInfo = new UserInfo("admin");

// Create options for DbContext
var options = new DbContextOptionsBuilder<OnLineStoreDbContext>()
    .UseSqlServer("YourConnectionStringHere")
    .Options;

// Create instance of business object
// Set logger, application user and context for database
using (var service = new SalesService(logger, userInfo, new OnLineStoreDbContext(options)))
{
    // Declare parameters and set values for paging
	var id = 1;

    // Get response from business object
	var response = await service.GetOrderAsync(id);

	// Validate if there was an error
	var valid = !response.DidError;
	
	// Get entity
	var entity = response.Model;
}

对于本文的传入版本,将有另一个操作的示例。

04章——Mocker

Mocker它是一个允许在一个日期范围内在Sales.OrderHeaderSales.OrderDetail以及Warehouse.ProductInventory表中创建行的项目,默认情况下Mocker会创建一年的行。

Program 类:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using OnLineStore.Common;
using OnLineStore.Core.EntityLayer.Sales;

namespace OnLineStore.Mocker
{
    public class Program
    {
        private static readonly ILogger Logger;

        static Program()
        {
            Logger = LoggingHelper.GetLogger<Program>();
        }

        public static void Main(string[] args)
        {
            MainAsync(args).GetAwaiter().GetResult();
        }

        static async Task MainAsync(string[] args)
        {
            var year = DateTime.Now.AddYears(-1).Year;
            var ordersLimitPerDay = 3;

            foreach (var arg in args)
            {
                if (arg.StartsWith("/year:"))
                    year = Convert.ToInt32(arg.Replace("/year:", string.Empty));
                else if (arg.StartsWith("/ordersLimitPerDay:"))
                    ordersLimitPerDay = Convert.ToInt32(arg.Replace("/ordersLimitPerDay:", string.Empty));
            }

            var start = new DateTime(year, 1, 1);
            var end = new DateTime(year, 12, DateTime.DaysInMonth(year, 12));

            if (start.DayOfWeek == DayOfWeek.Sunday)
                start = start.AddDays(1);

            do
            {
                if (start.DayOfWeek != DayOfWeek.Sunday)
                {
                    await CreateDataAsync(start, ordersLimitPerDay);

                    Thread.Sleep(1000);
                }

                start = start.AddDays(1);
            }
            while (start <= end);
        }

        static async Task CreateDataAsync(DateTime date, int ordersLimitPerDay)
        {
            var random = new Random();

            var warehouseService = ServiceMocker.GetWarehouseService();
            var salesService = ServiceMocker.GetSalesService();

            var customers = (await salesService.GetCustomersAsync()).Model.ToList();
            var currencies = (await salesService.GetCurrenciesAsync()).Model.ToList();
            var paymentMethods = (await salesService.GetPaymentMethodsAsync()).Model.ToList();
            var products = (await warehouseService.GetProductsAsync()).Model.ToList();

            Logger.LogInformation("Creating orders for {0}", date);

            for (var i = 0; i < ordersLimitPerDay; i++)
            {
                var header = new OrderHeader
                {
                    OrderDate = date,
                    CreationDateTime = date
                };

                var selectedCustomer = random.Next(0, customers.Count - 1);
                var selectedCurrency = random.Next(0, currencies.Count - 1);
                var selectedPaymentMethod = random.Next(0, paymentMethods.Count - 1);

                header.CustomerID = customers[selectedCustomer].CustomerID;
                header.CurrencyID = currencies[selectedCurrency].CurrencyID;
                header.PaymentMethodID = paymentMethods[selectedPaymentMethod].PaymentMethodID;

                var details = new List<OrderDetail>();

                var detailsCount = random.Next(1, 5);

                for (var j = 0; j < detailsCount; j++)
                {
                    var detail = new OrderDetail
                    {
                        ProductID = products[random.Next(0, products.Count - 1)].ProductID,
                        Quantity = (short)random.Next(1, 5)
                    };

                    if (details.Count > 0 && details.Count(item => item.ProductID == detail.ProductID) == 1)
                        continue;

                    details.Add(detail);
                }

                await salesService.CreateOrderAsync(header, details.ToArray());

                Logger.LogInformation("Date: {0}", date);
            }

            warehouseService.Dispose();
            salesService.Dispose();
        }
    }
}

现在,在同一窗口终端,我们需要运行下面的命令:dotnet run ,如果一切正常,我们可以在我们的数据库中检查到OrderHeaderOrderDetailProductInventory表的数据。

Mocker怎么工作?设置日期的范围和每日订单的限制,然后迭代日期范围内的所有日期,除了星期日,因为我们假设星期日不允许创建订单处理然后创建DbContextServices的实例,并使用随机索引排列数据,从产品、客户、货币和支付方式中获取元素然后调用CreateOrderAsync方法。

您可以根据需要调整日期和订单的范围以模拟数据,一旦Mocker完成,您可以检查数据库中的数据。

05章——Web API

有一个名称为OnlineStore.WebAPI的项目,这表示此解决方案的Web API,此项目引用了OnLineStore.Core项目。

我们来看看SalesController类:

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using OnLineStore.Core.BusinessLayer.Contracts;
using OnLineStore.WebAPI.Requests;
using OnLineStore.WebAPI.Responses;

namespace OnLineStore.WebAPI.Controllers
{
#pragma warning disable CS1591
    [ApiController]
    [Route("api/v1/[controller]")]
    public class SalesController : ControllerBase
    {
        protected ILogger Logger;
        protected ISalesService SalesService;

        public SalesController(ILogger<SalesController> logger, ISalesService salesService)
        {
            Logger = logger;
            SalesService = salesService;
        }
#pragma warning restore CS1591

        /// <summary>
        /// Retrieves the orders generated by customers
        /// </summary>
        /// <param name="pageSize">Page size</param>
        /// <param name="pageNumber">Page number</param>
        /// <param name="orderStatusID">Order status</param>
        /// <param name="customerID">Customer</param>
        /// <param name="employeeID">Employee</param>
        /// <param name="shipperID">Shipper</param>
        /// <param name="currencyID">Currency</param>
        /// <param name="paymentMethodID">Payment method</param>
        /// <returns>A sequence of orders</returns>
        [HttpGet("Order")]
        [ProducesResponseType(200)]
        [ProducesResponseType(500)]
        public async Task<IActionResult> GetOrdersAsync(int? pageSize = 50, int? pageNumber = 1, short? orderStatusID = null, int? customerID = null, int? employeeID = null, int? shipperID = null, short? currencyID = null, Guid? paymentMethodID = null)
        {
            Logger?.LogDebug("{0} has been invoked", nameof(GetOrdersAsync));

            // Get response from business logic
            var response = await SalesService.GetOrdersAsync((int)pageSize, (int)pageNumber, orderStatusID, customerID, employeeID, shipperID, currencyID, paymentMethodID);

            // Return as http response
            return response.ToHttpResponse();
        }

        /// <summary>
        /// Retrieves an existing order by id
        /// </summary>
        /// <param name="id">Order ID</param>
        /// <returns>An existing order</returns>
        [HttpGet("Order/{id}")]
        [ProducesResponseType(200)]
        [ProducesResponseType(404)]
        [ProducesResponseType(500)]
        public async Task<IActionResult> GetOrderAsync(long id)
        {
            Logger?.LogDebug("{0} has been invoked", nameof(GetOrderAsync));

            // Get response from business logic
            var response = await SalesService.GetOrderAsync(id);

            // Return as http response
            return response.ToHttpResponse();
        }

        /// <summary>
        /// Retrieves the request model to create a new order
        /// </summary>
        /// <returns>A model that represents the request to create a new order</returns>
        [HttpGet("CreateOrderRequest")]
        public async Task<IActionResult> GetCreateOrderRequestAsync()
        {
            Logger?.LogDebug("{0} has been invoked", nameof(GetCreateOrderRequestAsync));

            // Get response from business logic
            var response = await SalesService.GetCreateOrderRequestAsync();

            // Return as http response
            return response.ToHttpResponse();
        }

        /// <summary>
        /// Creates a new order
        /// </summary>
        /// <param name="request">Model request</param>
        /// <returns>A result that contains the order ID generated by application</returns>
        [HttpPost]
        [Route("Order")]
        [ProducesResponseType(200)]
        [ProducesResponseType(400)]
        [ProducesResponseType(500)]
        public async Task<IActionResult> CreateOrderAsync([FromBody] OrderHeaderRequest request)
        {
            Logger?.LogDebug("{0} has been invoked", nameof(CreateOrderAsync));

            // Get response from business logic
            var response = await SalesService.CreateOrderAsync(request.GetOrder(), request.GetOrderDetails().ToArray());

            // Return as http response
            return response.ToHttpResponse();
        }

        /// <summary>
        /// Creates a new order from existing order
        /// </summary>
        /// <param name="id">Order ID</param>
        /// <returns>A model for a new order</returns>
        [HttpGet("CloneOrder/{id}")]
        [ProducesResponseType(200)]
        [ProducesResponseType(500)]
        public async Task<IActionResult> CloneOrderAsync(int id)
        {
            Logger?.LogDebug("{0} has been invoked", nameof(CloneOrderAsync));

            // Get response from business logic
            var response = await SalesService.CloneOrderAsync(id);

            // Return as http response
            return response.ToHttpResponse();
        }

        /// <summary>
        /// Deletes an existing order
        /// </summary>
        /// <param name="id">ID for order</param>
        /// <returns>A success response if order is deleted</returns>
        [HttpDelete("Order/{id}")]
        [ProducesResponseType(200)]
        [ProducesResponseType(500)]
        public async Task<IActionResult> DeleteOrderAsync(int id)
        {
            Logger?.LogDebug("{0} has been invoked", nameof(DeleteOrderAsync));

            // Get response from business logic
            var response = await SalesService.RemoveOrderAsync(id);

            // Return as http response
            return response.ToHttpResponse();
        }
    }
}

ViewModelRequest

ViewModel是一个包含行为的对象,request是与调用Web API方法相关的动作,这是误解:ViewModel是一个链接到视图的对象,包含处理更改和与视图同步的行为通常,Web API方法的参数是一个具有属性的对象,因此这个定义被命名Request; MVC不是MVVM,模型的生命周期在这些模式中是不同的,这个定义不保持UIAPI之间的状态,而且在查询字符串的请求中设置属性值的过程由模型绑定器处理。

现在来看看Startup.cs类:

using System;
using System.IO;
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using OnLineStore.Core;
using OnLineStore.Core.BusinessLayer;
using OnLineStore.Core.BusinessLayer.Contracts;
using OnLineStore.Core.DataLayer;
using Swashbuckle.AspNetCore.Swagger;

namespace OnLineStore.WebAPI
{
#pragma warning disable CS1591
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

            services.AddMvc().AddJsonOptions(options =>
            {
                options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
            });

            // Setting dependency injection

            // For DbContext
            services.AddDbContext<OnLineStoreDbContext>(options => options.UseSqlServer(Configuration["AppSettings:ConnectionString"]));

            // User info
            services.AddScoped<IUserInfo, UserInfo>();

            // Logger for services
            services.AddScoped<ILogger, Logger<Service>>();

            // Services
            services.AddScoped<IHumanResourcesService, HumanResourcesService>();
            services.AddScoped<IWarehouseService, WarehouseService>();
            services.AddScoped<ISalesService, SalesService>();

            // Configuration for Help page
            services.AddSwaggerGen(options =>
            {
                options.SwaggerDoc("v1", new Info { Title = "OnLine Store API", Version = "v1" });

                // Get xml comments path
                var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
                var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);

                // Set xml path
                options.IncludeXmlComments(xmlPath);
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
                app.UseDeveloperExceptionPage();

            // todo: Set port number for client app
            app.UseCors(policy =>
            {
                // Add client origin in CORS policy
                policy.WithOrigins("http://localhost:4200");
                policy.AllowAnyHeader();
                policy.AllowAnyMethod();
            });

            // Configuration for Swagger
            app.UseSwagger();

            app.UseSwaggerUI(c =>
            {
                c.SwaggerEndpoint("/swagger/v1/swagger.json", "OnLine Store API");
            });

            app.UseMvc();
        }
    }
#pragma warning restore CS1591
}

这个类是Web API项目的配置点,在这个类中有依赖注入的配置,API的配置和其他设置。

对于Web API项目,这些是控制器的路由:

动词

路线

描述

GET

api/v1/Sales/Order

获得订单

GET

api/v1/Sales/Order/1

ID获得订单

GET

api/v1/Sales/CreateOrderRequest

获取模型以创建订单

GET

api/v1/Sales/CloneOrder/3

克隆现有订单

POST

api/v1/Sales/Order

创建新订单

DELETE

api/v1/Sales/Order

删除现有订单

我们可以看到在每条路径中都有一个v1,这是因为Web API的版本是1,并且该值是RouteWeb API项目中的控制器的属性中定义的。

06章——Web API的帮助页面

Web API使用Swagger来显示帮助页面。

需要以下软件包才能显示Swagger的帮助页面:

  • Swashbuckle.AspNetCore

Swagger的配置位于Startup类中,Swagger附加的配置在ConfigureServices方法中:

// Configuration for Help page
services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1", new Info { Title = "OnLine Store API", Version = "v1" });

    // Get xml comments path
    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);

    // Set xml path
    options.IncludeXmlComments(xmlPath);
});

端点的配置在Configure方法中:

// Configuration for Swagger
app.UseSwagger();

app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "OnLine Store API");
});

Swagger允许显示控制器中操作的描述,这些描述来自xml注释。

帮助页面:

Help Page For Web API

帮助页面中的模型部分:

Models Section In Help Page For Web API

Web API的帮助页面是一种很好的做法,因为它提供了有关客户端API的信息。

07章——Web API的单元测试

现在我们继续为Web API项目添加单元测试,这些测试在内存数据库中工作,单元测试和集成测试之间有什么区别?对于单元测试,我们模拟Web API项目的所有依赖项,对于集成测试,我们运行一个模拟Web API执行的过程。我的意思是模拟Web API(接受Http请求),显然有关于单元测试和集成测试的更多信息,但是在此文中这个基本想法就足够了。

什么是TDD测试在这些日子里很重要,因为通过单元测试,在发布之前很容易对功能进行测试,测试驱动开发(TDD是定义单元测试和验证代码行为的方式。TDD的另一个概念是AAA:安排,行动和断言 ; arrange是用于创建对象的块,act是用于放置方法的所有调用的块,assert是用于验证方法调用的结果的块。

现在,看看SalesControllerTests类:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using OnLineStore.Common;
using OnLineStore.Core.BusinessLayer.Requests;
using OnLineStore.Core.BusinessLayer.Responses;
using OnLineStore.Core.DataLayer.Sales;
using OnLineStore.Core.EntityLayer.Sales;
using OnLineStore.WebAPI.Controllers;
using OnLineStore.WebAPI.Requests;
using OnLineStore.WebAPI.UnitTests.Mocks;
using Xunit;

namespace OnLineStore.WebAPI.UnitTests
{
    public class SalesControllerTests
    {
        [Fact]
        public async Task TestGetOrdersAsync()
        {
            // Arrange
            var logger = LoggingHelper.GetLogger<SalesController>();
            var service = ServiceMocker.GetSalesService(nameof(TestGetOrdersAsync));
            var controller = new SalesController(logger, service);

            // Act
            var response = await controller.GetOrdersAsync() as ObjectResult;
            var value = response.Value as IPagedResponse<OrderInfo>;

            service.Dispose();

            // Assert
            Assert.False(value.DidError);
        }

        [Fact]
        public async Task TestGetOrdersByCurrencyAsync()
        {
            // Arrange
            var logger = LoggingHelper.GetLogger<SalesController>();
            var service = ServiceMocker.GetSalesService(nameof(TestGetOrdersByCurrencyAsync));
            var controller = new SalesController(logger, service);
            var currencyID = (short?)1000;

            // Act
            var response = await controller.GetOrdersAsync(currencyID: currencyID) as ObjectResult;
            var value = response.Value as IPagedResponse<OrderInfo>;

            service.Dispose();

            // Assert
            Assert.False(value.DidError);
            Assert.True(value.Model.Count() > 0);
            Assert.True(value.Model.Count(item => item.CurrencyID == currencyID) == value.Model.Count());
        }

        [Fact]
        public async Task TestGetOrdersByCustomerAsync()
        {
            // Arrange
            var logger = LoggingHelper.GetLogger<SalesController>();
            var service = ServiceMocker.GetSalesService(nameof(TestGetOrdersByCustomerAsync));
            var controller = new SalesController(logger, service);
            var customerID = 1;

            // Act
            var response = await controller.GetOrdersAsync(customerID: customerID) as ObjectResult;
            var value = response.Value as IPagedResponse<OrderInfo>;

            service.Dispose();

            // Assert
            Assert.False(value.DidError);
            Assert.True(value.Model.Count(item => item.CustomerID == customerID) == value.Model.Count());
        }

        [Fact]
        public async Task TestGetOrdersByEmployeeAsync()
        {
            // Arrange
            var logger = LoggingHelper.GetLogger<SalesController>();
            var service = ServiceMocker.GetSalesService(nameof(TestGetOrdersByEmployeeAsync));
            var controller = new SalesController(logger, service);
            var employeeID = 1;

            // Act
            var response = await controller.GetOrdersAsync(employeeID: employeeID) as ObjectResult;
            var value = response.Value as IPagedResponse<OrderInfo>;

            service.Dispose();

            // Assert
            Assert.False(value.DidError);
            Assert.True(value.Model.Count(item => item.EmployeeID == employeeID) == value.Model.Count());
        }

        [Fact]
        public async Task TestGetOrderAsync()
        {
            // Arrange
            var logger = LoggingHelper.GetLogger<SalesController>();
            var service = ServiceMocker.GetSalesService(nameof(TestGetOrderAsync));
            var controller = new SalesController(logger, service);
            var id = 1;

            // Act
            var response = await controller.GetOrderAsync(id) as ObjectResult;
            var value = response.Value as ISingleResponse<OrderHeader>;

            service.Dispose();

            // Assert
            Assert.False(value.DidError);
        }

        [Fact]
        public async Task TestGetNonExistingOrderAsync()
        {
            // Arrange
            var logger = LoggingHelper.GetLogger<SalesController>();
            var service = ServiceMocker.GetSalesService(nameof(TestGetNonExistingOrderAsync));
            var controller = new SalesController(logger, service);
            var id = 0;

            // Act
            var response = await controller.GetOrderAsync(id) as ObjectResult;
            var value = response.Value as ISingleResponse<OrderHeader>;

            service.Dispose();

            // Assert
            Assert.False(value.DidError);
        }

        [Fact]
        public async Task TestGetCreateOrderRequestAsync()
        {
            // Arrange
            var logger = LoggingHelper.GetLogger<SalesController>();
            var service = ServiceMocker.GetSalesService(nameof(TestGetCreateOrderRequestAsync));
            var controller = new SalesController(logger, service);

            // Act
            var response = await controller.GetCreateOrderRequestAsync() as ObjectResult;
            var value = response.Value as ISingleResponse<CreateOrderRequest>;

            service.Dispose();

            // Assert
            Assert.False(value.DidError);
            Assert.True(value.Model.Products.Count() > 0);
            Assert.True(value.Model.Customers.Count() > 0);
        }

        [Fact]
        public async Task TestCreateOrderAsync()
        {
            // Arrange
            var logger = LoggingHelper.GetLogger<SalesController>();
            var service = ServiceMocker.GetSalesService(nameof(TestCreateOrderAsync));
            var controller = new SalesController(logger, service);
            var model = new OrderHeaderRequest
            {
                CustomerID = 1,
                PaymentMethodID = new Guid("7671A4F7-A735-4CB7-AAB4-CF47AE20171D"),
                Comments = "Order from unit tests",
                CreationUser = "unitests",
                CreationDateTime = DateTime.Now,
                Details = new List<OrderDetailRequest>
                {
                    new OrderDetailRequest
                    {
                        ProductID = 1,
                        ProductName = "The King of Fighters XIV",
                        Quantity = 1,
                    }
                }
            };

            // Act
            var response = await controller.CreateOrderAsync(model) as ObjectResult;
            var value = response.Value as ISingleResponse<OrderHeader>;

            service.Dispose();

            // Assert
            Assert.False(value.DidError);
            Assert.True(value.Model.OrderHeaderID.HasValue);
        }

        [Fact]
        public async Task TestCloneOrderAsync()
        {
            // Arrange
            var logger = LoggingHelper.GetLogger<SalesController>();
            var service = ServiceMocker.GetSalesService(nameof(TestCloneOrderAsync));
            var controller = new SalesController(logger, service);
            var id = 1;

            // Act
            var response = await controller.CloneOrderAsync(id) as ObjectResult;
            var value = response.Value as ISingleResponse<OrderHeader>;

            service.Dispose();

            // Assert
            Assert.False(value.DidError);
        }
    }
}

我们可以看到这些方法在Web API项目中对Urls执行测试,请注意测试是异步方法。

08章——Web API的集成测试

为了使用集成测试,我们需要创建一个类来提供一个Web Host来执行Http行为,这个类它将是TestFixture并代表Web APIHttp请求,还有一个带有名称为SalesTests的类,这个类将包含所有在SalesController类中定义的请求操作,但使用模拟的Http客户端。

TestFixture类代码:

using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace Store.API.IntegrationTests
{
    public class TestFixture<TStartup> : IDisposable
    {
        public static string GetProjectPath(string projectRelativePath, Assembly startupAssembly)
        {
            var projectName = startupAssembly.GetName().Name;

            var applicationBasePath = AppContext.BaseDirectory;

            var directoryInfo = new DirectoryInfo(applicationBasePath);

            do
            {
                directoryInfo = directoryInfo.Parent;

                var projectDirectoryInfo = new DirectoryInfo(Path.Combine(directoryInfo.FullName, projectRelativePath));

                if (projectDirectoryInfo.Exists)
                    if (new FileInfo(Path.Combine(projectDirectoryInfo.FullName, projectName, $"{projectName}.csproj")).Exists)
                        return Path.Combine(projectDirectoryInfo.FullName, projectName);
            }
            while (directoryInfo.Parent != null);

            throw new Exception($"Project root could not be located using the application root {applicationBasePath}.");
        }

        private TestServer Server;

        public TestFixture()
            : this(Path.Combine(""))
        {
        }

        protected TestFixture(string relativeTargetProjectParentDir)
        {
            var startupAssembly = typeof(TStartup).GetTypeInfo().Assembly;
            var contentRoot = GetProjectPath(relativeTargetProjectParentDir, startupAssembly);

            var configurationBuilder = new ConfigurationBuilder()
                .SetBasePath(contentRoot)
                .AddJsonFile("appsettings.json");

            var webHostBuilder = new WebHostBuilder()
                .UseContentRoot(contentRoot)
                .ConfigureServices(InitializeServices)
                .UseConfiguration(configurationBuilder.Build())
                .UseEnvironment("Development")
                .UseStartup(typeof(TStartup));

            Server = new TestServer(webHostBuilder);

            Client = Server.CreateClient();
            Client.BaseAddress = new Uri("http://localhost:1234");
            Client.DefaultRequestHeaders.Accept.Clear();
            Client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        }

        public void Dispose()
        {
            Client.Dispose();
            Server.Dispose();
        }

        public HttpClient Client { get; }

        protected virtual void InitializeServices(IServiceCollection services)
        {
            var startupAssembly = typeof(TStartup).GetTypeInfo().Assembly;

            var manager = new ApplicationPartManager();

            manager.ApplicationParts.Add(new AssemblyPart(startupAssembly));
            manager.FeatureProviders.Add(new ControllerFeatureProvider());
            manager.FeatureProviders.Add(new ViewComponentFeatureProvider());

            services.AddSingleton(manager);
        }
    }
}

现在,这是SalesTests类的代码:

using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using OnLineStore.WebAPI.IntegrationTests.Helpers;
using Xunit;

namespace OnLineStore.WebAPI.IntegrationTests
{
    public class SalesTests : IClassFixture<TestFixture<Startup>>
    {
        private HttpClient Client;

        public SalesTests(TestFixture<Startup> fixture)
        {
            Client = fixture.Client;
        }

        [Fact]
        public async Task TestGetOrdersAsync()
        {
            // Arrange
            var request = "/api/v1/Sales/Order";

            // Act
            var response = await Client.GetAsync(request);

            // Assert
            response.EnsureSuccessStatusCode();
        }

        [Fact]
        public async Task TestGetOrdersByCurrencyAsync()
        {
            // Arrange
            var currencyID = (short)1;
            var request = string.Format("/api/v1/Sales/Order?currencyID={0}", currencyID);

            // Act
            var response = await Client.GetAsync(request);

            // Assert
            response.EnsureSuccessStatusCode();
        }

        [Fact]
        public async Task TestGetOrdersByCustomerAsync()
        {
            // Arrange
            var customerID = 1;
            var request = string.Format("/api/v1/Sales/Order?customerID={0}", customerID);

            // Act
            var response = await Client.GetAsync(request);

            // Assert
            response.EnsureSuccessStatusCode();
        }

        [Fact]
        public async Task TestGetOrdersByEmployeeAsync()
        {
            // Arrange
            var employeeID = 1;
            var request = string.Format("/api/v1/Sales/Order?employeeID={0}", employeeID);

            // Act
            var response = await Client.GetAsync(request);

            // Assert
            response.EnsureSuccessStatusCode();
        }

        [Fact]
        public async Task TestGetOrderByIdAsync()
        {
            // Arrange
            var id = 1;
            var request = string.Format("/api/v1/Sales/Order/{0}", id);

            // Act
            var response = await Client.GetAsync(request);

            // Assert
            response.EnsureSuccessStatusCode();
        }

        [Fact]
        public async Task TestGetOrderByNonExistingIdAsync()
        {
            // Arrange
            var id = 0;
            var request = string.Format("/api/v1/Sales/Order/{0}", id);

            // Act
            var response = await Client.GetAsync(request);

            // Assert
            Assert.True(response.StatusCode == HttpStatusCode.NotFound);
        }

        [Fact]
        public async Task TestGetCreateOrderRequestAsync()
        {
            // Arrange
            var request = "/api/v1/Sales/CreateOrderRequest";

            // Act
            var response = await Client.GetAsync(request);

            // Assert
            response.EnsureSuccessStatusCode();
        }

        [Fact]
        public async Task TestCreateOrderAsync()
        {
            // Arrange
            var request = "/api/v1/Sales/Order";
            var model = new
            {
                CustomerID = 1,
                PaymentMethodID = new Guid("7671A4F7-A735-4CB7-AAB4-CF47AE20171D"),
                Comments = "Order from integration tests",
                CreationUser = "integrationtests",
                Details = new[]
                {
                    new
                    {
                        ProductID = 1,
                        Quantity = 1
                    }
                }
            };

            // Act
            var response = await Client.PostAsync(request, ContentHelper.GetStringContent(model));

            // Assert
            response.EnsureSuccessStatusCode();
        }

        [Fact]
        public async Task TestCloneOrderAsync()
        {
            // Arrange
            var id = 1;
            var request = string.Format("/api/v1/Sales/CloneOrder/{0}", id);

            // Act
            var response = await Client.GetAsync(request);

            // Assert
            response.EnsureSuccessStatusCode();
        }
    }
}

不要忘记我们可以有更多的测试,我们有名称为ProductionTests的类来为ProductionController执行请求。

 

原文地址:https://www.codeproject.com/Articles/1160586/Entity-Framework-Core-for-Enterprise#chapter08

猜你喜欢

转载自blog.csdn.net/mzl87/article/details/86514577