How can I do dependency injection into action filters in ASP.NET 4 RC WebAPI?
Asked Answered
N

2

8

I'm using Windsor to manage IoC for my controllers in a WebAPI project. I've got a DependencyResolver working nicely to resolve controller dependencies, but now I'm looking to inject dependencies into a custom action filter I'm using to manage authentication.

I've looked into using a custom ActionInvoker but it's not clear from the interface that WebAPI is using how I would go about resolving property dependencies on the custom action filter attribute before it executes. Anyone have a good example of how to do this in the MVC 4 RC?

EDIT: I'm aware you can't do constructor injection on filters, because they're attributes and therefore instantiated by the .NET framework - but I'm hoping there's some point in the execution lifecycle that happens AFTER the filter is instantiated but BEFORE it gets executed, where I could run some custom code to enumerate across the filters' public properties and inject the necessary services.

Natividadnativism answered 11/6, 2012 at 11:17 Comment(1)
IMHO very good decoupled version is described in this question (and answer) - ASP.NET MVC IFilterProvider and separation of concerns.Phreno
M
11

Action filters are attributes. In .NET attribute the instantiation process is managed by the .NET runtime and you don't have control over it. So one possibility is to use Poor Man's Dependency Injection which I would personally advice you against.

Another possibility is to use a marker attribute:

public class MyActionFilterAttribute : Attribute 
{ 

}

and then have the action filter using constructor injection:

public class MyActionFilter : ActionFilterAttribute
{
    private readonly IFoo _foo;
    public MyActionFilter(IFoo foo)
    {
        _foo = foo;
    }

    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        if (actionContext.ActionDescriptor.GetCustomAttributes<MyActionFilterAttribute>().Any())
        {
            // The action is decorated with the marker attribute => 
            // do something with _foo
        }
    }
}

and then register it as a global action filter in Application_Start:

IFoo foo = ....
GlobalConfiguration.Configuration.Filters.Add(new MyActionFilter(foo));
Marmoset answered 11/6, 2012 at 11:30 Comment(3)
Darin - thanks for this; I've already tried the service locator approach but I'm looking for something a little cleaner - see edit on my question which hopefully clarifies what I'm looking for.Natividadnativism
@DylanBeattie, no, if you want to use constructor injection (which is the proper way to inject required dependencies into classes) you need to have control over the instantiation of the class which unfortunately you don't in the case of attributes. That's why you could use the marker interface as shown in my answer.Marmoset
if you register the filters with your container, and you are creating the container in the global.asax you could use the container to resolver the filters, e.g. _container.ResolveAll(t).Cast<IFilter>().ForEach(GlobalConfiguration.Configuration.Filters.Add)Lab
L
6

I had the same problem, but decided to go for the ServiceLocator (DependencyResolver.GetService) for this, as its in the framework it seems to me to be a valid approach

public class RequiresSessionAttribute :
    ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        var sessionService =
            (ISessionService) actionContext
                    .ControllerContext.Configuration.DependencyResolver
                    .GetService(typeof (ISessionService));

        var sessionId = HttpUtility
            .ParseQueryString(actionContext.Request.RequestUri.Query)
            .Get("sessionId");

        if (sessionId == null
            || !sessionService.IsValid(sessionId))
            throw new SessionException();

        base.OnActionExecuting(actionContext);
    }
}

and here is a test for this attribute, bit of a pain but possible

public class requires_sessionId
{
    [Fact]
    void can_call_action_with_session_id()
    {
        var context = GetContext("http://example.com/?sessionId=blaa");

        var sut = new RequiresSessionAttribute();

        Assert.DoesNotThrow(
            () => sut.OnActionExecuting(context));
    }

    [Fact]
    void can_not_call_action_without_session_id()
    {
        var context = GetContext("http://example.com/");

        var sut = new RequiresSessionAttribute();

        Assert.Throws<SessionException>(
            () => sut.OnActionExecuting(context));
    }

    HttpActionContext GetContext(string url)
    {
        var sessionServiceMock = new Mock<ISessionService>();
        sessionServiceMock
            .Setup(x => x.IsValid(It.IsAny<string>()))
            .Returns(true);

        var dependancyResolverMock = new Mock<IDependencyResolver>();
        dependancyResolverMock
            .Setup(x => x.GetService(It.IsAny<Type>()))
            .Returns(sessionServiceMock.Object);

        var config = new HttpConfiguration
               {
                   DependencyResolver = dependancyResolverMock.Object
               };
        var controllerContext = new HttpControllerContext
               {
                    Configuration = config,
                    Request = new HttpRequestMessage(
                               HttpMethod.Get,
                               url)
                };

        return
            new HttpActionContext
                {
                    ControllerContext = controllerContext,
                };
    }
}
Lab answered 16/8, 2012 at 13:9 Comment(1)
Maybe not the best way, but in some cases it might be the only way.Cacilia

© 2022 - 2024 — McMap. All rights reserved.