Dependency injection with custom MVC filter attributes in ASP.NET Core

Using dependency injection with custom ExceptionFilterAttribute in ASP.NET Core

Apart from adding your own middleware to the ASP.NET MVC Core application pipeline, you can control the response using custom MVC filter attributes and selectively apply them to either whole controller or controller actions.

One of the commonly used MVC filter in ASP.NET Core is ExceptionFilterAttribute for handling error response in Wep API applications. It is easy to implement and I also wrote an article about Error handling in ASP.NET Core Web API few months ago. The problem that developers and me among them face with MVC filter attributes in ASP.NET Core is access to components which are injected in the Startup.cs class. These are often configuration, environment or logging.

One pretty useful usage of commonly dependency injected objects such as above mentioned IEnvironment, IConfiguration and ILogger<T> is different behavior of the MVC filter attribute you are implementing. Depending on these values your attribute behavior may be different. For example, you do not want to expose your error details and stack trace to Production Web API service error response, especially if that service endpoint is public. You want to do this only for the isolated Development and Staging environments.

Sample MVC filter attribute

On a simple example of custom ExceptionFilterAttrubute class I will demonstrate you how to use dependency injected objects in your custom attribute. So let's start wit the code.

First thing we are going to need is model class for returning the error details.

    public class ExceptionMessage
    {
        private object errorMessages;
        public string Message { get; private set; }
        public string Description { get; private set; }
        public IDictionary<string, string> ValidationErrors { get; private set; }

        public ExceptionMessage(ExceptionContext context)
        {
            if (context.ModelState != null && context.ModelState.Any(m => m.Value.Errors.Any()))
            {
                this.Message = "Model validation failed.";
                this.ValidationErrors = context.ModelState.Keys
                    .SelectMany(key => context.ModelState[key].Errors.ToDictionary(k => key, v => v.ErrorMessage))
                    .ToDictionary(k => k.Key, v => v.Value);
            }
            else
            {
                this.Message = context.Exception.Message;
                this.Description = context.Exception.StackTrace;
            }
        }
    }
    

Since focus of this article is accessing dependency injected objects, we're not going to discuss the message structure. Some guidelines for the error message are provided by Microsoft at Microsoft REST API Guidelines Github repository, but from my experience, error response message will depend a lot on the requirements of the system you are developing your Web API for.

Now your error response would ideally be a JSON message, but let's leave the serialization to the pipeline of the application and just return an ObjectResponse derived instance. For this purpose I created class ErrorObjectResult.

    public class ErrorObjectResult: ObjectResult
    {
        public ErrorObjectResult(object value, HttpStatusCode statusCode = HttpStatusCode.InternalServerError) : base(value)
        {
            StatusCode = (int)statusCode;
        }
    }
    

Nothing special in this class apart from constructor that takes status code (500 Internal Server Error as default) and object for ObjectResponse base constructor. In my previous article Error handling in ASP.NET Core Web API which I wrote in may, I am responding with JSON, but in the meanwhile I realized that it is better to leave serialization to the pipeline as your endpoint may not necessary response with JSON, depending on the headers sent from the client and requirements of the system you are developing service for. More about different response formats in ASP.NET Core Web API you can find in Using custom request and response serializers in ASP.NET Core article.

The core component we are focusing in this article is our custom ExceptionFilterAttribute derived class.

    public class ApiExceptionFilter : ExceptionFilterAttribute
    {
        public override void OnException(ExceptionContext context)
        {
            var errorMessage = new ExceptionMessage(context);
            if (context.ModelState.ErrorCount == 0)
            {
                //Unhandled exception
                context.Result = new ErrorObjectResult(errorMessage);
            }
            else
            {
                //Validation exception
                context.Result = new ErrorObjectResult(errorMessage,HttpStatusCode.BadRequest);
            }           
            base.OnException(context);
        }
    }
    

Let's decorate our controller with this attribute to handle errors that can occur in it.

    [Route("api/[controller]")]
    [ApiController]
    [ApiExceptionFilter]
    public class ValuesController : ControllerBase
    {
        // GET: api/Values
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new string[] { "value1", "value2" };
        }
    }
    

Everything looks fine right? Remember I mentioned different behavior for different environment and logging of errors? How do you think we do that?

Setting up dependency injection

Well, we first need to setup dependency injection for these these three interfaces we are going to access in our MVC filter attribute in Startup class

    public class Startup
    {
        public IConfiguration Configuration { get; private set; }
        public IHostingEnvironment HostingEnvironment { get; private set; }

        public Startup(IConfiguration configuration, IHostingEnvironment env)
        {
            Configuration = configuration;
            HostingEnvironment = env;
            Log.Logger = new LoggerConfiguration()
                                .Enrich.FromLogContext()
                                .ReadFrom.Configuration(configuration)
                                .CreateLogger();
        }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<IConfiguration>(new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile($"appsettings.{this.HostingEnvironment.EnvironmentName.ToLower()}.json")
                .Build());
            services.AddLogging();
            services.AddMvc();
			
			services.AddScoped<ApiExceptionFilter>();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddSerilog();
            app.UseMvc();
        }
    }
    

You can see that we added ApiExceptionFilter as a scoped service to our DI. This is because we are going to reference it in a different way on our controller so it gets initialized by the dependency injection with a new constructor with parameters.

We already have our IEnvironment and our ILogger injected at ConfigureServices method in Startup class, but we cannot access it in the attribute. If we add constructor of the attribute with IEnvironment and ILogger parameters we'll get compilation error as you cannot decorate Controller/Action with [ApiExceptionFilter] as it requires now IEnvironment and ILogger interface implementation passed. For this purpose we use decorate our controller with attribute ServiceFilter which takes type of our ApiExceptionFilter class as a constructor parameter

    [Route("api/[controller]")]
    [ApiController]
    [ServiceFilter(typeof(ApiExceptionFilter))]
    public class ValuesController : ControllerBase
    {
        // GET: api/Values
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new string[] { "value1", "value2" };
        }
    }
    
    

Finally, we have to update the constructor of our MVC filter attribute to accept IEnvironment, IConfiguration and ILogger parameters

    public class ApiExceptionFilter : ExceptionFilterAttribute
    {
        private ILogger<ApiExceptionFilter> logger;
        private IHostingEnvironment environment;
        private IConfiguration configuration;

        public ApiExceptionFilter(IHostingEnvironment environment, IConfiguration configuration,ILogger<ApiExceptionFilter> logger)
        {
            this.environment = environment;
            this.configuration = configuration;
            this.logger = logger;
        }
        public override void OnException(ExceptionContext context)
        {
            var errorMessage = new ExceptionMessage(context);

            if (context.ModelState.ErrorCount == 0)
            {
                //Unhandled exception
                context.Result = new ErrorObjectResult(errorMessage);
                logger.LogError(context.Exception, context.Exception.Message);
            }
            else
            {
                //Validation exception
                context.Result = new ErrorObjectResult(errorMessage,HttpStatusCode.BadRequest);
            }
            
            base.OnException(context);
        }
    }
    

We have both IEnvironment and ILogger<T> injected and available in our custom filter attribute class. Same approach can be used for any class you derive from any action filter attribute from Microsoft.AspNetCore.Mvc.Filters namespace

References

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.


About the author

DEJAN STOJANOVIC

Dejan is a passionate Software Architect/Developer. He is highly experienced in .NET programming platform includion ASP.NET MVC and WebApi. He likes working on new technologies and exciting challenging projects

CONNECT WITH DEJAN  Loginlinkedin Logintwitter Logingoogleplus Logingoogleplus

JavaScript

read more

SQL/T-SQL

read more

Umbraco CMS

read more

PowerShell

read more

Comments for this article