Skip to content

Commit

Permalink
Ability to provide lifetime for .AddServicesFromAssembly()
Browse files Browse the repository at this point in the history
Signed-off-by: Tomasz Maruszak <[email protected]>
  • Loading branch information
zarusz committed May 7, 2023
1 parent fec2042 commit 548d98f
Show file tree
Hide file tree
Showing 8 changed files with 80 additions and 92 deletions.
31 changes: 23 additions & 8 deletions docs/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -566,24 +566,39 @@ services.AddSlimMessageBus(mbb =>
Consider the following example:

```cs
// Given a consumer that is found:
public class SomeMessageConsumer : IConsumer<SomeMessage>
{
}

// Given a consumer that is found in the executing assembly:
public class SomeMessageConsumer : IConsumer<SomeMessage> { }

// When configuring the bus:
services.AddSlimMessageBus(mbb =>
{
// When auto-registration is used:
mbb.AddConsumersFromAssembly(Assembly.GetExecutingAssembly());
};

// Then the .AddConsumersFromAssembly() will result in an equivalent services registration (if it were done manually):
// services.TryAddTransient<SomeMessageConsumer>();
// services.TryAddTransient<IConsumer<SomeMessage>>(svp => svp.GetRequiredService<SomeMessageConsumer>());
```

This will result in an equivalent services registration:
There is also an option to override the default lifetime of the discovered types (since v2.1.5):

```cs
services.TryAddTransient<SomeMessageConsumer>();
services.TryAddTransient<IConsumer<SomeMessage>, SomeMessageConsumer>();
services.AddSlimMessageBus(mbb =>
{
// Register the found types as Scoped lifetime in MSDI
mbb.AddConsumersFromAssembly(Assembly.GetExecutingAssembly(), consumerLifetime: ServiceLifetime.Scoped);
};
```

There is also an option to provide a type filter predicate. This might be helpful to scan only for types in a specified namespace inside of the assembly:

```cs
services.AddSlimMessageBus(mbb =>
{
// Register the found types that contain DomainEventHandlers in the namespacce
mbb.AddConsumersFromAssembly(Assembly.GetExecutingAssembly(), filter: (type) => type.Namespace.Contains("DomainEventHandlers"));
};
```

### ASP.Net Core
Expand Down
2 changes: 1 addition & 1 deletion src/Host.Transport.Properties.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<Import Project="Common.Properties.xml" />

<PropertyGroup>
<Version>2.1.4</Version>
<Version>2.1.5</Version>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,11 @@ public IReadOnlyCollection<DiscoveryConsumerType> GetConsumerTypes(Func<Type, bo
typeof(IRequestHandler<>)
};

public IReadOnlyCollection<DiscoveryProspectType> GetInterceptorTypes()
public IReadOnlyCollection<DiscoveryProspectType> GetInterceptorTypes(Func<Type, bool> filterPredicate = null)
{
var foundTypes = ProspectTypes
.Where(x => x.InterfaceType.IsGenericType && GenericTypesInterceptors.Contains(x.InterfaceType.GetGenericTypeDefinition()))
.Where(x => filterPredicate == null || filterPredicate(x.Type))
.ToList();

return foundTypes;
Expand Down
7 changes: 5 additions & 2 deletions src/SlimMessageBus.Host.Memory/MemoryMessageBusBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,17 @@ internal MemoryMessageBusBuilder(MessageBusBuilder other) : base(other)
/// For every found type declares the produced and consumer/handler by applying the topic name that corresponds to the mesage name.
/// </summary>
/// <param name="assemblies"></param>
/// <param name="consumerTypeFilter">Allows to apply a filter for any found consumer/handler.</param>
/// <param name="consumerTypeFilter">Allows to apply a filter for the found consumer/handler types.</param>
/// <param name="messageTypeToTopicConverter">By default the type name is used for the topic name. This can be used to customize the topic name. For example, if have types that have same names but are in the namespaces, you might want to include the full type in the topic name.</param>
/// <returns></returns>
public MemoryMessageBusBuilder AutoDeclareFrom(IEnumerable<Assembly> assemblies, Func<Type, bool> consumerTypeFilter = null, Func<Type, string> messageTypeToTopicConverter = null)
{
messageTypeToTopicConverter ??= DefaultMessageTypeToTopicConverter;

var prospectTypes = ReflectionDiscoveryScanner.From(assemblies).Scan().GetConsumerTypes(consumerTypeFilter);
var prospectTypes = ReflectionDiscoveryScanner.From(assemblies).Scan()
.GetConsumerTypes(consumerTypeFilter)
// Take only closed generic types
.Where(x => !x.ConsumerType.IsGenericType || x.ConsumerType.IsConstructedGenericType);

var foundConsumers = prospectTypes.Where(x => x.InterfaceType.GetGenericTypeDefinition() == typeof(IConsumer<>)).ToList();
var foundHandlers = prospectTypes.Where(x => x.InterfaceType.GetGenericTypeDefinition() == typeof(IRequestHandler<,>) || x.InterfaceType.GetGenericTypeDefinition() == typeof(IRequestHandler<>)).ToList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,110 +101,73 @@ public static IServiceCollection AddSlimMessageBus(this IServiceCollection servi
}

/// <summary>
/// Scans the specified assemblies (using reflection) for types that implement any consumer/handler interface, or any interceptor interface (<see cref="IMessageBusConfigurator"/>).
/// The found types are registered in the DI as Transient service (both the consumer type and its interface are registered).
/// Scans the specified assemblies (using reflection) for types that implement consumer/handler interface, or interceptor interface).
/// The found types are registered in the MSDI (both the consumer type and its interface are registered).
/// </summary>
/// <param name="mbb"></param>
/// <param name="assembly"></param>
/// <param name="filterPredicate"></param>
/// <param name="filter">The filter to be applied for the found types - only types that evaluate the given filter predicate will be registered in MSDI.</param>
/// <param name="consumerLifetime">The consumer lifetime under which the found types should be registerd as.</param>
/// <param name="interceptorLifetime">The interceptor lifetime under which the found types should be registerd as.</param>
/// <returns></returns>
public static MessageBusBuilder AddServicesFromAssembly(this MessageBusBuilder mbb, Assembly assembly, Func<Type, bool> filterPredicate = null)
public static MessageBusBuilder AddServicesFromAssembly(
this MessageBusBuilder mbb,
Assembly assembly,
Func<Type, bool> filter = null,
ServiceLifetime consumerLifetime = ServiceLifetime.Transient,
ServiceLifetime interceptorLifetime = ServiceLifetime.Transient)
{
var scan = ReflectionDiscoveryScanner.From(assembly);
var foundConsumerTypes = scan.GetConsumerTypes(filterPredicate);
var foundConsumerTypes = scan.GetConsumerTypes(filter);
var foundInterceptorTypes = scan.GetInterceptorTypes(filter);

mbb.PostConfigurationActions.Add(services =>
{
foreach (var (foundType, interfaceTypes) in foundConsumerTypes.GroupBy(x => x.ConsumerType, x => x.InterfaceType).ToDictionary(x => x.Key, x => x))
{
// Register the consumer/handler type
services.TryAddTransient(foundType);
services.TryAdd(ServiceDescriptor.Describe(foundType, foundType, consumerLifetime));

foreach (var interfaceType in interfaceTypes)
{
if (foundType.IsGenericType && !foundType.IsConstructedGenericType)
{
// Skip open generic types
continue;
}

// Register the interface of the consumer / handler
services.TryAddTransient(interfaceType, foundType);
services.TryAdd(ServiceDescriptor.Describe(interfaceType, svp => svp.GetRequiredService(foundType), consumerLifetime));
}
}

var foundInterceptorTypes = scan.GetInterceptorTypes();
foreach (var foundType in foundInterceptorTypes)
{
services.AddTransient(foundType.InterfaceType, foundType.Type);
if (foundType.Type.IsGenericType && !foundType.Type.IsConstructedGenericType)
{
// Skip open generic types
continue;
}
services.TryAddEnumerable(ServiceDescriptor.Describe(foundType.InterfaceType, foundType.Type, interceptorLifetime));
}
});
return mbb;
}

/// <summary>
/// Scans the specified assemblies (using reflection) for types that implement any consumer/handler interface, any interceptor interface or message bus configurator interface (<see cref="IMessageBusConfigurator"/>).
/// The found types are registered in the DI as Transient service (both the consumer type and its interface are registered).
/// Scans the specified assemblies (using reflection) for types that implement consumer/handler interface, or interceptor interface).
/// The found types are registered in the MSDI (both the consumer type and its interface are registered).
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="mbb"></param>
/// <param name="filterPredicate"></param>
/// <returns></returns>
public static MessageBusBuilder AddServicesFromAssemblyContaining<T>(this MessageBusBuilder mbb, Func<Type, bool> filterPredicate = null) =>
mbb.AddServicesFromAssembly(typeof(T).Assembly, filterPredicate);

#region Obsolete

/// <summary>
/// Scans the specified assemblies (using reflection) for types that implement either <see cref="IConsumer{TMessage}"/> or <see cref="IRequestHandler{TRequest, TResponse}"/> or <see cref="IRequestHandler{TRequest}"/>.
/// The found types are registered in the DI as Transient service (both the consumer type and its interface are registered).
/// </summary>
/// <param name="services"></param>
/// <param name="filterPredicate">Filtering predicate that allows to further narrow down the </param>
/// <param name="assemblies">Assemblies to be scanned</param>
/// <param name="filter">The filter to be applied for the found types - only types that evaluate the given filter predicate will be registered in MSDI.</param>
/// <param name="consumerLifetime">The consumer lifetime under which the found types should be registerd as.</param>
/// <param name="interceptorLifetime">The interceptor lifetime under which the found types should be registerd as.</param>
/// <returns></returns>
[Obsolete("Use the new mbb.AddServicesFromAssembly() or mbb.AddServicesFromAssemblyContaining()")]
public static IServiceCollection AddMessageBusConsumersFromAssembly(this IServiceCollection services, Func<Type, bool> filterPredicate, params Assembly[] assemblies)
{
var foundTypes = ReflectionDiscoveryScanner.From(assemblies).GetConsumerTypes(filterPredicate);
foreach (var (foundType, interfaceTypes) in foundTypes.GroupBy(x => x.ConsumerType, x => x.InterfaceType).ToDictionary(x => x.Key, x => x))
{
// 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;
}

/// <summary>
/// Scans the specified assemblies (using reflection) for types that implement either <see cref="IConsumer{TMessage}"/> or <see cref="IRequestHandler{TRequest, TResponse}"/> or <see cref="IRequestHandler{TRequest}"/>.
/// The found types are registered in the DI as Transient service.
/// </summary>
/// <param name="services"></param>
/// <param name="assemblies">Assemblies to be scanned</param>
/// <returns></returns>
[Obsolete("Use the new mbb.AddServicesFromAssembly() or mbb.AddServicesFromAssemblyContaining()")]
public static IServiceCollection AddMessageBusConsumersFromAssembly(this IServiceCollection services, params Assembly[] assemblies)
=> services.AddMessageBusConsumersFromAssembly(filterPredicate: null, assemblies);

/// <summary>
/// Scans the specified assemblies (using reflection) for types that implement one of the interceptor interfaces (<see cref="IPublishInterceptor{TMessage}"/> or <see cref="IConsumerInterceptor{TMessage}"/>) and adds them to DI.
/// This types will be use during message bus configuration.
/// </summary>
/// <param name="services"></param>
/// <param name="assemblies">Assemblies to be scanned</param>
/// <returns></returns>
[Obsolete("Use the new mbb.AddServicesFromAssembly() or mbb.AddServicesFromAssemblyContaining()")]
public static IServiceCollection AddMessageBusInterceptorsFromAssembly(this IServiceCollection services, params Assembly[] assemblies)
{
var foundTypes = ReflectionDiscoveryScanner.From(assemblies).GetInterceptorTypes();
foreach (var foundType in foundTypes)
{
services.AddTransient(foundType.InterfaceType, foundType.Type);
}

return services;
}

#endregion
public static MessageBusBuilder AddServicesFromAssemblyContaining<T>(
this MessageBusBuilder mbb,
Func<Type, bool> filter = null,
ServiceLifetime consumerLifetime = ServiceLifetime.Transient,
ServiceLifetime interceptorLifetime = ServiceLifetime.Transient) =>
mbb.AddServicesFromAssembly(typeof(T).Assembly, filter, consumerLifetime: consumerLifetime, interceptorLifetime: interceptorLifetime);
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ public void Given_AssemblyWithConsumers_When_AutoDiscoverConsumers_Then_AllOfThe
// arrange

// act
var whitelistNames = new[] { "Ping", "Echo", "Some", "Generic" };
subject.AutoDeclareFrom(Assembly.GetExecutingAssembly(),
consumerTypeFilter: (consumerType) => consumerType.Name.Contains("Ping") || consumerType.Name.Contains("Echo") || consumerType.Name.Contains("Some")); // include specific consumers only
consumerTypeFilter: (consumerType) => whitelistNames.Any(name => consumerType.Name.Contains(name))); // include specific consumers only

// assert

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,11 @@ public virtual void Dispose()
public virtual Task OnHandle(SomeMessageA messageA) => Task.CompletedTask;
}

public class GenericConsumer<T> : IConsumer<T>
{
public Task OnHandle(T message) => Task.CompletedTask;
}

public class SomeMessageAConsumer2 : IConsumer<SomeMessageA>
{
public virtual Task OnHandle(SomeMessageA messageA) => Task.CompletedTask;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,22 +117,22 @@ public void When_AddSlimMessageBus_Given_AddServicesFromAssembly_Then_AllOfTheCo
{
mbb.AddChildBus("bus1", mbb =>
{
mbb.AddServicesFromAssemblyContaining<ServiceCollectionExtensionsTest>(x => x.Name == nameof(SomeMessageConsumer));
mbb.AddServicesFromAssemblyContaining<ServiceCollectionExtensionsTest>(x => x.Name == nameof(SomeMessageConsumer), consumerLifetime: ServiceLifetime.Scoped);
});
});

_services.AddSlimMessageBus(mbb =>
{
mbb.AddChildBus("bus2", mbb =>
{
mbb.AddServicesFromAssemblyContaining<ServiceCollectionExtensionsTest>(x => x.Name == nameof(SomeMessageConsumer));
mbb.AddServicesFromAssemblyContaining<ServiceCollectionExtensionsTest>(x => x.Name == nameof(SomeMessageConsumer), consumerLifetime: ServiceLifetime.Transient);
});
});

// assert
_services.Count(x => x.ServiceType == typeof(SomeMessageConsumer)).Should().Be(1);
_services.Count(x => x.ServiceType == typeof(IConsumer<SomeMessage>)).Should().Be(1);
_services.Should().Contain(x => x.ServiceType == typeof(SomeMessageConsumer) && x.ImplementationType == typeof(SomeMessageConsumer) && x.Lifetime == ServiceLifetime.Transient);
_services.Should().Contain(x => x.ServiceType == typeof(IConsumer<SomeMessage>) && x.ImplementationType == typeof(SomeMessageConsumer) && x.Lifetime == ServiceLifetime.Transient);
_services.Should().Contain(x => x.ServiceType == typeof(SomeMessageConsumer) && x.ImplementationType == typeof(SomeMessageConsumer) && x.Lifetime == ServiceLifetime.Scoped);
_services.Should().Contain(x => x.ServiceType == typeof(IConsumer<SomeMessage>) && x.ImplementationFactory != null && x.Lifetime == ServiceLifetime.Scoped);
}
}

0 comments on commit 548d98f

Please sign in to comment.