From 584c4157d39a06b910aa6d63100cc1ed8e8f8fb6 Mon Sep 17 00:00:00 2001 From: Tomasz Maruszak Date: Wed, 22 Feb 2023 00:12:38 +0100 Subject: [PATCH] Default consumer type to the generic interface to shorten consumer configuration Signed-off-by: Tomasz Maruszak --- docs/intro.md | 24 ++++++- src/Host.Interceptor.Properties.xml | 2 +- src/Host.Transport.Properties.xml | 2 +- .../Config/ConsumerSettings.cs | 7 +- .../Config/Fluent/ConsumerBuilder.cs | 4 +- .../Config/Fluent/HandlerBuilder.cs | 5 +- .../Config/Fluent/MessageBusBuilder.cs | 67 ++++++++++++------- .../ServiceCollectionExtensions.cs | 13 +++- .../OutboxTests.cs | 1 - .../Config/ConsumerBuilderTest.cs | 4 +- .../Config/HandlerBuilderTest.cs | 42 ++++++++++-- .../Config/MessageBusBuilderTests.cs | 45 +++++++++++-- 12 files changed, 158 insertions(+), 58 deletions(-) diff --git a/docs/intro.md b/docs/intro.md index d0d1d76b..05df6616 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -236,7 +236,9 @@ public class SomeConsumer : IConsumer } ``` -The `SomeConsumer` needs to be registered in your DI container. The SMB runtime will ask the chosen DI to provide the desired number of consumer instances. Any collaborators of the consumer will be resolved according to your DI configuration. +The `SomeConsumer` needs to be registered in the DI container. The SMB runtime will ask the DI to provide the consumer instance. + +> When `.WithConsumer()` is not declared, then a default consumer of type `IConsumer` will be assumed (since v2.0.0). Alternatively, if you do not want to implement the `IConsumer`, then you can provide the method name (2) or a delegate that calls the consumer method (3): @@ -546,7 +548,9 @@ mbb.Handle(x => x ) ``` -> The same micro-service can both send the request and also be the handler of those requests. +The same micro-service can both send the request and also be the handler of those requests. + +> When `.WithHandler()` is not declared, then a default handler of type `IRequestHandler` will be assumed (since v2.0.0). ## Static accessor @@ -598,6 +602,22 @@ services.AddMessageBusInterceptorsFromAssembly(Assembly.GetExecutingAssembly()); services.AddMessageBusConfiguratorsFromAssembly(Assembly.GetExecutingAssembly()); ``` +Consider the following example: + +```cs +// Given a consumer that is found: +public class SomeMessageConsumer : IConsumer +{ +} + +// When auto-registration is used: +services.AddMessageBusConsumersFromAssembly(Assembly.GetExecutingAssembly()); + +// Then it will cause the following MSDI setup: +services.TryAddTransient(); +services.TryAddTransient, SomeMessageConsumer>(); +``` + #### ASP.Net Core For ASP.NET services, it is recommended to use the [`AspNetCore`](https://www.nuget.org/packages/SlimMessageBus.Host.AspNetCore) plugin. To properly support request scopes for [MessageBus.Current](#static-accessor) static accessor, it has a dependency on the `IHttpContextAccessor` which [needs to be registered](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-context?view=aspnetcore-6.0#use-httpcontext-from-custom-components) during application setup: diff --git a/src/Host.Interceptor.Properties.xml b/src/Host.Interceptor.Properties.xml index fdf50553..c8b9aa50 100644 --- a/src/Host.Interceptor.Properties.xml +++ b/src/Host.Interceptor.Properties.xml @@ -4,7 +4,7 @@ - 2.0.0-rc1 + 2.0.0-rc2 true \ No newline at end of file diff --git a/src/Host.Transport.Properties.xml b/src/Host.Transport.Properties.xml index 5553966f..fb3c59fc 100644 --- a/src/Host.Transport.Properties.xml +++ b/src/Host.Transport.Properties.xml @@ -4,7 +4,7 @@ - 2.0.0-rc1 + 2.0.0-rc2 true \ No newline at end of file diff --git a/src/SlimMessageBus.Host/Config/ConsumerSettings.cs b/src/SlimMessageBus.Host/Config/ConsumerSettings.cs index dce7d3a4..33845755 100644 --- a/src/SlimMessageBus.Host/Config/ConsumerSettings.cs +++ b/src/SlimMessageBus.Host/Config/ConsumerSettings.cs @@ -24,11 +24,6 @@ private void CalculateResponseType() .SingleOrDefault(); } - public ConsumerSettings() - { - Invokers = new List(); - } - /// Type of consumer that is configured (subscriber or request handler). /// public ConsumerMode ConsumerMode { get; set; } @@ -39,7 +34,7 @@ public ConsumerSettings() /// /// List of all declared consumers that handle any derived message type of the declared message type. /// - public IList Invokers { get; } + public ISet Invokers { get; } = new HashSet(); public ConsumerSettings ParentSettings => this; /// diff --git a/src/SlimMessageBus.Host/Config/Fluent/ConsumerBuilder.cs b/src/SlimMessageBus.Host/Config/Fluent/ConsumerBuilder.cs index 93e6fbcd..3b495de3 100644 --- a/src/SlimMessageBus.Host/Config/Fluent/ConsumerBuilder.cs +++ b/src/SlimMessageBus.Host/Config/Fluent/ConsumerBuilder.cs @@ -5,6 +5,7 @@ public class ConsumerBuilder : AbstractConsumerBuilder public ConsumerBuilder(MessageBusSettings settings, Type messageType = null) : base(settings, messageType ?? typeof(T)) { + ConsumerSettings.ConsumerMode = ConsumerMode.Consumer; } public ConsumerBuilder Path(string path) @@ -35,7 +36,6 @@ public ConsumerBuilder Path(string path, Action> pathConfi public ConsumerBuilder WithConsumer() where TConsumer : class, IConsumer { - ConsumerSettings.ConsumerMode = ConsumerMode.Consumer; ConsumerSettings.ConsumerType = typeof(TConsumer); ConsumerSettings.ConsumerMethod = (consumer, message) => ((IConsumer)consumer).OnHandle((T)message); @@ -98,7 +98,6 @@ public ConsumerBuilder WithConsumer(Func consu { if (consumerMethod == null) throw new ArgumentNullException(nameof(consumerMethod)); - ConsumerSettings.ConsumerMode = ConsumerMode.Consumer; ConsumerSettings.ConsumerType = typeof(TConsumer); ConsumerSettings.ConsumerMethod = (consumer, message) => consumerMethod((TConsumer)consumer, (T)message); @@ -135,7 +134,6 @@ public ConsumerBuilder WithConsumer(Type consumerType, string consumerMethodN consumerMethodName ??= nameof(IConsumer.OnHandle); - ConsumerSettings.ConsumerMode = ConsumerMode.Consumer; ConsumerSettings.ConsumerType = consumerType; SetupConsumerOnHandleMethod(ConsumerSettings, consumerMethodName); diff --git a/src/SlimMessageBus.Host/Config/Fluent/HandlerBuilder.cs b/src/SlimMessageBus.Host/Config/Fluent/HandlerBuilder.cs index fcc30eeb..b1bfd491 100644 --- a/src/SlimMessageBus.Host/Config/Fluent/HandlerBuilder.cs +++ b/src/SlimMessageBus.Host/Config/Fluent/HandlerBuilder.cs @@ -7,6 +7,7 @@ public HandlerBuilder(MessageBusSettings settings, Type requestType = null, Type { if (settings == null) throw new ArgumentNullException(nameof(settings)); + ConsumerSettings.ConsumerMode = ConsumerMode.RequestResponse; ConsumerSettings.ResponseType = responseType ?? typeof(TResponse); } @@ -24,7 +25,7 @@ public HandlerBuilder(MessageBusSettings settings, Type requestType = null, Type /// public HandlerBuilder Path(string path) { - var consumerSettingsExist = Settings.Consumers.Any(x => x.Path == path && x.ConsumerMode == ConsumerMode.RequestResponse); + var consumerSettingsExist = Settings.Consumers.Any(x => x.Path == path && x.ConsumerMode == ConsumerMode.RequestResponse && x != ConsumerSettings); Assert.IsFalse(consumerSettingsExist, () => new ConfigurationMessageBusException($"Attempted to configure request handler for topic/queue '{path}' when one was already configured. You can only have one request handler for a given topic/queue, otherwise which response would you send back?")); @@ -61,7 +62,6 @@ public HandlerBuilder WithHandler() Assert.IsNotNull(ConsumerSettings.ResponseType, () => new ConfigurationMessageBusException($"The {nameof(ConsumerSettings)}.{nameof(ConsumerSettings.ResponseType)} is not set")); - ConsumerSettings.ConsumerMode = ConsumerMode.RequestResponse; ConsumerSettings.ConsumerType = typeof(THandler); ConsumerSettings.ConsumerMethod = (consumer, message) => ((THandler)consumer).OnHandle((TRequest)message); @@ -75,7 +75,6 @@ public HandlerBuilder WithHandler(Type handlerType) Assert.IsNotNull(ConsumerSettings.ResponseType, () => new ConfigurationMessageBusException($"The {nameof(ConsumerSettings)}.{nameof(ConsumerSettings.ResponseType)} is not set")); - ConsumerSettings.ConsumerMode = ConsumerMode.RequestResponse; ConsumerSettings.ConsumerType = handlerType; SetupConsumerOnHandleMethod(ConsumerSettings); diff --git a/src/SlimMessageBus.Host/Config/Fluent/MessageBusBuilder.cs b/src/SlimMessageBus.Host/Config/Fluent/MessageBusBuilder.cs index 63a0d8d8..18127733 100644 --- a/src/SlimMessageBus.Host/Config/Fluent/MessageBusBuilder.cs +++ b/src/SlimMessageBus.Host/Config/Fluent/MessageBusBuilder.cs @@ -45,14 +45,14 @@ public MessageBusBuilder MergeFrom(MessageBusSettings settings) /// Configures (declares) the production (publishing for pub/sub or request sending in request/response) of a message /// /// Type of the message - /// + /// /// - public MessageBusBuilder Produce(Action> producerBuilder) + public MessageBusBuilder Produce(Action> builder) { - if (producerBuilder == null) throw new ArgumentNullException(nameof(producerBuilder)); + if (builder == null) throw new ArgumentNullException(nameof(builder)); var item = new ProducerSettings(); - producerBuilder(new ProducerBuilder(item)); + builder(new ProducerBuilder(item)); Settings.Producers.Add(item); return this; } @@ -61,14 +61,14 @@ public MessageBusBuilder Produce(Action> producerBuilder) /// Configures (declares) the production (publishing for pub/sub or request sending in request/response) of a message /// /// Type of the message - /// + /// /// - public MessageBusBuilder Produce(Type messageType, Action> producerBuilder) + public MessageBusBuilder Produce(Type messageType, Action> builder) { - if (producerBuilder == null) throw new ArgumentNullException(nameof(producerBuilder)); + if (builder == null) throw new ArgumentNullException(nameof(builder)); var item = new ProducerSettings(); - producerBuilder(new ProducerBuilder(item, messageType)); + builder(new ProducerBuilder(item, messageType)); Settings.Producers.Add(item); return this; } @@ -77,13 +77,20 @@ public MessageBusBuilder Produce(Type messageType, Action /// Type of message - /// + /// /// - public MessageBusBuilder Consume(Action> consumerBuilder) + public MessageBusBuilder Consume(Action> builder) { - if (consumerBuilder == null) throw new ArgumentNullException(nameof(consumerBuilder)); - - consumerBuilder(new ConsumerBuilder(Settings)); + if (builder == null) throw new ArgumentNullException(nameof(builder)); + + var b = new ConsumerBuilder(Settings); + builder(b); + + if (b.ConsumerSettings.ConsumerType is null) + { + // Apply default consumer type of not set + b.WithConsumer>(); + } return this; } @@ -91,13 +98,13 @@ public MessageBusBuilder Consume(Action> con /// Configures (declares) the consumer of given message types in pub/sub or queue communication. /// /// Type of message - /// + /// /// - public MessageBusBuilder Consume(Type messageType, Action> consumerBuilder) + public MessageBusBuilder Consume(Type messageType, Action> builder) { - if (consumerBuilder == null) throw new ArgumentNullException(nameof(consumerBuilder)); + if (builder == null) throw new ArgumentNullException(nameof(builder)); - consumerBuilder(new ConsumerBuilder(Settings, messageType)); + builder(new ConsumerBuilder(Settings, messageType)); return this; } @@ -106,13 +113,21 @@ public MessageBusBuilder Consume(Type messageType, Action /// /// - /// + /// /// - public MessageBusBuilder Handle(Action> handlerBuilder) + public MessageBusBuilder Handle(Action> builder) { - if (handlerBuilder == null) throw new ArgumentNullException(nameof(handlerBuilder)); - - handlerBuilder(new HandlerBuilder(Settings)); + if (builder == null) throw new ArgumentNullException(nameof(builder)); + + var b = new HandlerBuilder(Settings); + builder(b); + + if (b.ConsumerSettings.ConsumerType is null) + { + // Apply default handler type of not set + b.WithHandler>(); + } + return this; } @@ -121,15 +136,15 @@ public MessageBusBuilder Handle(Action /// /// - /// + /// /// - public MessageBusBuilder Handle(Type requestType, Type responseType, Action> handlerBuilder) + public MessageBusBuilder Handle(Type requestType, Type responseType, Action> builder) { if (requestType == null) throw new ArgumentNullException(nameof(requestType)); if (responseType == null) throw new ArgumentNullException(nameof(responseType)); - if (handlerBuilder == null) throw new ArgumentNullException(nameof(handlerBuilder)); + if (builder == null) throw new ArgumentNullException(nameof(builder)); - handlerBuilder(new HandlerBuilder(Settings, requestType, responseType)); + builder(new HandlerBuilder(Settings, requestType, responseType)); return this; } diff --git a/src/SlimMessageBus.Host/DependencyResolver/ServiceCollectionExtensions.cs b/src/SlimMessageBus.Host/DependencyResolver/ServiceCollectionExtensions.cs index c67b158f..bdb74747 100644 --- a/src/SlimMessageBus.Host/DependencyResolver/ServiceCollectionExtensions.cs +++ b/src/SlimMessageBus.Host/DependencyResolver/ServiceCollectionExtensions.cs @@ -98,7 +98,7 @@ public static IServiceCollection AddSlimMessageBus( /// /// Scans the specified assemblies (using reflection) for types that implement either or . - /// The found types are registered in the DI as Transient service. + /// The found types are registered in the DI as Transient service (both the consumer type and its interface are registered). /// /// /// Filtering predicate that allows to further narrow down the @@ -107,9 +107,16 @@ public static IServiceCollection AddSlimMessageBus( public static IServiceCollection AddMessageBusConsumersFromAssembly(this IServiceCollection services, Func filterPredicate, params Assembly[] assemblies) { var foundTypes = ReflectionDiscoveryScanner.From(assemblies).GetConsumerTypes(filterPredicate); - foreach (var foundType in foundTypes) + foreach (var (foundType, interfaceTypes) in foundTypes.GroupBy(x => x.ConsumerType, x => x.InterfaceType).ToDictionary(x => x.Key, x => x)) { - services.AddTransient(foundType.ConsumerType); + // Register the consumer/handler type + services.TryAddTransient(foundType); + + foreach (var interfaceType in interfaceTypes) + { + // Register the interface of the consumer / handler + services.TryAddTransient(interfaceType, foundType); + } } return services; diff --git a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/OutboxTests.cs b/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/OutboxTests.cs index 27677f66..34db1c07 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/OutboxTests.cs +++ b/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/OutboxTests.cs @@ -1,6 +1,5 @@ namespace SlimMessageBus.Host.Outbox.DbContext.Test; -using System.Collections.Concurrent; using System.Reflection; using Confluent.Kafka; diff --git a/src/Tests/SlimMessageBus.Host.Test/Config/ConsumerBuilderTest.cs b/src/Tests/SlimMessageBus.Host.Test/Config/ConsumerBuilderTest.cs index 19cd3519..8a53b1a8 100644 --- a/src/Tests/SlimMessageBus.Host.Test/Config/ConsumerBuilderTest.cs +++ b/src/Tests/SlimMessageBus.Host.Test/Config/ConsumerBuilderTest.cs @@ -12,7 +12,7 @@ public ConsumerBuilderTest() } [Fact] - public void Given_MessageType_When_Configured_Then_MessageType_ProperlySet() + public void Given_MessageType_When_Configured_Then_MessageType_ProperlySet_And_ConsumerTypeNull() { // arrange @@ -20,6 +20,8 @@ public void Given_MessageType_When_Configured_Then_MessageType_ProperlySet() var subject = new ConsumerBuilder(messageBusSettings); // assert + subject.ConsumerSettings.ConsumerMode.Should().Be(ConsumerMode.Consumer); + subject.ConsumerSettings.ConsumerType.Should().BeNull(); subject.ConsumerSettings.MessageType.Should().Be(typeof(SomeMessage)); } diff --git a/src/Tests/SlimMessageBus.Host.Test/Config/HandlerBuilderTest.cs b/src/Tests/SlimMessageBus.Host.Test/Config/HandlerBuilderTest.cs index f0e29bb5..2f2e7b44 100644 --- a/src/Tests/SlimMessageBus.Host.Test/Config/HandlerBuilderTest.cs +++ b/src/Tests/SlimMessageBus.Host.Test/Config/HandlerBuilderTest.cs @@ -4,22 +4,57 @@ public class HandlerBuilderTest { + private readonly MessageBusSettings messageBusSettings; + + public HandlerBuilderTest() + { + messageBusSettings = new MessageBusSettings(); + } + + [Fact] + public void Given_RequestAndResposeType_When_Configured_Then_MessageType_And_ResponseType_And_DefaultHandlerTypeSet_ProperlySet() + { + // arrange + + // act + var subject = new HandlerBuilder(messageBusSettings); + + // assert + subject.ConsumerSettings.ConsumerMode.Should().Be(ConsumerMode.RequestResponse); + subject.ConsumerSettings.ConsumerType.Should().BeNull(); + subject.ConsumerSettings.MessageType.Should().Be(typeof(SomeRequest)); + subject.ConsumerSettings.ResponseType.Should().Be(typeof(SomeResponse)); + } + + [Fact] + public void Given_Path_Set_When_Configured_Then_Path_ProperlySet() + { + // arrange + var path = "topic"; + + // act + var subject = new HandlerBuilder(messageBusSettings) + .Path(path); + + // assert + subject.ConsumerSettings.Path.Should().Be(path); + subject.ConsumerSettings.PathKind.Should().Be(PathKind.Topic); + } + [Fact] public void BuildsProperSettings() { // arrange var path = "topic"; - var settings = new MessageBusSettings(); // act - var subject = new HandlerBuilder(settings) + var subject = new HandlerBuilder(messageBusSettings) .Topic(path) .Instances(3) .WithHandler(); // assert subject.ConsumerSettings.MessageType.Should().Be(typeof(SomeRequest)); - subject.MessageType.Should().Be(typeof(SomeRequest)); subject.ConsumerSettings.Path.Should().Be(path); subject.ConsumerSettings.Instances.Should().Be(3); subject.ConsumerSettings.ConsumerType.Should().Be(typeof(SomeRequestMessageHandler)); @@ -34,6 +69,5 @@ public void BuildsProperSettings() consumerInvokerSettings.ConsumerType.Should().Be(typeof(SomeRequestMessageHandler)); Func call = () => consumerInvokerSettings.ConsumerMethod(new SomeRequestMessageHandler(), new SomeRequest()); call.Should().ThrowAsync().WithMessage(nameof(SomeRequest)); - } } \ No newline at end of file diff --git a/src/Tests/SlimMessageBus.Host.Test/Config/MessageBusBuilderTests.cs b/src/Tests/SlimMessageBus.Host.Test/Config/MessageBusBuilderTests.cs index 704e7038..722f3349 100644 --- a/src/Tests/SlimMessageBus.Host.Test/Config/MessageBusBuilderTests.cs +++ b/src/Tests/SlimMessageBus.Host.Test/Config/MessageBusBuilderTests.cs @@ -11,20 +11,51 @@ public DerivedMessageBusBuilder(MessageBusBuilder other) : base(other) } } + [Fact] + public void When_Consume_Given_NoDeclaredConsumerType_Then_DefaultConsumerTypeSet() + { + // arrange + var subject = MessageBusBuilder.Create(); + + // act + subject.Consume(x => { }); + + // assert + subject.Settings.Consumers.Count.Should().Be(1); + subject.Settings.Consumers[0].MessageType.Should().Be(typeof(SomeMessage)); + subject.Settings.Consumers[0].ConsumerType.Should().Be(typeof(IConsumer)); + } + + [Fact] + public void When_Handle_Given_NoDeclaredHandlerType_Then_DefaultHandlerTypeSet() + { + // arrange + var subject = MessageBusBuilder.Create(); + + // act + subject.Handle(x => { }); + + // assert + subject.Settings.Consumers.Count.Should().Be(1); + subject.Settings.Consumers[0].MessageType.Should().Be(typeof(SomeRequest)); + subject.Settings.Consumers[0].ResponseType.Should().Be(typeof(SomeResponse)); + subject.Settings.Consumers[0].ConsumerType.Should().Be(typeof(IRequestHandler)); + } + [Fact] public void Given_OtherBuilder_When_CopyConstructorUsed_Then_AllStateIsCopied() { // arrange - var prototype = MessageBusBuilder.Create(); + var subject = MessageBusBuilder.Create(); // act - var copy = new DerivedMessageBusBuilder(prototype); + var copy = new DerivedMessageBusBuilder(subject); // assert - copy.Settings.Should().BeSameAs(prototype.Settings); - copy.Settings.Name.Should().BeSameAs(prototype.Settings.Name); - copy.Configurators.Should().BeSameAs(prototype.Configurators); - copy.ChildBuilders.Should().BeSameAs(prototype.ChildBuilders); - copy.BusFactory.Should().BeSameAs(prototype.BusFactory); + copy.Settings.Should().BeSameAs(subject.Settings); + copy.Settings.Name.Should().BeSameAs(subject.Settings.Name); + copy.Configurators.Should().BeSameAs(subject.Configurators); + copy.ChildBuilders.Should().BeSameAs(subject.ChildBuilders); + copy.BusFactory.Should().BeSameAs(subject.BusFactory); } } \ No newline at end of file