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.
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)
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.
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
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
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.