2

I have an ASP.NET Core 2.2 MVC web application using the Repository Pattern. I created a class called LogAttribute that is derived from ActionFilterAttribute so that I can log information after execution of controller actions.

Here is an example of using this action filter attribute in a mvc controller class:

public class HomeController : Controller
{
    private readonly IMyRepository _repository;

    public HomeController(IMyRepository repository)
    {
        _repository = repository;
    }

    [Log("Go to Home Page")]
    public async Task<IActionResult> Index()
    {
        ...
    }

    [Log("Go to About Page")]
    public async Task<IActionResult> About()
    {
        ...
    }
}

So when I go to /Home, it should log "Go to Home Page". And when I go to /About page, it should log "Go to About Page".

However, I have no idea how to access my repository from LogAttribute class. Here is the LogAttribute class:

public class LogAttribute : ActionFilterAttribute
{
    private IDictionary<string, object> _arguments;
    private IMyRepository _repository;

    public string Description { get; set; }

    public LogAttribute(string description)
    {
        Description = description;
    }

    // // Injecting repository as a dependency in the ctor DOESN'T WORK
    // public LogAttribute(string description, IMyRepository repository)
    // {
    //     Description = description;
    //     _repository = repository;
    // }

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        _arguments = filterContext.ActionArguments;
        base.OnActionExecuting(filterContext);
    }

    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        var description = Description;

        // NullReferenceException since I don't know
        // how to access _repository from this class
        _repository.AddLog(new LogAction
        {
            Description = description
        });
    }
}

So my question is how can I access my repository (or at least my DbContext) from my LogAttribute class?

Dai
  • 141,631
  • 28
  • 261
  • 374
kimbaudi
  • 13,655
  • 9
  • 62
  • 74
  • 1
    [How to use dependency injection with an attribute?](https://stackoverflow.com/questions/4102138/how-to-use-dependency-injection-with-an-attribute) – John Wu Oct 05 '19 at 02:21
  • @JohnWu - the link you provide suggests to create an attribute with public setters that can receive dependencies (i.e., `public ApplicationDbContext Context { get; set; }`). However, I already tried that, but it still gives me `NullReferenceException: Object reference not set to an instance of an object` error since Context is null. – kimbaudi Oct 05 '19 at 02:28
  • @Dai - I am using ASP.NET Core 2.2 (`Microsoft.AspNetCore.*`). I already mentioned this in my question. – kimbaudi Oct 05 '19 at 02:29
  • The QA that @JohnWu linked to is for non-Core ASP.NET MVC - though confusingly some posted answers _do_ apply to ASP.NET Core. – Dai Oct 05 '19 at 02:30
  • Yes, that link suggestion would work in ASP.NET 4.x MVC application, but not in ASP.NET Core. – kimbaudi Oct 05 '19 at 02:32

2 Answers2

2

You don't. Attributes cannot have constructor injected parameters and their lifetime is unbounded, making them a very poor choice for integration with the ASP.NET Core pipeline.

Therefore, attributes should not perform any "heavy lifting" themselves. I feel the various tutorials and guides online that show trivial logging (using Debug.WriteLine) inside an ActionFilterAttribute are doing their readers a disservice (e.g. (DO NOT DO THIS!) https://www.tutorialsteacher.com/mvc/action-filters-in-mvc )

If you're using ASP.NET Core, then implement IActionFilter (or IAsyncActionFilter) and IFilterFactory separately and make the IFilterFactory the Attribute (instead of the IActionFilter), like so:

// This class is the attribute. Note that it is not an action filter itself.
// This class cannot have DI constructor injection, but it can access the IServiceProvider.
public class LogAttribute : Attribute, IFilterFactory
{
    public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
    {
        return serviceProvider.GetService<LogFilter>();
    }
}


// This class is the actual filter. It is not an attribute.
// This class *can* have DI constructor injection.
public class LogFilter : IActionFilter // or IAsyncActionFilter
{
    public LogFilter( DbContext db )
    {

    }
}

A full example can be found here: ( https://www.devtrends.co.uk/blog/dependency-injection-in-action-filters-in-asp.net-core )

Dai
  • 141,631
  • 28
  • 261
  • 374
  • the link you provided was very helpful! not only did it provide solutions for injecting dependency in action filters, but it also talks about the service locator anti-pattern and suggests using custom filter factories instead as a better alternative. – kimbaudi Oct 05 '19 at 07:10
0

You can 100% inject into an action filter. The problem is when it's used as an attribute, as attributes are instantiated inline, providing no opportunity for the service collection to actually do the injection. However, there's a workaround for that with TypeFilterAttribute:

[TypeFilter(typeof(LogAttribute),
    Arguments = new object[] { "Go to Home Page" })]

Then, you'd have a constructor like:

public LogAttribute(string description, IMyRepository repository)
Chris Pratt
  • 232,153
  • 36
  • 385
  • 444
  • I chose not to use `TypeFilter` as a possible solution to inject dependencies "due to the generic nature of the filter, the syntax for doing so is terrible and you have no type safety" according to the [link](https://www.devtrends.co.uk/blog/dependency-injection-in-action-filters-in-asp.net-core). I rather chose to implement IFilterFactory and use my custom LogFilterFactory instead of TypeFilter (which is an implementation of IFilterFactory). So now, my attributes look like `[LogFilterFactory(Description = "Go to Home Page")]` – kimbaudi Oct 05 '19 at 15:04
  • `ServiceFilter`, `TypeFilter`, and implementing custom `IFilterFactory` seems to be the recommended approach to injecting dependency in attributes. Use these approach instead of `RequestServices.GetService` service locator (anti)pattern. – kimbaudi Oct 05 '19 at 15:08