.项目分析

  在上篇中介绍了什么是"干净架构",DDD符合了这种干净架构的特点,重点描述了DDD架构遵循的依赖倒置原则,使软件达到了低藕合。eShopOnWeb项目是学习DDD领域模型架构的一个很好案例,本篇继续分析该项目各层的职责功能,主要掌握ApplicationCore领域层内部的术语、成员职责。

  

  1. web层介绍

    eShopOnWeb项目与Equinox项目,双方在表现层方面对比,没有太大区别。都是遵循了DDD表现层的功能职责。有一点差异的是eShopOnWeb把表现层和应用服务层集中在了项目web层下,这并不影响DDD风格架构。

    项目web表现层引用了ApplicationCore领域层和Infrastructure基础设施层,这种引用依赖是正常的。引用Infrastructure层是为了添加EF上下文以及Identity用户管理。 引用ApplicationCore层是为了应用程序服务 调用 领域服务处理领域业务。

    在DDD架构下依赖关系重点强调的是领域层的独立,领域层是同心圆中最核心的层,所以在eShopOnWeb项目中,ApplicationCore层并没有依赖引用项目其它层。再回头看Equinox项目,领域层也不需要依赖引用项目其它层。

    下面web混合了MVC和Razor,结构目录如下所示:

    

    (1) Health checks 

      Health checks是ASP.NET Core的特性,用于可视化web应用程序的状态,以便开发人员可以确定应用程序是否健康。运行状况检查端点/health。

         //添加服务
services.AddHealthChecks()
.AddCheck<HomePageHealthCheck>("home_page_health_check")
.AddCheck<ApiHealthCheck>("api_health_check");
 //添加中间件
   app.UseHealthChecks("/health");

       下图检查了web首页和api接口的健康状态,如下图所示

    (2) Extensions

      向现有对象添加辅助方法。该Extensions文件夹有两个类,包含用于电子邮件发送和URL生成的扩展方法。

    (3) 缓存

      对于Web层获取数据库的数据,如果数据不会经常更改,可以使用缓存,避免每次请求页面时,都去读取数据库数据。这里用的是本机内存缓存。

        //缓存接口类
private readonly IMemoryCache _cache; // 添加服务,缓存类实现
  services.AddScoped<ICatalogViewModelService, CachedCatalogViewModelService>(); //添加服务,非缓存的实现
//services.AddScoped<ICatalogViewModelService, CatalogViewModelService>();
  2. ApplicationCore

    ApplicationCore是领域层,是项目中最重要最复杂的一层。ApplicationCore层包含应用程序的业务逻辑,此业务逻辑包含在领域模型中。领域层知识在Equinox项目中并没有讲清楚,这里在重点解析领域层内部成员,并结合项目来说清楚。

    下面讲解领域层内部的成员职责描述定义,参考了“Microsoft.NET企业级应用架构设计 第二版”。

    领域层内部包括:领域模型和领域服务二大块。涉及到的术语:

  领域模型(模型)

             1)模块

        2)领域实体(也叫"实体")

        3)值对象

        4)聚合

    领域服务(也叫"服务")

    仓储

    下面是领域层主要的成员:

    下面是聚合与领域模型的关系。最终领域模型包含了:聚合、单个实体、值对象的结合。

    (1) 领域模型

      领域模型是提供业务领域的概念视图,它由实体和值对象构成。在下图中Entities文件夹是领域模型,可以看到包含了聚合、实体、值对象。

      1.1 模块

        模块是用来组织领域模型,在.net中领域模型通过命令空间组织,模块也就是命名空间,用来组织类库项目里的类。比如:

        namespace Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate

        namespace Microsoft.eShopWeb.ApplicationCore.Entities.BuyerAggregate

      1.2 实体

        实体通常由数据和行为构成。如果要在整个生命周期的上下文里唯一跟踪它,这个对象就需要一个身份标识(ID主键),并看成实体。 如下所示是一个实体: 

    /// <summary>
/// 领域实体都有唯一标识,这里用ID做唯一标识
/// </summary>
public class BaseEntity
{
public int Id { get; set; }
} /// <summary>
/// 领域实体,该实体行为由Basket聚合根来操作
/// </summary>
public class BasketItem : BaseEntity
{
public decimal UnitPrice { get; set; }
public int Quantity { get; set; }
public int CatalogItemId { get; set; }
}

      1.3 值对象

        值对象和实体都由.net 类构成。值对象是包含数据的类,没有行为,可能有方法本质上是辅助方法。值对象不需要身份标识,因为它们不会改变状态。如下所示是一个值对象

    /// <summary>
/// 订单地址 值对象是普通的DTO类,没有唯一标识。
/// </summary>
public class Address // ValueObject
{
public String Street { get; private set; } public String City { get; private set; } public String State { get; private set; } public String Country { get; private set; } public String ZipCode { get; private set; } private Address() { } public Address(string street, string city, string state, string country, string zipcode)
{
Street = street;
City = city;
State = state;
Country = country;
ZipCode = zipcode;
}
}

      

      1.4 聚合

        在开发中单个实体总是互相引用,聚合的作用是把相关逻辑的实体组合当作一个整体对待。聚合是一致性(事务性)的边界,对领域模型进行分组和隔离。聚合是关联的对象(实体)群,放在一个聚合容器中,用于数据更改的目的。每个聚合通常被限制于2~3个对象。聚合根在整个领域模型都可见,而且可以直接引用。   

    /// <summary>
/// 定义聚合根,严格来说这个接口不需要任务功能,它是一个普通标记接口
/// </summary>
public interface IAggregateRoot
{ } /// <summary>
/// 创建购物车聚合根,通常实现IAggregateRoot接口
/// 购物车聚合模型(包括Basket、BasketItem实体)
/// </summary>
public class Basket : BaseEntity, IAggregateRoot
{
public string BuyerId { get; set; }
private readonly List<BasketItem> _items = new List<BasketItem>(); public IReadOnlyCollection<BasketItem> Items => _items.AsReadOnly();
//...
}

  

    在该项目中领域模型与“Microsoft.NET企业级应用架构设计第二版”书中描述的职责有不一样地方,来看一下:

      (1) 领域服务有直接引用聚合中的实体(如:BasketItem)。书中描述是聚合中实体不能从聚合之处直接引用,应用把聚合看成一个整体。

      (2) 领域实体几乎都是贫血模型。书中描述是领域实体应该包括行为和数据。

    (2) 领域服务

      领域服务类方法实现领域逻辑,不属于特定聚合中(聚合是属于领域模型的),很可能跨多个实体。当一块业务逻辑无法融入任何现有聚合,而聚合又无法通过重新设计适应操作时,就需要考虑使用领域服务。下图是领域服务文件夹:

        /// <summary>
/// 下面是创建订单服务,用到的实体包括了:Basket、BasketItem、OrderItem、Order跨越了多个聚合,该业务放在领域服务中完全正确。
/// </summary>
/// <param name="basketId">购物车ID</param>
/// <param name="shippingAddress">订单地址</param>
/// <returns>回返回类型</returns>
public async Task CreateOrderAsync(int basketId, Address shippingAddress)
{
var basket = await _basketRepository.GetByIdAsync(basketId);
Guard.Against.NullBasket(basketId, basket);
var items = new List<OrderItem>();
foreach (var item in basket.Items)
{
var catalogItem = await _itemRepository.GetByIdAsync(item.CatalogItemId);
var itemOrdered = new CatalogItemOrdered(catalogItem.Id, catalogItem.Name, catalogItem.PictureUri);
var orderItem = new OrderItem(itemOrdered, item.UnitPrice, item.Quantity);
items.Add(orderItem);
}
var order = new Order(basket.BuyerId, shippingAddress, items); await _orderRepository.AddAsync(order);
}

      在该项目与“Microsoft.NET企业级应用架构设计第二版”书中描述的领域服务职责不完全一样,来看一下:

      (1) 项目中,领域服务只是用来执行领域业务逻辑,包括了订单服务OrderService和购物车服务BasketService。书中描述是可能跨多个实体。当一块业务逻辑无法融入任何现有聚合。

       /// <summary>
/// 添加购物车服务,没有跨越多个聚合,应该不放在领域服务中。
/// </summary>
/// <param name="basketId"></param>
/// <param name="catalogItemId"></param>
/// <param name="price"></param>
/// <param name="quantity"></param>
/// <returns></returns>
public async Task AddItemToBasket(int basketId, int catalogItemId, decimal price, int quantity)
{
var basket = await _basketRepository.GetByIdAsync(basketId); basket.AddItem(catalogItemId, price, quantity); await _basketRepository.UpdateAsync(basket);
}

     总的来说,eShopOnWeb项目虽然没有完全遵循领域层中,成员职责描述,但可以理解是在代码上简化了领域层的复杂性。

    (3) 仓储

      仓储是协调领域模型和数据映射层的组件。仓储是领域服务中最常见类型,它负责持久化。仓储接口的实现属于基础设施层。仓储通常基于一个IRepository接口。 下面看下项目定义的仓储接口。

    /// <summary>
/// T是领域实体,是BaseEntity类型的实体
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IAsyncRepository<T> where T : BaseEntity
{
Task<T> GetByIdAsync(int id);
Task<IReadOnlyList<T>> ListAllAsync();
//使用领域规则查询
Task<IReadOnlyList<T>> ListAsync(ISpecification<T> spec);
Task<T> AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(T entity);
//使用领域规则查询
Task<int> CountAsync(ISpecification<T> spec);
}

    (4) 领域规则

      在仓储设计查询接口时,可能还会用到领域规则。 在仓储中一般都是定义固定的查询接口,如上面仓储的IAsyncRepository所示。而复杂的查询条件可能需要用到领域规则。在本项目中通过强大Linq 表达式树Expression 来实现动态查询。

    /// <summary>
/// 领域规则接口,由BaseSpecification实现
/// 最终由Infrastructure.Data.SpecificationEvaluator<T>类来构建完整的表达树
/// </summary>
/// <typeparam name="T"></typeparam>
public interface ISpecification<T>
{
//创建一个表达树,并通过where首个条件缩小查询范围。
//实现:IQueryable<T> query = query.Where(specification.Criteria)
Expression<Func<T, bool>> Criteria { get; } //基于表达式的包含
//实现如: Includes(b => b.Items)
List<Expression<Func<T, object>>> Includes { get; }
List<string> IncludeStrings { get; } //排序和分组
Expression<Func<T, object>> OrderBy { get; }
Expression<Func<T, object>> OrderByDescending { get; }
Expression<Func<T, object>> GroupBy { get; } //查询分页
int Take { get; }
int Skip { get; }
bool isPagingEnabled { get;}
}

    最后Interfaces文件夹中定义的接口,都由基础设施层来实现。如:

     IAppLogger日志接口

     IEmailSender邮件接口

      IAsyncRepository仓储接口

  3.Infrastructure层

    基础设施层Infrastructure依赖于ApplicationCore,这遵循依赖倒置原则(DIP),Infrastructure中代码实现了ApplicationCore中定义的接口(Interfaces文件夹)。该层没有太多要讲的,功能主要包括:使用EF Core进行数据访问、Identity、日志、邮件发送。与Equinox项目的基础设施层差不多,区别多了领域规则。

           领域规则SpecificationEvaluator.cs类用来构建查询表达式(Linq expression),该类返回IQueryable<T>类型。IQueryable接口并不负责查询的实际执行,它所做的只是描述要执行的查询。

public class EfRepository<T> : IAsyncRepository<T> where T : BaseEntity
{
//...这里省略的是常规查询,如ADDAsync、UpdateAsync、GetByIdAsync ... //获取构建的查询表达式
   private IQueryable<T> ApplySpecification(ISpecification<T> spec)
{
return SpecificationEvaluator<T>.GetQuery(_dbContext.Set<T>().AsQueryable(), spec);
}
}
  public class SpecificationEvaluator<T> where T : BaseEntity
{
/// <summary>
/// 做查询时,把返回类型IQueryable当作通货
/// </summary>
/// <param name="inputQuery"></param>
/// <param name="specification"></param>
/// <returns></returns>
public static IQueryable<T> GetQuery(IQueryable<T> inputQuery, ISpecification<T> specification)
{
var query = inputQuery; // modify the IQueryable using the specification's criteria expression
if (specification.Criteria != null)
{
query = query.Where(specification.Criteria);
} // Includes all expression-based includes
//TAccumulate Aggregate<TSource, TAccumulate>(this IEnumerable<TSource> source, TAccumulate seed, Func<TAccumulate, TSource, TAccumulate> func);
//seed:query初始的聚合值
//func:对每个元素调用的累加器函数
//返回TAccumulate:累加器的最终值
//https://msdn.microsoft.com/zh-cn/windows/desktop/bb549218
query = specification.Includes.Aggregate(query,
(current, include) => current.Include(include)); // Include any string-based include statements
query = specification.IncludeStrings.Aggregate(query,
(current, include) => current.Include(include)); // Apply ordering if expressions are set
if (specification.OrderBy != null)
{
query = query.OrderBy(specification.OrderBy);
}
else if (specification.OrderByDescending != null)
{
query = query.OrderByDescending(specification.OrderByDescending);
} if (specification.GroupBy != null)
{
query = query.GroupBy(specification.GroupBy).SelectMany(x => x);
} // Apply paging if enabled
if (specification.isPagingEnabled)
{
query = query.Skip(specification.Skip)
.Take(specification.Take);
}
return query;
}
}

 

 参考资料

    Microsoft.NET企业级应用架构设计 第二版

asp.net core系列 64 结合eShopOnWeb全面认识领域模型架构的更多相关文章

  1. 【目录】asp.net core系列篇

    随笔分类 - asp.net core系列篇 asp.net core系列 68 Filter管道过滤器 摘要: 一.概述 本篇详细了解一下asp.net core filters,filter叫&q ...

  2. WPF中的常用布局 栈的实现 一个关于素数的神奇性质 C# defualt关键字默认值用法 接口通俗理解 C# Json序列化和反序列化 ASP.NET CORE系列【五】webapi整理以及RESTful风格化

    WPF中的常用布局   一 写在开头1.1 写在开头微软是一家伟大的公司.评价一门技术的好坏得看具体的需求,没有哪门技术是面面俱到地好,应该抛弃对微软和微软的技术的偏见. 1.2 本文内容本文主要内容 ...

  3. Ajax跨域问题及解决方案 asp.net core 系列之允许跨越访问(Enable Cross-Origin Requests:CORS) c#中的Cache缓存技术 C#中的Cookie C#串口扫描枪的简单实现 c#Socket服务器与客户端的开发(2)

    Ajax跨域问题及解决方案   目录 复现Ajax跨域问题 Ajax跨域介绍 Ajax跨域解决方案 一. 在服务端添加响应头Access-Control-Allow-Origin 二. 使用JSONP ...

  4. 1.1专题介绍「深入浅出ASP.NET Core系列」

    大家好,我是IT人张飞洪,专注于.NET平台十年有余. 工作之余喜欢阅读和写作,学习的内容包括数据结构/算法.网络技术.Linux系统原理.数据库技术原理,设计模式.前沿架构.微服务.容器技术等等…… ...

  5. asp.net core系列 30 EF管理数据库架构--必备知识 迁移

    一.管理数据库架构概述 EF Core 提供两种主要方法来保持 EF Core 模型和数据库架构同步.一是以 EF Core 模型为基准,二是以数据库为基准. (1)如果希望以 EF Core 模型为 ...

  6. asp.net core系列 40 Web 应用MVC 介绍与详细示例

    一. MVC介绍 MVC架构模式有助于实现关注点分离.视图和控制器均依赖于模型. 但是,模型既不依赖于视图,也不依赖于控制器. 这是分离的一个关键优势. 这种分离允许模型独立于可视化展示进行构建和测试 ...

  7. asp.net core系列 39 Web 应用Razor 介绍与详细示例

    一. Razor介绍 在使用ASP.NET Core Web开发时, ASP.NET Core MVC 提供了一个新特性Razor. 这样开发Web包括了MVC框架和Razor框架.对于Razor来说 ...

  8. asp.net core系列 38 WebAPI 返回类型与响应格式--必备

    一.返回类型 ASP.NET Core 提供以下 Web API Action方法返回类型选项,以及说明每种返回类型的最佳适用情况: (1) 固定类型 (2) IActionResult (3) Ac ...

  9. asp.net core系列 36 WebAPI 搭建详细示例

    一.概述 HTTP不仅仅用于提供网页.HTTP也是构建公开服务和数据的API强大平台.HTTP简单灵活且无处不在.几乎任何你能想到的平台都有一个HTTP库,因此HTTP服务可以覆盖广泛的客户端,包括浏 ...

随机推荐

  1. 大话设计模式--桥接模式 Bridge -- C++实现实例

    1. 桥接模式: 将抽象部分与它的实现部分分离,使它们都可以独立的变化. 分离是指 抽象类和它的派生类用来实现自己的对象分离. 实现系统可以有多角度分类,每一种分类都有可能变化,那么把这种多角度分离出 ...

  2. 常用连续型分布介绍及R语言实现

    常用连续型分布介绍及R语言实现 R的极客理想系列文章,涵盖了R的思想,使用,工具,创新等的一系列要点,以我个人的学习和体验去诠释R的强大. R语言作为统计学一门语言,一直在小众领域闪耀着光芒.直到大数 ...

  3. 消息队列(Message Queue)基本概念

    背景 之前做日志收集模块时,用到flume.另外也有的方案,集成kafaka来提升系统可扩展性,其中涉及到消息队列当时自己并不清楚为什么要使用消息队列.而在我自己提出的原始日志采集方案中不适用消息队列 ...

  4. 恢复delete删除的数据

    SELECT * FROM tablename AS OF TIMESTAMP TO_TIMESTAMP('2010-12-15 11:10:17', 'YYYY-MM-DD HH:MI:SS')

  5. php:如何使用PHP排序, key为字母+数字的数组(多维数组)

    你还在为如何使用PHP排序字母+数字的数组而烦恼吗? 今天有个小伙伴在群里问:如何将一个key为字母+数字的数组按升序排序呢? 举个例子: $test = [ 'n1' => 22423, 'n ...

  6. 分享知识-快乐自己:zookeeper 伪集群搭建

    1):单一 zookeeper  搭建步骤 2):zookeeper 伪集群搭建 1):新建一个集群目录 [root@zoodubbo opt]# mkdir zookeeper_cluster 2) ...

  7. 分享知识-快乐自己:HTTP 响应码

    状态码 含义 100 客户端应当继续发送请求.这个临时响应是用来通知客户端它的部分请求已经被服务器接收,且仍未被拒绝.客户端应当继续发送请求的剩余部分,或者如果请求已经完成,忽略这个响应.服务器必须在 ...

  8. winform 添加帮助按钮

    1. 添加提示信息 新建个窗体项目,项目名称为WinFormUI,解决方案名称为WinFormWithHelpDoc.删除默认创建的Form1,新建窗体MainForm,设置相关属性.我们要完成的效果 ...

  9. java--xml文件读取(DOM)

    1.表现:一“.xml”为扩展名的文件 2.存储:树形结构 3.xml解析应用: 不同应用程序之间的通信-->订票软件和支付软件 不同的平台间通信-->操作系统 不同平台间数据的共享--& ...

  10. BEC listen and translation exercise 41

    Its advantages are that it can be used for outside activities So my recommendation I'm afraid would ...