As mentioned in the previous post, all the projects at work are in the process of upgrading to latest .NET Core 3+. In this post, I’ll be discussing another issue we come across during the upgrade.
One of the projects has a custom Authorize attribute where we need to read the request body. The custom attribute looks something like
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class RsaAuthorizeAttribute : AuthorizeAttribute, IAsyncActionFilter, IAuthorizeData
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
context.HttpContext.Request.EnableBuffering();
context.HttpContext.Request.Body.Position = 0;
string bodyContent = null;
using(var streamReader = new StreamReader(context.HttpContext.Request.Body))
{
bodyContent = streamReader.ReadToEnd();
}
Log.Information($"Body content:{bodyContent}");
context.HttpContext.Request.Body.Position = 0;
// ...More logic goes here
}
}
This code works fine before. After we upgrade this project to latest .NET Core 3+, we get the following error.
This seems pretty simple to fix as the error message tells us the solution. So we set AllowSynchronousIO to true in the Startup.cs file. This is a breaking change in .NET Core 3.0 which synchronous server operations are disabled by default. See full documentation here.
Yes, we could change ReadToEnd to ReadToEndAsync in the custom Authorize attribute and we did but this exception comes from the XmlSerializerInputFormatter so changing to an async method in the custom attribute does not fix this.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<KestrelServerOptions>(options => options.AllowSynchronousIO = true);
// ...More configurations go here
}
We no longer get the exception. However, the request body is now always empty. This is the updated code after the upgrade.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class RsaAuthorizeAttribute : AuthorizeAttribute, IAsyncActionFilter, IAuthorizeData
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
context.HttpContext.Request.EnableBuffering();
context.HttpContext.Request.Body.Position = 0;
using var streamReader = new StreamReader(context.HttpContext.Request.Body);
string bodyContent = await streamReader.ReadToEndAsync();
Log.Information($"Body content:{bodyContent}");
context.HttpContext.Request.Body.Position = 0;
// ...More logic goes here
}
}
After a lot of investigation and research, we find that EnableBuffering in the custom Authorize attribute maybe too late in the pipeline and that the request body might have been read by other middlewares. Apparently it worked in .NET Core 2 was unintentional based on this comment on a github issue. The solution is to enable buffering before invoking MVC. So we add a middleware at the start of the pipeline just to enable buffering and remove it from the Authorize attribute.
public void Configure(IApplicationBuilder app)
{
app.Use((context, next) =>
{
context.Request.EnableBuffering();
return next();
});
// ...More configurations go here
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class RsaAuthorizeAttribute : AuthorizeAttribute, IAsyncActionFilter, IAuthorizeData
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
context.HttpContext.Request.Body.Position = 0;
using var streamReader = new StreamReader(context.HttpContext.Request.Body);
string bodyContent = await streamReader.ReadToEndAsync();
Log.Information($"Body content:{bodyContent}");
context.HttpContext.Request.Body.Position = 0;
// ...More logic goes here
}
}
This solves the issue and we can read the request body content successfully.