Search code examples
c#asp.net-coreasp.net-core-mvcasp.net-core-2.1custom-action-filter

Asp.net Core custom filter implementing IActionModelConvention and IFilterFactory


I need to create a custom action filter that implements both IActionModelConvention and IFilterFactory.

I use IActionModelConvention for setting several routes at the same time, and I use IFilterFactory to inject some services I need to use.

The problem is that the Apply() method from the IActionModelConvention is being called before the CreateInstance() method from the IFilterFactory, and I need the injected services to be available in the Apply().

My question is how do I inject the services before the Apply() method is being called? and I also prefer to use IFilterFactory to inject services because it doesn't force me to wrap the actual attribute with the [ServiceFilter] or [TypeFilter] attributes.

Here is my code:

public class Contains2RoutesAttribute : Attribute, IActionModelConvention, IFilterFactory
{
    public ISomeService SomeService{ get; set; }

    public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
    {
        ISomeService someService = serviceProvider.GetService<ISomeService>();
        return new Contains2RoutesAttribute() { SomeService = someService };
    }

    public void Apply(ActionModel action)
    {
        // Here I need to use the service injected:
        this.SomeService.DoSomething(); // ERROR: The service here is null.

        action.Selectors.Clear();

        // Adding route 1:
        action.Selectors.Add(new SelectorModel
        {
            AttributeRouteModel = new AttributeRouteModel { Template = "~/index1" }
        });

        // Adding route 2:
        action.Selectors.Add(new SelectorModel
        {
            AttributeRouteModel = new AttributeRouteModel { Template = "~/index2" }
        });
    }
}

Solution

  • Your IActionModelConvention implementation will run only once, at startup. Apply will be called once for each action. To use ISomeService inside of the Apply function, pass it through as a constructor argument. Your Contains2RoutesAttribute class need not be an attribute or an implementation of IFilterFactory, as you've confirmed in the comments that it does not participate in the filter pipeline. Here's a code example, where I've also renamed the class to better represent what it's doing (it's no longer an attribute):

    public class Contains2RoutesConvention : IActionModelConvention
    {
        private readonly ISomeService someService;
    
        public Contains2RoutesConvention(ISomeService someService)
        {
            this.someService = someService;
        }
    
        public void Apply(ActionModel actionModel)
        {
            someService.DoSomething();
    
            ...
        }
    }
    

    Register this convention in Startup.ConfigureServices:

    services.AddMvc(options =>
    {
        options.Conventions.Add(new Contains2RoutesConvention(new SomeService()));
    });
    

    This is where it gets a bit more interesting. You can't use dependency injection with a convention, so in this example, I've created an instance of SomeService inline when constructing Contains2RoutesConvention. If you want this instance to be a singleton that can be used elsewhere in your application, you can do something like this in ConfigureServices:

    var someService = new SomeService();
    
    services.AddMvc(options =>
    {
        options.Conventions.Add(new Contains2RoutesConvention(someService));
    });
    
    services.AddSingleton<ISomeService>(someService);
    

    Of course, this depends on whether or not SomeService has dependencies of its own, but if it does, they would not be resolvable from the DI container as it's too early in the pipeline.