CQRS架构下Equinox开源项目分析

一.DDD分层架构介绍

  本篇分析CQRS架构下的Equinox开源项目。该项目在github上star占有2.4k。便决定分析Equinox项目来学习下CQRS架构。再讲CQRS架构时,先简述下DDD风格,在DDD分层架构中,一般包含表现层、应用程序层(应用服务层)、领域层(领域服务层)、基础设施层。在DDD中讲到服务这个术语时,比如领域服务,应用层服务等,这个服务是指业务逻辑,而不是指任何技术如wcf,web服务。

  下图是从经典三层构架演变为DDD下的分层架构图:

640?wx_fmt=jpeg

  1.表现层

    表现层前端往后端post的数据称"输入模型(InputModel)",后端控制器传给前端要显示的数据称"视图模型(ViewModel)",大多时候视图模型与输入模型是重合的,所在在下面要介绍的开源项目中,作者在应用服务层只定义了ViewModels文件夹。例如在MVC中,控制器里只是编排任务,调用应用程序层。在控制器中代码块应该尽可能轻薄,主要作用是找出层与层之间的分离,控制器只是业务逻辑占位符。

    在表现层中与运行环境密切相连,表现层需要关注的是http上下文、会话状态等。

640?wx_fmt=jpeg

  2. 应用服务层

    可以在应用服务层引用领域层和基础设施层,是在领域层之上编排业务用例的服务。该层对业务规则一无所知,不会包含任何与业务有关的状态信息。该层关键特点:

    (1) 该层是针对不同的前端。该层与表现层有关,是为表现层服务。不同的表现层(移动,webapi, web)都有自己的应用服务层。该层与表现层属于系统的前端。

    (2) 应用服务层可能是有状态的,至少就UI任务进度而言。

    (3) 它从表现层获取输入模型,然后把视图模型返回去。

  3. 领域层

    领域层是最重要和最复杂的一层。在DDD的领域模型架构下。该层包含了所有针对一个或多个用例业务逻辑,领域层包含一个领域模型和一组可能的服务。

    领域模型大多时候是一个实体关系模型,可以由方法组成。是拥有数据和行为。如果缺少重要行为,那就是一个数据结构,称为贫血模型。领域模型是实现统一语言和表达业务流程所需的操作。

    领域层包含的服务是领域服务,是涉及多个领域模型而无法放个单个领域模型中的领域逻辑。领域服务是一个类,包含了多个领域模型实体的行为。领域服务通常也需要访问基础设施层。

    在DDD的CQRS架构下,使用二个不同的领域层,而不是一个(在Equinox项目中混合成一个)。这种分离把查询操作放在一层(查询领域层),把命令操作放在另一层(命令领域层)。在CQRS里,查询栈仅仅基于SQL查询,可以完全没有模型、应用程序层和领域层。查询领域层只需要贫血模型类DTO来做传输对象。

  4. 基础设施层

    这层使用具体技术有关的任何东西:O/RM工具的数据访问持久层、IOC容器的实现(Unity)、以及很多其它横切关注点的实现,如安全(Oauth2)、日志记录、跟踪、缓存等。最突出的组件是持久层。

二.CQRS概述

  1.简介

    CQRS是DDD开发风格下对领域模型架构的一种简化改进。任何业务系统基本都是查询与写入,对应CQRS是指命令/查询责任分离,查询不以任何方式修改系统状态,只返回数据。另一方面,命令(写入)则修改系统的的状态,但不返回数据,除了状态代码或确认信息。在CQRS里,查询栈仅基于sql查询,可以完全没有模型,应用程序层和领域层。CQRS方案还可以为命令栈和查询栈准备不同的数据库(读与写)。

  2.CQRS的好处

    (1)是简化设计降低复杂性,对于查询来说,可以直接读取基础设施层的仓储。

    (2)是增强可伸缩性的潜能。比如读取是主导操作,可以引入某种程序的缓存,极大减少访问数据库的次数。比如写入在高峰期减慢系统,可以考虑从经典的同步写入模型换到异步写入甚至命令队列。分离了查询和命令,可以完全隔离处理这两个部分的可伸缩性。

  3.CQRS实现全局图

    在全局图中,右图通过虚线表示双重分层架构,分开了命令通道和查询通道,每个通道都有独立架构。在命令通道里,任何来自表现层的请求都会变成一个命令,并加入到处理器队列。每个命令都携带信息。每个命令都是一个逻辑单元,可以充分地验证相关对象的状态,智能的决定执行哪些更新以及拒绝哪些更新。处理命令可能会产生事件(事件通常是记录命令发生的事情),这些事件会被其它注册组件处理。

640?wx_fmt=png

三. Equinox开源项目总览

  1.准备环境

    (1)  Github开源地址下载。Full ASP.NET Core 2.2 application with DDD, CQRS and Event Sourcing

    (2)  在sqlserver里执行sql文件GenerateDataBase.sql。

    (3)  修改appsettings.json中的ConnectionStrings的数据库连接地址。

640?wx_fmt=png

  2.项目分层说明

                   表现层:Equinox.UI.Web、Equinox.Services.Api

                   应用服务层: Equinox.Application

                   领域层: Equinox.Domain、Equinox.Domain.Core

                   基础设施层: Equinox.Infra.Data(EF持久化)

                   基础设施层下的横切关注点:

                     Equinox.Infra.CrossCutting.Bus(事件和命令总线)

                     Equinox.Infra.CrossCutting.Identity(用户管理如登录、注册、授权)

                     Equinox.Infra.CrossCutting.IoC(控制反转的服务注入)

  3. 项目架构流程梳理图

640?wx_fmt=png

四.表现层分析

  在表现层是Equinox.UI.Web和Equinox.Services.Api 服务。在Equinox.UI.Web下主要是用控制器中的CustomerController来演示CQRS框架的实现,以及AccountController和ManageController的用户登录、注册、退出和用户信息管理。

  对于AccountController和ManageController两个控制器关联着Equinox.Infra.CrossCutting.Identity项目。Identity项目包括了需要用的视图模型、对系统的授权、自定义用户表数据、用户数据同步到数据库的迁移版本管理、邮件和SMS。对于授权方案通过Equinox.Infra.CrossCutting.IoC来注入服务。如下所示:

        // ASP.NET Authorization Polices
services.AddSingleton<IAuthorizationHandler, ClaimsRequirementHandler>();

  Equinox.Services.Api项目实现的功能与Web站点差不多,是通过暴露Web API来实现。下面是表现层的二个项目:

640?wx_fmt=png

五. 应用服务层分析

  Equinox.Application应用服务层包括对AutoMapper的配置管理,通过AutoMapper实现视图模型和领域模型的实体互转。定义ICustomerAppService服务接口供表现层调用,由CustomerAppService类来实现该接口。项目包含了Customer需要的视图模型。还有事件源EventSource。

  由CustomerAppService类来实现表现层的查询、命令、获取事件源。项目结构如下:

640?wx_fmt=png

六.领域层Domain.Core分析

  领域层是项目分层架构中,最重要的一层,也是相对复杂的一层。该层作者用了二个项目包括:Domain.Core和Domain。Domain.Core项目结构如下所示:

640?wx_fmt=png

  

对于Domain.Core项目主要是定义命令和事件的基类。源头是定义的抽象类Message。对于命令和事件,任何前端都会发送消息给应用程序层, Message消息就是数据传输对象,通常消息定义为一个Message基类开始,作为数据容器。

  这里使用MediatR中间件作为命令和事件的实现。MediatR支持两种消息类型:Request/Response和Notification。先看下Message消息基类定义:

    //注入服务
services.AddMediatR(typeof(Startup));
    /// <summary>
/// Message消息
/// 放入通用属性,甚至是普通标记,没有属性
/// </summary>
public abstract class Message : IRequest<bool>
{
/// <summary>
/// 消息类型:实现Message的命令或事件类型
/// </summary>
public string MessageType { get; protected set; }

/// <summary>
/// 聚合ID
/// </summary>
public Guid AggregateId { get; protected set; }

protected Message()
{
MessageType
= GetType().Name;
}
}

  消息有二种:命令和事件。两种消息都包含了数据传输对象。命令和事件有些微妙差别,命令和事件都是Message派生类。

    /// <summary>
/// Event 领域消息
/// 事件类是不可变的,它表示已经发生的事情,意味着只有私有set,没有写入方法。
/// 事件存放通用属性,例如事件触发时间,触发的用户,数据版本号。
/// </summary>
public abstract class Event : Message, INotification
{
public DateTime Timestamp { get; private set; }

protected Event()
{
//事件时间
Timestamp = DateTime.Now;
}
}
    /// <summary>
/// Command领域命令(增删改),不返回任何结果(void),但会改变数据对象的状态。
/// </summary>
public abstract class Command : Message
{
public DateTime Timestamp { get; private set; }

//DTO绑定验证,使用Fluent API来实现
public ValidationResult ValidationResult { get; set; }

protected Command()
{
//命令时间
Timestamp = DateTime.Now;
}

//实现Command抽象类的DTO数据验证
public abstract bool IsValid();
}

  Domain.Core项目还定义了领域实体和领域值对象的基类实现。例如:在领域实体基类中实现了相等性、运算符重载、重写HashCode。对于实体和值对象主要区别是:实体有明确的身份标识如主键ID,GUID。

      public abstract class Entity
  
public abstract class ValueObject<T> where T : ValueObject<T>

  Domain.Core项目中的Notifications消息文件夹,用来确认消息发送后的处理状态。下面是表现层发送更新命令后,IsValidOperation()确认消息处理的状态情况。

        [HttpPost]
[Authorize(Policy
= "CanWriteCustomerData")]
[Route(
"customer-management/edit-customer/{id:guid}")]
[ValidateAntiForgeryToken]
public IActionResult Edit(CustomerViewModel customerViewModel)
{
if (!ModelState.IsValid) return View(customerViewModel);

_customerAppService.Update(customerViewModel);

if (IsValidOperation())
ViewBag.Sucesso
= "Customer Updated!";

return View(customerViewModel);
}

  Domain.Core项目中的Bus文件夹,用来做命令总线和事件总线的发送接口,由Equinox.Infra.CrossCutting.Bus项目来实现总线接口的发送。

七.领域层Domain分析

  下面是Domain项目结构如下:

640?wx_fmt=png

  在上面结构中,Commands和Events文件夹分别用来存储命令和事件的数据传输对象,是贫血的DTO类,也可以理解为领域实体。例如Commands文件夹下命令数据传输对象定义:

     /// <summary>
/// Customer数据转输对象抽象类,放Customer通过属性
/// </summary>
public abstract class CustomerCommand : Command
{
public Guid Id { get; protected set; }

public string Name { get; protected set; }

public string Email { get; protected set; }

public DateTime BirthDate { get; protected set; }
}
    /// <summary>
/// Customer注册命令消息参数
/// </summary>
public class RegisterNewCustomerCommand : CustomerCommand
{
public RegisterNewCustomerCommand(string name, string email, DateTime birthDate)
{
Name
= name;
Email
= email;
BirthDate
= birthDate;
}

/// <summary>
/// 命令信息参数验证
/// </summary>
/// <returns></returns>
public override bool IsValid()
{
ValidationResult
= new RegisterNewCustomerCommandValidation().Validate(this);
return ValidationResult.IsValid;
}
}

  当在应用服务层发送命令(Bus.SendCommand)后,由领域层的CommandHandlers文件夹下的类来处理命令,再调用EF持久层来改变实体状态。下面梳理下命令的执行流程,由表现层开始一个customer新增如下所示

640?wx_fmt=png

    当在表现层点击Create后,调用应用服务层Register方法,触发一个新增事件,代码如下:

        /// <summary>
/// 新增
/// </summary>
/// <param name="customerViewModel">视图模型</param>
public void Register(CustomerViewModel customerViewModel)
{
//将视图模型 映射到 RegisterNewCustomerCommand 新增命令实体
var registerCommand = _mapper.Map<RegisterNewCustomerCommand>(customerViewModel);
Bus.SendCommand(registerCommand);
}

     当SendCommand发送命令后,由领域层CustomerCommandHandler类中的Handle来处理该命令,如下所示:

         /// <summary>
/// Customer注册命令处理
/// </summary>
/// <param name="message"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task<bool> Handle(RegisterNewCustomerCommand message, CancellationToken cancellationToken)
{
//对实体属性进行验证
if (!message.IsValid())
{
NotifyValidationErrors(message);
return Task.FromResult(false);
}

//将命令消息转成领域实体
var customer = new Customer(Guid.NewGuid(), message.Name, message.Email, message.BirthDate);

//如果注册用户邮件已存在,发起一个事件
if (_customerRepository.GetByEmail(customer.Email) != null)
{
Bus.RaiseEvent(
new DomainNotification(message.MessageType, "The customer e-mail has already been taken."));
return Task.FromResult(false);
}

//由Equinox.Infra.Data.Repository来实现数据持久化。事件是过去在系统中发生的事情。该事件通常是命令的结果.
_customerRepository.Add(customer);

//新增成功后,使用事件记录这次命令。
if (Commit())
{
Bus.RaiseEvent(
new CustomerRegisteredEvent(customer.Id, customer.Name, customer.Email, customer.BirthDate));
}

return Task.FromResult(true);
}

    下面是注册customer的信息,以及注册产生的事件数据,如下所示:

640?wx_fmt=png

640?wx_fmt=png

  在领域层的Interfaces文件夹中,最重要的包括IRepository<TEntity>接口,是通过Equinox.Infra.Data.Repository来实现接口,来进行数据持久化。下面是领域层仓储接口:

    /// <summary>
/// 领域层仓储接口,定义了通用的方法
/// </summary>
/// <typeparam name="TEntity"></typeparam>
public interface IRepository<TEntity> : IDisposable where TEntity : class
{
void Add(TEntity obj);
TEntity GetById(Guid id);
IQueryable
<TEntity> GetAll();
void Update(TEntity obj);
void Remove(Guid id);
int SaveChanges();
}
    /// <summary>
/// Customer仓储接口,在基数仓储上扩展
/// </summary>
public interface ICustomerRepository : IRepository<Customer>
{
Customer GetByEmail(
string email);
}

   Interfaces文件夹中还定义了IUser和IUnitOfWork接口类,也是需要Equinox.Infra.Data.Repository来实现。

八. 基础设施层分析

   Equinox.Infra.Data项目是EF用来持久化命令和事件,以及查询数据的仓储,结构如下:

640?wx_fmt=png

  其中UoW文件夹下的UnitOfWork类用来实现领域层的IUnitOfWork,使用Commit保存数据。

      public bool Commit()
{
return _context.SaveChanges() > 0;
}

  Repository文件夹下的类用来实现领域层的IRepository接口,使用EF的DbSet来操作EF TEntity对象,再调用Commit提交到数据库。

      public virtual void Add(TEntity obj)
{
DbSet.Add(obj);
}

  Repository文件夹下还包含EventSourcing事件源,存储到StoredEvent表中。

九.命令总线分析

  Equinox.Infra.CrossCutting.Bus项目中使用了中间件MediatR,定义了InMemoryBus类来实现领域层的IMediatorHandler命令总线接口发送,使用SendCommand (T)和RaiseEvent (T)方法发送命令和事件。

  MediatR是用于消息发送和消息处理的解耦,MediatR是一种进程内消息传递机制。 支持以同步或异步的形式进行请求/响应,命令,查询,通知和事件的消息传递,并通过C#泛型支持消息的智能调度。 其中IRequest和INotification分别对应单播和多播消息的抽象。

  例如:在领域层中,Message消息实现IRequest,代码如下:

    /// <summary>
/// Message消息
/// 放入通用属性,甚至是普通标记,没有属性。IRequest<T> - 有返回值
/// </summary>
public abstract class Message : IRequest<bool>

  最后Equinox.Infra.CrossCutting.Identity主要做用户管理,授权,迁移管理。Equinox.Infra.CrossCutting.IoC做整个解决方案下项目需要的服务注入。

参考文献:

  Introduction-to-CQRS

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

原文地址:https://www.cnblogs.com/MrHSR/p/10820545.html

.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com 
640?wx_fmt=jpeg

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/316017.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

仿B站(一) 目的分析以及创建 WebAPI + Angular7 项目

前言&#xff1a;本系列文章主要为对所学 Angular 框架的一次微小的实践&#xff0c;对 b站页面作简单的模仿。本系列文章主要参考资料&#xff1a;微软文档&#xff1a;    https://docs.microsoft.com/zh-cn/aspnet/core/getting-started/?viewaspnetcore-2.1&tabsw…

Mac中搭建Kubernetes

Kubernetes是Google和RadHat公司共同主导的开源容器编排项目&#xff0c;功能非常强大&#xff0c;也非常的火热和流行&#xff0c;但同时里面也有很多的概念和名词需要我们去学习和理解。学习任何一个技术先需要把基础环境搭建起来&#xff0c;本篇就介绍怎样在Mac中启动单节点…

树莓派也跑Docker和.NET Core

树莓派就是一个卡片大小的迷你电脑。有了电脑&#xff0c;我们当然得先安装系统。系统下载https://www.raspberrypi.org/downloads/raspbian/ &#xff0c;我选择的Raspbian Stretch Lite&#xff0c;不带界面的最小安装。下载win32diskimager&#xff08;烧录系统&#xff09;…

开源]OSharpNS 步步为营系列 - 1. 业务模块设计

OSharpNS全称OSharp Framework with .NetStandard2.0&#xff0c;是一个基于.NetStandard2.0开发的一个.NetCore快速开发框架。这个框架使用最新稳定版的.NetCore SDK&#xff08;当前是.NET Core 2.2&#xff09;&#xff0c;对 AspNetCore 的配置、依赖注入、日志、缓存、实体…

CF1479A Searching Local Minimum

CF1479A Searching Local Minimum 题意&#xff1a; 题解&#xff1a; 先说结论&#xff1a; 若l&#xff0c;r满足&#xff1a; al−1>al,ar<ar1a_{l-1}>a_{l},a_{r}<a_{r1}al−1​>al​,ar​<ar1​al,al1,....,ara_{l},a_{l1},....,a_{r}al​,al1​,....…

C#8.0的两个有趣的新特性以及gRPC

最近每天忙着跑很多地方&#xff0c;回家就不想动了&#xff0c;没什么心情写东西。今天有空&#xff0c;稍微写一点。下文中&#xff1a;关于C#语法特性的部分需要Visual Studio 2019支持。关于.NET Core的部分需要安装.NET 3.0 Preview4&#xff0c;低版本或许也可以但我没实…

CF1479C Continuous City

CF1479C Continuous City 题意&#xff1a; 给定 L, R. 构造一个有向带权图, 其中点数不大于 32, 且所有边都是从较小的点指向较大的点. 假设这个有向图有 n 个点, 你需要保证从 1到n 的所有路径的权值都在 [L, R]内且不存在 x∈[L,R], 使得不存在或存在多于一条从 1 到 n 的…

Office转PDF,Aspose太贵,怎么办?

在程序开发中经常需要将Office文件转换成PDF&#xff0c;著名的Aspose的三大组件可以很容易完成这个功能&#xff0c;但是Aspose的每个组件都单独收费&#xff0c;而且每个都卖的不便宜。在老大的提示下&#xff0c;换了一种思路来解决这个问题。环境dotNetCore:2.1CentOS:7.5D…

收起.NET程序的dll来

作为上床后需要下床检查好几次门关了没有的资深强迫症患者&#xff0c;有一个及其搞我的问题&#xff0c;就是dll问题。曾几何时&#xff0c;在没有nuget的年代&#xff0c;当有依赖项需要引用的时候&#xff0c;只能通过文件引用来管理引用问题&#xff0c;版本问题&#xff0…

从壹开始 [ Ids4实战 ] 之三║ 详解授权持久化 用户数据迁移

哈喽大家周三好&#xff0c;今天终于又重新开启 IdentityServer4 的落地教程了&#xff0c;不多说&#xff0c;既然开始了&#xff0c;就要努力做好?。书接上文&#xff0c;在很久之前的上篇文章《二║ 基础知识集合 & 项目搭建一》中&#xff0c;我们简单的说了说 Identi…

微软XAML Studio - WPF, UWP, Xamarin等技术开发者的福音

最近在继续倒腾WPF的项目&#xff0c;继续使用Caliburn.Micro和Xceed来堆代码。每次调试xaml上的binding&#xff0c;都有种要疯的赶脚。今天路过 https://channel9.msdn.com/ 浏览 WPF相关的学习视频时&#xff0c;遇到微软推荐的相关视频 - XAML sutdio简介https://channel9.…

AddMvc 和 AddMvcCore 的区别

目录本文出自《从零开始学 ASP.NET CORE MVC》目录 视频课程效果更佳&#xff1a;从零开始学 Asp.Net Core MVC ASP.NET Core 为什么有 AddMvc 和 AddMvcCore 他们是什么关系&#xff1f;在本视频中&#xff0c;我们将讨论 AddMvc()和 AddMvcCore()方法之间的区别。要在 ASP.NE…

浅谈容量规划

俗话说&#xff0c;”人无远虑&#xff0c;必有近忧”&#xff0c;容量规划就是”远虑”。所谓容量规划&#xff0c;是一个产品满足用户目标需求而决定生产能力的过程。当产品发展到一个较为稳定成熟的阶段&#xff0c;产品的整体处理能力的把控自然是不可或缺&#xff0c;尽管…

CF1063C Dwarves, Hats and Extrasensory Abilities

CF1063C Dwarves, Hats and Extrasensory Abilities 题意&#xff1a; 首先题目会给出 n &#xff0c;表示要输入多少点。 然后你输出n 个点的坐标&#xff0c;每输出一个点会告诉你这个点的颜色是黑色或者白色。 最后你需要输出两个点的坐标代表一条直线&#xff0c;这条直线…

Blazor——Asp.net core的新前端框架

Blazor是微软在Asp.net core 3.0中推出的一个前端MVVM模型&#xff0c;它可以利用Razor页面引擎和C#作为脚本语言来构建WEB页面&#xff0c;如下代码简单演示了它的基本功能&#xff1a;和Angular JS和VUE的模型非常类似&#xff0c;Blazor 支持大多数应用所需的核心方案&#…

CF1149B Three Religions

CF1149B Three Religions 题意&#xff1a; 给定长度为 n 的母串和三个子串s1,s2,s3s_1,s_2,s_3s1​,s2​,s3​ 。初始时子串均为空。有 q 次询问。你需要支持两种操作&#xff1a;向某个子串末尾添加一个字母&#xff0c;或者删去某个子串末尾的字母。在每次操作后&#xff…

【译文】领域模型的五个特征

我在这篇博客文章中&#xff0c;我试图给领域模型下一个非常合适的定义&#xff0c;我发现我的这些定义都不太妥当&#xff0c;不过&#xff0c;我们还是可以先来看一下wiki百科对领域驱动模型下的定义&#xff1a;问题解决和软件工程中的领域模型可以被认为是感兴趣的领域&…

使用ASP.NET Core 实现Docker的HealthCheck指令

写在前面HealthCheck 不仅是对应用程序内运行情况、数据流通情况进行检查&#xff0c; 还包括应用程序对外部服务或依赖资源的健康检查。健康检查通常是以暴露应用程序的HTTP端点的形式 实施&#xff0c;可用于配置健康探测的的场景有 &#xff1a;容器或负载均衡器 探测应用状…

VS Code 中有哪些好用的 Azure 插件?

在之前的文章中&#xff0c;我们提到了 Amazon、Google、IBM、Red Hat、Salesforce、Pivotal 等大厂都在 VS Code 中有提供相应的开发工具&#xff1a;亚马逊上了 VS Code 的船&#xff0c;还有哪些大厂也上了船&#xff1f;微软必定更是把 VS Code 作为其最重要的开发者平台了…

邀请 | 关于微软容器服务,你需要知道的二三事

容器是近年来特别火的话题&#xff0c;那么&#xff0c;什么是容器&#xff1f;为什么我们需要容器服务&#xff1f;微软容器服务特点是什么&#xff1f;今天我们用一种特殊的方式为大家介绍这三大话题。什么是容器这个部分&#xff0c;我们用一个故事进行开场。很久以前&#…