From 8f7663435f5b5d77af083f151f3d79186585125a Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Mon, 14 Sep 2026 15:52:17 +0200 Subject: [PATCH] Reading request body: retarget net10.0, move to the built-in OpenAPI generator - ReadingRequestBody and Tests move from net8.0 to net10.0 with nullable enabled - Swashbuckle 6.5.0 removed; Microsoft.AspNetCore.OpenApi 10.0.12 with AddOpenApi() and MapOpenApi(), and the raw-text operation filter ported to IOpenApiOperationTransformer; no UI package - RequestBodyMiddleware takes ILogger, so the AddSingleton(typeof(ILogger), ...) workaround in Program.cs is gone - The size guard checks Request.ContentLength before the body is read, and EnableBuffering gets a bufferThreshold and a bufferLimit - Path checks use Contains with StringComparison.OrdinalIgnoreCase on a null-safe Path.Value; PersonItemDto is a record; test packages bumped --- .../Controllers/HomeController.cs | 14 ++++---- .../Models/PersonItemDto.cs | 11 ++---- .../OpenApiUtils/RawTextRequestAttribute.cs | 12 +++++++ .../RawTextRequestOperationTransformer.cs | 30 ++++++++++++++++ .../ReadingRequestBody/Program.cs | 13 +++---- .../ReadingRequestBody.csproj | 9 ++--- .../RawTextRequestOperationFilter.cs | 21 ----------- .../SwaggerEnableRawTextAttribute.cs | 12 ------- .../Utils/RequestBodyActionFilter.cs | 6 ++-- .../Utils/RequestBodyMiddleware.cs | 35 ++++++++----------- .../Tests/HomeControllerTests.cs | 18 +++++----- .../ReadingRequestBody/Tests/Tests.csproj | 21 +++++------ 12 files changed, 98 insertions(+), 104 deletions(-) create mode 100644 aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/OpenApiUtils/RawTextRequestAttribute.cs create mode 100644 aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/OpenApiUtils/RawTextRequestOperationTransformer.cs delete mode 100644 aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/SwaggerUtils/RawTextRequestOperationFilter.cs delete mode 100644 aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/SwaggerUtils/SwaggerEnableRawTextAttribute.cs diff --git a/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/Controllers/HomeController.cs b/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/Controllers/HomeController.cs index cb8be9e9ba..e86503865c 100644 --- a/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/Controllers/HomeController.cs +++ b/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/Controllers/HomeController.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Mvc; using ReadingRequestBody.Models; -using ReadingRequestBody.SwaggerUtils; +using ReadingRequestBody.OpenApiUtils; using ReadingRequestBody.Utils; namespace ReadingRequestBody.Controllers; @@ -15,7 +15,7 @@ public IActionResult Index() return Ok("Web API is ready."); } - [SwaggerEnableRawText] + [RawTextRequest] [HttpPost("read-as-string")] public async Task ReadAsString() { @@ -24,7 +24,7 @@ public async Task ReadAsString() return Ok($"Request Body As String: {requestBody}"); } - [SwaggerEnableRawText] + [RawTextRequest] [HttpPost("read-as-string-multiple")] public async Task ReadAsStringMultiple() { @@ -34,7 +34,7 @@ public async Task ReadAsStringMultiple() return Ok($"First: {requestBody}, Second:{requestBodySecond}"); } - [SwaggerEnableRawText] + [RawTextRequest] [HttpPost("read-multiple-enable-buffering")] public async Task ReadMultipleEnableBuffering() { @@ -55,7 +55,7 @@ public IActionResult ReadFromBody([FromBody] PersonItemDto model) return Ok(message); } - [SwaggerEnableRawText] + [RawTextRequest] [HttpPost("read-from-attribute")] [ReadRequestBody] public IActionResult ReadFromAttribute() @@ -66,7 +66,7 @@ public IActionResult ReadFromAttribute() return Ok(message); } - [SwaggerEnableRawText] + [RawTextRequest] [HttpPost("read-from-action-filter")] public IActionResult ReadFromActionFilter() { @@ -76,7 +76,7 @@ public IActionResult ReadFromActionFilter() return Ok(message); } - [SwaggerEnableRawText] + [RawTextRequest] [HttpPost("read-from-middleware")] public IActionResult ReadFromMiddleware() { diff --git a/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/Models/PersonItemDto.cs b/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/Models/PersonItemDto.cs index 7a2fb47701..502671598e 100644 --- a/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/Models/PersonItemDto.cs +++ b/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/Models/PersonItemDto.cs @@ -1,8 +1,3 @@ -namespace ReadingRequestBody.Models -{ - public class PersonItemDto - { - public string Name { get; set; } - public int Age { get; set; } - } -} +namespace ReadingRequestBody.Models; + +public record PersonItemDto(string Name, int Age); diff --git a/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/OpenApiUtils/RawTextRequestAttribute.cs b/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/OpenApiUtils/RawTextRequestAttribute.cs new file mode 100644 index 0000000000..90f09daf35 --- /dev/null +++ b/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/OpenApiUtils/RawTextRequestAttribute.cs @@ -0,0 +1,12 @@ +namespace ReadingRequestBody.OpenApiUtils; + +[AttributeUsage(AttributeTargets.Method)] +public class RawTextRequestAttribute : Attribute +{ + public RawTextRequestAttribute() + { + MediaType = "text/plain"; + } + + public string MediaType { get; set; } +} diff --git a/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/OpenApiUtils/RawTextRequestOperationTransformer.cs b/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/OpenApiUtils/RawTextRequestOperationTransformer.cs new file mode 100644 index 0000000000..810858d715 --- /dev/null +++ b/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/OpenApiUtils/RawTextRequestOperationTransformer.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.OpenApi; +using Microsoft.OpenApi; + +namespace ReadingRequestBody.OpenApiUtils; + +public class RawTextRequestOperationTransformer : IOpenApiOperationTransformer +{ + public Task TransformAsync(OpenApiOperation operation, + OpenApiOperationTransformerContext context, + CancellationToken cancellationToken) + { + if (context.Description.ActionDescriptor.EndpointMetadata + .OfType() + .SingleOrDefault() is RawTextRequestAttribute rawTextRequestAttribute) + { + operation.RequestBody = new OpenApiRequestBody + { + Content = new Dictionary + { + [rawTextRequestAttribute.MediaType] = new OpenApiMediaType + { + Schema = new OpenApiSchema { Type = JsonSchemaType.String } + } + } + }; + } + + return Task.CompletedTask; + } +} diff --git a/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/Program.cs b/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/Program.cs index bf2a24f5ff..0ae53f11c5 100644 --- a/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/Program.cs +++ b/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/Program.cs @@ -1,28 +1,23 @@ -using Microsoft.OpenApi.Models; -using ReadingRequestBody.SwaggerUtils; +using ReadingRequestBody.OpenApiUtils; using ReadingRequestBody.Utils; var builder = WebApplication.CreateBuilder(args); -builder.Services.AddSingleton(typeof(ILogger), typeof(Logger)); builder.Services.AddControllers(options => { options.Filters.Add(); }); -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(c => +builder.Services.AddOpenApi(options => { - c.SwaggerDoc("v1", new OpenApiInfo { Title = "Reading Request Body", Version = "v1" }); - c.OperationFilter(); + options.AddOperationTransformer(); }); var app = builder.Build(); if (app.Environment.IsDevelopment()) { - app.UseSwagger(); - app.UseSwaggerUI(); + app.MapOpenApi(); } app.UseMiddleware(); diff --git a/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/ReadingRequestBody.csproj b/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/ReadingRequestBody.csproj index cd03129d6c..ffc01a1b00 100644 --- a/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/ReadingRequestBody.csproj +++ b/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/ReadingRequestBody.csproj @@ -1,9 +1,10 @@ - + - net8.0 + net10.0 enable + enable - + - \ No newline at end of file + diff --git a/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/SwaggerUtils/RawTextRequestOperationFilter.cs b/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/SwaggerUtils/RawTextRequestOperationFilter.cs deleted file mode 100644 index 2caee6bc02..0000000000 --- a/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/SwaggerUtils/RawTextRequestOperationFilter.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Microsoft.OpenApi.Models; -using Swashbuckle.AspNetCore.SwaggerGen; - -namespace ReadingRequestBody.SwaggerUtils; -public class RawTextRequestOperationFilter : IOperationFilter -{ - public void Apply(OpenApiOperation operation, OperationFilterContext context) - { - if (context.MethodInfo.GetCustomAttributes(true).SingleOrDefault((attribute) => attribute is SwaggerEnableRawTextAttribute) is SwaggerEnableRawTextAttribute rawTextRequestAttribute) - { - operation.RequestBody = new OpenApiRequestBody(); - operation.RequestBody.Content.Add(rawTextRequestAttribute.MediaType, new OpenApiMediaType() - { - Schema = new OpenApiSchema() - { - Type = "string" - } - }); - } - } -} diff --git a/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/SwaggerUtils/SwaggerEnableRawTextAttribute.cs b/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/SwaggerUtils/SwaggerEnableRawTextAttribute.cs deleted file mode 100644 index 3d987134e6..0000000000 --- a/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/SwaggerUtils/SwaggerEnableRawTextAttribute.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace ReadingRequestBody.SwaggerUtils; - -[AttributeUsage(AttributeTargets.Method)] -public class SwaggerEnableRawTextAttribute : Attribute -{ - public SwaggerEnableRawTextAttribute() - { - MediaType = "text/plain"; - } - - public string MediaType { get; set; } -} diff --git a/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/Utils/RequestBodyActionFilter.cs b/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/Utils/RequestBodyActionFilter.cs index 96b05e2038..af3e59d2fd 100644 --- a/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/Utils/RequestBodyActionFilter.cs +++ b/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/Utils/RequestBodyActionFilter.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Filters; namespace ReadingRequestBody.Utils; @@ -6,9 +6,9 @@ public class ReadRequestBodyActionFilter : IAsyncActionFilter { public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { - var requestPath = context.HttpContext.Request.Path.Value; + var requestPath = context.HttpContext.Request.Path.Value ?? string.Empty; - if (requestPath.IndexOf("read-from-action-filter") > -1) + if (requestPath.Contains("read-from-action-filter", StringComparison.OrdinalIgnoreCase)) { var requestBody = await context.HttpContext.Request.Body.ReadAsStringAsync(); context.HttpContext.Request.Headers.Append("ReadRequestBodyActionFilter", requestBody); diff --git a/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/Utils/RequestBodyMiddleware.cs b/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/Utils/RequestBodyMiddleware.cs index 1340d6eae9..2f92b3093b 100644 --- a/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/Utils/RequestBodyMiddleware.cs +++ b/aspnetcore-webapi/ReadingRequestBody/ReadingRequestBody/Utils/RequestBodyMiddleware.cs @@ -1,39 +1,32 @@ -namespace ReadingRequestBody.Utils; +namespace ReadingRequestBody.Utils; -public class RequestBodyMiddleware +public class RequestBodyMiddleware(RequestDelegate next, ILogger logger) { - private readonly RequestDelegate _next; - private readonly ILogger _logger; - private readonly int MaxContentLength = 1024; - - public RequestBodyMiddleware(RequestDelegate next, ILogger logger) - { - _next = next; - _logger = logger; - } + private const int MaxContentLength = 1024; public async Task Invoke(HttpContext context) { - var requestPath = context.Request.Path.Value; + var requestPath = context.Request.Path.Value ?? string.Empty; - if (requestPath.IndexOf("read-from-middleware") > -1) + if (requestPath.Contains("read-from-middleware", StringComparison.OrdinalIgnoreCase)) { - context.Request.EnableBuffering(); - var requestBody = await context.Request.Body.ReadAsStringAsync(true); - - if (requestBody.Length > MaxContentLength) + if (context.Request.ContentLength > MaxContentLength) { - context.Response.StatusCode = 413; + context.Response.StatusCode = StatusCodes.Status413PayloadTooLarge; await context.Response.WriteAsync("Request Body Too Large"); + return; } - _logger.LogInformation("Request Body:{@requestBody}", requestBody); + context.Request.EnableBuffering(bufferThreshold: MaxContentLength, bufferLimit: MaxContentLength); + var requestBody = await context.Request.Body.ReadAsStringAsync(true); + + logger.LogInformation("Request Body:{@requestBody}", requestBody); context.Request.Headers.Append("RequestBodyMiddleware", requestBody); context.Items.Add("RequestBody", requestBody); context.Request.Body.Position = 0; } - await _next(context); + await next(context); } -} \ No newline at end of file +} diff --git a/aspnetcore-webapi/ReadingRequestBody/Tests/HomeControllerTests.cs b/aspnetcore-webapi/ReadingRequestBody/Tests/HomeControllerTests.cs index 79103297ed..04c4617f17 100644 --- a/aspnetcore-webapi/ReadingRequestBody/Tests/HomeControllerTests.cs +++ b/aspnetcore-webapi/ReadingRequestBody/Tests/HomeControllerTests.cs @@ -17,14 +17,14 @@ namespace Tests [TestClass] public class HomeControllerTests { - private IFixture _fixture; - private Mock _loggerMock; + private IFixture _fixture = null!; + private Mock> _loggerMock = null!; [TestInitialize] public void Setup() { _fixture = new Fixture(); - _loggerMock = new Mock(); + _loggerMock = new Mock>(); } [TestMethod] @@ -34,7 +34,7 @@ public void WhenIndexActionCalled_ThenResponseMustBeReturn() var result = controller.Index(); Assert.AreEqual(typeof(OkObjectResult), result.GetType()); - Assert.AreEqual((result as OkObjectResult).Value, "Web API is ready."); + Assert.AreEqual("Web API is ready.", (result as OkObjectResult)!.Value); } [TestMethod] @@ -71,11 +71,11 @@ public async Task WhenReadFromAttributeActionCalled_ThenResponseMustBeReturn() ActionDescriptor = controller.ControllerContext.ActionDescriptor }, new List(), - new Dictionary(), + new Dictionary(), controller); var attribute = new ReadRequestBodyAttribute(); - await attribute.OnActionExecutionAsync(context, () => Task.FromResult(null)); + await attribute.OnActionExecutionAsync(context, () => Task.FromResult(null!)); var result = controller.ReadFromAttribute(); @@ -138,7 +138,7 @@ public async Task WhenReadFromActionFilterActionCalled_ThenResponseMustBeReturn( new RouteData(), new ActionDescriptor(), new ModelStateDictionary()); - var actionExecutingContext = new ActionExecutingContext(actionContext, new List(), new Dictionary(), controller: controller); + var actionExecutingContext = new ActionExecutingContext(actionContext, new List(), new Dictionary(), controller: controller); Task next() { @@ -156,11 +156,11 @@ Task next() private static void TestRequest(IActionResult result, string responsePrefix, string bodyString) { - var resultValue = result != null ? (result as OkObjectResult).Value : string.Empty; + var resultValue = result != null ? (result as OkObjectResult)!.Value : string.Empty; Assert.IsNotNull(result); Assert.AreEqual(typeof(OkObjectResult), result.GetType()); - Assert.AreEqual(resultValue, $"{responsePrefix} {bodyString}"); + Assert.AreEqual($"{responsePrefix} {bodyString}", resultValue); } private static HomeController GetControllerInstance(string bodyString) diff --git a/aspnetcore-webapi/ReadingRequestBody/Tests/Tests.csproj b/aspnetcore-webapi/ReadingRequestBody/Tests/Tests.csproj index 7552c3f7ee..65e7e0132d 100644 --- a/aspnetcore-webapi/ReadingRequestBody/Tests/Tests.csproj +++ b/aspnetcore-webapi/ReadingRequestBody/Tests/Tests.csproj @@ -1,19 +1,20 @@ - + - net8.0 + net10.0 enable + enable false - - - - - - - + + + + + + + - \ No newline at end of file +