Skip to content

Commit 43b5fbf

Browse files
authored
Fixed the issue. Will be adding the tests as a separate commit. (#60096)
# Include typed result metadata in the action descriptors for MVC Controller actions ## Description The endpoint metadata wasn't being captured in ActionModel during the ApplicationModel creation. This is addressed by the change in the DefaultApplicationModelProvider class. The metadata is now being extracted based on the return type of the action and then the selectors associated with the action are populated with that metadata, as that's what is being used later on for populating the ActionDescriptor's EndpointMetadata. Fixes #44988
1 parent f5ce3f3 commit 43b5fbf

File tree

12 files changed

+207
-9
lines changed

12 files changed

+207
-9
lines changed

src/Mvc/Mvc.Core/src/ApplicationModels/DefaultApplicationModelProvider.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Linq;
55
using System.Reflection;
66
using Microsoft.AspNetCore.Http;
7+
using Microsoft.AspNetCore.Http.Metadata;
78
using Microsoft.AspNetCore.Mvc.ActionConstraints;
89
using Microsoft.AspNetCore.Mvc.ApiExplorer;
910
using Microsoft.AspNetCore.Mvc.Filters;
@@ -349,9 +350,41 @@ internal PropertyModel CreatePropertyModel(PropertyInfo propertyInfo)
349350
applicableAttributes.AddRange(routeAttributes);
350351
AddRange(actionModel.Selectors, CreateSelectors(applicableAttributes));
351352

353+
AddReturnTypeMetadata(actionModel.Selectors, methodInfo);
354+
352355
return actionModel;
353356
}
354357

358+
internal static void AddReturnTypeMetadata(IList<SelectorModel> selectors, MethodInfo methodInfo)
359+
{
360+
// Get metadata from return type
361+
var returnType = methodInfo.ReturnType;
362+
if (CoercedAwaitableInfo.IsTypeAwaitable(returnType, out var coercedAwaitableInfo))
363+
{
364+
returnType = coercedAwaitableInfo.AwaitableInfo.ResultType;
365+
}
366+
367+
if (returnType is not null && typeof(IEndpointMetadataProvider).IsAssignableFrom(returnType))
368+
{
369+
// Return type implements IEndpointMetadataProvider
370+
var builder = new InertEndpointBuilder();
371+
var invokeArgs = new object[2];
372+
invokeArgs[0] = methodInfo;
373+
invokeArgs[1] = builder;
374+
EndpointMetadataPopulator.PopulateMetadataForEndpointMethod.MakeGenericMethod(returnType).Invoke(null, invokeArgs);
375+
376+
// The metadata is added to the builder's metadata collection.
377+
// We need to populate the selectors with that metadata.
378+
foreach (var metadata in builder.Metadata)
379+
{
380+
foreach (var selector in selectors)
381+
{
382+
selector.EndpointMetadata.Add(metadata);
383+
}
384+
}
385+
}
386+
}
387+
355388
private string CanonicalizeActionName(string actionName)
356389
{
357390
const string Suffix = "Async";

src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.WarningSuppressions.xml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@
3737
<property name="Scope">member</property>
3838
<property name="Target">M:Microsoft.AspNetCore.Mvc.AcceptedAtRouteResult.#ctor(System.String,System.Object,System.Object)</property>
3939
</attribute>
40+
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
41+
<argument>ILLink</argument>
42+
<argument>IL2026</argument>
43+
<property name="Scope">member</property>
44+
<property name="Target">M:Microsoft.AspNetCore.Mvc.ApplicationModels.DefaultApplicationModelProvider.AddReturnTypeMetadata(System.Collections.Generic.IList{Microsoft.AspNetCore.Mvc.ApplicationModels.SelectorModel},System.Reflection.MethodInfo)</property>
45+
</attribute>
4046
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
4147
<argument>ILLink</argument>
4248
<argument>IL2026</argument>
@@ -517,6 +523,12 @@
517523
<property name="Scope">member</property>
518524
<property name="Target">M:Microsoft.AspNetCore.Mvc.ApplicationParts.ProvideApplicationPartFactoryAttribute.GetFactoryType</property>
519525
</attribute>
526+
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
527+
<argument>ILLink</argument>
528+
<argument>IL2060</argument>
529+
<property name="Scope">member</property>
530+
<property name="Target">M:Microsoft.AspNetCore.Mvc.ApplicationModels.DefaultApplicationModelProvider.AddReturnTypeMetadata(System.Collections.Generic.IList{Microsoft.AspNetCore.Mvc.ApplicationModels.SelectorModel},System.Reflection.MethodInfo)</property>
531+
</attribute>
520532
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
521533
<argument>ILLink</argument>
522534
<argument>IL2060</argument>
@@ -637,6 +649,12 @@
637649
<property name="Scope">member</property>
638650
<property name="Target">M:Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.DefaultBindingMetadataProvider.GetBoundConstructor(System.Type)</property>
639651
</attribute>
652+
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
653+
<argument>ILLink</argument>
654+
<argument>IL2072</argument>
655+
<property name="Scope">member</property>
656+
<property name="Target">M:Microsoft.AspNetCore.Mvc.ApplicationModels.DefaultApplicationModelProvider.AddReturnTypeMetadata(System.Collections.Generic.IList{Microsoft.AspNetCore.Mvc.ApplicationModels.SelectorModel},System.Reflection.MethodInfo)</property>
657+
</attribute>
640658
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
641659
<argument>ILLink</argument>
642660
<argument>IL2072</argument>

src/Mvc/Mvc.Core/src/Routing/ActionEndpointFactory.cs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -543,12 +543,4 @@ private static RequestDelegate CreateRequestDelegate()
543543
return invoker!.InvokeAsync();
544544
};
545545
}
546-
547-
private sealed class InertEndpointBuilder : EndpointBuilder
548-
{
549-
public override Endpoint Build()
550-
{
551-
return new Endpoint(RequestDelegate, new EndpointMetadataCollection(Metadata), DisplayName);
552-
}
553-
}
554546
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Builder;
5+
using Microsoft.AspNetCore.Http;
6+
7+
namespace Microsoft.AspNetCore.Mvc.Routing;
8+
9+
internal sealed class InertEndpointBuilder : EndpointBuilder
10+
{
11+
public override Endpoint Build()
12+
{
13+
return new Endpoint(RequestDelegate, new EndpointMetadataCollection(Metadata), DisplayName);
14+
}
15+
}

src/Mvc/Mvc.Core/test/ApplicationModels/DefaultApplicationModelProviderTest.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1243,6 +1243,45 @@ public void CreateActionModel_InheritedAttributeRoutesOverridden()
12431243
Assert.Contains(selectorModel.AttributeRouteModel.Attribute, action.Attributes);
12441244
}
12451245

1246+
[Fact]
1247+
public void CreateActionModel_PopulatesReturnTypeEndpointMetadata() {
1248+
// Arrange
1249+
var builder = new TestApplicationModelProvider();
1250+
var typeInfo = typeof(TypedResultsReturningActionsController).GetTypeInfo();
1251+
var actionName = nameof(TypedResultsReturningActionsController.Get);
1252+
1253+
// Act
1254+
var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName));
1255+
1256+
// Assert
1257+
Assert.NotNull(action.Selectors);
1258+
Assert.All(action.Selectors, selector =>
1259+
{
1260+
Assert.NotNull(selector.EndpointMetadata);
1261+
Assert.Contains(selector.EndpointMetadata, m => m is ProducesResponseTypeMetadata);
1262+
});
1263+
var metadata = action.Selectors[0].EndpointMetadata.OfType<ProducesResponseTypeMetadata>().Single();
1264+
Assert.Equal(200, metadata.StatusCode);
1265+
}
1266+
1267+
[Fact]
1268+
public void AddReturnTypeMetadata_ExtractsMetadataFromReturnType()
1269+
{
1270+
// Arrange
1271+
var selector = new SelectorModel();
1272+
var selectors = new List<SelectorModel> { selector };
1273+
var actionMethod = typeof(TypedResultsReturningActionsController).GetMethod(nameof(TypedResultsReturningActionsController.Get));
1274+
1275+
// Act
1276+
DefaultApplicationModelProvider.AddReturnTypeMetadata(selectors, actionMethod);
1277+
1278+
// Assert
1279+
Assert.NotNull(selector.EndpointMetadata);
1280+
Assert.Single(selector.EndpointMetadata);
1281+
Assert.IsType<ProducesResponseTypeMetadata>(selector.EndpointMetadata.Single());
1282+
Assert.Equal(200, ((ProducesResponseTypeMetadata)selector.EndpointMetadata[0]).StatusCode);
1283+
}
1284+
12461285
[Fact]
12471286
public void ControllerDispose_ExplicitlyImplemented_IDisposableMethods_AreTreatedAs_NonActions()
12481287
{
@@ -1711,6 +1750,16 @@ public void Details() { }
17111750
public void List() { }
17121751
}
17131752

1753+
private class TypedResultsReturningActionsController : Controller
1754+
{
1755+
[HttpGet]
1756+
public Http.HttpResults.Ok<Foo> Get() => TypedResults.Ok<Foo>(new Foo { Info = "Hello" });
1757+
}
1758+
1759+
public class Foo {
1760+
public required string Info { get; set; }
1761+
}
1762+
17141763
private class CustomHttpMethodsAttribute : Attribute, IActionHttpMethodProvider
17151764
{
17161765
private readonly string[] _methods;

src/Mvc/Mvc.Core/test/Microsoft.AspNetCore.Mvc.Core.Test.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<ItemGroup>
1313
<Reference Include="FSharp.Core" />
1414
<Reference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" />
15+
<Reference Include="Microsoft.AspNetCore.Http.Results" />
1516
<ProjectReference Include="..\..\shared\Mvc.Core.TestCommon\Microsoft.AspNetCore.Mvc.Core.TestCommon.csproj" />
1617
<ProjectReference Include="..\..\shared\Mvc.TestDiagnosticListener\Microsoft.AspNetCore.Mvc.TestDiagnosticListener.csproj" />
1718

src/Mvc/test/Mvc.FunctionalTests/ApiExplorerTest.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111
using Microsoft.Extensions.Logging;
1212
using System.Reflection;
1313
using Xunit.Abstractions;
14+
using Microsoft.AspNetCore.Mvc.ApplicationModels;
15+
using Microsoft.Extensions.DependencyInjection;
16+
using Microsoft.AspNetCore.Mvc.ApiExplorer;
17+
using Microsoft.AspNetCore.Http.HttpResults;
18+
using Microsoft.AspNetCore.Http;
1419

1520
namespace Microsoft.AspNetCore.Mvc.FunctionalTests;
1621

@@ -1565,6 +1570,26 @@ public async Task ApiExplorer_LogsInvokedDescriptionProvidersOnStartup()
15651570
Assert.Contains(TestSink.Writes, w => w.Message.Equals("Executing API description provider 'JsonPatchOperationsArrayProvider' from assembly Microsoft.AspNetCore.Mvc.NewtonsoftJson v42.42.42.42.", StringComparison.Ordinal));
15661571
}
15671572

1573+
[Fact]
1574+
public void ApiExplorer_BuildsMetadataForActionWithTypedResult()
1575+
{
1576+
var apiDescCollectionProvider = Factory.Server.Services.GetService<IApiDescriptionGroupCollectionProvider>();
1577+
var testGroupName = nameof(ApiExplorerWithTypedResultController).Replace("Controller", string.Empty);
1578+
var group = apiDescCollectionProvider.ApiDescriptionGroups.Items.Where(i => i.GroupName == testGroupName).SingleOrDefault();
1579+
Assert.NotNull(group);
1580+
var apiDescription = Assert.Single<ApiDescription>(group.Items);
1581+
1582+
var responseType = Assert.Single(apiDescription.SupportedResponseTypes);
1583+
Assert.Equal(StatusCodes.Status200OK, responseType.StatusCode);
1584+
Assert.Equal(typeof(Product), responseType.Type);
1585+
1586+
Assert.NotNull(apiDescription.ActionDescriptor.EndpointMetadata);
1587+
var producesResponseTypeMetadata = apiDescription.ActionDescriptor.EndpointMetadata.OfType<ProducesResponseTypeMetadata>().SingleOrDefault();
1588+
Assert.NotNull(producesResponseTypeMetadata);
1589+
Assert.Equal(StatusCodes.Status200OK, producesResponseTypeMetadata.StatusCode);
1590+
Assert.Equal(typeof(Product), producesResponseTypeMetadata.Type);
1591+
}
1592+
15681593
private IEnumerable<string> GetSortedMediaTypes(ApiExplorerResponseType apiResponseType)
15691594
{
15701595
return apiResponseType.ResponseFormats

src/Mvc/test/WebSites/ApiExplorerWebSite/ApiExplorerWebSite.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<ItemGroup>
88
<Reference Include="Microsoft.AspNetCore.Mvc" />
99
<Reference Include="Microsoft.AspNetCore.Mvc.Formatters.Xml" />
10+
<Reference Include="Microsoft.AspNetCore.Http.Results" />
1011
<Reference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" />
1112

1213
<Reference Include="Microsoft.AspNetCore.Server.IISIntegration" />
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Http.HttpResults;
5+
using Microsoft.AspNetCore.Mvc;
6+
7+
namespace ApiExplorerWebSite;
8+
9+
[Route("ApiExplorerWithTypedResult/[Action]")]
10+
public class ApiExplorerWithTypedResultController : Controller
11+
{
12+
[HttpGet]
13+
public Ok<Product> GetProduct() => TypedResults.Ok(new Product { Name = "Test product" });
14+
}

src/OpenApi/sample/Controllers/TestController.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.ComponentModel.DataAnnotations;
55
using System.Diagnostics.CodeAnalysis;
6+
using Microsoft.AspNetCore.Http.HttpResults;
67
using Microsoft.AspNetCore.Mvc;
78

89
[ApiController]
@@ -17,6 +18,13 @@ public string GetByIdAndName(RouteParamsContainer paramsContainer)
1718
return paramsContainer.Id + "_" + paramsContainer.Name;
1819
}
1920

21+
[HttpGet]
22+
[Route("/gettypedresult")]
23+
public Ok<MvcTodo> GetTypedResult()
24+
{
25+
return TypedResults.Ok(new MvcTodo("Title", "Description", true));
26+
}
27+
2028
[HttpPost]
2129
[Route("/forms")]
2230
public IActionResult PostForm([FromForm] MvcTodo todo)

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=controllers.verified.txt

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,25 @@
5454
}
5555
}
5656
},
57+
"/gettypedresult": {
58+
"get": {
59+
"tags": [
60+
"Test"
61+
],
62+
"responses": {
63+
"200": {
64+
"description": "OK",
65+
"content": {
66+
"application/json": {
67+
"schema": {
68+
"$ref": "#/components/schemas/MvcTodo"
69+
}
70+
}
71+
}
72+
}
73+
}
74+
}
75+
},
5776
"/forms": {
5877
"post": {
5978
"tags": [
@@ -88,6 +107,29 @@
88107
}
89108
}
90109
},
110+
"components": {
111+
"schemas": {
112+
"MvcTodo": {
113+
"required": [
114+
"title",
115+
"description",
116+
"isCompleted"
117+
],
118+
"type": "object",
119+
"properties": {
120+
"title": {
121+
"type": "string"
122+
},
123+
"description": {
124+
"type": "string"
125+
},
126+
"isCompleted": {
127+
"type": "boolean"
128+
}
129+
}
130+
}
131+
}
132+
},
91133
"tags": [
92134
{
93135
"name": "Test"

src/Shared/EndpointMetadataPopulator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Http;
1616
internal static class EndpointMetadataPopulator
1717
{
1818
private static readonly MethodInfo PopulateMetadataForParameterMethod = typeof(EndpointMetadataPopulator).GetMethod(nameof(PopulateMetadataForParameter), BindingFlags.NonPublic | BindingFlags.Static)!;
19-
private static readonly MethodInfo PopulateMetadataForEndpointMethod = typeof(EndpointMetadataPopulator).GetMethod(nameof(PopulateMetadataForEndpoint), BindingFlags.NonPublic | BindingFlags.Static)!;
19+
internal static readonly MethodInfo PopulateMetadataForEndpointMethod = typeof(EndpointMetadataPopulator).GetMethod(nameof(PopulateMetadataForEndpoint), BindingFlags.NonPublic | BindingFlags.Static)!;
2020

2121
public static void PopulateMetadata(MethodInfo methodInfo, EndpointBuilder builder, IEnumerable<ParameterInfo>? parameters = null)
2222
{

0 commit comments

Comments
 (0)