OnionArch 2.0 - 基于DDD的洋葱架构改进版开源
大家好,去年我发布了一篇 OnionArch - 采用DDD+CQRS+.Net 7.0实现的洋葱架构 。很多程序员都比较感兴趣,给我要源代码。这次我把OnionArch进行了升级,改进了一些特性,并放出源代码,iamxiaozhuang/OnionArch2 (github.com) 欢迎大家研究使用。
一、自动生成和发布领域事件
我在OninArch1.0实现了对删除的实体自动生成和发布领域事件,并通过订阅这个领域事件,将删除的实体数据备份至回收站表中,以备审计和数据恢复。
本次我改进了这个特性,对实体数据的新增,修改和删除都会自动生成和发布领域事件。我认为尽量不要通过修改代码来新增和发布领域事件,这会导致新增的业务功能也需要修改代码而不是新增代码来实现,不符合对修改关闭和对扩展开放的设计原则。应该对实体数据的任何变动都自动发布领域事件,然后在事件Handler中筛选需要的领域事件并进行处理。
我基于这个特性实现了按配置自动审计记录和和按配置自动发布集成事件功能。
按配置自动审计功能
我们可以通过配置的方式来实现那些实体,那种修改类型,那个字段需要审计。请看如下配置:
"EntityChangedAuditLogsConfig": [ { "EntityFullName": "OnionArch.Domain.ProductInventory.ProductInventory", "ChangeType": "Added" }, { "EntityFullName": "OnionArch.Domain.ProductInventory.ProductInventory", "ChangeType": "Modified", "Properties": "InventoryAmount" }, { "EntityFullName": "OnionArch.Domain.ProductInventory.ProductInventory", "ChangeType": "Deleted" } ],
可以按照实体的全名,修改类型(新增,修改,删除),甚至是实体的修改字段来配置是否需要进行数据审计,如过需要审计则会自动保存审计日志到审计表,审计表包含实体的原值和当前值,修改人和修改时间等。
按配置自动发布集成事件功能
我们可以通过配置来实现该微服务的那些领域事件需要转为集成事件发布出去,供其它的微服务订阅使用。这样我们在微服务中新增集成事件订阅的时候就不需要修改源微服务的代码,只需要在源微服务中增加配置即可。
我们需要配置Dapr的发布订阅名称,事件Topic和这个Topic的发布条件,例如,在产品仓库实体的库存数量被修改后发布Topic为“ProductInventoryAmountChanged”集成事件。
"EntityChangedIntegrationEventConfig": { "PubsubName": "pubsub", "Topics": [ { "TopicName": "ProductInventoryAmountChanged", "EntityFullName": "OnionArch.Domain.ProductInventory.ProductInventory", "ChangeType": "Modified", "Properties": "InventoryAmount" } ] },
然后就可通过Dapr在其它的微服务中订阅并处理该集成事件,通过Dapr发布集成事件的代码请查看源代码。
已发布的集成事件也会自动保存至集成事件记录表中,以备对该事件进行后续执行跟踪和重发。
二、自动生成Minimal WebApi接口
该特性我在 根据MediatR的Contract Messages自动生成Minimal WebApi接口 中做过介绍。因为OninArch通过MediatR实现了CQRS和其它AOP功能,例如业务实体验证,异常处理、工作单元等特性。
本次将OninArch1.0的GRPC接口替换成了自动生成的WebAPI接口。并对自动生成WebAPI接口做了改进,可以指定生成的WebAPI的Http方法,地址、介绍和详细说明。自动对接口按命名空间分类,将Get方法参数自动映射到Query参数等。
[MediatorWebAPIConfig(HttpMethod = HttpMethodToGenerate.Post, HttpUrl = "/productinventory", Summary = "创建产品库存", Description = "创建产品库存 Description")] public class CreateProductInventory : ICommand<Unit> { public string ProductCode { get; set; } public int InventoryAmount { get; set; } } [MediatorWebAPIConfig(HttpMethod = HttpMethodToGenerate.Patch, HttpUrl = "/productinventory/increase", Summary = "增加产品库存")] public class IncreaseProductInventory : ICommand<Unit> { public Guid Id { get; set; } public int Amount { get; set; } }
生成的WebAPI:
三、对充血模型的支持
我在OninArch1.0中并没有刻意强调充血模型,本次按照我对充血模型的理解改进了仓储代码,即,仓储服务只实现实体的Add,Remove和Query,不实现实体的Create和Modify。实体的创建和修改必须放入实体中实现。也就是说,实体字段的set都是私有的,只能在实体内部对实体的字段进行修改,以保证将业务逻辑封装到实体中,并提高系统的稳定性和业务逻辑重用性。
public static ProductInventory Create<TModel>(TModel model) { //var entity = new ProductInventory(); var entity = model.Adapt<ProductInventory>(); return entity; } public ProductInventory Update<TModel>(ProductInventory entity, TModel model) { model.Adapt(entity); return entity; } public int InventoryAmount { get; private set; } public void IncreaseInventory(int amount) { this.InventoryAmount += amount; }
仓储接口新增了Edit方法,以获取实体对象,再调用实体对象内部方法进行实体数据的修改。
仓库接口的Query方法不直接返回实体对象,而是直接返回Model对象(Dto、VO),提高数据库查询性能(通过Mapster的ProjectToType方法实现)。
仓储服务代码如下:
using MediatR; using OnionArch.Domain.Common.Entities; using OnionArch.Domain.Common.Paged; using System.Linq.Expressions; namespace OnionArch.Domain.Common.Repositories { public class RepositoryService<TEntity> where TEntity : BaseEntity { private readonly IMediator _mediator; public RepositoryService(IMediator mediator) { _mediator = mediator; } /// <summary> /// 创建单个实体 /// </summary> /// <param name="entity"></param> /// <returns></returns> public async Task<TEntity> Add(TEntity entity) { return await _mediator.Send(new AddEntityRequest<TEntity>(entity)); } /// <summary> /// 创建多个实体 /// </summary> /// <param name="entities"></param> /// <returns></returns> public async Task Add(params TEntity[] entities) { await _mediator.Send(new AddEntitiesRequest<TEntity>(entities)); } /// <summary> /// 删除单个实体 /// </summary> /// <param name="Id"></param> /// <returns></returns> public async Task<TEntity> Remove(Guid Id) { return await _mediator.Send(new RemoveEntityRequest<TEntity>(Id)); } /// <summary> /// 删除多个实体 /// </summary> /// <param name="whereLambda"></param> /// <returns></returns> public async Task<int> Remove(Expression<Func<TEntity, bool>> whereLambda) { return await _mediator.Send(new RemoveEntitiesRequest<TEntity>(whereLambda)); } /// <summary> /// 获取单个实体以更新实体字段 /// </summary> /// <param name="Id"></param> /// <returns></returns> public async Task<TEntity> Edit(Guid Id) { return await _mediator.Send(new EditEntityRequest<TEntity>(Id)); } public async Task<IQueryable<TEntity>> Edit(Expression<Func<TEntity, bool>> whereLambda) { return await _mediator.Send(new EditEntitiesRequest<TEntity>(whereLambda)); } /// <summary> /// 查询单个实体(不支持更新实体) /// </summary> /// <param name="Id"></param> /// <returns></returns> public async Task<TModel> Query<TModel>(Guid Id) { return await _mediator.Send(new QueryEntityRequest<TEntity,TModel>(Id)); } /// <summary> /// 查询多个实体 /// </summary> public async Task<IQueryable<TModel>> Query<TModel>(Expression<Func<TEntity, bool>> whereLambda) { return await _mediator.Send(new QueryEntitiesRequest<TEntity,TModel>(whereLambda)); } /// <summary> /// 分页查询多个实体 /// </summary> /// <typeparam name="TOrder"></typeparam> /// <param name="whereLambda"></param> /// <param name="pageOption"></param> /// <param name="orderbyLambda"></param> /// <param name="isAsc"></param> /// <returns></returns> public async Task<PagedResult<TModel>> Query<TOrder,TModel>(Expression<Func<TEntity, bool>> whereLambda, PagedOption pagedOption, Expression<Func<TEntity, TOrder>> orderbyLambda, bool isAsc = true) { return await _mediator.Send(new QueryPagedEntitiesRequest<TEntity, TOrder,TModel>(whereLambda, pagedOption, orderbyLambda, isAsc)); } /// <summary> /// 判断是否有存在 /// </summary> /// <param name="whereLambda"></param> /// <returns></returns> public async Task<bool> Any(Expression<Func<TEntity, bool>> whereLambda) { return await _mediator.Send(new AnyEntitiesRequest<TEntity>(whereLambda)); } } }
四、对采用MediatR代替接口的探索
如上仓库服务代码,我并没有创建仓库接口并实现,而是完全基于MediatR直接实现仓库服务。这个我在MediatRPC - 基于MediatR和Quic通讯实现的RPC框架,比GRPC更简洁更低耦合,开源发布第一版 的MediatR编程思想中做过介绍,本次是实现这个编程思想,即不通过接口和依赖注入,而是通过MediatR来实现控制反转。如果大家不喜欢这种方式也可以修改回接口的方式。
鉴于篇幅所限,不能一一说明本次升级的所有改动,请大家下载代码自行研究,下面又到了找工作时间(是的,我还在找工作)。
五、找工作
▪ 博主有15年以上的软件技术经验(曾担任架构师和技术 Leader),擅长云原生、微服务和领域驱动软件架构设计,.Net Core 开发。
▪ 博主有15年以上的项目交付经验(曾担任项目经理和产品经理),专注于敏捷(Scrum )项目管理,业务分析和产品设计。
▪ 博主熟练配置和使用 Microsoft Azure 和Microsoft 365云(曾担任微软顾问)。
▪ 博主为人诚恳,工作认真负责,态度积极乐观。
我家在广州,也可以去深圳工作。做架构师、产品经理、项目经理都可以。有工作机会推荐的朋友可以加我微信 15920128707,微信名字叫Jerry。