Manipulating a request body in an Action Filter

An Action Filter is a very neat mechanism to do a bit of pre or post processing on your controller calls.
I’m not going to go much into detail about what they are but essentially they provide a hook to call a piece of code right before or right after the controller code is executed.

In this post I will show how to properly manipulate the Request Body in an Action Filter in a way that does not break it for further processing.

This is by no means a new invention of mine, there are probably quite a few questions and answers for this sort of thing on StackOverflow, however they never show the full picture.

The basic controller

1
2
3
4
5
6
7
8
9
10
11
//POST api/users
[HttpPost]
public async Task<ActionResult> AddUser([FromBody]UserRequest userRequest)
{
if(!ModelState.IsValid)
{
return BadRequest(ModelState);
}
await _userRepository.AddUser(userRequest);
return Ok();
}

This is a fairly basic controller action that takes a JSON body representing a user request, checks that the model state is valid and adds the user in the database via the user Repository.

What we would like to add to this basic code is actually logging the request body that came in as this could be handy during development (or even sometimes production). And because we don’t want to write this code in every controller action, we will extract the whole logic in an Action Filter.

*Pro tip: Use a Typed Filter which allows you to use constructor dependency injection to pass in an ILoggerFactory that you’ll use to create the logger inside the Filter

*Pro tip 2: Since your filter has no dependency you can mark it as Reusable using the IsReusable flag so that it does not get instantiated for every call.

ModelState Validaion Filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class ModelStateValidationActionFilter : TypeFilterAttribute
{
public ModelStateValidationActionFilter() : base(typeof(ModelStateValidationActionFilterImpl))
{
}

private class ModelStateValidationActionFilterImpl : IActionFilter
{
private readonly ILogger _logger;

public ModelStateValidationActionFilterImpl(ILoggerFactory lf)
{
_logger = lf.CreateLogger("ModelStateValidationActionFilter");
}

public void OnActionExecuted(ActionExecutedContext context)
{
}

public void OnActionExecuting(ActionExecutingContext context)
{
try
{
if (!context.ModelState.IsValid)
{
var body = context.HttpContext?.Request?.BodyToString();
_logger.LogError($"Invalid request body:{Environment.NewLine}{body}");

context.Result = new BadRequestObjectResult(context.ModelState);
}
}
catch (Exception e)
{
_logger?.LogCritical(e, $"ModelStateValidation Filter failed.");
}
}
}
}

The updated controller

1
2
3
4
5
6
7
8
//POST api/users
[HttpPost]
[ModelStateValidationActionFilter(IsReusable = true)]
public async Task<ActionResult> AddUser([FromBody]UserRequest userRequest)
{
await _userRepository.AddUser(userRequest);
return Ok();
}

Reading the Request Body

Let’s dive in the extension method there - BodyToString() that extracts the request body. A simple version of that method would look something like this:

1
2
3
4
5
6
7
public static string BodyToString(this HttpRequest request)
{
using (var reader = new System.IO.StreamReader(request.Body))
{
return reader.ReadToEnd();
}
}

The issue with this method is that once it reads the body it closes the stream and it can’t be read again. While that might not be a problem immediatelly, when it’ll become a problem, it’s going to be really difficult to trace. For example, I have a global Action Fiter I use to log information about all requests being made and part of that is reading the body.

Final version

That being said, here’s the final version of the method that will allow other consumers down the pipeline to read the body over and over again.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static string BodyToString(this HttpRequest request)
{
var returnValue = string.Empty;
request.EnableRewind();
//ensure we read from the begining of the stream - in case a reader failed to read to end before us.
request.Body.Position = 0;
//use the leaveOpen parameter as true so further reading and processing of the request body can be done down the pipeline
using (var stream = new StreamReader(request.Body, Encoding.UTF8, true, 1024, leaveOpen:true))
{
returnValue = stream.ReadToEnd();
}
//reset position to ensure other readers have a clear view of the stream
request.Body.Position = 0;
return returnValue;
}

Share Comments