diff --git a/Directory.Build.props b/Directory.Build.props index 930ae77..a3b2cd8 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,7 +6,7 @@ - 3.0.2 + 3.1.0 3.0.0 diff --git a/src/ReflectionEventing.DependencyInjection/DependencyInjectionEventBusBuilder.cs b/src/ReflectionEventing.DependencyInjection/DependencyInjectionEventBusBuilder.cs index 84fd2a1..a1bfc63 100644 --- a/src/ReflectionEventing.DependencyInjection/DependencyInjectionEventBusBuilder.cs +++ b/src/ReflectionEventing.DependencyInjection/DependencyInjectionEventBusBuilder.cs @@ -3,15 +3,50 @@ // Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. // All Rights Reserved. -using System.Diagnostics.CodeAnalysis; - namespace ReflectionEventing.DependencyInjection; /// /// Represents a builder for configuring the event bus with .NET Core's built-in dependency injection. /// +// ReSharper disable once ClassWithVirtualMembersNeverInherited.Global public class DependencyInjectionEventBusBuilder(IServiceCollection services) : EventBusBuilder { + /// + /// Adds a consumer to the event bus and with a specified service lifetime. + /// + /// The type of the consumer to add. + /// The service lifetime of the consumer. + /// The current instance of . + /// + /// Thrown if the consumer is already registered with a different lifetime or if the consumer is not registered in the service collection. + /// + public virtual EventBusBuilder AddConsumer( +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] +#endif + Type consumerType, + ServiceLifetime lifetime + ) + { + ServiceDescriptor? descriptor = services.FirstOrDefault(d => d.ServiceType == consumerType); + + if (descriptor is not null) + { + if (descriptor.Lifetime != lifetime) + { + throw new InvalidOperationException( + "Event consumer must be registered with the same lifetime as the one provided." + ); + } + + return base.AddConsumer(consumerType); + } + + services.Add(new ServiceDescriptor(consumerType, consumerType, lifetime)); + + return base.AddConsumer(consumerType); + } + /// public override EventBusBuilder AddConsumer( #if NET5_0_OR_GREATER diff --git a/src/ReflectionEventing.DependencyInjection/EventBusBuilderExtensions.cs b/src/ReflectionEventing.DependencyInjection/EventBusBuilderExtensions.cs new file mode 100644 index 0000000..b229253 --- /dev/null +++ b/src/ReflectionEventing.DependencyInjection/EventBusBuilderExtensions.cs @@ -0,0 +1,189 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. +// All Rights Reserved. + +namespace ReflectionEventing.DependencyInjection; + +public static class EventBusBuilderExtensions +{ + /// + /// Adds a consumer to the event bus builder and . + /// + /// The event bus builder to add the consumer to. + /// The type of the consumer to add. + /// The event bus builder with the consumer added. + public static EventBusBuilder AddTransientConsumer( + this EventBusBuilder builder, +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] +#endif + Type consumerType + ) + { + if (builder is not DependencyInjectionEventBusBuilder dependencyInjectionEventBusBuilder) + { + throw new InvalidOperationException( + "The event bus builder must be of type DependencyInjectionEventBusBuilder to add a transient consumer." + ); + } + + return dependencyInjectionEventBusBuilder.AddConsumer( + consumerType, + ServiceLifetime.Transient + ); + } + + /// + /// Adds a consumer to the event bus builder and . + /// + /// The type of the consumer to add. + /// The event bus builder to add the consumer to. + /// The event bus builder with the consumer added. + public static EventBusBuilder AddTransientConsumer< +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] +#endif + TConsumer + >(this EventBusBuilder builder) + { + if (builder is not DependencyInjectionEventBusBuilder dependencyInjectionEventBusBuilder) + { + throw new InvalidOperationException( + "The event bus builder must be of type DependencyInjectionEventBusBuilder to add a transient consumer." + ); + } + + return dependencyInjectionEventBusBuilder.AddConsumer( + typeof(TConsumer), + ServiceLifetime.Transient + ); + } + + /// + /// Adds a consumer to the event bus builder and . + /// + /// The event bus builder to add the consumer to. + /// The type of the consumer to add. + /// The event bus builder with the consumer added. + public static EventBusBuilder AddScopedConsumer( + this EventBusBuilder builder, +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] +#endif + Type consumerType + ) + { + if (builder is not DependencyInjectionEventBusBuilder dependencyInjectionEventBusBuilder) + { + throw new InvalidOperationException( + "The event bus builder must be of type DependencyInjectionEventBusBuilder to add a scoped consumer." + ); + } + + return dependencyInjectionEventBusBuilder.AddConsumer(consumerType, ServiceLifetime.Scoped); + } + + /// + /// Adds a consumer to the event bus builder and . + /// + /// The type of the consumer to add. + /// The event bus builder to add the consumer to. + /// The event bus builder with the consumer added. + public static EventBusBuilder AddScopedConsumer< +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] +#endif + TConsumer + >(this EventBusBuilder builder) + { + if (builder is not DependencyInjectionEventBusBuilder dependencyInjectionEventBusBuilder) + { + throw new InvalidOperationException( + "The event bus builder must be of type DependencyInjectionEventBusBuilder to add a scoped consumer." + ); + } + + return dependencyInjectionEventBusBuilder.AddConsumer( + typeof(TConsumer), + ServiceLifetime.Scoped + ); + } + + /// + /// Adds a consumer to the event bus builder and . + /// + /// The event bus builder to add the consumer to. + /// The type of the consumer to add. + /// The event bus builder with the consumer added. + public static EventBusBuilder AddSingletonConsumer( + this EventBusBuilder builder, +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] +#endif + Type consumerType + ) + { + if (builder is not DependencyInjectionEventBusBuilder dependencyInjectionEventBusBuilder) + { + throw new InvalidOperationException( + "The event bus builder must be of type DependencyInjectionEventBusBuilder to add a singleton consumer." + ); + } + + return dependencyInjectionEventBusBuilder.AddConsumer( + consumerType, + ServiceLifetime.Singleton + ); + } + + /// + /// Adds a consumer to the event bus builder and . + /// + /// The type of the consumer to add. + /// The event bus builder to add the consumer to. + /// The event bus builder with the consumer added. + public static EventBusBuilder AddSingletonConsumer< +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] +#endif + TConsumer + >(this EventBusBuilder builder) + { + if (builder is not DependencyInjectionEventBusBuilder dependencyInjectionEventBusBuilder) + { + throw new InvalidOperationException( + "The event bus builder must be of type DependencyInjectionEventBusBuilder to add a singleton consumer." + ); + } + + return dependencyInjectionEventBusBuilder.AddConsumer( + typeof(TConsumer), + ServiceLifetime.Singleton + ); + } + + /// + /// Adds a consumer to the event bus builder and . + /// + /// The type of the consumer to add. + /// The event bus builder to add the consumer to. + /// The service lifetime of the consumer. + /// The event bus builder with the consumer added. + public static EventBusBuilder AddConsumer< +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] +#endif + TConsumer + >(this EventBusBuilder builder, ServiceLifetime lifetime) + { + if (builder is not DependencyInjectionEventBusBuilder dependencyInjectionEventBusBuilder) + { + throw new InvalidOperationException( + "The event bus builder must be of type DependencyInjectionEventBusBuilder to add a transient consumer." + ); + } + + return dependencyInjectionEventBusBuilder.AddConsumer(typeof(TConsumer), lifetime); + } +} diff --git a/src/ReflectionEventing.DependencyInjection/GlobalUsings.cs b/src/ReflectionEventing.DependencyInjection/GlobalUsings.cs index b52d80b..eb86b05 100644 --- a/src/ReflectionEventing.DependencyInjection/GlobalUsings.cs +++ b/src/ReflectionEventing.DependencyInjection/GlobalUsings.cs @@ -5,5 +5,6 @@ global using System; global using System.Collections.Generic; +global using System.Diagnostics.CodeAnalysis; global using System.Linq; global using Microsoft.Extensions.DependencyInjection; diff --git a/tests/ReflectionEventing.DependencyInjection.UnitTests/EventBusBuilderExtensionsTests.cs b/tests/ReflectionEventing.DependencyInjection.UnitTests/EventBusBuilderExtensionsTests.cs new file mode 100644 index 0000000..e917750 --- /dev/null +++ b/tests/ReflectionEventing.DependencyInjection.UnitTests/EventBusBuilderExtensionsTests.cs @@ -0,0 +1,117 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. +// All Rights Reserved. + +using Microsoft.Extensions.DependencyInjection; + +namespace ReflectionEventing.DependencyInjection.UnitTests; + +public sealed class EventBusBuilderExtensionsTests +{ + [Fact] + public void AddConsumer_ShouldRegisterConsumerWhenLifetimeProvided() + { + IServiceCollection services = new ServiceCollection(); + DependencyInjectionEventBusBuilder eventBusBuilder = new(services); + + eventBusBuilder.AddConsumer(ServiceLifetime.Transient); + + IConsumerTypesProvider consumerTypesProvider = eventBusBuilder.BuildTypesProvider(); + _ = consumerTypesProvider + .GetConsumerTypes(typeof(TestEvent)) + .Should() + .Contain(typeof(TestConsumer)); + + services + .First(d => d.ServiceType == typeof(TestConsumer)) + .Lifetime.Should() + .Be(ServiceLifetime.Transient); + } + + [Fact] + public void AddScopedConsumer_ShouldThrowExceptionWhenConsumerIsRegisteredWithDifferentScope() + { + IServiceCollection services = new ServiceCollection(); + _ = services.AddSingleton(); + DependencyInjectionEventBusBuilder eventBusBuilder = new(services); + + Action act = () => eventBusBuilder.AddScopedConsumer(typeof(TestConsumer)); + + _ = act.Should() + .Throw() + .WithMessage( + "Event consumer must be registered with the same lifetime as the one provided." + ); + } + + [Fact] + public void AddScopedConsumer_ShouldRegisterTransientConsumer() + { + IServiceCollection services = new ServiceCollection(); + DependencyInjectionEventBusBuilder eventBusBuilder = new(services); + + eventBusBuilder.AddTransientConsumer(); + + IConsumerTypesProvider consumerTypesProvider = eventBusBuilder.BuildTypesProvider(); + _ = consumerTypesProvider + .GetConsumerTypes(typeof(TestEvent)) + .Should() + .Contain(typeof(TestConsumer)); + + services + .First(d => d.ServiceType == typeof(TestConsumer)) + .Lifetime.Should() + .Be(ServiceLifetime.Transient); + } + + [Fact] + public void AddScopedConsumer_ShouldRegisterScopedConsumer() + { + IServiceCollection services = new ServiceCollection(); + DependencyInjectionEventBusBuilder eventBusBuilder = new(services); + + eventBusBuilder.AddScopedConsumer(); + + IConsumerTypesProvider consumerTypesProvider = eventBusBuilder.BuildTypesProvider(); + _ = consumerTypesProvider + .GetConsumerTypes(typeof(TestEvent)) + .Should() + .Contain(typeof(TestConsumer)); + + services + .First(d => d.ServiceType == typeof(TestConsumer)) + .Lifetime.Should() + .Be(ServiceLifetime.Scoped); + } + + [Fact] + public void AddScopedConsumer_ShouldRegisterSingletonConsumer() + { + IServiceCollection services = new ServiceCollection(); + DependencyInjectionEventBusBuilder eventBusBuilder = new(services); + + eventBusBuilder.AddSingletonConsumer(); + + IConsumerTypesProvider consumerTypesProvider = eventBusBuilder.BuildTypesProvider(); + _ = consumerTypesProvider + .GetConsumerTypes(typeof(TestEvent)) + .Should() + .Contain(typeof(TestConsumer)); + + services + .First(d => d.ServiceType == typeof(TestConsumer)) + .Lifetime.Should() + .Be(ServiceLifetime.Singleton); + } + + public interface ITestEvent; + + public record TestEvent : ITestEvent; + + public class TestConsumer : IConsumer + { + public Task ConsumeAsync(TestEvent payload, CancellationToken cancellationToken) => + Task.CompletedTask; + } +}