Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing a TypeFilterAttribute with Dependency Injection

I have a simple TypeFilterAttribute, called MyFilter that utilises dependency injection under the hood (.net core 2.2):

public class MyFilter : TypeFilterAttribute
{
    public MyFilter() :
        base(typeof(MyFilterImpl))
    {
    }

    private class MyFilterImpl : IActionFilter
    {
        private readonly IDependency _dependency;

        public MyFilterImpl(IDependency injected)
        {
            _dependency = injected;
        }

        public void OnActionExecuting(ActionExecutingContext context)
        {
            _dependency.DoThing();
        }

        public void OnActionExecuted(ActionExecutedContext context)
        {
        }
    }
}

I'm trying to test this with xUnit using a FakeController that looks like this:

[ApiController]
[MyFilter]
public class FakeApiController : ControllerBase
{
    public FakeApiController()
    {
    }

    [HttpGet("ping/{pong}")]
    public ActionResult<string> Ping(string pong)
    {
        return pong;
    }
}

The problem I'm having is that I don't seem to be able to trigger the MyFilter logic at test time. This is what my test method looks like so far:

[Fact]
public void MyFilterTest()
{
    IServiceCollection services = new ServiceCollection();
    services.AddScoped<IDependency, InMemoryThing>();

    var provider = services.BuildServiceProvider();
    var httpContext = new DefaultHttpContext();

    httpContext.RequestServices = provider;

    var actionContext = new ActionContext
    {
        HttpContext = httpContext,
        RouteData = new RouteData(),
        ActionDescriptor = new ControllerActionDescriptor()
    };

    var controller = new FakeApiController()
    {
        ControllerContext = new ControllerContext(actionContext)
    };

    var result = controller.Ping("hi");
}

Any idea what I'm missing here?

Thanks!

like image 602
gplumb Avatar asked Oct 28 '25 05:10

gplumb


1 Answers

I've cracked it after reading this: https://stackoverflow.com/a/50817536/1403748

While that doesn't directly answer my question, it set me on the right track. The key was the scoping of the class MyFilterImpl. By hoisting it, the test concerns of the filter can be separated from the controller it augments.

public class MyFilter : TypeFilterAttribute
{
    public MyFilter() : base(typeof(MyFilterImpl))
    {
    }
}

public class MyFilterImpl : IActionFilter
{
    private readonly IDependency _dependency;

    public MyFilterImpl(IDependency injected)
    {
        _dependency = injected;
    }

    public void OnActionExecuting(ActionExecutingContext context)
    {
        _dependency.DoThing();
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
    }
}

Once that's done, it's just a matter of instantiating an ActionExecutingContext and directly calling .OnActionExecuting() on an instance of the filter. I ended up writing a helper method that allows an IServiceCollection to be passed into it (required to ensure that test services/data can be injected at test-time):

/// <summary>
/// Triggers IActionFilter execution on FakeApiController
/// </summary>
private static async Task<HttpContext> SimulateRequest(IServiceCollection services, string methodName)
{
    var provider = services.BuildServiceProvider();

    // Any default request headers can be set up here
    var httpContext = new DefaultHttpContext()
    {
        RequestServices = provider
    };

    // This is only necessary if MyFilterImpl is examining the Action itself
    MethodInfo info = typeof(FakeApiController)
        .GetMethods(BindingFlags.Public | BindingFlags.Instance)
        .FirstOrDefault(x => x.Name.Equals(methodName));

    var actionContext = new ActionContext
    {
        HttpContext = httpContext,
        RouteData = new RouteData(),
        ActionDescriptor = new ControllerActionDescriptor()
        {
            MethodInfo = info
        }
    };

    var actionExecutingContext = new ActionExecutingContext(
        actionContext,
        new List<IFilterMetadata>(),
        new Dictionary<string, object>(),
        new FakeApiController()
        {
            ControllerContext = new ControllerContext(actionContext),
        }
    );

    var filter = new MyFilterImpl(provider.GetService<IDependency>());
    filter.OnActionExecuting(actionExecutingContext);

    await (actionExecutingContext.Result?.ExecuteResultAsync(actionContext) ?? Task.CompletedTask);
    return httpContext;
}

The test method itself just becomes something like this:

[Fact]
public void MyFilterTest()
{
    IServiceCollection services = new ServiceCollection();
    services.AddScoped<IDependency, MyDependency>();

    var httpContext = await SimulateRequest(services, "Ping");
    Assert.Equal(403, httpContext.Response.StatusCode);
}

Hopefully this will be of some use to someone else :-)

like image 117
gplumb Avatar answered Oct 29 '25 18:10

gplumb



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!