From 7d442dcf291e7459db2e603f8d00f56b5c246f2e Mon Sep 17 00:00:00 2001 From: dudu Date: Sun, 19 Feb 2023 14:31:04 +0800 Subject: [PATCH 1/3] refactor: introduce non-generic IIntegrationEventHandler --- .../IIntegrationEventHandler.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Cnblogs.Architecture.Ddd.EventBus.Abstractions/IIntegrationEventHandler.cs b/src/Cnblogs.Architecture.Ddd.EventBus.Abstractions/IIntegrationEventHandler.cs index 17d17d2..e911aa2 100644 --- a/src/Cnblogs.Architecture.Ddd.EventBus.Abstractions/IIntegrationEventHandler.cs +++ b/src/Cnblogs.Architecture.Ddd.EventBus.Abstractions/IIntegrationEventHandler.cs @@ -1,4 +1,4 @@ -using MediatR; +using MediatR; namespace Cnblogs.Architecture.Ddd.EventBus.Abstractions; @@ -6,7 +6,16 @@ namespace Cnblogs.Architecture.Ddd.EventBus.Abstractions; /// 集成事件处理器。 /// /// 集成事件。 -public interface IIntegrationEventHandler : INotificationHandler +#pragma warning disable SA1402 // File may only contain a single type +public interface IIntegrationEventHandler : INotificationHandler, IIntegrationEventHandler +#pragma warning restore SA1402 // File may only contain a single type where TEvent : IntegrationEvent { -} \ No newline at end of file +} + +/// +/// The non-generic interface as a generic type constraint +/// +public interface IIntegrationEventHandler +{ +} From 6b533c2652b8f2824eb40c2aecce220fe032c547 Mon Sep 17 00:00:00 2001 From: dudu Date: Sun, 19 Feb 2023 19:36:30 +0800 Subject: [PATCH 2/3] feat: subscribe integration events by event handlers --- .../EndPointExtensions.cs | 120 +++++++++++++----- .../TestIntegrationEventHandler.cs | 10 +- .../DaprTests.cs | 24 +++- .../SubscribeType.cs | 10 ++ .../BlogPostCreatedIntegrationEvent.cs | 5 + 5 files changed, 134 insertions(+), 35 deletions(-) create mode 100644 test/Cnblogs.Architecture.IntegrationTests/SubscribeType.cs create mode 100644 test/Cnblogs.Architecture.TestIntegrationEvents/BlogPostCreatedIntegrationEvent.cs diff --git a/src/Cnblogs.Architecture.Ddd.EventBus.Dapr/EndPointExtensions.cs b/src/Cnblogs.Architecture.Ddd.EventBus.Dapr/EndPointExtensions.cs index 00e4d41..6c7f850 100644 --- a/src/Cnblogs.Architecture.Ddd.EventBus.Dapr/EndPointExtensions.cs +++ b/src/Cnblogs.Architecture.Ddd.EventBus.Dapr/EndPointExtensions.cs @@ -1,6 +1,7 @@ -using System.Reflection; +using System.Reflection; using Cnblogs.Architecture.Ddd.EventBus.Abstractions; using Cnblogs.Architecture.Ddd.EventBus.Dapr; +using MediatR; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -19,20 +20,11 @@ public static class EndPointExtensions /// /// 事件类型。 /// - public static IEndpointConventionBuilder Subscribe(this IEndpointRouteBuilder builder) + public static IEndpointRouteBuilder Subscribe(this IEndpointRouteBuilder builder) where TEvent : IntegrationEvent { - var attr = typeof(TEvent).Assembly - .GetCustomAttributes(typeof(AssemblyAppNameAttribute), false) - .Cast() - .FirstOrDefault(); - if (attr is null || string.IsNullOrEmpty(attr.Name)) - { - throw new InvalidOperationException( - $"No AppName was configured in assembly for event: {typeof(TEvent).Name}, either use Subscribe(string appName) method to set AppName manually or add [assembly:AssemblyAppName()] to the Assembly that {typeof(TEvent).Name} belongs to"); - } - - return builder.Subscribe(attr.Name); + var appName = typeof(TEvent).Assembly.GetAppName(); + return builder.Subscribe(appName); } /// @@ -42,7 +34,7 @@ public static IEndpointConventionBuilder Subscribe(this IEndpointRouteBu /// 事件隶属名称。 /// 事件类型。 /// - public static IEndpointConventionBuilder Subscribe(this IEndpointRouteBuilder builder, string appName) + public static IEndpointRouteBuilder Subscribe(this IEndpointRouteBuilder builder, string appName) where TEvent : IntegrationEvent { var eventName = typeof(TEvent).Name; @@ -57,7 +49,7 @@ public static IEndpointConventionBuilder Subscribe(this IEndpointRouteBu /// 应用名称。 /// 事件类型。 /// - public static IEndpointConventionBuilder Subscribe( + public static IEndpointRouteBuilder Subscribe( this IEndpointRouteBuilder builder, string route, string appName) @@ -68,7 +60,8 @@ public static IEndpointConventionBuilder Subscribe( var result = builder .MapPost(route, (TEvent receivedEvent, IEventBus eventBus) => eventBus.ReceiveAsync(receivedEvent)) .WithTopic(DaprOptions.PubSubName, DaprUtils.GetDaprTopicName(appName)); - return result; + + return builder; } /// @@ -76,28 +69,69 @@ public static IEndpointConventionBuilder Subscribe( /// /// /// - public static void Subscribe(this IEndpointRouteBuilder builder, params Assembly[] assemblies) + public static IEndpointRouteBuilder Subscribe(this IEndpointRouteBuilder builder, params Assembly[] assemblies) { builder.EnsureDaprEventBus(); - var method = typeof(EndPointExtensions).GetMethod( - nameof(Subscribe), - new[] { typeof(IEndpointRouteBuilder), typeof(string) })!; + var method = GetSubscribeMethod(); + foreach (var assembly in assemblies) { var events = assembly.GetTypes().Where(x => x.IsSubclassOf(typeof(IntegrationEvent))).ToList(); - var attr = assembly - .GetCustomAttributes(typeof(AssemblyAppNameAttribute), false) - .Cast() - .FirstOrDefault(); - if (attr is null || string.IsNullOrEmpty(attr.Name)) + var appName = assembly.GetAppName(); + events.ForEach(e => method.InvokeSubscribe(e, builder, appName)); + } + + return builder; + } + + /// + /// Subscribes integration events that the TEventHandler implements + /// + /// The integration event handler that implements ]]> + /// + public static IEndpointRouteBuilder SubscribeByEventHandler(this IEndpointRouteBuilder builder) + where TEventHandler : IIntegrationEventHandler + { + return builder.SubscribeByEventHandler(typeof(TEventHandler)); + } + + /// + /// Subscribes integration events that event handlers implement in assemblies + /// + /// + /// assemblies that event handlers reside + /// + public static IEndpointRouteBuilder SubscribeByEventHandler(this IEndpointRouteBuilder builder, params Assembly[] assemblies) + { + foreach (var assembly in assemblies) + { + foreach (Type type in assembly.GetTypes()) { - throw new InvalidOperationException( - $"No AppName was configured in assembly: {assembly.FullName}, either use Subscribe(string appName) method to set AppName manually or add [assembly:AssemblyAppName()] to the Assembly"); + builder.SubscribeByEventHandler(type); } + } + + return builder; + } - events.ForEach(e => method.MakeGenericMethod(e).Invoke(null, new object[] { builder, attr.Name })); + private static IEndpointRouteBuilder SubscribeByEventHandler(this IEndpointRouteBuilder builder, Type type) + { + var interfaces = type.GetInterfaces() + .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IIntegrationEventHandler<>)); + + foreach (var handlerInterface in interfaces) + { + var eventType = handlerInterface.GetGenericArguments().FirstOrDefault(); + if (eventType != null) + { + var assembly = eventType.Assembly; + var appName = assembly.GetAppName(); + GetSubscribeMethod().InvokeSubscribe(eventType, builder, appName); + } } + + return builder; } private static void EnsureEventBusRegistered(this IEndpointRouteBuilder builder, DaprOptions daprOptions) @@ -142,4 +176,32 @@ private static void EnsureDaprEventBus(this IEndpointRouteBuilder builder) builder.EnsureDaprSubscribeHandlerMapped(options); builder.EnsureEventBusRegistered(options); } -} \ No newline at end of file + + private static MethodInfo GetSubscribeMethod() + { + return typeof(EndPointExtensions).GetMethod( + nameof(Subscribe), + new[] { typeof(IEndpointRouteBuilder), typeof(string) })!; + } + + private static void InvokeSubscribe(this MethodInfo method, Type eventType, IEndpointRouteBuilder builder, string appName) + { + method.MakeGenericMethod(eventType).Invoke(null, new object[] { builder, appName }); + } + + private static string GetAppName(this Assembly assembly) + { + var appName = assembly + .GetCustomAttributes(typeof(AssemblyAppNameAttribute), false) + .Cast() + .FirstOrDefault()?.Name; + + if (string.IsNullOrEmpty(appName)) + { + throw new InvalidOperationException( + $"No AppName was configured in assembly: {assembly.FullName}, either use Subscribe(string appName) method to set AppName manually or add [assembly:AssemblyAppName()] to the Assembly"); + } + + return appName; + } +} diff --git a/test/Cnblogs.Architecture.IntegrationTestProject/EventHandlers/TestIntegrationEventHandler.cs b/test/Cnblogs.Architecture.IntegrationTestProject/EventHandlers/TestIntegrationEventHandler.cs index b77a339..72e469c 100644 --- a/test/Cnblogs.Architecture.IntegrationTestProject/EventHandlers/TestIntegrationEventHandler.cs +++ b/test/Cnblogs.Architecture.IntegrationTestProject/EventHandlers/TestIntegrationEventHandler.cs @@ -4,7 +4,8 @@ namespace Cnblogs.Architecture.IntegrationTestProject.EventHandlers; -public class TestIntegrationEventHandler : IIntegrationEventHandler +public class TestIntegrationEventHandler : IIntegrationEventHandler, + IIntegrationEventHandler { private readonly ILogger _logger; @@ -19,4 +20,11 @@ public Task Handle(TestIntegrationEvent notification, CancellationToken cancella return Task.CompletedTask; } + + public Task Handle(BlogPostCreatedIntegrationEvent notification, CancellationToken cancellationToken) + { + _logger.LogInformation(LogTemplates.HandledIntegratonEvent, notification); + + return Task.CompletedTask; + } } \ No newline at end of file diff --git a/test/Cnblogs.Architecture.IntegrationTests/DaprTests.cs b/test/Cnblogs.Architecture.IntegrationTests/DaprTests.cs index fbbce30..bff534f 100644 --- a/test/Cnblogs.Architecture.IntegrationTests/DaprTests.cs +++ b/test/Cnblogs.Architecture.IntegrationTests/DaprTests.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.Net; +using Cnblogs.Architecture.IntegrationTestProject.EventHandlers; using Cnblogs.Architecture.TestIntegrationEvents; using FluentAssertions; using Microsoft.AspNetCore.Builder; @@ -10,16 +11,29 @@ namespace Cnblogs.Architecture.IntegrationTests; public class DaprTests { - [Fact] - public async Task Dapr_SubscribeEndpoint_OkAsync() + [Theory] + [InlineData(SubscribeType.ByEvent)] + [InlineData(SubscribeType.ByEventAssemblies)] + [InlineData(SubscribeType.ByEventHandler)] + [InlineData(SubscribeType.ByEventHandlerAssemblies)] + public async Task Dapr_SubscribeEndpoint_OkAsync(SubscribeType subscribeType) { // Arrange var builder = WebApplication.CreateBuilder(); builder.Services.AddDaprEventBus(nameof(DaprTests)); builder.WebHost.UseTestServer(); - var app = builder.Build(); - app.Subscribe(); + using var app = builder.Build(); + + _ = subscribeType switch + { + SubscribeType.ByEvent => app.Subscribe().Subscribe(), + SubscribeType.ByEventAssemblies => app.Subscribe(typeof(TestIntegrationEvent).Assembly), + SubscribeType.ByEventHandler => app.SubscribeByEventHandler(), + SubscribeType.ByEventHandlerAssemblies => app.SubscribeByEventHandler(typeof(TestIntegrationEventHandler).Assembly), + _ => app + }; + await app.StartAsync(); var httpClient = app.GetTestClient(); @@ -29,8 +43,8 @@ public async Task Dapr_SubscribeEndpoint_OkAsync() // Assert response.Should().BeSuccessful(); var responseText = await response.Content.ReadAsStringAsync(); - Debug.WriteLine(responseText); responseText.Should().Contain(nameof(TestIntegrationEvent)); + responseText.Should().Contain(nameof(BlogPostCreatedIntegrationEvent)); } [Fact] diff --git a/test/Cnblogs.Architecture.IntegrationTests/SubscribeType.cs b/test/Cnblogs.Architecture.IntegrationTests/SubscribeType.cs new file mode 100644 index 0000000..708d112 --- /dev/null +++ b/test/Cnblogs.Architecture.IntegrationTests/SubscribeType.cs @@ -0,0 +1,10 @@ +namespace Cnblogs.Architecture.IntegrationTests; + +public enum SubscribeType +{ + None, + ByEvent, + ByEventAssemblies, + ByEventHandler, + ByEventHandlerAssemblies, +} \ No newline at end of file diff --git a/test/Cnblogs.Architecture.TestIntegrationEvents/BlogPostCreatedIntegrationEvent.cs b/test/Cnblogs.Architecture.TestIntegrationEvents/BlogPostCreatedIntegrationEvent.cs new file mode 100644 index 0000000..06af883 --- /dev/null +++ b/test/Cnblogs.Architecture.TestIntegrationEvents/BlogPostCreatedIntegrationEvent.cs @@ -0,0 +1,5 @@ +using Cnblogs.Architecture.Ddd.EventBus.Abstractions; + +namespace Cnblogs.Architecture.TestIntegrationEvents; + +public record BlogPostCreatedIntegrationEvent(Guid Id, DateTimeOffset CreatedTime, string Title) : IntegrationEvent(Id, CreatedTime); \ No newline at end of file From 1d8b3796ab582cb89c6ce7071ff6fb73f02614dc Mon Sep 17 00:00:00 2001 From: dudu Date: Mon, 20 Feb 2023 13:42:05 +0800 Subject: [PATCH 3/3] refactor: rename non-generic IIntegrationEventHandler to IEventBusHandler --- .../IEventBusHandler.cs | 8 ++++++++ .../IIntegrationEventHandler.cs | 11 +---------- .../EndPointExtensions.cs | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) create mode 100644 src/Cnblogs.Architecture.Ddd.EventBus.Abstractions/IEventBusHandler.cs diff --git a/src/Cnblogs.Architecture.Ddd.EventBus.Abstractions/IEventBusHandler.cs b/src/Cnblogs.Architecture.Ddd.EventBus.Abstractions/IEventBusHandler.cs new file mode 100644 index 0000000..15a10f0 --- /dev/null +++ b/src/Cnblogs.Architecture.Ddd.EventBus.Abstractions/IEventBusHandler.cs @@ -0,0 +1,8 @@ +namespace Cnblogs.Architecture.Ddd.EventBus.Abstractions; + +/// +/// The empty interface as a generic type constraint +/// +public interface IEventBusHandler +{ +} diff --git a/src/Cnblogs.Architecture.Ddd.EventBus.Abstractions/IIntegrationEventHandler.cs b/src/Cnblogs.Architecture.Ddd.EventBus.Abstractions/IIntegrationEventHandler.cs index e911aa2..f533225 100644 --- a/src/Cnblogs.Architecture.Ddd.EventBus.Abstractions/IIntegrationEventHandler.cs +++ b/src/Cnblogs.Architecture.Ddd.EventBus.Abstractions/IIntegrationEventHandler.cs @@ -6,16 +6,7 @@ namespace Cnblogs.Architecture.Ddd.EventBus.Abstractions; /// 集成事件处理器。 /// /// 集成事件。 -#pragma warning disable SA1402 // File may only contain a single type -public interface IIntegrationEventHandler : INotificationHandler, IIntegrationEventHandler -#pragma warning restore SA1402 // File may only contain a single type +public interface IIntegrationEventHandler : INotificationHandler, IEventBusHandler where TEvent : IntegrationEvent { } - -/// -/// The non-generic interface as a generic type constraint -/// -public interface IIntegrationEventHandler -{ -} diff --git a/src/Cnblogs.Architecture.Ddd.EventBus.Dapr/EndPointExtensions.cs b/src/Cnblogs.Architecture.Ddd.EventBus.Dapr/EndPointExtensions.cs index 6c7f850..8fd0c4c 100644 --- a/src/Cnblogs.Architecture.Ddd.EventBus.Dapr/EndPointExtensions.cs +++ b/src/Cnblogs.Architecture.Ddd.EventBus.Dapr/EndPointExtensions.cs @@ -91,7 +91,7 @@ public static IEndpointRouteBuilder Subscribe(this IEndpointRouteBuilder builder /// The integration event handler that implements ]]> /// public static IEndpointRouteBuilder SubscribeByEventHandler(this IEndpointRouteBuilder builder) - where TEventHandler : IIntegrationEventHandler + where TEventHandler : IEventBusHandler { return builder.SubscribeByEventHandler(typeof(TEventHandler)); }