ASP.NET Core offers a versatile request-response pipeline, allowing seamless customization and intervention. Managing incoming requests to read the request body in an ASP.NET Core Web API application is a common and crucial task. There are multiple methods and techniques available, each tailored to specific requirements. This comprehensive guide delves into various approaches, providing detailed insights into their usage and benefits.
So let’s dive into the details.
Reading Request Body in a Controller
Parsing the request body within our ASP.NET Core Web API controllers gives us the flexibility to manage incoming data. Whether we choose to read the body as a string or employ model binding for structured data, these methods provide us with the tools to seamlessly process incoming requests.
Reading as String
We can read the request body as a string in a controller action. ASP.NET Core offers the Request
property within controller actions, granting access to the Request.Body
. However, this body has a Stream
type and is unreadable to us. To handle this stream data as text, we need to convert it into a string format:
[HttpPost("read-as-string")] public async Task<IActionResult> ReadAsString() { var requestBody = await Request.Body.ReadAsStringAsync(); return Ok($"Request Body As String: {requestBody}"); }
Here, we access the request body by invoking an extension method called ReadAsStringAsync()
. We’ll delve into the details shortly. Once we obtain the response from the extension method, we straightforwardly return it from our action for testing purposes.
ReadAsStringAsync() Extension Method
Rather than directly converting the stream data to a string within the controller action, we can implement an extension method. This approach allows us to use it in various scenarios. As we progress through this article, there will be a recurrent need to convert stream data to a string. Leveraging extension methods provides an efficient solution to implement once and employ multiple times.
Let’s implement the extension method:
public static class RequestExtensions { public static async Task<string> ReadAsStringAsync(this Stream requestBody, bool leaveOpen = false) { using StreamReader reader = new(requestBody, leaveOpen: leaveOpen); var bodyAsString = await reader.ReadToEndAsync(); return bodyAsString; } }
We establish an extension method named ReadAsStringAsync()
. This method enhances the functionalities of the Stream
type, enabling us to seamlessly convert stream data into a string format. To achieve this, we create an instance of the StreamReader
class. Utilizing the StreamReader
class facilitates the reading of characters from a stream, be it a file or a network stream.
Despite the numerous constructor options available, in our context, we provide two parameters for the StreamReader
. First is the stream data representing our request body and the second parameter, leaveOpen
. This parameter ensures that the stream remains open even after the StreamReader
completes its operations and is disposed.
In the subsequent step, we invoke the ReadToEndAsync()
method of the reader object, which yields the string representation of the stream data.Â
Using EnableBuffering for Multiple Reads
What occurs if we attempt to read the request body once more or multiple times in the above scenario?Â
Let’s check this:
[HttpPost("read-as-string-multiple")] public async Task<IActionResult> ReadAsStringMultiple() { var requestBody = await Request.Body.ReadAsStringAsync(); var requestBodySecond = await Request.Body.ReadAsStringAsync(); return Ok($"First: {requestBody}, Second:{requestBodySecond}"); }
Now, let’s make an API call to this action method and inspect the response through Swagger:
Here, we send the string “CodeMaze” as the request payload. When we check the response, we see that the first read attempt is successful, but the second one is not what we expect.
In situations requiring multiple reads of the request body, enabling buffering is essential. To achieve this, we can utilize the EnableBuffering()
method of the Request
object:
[HttpPost("read-multiple-enable-buffering")] public async Task<IActionResult> ReadMultipleEnableBuffering() { Request.EnableBuffering(); var requestBody = await Request.Body.ReadAsStringAsync(true); Request.Body.Position = 0; var requestBodySecond = await Request.Body.ReadAsStringAsync(); return Ok($"First: {requestBody}, Second:{requestBodySecond}"); }
Here, we invoke the Request.EnableBuffering()
method, allowing the reading of the request body multiple times. Following that, we invoke the ReadAsStringAsync()
method. To ensure the stream remains open for subsequent reads, we set the leaveOpen
parameter to true. Just before the second attempt, we reset the position of the request body to zero.
With the latest modifications in place, let’s test the API with the same parameter:
First: CodeMaze, Second: CodeMaze
After invoking the EnableBuffering()
method, we effectively retrieve the request body for all subsequent attempts.
Model Binding
ASP.NET Core allows automatic deserialization of the request body to a predefined model class. This approach simplifies the handling of structured data. To make use of this intriguing feature, we utilize the [FromBody]
attribute within our action, preceding the model parameter:
[HttpPost("read-from-body")] public IActionResult ReadFromBody([FromBody] PersonItemDto model) { var message = $"Person Data => Name: {model.Name}, Age: {model.Age}"; return Ok(message); }
Here, ASP.NET Core automatically maps the incoming request body to the PersonItemDto
class. Then, within the action body, we have the ability to access and utilize the properties of the model.
We are free to design the PersonItemDto
class and its properties to match our application’s needs precisely:
public class PersonItemDto { public string Name { get; set; } public int Age { get; set; } }
Let’s explore one more scenario before bidding farewell to this topic. Let’s say we want to send and read an extra salary parameter to our action method. To solve this, we can try to add additional salary
parameter with the FromBody
attribute:
[HttpPost("read-from-body-multi-param")] public IActionResult ReadFromBodyMultiParam([FromBody] PersonItemDto model, [FromBody] decimal salary) { var message = $"Person Data => Name: {model.Name}, Age: {model.Age}, Salary: {salary}"; return Ok(message); }
Is this approach correct?
There are no compilation errors when we build the code. However, during the runtime, when our application attempts to execute, an InvalidOperationException
occurs at the line app.MapControllers()
in the Program.cs
file:
Action 'ReadingRequestBody.Controllers.HomeController.ReadFromBodyMultiParam (ReadingRequestBody)' has more than one parameter that was specified or inferred as bound from request body. Only one parameter per action may be bound from body. Inspect the following parameters, and use 'FromQueryAttribute' to specify bound from query, 'FromRouteAttribute' to specify bound from route, and 'FromBodyAttribute' for parameters to be bound from body:
In simple terms, this exception notifies us that the FromBody
attribute is permitted only once in action method parameters. Hence, it is advisable to gather all parameters within a single request model parameter.
Using a Custom Middleware
Middleware provides a powerful way to intercept requests and responses globally within our ASP.NET Core application. We can create custom middleware to read the request body. We will not go into details of Middleware in this article, but if you need a refresher, visit our ASP.NET Core Middleware – Creating Flexible Application Flows article.
Let’s see how to create a custom middleware to read the request body:
public class RequestBodyMiddleware { private readonly RequestDelegate _next; private readonly ILogger _logger; private readonly int MaxContentLength = 1024; public RequestBodyMiddleware(RequestDelegate next, ILogger logger) { _next = next; _logger = logger; } public async Task Invoke(HttpContext context) { await _next(context); } }
Here, we create a custom middleware named RequestBodyMiddleware
.
Let’s now implement the Invoke()
method to access and read the request body:
public async Task Invoke(HttpContext context) { var requestPath = context.Request.Path.Value; if (requestPath.IndexOf("read-from-middleware") > -1) { context.Request.EnableBuffering(); var requestBody = await context.Request.Body.ReadAsStringAsync(true); if (requestBody.Length > MaxContentLength) { context.Response.StatusCode = 413; await context.Response.WriteAsync("Request Body Too Large"); return; } _logger.LogInformation("Request Body:{@requestBody}", requestBody); context.Request.Headers.Add("RequestBodyMiddleware", requestBody); context.Items.Add("RequestBody", requestBody); context.Request.Body.Position = 0; } await _next(context); }
We examine the request path to identify specific routes initially. After that, we seamlessly translate the request body stream into a raw string representation by calling our extension method ReadAsStringAsync()
. Following this, numerous options are available for leveraging the request body. Examples include logging the result, appending it to the request header, or checking the request length, among other potential uses. After concluding our processing of the request body, we direct the request to the subsequent middleware by invoking the _next()
delegate.
To utilize this middleware, it’s essential to incorporate it in the Program.cs
file:
var app = builder.Build(); app.UseMiddleware<RequestBodyMiddleware>();
Avoid Reading Large Request Bodies
Handling large request bodies in a web application demands caution due to potential memory issues, performance degradation, and resource exhaustion. The time-consuming processing of large bodies may lead to slower response times and reduced overall application throughput. Concerns include the risk of denial-of-service (DoS) attacks through intentionally large bodies and increased network overhead. To address these challenges, some best practices include setting size constraints, incorporating streaming mechanisms, and deploying asynchronous processing to improve scalability.
Let’s revisit our custom middleware to inspect the analysis of the request payload:
if (requestBody.Length > MaxContentLength) { context.Response.StatusCode = 413; await context.Response.WriteAsync("Request Body Too Large"); return; }
Here, we establish a condition to verify if the request length exceeds a predefined maximum length value. If the condition is met, we halt the request pipeline and return a status code of Payload Too Large (413).
Using Action Filters to Read the Request Body
Action filters in ASP.NET Core provide a way to run logic before or after controller action methods execute. This functionality empowers us to intercept incoming requests and establish a stopping point to inspect the request body.
Let’s create a custom action filter:
public class ReadRequestBodyActionFilter : IAsyncActionFilter { public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { var requestPath = context.HttpContext.Request.Path.Value; if (requestPath.IndexOf("read-from-action-filter") > -1) { var requestBody = await context.HttpContext.Request.Body.ReadAsStringAsync(); context.HttpContext.Request.Headers.Add("ReadRequestBodyActionFilter", requestBody); } await next(); } }
In this scenario, we create a custom action filter called ReadRequestBodyActionFilter
that implements the IActionFilter
interface. Within this filter, we define the OnActionExecuting()
method to handle our specific logic. Then, we examine the request path and extract the request body using the ReadAsStringAsync()
extension method. Lastly, we append the request body to the request header using the key ReadRequestBodyActionFilter
.
To utilize this action filter, it’s essential to register it in the Program.cs
file:
builder.Services.AddControllers(options => { options.Filters.Add<ReadRequestBodyActionFilter>(); });
Using a Custom Attribute to Read the Request Body
When it comes to intercepting incoming requests, custom attributes can be used in combination with action filters to modify the behavior of the request processing pipeline.
Let’s create a custom attribute to inspect and read the request body:
[AttributeUsage(AttributeTargets.Method)] public class ReadRequestBodyAttribute : Attribute, IAsyncActionFilter { public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { var requestBody = await context.HttpContext.Request.Body.ReadAsStringAsync(); context.HttpContext.Request.Headers.Add("ReadRequestBodyAttribute", requestBody); await next(); } }
Here, we create a custom attribute called ReadRequestBodyAttribute
that implements the IAsyncActionFilter
interface. Then, we implement the OnActionExecutionAsync()
method to read the request body. Once again, we create a StreamReader
object and we access the request body as a string by calling the ReadToEndAsync()
method. Finally, we append the request body to the request header using the key ReadRequestBodyAttribute
.
We can now proceed to utilize our custom attribute. To do so, we need to apply it to our controller action:
[ReadRequestBody] public IActionResult ReadFromAttribute() { var requestBody = Request.Headers["ReadRequestBodyAttribute"]; var message = $"Request Body From Attribute : {requestBody}"; return Ok(message); }
Here, we apply our custom attribute ReadRequestBody
to the controller action ReadFromAttribute()
. Within the action, we inspect the request header ReadRequestBodyAttribute
and assign its content to the action response.
Conclusion
In this article, we have explored various methods to answer how to read the request body in an ASP.NET Core Web API application. Retrieving the request body by reading in the controller actions offers simplicity and control for basic scenarios. This approach is the most commonly used in .Net Core Web API projects.
Custom middleware can be used when we want extensive global interception abilities. This can allow us to log the request body inside only one place of the Web API endpoints. Also, custom middleware allows easy manipulation of both requests and responses in one place.
Action filters are a good candidate to encapsulate logic, enhancing the clarity and focus of controller actions. By using action filters, we abstract the intricacies of handling the request body. This allows the controller action to maintain a cleaner and more dedicated focus on its primary purpose.
In the realm of reading request bodies, we leverage custom attributes for specialized, declarative handling. This empowers us with precise control over the processing of request bodies. The suitability of each approach depends on the specific requirements of our application, spanning from fundamental control to the demand for encapsulation and specialization.