目录
在Entity Framework Core上实现泛型存储库模式
构建一个简单的领域驱动ASP.NET Core Web API应用程序
PriceSaveRequestModelValidator.cs
PriceSaveRequestModelValidator.cs
OrderSaveRequestModelValidator.cs
介绍
阅读本文将使您正确理解如何使用ASP.NET Core 3.1 Web API和Entity Framework Core 5.0中的域驱动设计实现方法、泛型存储库和工作单元模式实现ASP.NET Core Web-API应用程序。
我将介绍数据访问层的泛型存储库和工作单元模式,然后我将开发一个ASP.NET Web API应用程序,用于在干净、可测试和可维护的架构中注册客户订单和订单项目,使用领域驱动设计(DDD)实现方法。
背景
您必须了解面向对象编程的基础知识、领域驱动设计(DDD)方法、Microsoft ORM实体框架、单元测试以及由Robert C. Martin介绍的面向对象设计的可靠原则。
此外,对于可靠原则的简要概述,您只需在Google中输入几个词,例如:“面向对象设计的可靠原则”。
领域驱动设计
术语领域驱动设计(DDD)是由埃里克·埃文斯在他2004年的书中创造的。领域驱动设计是一个很大的主题,在这篇文章中,我们只是想对它有一个粗略的了解,我们想专注于领域驱动设计的实现细节,我们还将编写一个简单的购物教程应用程序,我们的主要目标是在领域模型(富实体)和领域服务中保持应用程序业务逻辑的安全。在这篇关于示例购物教程应用程序的文章中,我们将专注于领域和领域逻辑,而不用担心数据持久性。
正如马丁福勒所说:
“领域驱动设计是一种软件开发方法,其开发的核心是对领域模型进行编程,该模型对领域的流程和规则有丰富的理解”。
我们将查看域元素(构建块),例如实体、值对象、聚合根实体、数据传输对象 (DTO)、服务和存储库。我们将研究可以在实现工作中使用的软件设计原则、框架和工具。实现DDD最著名的架构是Onion架构,如下图所示: 业务逻辑所在的应用核心(域模型和域服务),不依赖于任何数据持久化工具或任何技术,所以我们可以说,因为我们的应用程序核心业务逻辑不依赖于应用程序的其他部分,如数据库等,所以它可以很容易地单独测试和调试,这是非常棒的。
因此,我们可以在应用程序的任何其他部分更改我们将来想要的任何内容,例如:存储库、数据库、ORM、UI等,并且整个应用程序应该可以正常工作,而内层(核心领域)的更改最少或没有更改。
我不想过多谈论这种方法,将重点介绍领域驱动方法的实现细节,因为互联网上有很多关于此主题的理论信息。因此,如果您想了解更多关于这种开发方法的信息,请在Google中输入几个词,例如:“领域驱动设计实现方法”。
存储库模式
存储库是领域驱动设计(DDD)实现元素或构建块之一。存储库模式为我们提供了干净、可测试和可维护的方法来访问和操作应用程序中的数据。正如Martin Fowler在他的《企业应用程序架构模式》一书中所说:“存储库在领域和数据映射层之间起到中介作用,就像一个内存中的领域对象集合,它将业务实体与底层数据基础设施隔离开来”。存储库模式提供了一个接口并提供了添加、删除和检索域实体的方法,使域实体能够保持对底层数据持久层(Persistence Ignorance)不可知,鼓励松散耦合编程并允许独立扩展应用程序层,这导致我们独立于外部依赖项(可测试和敏捷架构原则)来测试业务逻辑。此外,存储库模式使我们能够集中和封装查询,以便在应用程序的其他部分重用它(DRY原则)。
如下图所示,应用逻辑依赖于客户存储库接口(Customer Repository Interface)而不依赖于存储库的具体实现,为了更多的抽象(Dependency Inversion Principle),这意味着应用逻辑完全不知道客户存储库和数据的任何实现访问问题,保持应用程序逻辑代码完整并免受未来数据访问更改的影响,确保它不那么脆弱且易于扩展。
所以没关系,如果我们想模拟客户存储库实现并通过DI(依赖注入)工具将其注入到应用程序逻辑类(领域服务或控制器)中,以便隔离地测试应用程序逻辑,给我们提供单元测试的机会而无需担心数据访问逻辑和实现(可测试架构原则)。
工作单元模式
工作单元设计模式将多个业务对象的数据持久化操作协调为一个原子事务,保证整个事务将被提交或回滚。如下图所示,工作单元设计模式封装了多个存储库并在它们之间共享单个数据库上下文。
有关工作单元设计模式的更多信息,您只需在Google中输入几个词,例如:“martin fowler工作单元”。
使用代码
创建空白解决方案和解决方案架构
我们首先通过添加一个空白解决方案来启动我们的应用程序,通过打开Visual Studio 2019,在右侧菜单中选择“创建新项目”,选择空白解决方案并命名它为DomainDrivenTutorial,如下图所示。然后我们在解决方案中添加Framework.Core文件夹。
添加和实现应用程序共享内核库
接下来我们在Framework.Core文件夹中添加.NET Core Class Library并命名为Shared Kernel,如下图所示:
共享内核库是我们想要放置公共模型和辅助类的地方,它们将在整个解决方案中的所有库中使用。我们在其中创建一个Models文件夹,然后我们添加PageParam类来制作数据分页请求模型,稍后将在存储库和应用程序服务层之间使用该模型。
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上实现泛型存储库模式
下一步将在Framework.Core文件夹中添加下一个.NET Core类库,并将其命名为GenericRepositoryEntityFramework。该库将仅包含三个.CS文件、存储库接口、存储库实现和SortingExtension静态帮助器类,以便按升序或降序对从数据库中获取的记录进行排序。
所以在继续之前,我们必须为实体框架核心添加NuGet包,如下图所示。右键单击应用程序解决方案名称,然后单击管理解决方案的NuGet包。
在NuGet表单中,点击Browse选项卡,在搜索条件过滤文本框中输入“EntityFrameworkCore”,如下图所示:
选择Microsoft.EntityFrameworkCore并在右侧部分,选择Generic Repository Entity Framework项目,然后单击Install按钮,如下图所示:
对NuGet包:Microsoft.EntityFrameworkCore.SqlServer和Microsoft.EntityFrameworkCore.Tools重复此方案。
好了,项目添加nuget包就完成了。如下所示,有一个使用实体框架核心5.0实现存储库模式的示例代码。
为了访问完整的源代码,您可以从 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);
}
}
请注意,我在Repository实现类中特意定义了一个受保护的DbContext属性,因为我想在派生的存储库类中使用它,我想让派生的存储库类进行灵活丰富的查询,这肯定会是一个派生类和基本泛型存储库类之间的关系。
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应用程序,以便更好地理解域驱动设计的实现方法。
我们想要做的是创建两个域实体,通过实体框架的核心代码优先方法的Order和Order Items实体,并在其上使用领域驱动设计方法,聚合根和存储库模式,以封装应用程序逻辑域执行CRUD操作模型和领域服务等等……
所以首先,我们将创建一个类库来将应用程序持久性和业务逻辑与ASP.NET Core Web API分开,我们将我们的应用程序逻辑放在领域模型和领域服务中,然后我们将添加两个领域实体,Order和Order Items实体。说了这么多,让我们开始示例项目。
右键单击解决方案名称并创建一个新文件夹并将其命名为EShoppingTutorial,然后通过转到File -> New -> Project添加一个.NET Core类库项目,并将其命名为EShoppingTutorial.Core,如下图所示:
接下来,我们要定义应用程序解决方案结构,将文件夹中的文件分开,例如:Domain,Entities,Services,Persistence,Repositories等,以便分离数据持久性关注的应用程序业务逻辑,以获得更好和干净的应用程序架构,如下图所示:
希望大家在编码之前仔细看看应用方案的结构,我们已经把文件夹分开了,接下来我们会给它们添加一些类,这可以让我们有更好的应用架构(Clean Architecture Principle),更好的重构和维护。 一开始,我们决定将Domain Entities和Domain Services文件夹和边界与Persistence层分开,我们的目的是将将来可能会改变的东西(例如:数据持久化逻辑、ORM版本或数据库)与几乎不改变或从不改变的东西,或者它们是不变或持久无知的东西,比如:域不变量、域服务或值对象,正如我在本文开头的域驱动设计一节中所讨论的。
所以在继续之前,最好为实体框架核心添加NuGet包。就像下图一样。右键单击应用程序解决方案名称,然后单击管理解决方案的NuGet包。
在NuGet表单中,点击Browse选项卡,在搜索条件过滤文本框中输入“EntityFrameworkCore”,如下图所示:
选择Microsoft.EntityFrameworkCore并在右侧部分,选择EShoppingTutorial.Core项目,然后单击安装按钮,如下图所示。对NuGet包Microsoft.EntityFrameworkCore.SqlServer和Microsoft.EntityFrameworkCore.Tools重复此操作。
此外,我们必须添加Generic Repository Entity Framework和Shared Kernel项目库的项目引用。
添加应用程序领域模型
因此,对于下一步,我们添加两个领域模型(实体类),在Entities文件夹中调用Order和Order Items,在ValueObjects文件夹中调用一个Price值对象,在Enums文件夹中调用一个叫MoneyUnit的Enum,如下面的代码所示:
MoneyUnit.cs
namespace EShoppingTutorial.Core.Domain.Enums
{
public enum MoneyUnit : int
{
UnSpecified = 0,
Rial = 1,
Dollar,
Euro
}
}
添加Price值对象
下一步,我们添加一个在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数据的唯一方法将是通过这个聚合根富实体模型。
正如你在下面看到的,OrderItems 属性是一个只读集合,所以我们将无法通过这个只读属性从外部添加订单项,添加订单项的唯一方法是通过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(在共享内核库定义的自定义异常)。
此外,它是一个纯 .NET 对象(POCO 类)和持久性无知 (PI)。因此,无需连接到任何外部服务或数据库存储库,我们就可以轻松编写单元测试以检查模型的行为(业务规则)。因此,让我们一起为Order实体的业务规则编写一些单元测试。
添加单元测试项目并为订单实体编写一个简单的单元测试
因此,右键单击解决方案资源管理器,然后选择Add --> New project。
然后选择您要使用的模板项目,对于此应用程序,您可以选择NUnit Test或Unit Test Project,然后选择Next。
因此,为您的项目输入类似“EShoppingTutorial.UnitTests”的名称,然后选择Create,如下图所示:
将泛型存储库实体框架和共享内核项目库的项目引用添加到此项目。
创建文件夹:Domain、Entities和Repositories,并在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"));
}
}
}
添加实体框架DbContext和数据库迁移
这真好; 我们能够轻松地为订单实体业务规则和不变量编写单元测试,而无需连接到任何外部数据库。所以现在,我们将使用Microsoft Entity Framework Core (ORM)代码优先Fluent API方法并使用add-migration命令生成数据库和表创建脚本。我们开始做吧。
首先,我们应该配置C#实体类到数据库表的映射,如下代码。我们在Mappings文件夹中添加名为OrderMapConfig和OrderItemMapConfig的映射类文件,然后我们放置实体映射配置。
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原则)。
好的,下一步通过代码优先的方法生成数据库和表将添加数据库连接字符串,为此,首先我们要在解决方案中添加一个ASP.NET Core web API项目到EShoppingTutorial文件夹中,然后我们添加Web API项目中的连接字符串,如下图所示:
在下一个表单中,选择API模板,然后删除为Https配置的勾选,如下图所示:
对于下一步,我们为微软实体框架的核心和微软EntityFrameworkCore设计添加的NuGet包,以及EShoppingTutorial.Core和SharedKernel的项目引用,就像我们在本文中做的那样。
最后,我们自己准备合适的数据库服务器,然后在web API项目的appsettings.json中添加连接字符串,最后在Startup.cs中使用,appsettings中指定的连接字符串添加EShoppingTutorialDbContext,如下代码所示。
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命令。实体框架会要求你给它一个名字,给它一个有意义的名字,比如:“Adding Order and OrderItems tables”,如下图所示:
好的,从实体框架得到成功消息后,运行如下图所示的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部分所讲的,应用程序领域将保持纯粹且持久性不可知,我们只会在应用程序域文件夹中添加存储库接口,如下图所示,存储库实现是分开的来自应用核心领域。
如下图所示,应用核心域逻辑依赖于存储库接口,而不是存储库的具体实现,为了更多的抽象,这意味着应用逻辑完全不关心存储库的任何实现和数据访问问题。
现在我们将在Persistence -> Repositories文件夹中添加OrderRepository实现,它将从泛型存储库类继承,如下代码。为了存储库类,我们可以覆盖基本虚拟方法,或者我们可以添加新的自定义方法。例如,我已经重写了基本存储库的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添加方法编写简单的单元测试
好了,订单库的实现已经完成了,接下来我们开始编写一些简单的单元测试,用于订单库的添加和获取方法。
所以首先,我们必须为单元测试模拟实体框架的DbContext(模拟是我们在单元测试中使用的一种方法,当我们的测试目标具有外部依赖项时,例如:数据库或其他外部服务,有关更多信息,您可以在互联网上,有很多关于它的信息)。
所以为了模拟EShoppingTutorial DbContext,我们应该添加Entity Framework Core InMemory数据库和Microsoft Entity Framework Core NuGet Packages。
右键单击EShoppingTutorial.UnitTests项目并选择管理NuGet包,然后添加NuGet包。
好的,现在是时候添加一个在EShoppingTutorial.UnitTests项目-> Repositories文件夹中调用的OrderRepositoryShould类,以测试订单存储库的行为。
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();
}
}
}
添加工作现单元的接口和实
添加存储库后,现在是添加工作单元接口和实现类的时候了。如前所述,我们在本文中解释过,我们只在应用程序域中添加接口,而不是具体的实现。
所以我们在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实现。正如您在下面的代码中看到的,我创建了一个实现IUnitOfWork和IAsyncDisposable的简单类,当时它只有一个存储库,如果您将来需要添加其他存储库,例如:客户或购物篮存储库,只需创建和将它们添加到这个类中。
如您所见,工作单元模式的这种实现并不太复杂,它只包含存储库,以及一个用于保存所有应用程序存储库更改的简单方法ComleteAsync,它还充当创建模式和占位符,将减少领域或应用程序服务中的存储库注入次数,并使应用程序保持简单和易于维护。
但我再说一遍,一刀切不能放过,如果你有一个简单的应用程序,或者一个微服务,在最大化状态下只有八个或十个存储库,就保持简单(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”中所说:
“真正不适合单个实体或需要访问存储库的方法包含在领域服务中。领域服务层也可以包含它自己的领域逻辑,并且与实体和值对象一样是领域模型的一部分”。
所以,也许在现实世界的场景中,你需要有领域服务层,但在这个简单的例子中,域服务层将是空的。
添加应用服务层(Web API控制器)
在这个简单的例子中,我们将通过构造函数将IUnitOfWork接口直接注入到应用服务层,在ASP.NET Web API控制器中,但在此之前,我们必须完成一些步骤。
1、添加AutoMapper和AutoMapper.Extensions.Microsoft.DependencyInjection NuGet包,用于映射DTO模型。
2、添加FluentValidation.AspNetCore NuGet包,用于验证DTO模型。
3、在Startup.cs类中配置IUnitOfWork和AutoMapper注入,如下所示:
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; }
}
}
所以,我们添加了一个Order 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));
}
}
}
- 右键单击在控制器文件夹,并添加一个新的Web API空控制器,并将其命名OrderController,并添加然后添加Get,GetPage,Post和Delete行动。请注意,在控制器类中,我们只注入了IunitOfWork和 Automapper,如下面的代码:
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();
}
}
}
好的,我们差不多完成了,现在我们可以运行并调用API服务,使用postman或swagger。我已经配置了swagger并测试了API服务,下面是演示。您还可以下载源代码,查看代码并测试结果。
结论
好吧,我们终于完成了!在本文中,我们尝试将注意力集中在领域模型和领域服务中的应用程序域逻辑上。我们跟进了许多主题,例如:领域驱动、测试驱动、松散耦合编程,以拥有一个干净、易于理解和可维护的应用程序,它将以面向对象的方式在其域模型中演示应用程序的行为和业务逻辑(自我意图揭示应用)。在我们将拥有数十或数百个实体的复杂应用程序中,DDD将帮助我们解决复杂性,并将引导我们拥有一个面向对象、干净且易于理解的应用程序。但不管我们在这篇文章中付出了多少努力,我的座右铭是:“一种尺寸并不适合所有人”。如果你真的有一个简单的数据驱动的应用,业务逻辑最少,你确定以后很少改动,所以不要自欺欺人,不要遵循DDD规则,保持简单(KISS原则)。
https://www.codeproject.com/Articles/5296451/Domain-Driven-Design-Implementation-Approach-with