Using Action Filters to inject action arguments and reduce code duplicatio
Update 2019-01-02: It turns out that using a custom model binder is probably a better option than what is suggested below.
Recently, I've been experimenting with Action Filters in ASP.NET Core. Action Filters are attributes that you decorate your Controller action methods with.
Action filters can run code immediately before and after an individual action method is called. They can be used to manipulate the arguments passed into an action and the result returned from the action.
The part about manipulating the arguments passed into an action is what this post is about. We're going to see how Action Filters can help reducing code duplication in action methods.
Here are two controller actions for displaying a details page and an edit page for a Widget
entity:
// GET: Widgets/Details/5
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
var widget = await _context.Widget
.SingleOrDefaultAsync(m => m.Id == id);
if (widget == null)
{
return NotFound();
}
return View(widget);
}
// GET: Widgets/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
var widget = await _context.Widget.SingleOrDefaultAsync(m => m.Id == id);
if (widget == null)
{
return NotFound();
}
return View(widget);
}
There's obvious code duplication in these two controller actions. In both methods we have to check if the id
parameter was supplied, then check if the requested entity exists. If we add more action methods, we'd have to duplicate the same code over and over.
Fortunately, this repetitive code can be refactored into an Action Filter. The Action Filter can even manipulate method arguments to inject the Widget
entity directly into the controller action, like this:
// GET: Widgets/Details/5
[InjectWidget]
public IActionResult Details(Widget widget)
{
return View(widget);
}
// GET: Widgets/Edit/5
[InjectWidget]
public IActionResult Edit(Widget widget)
{
return View(widget);
}
Thanks to the [InjectWidget]
attribute, the Widget
entity is automatically injected into the controller action, thus reducing the amount of duplicated code and improving readability. Of course, if the entity does not exist, the controller action is short-circuited and the filter itself returns a 404 Not Found result.
There are several ways to implement filters that can be found in the documentation. Here I chose one the less straightforward way, by subclassing a TypeFilterAttribute
.
It's a bit lengthy, but the advantage of this method is that it supports Dependency Injection (DI) and the filter does not need to be registered with the DI container. We need DI in order to inject an ApplicationDbContext
instance into the filter.
public class InjectFilterAttribute : TypeFilterAttribute
{
private string _parameterName;
public InjectFilterAttribute()
: base(typeof(InjectWidgetImpl))
{
}
private class InjectWidgetImpl : IAsyncActionFilter
{
private readonly ApplicationDbContext _dbContext;
public InjectWidgetImpl(ApplicationDbContext context)
{
_dbContext = context;
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var id = context.RouteData.Values["id"] as int?;
if (id == null)
{
context.Result = new NotFoundResult();
return;
}
var widget = await _dbContext.Widget.SingleOrDefaultAsync(m => m.Id == id);
if (widget == null)
{
context.Result = new NotFoundResult();
return;
}
context.ActionArguments["widget"] = widget;
await next();
}
}
}
The interesting bits of the code are in the OnActionExecutingAsync
method. There we find the code that was initially duplicated in the Controller action methods. At the end of the method, the Widget
instance is added to the ActionArguments
dictionary: context.ActionArguments["widget"] = widget;
. It will then be passed as the argument of the corresponding "widget" parameter defined on the action method.
Besides reducing code duplication, there are more advantages to this technique:
- It makes the code easier to maintain and reduces the risk of introducing errors by copy/pasting the same code in every action methods.
- The Action Filter can be unit tested and it makes writing unit tests for action methods much simpler.