net core-EventBus(CAP)
一 微服务之间的相关通信就是通过EventBus实现的,调用接口也不现实,不然还得等接口返回结果,如果其他接口挂了呢,要等半天吗。
相关资料:
https://blog.51cto.com/u_15127692/3465750
https://github.com/dotnetcore/CAP
https://www.cnblogs.com/savorboard/p/cap.html
常见的一些服务:
AddScoped,添加服务,服务实例的生命周期为Scoped。 AddTransient,添加服务,服务实例的生命周期为Transient(每次被使用都创建新对象)。 AddSingleton,添加服务,服务实例的生命周期为单例。 AddMvc,添加所有MVC所需的服务。 AddMvcCore,仅添加核心必要的MVC所需的服务。 AddControllers,添加启用Controller 所需要的服务,不包括View和Pages所需要的服务。 AddControllersWithViews,添加启用 Controller 以及 Razor 页面所需要的服务。 AddRazorPages,添加 Razor Pages 所需要的服务。 AddAntiforgery,添加防止CSRF攻击的服务。 AddAuthentication,添加启用Authentication中间件所需的服务。 AddAuthenticationCore,添加启用Authentication中间件所需的核心服务。 AddAuthorization,添加启用Authorization中间件所需的服务。 AddAuthorizationCore,添加启用Authorization中间件所需的核心服务。 AddAuthorizationPolicyEvaluator,添加 Authorization 策略评估服务。 AddCertificateForwarding,添加CertificateForwarding中间件所需的服务。 AddConnections,添加 http://ASP.NET Core Connection Handlers 所需的服务。 AddCors,添加CORS中间件 所需的服务。 AddDataProtection,添加 http://ASP.NET Core Data Protection 所需的服务。 AddDirectoryBrowser,添加 DirectoryBrowser 中间件所需的服务。 AddDistributedMemoryCache,添加分布式缓冲服务IDistributedCache,默认的实现将缓冲保存在内存中,要实现实际上的分布式缓冲你需要提供一个保存缓存的实现(Redis或数据库,如AddStackExchangeRedisCache和AddDistributedSqlServerCache)。 AddHealthChecks,添加HealthChecks中间件所需的服务。 AddHostedService,添加宿主服务,如持续运行的服务。 AddHostFiltering,添加HostFiltering中间件所需的服务。 AddHsts,添加HSTS中间件所需的服务。 AddHttpClient,添加IHttpClientFactory服务用于获取在服务器端发起Http请求的HttpClient对象。 AddHttpContextAccessor,添加Http上下文访问器服务,在例如Controller里有HttpContext属性的地方优先使用HttpContext,但如果在一个自定义的服务里你就需要IHttpContextAccessor服务来获取Http上下文。 AddHttpsRedirection,为HttpsRedirection中间件添加所需的服务。 AddIdentity,添加默认的身份系统,并制定 Role和User类型。 AddIdentityCore,添加默认身份执行的核心部分,并制定User类型。 AddLocalization,添加本地化服务。 AddLogging,添加日志服务。 AddMemoryCache,添加非分布式的内存缓存服务。 AddOptions,添加 Option 服务。 AddResponseCaching,为ResponseCaching中间件添加所需的服务。 AddResponseCompression,为ResponseCompression中间件添加所需的服务。 AddRouting,添加Routing中间件所需的服务。 AddSignalR,添加SignalR所需的服务。 AddSignalRCore,添加SignalR所需的核心服务。 AddServerSideBlazor,添加 Server-Side Blazor所需的服务。 AddWebEncoders,添加 HtmlEncoder,JavaScriptEncoder,UrlEncoder 三个服务
二 实际使用
1 appsetting:
"RabbitMQ": { "HostName": "127.0.0.1", "UserName": "guest", "Password": "guest", "VirtualHost": "blog", "ExchangeName": "blog_queue" }, "ConnectionStrings": { "postgres":"PORT=5432;DATABASE=demo;HOST=127.0.0.1;PASSWORD=1234;USER ID=postgres" }
2 Program.cs
var services = builder.Services; services.AddCap(options => { options.UsePostgreSql(builder.Configuration.GetConnectionString("postgres")); options.UseRabbitMQ(options => { builder.Configuration.GetSection("RabbitMQ").Bind(options); }); });
3 发布,这样是到数据库里,但没有到RabbitMQ中。
[HttpGet("AdonetWithTransaction")] public IActionResult AdonetWithTransaction() { using (var connection = new NpgsqlConnection(ConnectionString)) { using (var transaction = connection.BeginTransaction(_capBus, autoCommit: true)) { //your business logic code _capBus.Publish("xxx.services.show.time", DateTime.Now); // Publish delay message _capBus.PublishDelayAsync(TimeSpan.FromSeconds(delaySeconds), "xxx.services.show.time", DateTime.Now); } } return Ok(); }
接收:CapSubscribe特性
[HttpGet("CheckReceivedMessage")] [CapSubscribe("xxx.services.show.time")] public void CheckReceivedMessage(DateTime datetime) { Console.WriteLine(datetime); }
二 应用层:Application处理权限,数据权限,返回值封装单个url处理逻辑封装等
三 领域层 包含实体(充血模型)
1 领域数据校验,如下图是在Customer内部创建数据的时候进行校验。
public static Customer CreateRegistered( string email, string name, ICustomerUniquenessChecker customerUniquenessChecker) { CheckRule(new CustomerEmailMustBeUniqueRule(customerUniquenessChecker, email)); return new Customer(email, name); }
放在领域实体
public abstract class Entity { private List<IDomainEvent> _domainEvents; /// <summary> /// Domain events occurred. /// </summary> public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents?.AsReadOnly(); /// <summary> /// Add domain event. /// </summary> /// <param name="domainEvent"></param> protected void AddDomainEvent(IDomainEvent domainEvent) { _domainEvents = _domainEvents ?? new List<IDomainEvent>(); this._domainEvents.Add(domainEvent); } /// <summary> /// Clear domain events. /// </summary> public void ClearDomainEvents() { _domainEvents?.Clear(); } protected static void CheckRule(IBusinessRule rule) { if (rule.IsBroken()) { throw new BusinessRuleValidationException(rule); } } } }
具体校验过程
public class CustomerEmailMustBeUniqueRule : IBusinessRule { private readonly ICustomerUniquenessChecker _customerUniquenessChecker; private readonly string _email; public CustomerEmailMustBeUniqueRule( ICustomerUniquenessChecker customerUniquenessChecker, string email) { _customerUniquenessChecker = customerUniquenessChecker; _email = email; } public bool IsBroken() => !_customerUniquenessChecker.IsUnique(_email); public string Message => "Customer with this email already exists."; }
这个校验接口放在领域层_customerUniquenessChecker,实现放在数据持久层
public class CustomerUniquenessChecker : ICustomerUniquenessChecker { private readonly ISqlConnectionFactory _sqlConnectionFactory; public CustomerUniquenessChecker(ISqlConnectionFactory sqlConnectionFactory) { _sqlConnectionFactory = sqlConnectionFactory; } public bool IsUnique(string customerEmail) { var connection = this._sqlConnectionFactory.GetOpenConnection(); const string sql = "SELECT TOP 1 1" + "FROM [orders].[Customers] AS [Customer] " + "WHERE [Customer].[Email] = @Email"; var customersNumber = connection.QuerySingleOrDefault<int?>(sql, new { Email = customerEmail }); return !customersNumber.HasValue; } }
返回结果:
public class BusinessRuleValidationException : Exception { public IBusinessRule BrokenRule { get; } public string Details { get; } public BusinessRuleValidationException(IBusinessRule brokenRule) : base(brokenRule.Message) { BrokenRule = brokenRule; this.Details = brokenRule.Message; } public override string ToString() { return $"{BrokenRule.GetType().FullName}: {BrokenRule.Message}"; } }
四 领域事件 比如新增一个订单,可以通过事件通知给物流,已经短信处理等。按照实际业务,每一个操作后面都要相应的逻辑要同步。放在一起就是"事务代码",堆到一定程度就是shi山。下面这个实现逻辑是参考文章给的资料上的。
1 这里先注册一下MediatR服务
// 注册中间者:MediatR services.AddMediatRServices();
2 具体注册:
public static IServiceCollection AddMediatRServices(this IServiceCollection services) { // 注册事务流程管理类 services.AddTransient(typeof(IPipelineBehavior<,>), typeof(DomainContextTransactionBehavior<,>)); // package: MediatR.Extensions.Microsoft.Dependency return services.AddMediatR(typeof(Order).Assembly, typeof(Program).Assembly); }
3 用来标志领域事件的接口:
/// <summary> /// 领域事件接口 /// 用来标记我们某一个对象是否是领域事件 /// </summary> public interface IDomainEvent : INotification { }
4 用来标记领域事件处理的接口:
/// <summary> /// 领域事件处理器接口 /// </summary> public interface IDomainEventHandler<TDomainEvent> : INotificationHandler<TDomainEvent> where TDomainEvent : IDomainEvent { //这里我们使用了INotificationHandler的Handle方法来作为处理方法的定义,所以无需重新定义 //Task Handle(TDomainEvent domainEvent, CancellationToken cancellationToken); }
实际上就是在mediator的INotification和INotificationHandler 封装一层。
那么是否有必要封装呢?其实我们大多数使用别人的库,如果不是静态调用最好封装一层,这样可以让上层看来依赖于下层的IDomainEvent和IDomainEventHandler,而不是和某个框架耦合在一起。
比如说有一个框架是Mediator的升级版,兼容了Mediator的功能,但是暴露出来的接口是INotificationPlusHandler,如果上层去耦合的话,改动的地方就有点多,风险也就越高,这不符合稳定性。
/// <summary> /// 实体抽象类(包含多个主键的实体接口) /// </summary> public abstract class Entity : IEntity { public abstract object[] GetKeys(); public override string ToString() { return $"[Entity:{GetType().Name}] Keys = {string.Join(",", GetKeys())}"; } #region 领域事件定义处理 DomainEvents /// <summary> /// 领域事件集合 /// </summary> private List<IDomainEvent> _domainEvents; /// <summary> /// 获取当前实体对象领域事件集合(只读) /// </summary> public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents?.AsReadOnly(); /// <summary> /// 添加领域事件至当前实体对象领域事件结合中 /// </summary> /// <param name="eventItem"></param> public void AddDomainEvent(IDomainEvent eventItem) { _domainEvents = _domainEvents ?? new List<IDomainEvent>(); _domainEvents.Add(eventItem); } /// <summary> /// 移除指定领域事件 /// </summary> /// <param name="eventItem"></param> public void RemoveDomainEvent(IDomainEvent eventItem) { _domainEvents?.Remove(eventItem); } /// <summary> /// 清空所有领域事件 /// </summary> public void ClearDomainEvents() { _domainEvents?.Clear(); } #endregion } /// <summary> /// 实体抽象类(包含唯一主键Id的实体接口) /// </summary> /// <typeparam name="TKey">主键ID类型</typeparam> public abstract class Entity<TKey> : Entity, IEntity<TKey> { int? _requestedHasCode; public virtual TKey Id { get; protected set; } public override object[] GetKeys() { return new object[] { Id }; } /// <summary> /// 对象是否想等 /// </summary> /// <param name="obj"></param> /// <returns></returns> public override bool Equals(object obj) { if (obj == null || !(obj is Entity<TKey>)) { return false; } if (Object.ReferenceEquals(this, obj)) { return true; } Entity<TKey> item = (Entity<TKey>)obj; if (item.IsTransient() || this.IsTransient()) { return false; } else { return item.Id.Equals(this.Id); } } public override int GetHashCode() { if (!IsTransient()) { if (!_requestedHasCode.HasValue) { _requestedHasCode = this.Id.GetHashCode() ^ 31; // TODO } return _requestedHasCode.Value; } else { return base.GetHashCode(); } } /// <summary> /// 对象是否为全新创建的,未持久化的 /// </summary> /// <returns></returns> public bool IsTransient() { return EqualityComparer<TKey>.Default.Equals(Id, default); } public override string ToString() { return $"[Entity:{GetType().Name}] Id = {Id}"; } /// <summary> /// == 操作符重载 /// </summary> /// <param name="left"></param> /// <param name="right"></param> /// <returns></returns> public static bool operator ==(Entity<TKey> left,Entity<TKey> right) { if (Object.Equals(left,null)) { return (Object.Equals(right, null)) ? true : false; } else { return left.Equals(right); } } /// <summary> /// != 操作符重载 /// </summary> /// <param name="left"></param> /// <param name="right"></param> /// <returns></returns> public static bool operator !=(Entity<TKey> left, Entity<TKey> right) { return !(left == right); } }
那么我们在领域层中,我们的实体可以这样定义:
/// <summary> /// 订单实体 /// </summary> public class Order : Entity<long>, IAggregateRoot { // 实体内字段的 set 方法都是 private 的 // 实体类型相关的数据操作,都应该是由我们实体来负责,而不是被外部的对象去操作 // 这样的好处是让我们的领域模型符合封闭开放的原则 public string UserId { get; private set; } public string UserName { get; private set; } public Address Address { get; private set; } public int ItemCount { get; set; } protected Order() { } public Order(string userId, string userName, int itemCount, Address address) { this.UserId = userId; this.UserName = userName; this.ItemCount = itemCount; this.Address = address; // 构造新的Order对象的时候,添加一个创建Order领域事件 this.AddDomainEvent(new OrderCreatedDomainEvent(this)); } /// <summary> /// 修改收货地址 /// </summary> /// <param name="address"></param> public void ChangeAddress(Address address) { this.Address = address; // 同样的,在修改地址操作时,也该定义一个类似的修改地址领域事件 //this.AddDomainEvent(new OrderAddressChangedDomainEvent(this)); } }
CreateCommand:
/// <summary> /// 创建订单 Command /// </summary> public class CreateOrderCommand : IRequest<long> { public long ItemCount { get; private set; } // public CreateOrderCommand() { } public CreateOrderCommand(int itemCount) { ItemCount = itemCount; } }
/// <summary> /// 领域事件:订单创建命令处理程序 /// 注:在创建完我们的领域模型并将其保存之后才会触发该处理程序 /// </summary> public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, long> { IOrderRepository _orderRepository; ICapPublisher _capPublisher; public CreateOrderCommandHandler(IOrderRepository orderRepository, ICapPublisher capPublisher) { _orderRepository = orderRepository; _capPublisher = capPublisher; } /// <summary> /// 处理订单创建命令 /// </summary> /// <param name="request"></param> /// <param name="cancellationToken"></param> /// <returns></returns> public async Task<long> Handle(CreateOrderCommand request, CancellationToken cancellationToken) { var address = new Address("wen san lu", "hangzhou", "310000"); var order = new Order("xiaohong1999", "小红", (int)request.ItemCount, address); _orderRepository.Add(order); await _orderRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken); return order.Id; } }
创建订单:
/// <summary> /// 创建订单 /// </summary> /// <param name="cmd"></param> /// <returns></returns> [HttpPost] public async Task<long> CreateOrder([FromBody] CreateOrderCommand cmd) { // 中间者,发送订单创建命令 return await _mediator.Send(cmd, HttpContext.RequestAborted); }
/// <summary> /// 中间者,领域事件发布扩展类 /// </summary> public static class MediatorExtension { /// <summary> /// 领域事件发布,执行事件发送 /// </summary> /// <param name="mediator"></param> /// <param name="ctx"></param> /// <returns></returns> public static async Task DispatchDomainEventsAsync(this IMediator mediator, DbContext ctx) { // 1. 从当前要保存的 EntityContext 里面去跟踪实体 // 从跟踪到的实体对象中,获取到我们当前的 Event var domainEntities = ctx.ChangeTracker .Entries<Entity>() .Where(x => x.Entity.DomainEvents != null && x.Entity.DomainEvents.Any()); // Events 类型转换 var domainEvents = domainEntities.SelectMany(x => x.Entity.DomainEvents).ToList(); // 2. 将实体内的 Events 清除 domainEntities.ToList().ForEach(entity => entity.Entity.ClearDomainEvents()); // 3. 将所有的 Event 通过中间者发送出去 // 发出后,并找到对应的 handle 进行处理 foreach (var domainEvent in domainEvents) { await mediator.Publish(domainEvent); } } }
这里是在EFContext中的save中。
待续。。。