simpleinjector / SimpleInjector

An easy, flexible, and fast Dependency Injection library that promotes best practice to steer developers towards the pit of success.
https://simpleinjector.org
MIT License
1.21k stars 155 forks source link

How to get all registered types for an open generic type? #971

Closed Ergamon closed 8 months ago

Ergamon commented 1 year ago

When I register all my instances of an open generic type like this:

container.Collection.Register(
    typeof(IMessageHandler<>), Assembly.GetExecutingAssembly());

how do I get all possible T values?

Why do I want this: I have an application listening to RabbitMQ messages. The library I am using has a method for this with a signature:

ReceiveAsync<T>(
    string queue,  Func<T, CancellationToken, Task> onMessage,
    CancellationToken cancellationToken);

So my idea was to have an interface like this:

public interface IMessageHandler<in TMessage>
{
  public Task Handle(TMessage message, CancellationToken cancellationToken);
}

Actual implementations look like this:

[Queue("aqueue")]
public sealed class MessageHandler : IMessageHandler<Message>
{
  Task IMessageHandler<Message>.Handle(
    Message message, CancellationToken cancellationToken)
  {
    Console.WriteLine(message.ToString());

    return Task.CompletedTask;
  }
}

At startup I have a hosted service getting the necessary dependencies:

public sealed class QueueService : IHostedService
{
  private readonly Container _container;
  private readonly IBus _bus;

  public QueueService(Container container, IBus bus)
  {
    _container = container;
    _bus = bus;
  }

  async Task IHostedService.StartAsync(CancellationToken cancellationToken)
  {
    var method = GetType().GetMethod(nameof(QueueDeclare), BindingFlags.NonPublic | BindingFlags.Instance);

    foreach (var type in GetRegisteredTypes())
    {
      var handlerType = typeof(IMessageHandler<>).MakeGenericType(type);
      var metadataType = typeof(DependencyMetadata<>).MakeGenericType(handlerType);
      var metadata = _container.GetAllInstances(metadataType);
      var typeMethod = method!.MakeGenericMethod(type);

      foreach (var data in metadata)
      {
        var task = (Task)typeMethod.Invoke(this, new object[] { data, cancellationToken })!;
        await task;
      }
    }
  }

  Task IHostedService.StopAsync(CancellationToken cancellationToken)
  {
    return Task.CompletedTask;
  }

  private IEnumerable<Type> GetRegisteredTypes()
  {
    // How to get these from the container?
    yield return typeof(Message);
  }

  private async Task QueueDeclare<T>(DependencyMetadata<IMessageHandler<T>> metadata, CancellationToken cancellationToken)
  {
    var attribute = metadata.ImplementationType.GetAttribute<QueueAttribute>();

    await _bus.SendReceive.ReceiveAsync<T>(
      attribute.Name,
      (message, token) =>
      {
        using (AsyncScopedLifestyle.BeginScope(_container))
        {
          var handler = metadata.GetInstance();
          return handler.Handle(message, token);
        }
      },
      cancellationToken);
  }
}

I know it is a lot of ugly reflection code and the service has a dependency to the container. But I think the design is ok, cause I consider this hosted service more or less as part of my composition root.

So basically my question is, how to implement this GetRegisteredTypes method or is there a better design to achieve what I want?

dotnetjunkie commented 1 year ago

You can can call Container.GetTypesToRegister to find the types that Collection.Register would otherwise register:

// Get all handler types
var handlers =
    container.GetTypesToRegister(
        typeof(IMessageHandler<>), Assembly.GetExecutingAssembly());

// Register them
container.Collection.Register(typeof(IMessageHandler<>), handlers);

// Extract the messages from them
var messages = (
    from implementation in handlers
    let service = implementation.GetClosedTypeOf(typeof(IMessageHandler<>))
    let messageType = service.GetGenericArguments().First()
    select messageType)
    .Distinct()
    .ToArray();

This allows you to retrieve the handlers and messages in an early stage of the registration process.

The other option is to query the container after the last registration has been made:

var messages = (
    from registration in container.GetCurrentRegistrations()
    let serviceType = registration.ServiceType
    where serviceType.IsClosedTypeOf(typeof(IMessageHandler<>))
    let handlerType = serviceType.GetClosedTypeOf(typeof(IMessageHandler<>))
    let messageType = handlerType.GetGenericArguments().First()
    select messageType)
    .Distinct()
    .ToArray();

This requires that all registrations have been made and Container.Verify() has been called.