Repository: dotnet/aspnetcore
File: src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs (C#)

Code before:

        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.
            string? matchingDescription = null;
            foreach (var metadata in responseMetadataTypes)
            {
                if (metadata?.StatusCode == apiResponseType?.StatusCode &&
                    TypesAreCompatible(apiResponseType?.Type, metadata?.Type) &&
                    metadata?.Description is not null)
                {
                    matchingDescription = metadata.Description;
                }
            }
            return matchingDescription;
        }

        static bool TypesAreCompatible(Type? apiResponseType, Type? metadaType)
        {
            // We need to a special check for cases where the inferred type is different than the one specified in attributes.
            // For example, an endpoint that defines [ProducesResponseType<IEnumerable<WeatherForecast>>],
            // but the endpoint returns weatherForecasts.ToList(). Because List<> is a different type than IEnumerable<>, it would incorrectly return false.
            // Currently, we do a "simple" bidirectional check to see if the types are assignable to each other.
            // This isn't very thorough, but it works for most cases.
            // For more information, check the related bug: https://github.com/dotnet/aspnetcore/issues/60518
            return apiResponseType == metadaType ||
                metadaType?.IsAssignableFrom(apiResponseType) == true ||
                apiResponseType?.IsAssignableFrom(metadaType) == true;
        }
    }

    private static ApiResponseType CreateDefaultApiResponseType(Type responseType)
    {
        var apiResponseType = new ApiResponseType
        {
            ModelMetadata = CreateModelMetadata(responseType),
            StatusCode = 200,
            Type = responseType,
        };

        if (CreateDefaultApiResponseFormat(responseType) is { } responseFormat)
        {
            apiResponseType.ApiResponseFormats.Add(responseFormat);
        }

        return apiResponseType;
    }

    private static ApiResponseFormat? CreateDefaultApiResponseFormat(Type responseType)
    {
        if (responseType == typeof(void))

Review comment (bug):
## Potential Downsides of Bidirectional Type Compatibility

While the bidirectional approach (`Type.IsAssignableFrom()` in both directions) solves the immediate problem, there are some trade-offs to consider:

### 1. Over-Matching Risk
The bidirectional check could match unintended type relationships:

```csharp
[ProducesResponseType<object>(200, Description = "Any object")]
() => TypedResults.Ok("specific string")
```

Since object.IsAssignableFrom(string) is true, this would incorrectly match and use the generic "Any object" description instead of inferring a more specific string response description.


### 2. Ambiguous Inheritance Scenarios
With complex inheritance hierarchies, the matching might be too permissive:

```
[ProducesResponseType<BaseClass>(200, Description = "Base description")]
[ProducesResponseType<DerivedClass>(200, Description = "Derived description")]  
() => TypedResults.Ok(new DerivedClass())
```

## Alternative:  Unidirectional Approach

A more conservative approach would only check if the attribute type is assignable from the inferred type:

```
// Only allow: attribute type ← inferred type (inheritance/interface direction)
if (attributeType.IsAssignableFrom(responseType.Type))
```

This would:

- ✅ Handle IEnumerable<T> ← List<T> (the main use case)
- ✅ Handle BaseClass ← DerivedClass
- ❌ Prevent List<T> ← IEnumerable<T> (less common scenario)
- ✅ Avoid most over-matching issues

@captainsafia @halter73 
Should we switch to the more conservative unidirectional approach, or are the current bidirectional benefits worth the potential edge case risks?

The bidirectional approach handles more scenarios but could be "too smart" in some cases.


Code after:

        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.
            string? matchingDescription = null;
            foreach (var metadata in responseMetadataTypes)
            {
                if (metadata.StatusCode == apiResponseType.StatusCode &&
                    TypesAreCompatible(apiResponseType.Type, metadata.Type) &&
                    metadata.Description is not null)
                {
                    matchingDescription = metadata.Description;
                }
            }
            return matchingDescription;
        }

        static bool TypesAreCompatible(Type? apiResponseType, Type? metadataType)
        {
            // We need to a special check for cases where the inferred type is different than the one specified in attributes.
            // For example, an endpoint that defines [ProducesResponseType<IEnumerable<WeatherForecast>>],
            // but the endpoint returns weatherForecasts.ToList(). Because List<> is a different type than IEnumerable<>, it would incorrectly set OpenAPI metadata incorrectly.
            // We use a conservative unidirectional check where the attribute type must be assignable from the inferred type.
            // This handles inheritance (BaseClass ← DerivedClass) and interface implementation (IEnumerable<T> ← List<T>).
            // This should be sufficient, as it's more common to specify an interface or base class type in the attribute and a concrete type in the endpoint implementation,
            // compared to doing the opposite.
            // For more information, check the related bug: https://github.com/dotnet/aspnetcore/issues/60518
            return apiResponseType == metadataType ||
                metadataType?.IsAssignableFrom(apiResponseType) == true;
        }
    }

    private static ApiResponseType CreateDefaultApiResponseType(Type responseType)
    {
        var apiResponseType = new ApiResponseType
        {
            ModelMetadata = CreateModelMetadata(responseType),
            StatusCode = 200,
            Type = responseType,
        };

        if (CreateDefaultApiResponseFormat(responseType) is { } responseFormat)
        {
            apiResponseType.ApiResponseFormats.Add(responseFormat);
        }

        return apiResponseType;
    }

    private static ApiResponseFormat? CreateDefaultApiResponseFormat(Type responseType)
    {