Skip to content

Commit c274bff

Browse files
committed
Extract endpoint metadata during ActionModel creation
1 parent ad17323 commit c274bff

File tree

6 files changed

+102
-9
lines changed

6 files changed

+102
-9
lines changed

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics.CodeAnalysis;
45
using System.Linq;
56
using System.Reflection;
67
using Microsoft.AspNetCore.Http;
8+
using Microsoft.AspNetCore.Http.Metadata;
79
using Microsoft.AspNetCore.Mvc.ActionConstraints;
810
using Microsoft.AspNetCore.Mvc.ApiExplorer;
911
using Microsoft.AspNetCore.Mvc.Filters;
@@ -349,9 +351,43 @@ internal PropertyModel CreatePropertyModel(PropertyInfo propertyInfo)
349351
applicableAttributes.AddRange(routeAttributes);
350352
AddRange(actionModel.Selectors, CreateSelectors(applicableAttributes));
351353

354+
AddReturnTypeMetadata(actionModel.Selectors, methodInfo);
355+
352356
return actionModel;
353357
}
354358

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

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/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)