From 8386c6c81449c52c4ef42e6f10b2dd2191e94925 Mon Sep 17 00:00:00 2001 From: Magnus Sandgren <5285192+MagnusSandgren@users.noreply.github.com> Date: Tue, 5 Nov 2024 15:51:19 +0100 Subject: [PATCH 1/9] Draft solution --- .../DomainExtensions.cs | 3 +- .../Consumers/DomainEventConsumer.cs | 37 +++++++++++++++ .../Program.cs | 47 ++++++++++++++----- 3 files changed, 73 insertions(+), 14 deletions(-) diff --git a/src/Digdir.Domain.Dialogporten.Domain/DomainExtensions.cs b/src/Digdir.Domain.Dialogporten.Domain/DomainExtensions.cs index 587cdb42c..378156493 100644 --- a/src/Digdir.Domain.Dialogporten.Domain/DomainExtensions.cs +++ b/src/Digdir.Domain.Dialogporten.Domain/DomainExtensions.cs @@ -5,8 +5,7 @@ namespace Digdir.Domain.Dialogporten.Domain; public static class DomainExtensions { public static IEnumerable GetDomainEventTypes() - => DomainAssemblyMarker.Assembly - .GetTypes() + => DomainAssemblyMarker.Assembly.DefinedTypes .Where(x => !x.IsAbstract && !x.IsInterface && !x.IsGenericType) .Where(x => x.IsAssignableTo(typeof(IDomainEvent))); } diff --git a/src/Digdir.Domain.Dialogporten.Service/Consumers/DomainEventConsumer.cs b/src/Digdir.Domain.Dialogporten.Service/Consumers/DomainEventConsumer.cs index b93f4c137..af721272d 100644 --- a/src/Digdir.Domain.Dialogporten.Service/Consumers/DomainEventConsumer.cs +++ b/src/Digdir.Domain.Dialogporten.Service/Consumers/DomainEventConsumer.cs @@ -51,3 +51,40 @@ protected override void ConfigureConsumer( TimeSpan.FromMilliseconds(1000))); } } + +public sealed class DomainEventConsumer(THandler handler) : IConsumer + where THandler : INotificationHandler + where TEvent : class, IDomainEvent +{ + private readonly THandler _handler = handler ?? throw new ArgumentNullException(nameof(handler)); + public Task Consume(ConsumeContext context) => _handler.Handle(context.Message, context.CancellationToken); +} + +public sealed class DomainEventConsumerDefinition : ConsumerDefinition> + where THandler : INotificationHandler + where TEvent : class, IDomainEvent +{ + public DomainEventConsumerDefinition() + { + var endpointName = $"{typeof(THandler).Name}_{typeof(TEvent).Name}"; + EndpointName = endpointName; + } + + protected override void ConfigureConsumer( + IReceiveEndpointConfigurator endpointConfigurator, + IConsumerConfigurator> consumerConfigurator, + IRegistrationContext context) + { + endpointConfigurator.UseDelayedRedelivery(r => r.Intervals( + TimeSpan.FromMinutes(1), + TimeSpan.FromMinutes(5), + TimeSpan.FromMinutes(10), + TimeSpan.FromMinutes(15))); + endpointConfigurator.UseMessageRetry(r => r.Intervals( + TimeSpan.FromMilliseconds(100), + TimeSpan.FromMilliseconds(200), + TimeSpan.FromMilliseconds(500), + TimeSpan.FromMilliseconds(800), + TimeSpan.FromMilliseconds(1000))); + } +} diff --git a/src/Digdir.Domain.Dialogporten.Service/Program.cs b/src/Digdir.Domain.Dialogporten.Service/Program.cs index 2c6c407a9..17afb7f74 100644 --- a/src/Digdir.Domain.Dialogporten.Service/Program.cs +++ b/src/Digdir.Domain.Dialogporten.Service/Program.cs @@ -5,11 +5,14 @@ using Microsoft.ApplicationInsights.Extensibility; using Serilog; using Digdir.Domain.Dialogporten.Application.Externals.Presentation; -using Digdir.Domain.Dialogporten.Domain; +using Digdir.Domain.Dialogporten.Domain.Common.EventPublisher; using Digdir.Domain.Dialogporten.Service; using Digdir.Domain.Dialogporten.Service.Consumers; using Digdir.Library.Utils.AspNet; using MassTransit; +using MassTransit.Internals; +using MediatR; +using Microsoft.Extensions.DependencyInjection.Extensions; // Using two-stage initialization to catch startup errors. var telemetryConfiguration = TelemetryConfiguration.CreateDefault(); @@ -49,19 +52,38 @@ static void BuildAndRun(string[] args, TelemetryConfiguration telemetryConfigura .AddAzureConfiguration(builder.Environment.EnvironmentName) .AddLocalConfiguration(builder.Environment); - // Generic consumers are not registered through MassTransits assembly - // scanning, so we need to create domain event handlers for all - // domain events and register them manually - var openDomainEventConsumer = typeof(DomainEventConsumer<>); - var openDomainEventConsumerDefinition = typeof(DomainEventConsumerDefinition<>); - var domainEventConsumers = DomainExtensions.GetDomainEventTypes() + var openNotificationHandler = typeof(INotificationHandler<>); + var openDomainEventConsumer = typeof(DomainEventConsumer<,>); + var openDomainEventConsumerDefinition = typeof(DomainEventConsumerDefinition<,>); + var consumerTypes = ApplicationAssemblyMarker.Assembly.DefinedTypes + .Where(x => x is { IsInterface: false, IsAbstract: false }) + .SelectMany(x => x + .GetInterfaces() + .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == openNotificationHandler) + .Select(i => (handler: x.AsType(), domainEvent: i.GetGenericArguments().Single()))) + .Where(x => x.domainEvent.IsAssignableTo(typeof(IDomainEvent))) .Select(x => ( - consumerType: openDomainEventConsumer.MakeGenericType(x), - definitionType: openDomainEventConsumerDefinition.MakeGenericType(x)) - ) + appConsumerType: x.handler, + domainEventType: x.domainEvent, + busConsumerType: openDomainEventConsumer.MakeGenericType(x.handler, x.domainEvent), + busDefinitionType: openDomainEventConsumerDefinition.MakeGenericType(x.handler, x.domainEvent) + )) .ToArray(); + // Generic consumers are not registered through MassTransits assembly + // scanning, so we need to create domain event handlers for all + // domain events and register them manually + // var openDomainEventConsumer = typeof(DomainEventConsumer<>); + // var openDomainEventConsumerDefinition = typeof(DomainEventConsumerDefinition<>); + // var domainEventConsumers = DomainExtensions.GetDomainEventTypes() + // .Select(x => + // ( + // consumerType: openDomainEventConsumer.MakeGenericType(x), + // definitionType: openDomainEventConsumerDefinition.MakeGenericType(x)) + // ) + // .ToArray(); + builder.ConfigureTelemetry(); builder.Services @@ -71,9 +93,10 @@ static void BuildAndRun(string[] args, TelemetryConfiguration telemetryConfigura .WithPubSubCapabilities() .AndBusConfiguration(x => { - foreach (var (consumer, definition) in domainEventConsumers) + foreach (var (appConsumerType, _, busConsumerType, busDefinitionType) in consumerTypes) { - x.AddConsumer(consumer, definition); + x.TryAddTransient(appConsumerType); + x.AddConsumer(busConsumerType, busDefinitionType); } }) .Build() From 2f146d1e95f735070cd481d682cc9c86e7bf68d5 Mon Sep 17 00:00:00 2001 From: Magnus Sandgren <5285192+MagnusSandgren@users.noreply.github.com> Date: Wed, 6 Nov 2024 10:54:54 +0100 Subject: [PATCH 2/9] WIP --- .../Common/EndpointNameAttribute.cs | 17 +++++++ .../DialogActivityEventToAltinnForwarder.cs | 2 + .../Events/DialogEventToAltinnForwarder.cs | 5 +++ .../Common/Extensions.cs | 40 +++++++++++++++++ .../Consumers/DomainEventConsumer.cs | 6 --- .../Program.cs | 44 +++---------------- 6 files changed, 69 insertions(+), 45 deletions(-) create mode 100644 src/Digdir.Domain.Dialogporten.Application/Common/EndpointNameAttribute.cs create mode 100644 src/Digdir.Domain.Dialogporten.Service/Common/Extensions.cs diff --git a/src/Digdir.Domain.Dialogporten.Application/Common/EndpointNameAttribute.cs b/src/Digdir.Domain.Dialogporten.Application/Common/EndpointNameAttribute.cs new file mode 100644 index 000000000..ae812f49a --- /dev/null +++ b/src/Digdir.Domain.Dialogporten.Application/Common/EndpointNameAttribute.cs @@ -0,0 +1,17 @@ +namespace Digdir.Domain.Dialogporten.Application.Common; + +[AttributeUsage(AttributeTargets.Method, Inherited = false)] +public sealed class EndpointNameAttribute : Attribute +{ + public string Name { get; } + + public EndpointNameAttribute(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)); + } + + Name = name; + } +} diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/Events/DialogActivityEventToAltinnForwarder.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/Events/DialogActivityEventToAltinnForwarder.cs index 513afcdac..793ac0173 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/Events/DialogActivityEventToAltinnForwarder.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/Events/DialogActivityEventToAltinnForwarder.cs @@ -1,3 +1,4 @@ +using Digdir.Domain.Dialogporten.Application.Common; using Digdir.Domain.Dialogporten.Application.Externals; using Digdir.Domain.Dialogporten.Domain.Dialogs.Events.Activities; using MediatR; @@ -11,6 +12,7 @@ internal sealed class DialogActivityEventToAltinnForwarder : DomainEventToAltinn public DialogActivityEventToAltinnForwarder(ICloudEventBus cloudEventBus, IOptions settings) : base(cloudEventBus, settings) { } + [EndpointName("DialogEventToAltinnForwarder_DialogActivityCreatedDomainEvent")] public async Task Handle(DialogActivityCreatedDomainEvent domainEvent, CancellationToken cancellationToken) { var cloudEvent = new CloudEvent diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/Events/DialogEventToAltinnForwarder.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/Events/DialogEventToAltinnForwarder.cs index 1843a3c69..82dac8d42 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/Events/DialogEventToAltinnForwarder.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/Events/DialogEventToAltinnForwarder.cs @@ -1,3 +1,4 @@ +using Digdir.Domain.Dialogporten.Application.Common; using Digdir.Domain.Dialogporten.Application.Externals; using Digdir.Domain.Dialogporten.Domain.Dialogs.Events; using MediatR; @@ -14,6 +15,7 @@ internal sealed class DialogEventToAltinnForwarder : DomainEventToAltinnForwarde public DialogEventToAltinnForwarder(ICloudEventBus cloudEventBus, IOptions settings) : base(cloudEventBus, settings) { } + [EndpointName("DialogEventToAltinnForwarder_DialogCreatedDomainEvent")] public async Task Handle(DialogCreatedDomainEvent domainEvent, CancellationToken cancellationToken) { var cloudEvent = new CloudEvent @@ -30,6 +32,7 @@ public async Task Handle(DialogCreatedDomainEvent domainEvent, CancellationToken await CloudEventBus.Publish(cloudEvent, cancellationToken); } + [EndpointName("DialogEventToAltinnForwarder_DialogUpdatedDomainEvent")] public async Task Handle(DialogUpdatedDomainEvent domainEvent, CancellationToken cancellationToken) { var cloudEvent = new CloudEvent @@ -47,6 +50,7 @@ public async Task Handle(DialogUpdatedDomainEvent domainEvent, CancellationToken await CloudEventBus.Publish(cloudEvent, cancellationToken); } + [EndpointName("DialogEventToAltinnForwarder_DialogSeenDomainEvent")] public async Task Handle(DialogSeenDomainEvent domainEvent, CancellationToken cancellationToken) { var cloudEvent = new CloudEvent @@ -64,6 +68,7 @@ public async Task Handle(DialogSeenDomainEvent domainEvent, CancellationToken ca await CloudEventBus.Publish(cloudEvent, cancellationToken); } + [EndpointName("DialogEventToAltinnForwarder_DialogDeletedDomainEvent")] public async Task Handle(DialogDeletedDomainEvent domainEvent, CancellationToken cancellationToken) { var cloudEvent = new CloudEvent diff --git a/src/Digdir.Domain.Dialogporten.Service/Common/Extensions.cs b/src/Digdir.Domain.Dialogporten.Service/Common/Extensions.cs new file mode 100644 index 000000000..2edb3082c --- /dev/null +++ b/src/Digdir.Domain.Dialogporten.Service/Common/Extensions.cs @@ -0,0 +1,40 @@ +using System.Reflection; +using Digdir.Domain.Dialogporten.Application; +using Digdir.Domain.Dialogporten.Domain.Common.EventPublisher; +using Digdir.Domain.Dialogporten.Service.Consumers; +using MediatR; +using EndpointNameAttribute = Digdir.Domain.Dialogporten.Application.Common.EndpointNameAttribute; + +namespace Digdir.Domain.Dialogporten.Service.Common; + +internal static class Extensions +{ + public static ApplicationConsumerMapping[] GetApplicationConsumerMaps() + { + var openNotificationHandler = typeof(INotificationHandler<>); + var openDomainEventConsumer = typeof(DomainEventConsumer<,>); + var openDomainEventConsumerDefinition = typeof(DomainEventConsumerDefinition<,>); + var domainEventType = typeof(IDomainEvent); + return ApplicationAssemblyMarker.Assembly.DefinedTypes + .Where(x => x is { IsInterface: false, IsAbstract: false }) + .SelectMany(x => x + .GetInterfaces() + .Where(@interface => + @interface.IsGenericType + && @interface.GetGenericTypeDefinition() == openNotificationHandler + && @interface.GetGenericArguments().Single().IsAssignableTo(domainEventType)) + .Select(@interface => (@class: x, @interface, @event: @interface.GetGenericArguments().Single())) + .Select(x => new ApplicationConsumerMapping( + AppConsumerType: x.@class.AsType(), + BusConsumerType: openDomainEventConsumer.MakeGenericType(x.@class, x.@event), + BusDefinitionType: openDomainEventConsumerDefinition.MakeGenericType(x.@class, x.@event), + EndpointName: x.@class.GetInterfaceMap(x.@interface) + .TargetMethods.First() + .GetCustomAttribute()? + .Name ?? $"{x.@class.Name}_{x.@event.Name}") + )) + .ToArray(); + } +} + +internal record struct ApplicationConsumerMapping(Type AppConsumerType, Type BusConsumerType, Type BusDefinitionType, string EndpointName); diff --git a/src/Digdir.Domain.Dialogporten.Service/Consumers/DomainEventConsumer.cs b/src/Digdir.Domain.Dialogporten.Service/Consumers/DomainEventConsumer.cs index af721272d..f13b4d15b 100644 --- a/src/Digdir.Domain.Dialogporten.Service/Consumers/DomainEventConsumer.cs +++ b/src/Digdir.Domain.Dialogporten.Service/Consumers/DomainEventConsumer.cs @@ -64,12 +64,6 @@ public sealed class DomainEventConsumerDefinition : ConsumerDe where THandler : INotificationHandler where TEvent : class, IDomainEvent { - public DomainEventConsumerDefinition() - { - var endpointName = $"{typeof(THandler).Name}_{typeof(TEvent).Name}"; - EndpointName = endpointName; - } - protected override void ConfigureConsumer( IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator> consumerConfigurator, diff --git a/src/Digdir.Domain.Dialogporten.Service/Program.cs b/src/Digdir.Domain.Dialogporten.Service/Program.cs index 17afb7f74..373fcc8f8 100644 --- a/src/Digdir.Domain.Dialogporten.Service/Program.cs +++ b/src/Digdir.Domain.Dialogporten.Service/Program.cs @@ -5,13 +5,10 @@ using Microsoft.ApplicationInsights.Extensibility; using Serilog; using Digdir.Domain.Dialogporten.Application.Externals.Presentation; -using Digdir.Domain.Dialogporten.Domain.Common.EventPublisher; using Digdir.Domain.Dialogporten.Service; -using Digdir.Domain.Dialogporten.Service.Consumers; +using Digdir.Domain.Dialogporten.Service.Common; using Digdir.Library.Utils.AspNet; using MassTransit; -using MassTransit.Internals; -using MediatR; using Microsoft.Extensions.DependencyInjection.Extensions; // Using two-stage initialization to catch startup errors. @@ -52,38 +49,6 @@ static void BuildAndRun(string[] args, TelemetryConfiguration telemetryConfigura .AddAzureConfiguration(builder.Environment.EnvironmentName) .AddLocalConfiguration(builder.Environment); - var openNotificationHandler = typeof(INotificationHandler<>); - var openDomainEventConsumer = typeof(DomainEventConsumer<,>); - var openDomainEventConsumerDefinition = typeof(DomainEventConsumerDefinition<,>); - var consumerTypes = ApplicationAssemblyMarker.Assembly.DefinedTypes - .Where(x => x is { IsInterface: false, IsAbstract: false }) - .SelectMany(x => x - .GetInterfaces() - .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == openNotificationHandler) - .Select(i => (handler: x.AsType(), domainEvent: i.GetGenericArguments().Single()))) - .Where(x => x.domainEvent.IsAssignableTo(typeof(IDomainEvent))) - .Select(x => - ( - appConsumerType: x.handler, - domainEventType: x.domainEvent, - busConsumerType: openDomainEventConsumer.MakeGenericType(x.handler, x.domainEvent), - busDefinitionType: openDomainEventConsumerDefinition.MakeGenericType(x.handler, x.domainEvent) - )) - .ToArray(); - - // Generic consumers are not registered through MassTransits assembly - // scanning, so we need to create domain event handlers for all - // domain events and register them manually - // var openDomainEventConsumer = typeof(DomainEventConsumer<>); - // var openDomainEventConsumerDefinition = typeof(DomainEventConsumerDefinition<>); - // var domainEventConsumers = DomainExtensions.GetDomainEventTypes() - // .Select(x => - // ( - // consumerType: openDomainEventConsumer.MakeGenericType(x), - // definitionType: openDomainEventConsumerDefinition.MakeGenericType(x)) - // ) - // .ToArray(); - builder.ConfigureTelemetry(); builder.Services @@ -93,10 +58,11 @@ static void BuildAndRun(string[] args, TelemetryConfiguration telemetryConfigura .WithPubSubCapabilities() .AndBusConfiguration(x => { - foreach (var (appConsumerType, _, busConsumerType, busDefinitionType) in consumerTypes) + foreach (var map in Extensions.GetApplicationConsumerMaps()) { - x.TryAddTransient(appConsumerType); - x.AddConsumer(busConsumerType, busDefinitionType); + x.TryAddTransient(map.AppConsumerType); + x.AddConsumer(map.BusConsumerType, map.BusDefinitionType) + .Endpoint(x => x.Name = map.EndpointName); } }) .Build() From 412ef7396484bf6a9829451db0bc6711ab54aaa1 Mon Sep 17 00:00:00 2001 From: Magnus Sandgren <5285192+MagnusSandgren@users.noreply.github.com> Date: Thu, 7 Nov 2024 13:34:45 +0100 Subject: [PATCH 3/9] WIP --- .../Common/EndpointNameAttribute.cs | 7 ++- .../INotificationProcessingContextFactory.cs | 46 +++++++++++++------ .../Common/Extensions.cs | 2 +- 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/src/Digdir.Domain.Dialogporten.Application/Common/EndpointNameAttribute.cs b/src/Digdir.Domain.Dialogporten.Application/Common/EndpointNameAttribute.cs index ae812f49a..2756e5d0f 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Common/EndpointNameAttribute.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Common/EndpointNameAttribute.cs @@ -1,6 +1,11 @@ +using MediatR; + namespace Digdir.Domain.Dialogporten.Application.Common; -[AttributeUsage(AttributeTargets.Method, Inherited = false)] +/// +/// Attribute to specify which endpoint name MassTransit will use when wrapping . methods. +/// +[AttributeUsage(AttributeTargets.Method)] public sealed class EndpointNameAttribute : Attribute { public string Name { get; } diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Persistence/IdempotentNotifications/INotificationProcessingContextFactory.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Persistence/IdempotentNotifications/INotificationProcessingContextFactory.cs index 4842884a9..24c6632a4 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/Persistence/IdempotentNotifications/INotificationProcessingContextFactory.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Persistence/IdempotentNotifications/INotificationProcessingContextFactory.cs @@ -33,11 +33,11 @@ public async Task CreateContext( bool isFirstAttempt = false, CancellationToken cancellationToken = default) { - var transaction = GetOrAddContext(domainEvent.EventId); + var context = GetOrAddContext(domainEvent.EventId); try { - await transaction.Initialize(isFirstAttempt, cancellationToken); - return transaction; + await context.Initialize(isFirstAttempt, cancellationToken); + return context; } catch (Exception) { @@ -63,14 +63,22 @@ public void Dispose() private NotificationProcessingContext GetOrAddContext(Guid eventId) { + // We keep a strong reference to the context while it's being + // created to avoid it being garbage collected prematurely + // before we can extract it from the weak reference. + NotificationProcessingContext? context; var weakContext = _contextByEventId.AddOrUpdate(eventId, - addValueFactory: eventId => new(new(_serviceScopeFactory, eventId, onDispose: RemoveContext)), + addValueFactory: eventId => new(context = new(_serviceScopeFactory, eventId, onDispose: RemoveContext)), // Should the context, for whatever reason, be garbage collected or // disposed but still remain in the dictionary, we should recreate it. - updateValueFactory: (eventId, old) => TryGetLiveContext(old, out _) ? old - : new(new(_serviceScopeFactory, eventId, onDispose: RemoveContext))); + updateValueFactory: (eventId, old) => TryGetLiveContext(old, out context) ? old + : new(context = new(_serviceScopeFactory, eventId, onDispose: RemoveContext))); - return TryGetLiveContext(weakContext, out var context) ? context + // Although we have a strong reference to __a__ context, it may + // not be __the__ context in a multithreaded scenario. We + // know that the actual context is in the week reference, so + // we extract it before returning. + return TryGetLiveContext(weakContext, out context) ? context : throw new UnreachableException("The context should be alive at this point in time."); } @@ -78,9 +86,9 @@ private NotificationProcessingContext GetOrAddContext(Guid eventId) private async Task ContextHousekeeping() { - try + while (await WaitForNextTickSafeAsync()) { - while (await _cleanupTimer.WaitForNextTickAsync(_cleanupCts.Token)) + try { foreach (var key in _contextByEventId.Keys) { @@ -91,14 +99,26 @@ private async Task ContextHousekeeping() } } } + catch (OperationCanceledException) + { + // Ignore + } + catch (Exception e) + { + _logger.LogWarning(e, "An unhandled exception occurred in the notification processing context cleanup task. This may lead to memory leaks."); + } } - catch (OperationCanceledException) + } + + private async ValueTask WaitForNextTickSafeAsync() + { + try { - // Ignore + return await _cleanupTimer.WaitForNextTickAsync(_cleanupCts.Token); } - catch (Exception e) + catch (OperationCanceledException) { - _logger.LogWarning(e, "An unhandled exception occurred in the notification processing context cleanup task. This may lead to memory leaks."); + return false; } } diff --git a/src/Digdir.Domain.Dialogporten.Service/Common/Extensions.cs b/src/Digdir.Domain.Dialogporten.Service/Common/Extensions.cs index 2edb3082c..f9f5cd8fb 100644 --- a/src/Digdir.Domain.Dialogporten.Service/Common/Extensions.cs +++ b/src/Digdir.Domain.Dialogporten.Service/Common/Extensions.cs @@ -29,7 +29,7 @@ public static ApplicationConsumerMapping[] GetApplicationConsumerMaps() BusConsumerType: openDomainEventConsumer.MakeGenericType(x.@class, x.@event), BusDefinitionType: openDomainEventConsumerDefinition.MakeGenericType(x.@class, x.@event), EndpointName: x.@class.GetInterfaceMap(x.@interface) - .TargetMethods.First() + .TargetMethods.Single() .GetCustomAttribute()? .Name ?? $"{x.@class.Name}_{x.@event.Name}") )) From d473b248141e4b9ea557ea53de4c8c1a45615c4e Mon Sep 17 00:00:00 2001 From: Magnus Sandgren <5285192+MagnusSandgren@users.noreply.github.com> Date: Thu, 7 Nov 2024 14:20:45 +0100 Subject: [PATCH 4/9] WIP --- .../Common/EndpointNameAttribute.cs | 12 +++- .../Common/Extensions.cs | 10 +-- .../Consumers/DomainEventConsumer.cs | 61 ++++--------------- .../Program.cs | 3 +- 4 files changed, 27 insertions(+), 59 deletions(-) diff --git a/src/Digdir.Domain.Dialogporten.Application/Common/EndpointNameAttribute.cs b/src/Digdir.Domain.Dialogporten.Application/Common/EndpointNameAttribute.cs index 2756e5d0f..c500263d3 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Common/EndpointNameAttribute.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Common/EndpointNameAttribute.cs @@ -1,10 +1,17 @@ +using Digdir.Domain.Dialogporten.Domain.Common.EventPublisher; using MediatR; namespace Digdir.Domain.Dialogporten.Application.Common; /// -/// Attribute to specify which endpoint name MassTransit will use when wrapping . methods. +/// Attribute to specify which endpoint name MassTransit will use when wrapping . /// +/// +/// +/// Will default to "{handlerType.Name}_{eventType.Name}" if not specified. +/// MassTransit will only wrap where TNotification implements . +/// +/// [AttributeUsage(AttributeTargets.Method)] public sealed class EndpointNameAttribute : Attribute { @@ -19,4 +26,7 @@ public EndpointNameAttribute(string name) Name = name; } + + public static string DefaultName(Type handlerType, Type eventType) + => $"{handlerType.Name}_{eventType.Name}"; } diff --git a/src/Digdir.Domain.Dialogporten.Service/Common/Extensions.cs b/src/Digdir.Domain.Dialogporten.Service/Common/Extensions.cs index f9f5cd8fb..f45b76a06 100644 --- a/src/Digdir.Domain.Dialogporten.Service/Common/Extensions.cs +++ b/src/Digdir.Domain.Dialogporten.Service/Common/Extensions.cs @@ -1,9 +1,7 @@ -using System.Reflection; using Digdir.Domain.Dialogporten.Application; using Digdir.Domain.Dialogporten.Domain.Common.EventPublisher; using Digdir.Domain.Dialogporten.Service.Consumers; using MediatR; -using EndpointNameAttribute = Digdir.Domain.Dialogporten.Application.Common.EndpointNameAttribute; namespace Digdir.Domain.Dialogporten.Service.Common; @@ -27,14 +25,10 @@ public static ApplicationConsumerMapping[] GetApplicationConsumerMaps() .Select(x => new ApplicationConsumerMapping( AppConsumerType: x.@class.AsType(), BusConsumerType: openDomainEventConsumer.MakeGenericType(x.@class, x.@event), - BusDefinitionType: openDomainEventConsumerDefinition.MakeGenericType(x.@class, x.@event), - EndpointName: x.@class.GetInterfaceMap(x.@interface) - .TargetMethods.Single() - .GetCustomAttribute()? - .Name ?? $"{x.@class.Name}_{x.@event.Name}") + BusDefinitionType: openDomainEventConsumerDefinition.MakeGenericType(x.@class, x.@event)) )) .ToArray(); } } -internal record struct ApplicationConsumerMapping(Type AppConsumerType, Type BusConsumerType, Type BusDefinitionType, string EndpointName); +internal record struct ApplicationConsumerMapping(Type AppConsumerType, Type BusConsumerType, Type BusDefinitionType); diff --git a/src/Digdir.Domain.Dialogporten.Service/Consumers/DomainEventConsumer.cs b/src/Digdir.Domain.Dialogporten.Service/Consumers/DomainEventConsumer.cs index f13b4d15b..0bec9f371 100644 --- a/src/Digdir.Domain.Dialogporten.Service/Consumers/DomainEventConsumer.cs +++ b/src/Digdir.Domain.Dialogporten.Service/Consumers/DomainEventConsumer.cs @@ -1,57 +1,11 @@ +using System.Reflection; using Digdir.Domain.Dialogporten.Domain.Common.EventPublisher; -using Digdir.Domain.Dialogporten.Infrastructure.Persistence.IdempotentNotifications; using MassTransit; using MediatR; +using EndpointNameAttribute = Digdir.Domain.Dialogporten.Application.Common.EndpointNameAttribute; namespace Digdir.Domain.Dialogporten.Service.Consumers; -public sealed class DomainEventConsumer : IConsumer - where T : class, IDomainEvent -{ - private readonly IPublisher _publisher; - private readonly INotificationProcessingContextFactory _notificationProcessingContextFactory; - - public DomainEventConsumer(IPublisher publisher, INotificationProcessingContextFactory notificationProcessingContextFactory) - { - _publisher = publisher ?? throw new ArgumentNullException(nameof(publisher)); - _notificationProcessingContextFactory = notificationProcessingContextFactory ?? throw new ArgumentNullException(nameof(notificationProcessingContextFactory)); - } - - public async Task Consume(ConsumeContext context) - { - var isFirstAttempt = IsFirstAttempt(context); - await using var notificationContext = await _notificationProcessingContextFactory - .CreateContext(context.Message, isFirstAttempt, context.CancellationToken); - await _publisher.Publish(context.Message, context.CancellationToken); - await notificationContext.Ack(context.CancellationToken); - } - - private static bool IsFirstAttempt(ConsumeContext context) - => (context.GetRetryAttempt() + context.GetRedeliveryCount()) == 0; -} - -public sealed class DomainEventConsumerDefinition : ConsumerDefinition> - where T : class, IDomainEvent -{ - protected override void ConfigureConsumer( - IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator> consumerConfigurator, - IRegistrationContext context) - { - endpointConfigurator.UseDelayedRedelivery(r => r.Intervals( - TimeSpan.FromMinutes(1), - TimeSpan.FromMinutes(5), - TimeSpan.FromMinutes(10), - TimeSpan.FromMinutes(15))); - endpointConfigurator.UseMessageRetry(r => r.Intervals( - TimeSpan.FromMilliseconds(100), - TimeSpan.FromMilliseconds(200), - TimeSpan.FromMilliseconds(500), - TimeSpan.FromMilliseconds(800), - TimeSpan.FromMilliseconds(1000))); - } -} - public sealed class DomainEventConsumer(THandler handler) : IConsumer where THandler : INotificationHandler where TEvent : class, IDomainEvent @@ -64,6 +18,17 @@ public sealed class DomainEventConsumerDefinition : ConsumerDe where THandler : INotificationHandler where TEvent : class, IDomainEvent { + public DomainEventConsumerDefinition() + { + var @class = typeof(THandler); + var @interface = typeof(INotificationHandler); + var @event = typeof(TEvent); + EndpointName = @class.GetInterfaceMap(@interface) + .TargetMethods.Single() + .GetCustomAttribute()? + .Name ?? EndpointNameAttribute.DefaultName(@class, @event); + } + protected override void ConfigureConsumer( IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator> consumerConfigurator, diff --git a/src/Digdir.Domain.Dialogporten.Service/Program.cs b/src/Digdir.Domain.Dialogporten.Service/Program.cs index 373fcc8f8..c17fa3e7d 100644 --- a/src/Digdir.Domain.Dialogporten.Service/Program.cs +++ b/src/Digdir.Domain.Dialogporten.Service/Program.cs @@ -61,8 +61,7 @@ static void BuildAndRun(string[] args, TelemetryConfiguration telemetryConfigura foreach (var map in Extensions.GetApplicationConsumerMaps()) { x.TryAddTransient(map.AppConsumerType); - x.AddConsumer(map.BusConsumerType, map.BusDefinitionType) - .Endpoint(x => x.Name = map.EndpointName); + x.AddConsumer(map.BusConsumerType, map.BusDefinitionType); } }) .Build() From b0d5f569f504a1e0cce167d3fce131523deca19b Mon Sep 17 00:00:00 2001 From: Magnus Sandgren <5285192+MagnusSandgren@users.noreply.github.com> Date: Thu, 7 Nov 2024 16:05:02 +0100 Subject: [PATCH 5/9] WIP --- .../Common/ApplicationEventHandlerUtils.cs | 26 +++++++++++++ .../Common/EndpointNameAttribute.cs | 15 +++++++- .../Common/Extensions.cs | 34 ----------------- .../Common/MassTransitApplicationUtils.cs | 23 +++++++++++ .../Consumers/DomainEventConsumer.cs | 8 +--- .../Program.cs | 2 +- .../ApplicationEventHandlerUtilsTests.cs | 38 +++++++++++++++++++ ...Dialogporten.Application.Unit.Tests.csproj | 1 + ...sts.Should_Have_Same_Snapshot.verified.txt | 7 ++++ 9 files changed, 111 insertions(+), 43 deletions(-) create mode 100644 src/Digdir.Domain.Dialogporten.Application/Common/ApplicationEventHandlerUtils.cs delete mode 100644 src/Digdir.Domain.Dialogporten.Service/Common/Extensions.cs create mode 100644 src/Digdir.Domain.Dialogporten.Service/Common/MassTransitApplicationUtils.cs create mode 100644 tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/ApplicationEventHandlerUtilsTests.cs create mode 100644 tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Snapshots/ApplicationEventHandlerUtilsTests.Should_Have_Same_Snapshot.verified.txt diff --git a/src/Digdir.Domain.Dialogporten.Application/Common/ApplicationEventHandlerUtils.cs b/src/Digdir.Domain.Dialogporten.Application/Common/ApplicationEventHandlerUtils.cs new file mode 100644 index 000000000..bfc7f5780 --- /dev/null +++ b/src/Digdir.Domain.Dialogporten.Application/Common/ApplicationEventHandlerUtils.cs @@ -0,0 +1,26 @@ +using Digdir.Domain.Dialogporten.Domain.Common.EventPublisher; +using MediatR; + +namespace Digdir.Domain.Dialogporten.Application.Common; + +public static class ApplicationEventHandlerUtils +{ + public static HandlerEventMapping[] GetHandlerEventMaps() + { + var openNotificationHandler = typeof(INotificationHandler<>); + var domainEventType = typeof(IDomainEvent); + return ApplicationAssemblyMarker.Assembly.DefinedTypes + .Where(x => x is { IsInterface: false, IsAbstract: false }) + .SelectMany(x => x + .GetInterfaces() + .Where(@interface => + @interface.IsGenericType + && @interface.GetGenericTypeDefinition() == openNotificationHandler + && @interface.GetGenericArguments().Single().IsAssignableTo(domainEventType)) + .Select(@interface => (@class: x, @event: @interface.GetGenericArguments().Single())) + .Select(x => new HandlerEventMapping(x.@class.AsType(), x.@event))) + .ToArray(); + } +} + +public record struct HandlerEventMapping(Type HandlerType, Type EventType); diff --git a/src/Digdir.Domain.Dialogporten.Application/Common/EndpointNameAttribute.cs b/src/Digdir.Domain.Dialogporten.Application/Common/EndpointNameAttribute.cs index c500263d3..32853fc5d 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Common/EndpointNameAttribute.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Common/EndpointNameAttribute.cs @@ -1,3 +1,4 @@ +using System.Reflection; using Digdir.Domain.Dialogporten.Domain.Common.EventPublisher; using MediatR; @@ -27,6 +28,18 @@ public EndpointNameAttribute(string name) Name = name; } - public static string DefaultName(Type handlerType, Type eventType) + public static string GetName() + where THandler : INotificationHandler + where TEvent : class, IDomainEvent + => GetName(typeof(THandler), typeof(TEvent)); + + public static string GetName(Type handlerType, Type eventType) + => handlerType + .GetInterfaceMap(typeof(INotificationHandler<>).MakeGenericType(eventType)) + .TargetMethods.Single() + .GetCustomAttribute()? + .Name ?? DefaultName(handlerType, eventType); + + private static string DefaultName(Type handlerType, Type eventType) => $"{handlerType.Name}_{eventType.Name}"; } diff --git a/src/Digdir.Domain.Dialogporten.Service/Common/Extensions.cs b/src/Digdir.Domain.Dialogporten.Service/Common/Extensions.cs deleted file mode 100644 index f45b76a06..000000000 --- a/src/Digdir.Domain.Dialogporten.Service/Common/Extensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Digdir.Domain.Dialogporten.Application; -using Digdir.Domain.Dialogporten.Domain.Common.EventPublisher; -using Digdir.Domain.Dialogporten.Service.Consumers; -using MediatR; - -namespace Digdir.Domain.Dialogporten.Service.Common; - -internal static class Extensions -{ - public static ApplicationConsumerMapping[] GetApplicationConsumerMaps() - { - var openNotificationHandler = typeof(INotificationHandler<>); - var openDomainEventConsumer = typeof(DomainEventConsumer<,>); - var openDomainEventConsumerDefinition = typeof(DomainEventConsumerDefinition<,>); - var domainEventType = typeof(IDomainEvent); - return ApplicationAssemblyMarker.Assembly.DefinedTypes - .Where(x => x is { IsInterface: false, IsAbstract: false }) - .SelectMany(x => x - .GetInterfaces() - .Where(@interface => - @interface.IsGenericType - && @interface.GetGenericTypeDefinition() == openNotificationHandler - && @interface.GetGenericArguments().Single().IsAssignableTo(domainEventType)) - .Select(@interface => (@class: x, @interface, @event: @interface.GetGenericArguments().Single())) - .Select(x => new ApplicationConsumerMapping( - AppConsumerType: x.@class.AsType(), - BusConsumerType: openDomainEventConsumer.MakeGenericType(x.@class, x.@event), - BusDefinitionType: openDomainEventConsumerDefinition.MakeGenericType(x.@class, x.@event)) - )) - .ToArray(); - } -} - -internal record struct ApplicationConsumerMapping(Type AppConsumerType, Type BusConsumerType, Type BusDefinitionType); diff --git a/src/Digdir.Domain.Dialogporten.Service/Common/MassTransitApplicationUtils.cs b/src/Digdir.Domain.Dialogporten.Service/Common/MassTransitApplicationUtils.cs new file mode 100644 index 000000000..2de642b12 --- /dev/null +++ b/src/Digdir.Domain.Dialogporten.Service/Common/MassTransitApplicationUtils.cs @@ -0,0 +1,23 @@ +using Digdir.Domain.Dialogporten.Application.Common; +using Digdir.Domain.Dialogporten.Service.Consumers; + +namespace Digdir.Domain.Dialogporten.Service.Common; + +internal static class MassTransitApplicationUtils +{ + public static ApplicationConsumerMapping[] GetApplicationConsumerMaps() + { + var openDomainEventConsumer = typeof(DomainEventConsumer<,>); + var openDomainEventConsumerDefinition = typeof(DomainEventConsumerDefinition<,>); + return ApplicationEventHandlerUtils + .GetHandlerEventMaps() + .Select(x => new ApplicationConsumerMapping( + AppConsumerType: x.HandlerType, + BusConsumerType: openDomainEventConsumer.MakeGenericType(x.HandlerType, x.EventType), + BusDefinitionType: openDomainEventConsumerDefinition.MakeGenericType(x.HandlerType, x.EventType))) + .ToArray(); + } +} + + +internal record struct ApplicationConsumerMapping(Type AppConsumerType, Type BusConsumerType, Type BusDefinitionType); diff --git a/src/Digdir.Domain.Dialogporten.Service/Consumers/DomainEventConsumer.cs b/src/Digdir.Domain.Dialogporten.Service/Consumers/DomainEventConsumer.cs index 0bec9f371..0d514b73e 100644 --- a/src/Digdir.Domain.Dialogporten.Service/Consumers/DomainEventConsumer.cs +++ b/src/Digdir.Domain.Dialogporten.Service/Consumers/DomainEventConsumer.cs @@ -20,13 +20,7 @@ public sealed class DomainEventConsumerDefinition : ConsumerDe { public DomainEventConsumerDefinition() { - var @class = typeof(THandler); - var @interface = typeof(INotificationHandler); - var @event = typeof(TEvent); - EndpointName = @class.GetInterfaceMap(@interface) - .TargetMethods.Single() - .GetCustomAttribute()? - .Name ?? EndpointNameAttribute.DefaultName(@class, @event); + EndpointName = EndpointNameAttribute.GetName(); } protected override void ConfigureConsumer( diff --git a/src/Digdir.Domain.Dialogporten.Service/Program.cs b/src/Digdir.Domain.Dialogporten.Service/Program.cs index c17fa3e7d..fa2ab942d 100644 --- a/src/Digdir.Domain.Dialogporten.Service/Program.cs +++ b/src/Digdir.Domain.Dialogporten.Service/Program.cs @@ -58,7 +58,7 @@ static void BuildAndRun(string[] args, TelemetryConfiguration telemetryConfigura .WithPubSubCapabilities() .AndBusConfiguration(x => { - foreach (var map in Extensions.GetApplicationConsumerMaps()) + foreach (var map in MassTransitApplicationUtils.GetApplicationConsumerMaps()) { x.TryAddTransient(map.AppConsumerType); x.AddConsumer(map.BusConsumerType, map.BusDefinitionType); diff --git a/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/ApplicationEventHandlerUtilsTests.cs b/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/ApplicationEventHandlerUtilsTests.cs new file mode 100644 index 000000000..6f60c0cfb --- /dev/null +++ b/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/ApplicationEventHandlerUtilsTests.cs @@ -0,0 +1,38 @@ +using Digdir.Domain.Dialogporten.Application.Common; +using FluentAssertions; + +namespace Digdir.Domain.Dialogporten.Application.Unit.Tests; + +public class ApplicationEventHandlerUtilsTests +{ + [Fact] + public void Can_Not_Have_Duplicate_Endpoint_Names_On_Domain_Event_Handlers() + { + // Act + var sameNameEndpoints = ApplicationEventHandlerUtils + .GetHandlerEventMaps() + .Select(x => EndpointNameAttribute.GetName(x.HandlerType, x.EventType)) + .GroupBy(x => x) + .Where(x => x.Count() > 1) + .ToArray(); + + // Assert + sameNameEndpoints.Should().BeEmpty(); + } + + /// + /// Did you break this test, sir? If so, you should use caution when renaming endpoints. + /// + [Fact] + public async Task Developer_Should_Use_Caution_When_Renaming_Endpoints() + { + // Act + var map = ApplicationEventHandlerUtils + .GetHandlerEventMaps() + .Select(x => EndpointNameAttribute.GetName(x.HandlerType, x.EventType)) + .ToArray(); + + // Assert + await Verify(map).UseDirectory("Snapshots"); + } +} diff --git a/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Digdir.Domain.Dialogporten.Application.Unit.Tests.csproj b/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Digdir.Domain.Dialogporten.Application.Unit.Tests.csproj index 3f62f42de..a5439f5ef 100644 --- a/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Digdir.Domain.Dialogporten.Application.Unit.Tests.csproj +++ b/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Digdir.Domain.Dialogporten.Application.Unit.Tests.csproj @@ -13,6 +13,7 @@ + all diff --git a/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Snapshots/ApplicationEventHandlerUtilsTests.Should_Have_Same_Snapshot.verified.txt b/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Snapshots/ApplicationEventHandlerUtilsTests.Should_Have_Same_Snapshot.verified.txt new file mode 100644 index 000000000..0df503d36 --- /dev/null +++ b/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Snapshots/ApplicationEventHandlerUtilsTests.Should_Have_Same_Snapshot.verified.txt @@ -0,0 +1,7 @@ +[ + DialogEventToAltinnForwarder_DialogActivityCreatedDomainEvent, + DialogEventToAltinnForwarder_DialogCreatedDomainEvent, + DialogEventToAltinnForwarder_DialogUpdatedDomainEvent, + DialogEventToAltinnForwarder_DialogDeletedDomainEvent, + DialogEventToAltinnForwarder_DialogSeenDomainEvent +] \ No newline at end of file From 48f836ae7b6446e0f550e4709a3474f504e3a416 Mon Sep 17 00:00:00 2001 From: Magnus Sandgren <5285192+MagnusSandgren@users.noreply.github.com> Date: Fri, 8 Nov 2024 12:16:54 +0100 Subject: [PATCH 6/9] Add Events-topic name test. Add XML comments to tests for clarity. --- .../ApplicationEventHandlerUtilsTests.cs | 76 ++++++++++++++++++- ...ion_When_Modifying_Endpoints.verified.txt} | 4 +- ...Caution_When_Modifying_Events.verified.txt | 7 ++ 3 files changed, 82 insertions(+), 5 deletions(-) rename tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Snapshots/{ApplicationEventHandlerUtilsTests.Should_Have_Same_Snapshot.verified.txt => ApplicationEventHandlerUtilsTests.Developer_Should_Use_Caution_When_Modifying_Endpoints.verified.txt} (62%) create mode 100644 tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Snapshots/ApplicationEventHandlerUtilsTests.Developer_Should_Use_Caution_When_Modifying_Events.verified.txt diff --git a/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/ApplicationEventHandlerUtilsTests.cs b/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/ApplicationEventHandlerUtilsTests.cs index 6f60c0cfb..12dcd2ce5 100644 --- a/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/ApplicationEventHandlerUtilsTests.cs +++ b/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/ApplicationEventHandlerUtilsTests.cs @@ -14,22 +14,92 @@ public void Can_Not_Have_Duplicate_Endpoint_Names_On_Domain_Event_Handlers() .Select(x => EndpointNameAttribute.GetName(x.HandlerType, x.EventType)) .GroupBy(x => x) .Where(x => x.Count() > 1) + .Select(x => x.Key) .ToArray(); // Assert - sameNameEndpoints.Should().BeEmpty(); + sameNameEndpoints.Should().BeEmpty(because: + "multiple handlers with the same endpoint name will consume " + + "the same queue, thereby competing for the same messages"); } /// - /// Did you break this test, sir? If so, you should use caution when renaming endpoints. + /// We should use caution when modifying endpoints, as this will change the target queue. + /// Scenarios on deployment of new code where snapshot test fails: + /// + /// + /// Endpoint added to snapshot: A new queue is created. This is considered safe. + /// + /// + /// Endpoint removed from snapshot: The queue is NOT removed automatically and + /// possibly contains unconsumed messages. + /// + /// + /// Endpoint renamed in snapshot: See both of the above bullet points. + /// + /// + /// Required actions upon accepting snapshot changes: + /// + /// + /// Unconsumed messages: Move to new queue or delete (cautiously) through manual intervention. + /// + /// + /// Ghost queue: Delete through manual intervention when above is done. + /// + /// /// + /// + /// Did you only intend to rename the handler or event type? If so, consider using the + /// to keep the endpoint name static. + /// [Fact] - public async Task Developer_Should_Use_Caution_When_Renaming_Endpoints() + public async Task Developer_Should_Use_Caution_When_Modifying_Endpoints() { // Act var map = ApplicationEventHandlerUtils .GetHandlerEventMaps() .Select(x => EndpointNameAttribute.GetName(x.HandlerType, x.EventType)) + .Order() + .ToArray(); + + // Assert + await Verify(map).UseDirectory("Snapshots"); + } + + /// + /// We should use caution when modifying events, as this will change the target topic, and possibly + /// the message schema. Scenarios on deployment of new code where snapshot test fails: + /// + /// + /// Event added to snapshot: A new topic is created. This is considered safe. + /// + /// + /// Event removed from snapshot: Handlers whose queues were bound to this topic mey contain + /// unprocessable messages due to old schema format, and the topic is NOT removed + /// automatically. + /// + /// + /// Event renamed in snapshot: See both of the above bullet points. + /// + /// + /// Required actions upon accepting snapshot changes: + /// + /// + /// Unprocessable messages: Will end up in the dead-letter queue. From there they can + /// be altered, re-queued or deleted through manual intervention. + /// + /// Ghost topic: Delete through manual intervention. + /// + /// + [Fact] + public async Task Developer_Should_Use_Caution_When_Modifying_Events() + { + // Act + var map = ApplicationEventHandlerUtils + .GetHandlerEventMaps() + .Select(x => x.EventType.FullName) + .Order() + .Distinct() .ToArray(); // Assert diff --git a/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Snapshots/ApplicationEventHandlerUtilsTests.Should_Have_Same_Snapshot.verified.txt b/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Snapshots/ApplicationEventHandlerUtilsTests.Developer_Should_Use_Caution_When_Modifying_Endpoints.verified.txt similarity index 62% rename from tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Snapshots/ApplicationEventHandlerUtilsTests.Should_Have_Same_Snapshot.verified.txt rename to tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Snapshots/ApplicationEventHandlerUtilsTests.Developer_Should_Use_Caution_When_Modifying_Endpoints.verified.txt index 0df503d36..0215cd8fc 100644 --- a/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Snapshots/ApplicationEventHandlerUtilsTests.Should_Have_Same_Snapshot.verified.txt +++ b/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Snapshots/ApplicationEventHandlerUtilsTests.Developer_Should_Use_Caution_When_Modifying_Endpoints.verified.txt @@ -1,7 +1,7 @@ [ DialogEventToAltinnForwarder_DialogActivityCreatedDomainEvent, DialogEventToAltinnForwarder_DialogCreatedDomainEvent, - DialogEventToAltinnForwarder_DialogUpdatedDomainEvent, DialogEventToAltinnForwarder_DialogDeletedDomainEvent, - DialogEventToAltinnForwarder_DialogSeenDomainEvent + DialogEventToAltinnForwarder_DialogSeenDomainEvent, + DialogEventToAltinnForwarder_DialogUpdatedDomainEvent ] \ No newline at end of file diff --git a/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Snapshots/ApplicationEventHandlerUtilsTests.Developer_Should_Use_Caution_When_Modifying_Events.verified.txt b/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Snapshots/ApplicationEventHandlerUtilsTests.Developer_Should_Use_Caution_When_Modifying_Events.verified.txt new file mode 100644 index 000000000..9cf796dac --- /dev/null +++ b/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Snapshots/ApplicationEventHandlerUtilsTests.Developer_Should_Use_Caution_When_Modifying_Events.verified.txt @@ -0,0 +1,7 @@ +[ + Digdir.Domain.Dialogporten.Domain.Dialogs.Events.Activities.DialogActivityCreatedDomainEvent, + Digdir.Domain.Dialogporten.Domain.Dialogs.Events.DialogCreatedDomainEvent, + Digdir.Domain.Dialogporten.Domain.Dialogs.Events.DialogDeletedDomainEvent, + Digdir.Domain.Dialogporten.Domain.Dialogs.Events.DialogSeenDomainEvent, + Digdir.Domain.Dialogporten.Domain.Dialogs.Events.DialogUpdatedDomainEvent +] \ No newline at end of file From 5d5f06d71043989b2c6bff9c5c6de80d04ea7846 Mon Sep 17 00:00:00 2001 From: Magnus Sandgren <5285192+MagnusSandgren@users.noreply.github.com> Date: Fri, 8 Nov 2024 14:36:19 +0100 Subject: [PATCH 7/9] WIP --- ...Should_Use_Caution_When_Modifying_Endpoints.verified.txt | 0 ...er_Should_Use_Caution_When_Modifying_Events.verified.txt | 0 .../V1/Common/Utils}/ApplicationEventHandlerUtilsTests.cs | 6 +++--- 3 files changed, 3 insertions(+), 3 deletions(-) rename tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/{Snapshots => Features/V1/Common/Utils}/ApplicationEventHandlerUtilsTests.Developer_Should_Use_Caution_When_Modifying_Endpoints.verified.txt (100%) rename tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/{Snapshots => Features/V1/Common/Utils}/ApplicationEventHandlerUtilsTests.Developer_Should_Use_Caution_When_Modifying_Events.verified.txt (100%) rename tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/{ => Features/V1/Common/Utils}/ApplicationEventHandlerUtilsTests.cs (95%) diff --git a/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Snapshots/ApplicationEventHandlerUtilsTests.Developer_Should_Use_Caution_When_Modifying_Endpoints.verified.txt b/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Features/V1/Common/Utils/ApplicationEventHandlerUtilsTests.Developer_Should_Use_Caution_When_Modifying_Endpoints.verified.txt similarity index 100% rename from tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Snapshots/ApplicationEventHandlerUtilsTests.Developer_Should_Use_Caution_When_Modifying_Endpoints.verified.txt rename to tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Features/V1/Common/Utils/ApplicationEventHandlerUtilsTests.Developer_Should_Use_Caution_When_Modifying_Endpoints.verified.txt diff --git a/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Snapshots/ApplicationEventHandlerUtilsTests.Developer_Should_Use_Caution_When_Modifying_Events.verified.txt b/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Features/V1/Common/Utils/ApplicationEventHandlerUtilsTests.Developer_Should_Use_Caution_When_Modifying_Events.verified.txt similarity index 100% rename from tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Snapshots/ApplicationEventHandlerUtilsTests.Developer_Should_Use_Caution_When_Modifying_Events.verified.txt rename to tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Features/V1/Common/Utils/ApplicationEventHandlerUtilsTests.Developer_Should_Use_Caution_When_Modifying_Events.verified.txt diff --git a/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/ApplicationEventHandlerUtilsTests.cs b/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Features/V1/Common/Utils/ApplicationEventHandlerUtilsTests.cs similarity index 95% rename from tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/ApplicationEventHandlerUtilsTests.cs rename to tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Features/V1/Common/Utils/ApplicationEventHandlerUtilsTests.cs index 12dcd2ce5..85d242c02 100644 --- a/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/ApplicationEventHandlerUtilsTests.cs +++ b/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Features/V1/Common/Utils/ApplicationEventHandlerUtilsTests.cs @@ -1,7 +1,7 @@ using Digdir.Domain.Dialogporten.Application.Common; using FluentAssertions; -namespace Digdir.Domain.Dialogporten.Application.Unit.Tests; +namespace Digdir.Domain.Dialogporten.Application.Unit.Tests.Features.V1.Common.Utils; public class ApplicationEventHandlerUtilsTests { @@ -63,7 +63,7 @@ public async Task Developer_Should_Use_Caution_When_Modifying_Endpoints() .ToArray(); // Assert - await Verify(map).UseDirectory("Snapshots"); + await Verify(map); } /// @@ -103,6 +103,6 @@ public async Task Developer_Should_Use_Caution_When_Modifying_Events() .ToArray(); // Assert - await Verify(map).UseDirectory("Snapshots"); + await Verify(map); } } From 63f6e4f68f61368085e1f2882ef4b62a45612dd8 Mon Sep 17 00:00:00 2001 From: Magnus Sandgren <5285192+MagnusSandgren@users.noreply.github.com> Date: Fri, 8 Nov 2024 15:01:04 +0100 Subject: [PATCH 8/9] Make EndpointNameAttribute.cs internal. --- .../Common/ApplicationEventHandlerUtils.cs | 4 ++-- .../Common/EndpointNameAttribute.cs | 7 +------ .../Common/MassTransitApplicationUtils.cs | 5 +++-- .../Consumers/DomainEventConsumer.cs | 7 ------- src/Digdir.Domain.Dialogporten.Service/Program.cs | 3 ++- 5 files changed, 8 insertions(+), 18 deletions(-) diff --git a/src/Digdir.Domain.Dialogporten.Application/Common/ApplicationEventHandlerUtils.cs b/src/Digdir.Domain.Dialogporten.Application/Common/ApplicationEventHandlerUtils.cs index bfc7f5780..d026427fa 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Common/ApplicationEventHandlerUtils.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Common/ApplicationEventHandlerUtils.cs @@ -18,9 +18,9 @@ public static HandlerEventMapping[] GetHandlerEventMaps() && @interface.GetGenericTypeDefinition() == openNotificationHandler && @interface.GetGenericArguments().Single().IsAssignableTo(domainEventType)) .Select(@interface => (@class: x, @event: @interface.GetGenericArguments().Single())) - .Select(x => new HandlerEventMapping(x.@class.AsType(), x.@event))) + .Select(x => new HandlerEventMapping(x.@class.AsType(), x.@event, EndpointNameAttribute.GetName(x.@class, x.@event)))) .ToArray(); } } -public record struct HandlerEventMapping(Type HandlerType, Type EventType); +public record struct HandlerEventMapping(Type HandlerType, Type EventType, string EndpointName); diff --git a/src/Digdir.Domain.Dialogporten.Application/Common/EndpointNameAttribute.cs b/src/Digdir.Domain.Dialogporten.Application/Common/EndpointNameAttribute.cs index 32853fc5d..f9382c091 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Common/EndpointNameAttribute.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Common/EndpointNameAttribute.cs @@ -14,7 +14,7 @@ namespace Digdir.Domain.Dialogporten.Application.Common; /// /// [AttributeUsage(AttributeTargets.Method)] -public sealed class EndpointNameAttribute : Attribute +internal sealed class EndpointNameAttribute : Attribute { public string Name { get; } @@ -28,11 +28,6 @@ public EndpointNameAttribute(string name) Name = name; } - public static string GetName() - where THandler : INotificationHandler - where TEvent : class, IDomainEvent - => GetName(typeof(THandler), typeof(TEvent)); - public static string GetName(Type handlerType, Type eventType) => handlerType .GetInterfaceMap(typeof(INotificationHandler<>).MakeGenericType(eventType)) diff --git a/src/Digdir.Domain.Dialogporten.Service/Common/MassTransitApplicationUtils.cs b/src/Digdir.Domain.Dialogporten.Service/Common/MassTransitApplicationUtils.cs index 2de642b12..826b20f80 100644 --- a/src/Digdir.Domain.Dialogporten.Service/Common/MassTransitApplicationUtils.cs +++ b/src/Digdir.Domain.Dialogporten.Service/Common/MassTransitApplicationUtils.cs @@ -14,10 +14,11 @@ public static ApplicationConsumerMapping[] GetApplicationConsumerMaps() .Select(x => new ApplicationConsumerMapping( AppConsumerType: x.HandlerType, BusConsumerType: openDomainEventConsumer.MakeGenericType(x.HandlerType, x.EventType), - BusDefinitionType: openDomainEventConsumerDefinition.MakeGenericType(x.HandlerType, x.EventType))) + BusDefinitionType: openDomainEventConsumerDefinition.MakeGenericType(x.HandlerType, x.EventType), + EndpointName: x.EndpointName)) .ToArray(); } } -internal record struct ApplicationConsumerMapping(Type AppConsumerType, Type BusConsumerType, Type BusDefinitionType); +internal record struct ApplicationConsumerMapping(Type AppConsumerType, Type BusConsumerType, Type BusDefinitionType, string EndpointName); diff --git a/src/Digdir.Domain.Dialogporten.Service/Consumers/DomainEventConsumer.cs b/src/Digdir.Domain.Dialogporten.Service/Consumers/DomainEventConsumer.cs index 0d514b73e..8477aeeff 100644 --- a/src/Digdir.Domain.Dialogporten.Service/Consumers/DomainEventConsumer.cs +++ b/src/Digdir.Domain.Dialogporten.Service/Consumers/DomainEventConsumer.cs @@ -1,8 +1,6 @@ -using System.Reflection; using Digdir.Domain.Dialogporten.Domain.Common.EventPublisher; using MassTransit; using MediatR; -using EndpointNameAttribute = Digdir.Domain.Dialogporten.Application.Common.EndpointNameAttribute; namespace Digdir.Domain.Dialogporten.Service.Consumers; @@ -18,11 +16,6 @@ public sealed class DomainEventConsumerDefinition : ConsumerDe where THandler : INotificationHandler where TEvent : class, IDomainEvent { - public DomainEventConsumerDefinition() - { - EndpointName = EndpointNameAttribute.GetName(); - } - protected override void ConfigureConsumer( IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator> consumerConfigurator, diff --git a/src/Digdir.Domain.Dialogporten.Service/Program.cs b/src/Digdir.Domain.Dialogporten.Service/Program.cs index fa2ab942d..a0d9de702 100644 --- a/src/Digdir.Domain.Dialogporten.Service/Program.cs +++ b/src/Digdir.Domain.Dialogporten.Service/Program.cs @@ -61,7 +61,8 @@ static void BuildAndRun(string[] args, TelemetryConfiguration telemetryConfigura foreach (var map in MassTransitApplicationUtils.GetApplicationConsumerMaps()) { x.TryAddTransient(map.AppConsumerType); - x.AddConsumer(map.BusConsumerType, map.BusDefinitionType); + x.AddConsumer(map.BusConsumerType, map.BusDefinitionType) + .Endpoint(x => x.Name = map.EndpointName); } }) .Build() From d54d3eaaf4d0e9437c398733dfb1ebca8f333972 Mon Sep 17 00:00:00 2001 From: Magnus Sandgren <5285192+MagnusSandgren@users.noreply.github.com> Date: Fri, 8 Nov 2024 15:11:03 +0100 Subject: [PATCH 9/9] Remove rouge space --- .../Common/MassTransitApplicationUtils.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Digdir.Domain.Dialogporten.Service/Common/MassTransitApplicationUtils.cs b/src/Digdir.Domain.Dialogporten.Service/Common/MassTransitApplicationUtils.cs index 826b20f80..eef611a36 100644 --- a/src/Digdir.Domain.Dialogporten.Service/Common/MassTransitApplicationUtils.cs +++ b/src/Digdir.Domain.Dialogporten.Service/Common/MassTransitApplicationUtils.cs @@ -20,5 +20,4 @@ public static ApplicationConsumerMapping[] GetApplicationConsumerMaps() } } - internal record struct ApplicationConsumerMapping(Type AppConsumerType, Type BusConsumerType, Type BusDefinitionType, string EndpointName);