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)

  1. Authorization - IAuthorizationFilter
  2. Action - IActionFilter
  3. Result - IResultFilter
  4. 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:

1
namespace CSharpAttributes.Attributes
2
{
3
public class LogStatusAttribute : Attribute, IActionFilter
4
{
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]
3
public IEnumerable<WeatherForecast> Get()
4
{
5
Console.WriteLine("Executing Get");
6
return data;
7
}

The order of logging which we see will be as follows:

  1. Attribute Initialized when the controller is instantiated
  2. OnActionExecuting when the controller is called
  3. Executing Get when the controller is executed
  4. OnActionExecuted when the controller is done executing

IAuthorizationFilter

The IAuthorizationFilter executes as the first filter on a controller’s method call

1
namespace CSharpAttributes.Attributes
2
{
3
public class CustomAuthorizeAttribute : Attribute, IAuthorizationFilter
4
{
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]
3
public IEnumerable<WeatherForecast> Get()
4
{
5
Console.WriteLine("Executing Get");
6
return data;
7
}

The order of logging which we see will be as follows:

  1. Attribute Initialized when the controller is instantiated
  2. OnAuthorization when the controller is called
  3. Executing Get when 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:

1
public 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]")]
4
public class WeatherForecastController : ControllerBase
5
{
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:

1
namespace CSharpAttributes.Attributes
2
{
3
public class LogStatusAttribute : Attribute, IActionFilter
4
{
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]
3
public IEnumerable<WeatherForecast> Get()
4
{
5
Console.WriteLine("Executing Get");
6
return data;
7
}

So the new output will look like so:

  1. Attribute Initialized when the controller is instantiated
  2. WeatherForecast-Get:OnActionExecuting when the controller is called
  3. Executing Get when the controller is executed
  4. WeatherForecast-Get:OnActionExecuted when 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:

1
namespace CSharpAttributes.Attributes
2
{
3
[AttributeUsage(AttributeTargets.All, Inherited = false)]
4
public class LogStatusAttribute : Attribute, IActionFilter
5
{
6
...

This means that we can independently apply the attribute at class and method levels, so now our controller can look something like this:

1
namespace CSharpAttributes.Controllers
2
{
3
[ApiController]
4
[Route("[controller]")]
5
[LogStatus("WeatherForecast:")]
6
public class WeatherForecastController : ControllerBase
7
{
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:

  1. Attribute Initialized when the controller is instantiated
  2. WeatherForecast:OnActionExecuting when the class is called
  3. Get:OnActionExecuting when the controller is called
  4. Executing Get when the controller is executed
  5. Get:OnActionExecuted when the controller is done executing
  6. WeatherForecast:OnActionExecuted when the class is done executing