Search code examples
c#asp.netasp.net-web-apidependency-injectionsimple-injector

Ninject to Simple Injector: Register ActionFilter with Controller Attribute (with params)


I have an WebApi application that uses Simple Injector and I'm trying to configure a particular filter with controller attribute (with parameters). I have this configuration working in another project that uses Ninject, but I don't know how to do this on Simple Injector.

public enum UserType {
    Director,
    Developer,
    Leader
}

My controller:

[RequiresAtLeastOneOfUserTypes(UserType.Developer, UserType.Leader)]
public class MyController : Controller
{
    ...
}

My Attribute:

public sealed class RequiresAtLeastOneOfUserTypesAttribute : Attribute
{
    public UserType[] TypesToBeVerified { get; set; }

    public RequiresAtLeastOneOfUserTypesAttribute(params UserType[] typesToBeVerified)
    {
        TypesToBeVerified = typesToBeVerified;
    }
}

My Filter:

public class RequiresAtLeastOneOfUserTypesFilter : IActionFilter
{
    private readonly IUser _user;
    private readonly UserType[] _typesToBeVerified;

    protected RequiresAtLeastOneOfUserTypesFilter(IUser user, params UserType[] typesToBeVerified)
    {
        _user = user;
        _typesToBeVerified = typesToBeVerified;
    }

    public void OnActionExecuting(ActionExecutingContext filterContext)
    {
        bool authorized = _user.HasAtLeastOneOfTypes(_typesToBeVerified);
        if (!authorized)
        {
            throw new ForbiddenUserException();
        }
    }

    public void OnActionExecuted(ActionExecutedContext filterContext)
    {
        // do nothing
    }
}

And finally my Ninject configuration:

this.BindFilter<RequiresAtLeastOneOfUserTypesFilter>(FilterScope.Controller, 0)
    .WhenControllerHas<RequiresAtLeastOneOfUserTypesAttribute>()
    .WithConstructorArgumentFromControllerAttribute<RequiresAtLeastOneOfUserTypesAttribute>(
        "typesToBeVerified",
         attribute => attribute.typesToBeVerified);

My question is: How can I do this configuration using Simple Injector?


Solution

  • The Simple Injector Web API integration packages don't contain an integration feature for action filters as Ninject's integration package does. But such integration can be built in a few lines of code.

    There are a few options here. The first option is to revert to resolving services directly from inside your action filter, as demonstrated inside the documentation. This approach is fine when you have a single filter class, but isn't the cleanest approach, and would force you to make changes to your already created filter attribute.

    As a second option you can, therefore, create a action filter proxy class, that is able to forward the call to your real filter class, which can than be resolved by Simple Injector:

    public class ActionFilterProxy<T> : IActionFilter
        where T : IActionFilter
    {
        public ActionFilterProxy(Container container) => _container = container;
    
        public void OnActionExecuting(ActionExecutingContext filterContext) =>
            _container.GetInstance<T>().OnActionExecuting(filterContext);
    
        public void OnActionExecuted(ActionExecutedContext filterContext) =>
            _container.GetInstance<T>().OnActionExecuted(filterContext);
    }
    

    Using this proxy, you can make the following configuration:

    GlobalConfiguration.Configuration.Filters.Add(
        new ActionFilterProxy<RequiresAtLeastOneOfUserTypesFilter>(container));
    
    container.Register<RequiresAtLeastOneOfUserTypesFilter>();
    

    This still forces you to make a change to RequiresAtLeastOneOfUserTypesFilter, because Simple Injector can't provide the attribute's information (the UserType[]) to RequiresAtLeastOneOfUserTypesFilter's constructor. Instead,you can change RequiresAtLeastOneOfUserTypesFilter to the following:

    public class RequiresAtLeastOneOfUserTypesFilter : IActionFilter
    {
        private readonly IUser _user;
    
        public RequiresAtLeastOneOfUserTypesFilter(IUser user) => _user = user;
    
        public void OnActionExecuting(ActionExecutingContext filterContext)
        {
            // Get the attribute from the controller here
            var attribute = filterContext.ActionDescriptor.ControllerDescriptor
                .GetCustomAttribute<RequiresAtLeastOneOfUserTypesAttribute>();
    
            bool authorized = _user.HasAtLeastOneOfTypes(attribute.TypesToBeVerified);
            if (!authorized)
            {
                throw new ForbiddenUserException();
            }
        }
    
        public void OnActionExecuted(ActionExecutedContext filterContext)
        {
        }
    }
    

    A third option to use is the one referred to in the documentation, which is described in this blog post, which discusses a model where you place your filters behind an application-specific abstraction and allow them to be Auto-Registered. It uses the a similar proxy approach. This method is useful when you have multiple/many filters that need to be applied (where their order of execution is irrelevant).