Custom Attributes in C# Web Controllers
17 December 2020
Updated: 03 September 2023
Implementing an attribute for a WebAPI or class in C# can help to reduce duplication and centralize parts of the application logic. This could be used for a variety of tasks such as logging information when methods are called as well as managinng authorization
In this post I’m going to cover the following:
Attribute Types and Execution Order
There are a few different attribute types that we can handle on a WebAPI that provide us with the ability to wrap some functionality around our endpoints, below are some of the common attributes that we can implement and the order in which they execute (StackOverflow)
- Authorization -
IAuthorizationFilter - Action -
IActionFilter - Result -
IResultFilter - Exception -
IExceptionFilter
IActionFilter
The IActionFilter executes before and after a method is executed and contains two different methods for doing this, namely the OnActionExecuting and OnActionExecuted methods respectively. A basic implemtation of IActionFilter would look like this:
1namespace CSharpAttributes.Attributes2{3 public class LogStatusAttribute : Attribute, IActionFilter4 {5 public LogStatusAttribute()6 {7 Console.WriteLine("Attribute Initialized");8 }9
10 public void OnActionExecuting(ActionExecutingContext context)11 {12 Console.WriteLine("OnActionExecuting");13 }14
15 public void OnActionExecuted(ActionExecutedContext context)16 {17 Console.WriteLine("OnActionExecuted");18 }19 }20}This can then be implemented on a controller method with a [LogStatus] attribute:
1[LogStatus]2[HttpGet]3public IEnumerable<WeatherForecast> Get()4{5 Console.WriteLine("Executing Get");6 return data;7}The order of logging which we see will be as follows:
Attribute Initializedwhen the controller is instantiatedOnActionExecutingwhen the controller is calledExecuting Getwhen the controller is executedOnActionExecutedwhen the controller is done executing
IAuthorizationFilter
The IAuthorizationFilter executes as the first filter on a controller’s method call
1namespace CSharpAttributes.Attributes2{3 public class CustomAuthorizeAttribute : Attribute, IAuthorizationFilter4 {5 public CustomAuthorizeAttribute()6 {7 Console.WriteLine("Attribute Initialized");8 }9
10 public void OnAuthorization(AuthorizationFilterContext context)11 {12 Console.WriteLine("OnAuthorization");13 }14 }15}This can then be implemented on a controller method with a [CustomAuthorize] attribute:
1[CustomAuthorize]2[HttpGet]3public IEnumerable<WeatherForecast> Get()4{5 Console.WriteLine("Executing Get");6 return data;7}The order of logging which we see will be as follows:
Attribute Initializedwhen the controller is instantiatedOnAuthorizationwhen the controller is calledExecuting Getwhen the controller is executed
Modify Response Data
An attribute’s context parameter gives us ways by which we can access the HttpContext as well as set the result of a method call so that it can be handled down the line. For example, we can implement our CustomAuthorize attribute with the following:
1public void OnAuthorization(AuthorizationFilterContext context)2{3 if (!context.HttpContext.Request.Headers.ContainsKey("X-Custom-Auth"))4 {5 context.Result = new UnauthorizedResult();6 }7
8 Console.WriteLine("Attribute Called");9}This will mean that if we set the context.Result in our method then the controller will not be executed and the endpoint will return the UnauthorizedResult early. You can also see that we’re able to access things like the HttpContext which makes it easy for us to view the request/response data and do things based on that
Attribute on a Class
Note that it’s also possible to apply the above to each method in a class by adding the attribute at the top of the class declaration:
1[ApiController]2[LogStatus]3[Route("[controller]")]4public class WeatherForecastController : ControllerBase5{6 ...Attributes with Input Parameters
We are also able to create attributes that enable the consumer to modify their behaviour by taking input parameters to the constructor, we can update our LogStatus attribute to do something like add a prefix before all logs:
1namespace CSharpAttributes.Attributes2{3 public class LogStatusAttribute : Attribute, IActionFilter4 {5 private readonly string _prefix;6
7 public LogStatusAttribute(string prefix = "")8 {9 _prefix = prefix;10 Console.WriteLine("Attribute Initialized");11 }12
13 public void OnActionExecuted(ActionExecutedContext context)14 {15 Console.WriteLine(_prefix + "OnActionExecuted");16 }17
18 public void OnActionExecuting(ActionExecutingContext context)19 {20 Console.WriteLine(_prefix + "OnActionExecuting");21 }22 }23}Then, applying to our controller method like so:
1[LogStatus("WeatherController-Get:")]2[HttpGet]3public IEnumerable<WeatherForecast> Get()4{5 Console.WriteLine("Executing Get");6 return data;7}So the new output will look like so:
Attribute Initializedwhen the controller is instantiatedWeatherForecast-Get:OnActionExecutingwhen the controller is calledExecuting Getwhen the controller is executedWeatherForecast-Get:OnActionExecutedwhen the controller is done executing
Attribute Setting at Class and Method Level
Since an attribute can be implemented at a class and method level it’s useful for us to be able to implement it at a class and the override the behaviour or add behaviour for a specific method
We can do this by setting the attribute inheritence to false
Updating out LogStatusAttribute we can add the AttributeUsage Attribute as follows:
1namespace CSharpAttributes.Attributes2{3 [AttributeUsage(AttributeTargets.All, Inherited = false)]4 public class LogStatusAttribute : Attribute, IActionFilter5 {6 ...This means that we can independently apply the attribute at class and method levels, so now our controller can look something like this:
1namespace CSharpAttributes.Controllers2{3 [ApiController]4 [Route("[controller]")]5 [LogStatus("WeatherForecast:")]6 public class WeatherForecastController : ControllerBase7 {8 [LogStatus("Get:")]9 [HttpGet]10 public IEnumerable<WeatherForecast> Get()11 {12 Console.WriteLine("Executing Get");13 return data;14 }15 }16}Which will output the logs as follows:
Attribute Initializedwhen the controller is instantiatedWeatherForecast:OnActionExecutingwhen the class is calledGet:OnActionExecutingwhen the controller is calledExecuting Getwhen the controller is executedGet:OnActionExecutedwhen the controller is done executingWeatherForecast:OnActionExecutedwhen the class is done executing