Skip to content

Commit 6a32a96

Browse files
Copilotcaptainsafia
andcommitted
Implemented support for multiple Produces for the same status code
Co-authored-by: captainsafia <1857993+captainsafia@users.noreply.github.com>
1 parent 1d067f1 commit 6a32a96

File tree

3 files changed

+75
-12
lines changed

3 files changed

+75
-12
lines changed

src/Mvc/Mvc.ApiExplorer/src/ApiResponseTypeProvider.cs

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer;
1313

1414
internal sealed class ApiResponseTypeProvider
1515
{
16+
internal readonly record struct ResponseKey(
17+
int StatusCode,
18+
Type? DeclaredType,
19+
string? ContentType);
20+
1621
private readonly IModelMetadataProvider _modelMetadataProvider;
1722
private readonly IActionResultTypeMapper _mapper;
1823
private readonly MvcOptions _mvcOptions;
@@ -87,9 +92,7 @@ private ICollection<ApiResponseType> GetApiResponseTypes(
8792
responseTypeMetadataProviders,
8893
_modelMetadataProvider);
8994

90-
// Read response metadata from providers and
91-
// overwrite responseTypes from the metadata based
92-
// on the status code
95+
// Read response metadata from providers
9396
var responseTypesFromProvider = ReadResponseMetadata(
9497
responseMetadataAttributes,
9598
type,
@@ -98,6 +101,7 @@ private ICollection<ApiResponseType> GetApiResponseTypes(
98101
out var _,
99102
responseTypeMetadataProviders);
100103

104+
// Merge the response types
101105
foreach (var responseType in responseTypesFromProvider)
102106
{
103107
responseTypes[responseType.Key] = responseType.Value;
@@ -106,7 +110,11 @@ private ICollection<ApiResponseType> GetApiResponseTypes(
106110
// Set the default status only when no status has already been set explicitly
107111
if (responseTypes.Count == 0 && type != null)
108112
{
109-
responseTypes.Add(StatusCodes.Status200OK, new ApiResponseType
113+
var key = new ResponseKey(
114+
StatusCodes.Status200OK,
115+
type,
116+
null);
117+
responseTypes.Add(key, new ApiResponseType
110118
{
111119
StatusCode = StatusCodes.Status200OK,
112120
Type = type,
@@ -128,11 +136,16 @@ private ICollection<ApiResponseType> GetApiResponseTypes(
128136
CalculateResponseFormatForType(apiResponse, contentTypes, responseTypeMetadataProviders, _modelMetadataProvider);
129137
}
130138

131-
return responseTypes.Values;
139+
// Order the response types by status code, type name, and content type for consistent output
140+
return responseTypes.Values
141+
.OrderBy(r => r.StatusCode)
142+
.ThenBy(r => r.Type?.Name)
143+
.ThenBy(r => r.ApiResponseFormats.FirstOrDefault()?.MediaType)
144+
.ToList();
132145
}
133146

134147
// Shared with EndpointMetadataApiDescriptionProvider
135-
internal static Dictionary<int, ApiResponseType> ReadResponseMetadata(
148+
internal static Dictionary<ResponseKey, ApiResponseType> ReadResponseMetadata(
136149
IReadOnlyList<IApiResponseMetadataProvider> responseMetadataAttributes,
137150
Type? type,
138151
Type? defaultErrorType,
@@ -142,7 +155,7 @@ internal static Dictionary<int, ApiResponseType> ReadResponseMetadata(
142155
IModelMetadataProvider? modelMetadataProvider = null)
143156
{
144157
errorSetByDefault = false;
145-
var results = new Dictionary<int, ApiResponseType>();
158+
var results = new Dictionary<ResponseKey, ApiResponseType>();
146159

147160
// Get the content type that the action explicitly set to support.
148161
// Walk through all 'filter' attributes in order, and allow each one to see or override
@@ -213,21 +226,27 @@ internal static Dictionary<int, ApiResponseType> ReadResponseMetadata(
213226

214227
if (apiResponseType.Type != null)
215228
{
216-
results[apiResponseType.StatusCode] = apiResponseType;
229+
var mediaType = apiResponseType.ApiResponseFormats.FirstOrDefault()?.MediaType;
230+
var key = new ResponseKey(
231+
apiResponseType.StatusCode,
232+
apiResponseType.Type,
233+
mediaType);
234+
235+
results[key] = apiResponseType;
217236
}
218237
}
219238
}
220239

221240
return results;
222241
}
223242

224-
internal static Dictionary<int, ApiResponseType> ReadResponseMetadata(
243+
internal static Dictionary<ResponseKey, ApiResponseType> ReadResponseMetadata(
225244
IReadOnlyList<IProducesResponseTypeMetadata> responseMetadata,
226245
Type? type,
227246
IEnumerable<IApiResponseTypeMetadataProvider>? responseTypeMetadataProviders = null,
228247
IModelMetadataProvider? modelMetadataProvider = null)
229248
{
230-
var results = new Dictionary<int, ApiResponseType>();
249+
var results = new Dictionary<ResponseKey, ApiResponseType>();
231250

232251
foreach (var metadata in responseMetadata)
233252
{
@@ -269,7 +288,12 @@ internal static Dictionary<int, ApiResponseType> ReadResponseMetadata(
269288

270289
if (apiResponseType.Type != null)
271290
{
272-
results[apiResponseType.StatusCode] = apiResponseType;
291+
var mediaType = apiResponseType.ApiResponseFormats.FirstOrDefault()?.MediaType;
292+
var key = new ResponseKey(
293+
apiResponseType.StatusCode,
294+
apiResponseType.Type,
295+
mediaType);
296+
results[key] = apiResponseType;
273297
}
274298
}
275299

src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,12 @@ private static void AddSupportedResponseTypes(
346346

347347
// We favor types added via the extension methods (which implements IProducesResponseTypeMetadata)
348348
// over those that are added via attributes.
349-
var responseMetadataTypes = producesResponseMetadataTypes.Values.Concat(responseProviderMetadataTypes.Values);
349+
// Order the combined list of response types by status code for consistent output
350+
var responseMetadataTypes = producesResponseMetadataTypes.Values
351+
.Concat(responseProviderMetadataTypes.Values)
352+
.OrderBy(r => r.StatusCode)
353+
.ThenBy(r => r.Type?.Name)
354+
.ThenBy(r => r.ApiResponseFormats.FirstOrDefault()?.MediaType);
350355

351356
if (responseMetadataTypes.Any())
352357
{

src/Mvc/Mvc.ApiExplorer/test/ApiResponseTypeProviderTest.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,40 @@ public void GetApiResponseTypes_ReturnNoResponseTypes_IfActionWithIResultReturnT
823823
// Assert
824824
Assert.False(result.Any());
825825
}
826+
827+
[Fact]
828+
public void GetApiResponseTypes_HandlesMultipleResponseTypesWithSameStatusCodeButDifferentContentTypes()
829+
{
830+
// Arrange
831+
var actionDescriptor = GetControllerActionDescriptor(typeof(TestController), nameof(TestController.GetUser));
832+
actionDescriptor.FilterDescriptors.Add(new FilterDescriptor(new ProducesResponseTypeAttribute(typeof(BaseModel), 200, "application/json"), FilterScope.Action));
833+
actionDescriptor.FilterDescriptors.Add(new FilterDescriptor(new ProducesResponseTypeAttribute(typeof(string), 200, "text/html"), FilterScope.Action));
834+
835+
var provider = GetProvider();
836+
837+
// Act
838+
var result = provider.GetApiResponseTypes(actionDescriptor);
839+
840+
// Assert
841+
Assert.Equal(2, result.Count);
842+
843+
var orderedResults = result.OrderBy(r => r.ApiResponseFormats.FirstOrDefault()?.MediaType).ToList();
844+
845+
Assert.Collection(
846+
orderedResults,
847+
responseType =>
848+
{
849+
Assert.Equal(typeof(BaseModel), responseType.Type);
850+
Assert.Equal(200, responseType.StatusCode);
851+
Assert.Equal(new[] { "application/json" }, GetSortedMediaTypes(responseType));
852+
},
853+
responseType =>
854+
{
855+
Assert.Equal(typeof(string), responseType.Type);
856+
Assert.Equal(200, responseType.StatusCode);
857+
Assert.Equal(new[] { "text/html" }, GetSortedMediaTypes(responseType));
858+
});
859+
}
826860

827861
private static ApiResponseTypeProvider GetProvider()
828862
{

0 commit comments

Comments
 (0)