From 8b19d25629506d4c2c2e6e7a118cacbbe6a1d664 Mon Sep 17 00:00:00 2001 From: campersau Date: Thu, 3 Sep 2026 08:22:33 +0000 Subject: [PATCH] Add generic ConsumesAttribute Adds a generic ConsumesAttribute, mirroring the existing ProducesResponseTypeAttribute pattern, so the request type can be specified with compile-time type safety instead of via [Consumes(typeof(T), ...)]. Existing tests that exercised ConsumesAttribute with an explicit request type (EndpointMetadataApiDescriptionProviderTest, OpenApiDocumentServiceTests, OpenApiGeneratorTests) are updated to use the new Consumes syntax, confirming the generic form behaves the same as the non-generic one. Fixes #67701 Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01G6qhxdfagHUri2p6icypbT --- ...pointMetadataApiDescriptionProviderTest.cs | 4 ++-- src/Mvc/Mvc.Core/src/ConsumesOfTAttribute.cs | 19 +++++++++++++++++++ src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt | 2 ++ ...OpenApiDocumentServiceTests.RequestBody.cs | 8 ++++---- .../Services/OpenApiGeneratorTests.cs | 4 ++-- 5 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 src/Mvc/Mvc.Core/src/ConsumesOfTAttribute.cs diff --git a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs index cefce1b810bd..1114fb4f1bb2 100644 --- a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs +++ b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs @@ -104,7 +104,7 @@ public void AddsMultipleRequestFormatsFromMetadata() public void AddsMultipleRequestFormatsFromMetadataWithRequestTypeAndOptionalBodyParameter() { var apiDescription = GetApiDescription( - [Consumes(typeof(InferredJsonClass), "application/custom0", "application/custom1", IsOptional = true)] + [Consumes("application/custom0", "application/custom1", IsOptional = true)] () => { }); @@ -119,7 +119,7 @@ public void AddsMultipleRequestFormatsFromMetadataWithRequestTypeAndOptionalBody public void AddsMultipleRequestFormatsFromMetadataWithRequiredBodyParameter() { var apiDescription = GetApiDescription( - [Consumes(typeof(InferredJsonClass), "application/custom0", "application/custom1", IsOptional = false)] + [Consumes("application/custom0", "application/custom1", IsOptional = false)] (InferredJsonClass fromBody) => { }, httpMethods: ["POST"]); diff --git a/src/Mvc/Mvc.Core/src/ConsumesOfTAttribute.cs b/src/Mvc/Mvc.Core/src/ConsumesOfTAttribute.cs new file mode 100644 index 000000000000..fc7645897023 --- /dev/null +++ b/src/Mvc/Mvc.Core/src/ConsumesOfTAttribute.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Mvc; + +/// +/// The of object that is going to be read from the request. +public class ConsumesAttribute : ConsumesAttribute +{ + /// + /// Creates a new instance of . + /// + /// The request content type. + /// The additional list of allowed request content types. + public ConsumesAttribute(string contentType, params string[] otherContentTypes) + : base(typeof(T), contentType, otherContentTypes) + { + } +} diff --git a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt index aff0c7c0f51c..189cd2dc8e90 100644 --- a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt @@ -16,3 +16,5 @@ *REMOVED*static Microsoft.Extensions.DependencyInjection.MvcCoreMvcBuilderExtensions.SetCompatibilityVersion(this Microsoft.Extensions.DependencyInjection.IMvcBuilder! builder, Microsoft.AspNetCore.Mvc.CompatibilityVersion version) -> Microsoft.Extensions.DependencyInjection.IMvcBuilder! *REMOVED*static Microsoft.Extensions.DependencyInjection.MvcCoreMvcCoreBuilderExtensions.SetCompatibilityVersion(this Microsoft.Extensions.DependencyInjection.IMvcCoreBuilder! builder, Microsoft.AspNetCore.Mvc.CompatibilityVersion version) -> Microsoft.Extensions.DependencyInjection.IMvcCoreBuilder! *REMOVED*virtual Microsoft.AspNetCore.Mvc.Infrastructure.ConfigureCompatibilityOptions.PostConfigure(string? name, TOptions! options) -> void +Microsoft.AspNetCore.Mvc.ConsumesAttribute +Microsoft.AspNetCore.Mvc.ConsumesAttribute.ConsumesAttribute(string! contentType, params string![]! otherContentTypes) -> void diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.RequestBody.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.RequestBody.cs index b642b9f49638..56f368e89d7b 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.RequestBody.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.RequestBody.cs @@ -241,7 +241,7 @@ public async Task GetRequestBody_IFormFileHandlesConsumesAttribute() var builder = CreateBuilder(); // Act - builder.MapPost("/", [Consumes(typeof(IFormFile), "application/magic-foo-content-type")] (IFormFile formFile) => { }); + builder.MapPost("/", [Consumes("application/magic-foo-content-type")] (IFormFile formFile) => { }); // Assert await VerifyOpenApiDocument(builder, document => @@ -364,7 +364,7 @@ public async Task GetRequestBody_HandlesJsonBodyWithConsumesAttribute() var builder = CreateBuilder(); // Act - builder.MapPost("/", [Consumes(typeof(string), "application/magic-foo-content-type")] (string name) => { }); + builder.MapPost("/", [Consumes("application/magic-foo-content-type")] (string name) => { }); // Assert await VerifyOpenApiDocument(builder, document => @@ -1245,7 +1245,7 @@ public async Task GetRequestBody_HandlesJsonPatchBodyWithConsumesAttribute() var builder = CreateBuilder(); // Act - builder.MapPatch("/", [Consumes(typeof(JsonPatchDocument), "application/vnd.github.patch+json")] (JsonPatchDocument patch) => { }); + builder.MapPatch("/", [Consumes("application/vnd.github.patch+json")] (JsonPatchDocument patch) => { }); // Assert await VerifyOpenApiDocument(builder, document => @@ -1375,7 +1375,7 @@ public async Task GetRequestBody_HandlesGenericJsonPatchBodyWithConsumesAttribut var builder = CreateBuilder(); // Act - builder.MapPatch("/", [Consumes(typeof(JsonPatchDocument), "application/vnd.github.patch+json")] (JsonPatchDocument patch) => { }); + builder.MapPatch("/", [Consumes>("application/vnd.github.patch+json")] (JsonPatchDocument patch) => { }); // Assert await VerifyOpenApiDocument(builder, document => diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiGeneratorTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiGeneratorTests.cs index 0dc6f3f6e53d..78775b7870ab 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiGeneratorTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiGeneratorTests.cs @@ -113,7 +113,7 @@ public void AddsMultipleRequestFormatsFromMetadata() public void AddsMultipleRequestFormatsFromMetadataWithRequestTypeAndOptionalBodyParameter() { var operation = GetOpenApiOperation( - [Consumes(typeof(InferredJsonClass), "application/custom0", "application/custom1", IsOptional = true)] () => { }); + [Consumes("application/custom0", "application/custom1", IsOptional = true)] () => { }); var request = operation.RequestBody; Assert.NotNull(request); Assert.Equal(2, request.Content.Count); @@ -131,7 +131,7 @@ public void AddsMultipleRequestFormatsFromMetadataWithRequestTypeAndOptionalBody public void AddsMultipleRequestFormatsFromMetadataWithRequiredBodyParameter() { var operation = GetOpenApiOperation( - [Consumes(typeof(InferredJsonClass), "application/custom0", "application/custom1", IsOptional = false)] (InferredJsonClass fromBody) => { }); + [Consumes("application/custom0", "application/custom1", IsOptional = false)] (InferredJsonClass fromBody) => { }); var request = operation.RequestBody; Assert.NotNull(request);