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.

To download the source code for this article, you can visit our GitHub repository.

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.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

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.

For a deeper understanding of StreamReader and StreamWriter, we recommend consulting our article C# Back to Basics – Files, StreamWriter and StreamReader.

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:

Read request as a string

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.

To learn more about the action filters, please check out Implementing Action Filters in ASP.NET Core.

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.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!