Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
0f7cefb
api explorer support
DeagleGross Mar 3, 2026
c97f70b
api explorer
DeagleGross Mar 3, 2026
7b4f6aa
mvc support
DeagleGross Mar 4, 2026
dc11280
openapi oneof
DeagleGross Mar 4, 2026
c90e66d
Merge branch 'main' into dmkorolev/multiple-produces
DeagleGross Mar 4, 2026
bf1ddeb
rewrite filters
DeagleGross Mar 4, 2026
1de48e6
rewrite .verified
DeagleGross Mar 4, 2026
45c060a
fix invariant verification
DeagleGross Mar 5, 2026
4bd7c48
fix different scope overrides
DeagleGross Mar 5, 2026
2dc09bd
wip
DeagleGross Mar 6, 2026
8ddc9fd
Merge branch 'main' into dmkorolev/multiple-produces
DeagleGross Mar 6, 2026
c49578c
add description to mergine of formats
DeagleGross Mar 6, 2026
b84fec5
renaming
DeagleGross Mar 6, 2026
1a9b091
simplify
DeagleGross Mar 6, 2026
275fa44
rework tests to use Assert.Collection without ordering (ensure order …
DeagleGross Mar 6, 2026
abed03f
properly validate inferred types against excplitly defined ones
DeagleGross Mar 6, 2026
e99727c
tests
DeagleGross Mar 6, 2026
cf570db
elaborate on inferred types
DeagleGross Mar 9, 2026
0872027
simplify
DeagleGross Mar 12, 2026
6179e23
tests for controllers
DeagleGross Mar 12, 2026
6135446
minimal api tests
DeagleGross Mar 12, 2026
3186309
nit
DeagleGross Mar 12, 2026
f28fd3e
tests on Results.Ok and TypedResults.OK()
DeagleGross Mar 12, 2026
cb64b6d
nit
DeagleGross Mar 12, 2026
7436ce4
add explanation for filtering non-ApiResponseMetadataProviders
DeagleGross Mar 12, 2026
9b0920c
update .verified to include inferred types
DeagleGross Mar 12, 2026
9c69c14
include hiding inferred-type scenario
DeagleGross Mar 12, 2026
85d43c3
tests on groups
DeagleGross Mar 18, 2026
002fd0f
comments 1
DeagleGross Mar 18, 2026
3e76276
nit 2
DeagleGross Mar 18, 2026
98675d1
change to anyOf
DeagleGross Mar 27, 2026
5c621ce
merge main (tmp)
DeagleGross May 4, 2026
e748258
fix deterministic ordering in api explorer
DeagleGross May 4, 2026
8c0a65b
regen openapi integration tests
DeagleGross May 4, 2026
e740fc1
and fix name of route
DeagleGross May 4, 2026
3ba070b
add Debug.Assert to apiresponsetypeprovider scope processing
DeagleGross May 4, 2026
d9854e0
better in-place doc
DeagleGross May 4, 2026
7f8e8b9
fix naming of mvc-controller oneOf->anyOf
DeagleGross May 4, 2026
4daa5a6
explanations + test for corner case
DeagleGross May 4, 2026
af004d8
make it readonly
DeagleGross May 4, 2026
dbd3ad9
merge descriptions
DeagleGross May 13, 2026
f254424
Merge remote-tracking branch 'upstream/main' into dmkorolev/multiple-…
DeagleGross May 13, 2026
3dec050
fix test validating merge
DeagleGross May 13, 2026
b06df29
add duplicate description case for verification
DeagleGross May 14, 2026
9ea2818
fix nit formatting
DeagleGross May 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// 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.ApiExplorer;

internal readonly struct ApiResponseMetadataProviderWithScope(IApiResponseMetadataProvider provider, int scope)
{
public IApiResponseMetadataProvider Provider { get; } = provider;
public int Scope { get; } = scope;
}
224 changes: 169 additions & 55 deletions src/Mvc/Mvc.ApiExplorer/src/ApiResponseTypeProvider.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -340,13 +340,27 @@ private static void AddSupportedResponseTypes(
responseType = typeof(void);
}

var responseProviderMetadataTypes = ApiResponseTypeProvider.ReadResponseMetadata(
var responseProviderMetadataTypes = ApiResponseTypeProvider.ReadAttributeResponseMetadata(
responseProviderMetadata, responseType, defaultErrorType, contentTypes, out var errorSetByDefault);
var producesResponseMetadataTypes = ApiResponseTypeProvider.ReadResponseMetadata(producesResponseMetadata, responseType);
var producesResponseMetadataTypes = ApiResponseTypeProvider.ReadEndpointResponseMetadata(producesResponseMetadata, responseType);

// We favor types added via the extension methods (which implements IProducesResponseTypeMetadata)
// over those that are added via attributes.
var responseMetadataTypes = producesResponseMetadataTypes.Values.Concat(responseProviderMetadataTypes.Values);
// over those that are added via attributes (IApiResponseMetadataProvider).
//
// Note: TypedResults (e.g. TypedResults.Ok<Product>()) also add IProducesResponseTypeMetadata
// via IEndpointMetadataProvider, so they end up in the same bucket as .Produces<T>() and
// coexist for the same status code.
//
// Example:
// [ProducesResponseType(typeof(string), 200)] // attribute → IApiResponseMetadataProvider
// app.MapPost("/", () => TypedResults.Ok(new Product())) // TypedResults → IProducesResponseTypeMetadata (200, Product)
// .Produces<Customer>(200); // extension → IProducesResponseTypeMetadata (200, Customer)
//
// Result: (200, Product) and (200, Customer) both appear. The attribute (200, string) is
// dropped because status 200 is already claimed by IProducesResponseTypeMetadata entries.
var producesStatusCodes = producesResponseMetadataTypes.Values.Select(metadata => metadata.StatusCode).ToHashSet();
var responseMetadataTypes = producesResponseMetadataTypes.Values.Concat(
responseProviderMetadataTypes.Values.Where(metadata => !producesStatusCodes.Contains(metadata.StatusCode)));

if (responseMetadataTypes.Any())
{
Expand Down Expand Up @@ -377,7 +391,10 @@ private static void AddSupportedResponseTypes(

apiResponseType.Description ??= GetMatchingResponseTypeDescription(responseProviderMetadataTypes.Values, apiResponseType);

if (!supportedResponseTypes.Any(existingResponseType => existingResponseType.StatusCode == apiResponseType.StatusCode))
if (!supportedResponseTypes.Any(existingResponseType =>
existingResponseType.StatusCode == apiResponseType.StatusCode &&
existingResponseType.Type == apiResponseType.Type &&
existingResponseType.ApiResponseFormats.FirstOrDefault()?.MediaType == apiResponseType.ApiResponseFormats.FirstOrDefault()?.MediaType))
{
supportedResponseTypes.Add(apiResponseType);
}
Expand All @@ -398,6 +415,23 @@ private static void AddSupportedResponseTypes(
supportedResponseTypes.Add(defaultApiResponseType);
}

if (supportedResponseTypes.Count > 1)
{
// With multiple response types (e.g., different types for the same status code),
// we need deterministic ordering so that API documentation is stable across runs.
// This matches the ordering used by the controller path in ApiResponseTypeProvider.
var sorted = supportedResponseTypes
.OrderBy(rt => rt.StatusCode)
.ThenBy(rt => rt.Type?.Name)
.ThenBy(rt => rt.ApiResponseFormats.FirstOrDefault()?.MediaType)
.ToArray();
supportedResponseTypes.Clear();
foreach (var sortedResponseType in sorted)
{
supportedResponseTypes.Add(sortedResponseType);
}
}

static string? GetMatchingResponseTypeDescription(IEnumerable<ApiResponseType> responseMetadataTypes, ApiResponseType apiResponseType)
{
// We set the Description to the LAST non-null value we find that matches the status code.
Expand Down
Loading
Loading