gplumb
gplumb

Reputation: 760

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!

Upvotes: 4

Views: 3054

Answers (2)

gplumb
gplumb

Reputation: 760

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 :-)

Upvotes: 3

Muhammad Ali
Muhammad Ali

Reputation: 288

I had same kind of problem while implementing Custom validation Filter and I Find out that APIController attribute performs automatic model state validation so either remove apiController attribute from Controller or The better approach to disable the default behavior by setting SuppressModelStateInvalidFilter option to true. You can set this option to true in the ConfigureServices method. Like,

public void ConfigureServices(IServiceCollection services)
{
services.Configure<ApiBehaviorOptions>(options =>
{
    options.SuppressModelStateInvalidFilter = true;
});
}

Upvotes: 0

Related Questions