diff --git a/docs/intro.md b/docs/intro.md index 8a9f0d01..84fd9241 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -23,6 +23,7 @@ - [ASP.Net Core](#aspnet-core) - [Modularization of configuration](#modularization-of-configuration) - [Auto registration of consumers and interceptors](#auto-registration-of-consumers-and-interceptors) + - [Message Scope Accessor](#message-scope-accessor) - [Serialization](#serialization) - [Multiple message types on one topic (or queue)](#multiple-message-types-on-one-topic-or-queue) - [Message Type Resolver](#message-type-resolver) @@ -674,6 +675,14 @@ services.AddSlimMessageBus(mbb => }); ``` +### Message Scope Accessor + +During normal consumer/handler and interceptor life cycles, we can inject any scoped dependencies (services) using the constructor. All is nicely handled by MSDI. + +However, for advanced framework integration, if there is a need to get ahold of the `IServiceProvider` tied to the scope of the currently consumed message the [`IMessageScopeAccessor`](../src/SlimMessageBus.Host/Consumer/IMessageScope.cs) can be used. +It works in a similar way how the [`IHttpContextAccessor`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor?view=aspnetcore-8.0) works in ASP.NET Core to lookup the current ongoing HTTP request and the per request scoped services. +This is useful when the other framework is not managed by MSDI and we still want to hook into the current message scope. + ## Serialization SMB uses serialization plugins to serialize (and deserialize) the messages into the desired format. diff --git a/docs/intro.t.md b/docs/intro.t.md index 4c718cb0..ff4b09c7 100644 --- a/docs/intro.t.md +++ b/docs/intro.t.md @@ -23,6 +23,7 @@ - [ASP.Net Core](#aspnet-core) - [Modularization of configuration](#modularization-of-configuration) - [Auto registration of consumers and interceptors](#auto-registration-of-consumers-and-interceptors) + - [Message Scope Accessor](#message-scope-accessor) - [Serialization](#serialization) - [Multiple message types on one topic (or queue)](#multiple-message-types-on-one-topic-or-queue) - [Message Type Resolver](#message-type-resolver) @@ -674,6 +675,14 @@ services.AddSlimMessageBus(mbb => }); ``` +### Message Scope Accessor + +During normal consumer/handler and interceptor life cycles, we can inject any scoped dependencies (services) using the constructor. All is nicely handled by MSDI. + +However, for advanced framework integration, if there is a need to get ahold of the `IServiceProvider` tied to the scope of the currently consumed message the [`IMessageScopeAccessor`](../src/SlimMessageBus.Host/Consumer/IMessageScope.cs) can be used. +It works in a similar way how the [`IHttpContextAccessor`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor?view=aspnetcore-8.0) works in ASP.NET Core to lookup the current ongoing HTTP request and the per request scoped services. +This is useful when the other framework is not managed by MSDI and we still want to hook into the current message scope. + ## Serialization SMB uses serialization plugins to serialize (and deserialize) the messages into the desired format. diff --git a/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj b/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj index a1b90c4e..239663e6 100644 --- a/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj +++ b/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj @@ -6,7 +6,7 @@ Core configuration interfaces of SlimMessageBus SlimMessageBus SlimMessageBus.Host - 2.4.0 + 2.5.0-rc1 diff --git a/src/SlimMessageBus.Host/Consumer/IMessageScopeAccessor.cs b/src/SlimMessageBus.Host/Consumer/IMessageScopeAccessor.cs new file mode 100644 index 00000000..025cd785 --- /dev/null +++ b/src/SlimMessageBus.Host/Consumer/IMessageScopeAccessor.cs @@ -0,0 +1,13 @@ +namespace SlimMessageBus.Host.Consumer; + +/// +/// Allows to get ahold of the for the current message scope. +/// +public interface IMessageScopeAccessor +{ + /// + /// If the running code is within a message scope of a consumer, this property will return the for the current message scope. + /// Otherwise it will return null. + /// + IServiceProvider Current { get; } +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host/Consumer/MessageScopeAccessor.cs b/src/SlimMessageBus.Host/Consumer/MessageScopeAccessor.cs new file mode 100644 index 00000000..74890f55 --- /dev/null +++ b/src/SlimMessageBus.Host/Consumer/MessageScopeAccessor.cs @@ -0,0 +1,6 @@ +namespace SlimMessageBus.Host.Consumer; + +internal sealed class MessageScopeAccessor : IMessageScopeAccessor +{ + public IServiceProvider Current => MessageScope.Current; +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host/Consumer/MessageScopeWrapper.cs b/src/SlimMessageBus.Host/Consumer/MessageScopeWrapper.cs index 9fb82834..3ba9da75 100644 --- a/src/SlimMessageBus.Host/Consumer/MessageScopeWrapper.cs +++ b/src/SlimMessageBus.Host/Consumer/MessageScopeWrapper.cs @@ -1,5 +1,9 @@ namespace SlimMessageBus.Host.Consumer; +/// +/// Used by consumers to wrap the message processing in a message scope (MSDI). +/// The is being adjusted as part of this wrapper. +/// public sealed class MessageScopeWrapper : IMessageScope { private readonly ILogger _logger; diff --git a/src/SlimMessageBus.Host/DependencyResolver/ServiceCollectionExtensions.cs b/src/SlimMessageBus.Host/DependencyResolver/ServiceCollectionExtensions.cs index 5db30fe7..ac3edf82 100644 --- a/src/SlimMessageBus.Host/DependencyResolver/ServiceCollectionExtensions.cs +++ b/src/SlimMessageBus.Host/DependencyResolver/ServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ using System.Reflection; +using SlimMessageBus.Host.Consumer; using SlimMessageBus.Host.Hybrid; public static class ServiceCollectionExtensions @@ -95,6 +96,8 @@ public static IServiceCollection AddSlimMessageBus(this IServiceCollection servi services.TryAddSingleton(); services.TryAddEnumerable(ServiceDescriptor.Singleton()); + services.TryAddSingleton(); + services.AddHostedService(); return services; diff --git a/src/Tests/SlimMessageBus.Host.Integration.Test/MessageBusCurrent/MessageBusCurrentTests.cs b/src/Tests/SlimMessageBus.Host.Integration.Test/MessageBusCurrent/MessageBusCurrentTests.cs index aaf2d874..c04f67e4 100644 --- a/src/Tests/SlimMessageBus.Host.Integration.Test/MessageBusCurrent/MessageBusCurrentTests.cs +++ b/src/Tests/SlimMessageBus.Host.Integration.Test/MessageBusCurrent/MessageBusCurrentTests.cs @@ -3,13 +3,13 @@ using SlimMessageBus.Host; using SlimMessageBus.Host.Memory; +/// +/// This test verifies that the MessageBus.Current accessor works correctly and looks up in the current message scope. +/// +/// [Trait("Category", "Integration")] -public class MessageBusCurrentTests : BaseIntegrationTest +public class MessageBusCurrentTests(ITestOutputHelper testOutputHelper) : BaseIntegrationTest(testOutputHelper) { - public MessageBusCurrentTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) - { - } - protected override void SetupServices(ServiceCollection services, IConfigurationRoot configuration) { services.AddSlimMessageBus(mbb => @@ -42,37 +42,36 @@ public async Task Given_MemoryConsumer_When_MessageBusCurrentCalledInsideConsume var valueHolder = scope.ServiceProvider.GetRequiredService(); valueHolder.Value.Should().Be(value); } -} - -public record SetValueCommand(Guid Value); + public record SetValueCommand(Guid Value); -public class SetValueCommandHandler : IRequestHandler -{ - public async Task OnHandle(SetValueCommand request) + public class SetValueCommandHandler : IRequestHandler { - // Some other logic here ... + public async Task OnHandle(SetValueCommand request) + { + // Some other logic here ... - // and then notify about the value change using the MessageBus.Current accessor which should look up in the current message scope - await MessageBus.Current.Publish(new ValueChangedEvent(request.Value)); + // and then notify about the value change using the MessageBus.Current accessor which should look up in the current message scope + await MessageBus.Current.Publish(new ValueChangedEvent(request.Value)); + } } -} -public record ValueChangedEvent(Guid Value); + public record ValueChangedEvent(Guid Value); -public class ValueChangedEventHandler(ValueHolder valueHolder) : IRequestHandler -{ - public Task OnHandle(ValueChangedEvent request) + public class ValueChangedEventHandler(ValueHolder valueHolder) : IRequestHandler { - valueHolder.Value = request.Value; - return Task.CompletedTask; + public Task OnHandle(ValueChangedEvent request) + { + valueHolder.Value = request.Value; + return Task.CompletedTask; + } } -} -/// -/// Holds the value (per scope lifetime). -/// -public class ValueHolder -{ - public Guid Value { get; set; } + /// + /// Holds the value (per scope lifetime). + /// + public class ValueHolder + { + public Guid Value { get; set; } + } } diff --git a/src/Tests/SlimMessageBus.Host.Integration.Test/MessageScopeAccessor/MessageScopeAccessorTests.cs b/src/Tests/SlimMessageBus.Host.Integration.Test/MessageScopeAccessor/MessageScopeAccessorTests.cs new file mode 100644 index 00000000..6db21509 --- /dev/null +++ b/src/Tests/SlimMessageBus.Host.Integration.Test/MessageScopeAccessor/MessageScopeAccessorTests.cs @@ -0,0 +1,66 @@ +namespace SlimMessageBus.Host.Integration.Test.MessageScopeAccessor; + +using SlimMessageBus.Host; +using SlimMessageBus.Host.Consumer; +using SlimMessageBus.Host.Memory; + +/// +/// This test verifies that the correctly looks up the for the current message scope. +/// +/// +[Trait("Category", "Integration")] +public class MessageScopeAccessorTests(ITestOutputHelper testOutputHelper) + : BaseIntegrationTest(testOutputHelper) +{ + protected override void SetupServices(ServiceCollection services, IConfigurationRoot configuration) + { + services.AddSlimMessageBus(mbb => + { + mbb.AddChildBus("Memory", builder => + { + builder + .WithProviderMemory() + .AutoDeclareFrom(Assembly.GetExecutingAssembly(), t => t.Namespace.Contains("MessageScopeAccessor")) + .PerMessageScopeEnabled(); + }); + mbb.AddServicesFromAssemblyContaining(); + }); + services.AddScoped(); + } + + [Fact] + public async Task Given_MemoryConsumer_When_MessageScopeAccessorCalledInsideConsumer_Then_LooksUpInTheMessageScope() + { + // Arrange + using var scope = ServiceProvider.CreateScope(); + var bus = scope.ServiceProvider.GetRequiredService(); + + var value = Guid.NewGuid(); + + // Act + await bus.Publish(new TestMessage(value)); + + // Assert + var holder = scope.ServiceProvider.GetRequiredService(); + holder.ServiceProvider.Should().BeSameAs(holder.MessageScopeAccessorServiceProvider); + } + + public record TestMessage(Guid Value); + + public class TestMessageConsumer(TestValueHolder holder, IServiceProvider serviceProvider, IMessageScopeAccessor messageScopeAccessor) : IRequestHandler + { + public Task OnHandle(TestMessage request) + { + holder.ServiceProvider = serviceProvider; + holder.MessageScopeAccessorServiceProvider = messageScopeAccessor.Current; + return Task.CompletedTask; + } + } + + public class TestValueHolder + { + public IServiceProvider ServiceProvider { get; set; } + public IServiceProvider MessageScopeAccessorServiceProvider { get; set; } + } + +}