65.9K
CodeProject 正在变化。 阅读更多。
Home

在 ASP.NET Core 3.1 Web API 和 EF Core 5.0 中使用通用 Repository 和 UoW 模式实现领域驱动设计

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (16投票s)

2021年3月5日

CPOL

20分钟阅读

viewsIcon

45309

理解和实现领域驱动设计实现方法

引言

阅读本文将使您充分了解如何在 ASP.NET Core 3.1 Web API 和 Entity Framework Core 5.0 中使用领域驱动设计实现方法、通用 Repository 和工作单元模式构建 ASP.NET Core Web API 应用程序。

我将介绍用于数据访问层的通用 Repository 和工作单元模式,然后我将使用领域驱动设计 (DDD) 实现方法开发一个 ASP.NET Web API 应用程序,用于以清晰、可测试和可维护的架构注册客户订单和订单项。

背景

您需要了解面向对象编程、领域驱动设计 (DDD) 方法、Microsoft ORM Entity Framework、单元测试以及 Robert C. Martin 提出的面向对象设计 SOLID 原则的基础知识。

此外,要简要了解 SOLID 原则,您只需在 Google 中输入几个词,例如:“面向对象设计的 SOLID 原则”。

领域驱动设计

领域驱动设计 (DDD) 一词由 Eric Evans 在他 2004 年的书中提出。领域驱动设计是一个很大的话题,在本文中,我们只希望对其有一个大致的了解,并希望专注于领域驱动设计的实现细节,此外,我们还将编写一个简单的购物教程应用程序,我们的主要目标是将应用程序业务逻辑安全地保存在领域模型(富实体)和领域服务中。在本文的示例购物教程应用程序中,我们将专注于领域和领域逻辑,而不必担心数据持久化。

正如 Martin Fowler 所说:

领域驱动设计是一种软件开发方法,它以对领域流程和规则有深入理解的领域模型编程为中心”。

我们将研究领域元素(构建块),例如实体、值对象、聚合根实体、数据传输对象 (DTO)、服务和 Repository。我们将研究可用于实现工作的软件设计原则、框架和工具。实现 DDD 最著名的架构是洋葱架构,如下图所示:应用程序核心(领域模型和领域服务)是业务逻辑所在的地方,它不依赖于任何数据持久化工具或任何技术,因此我们可以说,我们的应用程序核心领域独立于外部环境和外部依赖项,这使我们遵循(可测试和敏捷的架构原则),因为我们的应用程序核心业务逻辑不依赖于应用程序的其他部分,如数据库等,因此可以轻松地进行隔离测试和调试,这非常棒。

因此,我们将来可以在应用程序的任何其他部分(如:存储库、数据库、ORM、UI 等)中更改任何我们想要的东西,并且整个应用程序应该在内部层(核心领域)中以最小或无需更改的情况下正常工作。

我不想过多地谈论这种方法,而是将重点放在领域驱动方法的实现细节上,因为互联网上有很多关于这个主题的理论信息。因此,如果您想了解更多关于这种开发方法的信息,请在 Google 中输入几个词,例如:“领域驱动设计实现方法”。

仓储模式

Repository 是领域驱动设计 (DDD) 的实现元素或构建块之一。Repository 模式为我们提供了一种清晰、可测试和可维护的方法来访问和操作应用程序中的数据。正如 Martin Fowler 在他的书《企业应用程序架构模式》中所说:“Repository 介于领域和数据映射层之间,充当内存中的领域对象集合,将业务实体与底层数据基础设施隔离”。Repository 模式提供了一个接口并提供添加、删除和检索领域实体的方法,使领域实体能够保持与底层数据持久化层无关(持久化无关),鼓励松散耦合编程并允许独立扩展应用程序层,这使我们能够隔离地测试业务逻辑与外部依赖项(可测试和敏捷的架构原则)。此外,Repository 模式使我们能够集中和封装查询,以便在应用程序的其他部分重用它(DRY 原则)。

正如您在下图中看到的,应用程序逻辑依赖于 Customer Repository 接口,而不是 Repository 的具体实现,以实现更高的抽象(依赖反转原则),这意味着应用程序逻辑完全不了解 Customer Repository 的任何实现和数据访问问题,从而保持应用程序逻辑代码完好无损,并避免将来数据访问更改,确保它不易碎且易于扩展。

因此,如果我们想模拟 Customer Repository 的实现并通过 DI(依赖注入器)工具将其注入到应用程序逻辑类(领域服务或控制器)中,以隔离测试应用程序逻辑,从而为我们提供单元测试的机会,而无需担心数据访问逻辑和实现(可测试架构原则),那将是可行的。

工作单元模式

工作单元设计模式将多个业务对象更改的数据持久化操作协调为一个原子事务,这保证整个事务将提交或回滚。正如您在下图中看到的,工作单元设计模式封装了多个 Repository 并在它们之间共享单个数据库上下文。

有关工作单元设计模式的更多信息,您只需在 Google 中输入几个词,例如:“martin fowler unit of work”。

Using the Code

创建空白解决方案和解决方案架构

我们首先通过打开 Visual Studio 2019,在右侧菜单中选择“创建新项目”,选择空白解决方案并将其命名为 DomainDrivenTutorial 来启动我们的应用程序,如下图所示。然后我们在解决方案中添加 Framework.Core 文件夹。

添加和实现应用程序共享内核库

接下来,我们在 Framework.Core 文件夹中添加 .NET Core 类库,并将其命名为 Shared Kernel,如下图所示

Shared Kernel 库是我们放置公共模型和辅助类的地方,这些类将在整个解决方案中的所有库之间使用。我们在其中创建一个 Models 文件夹,然后添加 PageParam 类以创建数据分页请求模型,该模型稍后将在 Repository 和应用程序服务层之间使用。

PageParam.cs

public class PageParam
{
        const int maxPageSize = 50;
 
        private int _pageSize = 10;
 
        public int PageSize
        {
          get
          {
                return _pageSize;
          }
          set
          {
            _pageSize = (value > maxPageSize) ? maxPageSize : value;
          }
        }
 
        public int PageNumber { get; set; } = 1;
}

在 Entity Framework Core 上实现通用 Repository 模式

下一步是在 Framework.Core 文件夹中添加下一个 .NET Core 类库,并将其命名为 GenericRepositoryEntityFramework。此库将只包含三个 .CS 文件、Repository 接口、Repository 实现和 SortingExtension 静态辅助类,以便按升序或降序对从数据库中获取的记录进行排序。

因此,在继续之前,我们必须为 Entity Framework Core 添加 NuGet 包,如下图所示。右键单击应用程序解决方案名称,然后单击管理解决方案的 NuGet 包。

在 NuGet 表单中,单击“浏览”选项卡,并在搜索条件筛选文本框中输入文本“EntityFrameworkCore”,如下图所示

选择 Microsoft.EntityFrameworkCore,并在右侧部分选择 Generic Repository Entity Framework 项目,然后单击“安装”按钮,如下图所示

对 NuGet 包 Microsoft.EntityFrameworkCore.SqlServerMicrosoft.EntityFrameworkCore.Tools 重复此操作。

好的,将 NuGet 包添加到项目已完成。如下所示,这是一个使用 Entity Framework Core 5.0 实现 Repository 模式的示例代码。

要访问完整的源代码,您可以从 Github 下载。

IRepository.cs

using SharedKernel.Models;
 
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using System.Collections.Generic; 
 
namespace GenericRepositoryEntityFramework
{
    public interface IRepository<TEntity> where TEntity : IAggregateRoot
    {
        void Add(TEntity entity);
 
        void Remove(TEntity entity);
 
        void Update(TEntity entity);
 
        Task<TEntity> GetByIdAsync(object id);
 
        Task<IEnumerable<TEntity>> GetAllAsync();
 
        Task<IEnumerable<TEntity>> GetAllAsync<TProperty>
        (Expression<Func<TEntity, TProperty>> include);
 
        Task<TEntity> SingleOrDefaultAsync(Expression<Func<TEntity, bool>> predicate);
 
        Task<QueryResult<TEntity>> GetPageAsync(QueryObjectParams queryObjectParams);
 
        Task<QueryResult<TEntity>> GetPageAsync(QueryObjectParams queryObjectParams, 
                                   Expression<Func<TEntity, bool>> predicate);
 
        Task<QueryResult<TEntity>> GetOrderedPageQueryResultAsync
        (QueryObjectParams queryObjectParams, IQueryable<TEntity> query);       
    } 
}

请注意,我故意将 DbContext 定义为 Repository 实现类中的受保护属性,因为我希望在派生 Repository 类中使用它,我希望让派生 Repository 类能够进行灵活而丰富的查询,这肯定会是派生类和基本通用 Repository 类之间的“is-a”关系。

Repository.cs

using Microsoft.EntityFrameworkCore;
using SharedKernel.Models;
 
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using System.Collections.Generic;
 
namespace GenericRepositoryEntityFramework
{
    public class Repository<TEntity> : IRepository<TEntity> where TEntity : class, IAggregateRoot
    {
        protected readonly DbContext Context; 
 
        private readonly DbSet<TEntity> _dbSet; 
 
        public Repository(DbContext context)
        {
            Context = context;
 
            if (context != null)
            {
                _dbSet = context.Set<TEntity>();
            }
        } 
 
        public virtual void Add(TEntity entity)
        {
            _dbSet.Add(entity);
        }
 
        public virtual void Remove(TEntity entity)
        {
            _dbSet.Remove(entity);
        }
 
        public virtual void Update(TEntity entity)
        {
            _dbSet.Update(entity);
        }
 
        public async Task<TEntity> GetByIdAsync(object id)
        {
            return await _dbSet.FindAsync(id).ConfigureAwait(false);
        }
 
        public async Task<IEnumerable<TEntity>> GetAllAsync()
        {
            return await _dbSet.ToListAsync().ConfigureAwait(false);
        }
 
        public async Task<IEnumerable<TEntity>> 
        GetAllAsync<TProperty>(Expression<Func<TEntity, TProperty>> include)
        {
            IQueryable<TEntity> query = _dbSet.Include(include);
 
            return await query.ToListAsync().ConfigureAwait(false);
        }
 
        public async Task<TEntity> 
        SingleOrDefaultAsync(Expression<Func<TEntity, bool>> predicate)
        {
            return await _dbSet.SingleOrDefaultAsync(predicate).ConfigureAwait(false);
        }
 
        public virtual async Task<QueryResult<TEntity>> 
        GetPageAsync(QueryObjectParams queryObjectParams)
        {
            return await GetOrderedPageQueryResultAsync
                   (queryObjectParams, _dbSet).ConfigureAwait(false);
        } 
 
        public virtual async Task<QueryResult<TEntity>> 
        GetPageAsync(QueryObjectParams queryObjectParams, 
        Expression<Func<TEntity, bool>> predicate)
        {
            IQueryable<TEntity> query = _dbSet;
 
            if (predicate != null)
                query = query.Where(predicate);
 
            return await GetOrderedPageQueryResultAsync
                         (queryObjectParams, query).ConfigureAwait(false);
        } 
 
        public async Task<QueryResult<TEntity>> 
        GetOrderedPageQueryResultAsync
        (QueryObjectParams queryObjectParams, IQueryable<TEntity> query)
        {
            IQueryable<TEntity> OrderedQuery = query;
 
            if (queryObjectParams.SortingParams != null && 
                queryObjectParams.SortingParams.Count > 0)
            {
                OrderedQuery = SortingExtension.GetOrdering
                               (query, queryObjectParams.SortingParams);
            }
 
            var totalCount = await query.CountAsync().ConfigureAwait(false);
 
            if (OrderedQuery != null)
            {
                var fecthedItems = 
                await GetPagePrivateQuery
                (OrderedQuery, queryObjectParams).ToListAsync().ConfigureAwait(false);
 
                return new QueryResult<TEntity>(fecthedItems, totalCount);
            }
 
            return new QueryResult<TEntity>(await GetPagePrivateQuery
            (_dbSet, queryObjectParams).ToListAsync().ConfigureAwait(false), totalCount);
        } 
 
        private IQueryable<TEntity> GetPagePrivateQuery
        (IQueryable<TEntity> query, QueryObjectParams queryObjectParams)
        {
            return query.Skip((queryObjectParams.PageNumber - 1) * 
            queryObjectParams.PageSize).Take(queryObjectParams.PageSize);
        }
    } 
}

构建一个简单的领域驱动 ASP.NET Core Web API 应用程序

现在让我们构建一个简单的 ASP.NET Core Web API 应用程序,以便更好地理解领域驱动设计实现方法。

我们要做的是通过 Entity Framework Core code first 方法创建两个领域实体,OrderOrder Items 实体,并利用领域驱动设计方法、聚合根和 Repository 模式对它们执行 CRUD 操作,以便将应用程序逻辑封装在领域模型和领域服务中,等等……

因此,首先,我们将创建一个类库,将应用程序持久化和业务逻辑与 ASP.NET Core Web API 分离,并将我们的应用程序逻辑放在领域模型和领域服务中,然后我们将添加两个领域实体,OrderOrder Items 实体。说得够多了,让我们开始示例项目。

右键单击解决方案名称并创建一个新文件夹,将其命名为 EShoppingTutorial,然后通过转到“文件”->“新建”->“项目”添加一个 .NET Core 类库项目,并将其命名为 EShoppingTutorial.Core,如下图所示

接下来,我们想定义应用程序解决方案结构,将文件分离到诸如:DomainEntitiesServicesPersistenceRepositories 等文件夹中,以便将数据持久化关注点的应用程序业务逻辑分离,以获得更好、更清晰的应用程序架构,如下图所示

我希望您在编码之前仔细查看应用程序解决方案结构,我们已经将文件夹分开了,接下来我们将向它们添加一些类,这可以使我们拥有更好的应用程序架构(干净架构原则)、更好的重构和维护。一开始,我们决定将领域实体和领域服务文件夹以及边界与持久层分开,我们的目的是将将来可能更改的事物(例如:数据持久逻辑、ORM 版本或数据库)与几乎不更改或从不更改的事物分开,或者它们是不变或持久化无关的,例如:领域不变性、领域服务或值对象,正如我在本文开头领域驱动设计部分所讨论的那样。

因此,在继续之前,最好为 Entity Framework Core 添加 NuGet 包。如下图所示。右键单击应用程序解决方案名称,然后单击管理解决方案的 NuGet 包。

在 NuGet 表单中,单击“浏览”选项卡,并在搜索条件筛选文本框中输入文本“EntityFrameworkCore”,如下图所示

选择 Microsoft.EntityFrameworkCore 并在右侧部分选择 EShoppingTutorial.Core 项目,然后单击“安装”按钮,如下图所示。对 NuGet 包 Microsoft.EntityFrameworkCore.SqlServerMicrosoft.EntityFrameworkCore.Tools 重复此操作。

此外,我们必须添加通用 Repository Entity Framework 和 Shared Kernel 项目库的项目引用。

添加应用程序领域模型

因此,对于下一步,我们在 Entities 文件夹中添加了两个领域模型(实体类),名为 OrderOrder Items,在 ValueObjects 文件夹中添加了一个值对象,名为 Price,在 Enums 文件夹中添加了一个枚举,名为 MoneyUnit,如下面的代码所示

MoneyUnit.cs

namespace EShoppingTutorial.Core.Domain.Enums
{
    public enum MoneyUnit : int
    {
        UnSpecified = 0,
        Rial = 1,
        Dollar,
        Euro
    }
}

添加价格值对象

下一步,我们在 ValueObjects 文件夹中添加一个名为 Price 的值对象和一个名为 MoneySymbols 的辅助类,我们应该封装与 Price 值对象相关的业务逻辑。

MoneySymbols.cs

using System.Collections.Generic;
using EShoppingTutorial.Core.Domain.Enums;
 
namespace EShoppingTutorial.Core.Domain.ValueObjects
{
    public static class MoneySymbols
    {
        private static Dictionary<MoneyUnit, string> _symbols;
 
        static MoneySymbols()
        {
            if (_symbols != null)
                return;
 
            _symbols = new Dictionary<MoneyUnit, string>
            {
                { MoneyUnit.UnSpecified, string.Empty },
 
                { MoneyUnit.Dollar, "$" },
 
                { MoneyUnit.Euro, "€" },
 
                { MoneyUnit.Rial, "Rial" },
            };
        }
 
        public static string GetSymbol(MoneyUnit moneyUnit)
        {
            return _symbols[moneyUnit].ToString();
        }
    }
}

Price.cs

using EShoppingTutorial.Core.Domain.Enums;
using SharedKernel.Exceptions;
using System.ComponentModel.DataAnnotations.Schema;

namespace EShoppingTutorial.Core.Domain.ValueObjects
{
    [ComplexType]
    public class Price
    {
        protected Price() // For Entity Framework Core
        {

        }

        public Price(int amount, MoneyUnit unit)
        {
            if (MoneyUnit.UnSpecified == unit)
                throw new BusinessRuleBrokenException("You must supply a valid money unit!");

            Amount = amount;

            Unit = unit;
        }


        public int Amount { get; protected set; }


        public MoneyUnit Unit { get; protected set; } = MoneyUnit.UnSpecified;


        public bool HasValue
        {
            get
            {
                return (Unit != MoneyUnit.UnSpecified);
            }
        }


        public override string ToString()
        {
            return 
                Unit != MoneyUnit.UnSpecified ? 
                Amount + " " + MoneySymbols.GetSymbol(Unit) : 
                Amount.ToString();
        }
    }
}

添加订单项实体模型

下一步,我们在 Entities 文件夹中添加一个名为 OrderItem 的领域模型,它将保存每个订单项的数据。

OrderItem.cs

using EShoppingTutorial.Core.Domain.ValueObjects;
using SharedKernel.Exceptions;

namespace EShoppingTutorial.Core.Domain.Entities
{
    public class OrderItem
    {
        public int Id { get; protected set; }

        public int ProductId { get; protected set; }

        public Price Price { get; protected set; }

        public int OrderId { get; protected set; }


        protected OrderItem() // For Entity Framework Core
        {
            
        }

        public OrderItem(int productId, Price price)
        {
            ProductId = productId;

            Price = price;

            CheckForBrokenRules();
        }

        private void CheckForBrokenRules()
        {
            if (ProductId == 0)
                throw new BusinessRuleBrokenException("You must supply valid Product!");

            if (Price is null)
                throw new BusinessRuleBrokenException("You must supply an Order Item!");
        }
    }
}

添加订单实体模型

因此,最后,我们将在 Entities 文件夹中添加一个名为 Order 的领域模型,它也将充当聚合根实体。正如您在下面的代码中看到的,它不是一个普通的弱实体模型,仅用于 CRUD 操作!它是一个富领域模型,将数据和逻辑结合在一起。它具有属性和行为。它应用了封装和信息隐藏,如下所示,与 Order Items 实体存在单向关系,访问应用程序中 OrderItem 数据的唯一方法将通过此聚合根富实体模型。

正如您在下面看到的,OrderItem 属性是一个只读集合,因此我们无法通过此只读属性从外部添加订单项,添加订单项的唯一方法将通过 Order 模型类的构造函数。因此,此类将隐藏和封装 OrderItem 的数据和相关的业务规则,并将履行其作为聚合根实体的职责。您可以轻松地在 Google 中搜索并阅读大量关于聚合根实体的信息。

Order.cs

using System;
using System.Linq;
using System.Collections.Generic;
using SharedKernel.Exceptions;
using SharedKernel.Models;

namespace EShoppingTutorial.Core.Domain.Entities
{
    public class Order : IAggregateRoot
    {
        public int Id { get; protected set; }

        public Guid? TrackingNumber { get; protected set; }

        public string ShippingAdress { get; protected set; }

        public DateTime OrderDate { get; protected set; }


        private List<OrderItem> _orderItems;
        public ICollection<OrderItem> OrderItems { get { return _orderItems.AsReadOnly(); } }


        protected Order() // For Entity Framework Core
        {
            _orderItems = new List<OrderItem>();
        }


        /// <summary>
        /// Throws Exception if Maximum price has been reached, or if no Order Item has been added to this Order
        /// </summary>
        /// <param name="orderItems"></param>
        public Order(string shippingAdress, IEnumerable<OrderItem> orderItems) : this()
        {
            CheckForBrokenRules(shippingAdress, orderItems);

            AddOrderItems(orderItems);


            ShippingAdress = shippingAdress;

            TrackingNumber = Guid.NewGuid();

            OrderDate = DateTime.Now;
        }

        private void CheckForBrokenRules(string shippingAdress, IEnumerable<OrderItem> orderItems)
        {
            if (string.IsNullOrWhiteSpace(shippingAdress))
                throw new BusinessRuleBrokenException("You must supply ShippingAdress!");

            if (orderItems is null || (!orderItems.Any()))
                throw new BusinessRuleBrokenException("You must supply an Order Item!");
        }

        private void AddOrderItems(IEnumerable<OrderItem> orderItems)
        {
            var maximumPriceLimit = MaximumPriceLimits.GetMaximumPriceLimit(orderItems.First().Price.Unit);

            foreach (var orderItem in orderItems)
                AddOrderItem(orderItem, maximumPriceLimit);
        }



        /// <summary>
        /// Throws Exception if Maximum price has been reached
        /// </summary>
        /// <param name="orderItem"></param>
        private void AddOrderItem(OrderItem orderItem, int maximumPriceLimit)
        {
            var sumPriceOfOrderItems = _orderItems.Sum(en => en.Price.Amount);

            if (sumPriceOfOrderItems + orderItem.Price.Amount > maximumPriceLimit)
            {
                throw new BusinessRuleBrokenException("Maximum price has been reached !");
            }

            _orderItems.Add(orderItem);
        }

    }
}

Order 实体模型会检查一些业务规则,如果任何业务规则被破坏,则会引发 BusinessRuleBrokenException(在 Shared Kernel Library 中定义的自定义异常)。

此外,它是一个纯 .NET 对象(POCO 类)并且是持久化无关的 (PI)。因此,无需连接任何外部服务或数据库存储库,我们就可以轻松地编写单元测试以检查模型的行为(业务规则)。这太棒了。那么,让我们一起为 Order 实体的业务规则编写一些单元测试。

添加单元测试项目并为订单实体编写一个简单的单元测试

因此,右键单击解决方案资源管理器,然后选择“添加”->“新项目”。

然后选择您要使用的模板项目,对于此应用程序,您可以选择 NUnit 测试或单元测试项目,然后选择“下一步”。

因此,为您的项目输入一个名称,例如“EShoppingTutorial.UnitTests”,然后选择“创建”,如下图所示

向此项目添加通用 Repository Entity Framework 和 Shared Kernel 项目库的项目引用。

创建文件夹:DomainEntitiesRepositories,并在 Entities 文件夹中添加一个名为 OrderShould 的新类,如下图所示

OrderShould 类将包含订单实体的单元测试。

OrderShould.cs

using NUnit.Framework;
using SharedKernel.Exceptions;
using EShoppingTutorial.Core.Domain.Entities;

using System;
using EShoppingTutorial.Core.Domain.ValueObjects;
using EShoppingTutorial.Core.Domain.Enums;

namespace EShoppingTutorial.UnitTests.Domain.Entities
{
    public class OrderShould
    {
        [Test]
        public void Test_InstantiatingOrder_WithEmptyOrderItems_ExpectsBusinessRuleBrokenException()
        {
            // act
            TestDelegate testDelegate = () => new Order("IRAN", new OrderItem[] { });


            // assert
            var ex = Assert.Throws<BusinessRuleBrokenException>(testDelegate);
        }


        [Test]
        public void Test_OrderItemsProperty_AddingOrderItemToReadOnlyCollection_ExpectsNotSupportedException()
        {
            // arrange
            var order = new Order("IRAN", new OrderItem[] { new OrderItem(1, new Price(1, MoneyUnit.Dollar)) });


            // act
            TestDelegate testDelegate = () => order.OrderItems.Add(new OrderItem(1, new Price(1, MoneyUnit.Dollar)));


            // assert
            var ex = Assert.Throws<NotSupportedException>(testDelegate);
        }


        [Test]
        public void Test_InstantiateOrder_WithOrderItems_ThatExccedsTotalPriceOf_10000_Dollar_ExpectsBusinessRuleBrokenException()
        {
            // arrange

            var orderItem1 = new OrderItem(1, new Price (5000, MoneyUnit.Dollar));

            var orderItem2 = new OrderItem(2, new Price(6000, MoneyUnit.Dollar));

            // act
            TestDelegate testDelegate = () =>
            {
                new Order("IRAN",new OrderItem[] { orderItem1, orderItem2 });
            };


            // assert
            var ex = Assert.Throws<BusinessRuleBrokenException>(testDelegate);

            Assert.That(ex.Message.ToLower().Contains("maximum price"));
        }


        [Test]
        public void Test_InstantiateOrder_WithOrderItems_ThatExccedsTotalPriceOf_9000_Euro_ExpectsBusinessRuleBrokenException()
        {
            // arrange

            var orderItem1 = new OrderItem(1, new Price(5000, MoneyUnit.Dollar));

            var orderItem2 = new OrderItem(2, new Price(6000, MoneyUnit.Dollar));

            // act
            TestDelegate testDelegate = () =>
            {
                new Order("IRAN", new OrderItem[] { orderItem1, orderItem2 });
            };


            // assert
            var ex = Assert.Throws<BusinessRuleBrokenException>(testDelegate);

            Assert.That(ex.Message.ToLower().Contains("maximum price"));
        }

    }
}

添加 Entity Framework DbContext 和数据库迁移

这太好了;我们能够轻松地为订单实体业务规则和不变式编写单元测试,而无需连接到任何外部数据库。所以现在,我们将使用 Microsoft Entity Framework Core (ORM) code first Fluent API 方法和 add-migration 命令生成数据库和表创建脚本。我们来做吧。

首先,我们应该配置 C# 实体类到数据库表的映射,如下面的代码所示。我们在 Mappings 文件夹中添加映射类文件 OrderMapConfigOrderItemMapConfig,然后放置实体映射配置。

OrderMapConfig.cs

using EShoppingTutorial.Core.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
 
namespace EShoppingTutorial.Core.Persistence.Mappings
{
    public class OrderMapConfig : IEntityTypeConfiguration<Order>
    {
        public void Configure(EntityTypeBuilder<Order> builder)
        {
            builder.ToTable("Orders");
 
            builder.HasKey(o => o.Id);
 
            builder.Property(o => o.Id).ValueGeneratedOnAdd().HasColumnName("Id");
 
            builder.Property
            (en => en.TrackingNumber).HasColumnName("TrackingNumber").IsRequired(false);
 
            builder.HasIndex(en => en.TrackingNumber).IsUnique();
 
            builder.Property
            (en => en.ShippingAdress).HasColumnName
            ("ShippingAdress").HasMaxLength(100).IsUnicode().IsRequired();
 
            builder.Property
            (en => en.OrderDate).HasColumnName("OrderDate").HasMaxLength(10).IsRequired();
        }
    }
}

OrderItemMapConfig.cs

using EShoppingTutorial.Core.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
 
namespace EShoppingTutorial.Core.Persistence.Mappings
{
    public class OrderItemMapConfig : IEntityTypeConfiguration<OrderItem>
    {
        public void Configure(EntityTypeBuilder<OrderItem> builder)
        {
            builder.ToTable("OrderItems");
 
            builder.HasKey(o => o.Id);
 
            builder.Property(o => o.Id).ValueGeneratedOnAdd().HasColumnName("Id");
 
            builder.Property(en => en.ProductId).HasColumnName("ProductId").IsRequired();
 
            builder.OwnsOne(en => en.Price, price =>
            {
                price.Property(x => x.Amount).HasColumnName("Amount");
 
                price.Property(x => x.Unit).HasColumnName("Unit");
            });
        }
    }
}

然后我们在 Persistence 文件夹中添加一个 DbContext 类文件,命名为“EShoppingTutorialDbContext”,然后我们将这些映射应用于我们的 DbContext 类,如下面的代码所示。为了简化我们的工作,我使用了“ApplyConfigurationsFromAssembly”命令来应用实体映射。

EShoppingTutorialDbContext.cs

using EShoppingTutorial.Core.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using System.Reflection;
 
namespace EShoppingTutorial.Core.Persistence
{
    public class EShoppingTutorialDbContext : DbContext
    {
        public virtual DbSet<Order> Orders { get; set; }
 
        public EShoppingTutorialDbContext
        (DbContextOptions<EShoppingTutorialDbContext> dbContextOptions) 
            : base(dbContextOptions)
        {
 
        }
 
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            //Scans a given assembly for all types that implement 
            //IEntityTypeConfiguration, and registers each one automatically
            modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
 
            base.OnModelCreating(modelBuilder);
        }
    }
}

Microsoft Entity Framework Code-First 也会根据约定创建数据库表和字段,其名称与 DbContext 类中的 DbSet 属性相同,那么我们为什么要分离映射文件并做额外的工作呢?好问题,不是吗?设想我们想重构和更改实体名称和属性,如果没有分离的映射文件,我们必须运行 add-migration 命令,并在数据库中应用这些更改,否则 .NET Framework 将给我们一个错误,那么如果我们不想这样做会发生什么?也许我们想要不同的实体和属性名称与数据库名称,等等。

因此,拥有分离的实体映射文件为我们提供了很多机会,它也使我们能够遵循干净架构原则,以便将来更好地重构和调试。因此,在软件设计中,永远不要忘记将可能更改的事物与不会更改的事物分开。但也要记住,一刀切不适用于所有情况。如果您确实有一个简单的、数据驱动的应用程序,业务逻辑最少,并且您确信它将来很少更改,那么就不要这样做,只需保持简单(KISS 原则)。

好的,通过代码优先方法生成数据库和表的下一步是添加数据库连接字符串,为此,我们首先要在 EShoppingTutorial 文件夹中向解决方案添加一个 ASP.NET Core Web API 项目,然后我们将连接字符串添加到 Web API 项目中,如下图所示

在下一个表单中,选择 API 模板,并取消勾选“为 Https 配置”,如下图所示

下一步,我们添加 Microsoft Entity Framework Core 和 Microsoft EntityFrameworkCore Design 的 Nuget 包,以及 EShoppingTutorial.CoreSharedKernel 的项目引用,就像我们之前在本文中做的那样。

最后,您自己准备好适当的数据库服务器,然后将连接字符串添加到 Web API 项目的 appsettings.json 中,最后将 EShoppingTutorialDbContext 添加到 Startup.cs 中,并使用 appsettings 中指定的连接字符串,如下面的代码所示。

appsettings.json

{
  "ConnectionStrings": {
    "EShoppingTutorialDB": "Data Source=localhost\\SQLEXPRESS;
    Initial Catalog=EShoppingTutorialDB;Integrated Security=True;Pooling=False"
  },
 
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

Startup 类的 ConfigureServices 方法中添加连接字符串并通过 AddDbContext 命令注入 EShoppingTutorialDbContext

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
 
    services.AddDbContext<EShoppingTutorialDbContext>
    (opts => opts.UseSqlServer(Configuration["ConnectionStrings:EShoppingTutorialDB"]));
}

好的,到目前为止一切顺利。所以如果我们一切都做对了,那么我们可以打开包管理器控制台并运行 add-migration 命令,但在此之前,请确保您已将 EShoppingTutorial Web API 项目设置为启动项目。

打开包管理器控制台,并在默认项目菜单中,确保您已选择 EShoppingTutorial.Core,然后运行 add-migration 命令。Entity Framework 会要求您提供一个名称,给它一个有意义的名称,例如:“Adding Order and OrderItems tables”,如下图所示

好的,在收到 Entity Framework 的成功消息后,运行 update-database 命令,如下图所示,否则您必须阅读错误并检查您遗漏了什么。

在收到 update-database 命令的成功消息后,去检查数据库,一个名为 EShoppingTutorialDB 的数据库,其中包含两个表,必须按照我们之前在实体映射配置中配置的那样创建,如下图所示

添加应用程序的其他部分

好的,我们已经成功创建了数据库和表,所以让我们继续完成应用程序的其他部分,例如:工作单元、订单存储库和 Web API 控制器。

我们在 Domain -> Repositories 文件夹中添加一个 IOrderRepository 接口,它将继承自通用 IRepository 接口,如下面的代码所示。

IOrderRepository.cs

using EShoppingTutorial.Core.Domain.Entities;
using GenericRepositoryEntityFramework;
 
namespace EShoppingTutorial.Core.Domain.Repositories
{
    public interface IOrderRepository : IRepository<Order>
    {
 
    }
}

请注意,应用程序核心域不会添加任何存储库实现。正如我在本文开头 DDD 部分所讨论的,应用程序域将保持纯粹且与持久性无关,我们只会在应用程序域文件夹中添加存储库接口,如下图所示,存储库实现与应用程序核心域是分离的。

正如您在下图中看到的,应用程序核心领域逻辑依赖于 Repository 接口而不是 Repository 的具体实现,以实现更高的抽象,这意味着应用程序逻辑完全不了解 Repository 的任何实现和数据访问问题。

现在我们将在 Persistence -> Repositories 文件夹中添加 OrderRepository 实现,它将继承自通用 Repository 类,如下面的代码所示。在 Order Repository 类中,我们可以重写基虚拟方法或添加新的自定义方法。例如,我重写了基 Repository 的 Add 方法以更改其行为,添加一个新订单。此外,我们将可以访问 EShoppingTutorial DbContext

OrderRepository.cs

using System;
using GenericRepositoryEntityFramework;
using EShoppingTutorial.Core.Domain.Entities;
using EShoppingTutorial.Core.Domain.Repositories;
 
namespace EShoppingTutorial.Core.Persistence.Repositories
{
    public class OrderRepository : Repository<Order>, IOrderRepository
    {
        public OrderRepository(EShoppingTutorialDbContext context) : base(context)
        {
            
        }
 
        public EShoppingTutorialDbContext EShoppingTutorialDbContext
        {
            get { return Context as EShoppingTutorialDbContext; }
        }
 
        public override void Add(Order entity)
        {
            // We can override repository virtual methods in order to customize repository behavior, Template Method Pattern
            // Code here

            base.Add(entity);
        } 
    }
}

为 OrderRepository Add 方法编写简单的单元测试

好的,订单存储库实现已经完成,现在让我们为订单存储库的添加和获取方法编写一些简单的单元测试。

所以首先,我们必须为单元测试模拟 Entity Framework 的 DbContext(模拟是我们进行单元测试时,如果我们的测试目标有外部依赖,如数据库或其他外部服务,所采用的一种方法,欲了解更多信息,您可以在互联网上搜索和阅读,有很多相关信息)。

因此,为了模拟 EShoppingTutorial DbContext,我们应该添加 Entity Framework Core InMemory 数据库和 Microsoft Entity Framework Core NuGet 包。

右键单击 EShoppingTutorial.UnitTests 项目并选择“管理 NuGet 包”,然后添加 NuGet 包。

好的,现在是时候在 EShoppingTutorial.UnitTests 项目 -> Repositories 文件夹中添加一个名为 OrderRepositoryShould 的类,以便测试订单 Repository 的行为。

OrderRepositoryShould.cs

using NUnit.Framework;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using EShoppingTutorial.Core.Persistence;
using EShoppingTutorial.Core.Domain.Entities;
using EShoppingTutorial.Core.Domain.ValueObjects;
using EShoppingTutorial.Core.Persistence.Repositories;
using EShoppingTutorial.Core.Domain.Enums;

namespace EShoppingTutorial.UnitTests.Repositories
{
    public class OrderRepositoryShould
    {
        private DbContextOptionsBuilder<EShoppingTutorialDbContext> _builder;

        private EShoppingTutorialDbContext _dbContext;

        private OrderRepository _orderRepository;


        [OneTimeSetUp]
        public void Setup()
        {
            _builder = new DbContextOptionsBuilder<EShoppingTutorialDbContext>()
                .UseInMemoryDatabase(databaseName: "Test_OrderRepository_Database");

            _dbContext = new EShoppingTutorialDbContext(_builder.Options);

            _orderRepository = new OrderRepository(_dbContext);
        }


        [Test]
        public async Task Test_MethodAdd_TrackingNumberMustNotBeNull_Ok()
        {
            // arrange
            var order = new Order("IRAN", new OrderItem[]
                                    {
                                        new OrderItem (3, new Price(2000, MoneyUnit.Euro))
                                    });

            // act

            _orderRepository.Add(order);

            var actualOrder = await _orderRepository.GetByIdAsync(1);

            // assert
            Assert.IsNotNull(actualOrder);

            Assert.IsNotNull(actualOrder.TrackingNumber);
        }


        [OneTimeTearDown]
        public void CleanUp()
        {
            _dbContext.Dispose();
        }
    }
}

添加工作单元接口和实现

添加 Repository 后,现在是时候添加工作单元接口和实现类了。正如本文前面所解释的,我们只在应用程序领域中添加接口,而不是具体的实现。

所以我们在 Domain 文件夹中添加一个 IUnitOfWork 接口。

IUnitOfWork.cs

using System.Threading;
using System.Threading.Tasks;
using EShoppingTutorial.Core.Domain.Repositories;
 
namespace EShoppingTutorial.Core.Domain
{
    public interface IUnitOfWork
    {
        IOrderRepository OrderRepository { get; }
 
        Task<int> CompleteAsync();
 
        Task<int> CompleteAsync(CancellationToken cancellationToken);
    }
}

现在我们将在 Persistence 文件夹中添加 UnitOfWork 实现。正如您在下面的代码中看到的,我创建了一个实现 IUnitOfWorkIAsyncDisposable 的简单类,目前它只有一个 Repository,如果您将来需要添加其他 Repository,例如:客户或购物车 Repository,只需在此类中创建并添加它们。

正如您所见,此工作单元模式的实现并不太复杂,它只包含 Repository,以及一个简单的 ComleteAsync 方法,用于保存所有应用程序 Repository 更改,它还充当创建型模式和占位符,这将减少领域或应用程序服务中 Repository 注入的数量,并将使应用程序保持简单且易于维护。

但我再说一遍,一刀切不适用于所有情况,如果你有一个简单的应用程序,或者一个微服务,在最大化状态下只有八到十个存储库,那么只需保持简单(KISS 原则),并像我们一样做。

UnitOfWork.cs

using System;
using System.Threading;
using System.Threading.Tasks;
using EShoppingTutorial.Core.Domain;
using EShoppingTutorial.Core.Domain.Repositories;
using EShoppingTutorial.Core.Persistence.Repositories;
 
namespace EShoppingTutorial.Core.Persistence
{
    public class UnitOfWork : IUnitOfWork, IAsyncDisposable
    {
        private readonly EShoppingTutorialDbContext _context;
 
        public IOrderRepository OrderRepository { get; private set; }
 
 
        public UnitOfWork(EShoppingTutorialDbContext context)
        {
            _context = context;
 
            OrderRepository = new OrderRepository(_context);
        } 
 
        public async Task<int> CompleteAsync()
        {
            return await _context.SaveChangesAsync().ConfigureAwait(false);
        } 
 
        public async Task<int> CompleteAsync(CancellationToken cancellationToken)
        {
            return await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
        } 
 
        /// <summary>
        /// No matter an exception has been raised or not, 
        /// this method always will dispose the DbContext 
        /// </summary>
        /// <returns></returns>
        public ValueTask DisposeAsync()
        {
            return _context.DisposeAsync();
        }
    }
}

领域服务层

正如 Scott Millett 在他的书《Professional ASP.NET Design Patterns》中所说

那些不适合单个实体或需要访问 Repository 的方法包含在领域服务中。领域服务层也可以包含自己的领域逻辑,并且与实体和值对象一样,都是领域模型的一部分”。

因此,在现实世界的场景中,您可能需要领域服务层,但在本示例中,领域服务层将保持为空。

添加应用程序服务层 (Web API 控制器)

在这个简单的例子中,我们将通过构造函数直接将 IUnitOfWork 接口注入到应用程序服务层,即 ASP.NET Web API 控制器中,但在此之前,我们必须执行一些步骤。

  1. 添加 AutoMapperAutoMapper.Extensions.Microsoft.DependencyInjection NuGet 包,用于映射 DTO 模型。
  2. 添加 FluentValidation.AspNetCore NuGet 包,用于验证 DTO 模型。
  3. Startup.cs 类中配置 IUnitOfWorkAutoMapper 以进行注入,如下所示
    public void ConfigureServices(IServiceCollection services)
            {
                services
                    .AddMvcCore()
                    .AddApiExplorer()
                    .AddFluentValidation(s =>
                    {
                        s.RegisterValidatorsFromAssemblyContaining<Startup>();
                        s.RunDefaultMvcValidationAfterFluentValidationExecutes = false;
                        s.AutomaticValidationEnabled = true;
                        s.ImplicitlyValidateChildProperties = true;
                    });
    
                // Register the Swagger services
                services.AddSwaggerDocument();
    
                services.AddDbContext<EShoppingTutorialDbContext>(opts => opts.UseSqlServer(Configuration["ConnectionStrings:EShoppingTutorialDB"]));
    
                services.AddScoped<IUnitOfWork, UnitOfWork>();
    
                services.AddAutoMapper(typeof(Startup));
            }}
  4. 创建 DTO 模型文件夹并命名为“Models”,然后我们添加 DTO 模型和 Fluent 验证器,DTO 映射配置,以便配置订单实体通过构造函数映射订单项,如下面的代码所示

     

PriceSaveRequestModel.cs

using EShoppingTutorial.Core.Domain.Enums;

namespace EShoppingTutorialWebAPI.Models.OrderModels
{
    public class PriceSaveRequestModel
    {
        /// <example>100</example>
        public int? Amount { get; set; }

        /// <example>MoneyUnit.Rial</example>
        public MoneyUnit? Unit { get; set; } = MoneyUnit.UnSpecified;
    }
}

 

PriceSaveRequestModelValidator.cs

using FluentValidation;

namespace EShoppingTutorialWebAPI.Models.OrderModels
{
    public class PriceSaveRequestModelValidator : AbstractValidator<PriceSaveRequestModel>
    {
        public PriceSaveRequestModelValidator()
        {
            RuleFor(x => x.Amount)
                .NotNull();

            RuleFor(x => x.Unit)
                .NotNull()
                .IsInEnum();
        }
    }
}

 

OrderItemSaveRequestModel.cs

using System.ComponentModel.DataAnnotations;
using EShoppingTutorial.Core.Domain.ValueObjects;
 
namespace EShoppingTutorialWebAPI.Models.OrderModels
{
    public class OrderItemSaveRequestModel
    {
        /// <example>1</example>
        public int? ProductId { get; set; }
   
        public PriceSaveRequestModel Price { get; set; }
    }
}

 

PriceSaveRequestModelValidator.cs

using FluentValidation;

namespace EShoppingTutorialWebAPI.Models.OrderModels
{
    public class PriceSaveRequestModelValidator : AbstractValidator<PriceSaveRequestModel>
    {
        public PriceSaveRequestModelValidator()
        {
            RuleFor(x => x.Amount)
                .NotNull();

            RuleFor(x => x.Unit)
                .NotNull()
                .IsInEnum();
        }
    }
}

 

OrderSaveRequestModel.cs

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
 
namespace EShoppingTutorialWebAPI.Models.OrderModels
{
    public class OrderSaveRequestModel
    {
        /// <example>IRAN Tehran Persia</example>
        public string ShippingAdress { get; set; }

        public IEnumerable<OrderItemSaveRequestModel> OrderItemsDtoModel { get; set; }
    }
}

 

OrderSaveRequestModelValidator.cs

using FluentValidation;

namespace EShoppingTutorialWebAPI.Models.OrderModels
{
    public class OrderSaveRequestModelValidator : AbstractValidator<OrderSaveRequestModel>
    {
        public OrderSaveRequestModelValidator()
        {
            RuleFor(x => x.ShippingAdress)
           .NotNull()
           .NotEmpty()
           .Length(2, 100);

            RuleFor(x => x.OrderItemsDtoModel)
           .NotNull().WithMessage("Please enter order items!");
        }
    }
}

 

OrderItemViewModel.cs

using EShoppingTutorial.Core.Domain.ValueObjects;
 
namespace EShoppingTutorialWebAPI.Models.OrderModels
{
    public class OrderItemViewModel
    {
        public int Id { get; set; }
 
        public int ProductId { get; set; }
 
        public Price Price { get; set; }
    }
}

OrderViewModel.cs

using System;
using System.Collections.Generic;
 
namespace EShoppingTutorialWebAPI.Models.OrderModels
{
    public class OrderViewModel
    {
        public int Id { get; set; }
 
        public Guid? TrackingNumber { get; set; }
 
        public string ShippingAdress { get; set; }
 
        public DateTime OrderDate { get; set; }
 
        public IEnumerable<OrderItemViewModel> OrderItems { get; set; }
    }
}

因此,我们添加了一个订单 DTO 模型,现在我们必须对其进行配置,以便将订单实体配置为通过构造函数映射订单项,如下面的代码所示

OrderMappingProfile.cs

using AutoMapper;
using EShoppingTutorial.Core.Domain.Entities;
using EShoppingTutorial.Core.Domain.ValueObjects;
using EShoppingTutorialWebAPI.Models.OrderModels;
using System.Collections.Generic;

namespace EShoppingTutorialWebAPI.Models.DtoMappingConfigs
{
    public class OrderMappingProfile : Profile
    {
        public OrderMappingProfile()
        {
            CreateMap<Order, OrderViewModel>();

            CreateMap<OrderSaveRequestModel, Order>()
            .ConstructUsing((src, res) =>
            {
                return new Order(src.ShippingAdress, orderItems: res.Mapper.Map<IEnumerable<OrderItem>>(src.OrderItemsDtoModel)
                );
            });

            CreateMap<OrderItem, OrderItemViewModel>();

            CreateMap<OrderItemSaveRequestModel, OrderItem>();

            CreateMap<PriceSaveRequestModel, Price>().ConvertUsing(x => new Price(x.Amount.Value, x.Unit.Value));
        }
    }
}
  1. 右键单击 controllers 文件夹下方,添加一个新的 Web API 空控制器,并将其命名为 OrderController,然后添加 GetGetPagePostDelete 操作。请注意,在控制器类中,我们只注入了 IunitOfWorkAutomapper,如下面的代码所示

    OrderController.cs

    using AutoMapper;
    using Microsoft.AspNetCore.Mvc;
    using EShoppingTutorial.Core.Domain;
    using EShoppingTutorial.Core.Domain.Entities;
    using EShoppingTutorialWebAPI.Models.OrderModels;
    using SharedKernel.Models;
     
    using System.Collections.Generic;
    using System.Threading.Tasks;
    using System.Linq;
     
    namespace EShoppingTutorialWebAPI.Controllers
    {
        [ApiController]
        [Produces("application/json")]
        [Route("api/[controller]")]
        public class OrderController : ControllerBase
        {
            private readonly IUnitOfWork _unitOfWork;
            private readonly IMapper _mapper;
     
            public OrderController(IUnitOfWork unitOfWork, IMapper mapper)
            {
                _unitOfWork = unitOfWork;
                _mapper = mapper;
            }
     
            [HttpGet]
            [Route("{id}")]
            public async Task<IActionResult> GetOrder(int id)
            {
                var order = 
                  await _unitOfWork.OrderRepository.GetByIdAsync(id).ConfigureAwait(false);
     
                if (order == null)
                    return NotFound();
     
                var mappedOrder = _mapper.Map<OrderViewModel>(order);
     
                return Ok(mappedOrder);
            } 
     
            [HttpGet]
            [Route("GetAll")]
            public async Task<IActionResult> GetAll()
            {
                var orders = await _unitOfWork.OrderRepository.GetAllAsync
                             (en => en.OrderItems).ConfigureAwait(false);
     
                if (orders is null)
                    return NotFound();
     
                var mappedOrders = _mapper.Map<IEnumerable<OrderViewModel>>(orders);
     
                return Ok(new QueryResult<OrderViewModel>
                         (mappedOrders, mappedOrders.Count()));
            } 
     
            [HttpPost]
            [Route("GetPaged")]
            public async Task<IActionResult> 
                   GetPaged([FromBody] QueryObjectParams queryObject)
            {
                var queryResult = await _unitOfWork.OrderRepository.GetPageAsync
                                  (queryObject).ConfigureAwait(false);
     
                if (queryResult is null)
                    return NotFound();
     
                var mappedOrders = 
                    _mapper.Map<IEnumerable<OrderViewModel>>(queryResult.Entities);
     
                return Ok(new QueryResult<OrderViewModel>
                         (mappedOrders, queryResult.TotalCount));
            } 
     
            [HttpPost]
            [Route("Add")]
            public async Task<IActionResult> Add([FromBody] 
                         OrderSaveRequestModel orderResource)
            {
                var order = _mapper.Map<OrderSaveRequestModel, Order>(orderResource);
     
                _unitOfWork.OrderRepository.Add(order);
     
                await _unitOfWork.CompleteAsync().ConfigureAwait(false);
     
                return Ok();
            }
     
            [HttpDelete]
            [Route("{id}")]
            public async Task<IActionResult> Delete(int id)
            {
                var order = await _unitOfWork.OrderRepository.
                            GetByIdAsync(id).ConfigureAwait(false);
     
                if (order is null)
                    return NotFound();
     
                _unitOfWork.OrderRepository.Remove(order);
     
                await _unitOfWork.CompleteAsync().ConfigureAwait(false);
     
                return Ok();
            }
        }
    }

好了,我们差不多完成了,现在我们可以运行并使用 Postman 或 Swagger 调用 API 服务。我已经配置了 Swagger 并测试了 API 服务,下面是演示。您还可以下载源代码,查看代码并测试结果。

结论

好的,我们终于完成了!在本文中,我们试图专注于领域模型和领域服务中的应用程序领域逻辑。我们遵循了许多主题,如:领域驱动、测试驱动、松散耦合编程,以拥有一个清晰、易于理解和可维护的应用程序,该应用程序将以面向对象的方式(自我意图揭示应用程序)在其领域模型中展示应用程序的行为和业务逻辑。在拥有数十或数百个实体的复杂应用程序中,DDD 将帮助我们解决复杂性,并使我们拥有一个面向对象、清晰且易于理解的应用程序。但无论我们在这篇文章中付出了多少努力,我的座右铭是:“一刀切不适用于所有情况”。如果你真的有一个简单的、数据驱动的应用程序,业务逻辑最少,并且你确信它将来很少更改,那么就不要欺骗自己,不要遵循 DDD 规则,只需保持简单(KISS 原则)。

历史

  • 2021年3月5日:初始发布
© . All rights reserved.