Search code examples
c#.netdependency-injectioninversion-of-controlautofac

How to configure dependency injection container with Func<T, Result>?


BusinessAction is used to represent an action that can be performed by a user. Each action is related to the specific entity, so if for example, that entity is Order, business actions could be CancelOrder, IssueRefund, etc.

public abstract class BusinessAction<T>
{
    public Guid Id { get; init; }
    public Func<T, bool> IsEnabledFor { get; init; }
}

public class CancelOrderAction : BusinessAction<Order>
{
    public CancelOrderAction ()
    {
        Id = Guid.Parse("0e07d05c-6298-4c56-87d7-d2ca339fee1e");
        IsEnabledFor = o => o.Status == OrderStatus.Active;
    }
}

Then I need to group all actions related to the specific type.

public interface IActionRegistry
{
    Task<IEnumerable<Guid>> GetEnabledActionIdsForAsync(Guid entityId);
}

public class ActionRegistry<T> : IActionRegistry
    where T : BaseEntity
{
    private readonly IEnumerable<BusinessAction<T>> _actions;
    private readonly IRepository<T> _repository;

    public ActionRegistry(IEnumerable<BusinessAction<T>> actions, IRepository<T> repository)
    {
        _actions = actions;
        _repository = repository;
    }

    public async Task<IEnumerable<Guid>> GetEnabledActionIdsForAsync(Guid entityId)
    {
        var entity = await _repository.FindByIdAsync(entityId);

        return entity == null
            ? Enumerable.Empty<Guid>()
            : _actions.Where(a => a.IsEnabledFor(entity)).Select(a => a.Id);
    }
}

Finally, there is an API endpoint that receives entity type (some enumeration that is later on mapped to real .NET type) and ID of an entity. The API endpoint is responsible to return action IDs that are enabled for the current state of the entity.

public class RequestHandler : IRequestHandler<Request, IEnumerable<Guid>>>
{
    private readonly Func<Type, IActionRegistry> _registryFactory;

    public RequestHandler(Func<Type, IActionRegistry> registryFactory)
    {
        _registryFactory = registryFactory;
    }

    public async Task<IEnumerable<Guid>> Handle(Request request, CancellationToken cancellationToken)
    {
        var type = request.EntityType.GetDotnetType();
        var actionRegistry = _registryFactory(type);
        var enabledActions = await actionRegistry.GetEnabledActionIdsForAsync(request.EntityId);

        return enabledActions;
    }
}

The question is: How can I configure the dependency injection container in ASP.NET (using default option or Autofac) so that Func<Type, IActionRegistry> can be resolved?

For parameters in ActionRegistry<T> I guess I can do:

builder.RegisterAssemblyTypes().AsClosedTypesOf(typeof(BusinessAction<>));

builder.RegisterGeneric(typeof(Repository<>))
       .As(typeof(IRepository<>))
       .InstancePerLifetimeScope();

But, how can I configure Func<Type, IActionRegistry> so that I am able to automatically connect a request for Order with ActionRegistry<Order>? Is there a way to do that or I will need to manually configure the factory by writing some switch statement based on type (and how will that look)?

Is there a better way to achieve what I need here? The end goal is that once I have runtime type, I can get a list of business actions related to that type as well as a repository (so that I can fetch entity from DB).


Solution

  • What you're trying to do is possible, but it's not a common thing and isn't something magic you'll get out of the box. You'll have to write code to implement it.

    Before I get to that... from a future perspective, you might get help faster and more eyes on your question if your repro is far more minimal. The whole BusinessAction<T> isn't really needed; the RequestHandler isn't needed... honestly, all you need to repro what you're doing is:

    public interface IActionRegistry
    {
    }
    
    public class ActionRegistry<T> : IActionRegistry
    {
    }
    

    If the other stuff is relevant to the question, definitely include it... but in this case, it's not, so adding it in here just makes the question harder to read through and answer. I know I, personally, will sometimes just skip questions where there's a lot of extra stuff because there are only so many hours in the day, you know?

    Anyway, here's how you'd do it, in working example form:

    var builder = new ContainerBuilder();
    
    // Register the action registry generic but not AS the interface.
    // You can't register an open generic as a non-generic interface.
    builder.RegisterGeneric(typeof(ActionRegistry<>));
    
    // Manually build the factory method. Going from reflection
    // System.Type to a generic ActionRegistry<Type> is not common and
    // not directly supported.
    builder.Register((context, parameters) => {
        // Capture the lifetime scope or you'll get an exception about
        // the resolve operation already being over.
        var scope = context.Resolve<ILifetimeScope>();
    
        // Here's the factory method. You can add whatever additional
        // enhancements you need, like better error handling.
        return (Type type) => {
            var closedGeneric = typeof(ActionRegistry<>).MakeGenericType(type);
            return scope.Resolve(closedGeneric) as IActionRegistry;
        };
    });
    
    var container = builder.Build();
    
    // Now you can resolve it and use it.
    var factory = container.Resolve<Func<Type, IActionRegistry>>();
    var instance = factory(typeof(DivideByZeroException));
    Assert.Equal("ActionRegistry`1", instance.GetType().Name);
    Assert.Equal("DivideByZeroException", instance.GetType().GenericTypeArguments[0].Name);