There are times when we want to be able to consistently add a behaviour across our ASP.NET Core
app that doesn’t quite belong in application logic – a classic example is timing how long a request takes. This is where Filters come in (or Middleware, but that’s for another post).
Filters allow us to take certain actions before and after execution of a controller action. Filters also give us a Context
object detailing things about the request and the current response.
Basic Implementation
Let’s see first how we’d implement an ActionFilter
.
public class LogOrderAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
Console.WriteLine("Action Executing.....");
base.OnActionExecuting(context);
}
public override void OnActionExecuted(ActionExecutedContext context)
{
base.OnActionExecuted(context);
Console.WriteLine("Action Executed.....");
}
public override void OnResultExecuting(ResultExecutingContext context)
{
base.OnResultExecuting(context);
Console.WriteLine("Result Executing.....");
}
public override void OnResultExecuted(ResultExecutedContext context)
{
base.OnResultExecuted(context);
Console.WriteLine("Result Executed.....");
}
}
After this, add it to your Controller
as an attribute.
public class HomeController : Controller
{
[LogOrder]
public IActionResult Index()
{
return View();
}
}
This implemention shows us the order of execution as follows.
Action Executing.....
Action Executed.....
Result Executing.....
Result Executed.....
What about Exceptions?
What about if we wanted to filter not just succesful requests, but also unsuccesful one’s that throw an exception? Well, we can implement IExceptionFilter
. In this case, I’ll create a reusable one and call it CompleteActionFilterAttribute
.
public abstract class CompleteActionFilterAttribute : ActionFilterAttribute, IExceptionFilter
{
public virtual void OnException(ExceptionContext context) { }
}
Now, let’s implement a simple timer that times both cases where the action succeeds and throws an Exception
.
public class TimerFilterAttribitute : CompleteActionFilterAttribute
{
private Stopwatch _stopWatch;
private string _name;
public TimeFilterAttribute(string name)
{
_name = name;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
_stopWatch.Start();
base.OnActionExecuting(context);
}
public override void OnException(ExceptionContext context)
{
LogTime();
}
public override void OnResultExecuted(ResultExecutedContext context)
{
base.OnResultExecuted(context);
LogTime();
}
private void LogTime()
{
_stopWatch.Stop();
Console.WriteLine($"{_name}: {_stopWatch.ElapsedMilliseconds}");
_stopWatch = null;
}
}
Then use it as an attribute for your action.
public class HomeController : Controller
{
[TimerFilter("Home.Index")]
public IActionResult Index()
{
return View();
}
}
Now, every time you hit that action, you should see the how long the request took logged in the Console.
What about Dependency?
ASP.NET Core comes with ways of doing Dependency Injection
out of the box. You can read more about Dependency Injection here.
First step you have to do is ensure that the dependency that you want to be injected is registered in the Investion of Control container (IoC Container). In our case, we’ll simply use ILog
for logging.
ASP.NET Core can inject dependencies to a FilterAttribute
wrapping it with TypeFilterAttribute
.
So, this is our basic implementation with our required dependency.
public class TimerFilterAttribitute : CompleteActionFilterAttribute
{
private Stopwatch _stopWatch;
private string _name;
private ILog _log; // this is our dependency
// our dependency is injected through our constructor
public TimeFilterAttribute(ILog log, string name)
{
_log = log;
_name = name;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
_stopWatch.Start();
base.OnActionExecuting(context);
}
public override void OnException(ExceptionContext context)
{
LogTime();
}
public override void OnResultExecuted(ResultExecutedContext context)
{
base.OnResultExecuted(context);
LogTime();
}
private void LogTime()
{
_stopWatch.Stop();
// using our logger
_logger.Debug($"{_name}: {_stopWatch.ElapsedMilliseconds}");
_stopWatch = null;
}
}
Now, we can use this implementation by wrapping it with TypeFilter
.
public class HomeController : Controller
{
[TypeFilter(typeof(TimerFilterAttribute),
Arguments = new object[] { "Home.Index" })]
public IActionResult Index()
{
return View();
}
}
Additional arguments after injected dependencies are passed by setting the Arguments
property with the arguments required by the class.
Done.
Or… wait… we can do better! we can actually subclass TypeFilter
to get the same effect with static typing for arguments.
public class TimerFilterAttribute : TypeFilterAttribute
{
// additional arguments after injected arguments are now set here,
// - name in this case
// we call the base with the class that we're wrapping, now called _TimerFilterAttribute
public TimerFilterAttribute(string name) : base(typeof(_TimerFilterAttribute))
{
// we set the additional arguments using
// the arguments property as we've done before
Arguments = new object[] { name };
}
}
// Changed the name of the class, doesn't really matter too much
public class _TimerFilterAttrbitute : CompleteActionFilterAttribute
{
private Stopwatch _stopWatch;
private string _name;
private ILogger _logger;
public TimeFilterAttribute(ILogger logger, string name)
{
_logger = logger;
_name = name;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
_stopWatch.Start();
base.OnActionExecuting(context);
}
public override void OnException(ExceptionContext context)
{
base.OnException(context);
LogTime();
}
public override void OnResultExecuted(ResultExecutedContext context)
{
base.OnResultExecuted(context);
LogTime();
}
private void LogTime()
{
_stopWatch.Stop();
_logger.Debug($"{_name}: {_stopWatch.ElapsedMilliseconds}");
_stopWatch = null;
}
}
Now, we can use it in a more elegant way with dependency injection included.
public class HomeController : Controller
{
[TimerFilter("Home.Index")]
public IActionResult Index()
{
return View();
}
}