Constructor Dependency Injection WebApi Attributes
M

2

4

I have been looking around for a non Parameter injection option for the WebApi attributes.

My question is simply whether this is actually possible using Structuremap?

I have been googling around but keep coming up with either property injection (which I prefer not to use) or supposed implementations of constructor injection that I have thus far been unable to replicate.

My container of choice is Structuremap however any example of this will suffice as I am able to convert it.

Anyone ever managed this?

Medardas answered 5/2, 2015 at 19:43 Comment(0)
W
25

Yes, it is possible. You (like most people) are being thrown by Microsoft's marketing of Action Filter Attributes, which are conveniently put into a single class, but not at all DI-friendly.

The solution is to break the Action Filter Attribute into 2 parts as demonstrated in this post:

  1. An attribute that contains no behavior to flag your controllers and action methods with.
  2. A DI-friendly class that implements IActionFilter and contains the desired behavior.

The approach is to use the IActionFilter to test for the presence of the attribute, and then execute the desired behavior. The action filter can be supplied with all dependencies (through the constructor) and then injected when the application is composed.

IConfigProvider provider = new WebConfigProvider();
IActionFilter filter = new MaxLengthActionFilter(provider);
config.Filters.Add(filter);

NOTE: If you need any of the filter's dependencies to have a lifetime shorter than singleton, you will need to use a GlobalFilterProvider as in this answer.

To wire this up with StructureMap, you will need to return an instance of the container from your DI configuration module. The Application_Start method is still part of the composition root, so you can use the container anywhere within this method and it is still not considered a service locator pattern. Note that I don't show a complete WebApi setup here, because I am assuming you already have a working DI configuration with WebApi. If you need one, that is another question.

public class DIConfig()
{
    public static IContainer Register()
    {
        // Create the DI container
        var container = new Container();

        // Setup configuration of DI
        container.Configure(r => r.AddRegistry<SomeRegistry>());
        // Add additional registries here...

        #if DEBUG
            container.AssertConfigurationIsValid();
        #endif

        // Return our DI container instance to the composition root
        return container;
    }
}

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        // Hang on to the container instance so you can resolve
        // instances while still in the composition root
        IContainer container = DIConfig.Register();

        AreaRegistration.RegisterAllAreas();

        // Pass the container so we can resolve our IActionFilter
        WebApiConfig.Register(GlobalConfiguration.Configuration, container);
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);
        AuthConfig.RegisterAuth();
    }
}

public static class WebApiConfig
{
    // Add a parameter for IContainer
    public static void Register(HttpConfiguration config, IContainer container)
    {
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );

        // Uncomment the following line of code to enable query support for actions with an IQueryable or IQueryable<T> return type.
        // To avoid processing unexpected or malicious queries, use the validation settings on QueryableAttribute to validate incoming queries.
        // For more information, visit http://go.microsoft.com/fwlink/?LinkId=279712.
        //config.EnableQuerySupport();

        // Add our action filter
        config.Filters.Add(container.GetInstance<IMaxLengthActionFilter>());
        // Add additional filters here that look for other attributes...
    }
}

The implementation of MaxLengthActionFilter would look something like this:

// Used to uniquely identify the filter in StructureMap
public interface IMaxLengthActionFilter : System.Web.Http.Filters.IActionFilter
{
}

public class MaxLengthActionFitler : IMaxLengthActionFilter
{
    public readonly IConfigProvider configProvider;

    public MaxLengthActionFilter(IConfigProvider configProvider)
    {
        if (configProvider == null)
            throw new ArgumentNullException("configProvider");
        this.configProvider = configProvider;
    }

    public Task<HttpResponseMessage> ExecuteActionFilterAsync(
        HttpActionContext actionContext,
        CancellationToken cancellationToken,
        Func<Task<HttpResponseMessage>> continuation)
    {
        var attribute = this.GetMaxLengthAttribute(filterContext.ActionDescriptor);
        if (attribute != null)
        {
            var maxLength = attribute.MaxLength;
            // Execute your behavior here (before the continuation), 
            // and use the configProvider as needed

            return continuation().ContinueWith(t =>
            {
                // Execute your behavior here (after the continuation), 
                // and use the configProvider as needed

                return t.Result;
            });
        }
        return continuation();
    }

    public bool AllowMultiple
    {
        get { return true; }
    }

    public MaxLengthAttribute GetMaxLengthAttribute(ActionDescriptor actionDescriptor)
    {
        MaxLengthAttribute result = null;

        // Check if the attribute exists on the action method
        result = (MaxLengthAttribute)actionDescriptor
            .GetCustomAttributes(typeof(MaxLengthAttribute), false)
            .SingleOrDefault();

        if (result != null)
        {
            return result;
        }

        // Check if the attribute exists on the controller
        result = (MaxLengthAttribute)actionDescriptor
            .ControllerDescriptor
            .GetCustomAttributes(typeof(MaxLengthAttribute), false)
            .SingleOrDefault();

        return result;
    }
}

And, your attribute which should not contain any behavior should look something like this:

// This attribute should contain no behavior. No behavior, nothing needs to be injected.
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)]
public class MaxLengthAttribute : Attribute
{
    public MaxLengthAttribute(int maxLength)
    {
        this.MaxLength = maxLength;
    }

    public int MaxLength { get; private set; }
}
Windbag answered 6/2, 2015 at 12:35 Comment(5)
Thanks for the detailed answer, I have seen this approach before but this explanation makes sense of it. Will report back and accept once i have had a go.Medardas
I have implemented a test based on the above and the link you provided. I am still struggling to see how I can use this to have Structuremap automatically inject the constructor arguments. Essentially you are newing up an instance of the the filter and adding it. Are you saying that this is the nearest we can get??Medardas
Here's an example of where I have gotten to in terms of DI. This occurs in WebApiConfig but requires me invoking via the GetInstance call. var filter = new StringToLowerFilter(WebApiApplication.Container.GetInstance<IStringService>()); config.Filters.Add(filter);Medardas
StrucureMap will automatically resolve the constructor-injected dependencies of your filter. You just need to uniquely identify your filter within StructureMap so you can resolve it. You can do that using a named instance, but it is cleaner just to give the filter a unique abstraction or to specify the concrete type explicitly in the GetInstance method.Windbag
Makes sense. For anyone else reading this, the above works perfectly. Cheers.Medardas
T
2

I struggled with custom action filter providers, without getting it to work for my auth attributes. I also trying out various approaches with constructor and property injection, but did not find a solution that felt nice.

I finally ended up injecting functions into my attributes. That way, unit tests can inject a function that returns a fake or mock, while the application can inject a function that resolves the dependency with the IoC container.

I just wrote about this approach here: http://danielsaidi.com/blog/2015/09/11/asp-net-and-webapi-attributes-with-structuremap

It works really well in my project and solves all problems I had with the other approaches.

Towne answered 11/9, 2015 at 8:52 Comment(2)
The link is broken ("Not Found").Sporadic
FYI - I was reading through your post and noticed you mentioned you couldn't get authorization filters to work with dependency injection. There is an example of doing that in this answer.Windbag

© 2022 - 2024 — McMap. All rights reserved.