我的第一个微服务系列(九):使用领域驱动创建Project项目
一、领域驱动设计DDD
因为在之前的工作中也未曾涉及到DDD,所以到最近才开始学习。看过《实现领域驱动设计》这本书,说实话,看完还是一知半解,在实际项目中怎么应用,也许需要锤炼才能逐渐明白。还好,在博客园里面找到了比较有深度的阐述DDD的文章进行学习,感谢。
关于DDD的限界上下文,实体、聚合、值对象、领域服务、仓储等概念这里就不说了。个人觉得DDD最重要的还是在对业务需求的把控上,只有深入了解了业务,才能更好的界定上下文。
DDD中认为涉及到操作数据库的需要通过聚合根来实现,其他的不应该直接的操作数据库。
关于DDD更多的知识可以参考:
Jesse的关于DDD的博文: https://www.cnblogs.com/jesse2013/category/610362.html
dax.net 关于DDD的博文: https://www.cnblogs.com/daxnet/archive/2010/11/02/1867392.html
二、CQRS命令查询职责分离
CQRS本身只是一个读写分离的架构思想,全称是:Command Query Responsibility Segregation,即命令查询职责分离,表示在架构层面,将一个系统分为写入(命令)和查询两部分。一个命令表示一种意图,表示命令系统做什么修改,命令的执行结果通常不需要返回;一个查询表示向系统查询数据并返回。
CQRS架构中,另外一个重要的概念就是事件,事件表示命令操作领域中的聚合根,然后聚合根的状态发生变化后产生的事件。
在我们的本项目中将使用MediatR来实现CQRS。
关于CQRS的更多内容可以学习汤雪华的博客:https://www.cnblogs.com/netfocus/
三、MediraR
什么是MediatR呢?我们看下其 Github 上的介绍。
.NET中的简单中介者模式实现,一种进程内消息传递机制(无其他外部依赖)。 支持以同步或异步的形式进行请求/响应,命令,查询,通知和事件的消息传递,并通过C#泛型支持消息的智能调度。
其核心是一个中介者模式的.NET实现,其目的是消息发送和消息处理的解耦。它支持以单播和多播形式使用同步或异步的模式来发布消息,创建和侦听事件。
关于其使用可以参考:https://github.com/jbogard/MediatR/wiki
四、项目分层
我们将项目分为Project.Domain、Project.Infrastructure、Project.Api。经典的DDD分层为四层,领域层、基础设施层、应用层、展现层。这里我们把应用层和Project.Api放到一起。
1、实现领域层Domain。定义实体,聚合根,值对象。在实体中我们可以添加或移除领域事件。
using System; using MediatR; using System.Collections.Generic; namespace Project.Domain.Seedwork { /// <summary> /// Description: Entity /// Author: Jesen /// Version: 1.0 /// Date: 2019/8/8 22:36:46 /// </summary> public abstract class Entity { int? _requestedHashCode; int _Id; public virtual int Id { get { return _Id; } protected set { _Id = value; } } private List<INotification> _domainEvents; public IReadOnlyCollection<INotification> DomainEvents => _domainEvents?.AsReadOnly(); public void AddDomainEvent(INotification eventItem) { _domainEvents = _domainEvents ?? new List<INotification>(); _domainEvents.Add(eventItem); } public void RemoveDomainEvent(INotification eventItem) { _domainEvents?.Remove(eventItem); } public void ClearDomainEvents() { _domainEvents?.Clear(); } public bool IsTransient() { return this.Id == default(Int32); } public override bool Equals(object obj) { if (obj == null || !(obj is Entity)) return false; if (Object.ReferenceEquals(this, obj)) return true; if (this.GetType() != obj.GetType()) return false; Entity item = (Entity)obj; if (item.IsTransient() || this.IsTransient()) return false; else return item.Id == this.Id; } public override int GetHashCode() { if (!IsTransient()) { if (!_requestedHashCode.HasValue) _requestedHashCode = this.Id.GetHashCode() ^ 31; // XOR for random distribution (http://blogs.msdn.com/b/ericlippert/archive/2011/02/28/guidelines-and-rules-for-gethashcode.aspx) return _requestedHashCode.Value; } else return base.GetHashCode(); } public static bool operator ==(Entity left, Entity right) { if (Object.Equals(left, null)) return (Object.Equals(right, null)) ? true : false; else return left.Equals(right); } public static bool operator !=(Entity left, Entity right) { return !(left == right); } } }
namespace Project.Domain.Seedwork { /// <summary> /// Description: IAggregateRoot /// Author: Jesen /// Version: 1.0 /// Date: 2019/8/8 22:36:46 /// </summary> public interface IAggregateRoot { } }
using System.Collections.Generic; using System.Linq; namespace Project.Domain.Seedwork { /// <summary> /// Description: ValueObject /// Author: Jesen /// Version: 1.0 /// Date: 2019/8/8 22:36:46 /// </summary> public abstract class ValueObject { protected static bool EqualOperator(ValueObject left, ValueObject right) { if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null)) { return false; } return ReferenceEquals(left, null) || left.Equals(right); } protected static bool NotEqualOperator(ValueObject left, ValueObject right) { return !(EqualOperator(left, right)); } protected abstract IEnumerable<object> GetAtomicValues(); public override bool Equals(object obj) { if (obj == null || obj.GetType() != GetType()) { return false; } ValueObject other = (ValueObject)obj; IEnumerator<object> thisValues = GetAtomicValues().GetEnumerator(); IEnumerator<object> otherValues = other.GetAtomicValues().GetEnumerator(); while (thisValues.MoveNext() && otherValues.MoveNext()) { if (ReferenceEquals(thisValues.Current, null) ^ ReferenceEquals(otherValues.Current, null)) { return false; } if (thisValues.Current != null && !thisValues.Current.Equals(otherValues.Current)) { return false; } } return !thisValues.MoveNext() && !otherValues.MoveNext(); } public override int GetHashCode() { return GetAtomicValues() .Select(x => x != null ? x.GetHashCode() : 0) .Aggregate((x, y) => x ^ y); } public ValueObject GetCopy() { return this.MemberwiseClone() as ValueObject; } } }
定义仓储接口
using System; using System.Threading; using System.Threading.Tasks; namespace Project.Domain.Seedwork { /// <summary> /// Description: IUnitOfWork /// Author: Jesen /// Version: 1.0 /// Date: 2019/8/8 22:36:46 /// </summary> public interface IUnitOfWork : IDisposable { Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken)); Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default(CancellationToken)); } }
namespace Project.Domain.Seedwork { /// <summary> /// Description: IRepository /// Author: Jesen /// Version: 1.0 /// Date: 2019/8/8 22:36:46 /// </summary> public interface IRepository<T> where T : IAggregateRoot { IUnitOfWork UnitOfWork { get; } } }
using Project.Domain.Seedwork; using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; namespace Project.Domain.AggregatesModel { /// <summary> /// Description: IProjectRepository /// Author: Jesen /// Version: 1.0 /// Date: 2019/8/8 22:30:09 /// </summary> public interface IProjectRepository : IRepository<Project> { Task<Project> GetAsync(int id); Project Add(Project project); void Update(Project project); } }
在此项目中,项目查看者和项目参与者都是需要现有项目后才能拥有,因此将项目作为聚合根来对待。
using Project.Domain.Events; using Project.Domain.Seedwork; using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Project.Domain.AggregatesModel { /// <summary> /// Description: Project /// Author: Jesen /// Version: 1.0 /// Date: 2019/8/8 22:30:23 /// </summary> public class Project : Entity,IAggregateRoot { /// <summary> /// 用户Id /// </summary> public int UserId { get; set; } /// <summary> /// 用户名称 /// </summary> public string UserName { get; set; } /// <summary> /// 项目logo /// </summary> public string Avatar { get; set; } /// <summary> /// 公司名称 /// </summary> public string Company { get; set; } /// <summary> /// 原BP文件地址 /// </summary> public string OriginBPFile { get; set; } /// <summary> /// 转换后的BP文件地址 /// </summary> public string FormatBPFile { get; set; } /// <summary> /// 是否显示敏感信息 /// </summary> public bool ShowSecurityInfo { get; set; } /// <summary> /// 公司所在省Id /// </summary> public int ProvinceId { get; set; } /// <summary> /// 公司所在省名称 /// </summary> public string Province { get; set; } /// <summary> /// 公司所在城市Id /// </summary> public int CityId { get; set; } /// <summary> /// 公司所在城市名称 /// </summary> public string City { get; set; } /// <summary> /// 区域Id /// </summary> public int AreaId { get; set; } /// <summary> /// 区域名称 /// </summary> public string AreaName { get; set; } /// <summary> /// 公司成立时间 /// </summary> public DateTime RegisterTime { get; set; } /// <summary> /// 项目基本信息 /// </summary> public string Introduction { get; set; } /// <summary> /// 出让股份比例 /// </summary> public string FinPercentage { get; set; } /// <summary> /// 融资阶段 /// </summary> public string FinStage { get; set; } /// <summary> /// 融资金额 单位(万) /// </summary> public int FinMoney { get; set; } /// <summary> /// 收入 单位(万) /// </summary> public int Income { get; set; } /// <summary> /// 利润 单位(万) /// </summary> public int Revenue { get; set; } /// <summary> /// 估值 单位(万) /// </summary> public int Valuation { get; set; } /// <summary> /// 佣金分配方式 : 线下商议 等比例分配 /// </summary> public int BrokerageOptions { get; set; } /// <summary> /// 是否委托给平台finbook /// </summary> public bool OnPlatform { get; set; } /// <summary> /// 可见范围设置 /// </summary> public ProjectVisibleRule VisibleRule { get; set; } /// <summary> /// 根引用项目Id /// </summary> public int SourceId { get; set; } /// <summary> /// 上级引用项目Id /// </summary> public int ReferenceId { get; set; } /// <summary> /// 项目标签 /// </summary> public string Tags { get; set; } /// <summary> /// 项目属性:行业领域、融资币种 /// </summary> public List<ProjectProperty> Properties { get; set; } /// <summary> /// 贡献者列表 /// </summary> public List<ProjectContributor> Contributors { get; set; } /// <summary> /// 查看者 /// </summary> public List<ProjectViewer> Viewers { get; set; } /// <summary> /// 更新时间 /// </summary> public DateTime UpdateTime { get; set; } /// <summary> /// 创建时间 /// </summary> public DateTime CreatedTime { get; set; } private Project Clone(Project source = null) { if (source == null) { source = this; } var newProject = new Project() { AreaId = source.AreaId, AreaName = source.AreaName, Avatar = source.Avatar, BrokerageOptions = source.BrokerageOptions, City = source.City, CityId = source.CityId, Company = source.Company, Contributors = new List<ProjectContributor> { }, CreatedTime = DateTime.Now, FinMoney = source.FinMoney, FinPercentage = source.FinPercentage, FinStage = source.FinStage, FormatBPFile = source.FormatBPFile, Income = source.Income, Introduction = source.Introduction, OnPlatform = source.OnPlatform, OriginBPFile = source.OriginBPFile, Province = source.Province, ProvinceId = source.ProvinceId, ReferenceId = source.ReferenceId, RegisterTime = source.RegisterTime, Revenue = source.Revenue, ShowSecurityInfo = source.ShowSecurityInfo, SourceId = source.SourceId, Tags = source.Tags, UpdateTime = source.UpdateTime, UserId = source.UserId, Valuation = source.Valuation, Viewers = new List<ProjectViewer> { }, VisibleRule = source.VisibleRule == null ? null : new ProjectVisibleRule { Visible = source.VisibleRule.Visible, Tags = source.VisibleRule.Tags } }; newProject.Properties = new List<ProjectProperty> { }; foreach(var item in source.Properties) { newProject.Properties.Add(new ProjectProperty(item.Key, item.Text, item.Value)); } return newProject; } /// <summary> /// 参与者得到项目拷贝 /// </summary> /// <param name="contributorId"></param> /// <param name="source"></param> /// <returns></returns> public Project ContributorFork(int contributorId,Project source = null) { if(source == null) { source = this; } var newProject = Clone(source); newProject.UserId = contributorId; newProject.SourceId = source.SourceId; newProject.ReferenceId = source.ReferenceId; newProject.UpdateTime = DateTime.Now; return newProject; } public Project() { this.Viewers = new List<ProjectViewer>(); this.Contributors = new List<ProjectContributor>(); AddDomainEvent(new ProjectCreatedEvent { Project = this }); //添加创建项目事件 } public void AddViewer(int userId,string userName,string avatar) { ProjectViewer viewer = new ProjectViewer() { UserId = UserId, UserName = userName, Avatar = avatar, CreatedTime = DateTime.Now }; if(!Viewers.Any(v=>v.UserId == userId)) { Viewers.Add(viewer); AddDomainEvent(new ProjectViewedEvent { Company = this.Company, Introduction = this.Introduction, Viewer = viewer }); } } public void AddContributor(ProjectContributor contributor) { if (!Contributors.Any(c => c.UserId == contributor.UserId)) { Contributors.Add(contributor); AddDomainEvent(new ProjectJoinedEvent { Company = this.Company, Introduction = this.Introduction, Contributor = contributor }); } } } }
using Project.Domain.Seedwork; using System; using System.Collections.Generic; using System.Text; namespace Project.Domain.AggregatesModel { /// <summary> /// Description: ProjectContributor /// Author: Jesen /// Version: 1.0 /// Date: 2019/8/8 22:30:46 /// </summary> public class ProjectContributor : Entity { public int ProjectId { get; set; } public int UserId { get; set; } public string UserName { get; set; } public string Avatar { get; set; } public DateTime CreatedTime { get; set; } /// <summary> /// 关闭者 /// </summary> public bool IsCloser { get; set; } /// <summary> /// 参与者类型:1 财务顾问 2 投资机构 /// </summary> public int ContributorType { get; set; } } }
using Project.Domain.Seedwork; using System; using System.Collections.Generic; using System.Text; namespace Project.Domain.AggregatesModel { /// <summary> /// Description: ProjectProperty /// Author: Jesen /// Version: 1.0 /// Date: 2019/8/8 22:31:01 /// </summary> public class ProjectProperty : ValueObject { public ProjectProperty(string key,string text,string value) { this.Key = key; this.Text = text; this.Value = value; } public string Key { get; set; } public string Text { get; set; } public string Value { get; set; } public int ProjectId { get; set; } //违背了DDD值对象 protected override IEnumerable<object> GetAtomicValues() { yield return Key; yield return Text; yield return Value; } } }
using Project.Domain.Seedwork; using System; using System.Collections.Generic; using System.Text; namespace Project.Domain.AggregatesModel { /// <summary> /// Description: ProjectViewer /// Author: Jesen /// Version: 1.0 /// Date: 2019/8/8 22:31:13 /// </summary> public class ProjectViewer : Entity { public int ProjectId { get; set; } public int UserId { get; set; } public string UserName { get; set; } public string Avatar { get; set; } public DateTime CreatedTime { get; set; } } }
同时,当项目被创建、查看、参与的时候可能需要发布一些事件,所以我们定义如下领域事件。
using MediatR; namespace Project.Domain.Events { /// <summary> /// Description: ProjectCreatedEvent /// Author: Jesen /// Version: 1.0 /// Date: 2019/8/10 22:15:56 /// </summary> public class ProjectCreatedEvent :INotification { public AggregatesModel.Project Project { get; set; } } } using MediatR; using Project.Domain.AggregatesModel; namespace Project.Domain.Events { /// <summary> /// Description: ProjectJoinedEvent /// Author: Jesen /// Version: 1.0 /// Date: 2019/8/10 22:16:25 /// </summary> public class ProjectJoinedEvent : INotification { public string Company { get; set; } public string Introduction { get; set; } public ProjectContributor Contributor { get; set; } } } using MediatR; using Project.Domain.AggregatesModel; namespace Project.Domain.Events { /// <summary> /// Description: ProjectViewedEvent /// Author: Jesen /// Version: 1.0 /// Date: 2019/8/10 22:16:09 /// </summary> public class ProjectViewedEvent :INotification { public string Company { get; set; } public string Introduction { get; set; } public ProjectViewer Viewer { get; set; } } }
2、实现基础设施层
在基础设施层中,我们将定义数据访问上下文和具体的仓储实现
using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Text; using Project.Domain.Seedwork; using System.Threading; using System.Threading.Tasks; using MediatR; using Project.Infrastructure.EntityConfigurations; namespace Project.Infrastructure { /// <summary> /// Description: ProjectContext /// Author: Jesen /// Version: 1.0 /// Date: 2019/8/8 22:26:13 /// </summary> public class ProjectContext : DbContext, IUnitOfWork { private IMediator _mediator; public DbSet<Domain.AggregatesModel.Project> Projects { get; set; } public ProjectContext(DbContextOptions<ProjectContext> options, IMediator mediator) : base(options) { _mediator = mediator; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfiguration(new ProjectEntityConfiguration()); modelBuilder.ApplyConfiguration(new ProjectPropertyConfiguration()); modelBuilder.ApplyConfiguration(new ProjectPropertyConfiguration()); modelBuilder.ApplyConfiguration(new ProjectViewerConfiguration()); modelBuilder.ApplyConfiguration(new ProjectContributorConfiguration()); //modelBuilder.Entity<Domain.AggregatesModel.Project>() // .ToTable("Projects") // .HasKey(p => p.Id); } public async Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken= default(CancellationToken)) { //发送领域事件 await _mediator.DispatchDomainEventsAsync(this); await base.SaveChangesAsync(); return true; } } }
using Project.Domain.AggregatesModel; using Project.Domain.Seedwork; using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; using System.Linq; using ProjectEntity = Project.Domain.AggregatesModel.Project; using Microsoft.EntityFrameworkCore; namespace Project.Infrastructure.Repositories { /// <summary> /// Description: ProjectRepository /// Author: Jesen /// Version: 1.0 /// Date: 2019/8/8 23:29:29 /// </summary> public class ProjectRepository : IProjectRepository { private readonly ProjectContext _context; public IUnitOfWork UnitOfWork => _context; public ProjectRepository(ProjectContext cotnext) { _context = cotnext; } public ProjectEntity Add(ProjectEntity project) { if (project.IsTransient()) { return _context.Add(project).Entity; } else { return project; } } public async Task<ProjectEntity> GetAsync(int id) { return await _context.Projects.Include(p => p.Properties) .Include(p => p.Viewers) .Include(p => p.Contributors) .Include(p => p.VisibleRule) .SingleOrDefaultAsync(); } public void Update(ProjectEntity project) { _context.Update(project); } } }
using MediatR; using Project.Domain.Seedwork; using System.Linq; using System.Threading.Tasks; namespace Project.Infrastructure { /// <summary> /// Description: MediatorExtension /// Author: Jesen /// Version: 1.0 /// Date: 2019/8/8 22:25:51 /// </summary> static class MediatorExtension { public static async Task DispatchDomainEventsAsync(this IMediator mediator, ProjectContext ctx) { var domainEntities = ctx.ChangeTracker .Entries<Entity>() .Where(x => x.Entity.DomainEvents != null && x.Entity.DomainEvents.Any()); var domainEvents = domainEntities .SelectMany(x => x.Entity.DomainEvents) .ToList(); domainEntities.ToList() .ForEach(entity => entity.Entity.ClearDomainEvents()); var tasks = domainEvents .Select(async (domainEvent) => { await mediator.Publish(domainEvent); }); await Task.WhenAll(tasks); } } }
3、完成应用层
在应用层中,我们采用CQRS架构,利用MediatR来实现Command和CommandHandler,在CommandHandler中调用ProjectContext的Save方法出发领域事件处理程序,其也在Application层实现,而在领域事件处理程序中,又可以触发集成事件,而在其他服务中订阅这个集成事件。
using MediatR; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using ProjectEntity = Project.Domain.AggregatesModel.Project; namespace Project.Api.Applications.Commands { public class CreateProjectCommand : IRequest<ProjectEntity> { public ProjectEntity Project { get; set; } } }
using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using MediatR; using Project.Domain; using Project.Domain.AggregatesModel; using ProjectEntity = Project.Domain.AggregatesModel.Project; namespace Project.Api.Applications.Commands { public class CreateProjectCommandHandler : IRequestHandler<CreateProjectCommand, ProjectEntity> { private IProjectRepository _projectRepository; public CreateProjectCommandHandler(IProjectRepository projectRepository) { _projectRepository = projectRepository; } public async Task<ProjectEntity> Handle(CreateProjectCommand request, CancellationToken cancellationToken) { _projectRepository.Add(request.Project); await _projectRepository.UnitOfWork.SaveEntitiesAsync(); return request.Project; } } }
using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using DotNetCore.CAP; using MediatR; using Project.Api.Applications.IntergrationEvents; using Project.Domain.Events; namespace Project.Api.Applications.DomainEventHandlers { public class ProjectCreatedDomainEventHandler : INotificationHandler<ProjectCreatedEvent> { private ICapPublisher _capPublisher; public ProjectCreatedDomainEventHandler(ICapPublisher capPublisher) { _capPublisher = capPublisher; } public Task Handle(ProjectCreatedEvent notification, CancellationToken cancellationToken) { var @event = new ProjectCreatedIntergrationEvent { CreatedTime = DateTime.Now, ProjectId = notification.Project.Id, UserId = notification.Project.UserId }; _capPublisher.Publish("finbook.projectapi.projectcreated",@event); return Task.CompletedTask; } } }
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace Project.Api.Applications.IntergrationEvents { public class ProjectCreatedIntergrationEvent { public int ProjectId { get; set; } public int UserId { get; set; } public DateTime CreatedTime { get; set; } public int FromUserId { get; set; } public string FromUserName { get; set; } public string FromUserAvatar { get; set; } /// <summary> /// 项目logo /// </summary> public string ProjectAvatar { get; set; } /// <summary> /// 公司名称 /// </summary> public string Company { get; set; } /// <summary> /// 项目介绍 /// </summary> public string Introduction { get; set; } /// <summary> /// 项目标签 /// </summary> public string Tags { get; set; } /// <summary> /// 融资阶段 /// </summary> public string FinStage { get; set; } } }
而Query就显得比较简单
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace Project.Api.Applications.Queries { public interface IProjectQueries { Task<dynamic> GetProjectByUserId(int userId); Task<dynamic> GetProjectDetail(int projectId); } }
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Dapper; using Microsoft.EntityFrameworkCore; using MySql.Data.MySqlClient; using Project.Infrastructure; namespace Project.Api.Applications.Queries { public class ProjectQueries : IProjectQueries { private readonly ProjectContext _context; public ProjectQueries(ProjectContext context) { _context = context; } public async Task<dynamic> GetProjectByUserId(int userId) { using (var conn = _context.Database.GetDbConnection()) { conn.Open(); var sql = @"SELECT Projects.Id,Projects.Avatar,Projects.Company, Projects.FinStage,Projects.Introduction,Projects.ShowSecurityInfo,Projects.CreatedTime FROM Projects WHERE Projects.UserId=@userId;"; var result = await conn.QueryAsync<dynamic>(sql,new { userId }); return result; } } public async Task<dynamic> GetProjectDetail(int projectId) { //using(var conn = new MySqlConnection(_connStr)) using (var conn = _context.Database.GetDbConnection()) { conn.Open(); string sql = @"SELECT b.Visible, b.Tags, a.UserId, a.Avatar, a.Company, a.OriginBPFile, a.FormatBPFile, a.ShowSecurityInfo, a.Province, a.AreaName, a.RegisterTime, a.Introduction, a.FinPercentage, a.FinStage, a.FinMoney, a.Income, a.Revenue, a.Valuation, a.BrokerageOptions, a.OnPlatform, a.Tags, a.UserName, a.City FROM Projects AS a INNER JOIN ProjectVisibleRule AS b ON a.Id = b.ProjectId WHERE a.Id = @projectId;"; var result = await conn.QueryAsync<dynamic>(sql, new { projectId }); return result; } } } }
最后需要注入MediatR
services.AddScoped<IProjectQueries, ProjectQueries>(); services.AddScoped<IRecommendService, RecommendService>(); services.AddScoped<IProjectRepository, ProjectRepository>(); //从CreateProjectCommand所在程序集中扫描相应的类进行注入 services.AddMediatR(typeof(Applications.Commands.CreateProjectCommand).Assembly); //services.AddMediatR();