diff --git a/Rebus.ServiceProvider.Tests/Examples/KeyedServices.cs b/Rebus.ServiceProvider.Tests/Examples/KeyedServices.cs new file mode 100644 index 00000000..5f9c0656 --- /dev/null +++ b/Rebus.ServiceProvider.Tests/Examples/KeyedServices.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Rebus.Config; +using Rebus.Handlers; +using Rebus.Tests.Contracts; +using Rebus.Transport.InMem; +#pragma warning disable CS1998 + +namespace Rebus.ServiceProvider.Tests.Examples; + +#if NET8_0_OR_GREATER +[TestFixture] +[Description("Demonstrates how a service key can be used to resolve Rebus handlers")] +public class KeyedServices : FixtureBase +{ + [Test] + public async Task ItWorks() + { + var services = new ServiceCollection(); + + var bus1Key = "bus1"; + var bus2Key = "bus2"; + + var log = new List(); + + services.AddRebusHandler(bus1Key, (serviceProvider, serviceKey) => new((string)serviceKey, log)); + services.AddRebusHandler(bus2Key, (serviceProvider, serviceKey) => new((string)serviceKey, log)); + + services.AddRebus( + configure => configure.Transport(t => t.UseInMemoryTransport(new InMemNetwork(), $"queue-{Guid.NewGuid():N}")), + isDefaultBus: true, + key: bus1Key, + serviceKey: bus1Key); + + services.AddRebus( + configure => configure.Transport(t => t.UseInMemoryTransport(new InMemNetwork(), $"queue-{Guid.NewGuid():N}")), + isDefaultBus: false, + key: bus2Key, + serviceKey: bus2Key); + + await using var provider = services.BuildServiceProvider(); + provider.StartRebus(); + + var bus1 = provider.GetRequiredService().GetBus(bus1Key); + var bus2 = provider.GetRequiredService().GetBus(bus2Key); + + await bus1.SendLocal("Hej!"); + await bus2.SendLocal("Hej!"); + + await Task.Delay(TimeSpan.FromSeconds(3)); + + Assert.That(log, Is.EquivalentTo(new[] { bus1Key, bus2Key })); + } + + class Handler1(string serviceKey, List log) : IHandleMessages + { + public async Task Handle(string _) => log.Add(serviceKey); + } + + class Handler2(string serviceKey, List log) : IHandleMessages + { + public async Task Handle(string _) => log.Add(serviceKey); + } +} +#endif \ No newline at end of file diff --git a/Rebus.ServiceProvider.Tests/Rebus.ServiceProvider.Tests.csproj b/Rebus.ServiceProvider.Tests/Rebus.ServiceProvider.Tests.csproj index 97291815..cd1cf4be 100644 --- a/Rebus.ServiceProvider.Tests/Rebus.ServiceProvider.Tests.csproj +++ b/Rebus.ServiceProvider.Tests/Rebus.ServiceProvider.Tests.csproj @@ -1,7 +1,7 @@  net48;net8.0 - 11 + latest diff --git a/Rebus.ServiceProvider/Config/ServiceCollectionExtensions.cs b/Rebus.ServiceProvider/Config/ServiceCollectionExtensions.cs index 7ea6c839..ff9a3fd6 100644 --- a/Rebus.ServiceProvider/Config/ServiceCollectionExtensions.cs +++ b/Rebus.ServiceProvider/Config/ServiceCollectionExtensions.cs @@ -50,7 +50,11 @@ public static class ServiceCollectionExtensions /// public static IServiceCollection AddRebus(this IServiceCollection services, Func configure, bool isDefaultBus = true, Func onCreated = null, - string key = null, bool startAutomatically = true) + string key = null, bool startAutomatically = true +#if NET8_0_OR_GREATER + , object serviceKey = null +#endif + ) { if (services == null) throw new ArgumentNullException(nameof(services)); if (configure == null) throw new ArgumentNullException(nameof(configure)); @@ -62,6 +66,9 @@ public static IServiceCollection AddRebus(this IServiceCollection services, onCreated: (bus, _) => onCreated?.Invoke(bus) ?? Task.CompletedTask, key: key, startAutomatically: startAutomatically +#if NET8_0_OR_GREATER + , serviceKey: serviceKey +#endif ); } @@ -93,12 +100,26 @@ public static IServiceCollection AddRebus(this IServiceCollection services, /// Configures whether this bus should be started automatically (i.e. whether message consumption should begin) when the host starts up (or when StartRebus() is called on the service provider). /// Setting this to false should be combined with providing a , because the bus can then be started by resolving and calling on it. /// - public static IServiceCollection AddRebus(this IServiceCollection services, Func configure, Func onCreated, bool isDefaultBus = true, string key = null, bool startAutomatically = true) + public static IServiceCollection AddRebus(this IServiceCollection services, Func configure, Func onCreated, bool isDefaultBus = true, string key = null, bool startAutomatically = true +#if NET8_0_OR_GREATER + , object serviceKey = null +#endif + ) { if (services == null) throw new ArgumentNullException(nameof(services)); if (configure == null) throw new ArgumentNullException(nameof(configure)); - return AddRebus(services, (configurer, _) => configure(configurer), isDefaultBus: isDefaultBus, onCreated: onCreated, key: key, startAutomatically: startAutomatically); + return AddRebus( + services, + (configurer, _) => configure(configurer), + isDefaultBus: isDefaultBus, + onCreated: onCreated, + key: key, + startAutomatically: startAutomatically +#if NET8_0_OR_GREATER + , serviceKey: serviceKey +#endif + ); } /// @@ -130,7 +151,11 @@ public static IServiceCollection AddRebus(this IServiceCollection services, Func /// public static IServiceCollection AddRebus(this IServiceCollection services, Func configure, bool isDefaultBus = true, - Func onCreated = null, string key = null, bool startAutomatically = true) + Func onCreated = null, string key = null, bool startAutomatically = true +#if NET8_0_OR_GREATER + , object serviceKey = null +#endif + ) { if (services == null) throw new ArgumentNullException(nameof(services)); if (configure == null) throw new ArgumentNullException(nameof(configure)); @@ -142,6 +167,9 @@ public static IServiceCollection AddRebus(this IServiceCollection services, onCreated: (bus, _) => onCreated?.Invoke(bus) ?? Task.CompletedTask, key: key, startAutomatically: startAutomatically +#if NET8_0_OR_GREATER + , serviceKey: serviceKey +#endif ); } @@ -173,8 +201,12 @@ public static IServiceCollection AddRebus(this IServiceCollection services, /// Setting this to false should be combined with providing a , because the bus can then be started by resolving and calling on it. /// public static IServiceCollection AddRebus(this IServiceCollection services, - Func configure, Func onCreated, - bool isDefaultBus = true, string key = null, bool startAutomatically = true) + Func configure, Func onCreated, + bool isDefaultBus = true, string key = null, bool startAutomatically = true +#if NET8_0_OR_GREATER + , object serviceKey = null +#endif + ) { if (services == null) throw new ArgumentNullException(nameof(services)); if (configure == null) throw new ArgumentNullException(nameof(configure)); @@ -208,6 +240,9 @@ public static IServiceCollection AddRebus(this IServiceCollection services, serviceProvider: p, isDefaultBus: true, lifetime: p.GetService() +#if NET8_0_OR_GREATER + , serviceKey: serviceKey +#endif )); services.AddSingleton(p => @@ -236,6 +271,9 @@ public static IServiceCollection AddRebus(this IServiceCollection services, serviceProvider: p, isDefaultBus: false, lifetime: p.GetService() +#if NET8_0_OR_GREATER + , serviceKey: serviceKey +#endif ); return new RebusBackgroundService(rebusInitializer); @@ -270,6 +308,16 @@ public static IServiceCollection AddRebusHandler(this IServiceCollecti return AddRebusHandler(services, typeof(THandler)); } +#if NET8_0_OR_GREATER + /// + /// Registers the given with a transient lifestyle using given + /// + public static IServiceCollection AddRebusHandler(this IServiceCollection services, object serviceKey) where THandler : IHandleMessages + { + return AddRebusHandler(services, typeof(THandler), serviceKey); + } +#endif + /// /// Register the given with a transient lifestyle /// @@ -283,18 +331,51 @@ public static IServiceCollection AddRebusHandler(this IServiceCollection service return services; } +#if NET8_0_OR_GREATER + /// + /// Register the given with a transient lifestyle using given + /// + public static IServiceCollection AddRebusHandler(this IServiceCollection services, Type typeToRegister, object serviceKey) + { + if (services == null) throw new ArgumentNullException(nameof(services)); + if (typeToRegister == null) throw new ArgumentNullException(nameof(typeToRegister)); + if (serviceKey == null) throw new ArgumentNullException(nameof(serviceKey)); + + RegisterType(services, typeToRegister, serviceKey); + + return services; + } +#endif + /// /// Registers the given with a transient lifestyle /// public static IServiceCollection AddRebusHandler(this IServiceCollection services, Func factory) where THandler : IHandleMessages { if (services == null) throw new ArgumentNullException(nameof(services)); + if (factory == null) throw new ArgumentNullException(nameof(factory)); RegisterFactory(services, typeof(THandler), provider => factory(provider)); return services; } +#if NET8_0_OR_GREATER + /// + /// Registers the given with a transient lifestyle using given + /// + public static IServiceCollection AddRebusHandler(this IServiceCollection services, object serviceKey, Func factory) where THandler : IHandleMessages + { + if (services == null) throw new ArgumentNullException(nameof(services)); + if (serviceKey == null) throw new ArgumentNullException(nameof(serviceKey)); + if (factory == null) throw new ArgumentNullException(nameof(factory)); + + RegisterFactory(services, typeof(THandler), serviceKey, (provider, serviceKey) => factory(provider, serviceKey)); + + return services; + } +#endif + /// /// Automatically picks up all handler types from the assembly containing and registers them in the container /// @@ -379,7 +460,11 @@ static IEnumerable GetImplementedHandlerInterfaces(Type type) => type.GetInterfaces() .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IHandleMessages<>)); +#if NET8_0_OR_GREATER + static void RegisterAssembly(IServiceCollection services, Assembly assemblyToRegister, string namespaceFilter = null, Func predicate = null, object serviceKey = null) +#else static void RegisterAssembly(IServiceCollection services, Assembly assemblyToRegister, string namespaceFilter = null, Func predicate = null) +#endif { var typesToAutoRegister = assemblyToRegister.GetTypes() .Where(IsClass) @@ -403,7 +488,14 @@ static void RegisterAssembly(IServiceCollection services, Assembly assemblyToReg foreach (var type in typesToAutoRegister) { +#if NET8_0_OR_GREATER + if (serviceKey != null) + RegisterType(services, type.Type, serviceKey); + else + RegisterType(services, type.Type); +#else RegisterType(services, type.Type); +#endif } } @@ -419,6 +511,18 @@ static void RegisterFactory(IServiceCollection services, Type typeToRegister, Fu } } +#if NET8_0_OR_GREATER + static void RegisterFactory(IServiceCollection services, Type typeToRegister, object serviceKey, Func factory) + { + var implementedHandlerInterfaces = GetImplementedHandlerInterfaces(typeToRegister).ToArray(); + + foreach (var handlerInterface in implementedHandlerInterfaces) + { + services.AddKeyedTransient(handlerInterface, serviceKey, factory); + } + } +#endif + static void RegisterType(IServiceCollection services, Type typeToRegister) { var implementedHandlerInterfaces = GetImplementedHandlerInterfaces(typeToRegister).ToArray(); @@ -428,4 +532,16 @@ static void RegisterType(IServiceCollection services, Type typeToRegister) services.AddTransient(handlerInterface, typeToRegister); } } + +#if NET8_0_OR_GREATER + static void RegisterType(IServiceCollection services, Type typeToRegister, object serviceKey) + { + var implementedHandlerInterfaces = GetImplementedHandlerInterfaces(typeToRegister).ToArray(); + + foreach (var handlerInterface in implementedHandlerInterfaces) + { + services.AddKeyedTransient(handlerInterface, serviceKey, typeToRegister); + } + } +#endif } \ No newline at end of file diff --git a/Rebus.ServiceProvider/ServiceProvider/DependencyInjectionHandlerActivator.cs b/Rebus.ServiceProvider/ServiceProvider/DependencyInjectionHandlerActivator.cs index 6ce049c0..2dc16128 100644 --- a/Rebus.ServiceProvider/ServiceProvider/DependencyInjectionHandlerActivator.cs +++ b/Rebus.ServiceProvider/ServiceProvider/DependencyInjectionHandlerActivator.cs @@ -25,12 +25,17 @@ public class DependencyInjectionHandlerActivator : IHandlerActivator { readonly ConcurrentDictionary _typesToResolveByMessage = new(); readonly IServiceProvider _provider; + readonly object _serviceKey; /// /// Initializes a new instance of the class. /// /// The service provider used to yield handler instances. - public DependencyInjectionHandlerActivator(IServiceProvider provider) => _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + public DependencyInjectionHandlerActivator(IServiceProvider provider, object serviceKey = null) + { + _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + _serviceKey = serviceKey; + } /// /// Resolves all handlers for the given message type @@ -81,7 +86,11 @@ IReadOnlyList> GetMessageHandlersForMessage( var typesToResolve = _typesToResolveByMessage.GetOrAdd(typeof(TMessage), FigureOutTypesToResolve); return typesToResolve +#if NET8_0_OR_GREATER + .SelectMany(type => (_serviceKey == null ? serviceProvider.GetServices(type) : serviceProvider.GetKeyedServices(type, _serviceKey)).Cast()) +#else .SelectMany(type => serviceProvider.GetServices(type).Cast()) +#endif .Distinct(new TypeEqualityComparer()) .Cast>() .ToList(); diff --git a/Rebus.ServiceProvider/ServiceProvider/Internals/RebusInitializer.cs b/Rebus.ServiceProvider/ServiceProvider/Internals/RebusInitializer.cs index bef3899c..42b3c04c 100644 --- a/Rebus.ServiceProvider/ServiceProvider/Internals/RebusInitializer.cs +++ b/Rebus.ServiceProvider/ServiceProvider/Internals/RebusInitializer.cs @@ -22,6 +22,7 @@ class RebusInitializer readonly IServiceProvider _serviceProvider; readonly bool _isDefaultBus; readonly CancellationToken? _cancellationToken; + readonly object _serviceKey; public RebusInitializer( bool startAutomatically, @@ -30,7 +31,11 @@ public RebusInitializer( Func onCreated, IServiceProvider serviceProvider, bool isDefaultBus, - IHostApplicationLifetime lifetime) + IHostApplicationLifetime lifetime +#if NET8_0_OR_GREATER + , object serviceKey = null +#endif + ) { _startAutomatically = startAutomatically; _key = key; @@ -39,6 +44,9 @@ public RebusInitializer( _serviceProvider = serviceProvider; _isDefaultBus = isDefaultBus; _cancellationToken = lifetime?.ApplicationStopping; +#if NET8_0_OR_GREATER + _serviceKey = serviceKey; +#endif _busAndEvents = GetLazyInitializer(); } @@ -54,7 +62,7 @@ public RebusInitializer( BusLifetimeEvents busLifetimeEventsHack = null; var rebusConfigurer = Configure - .With(new DependencyInjectionHandlerActivator(_serviceProvider)) + .With(new DependencyInjectionHandlerActivator(_serviceProvider, _serviceKey)) .Options(o => o.Decorate(c => { // snatch events here