Tidier Controllers with Request Filters

All of my ASP.NET Core controllers are starting to follow a certain pattern (as follows) and in the spirit of good DRY code, I’d like to define this in a single place.

try
    Logger.info ("TemplateController.Get(" + id.ToString() + ")")
    // Actual logic
with
| _ as ex -> Logger.error (sprintf "%O" ex); this.BadRequest() :> IActionResult

Furthermore, I’ve also noted that ASP.NET Core will happily provide me with an invalid model, which means I then have to do an extra null check. It’d be nice to handle all these scenarios transparently for all controllers.

ASP.NET Core provides a solution for this via filters, which exposes interfaces that can be implemented to intercept requests during the processing pipeline.

Exception Filter

An exception filter can be used to handle exceptions. An exception filter class must implement IExceptionFilter which includes a single method, OnException (context: ExceptionContext).

Unfortunately the setup for an exception filter goes in the Startup.ConfigureServices while the global logger factory is available from Startup.Configure. So using the application logger factory requires an un-F# hack as follows:

type Startup(env: IHostingEnvironment) =
    let mutable _loggerFactory : ILoggerFactory option = None

    member this.ConfigureServices(services: IServiceCollection) =
        let mvc = services.AddMvcCore()
        mvc.AddMvcOptions(fun mvcOptions -> mvcOptions.Filters.Add(new GlobalExceptionFilter(_loggerFactory.Value)))

    member this.Configure (app: IApplicationBuilder, loggerFactory: ILoggerFactory) =
        _loggerFactory <- Some(loggerFactory)

In my case I’m happy using my global logger, so my GlobalExceptionFilter won’t take any arguments. And here it is:

type GlobalExceptionFilter() =
    interface IExceptionFilter with
        member this.OnException (context: ExceptionContext) =
            Logger.error (sprintf "%O" context.Exception)

Action Filter

The action filter needs to do two things:
1. Log calls
2. Prevent invalid model state from reaching the actions.

Action Filters implement either the IActionFilter or IAsyncActionFilter interface and their execution surrounds the execution of action methods. Action filters are ideal for any logic that needs to see the results of model binding, or modify the controller or inputs to an action method. Additionally, action filters can view and directly modify the result of an action method.
https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/filters#action-filters

The aim here is to check and log the state before the action method is called so only the before-action method, OnActionExecuting, needs to be implemented. This implementation checks if the model state is valid, and if not logs the error and terminates the request with a 400 error without the actual action being executed. Where the model is valid, it logs the parameters.

type GeneralActionFilter() = 
    interface IActionFilter with

        member this.OnActionExecuting (context: ActionExecutingContext) =
            if not context.ModelState.IsValid then
                let errors = 
                    context.ModelState.Values 
                    |> Seq.collect (fun (value: ModelStateEntry) -> value.Errors)
                    |> Seq.map (fun (modelError: ModelError) -> sprintf "%s" modelError.Exception.Message)
                    |> String.concat "\n\t  "

                Logger.error (sprintf "Called %s. Error: Invalid model state\n\tException messages: \n\t  %s" context.ActionDescriptor.DisplayName errors)
                context.Result <- new BadRequestObjectResult(context.ModelState)
            else
                let args = [ for kvp in context.ActionArguments -> sprintf "%s %A" kvp.Key kvp.Value ] |> String.concat "\n\t"
                Logger.info (sprintf "Called %s with: \n\t%s" context.ActionDescriptor.DisplayName args)

        member this.OnActionExecuted (context: ActionExecutedContext) = ()

The end result is much cleaner controller methods:

    [<HttpGet>]
    member this.Get(id: System.Guid) : IActionResult =
        match GetTemplate id (new TemplateRepository()) with
        | Some template -> this.Json(template) :> IActionResult
        | None -> this.NotFound() :> IActionResult

    [<HttpPut>]
    member this.Create([<FromBody>]template: Template) : IActionResult =       
        CreateTemplate template (new TemplateCommandHandler())
        let url = new UrlActionContext (Controller = "Template", Action = "Get", Values = new RouteValueDictionary(dict [("id", box template.Id)]))
        this.Created((this.Url.Action url), "") :> IActionResult

Filters are added to the MVC pipeline in the Startup.ConfigureServices method:

member this.ConfigureServices(services: IServiceCollection) =
        let mvc = services.AddMvcCore()
        mvc.AddMvcOptions(fun mvcOptions -> mvcOptions.Filters.Add(new Api.Filters.GlobalExceptionFilter()))
        mvc.AddMvcOptions(fun mvcOptions -> mvcOptions.Filters.Add(new Api.Filters.GeneralActionFilter()))
        mvc.AddJsonFormatters() |> ignore

p.s.

As I noted in an earlier post I don’t have a functioning debugger or intellisense, and I’ve come to appreciate just how much time having a debugger and a navigable watch window saves – being able to scan through fields to find something appropriate is much faster and easier than trawling through documentation. An additional challenge is that the docs don’t include inherited members, so you have to open those separately to find all inherited members.