In ASP.NET Core, we can easily use constructor injection to make Dependent Injections on components such as Controller and ViewComponent. But how to do it with ActionFilterAttribute?

The Problem


My blog system has an ActionFilter for deleting subscription files, and I want to log errors when an exception happens. I use NLog as the logging component, so I need to use LogManager.GetCurrentClassLogger() to get an instance of a Logger if I do not use Dependency Injection. The code of this ActionFilter is:

public class DeleteSubscriptionCache : ActionFilterAttribute
{
    private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

    public override void OnActionExecuted(ActionExecutedContext context)
    {
        base.OnActionExecuted(context);
        DeleteSubscriptionFiles();
    }

    private void DeleteSubscriptionFiles()
    {
        try
        {
            // ...
        }
        catch (Exception e)
        {
            Logger.Error(e, "Error Delete Subscription Files");
        }
    }
}

And I use this attribute on Actions:

[Authorize]
[HttpPost, ValidateAntiForgeryToken, DeleteSubscriptionCache]
[Route("manage/edit")]
public IActionResult Edit(PostEditModel model)

Of course, this can run without any problem. But if someday in the future I may choose other logging frameworks to replace NLog, this code will have to change, while the code using ILogger interface won't require any change. So let's make it use the ILogger interface via Dependency Injection.

Modified ActionFilter


Just like how we did with a common Controller, we can use constructor injection to inject ILogger<DeleteSubscriptionCache> type. 

public class DeleteSubscriptionCache : ActionFilterAttribute
{
    protected readonly ILogger<DeleteSubscriptionCache> Logger;

    public DeleteSubscriptionCache(ILogger<DeleteSubscriptionCache> logger)
    {
        Logger = logger;
    }

    public override void OnActionExecuted(ActionExecutedContext context)
    {
        base.OnActionExecuted(context);
        DeleteSubscriptionFiles();
    }

    private void DeleteSubscriptionFiles()
    {
        try
        {
            // ...
        }
        catch (Exception e)
        {
            Logger.LogError(e, "Error Delete Subscription Files");
        }
    }
}

However, by doing this, we can no longer use this on Actions, because the constructor is requiring a parameter here. We can't just new() a Logger into this, it is not nice programming practice. Let's see how we handle this.

ServiceFilter


Actually, in ASP.NET Core, we have a ServiceFilter to finish the job. It is also a type of Attribute, which can be applied onto an Action. It is defined in Microsoft.AspNetCore.Mvc.Core assembly. 

// A filter that finds another filter in an System.IServiceProvider.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class ServiceFilterAttribute : Attribute, IFilterFactory, IFilterMetadata, IOrderedFilter
{
        public ServiceFilterAttribute(Type type);
        public int Order { get; set; }
        public Type ServiceType { get; }
        public bool IsReusable { get; set; }
        public IFilterMetadata CreateInstance(IServiceProvider serviceProvider);
}

ServiceFilter allows us to resolve a service that is already in the IoC container. So we need to register DeleteSubscriptionCache first:

services.AddScoped<DeleteSubscriptionCache>();

Then we can use it as an Attribute onto an Action:

[Authorize]
[HttpPost, ValidateAntiForgeryToken]
[ServiceFilter(typeof(DeleteSubscriptionCache))]
[Route("manage/edit")]
public IActionResult Edit(PostEditModel model)

We can see the ILogger is instantiated at runtime:

Refer:

https://stackoverflow.com/questions/36109052/inject-service-into-action-filter/36109690 

https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-2.2