Skip to content

Commit 11abf6e

Browse files
committed
Make ApiExplorer for minimal APIs trim-compatible
1 parent a5c1b26 commit 11abf6e

File tree

55 files changed

+1091
-174
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+1091
-174
lines changed

eng/TrimmableProjects.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
<TrimmableProject Include="Microsoft.AspNetCore.SpaServices.Extensions" />
8686
<TrimmableProject Include="Microsoft.AspNetCore.StaticFiles" />
8787
<TrimmableProject Include="Microsoft.AspNetCore.WebSockets" />
88+
<TrimmableProject Include="Microsoft.AspNetCore.Mvc.ApiExplorer" />
8889
<TrimmableProject Include="Microsoft.AspNetCore.SignalR.Client.Core" />
8990
<TrimmableProject Include="Microsoft.AspNetCore.SignalR.Client" />
9091
<TrimmableProject Include="Microsoft.AspNetCore.Http.Connections.Client" />
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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 System.Reflection;
5+
6+
namespace Microsoft.AspNetCore.Http.Metadata;
7+
8+
/// <summary>
9+
/// Exposes metadata about the parameter binding details associated with a parameter
10+
/// in the endpoints handler.
11+
/// </summary>
12+
/// <remarks>
13+
/// This metadata is injected by the RequestDelegateFactory and RequestDelegateGenerator components
14+
/// and is primarily intended for consumption by the EndpointMetadataApiDescriptionProvider in
15+
/// ApiExplorer.
16+
/// </remarks>
17+
public interface IParameterBindingMetadata
18+
{
19+
/// <summary>
20+
/// The name of the parameter.
21+
/// </summary>
22+
string Name { get; }
23+
24+
/// <summary>
25+
/// <see langword="true "/> is the parameter is associated with a type that implements IParsable or exposes a TryParse method.
26+
/// </summary>
27+
bool HasTryParse { get; }
28+
29+
/// <summary>
30+
/// <see langword="true"/> if the parameter is associated with a type that implements a BindAsync method.
31+
/// </summary>
32+
bool HasBindAsync { get; }
33+
34+
/// <summary>
35+
/// The <see cref="ParameterInfo"/> associated with the parameter.
36+
/// </summary>
37+
ParameterInfo ParameterInfo { get; }
38+
39+
/// <summary>
40+
/// <see langword="true"/> if the parameter is optional.
41+
/// </summary>
42+
bool IsOptional { get; }
43+
}

src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,9 @@ Microsoft.AspNetCore.Http.HostString.HostString(string? value) -> void
55
Microsoft.AspNetCore.Http.HostString.Value.get -> string?
66
Microsoft.AspNetCore.Http.HttpValidationProblemDetails.HttpValidationProblemDetails(System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string!, string![]!>>! errors) -> void
77
Microsoft.AspNetCore.Http.Metadata.IDisableHttpMetricsMetadata
8+
Microsoft.AspNetCore.Http.Metadata.IParameterBindingMetadata
9+
Microsoft.AspNetCore.Http.Metadata.IParameterBindingMetadata.HasBindAsync.get -> bool
10+
Microsoft.AspNetCore.Http.Metadata.IParameterBindingMetadata.HasTryParse.get -> bool
11+
Microsoft.AspNetCore.Http.Metadata.IParameterBindingMetadata.IsOptional.get -> bool
12+
Microsoft.AspNetCore.Http.Metadata.IParameterBindingMetadata.Name.get -> string!
13+
Microsoft.AspNetCore.Http.Metadata.IParameterBindingMetadata.ParameterInfo.get -> System.Reflection.ParameterInfo!

src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,12 @@
55
using System.Globalization;
66
using System.IO;
77
using System.Linq;
8-
using System.Text;
98
using Microsoft.AspNetCore.Analyzers.Infrastructure;
109
using Microsoft.AspNetCore.App.Analyzers.Infrastructure;
1110
using Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel;
1211
using Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel.Emitters;
1312
using Microsoft.CodeAnalysis;
1413
using Microsoft.CodeAnalysis.CSharp;
15-
using Microsoft.CodeAnalysis.Operations;
1614

1715
namespace Microsoft.AspNetCore.Http.RequestDelegateGenerator;
1816

@@ -243,6 +241,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
243241
var hasJsonBody = endpoints.Any(endpoint => endpoint.EmitterContext.HasJsonBody || endpoint.EmitterContext.HasJsonBodyOrService || endpoint.EmitterContext.HasJsonBodyOrQuery);
244242
var hasResponseMetadata = endpoints.Any(endpoint => endpoint.EmitterContext.HasResponseMetadata);
245243
var requiresPropertyAsParameterInfo = endpoints.Any(endpoint => endpoint.EmitterContext.RequiresPropertyAsParameterInfo);
244+
var requiresParameterBindingMetadataClass = endpoints.Any(endpoint => endpoint.EmitterContext.RequiresParameterBindingMetadataClass);
246245

247246
using var stringWriter = new StringWriter(CultureInfo.InvariantCulture);
248247
using var codeWriter = new CodeWriter(stringWriter, baseIndent: 0);
@@ -262,6 +261,11 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
262261
codeWriter.WriteLine(RequestDelegateGeneratorSources.PropertyAsParameterInfoClass);
263262
}
264263

264+
if (requiresParameterBindingMetadataClass)
265+
{
266+
codeWriter.WriteLine(RequestDelegateGeneratorSources.ParameterBindingMetadataClass);
267+
}
268+
265269
return stringWriter.ToString();
266270
});
267271

src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,27 @@ public override bool IsDefined(Type attributeType, bool inherit)
449449
}
450450
""";
451451

452+
public static string ParameterBindingMetadataClass = $$"""
453+
{{GeneratedCodeAttribute}}
454+
file sealed class ParameterBindingMetadata(
455+
string name,
456+
ParameterInfo parameterInfo,
457+
bool hasTryParse = false,
458+
bool hasBindAsync = false,
459+
bool isOptional = false) : Microsoft.AspNetCore.Http.Metadata.IParameterBindingMetadata
460+
{
461+
public string Name => name;
462+
463+
public bool HasTryParse => hasTryParse;
464+
465+
public bool HasBindAsync => hasBindAsync;
466+
467+
public ParameterInfo ParameterInfo => parameterInfo;
468+
469+
public bool IsOptional => isOptional;
470+
}
471+
""";
472+
452473
public static string AntiforgeryMetadataType = """
453474
file sealed class AntiforgeryMetadata : IAntiforgeryMetadata
454475
{

src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EmitterContext.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ internal sealed class EmitterContext
1212
public bool HasBindAsync { get; set; }
1313
public bool HasParsable { get; set; }
1414
public bool RequiresPropertyAsParameterInfo { get; set; }
15+
public bool RequiresParameterBindingMetadataClass { get; set; }
1516
public bool RequiresLoggingHelper { get; set; }
1617
public bool HasEndpointMetadataProvider { get; set; }
1718
public bool HasEndpointParameterMetadataProvider { get; set; }

src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointParameter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ private EndpointParameter(Endpoint endpoint, IPropertySymbol property, IParamete
4747
PropertyAsParameterInfoConstruction = parameter is not null
4848
? $"new PropertyAsParameterInfo({(IsOptional ? "true" : "false")}, {propertyInfo}, {parameter.GetParameterInfoFromConstructorCode()})"
4949
: $"new PropertyAsParameterInfo({(IsOptional ? "true" : "false")}, {propertyInfo})";
50-
endpoint.EmitterContext.RequiresPropertyAsParameterInfo = IsProperty && IsEndpointParameterMetadataProvider;
50+
endpoint.EmitterContext.RequiresPropertyAsParameterInfo = IsProperty;
5151
ProcessEndpointParameterSource(endpoint, property, attributeBuilder.ToImmutable(), wellKnownTypes);
5252
}
5353

src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure;
99
using Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel.Emitters;
1010
using Microsoft.CodeAnalysis;
11+
using Microsoft.CodeAnalysis.CSharp;
1112

1213
namespace Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel;
1314

@@ -199,21 +200,26 @@ public static void EmitFilteredRequestHandler(this Endpoint endpoint, CodeWriter
199200

200201
private static void EmitBuiltinResponseTypeMetadata(this Endpoint endpoint, CodeWriter codeWriter)
201202
{
202-
if (endpoint.Response is not { } response || response.ResponseType is not { } responseType)
203+
if (endpoint.Response is not { } response)
203204
{
204205
return;
205206
}
206207

207-
if (response.HasNoResponse || response.IsIResult)
208+
if (!endpoint.Response.IsAwaitable && (response.HasNoResponse || response.IsIResult))
208209
{
209210
return;
210211
}
211212

212-
if (responseType.SpecialType == SpecialType.System_String)
213+
endpoint.EmitterContext.HasResponseMetadata = true;
214+
if (response.ResponseType?.SpecialType == SpecialType.System_String)
213215
{
214-
codeWriter.WriteLine("options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, contentTypes: GeneratedMetadataConstants.PlaintextContentType));");
216+
codeWriter.WriteLine($"options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(string), contentTypes: GeneratedMetadataConstants.PlaintextContentType));");
215217
}
216-
else
218+
else if (response.IsAwaitable && response.ResponseType == null)
219+
{
220+
codeWriter.WriteLine($"options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(void), contentTypes: GeneratedMetadataConstants.JsonContentType));");
221+
}
222+
else if (response.ResponseType is { } responseType)
217223
{
218224
codeWriter.WriteLine($$"""options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof({{responseType.ToDisplayString(EmitterConstants.DisplayFormatWithoutNullability)}}), contentTypes: GeneratedMetadataConstants.JsonContentType));""");
219225
}
@@ -358,9 +364,39 @@ public static void EmitAcceptsMetadata(this Endpoint endpoint, CodeWriter codeWr
358364
}
359365
}
360366

367+
public static void EmitParameterBindingMetadata(this Endpoint endpoint, CodeWriter codeWriter)
368+
{
369+
foreach (var parameter in endpoint.Parameters)
370+
{
371+
endpoint.EmitterContext.RequiresParameterBindingMetadataClass = true;
372+
if (parameter.EndpointParameters is not null)
373+
{
374+
foreach (var propertyAsParameter in parameter.EndpointParameters)
375+
{
376+
EmitParameterBindingMetadataForParameter(propertyAsParameter, codeWriter);
377+
}
378+
}
379+
else
380+
{
381+
EmitParameterBindingMetadataForParameter(parameter, codeWriter);
382+
}
383+
}
384+
385+
static void EmitParameterBindingMetadataForParameter(EndpointParameter parameter, CodeWriter codeWriter)
386+
{
387+
var parameterName = SymbolDisplay.FormatLiteral(parameter.SymbolName, true);
388+
var parameterInfo = parameter.IsProperty ? parameter.PropertyAsParameterInfoConstruction : $"methodInfo.GetParameters()[{parameter.Ordinal}]";
389+
var hasTryParse = parameter.IsParsable ? "true" : "false";
390+
var hasBindAsync = parameter.Source == EndpointParameterSource.BindAsync ? "true" : "false";
391+
var isOptional = parameter.IsOptional ? "true" : "false";
392+
codeWriter.WriteLine($"options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata({parameterName}, {parameterInfo}, hasTryParse: {hasTryParse}, hasBindAsync: {hasBindAsync}, isOptional: {isOptional}));");
393+
}
394+
}
395+
361396
public static void EmitEndpointMetadataPopulation(this Endpoint endpoint, CodeWriter codeWriter)
362397
{
363398
endpoint.EmitAcceptsMetadata(codeWriter);
399+
endpoint.EmitParameterBindingMetadata(codeWriter);
364400
endpoint.EmitBuiltinResponseTypeMetadata(codeWriter);
365401
endpoint.EmitCallsToMetadataProvidersForParameters(codeWriter);
366402
endpoint.EmitCallToMetadataProviderForResponse(codeWriter);
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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 System.Reflection;
5+
6+
namespace Microsoft.AspNetCore.Http.Metadata;
7+
8+
internal sealed class ParameterBindingMetadata(
9+
string name,
10+
ParameterInfo parameterInfo,
11+
bool hasTryParse = false,
12+
bool hasBindAsync = false,
13+
bool isOptional = false) : IParameterBindingMetadata
14+
{
15+
public string Name => name;
16+
17+
public bool HasTryParse => hasTryParse;
18+
19+
public bool HasBindAsync => hasBindAsync;
20+
21+
public ParameterInfo ParameterInfo => parameterInfo;
22+
23+
public bool IsOptional => isOptional;
24+
}

src/Http/Http.Extensions/src/RequestDelegateFactory.cs

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -648,8 +648,18 @@ private static Expression[] CreateArguments(ParameterInfo[]? parameters, Request
648648

649649
for (var i = 0; i < parameters.Length; i++)
650650
{
651-
args[i] = CreateArgument(parameters[i], factoryContext);
651+
args[i] = CreateArgument(parameters[i], factoryContext, out var hasTryParse, out var hasBindAsync, out var isAsParameters);
652652

653+
if (!isAsParameters)
654+
{
655+
factoryContext.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata(
656+
name: parameters[i].Name!,
657+
parameterInfo: parameters[i],
658+
hasTryParse: hasTryParse,
659+
hasBindAsync: hasBindAsync,
660+
isOptional: IsOptionalParameter(parameters[i], factoryContext)
661+
));
662+
}
653663
factoryContext.ArgumentTypes[i] = parameters[i].ParameterType;
654664
factoryContext.BoxedArgs[i] = Expression.Convert(args[i], typeof(object));
655665
}
@@ -675,8 +685,11 @@ private static Expression[] CreateArguments(ParameterInfo[]? parameters, Request
675685
return args;
676686
}
677687

678-
private static Expression CreateArgument(ParameterInfo parameter, RequestDelegateFactoryContext factoryContext)
688+
private static Expression CreateArgument(ParameterInfo parameter, RequestDelegateFactoryContext factoryContext, out bool hasTryParse, out bool hasBindAsync, out bool isAsParameters)
679689
{
690+
hasTryParse = false;
691+
hasBindAsync = false;
692+
isAsParameters = false;
680693
if (parameter.Name is null)
681694
{
682695
throw new InvalidOperationException($"Encountered a parameter of type '{parameter.ParameterType}' without a name. Parameters must have a name.");
@@ -772,6 +785,7 @@ private static Expression CreateArgument(ParameterInfo parameter, RequestDelegat
772785
parameter.ParameterType == typeof(StringValues?) ||
773786
ParameterBindingMethodCache.HasTryParseMethod(parameter.ParameterType) ||
774787
(parameter.ParameterType.IsArray && ParameterBindingMethodCache.HasTryParseMethod(parameter.ParameterType.GetElementType()!));
788+
hasTryParse = useSimpleBinding;
775789
return useSimpleBinding
776790
? BindParameterFromFormItem(parameter, formAttribute.Name ?? parameter.Name, factoryContext)
777791
: BindComplexParameterFromFormItem(parameter, string.IsNullOrEmpty(formAttribute.Name) ? parameter.Name : formAttribute.Name, factoryContext);
@@ -797,6 +811,7 @@ private static Expression CreateArgument(ParameterInfo parameter, RequestDelegat
797811
}
798812
else if (parameterCustomAttributes.OfType<AsParametersAttribute>().Any())
799813
{
814+
isAsParameters = true;
800815
if (parameter is PropertyAsParameterInfo)
801816
{
802817
throw new NotSupportedException(
@@ -847,10 +862,12 @@ private static Expression CreateArgument(ParameterInfo parameter, RequestDelegat
847862
}
848863
else if (ParameterBindingMethodCache.HasBindAsyncMethod(parameter))
849864
{
865+
hasBindAsync = true;
850866
return BindParameterFromBindAsync(parameter, factoryContext);
851867
}
852868
else if (parameter.ParameterType == typeof(string) || ParameterBindingMethodCache.HasTryParseMethod(parameter.ParameterType))
853869
{
870+
hasTryParse = true;
854871
// 1. We bind from route values only, if route parameters are non-null and the parameter name is in that set.
855872
// 2. We bind from query only, if route parameters are non-null and the parameter name is NOT in that set.
856873
// 3. Otherwise, we fallback to route or query if route parameters is null (it means we don't know what route parameters are defined). This case only happens
@@ -881,7 +898,7 @@ private static Expression CreateArgument(ParameterInfo parameter, RequestDelegat
881898
(parameter.ParameterType.IsArray && ParameterBindingMethodCache.HasTryParseMethod(parameter.ParameterType.GetElementType()!))))
882899
{
883900
// We only infer parameter types if you have an array of TryParsables/string[]/StringValues/StringValues?, and DisableInferredFromBody is true
884-
901+
hasTryParse = true;
885902
factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.QueryStringParameter);
886903
return BindParameterFromProperty(parameter, QueryExpr, QueryIndexerProperty, parameter.Name, factoryContext, "query string");
887904
}
@@ -1009,20 +1026,22 @@ private static void PopulateBuiltInResponseTypeMetadata(Type returnType, Endpoin
10091026
throw GetUnsupportedReturnTypeException(returnType);
10101027
}
10111028

1029+
var isAwaitable = false;
10121030
if (CoercedAwaitableInfo.IsTypeAwaitable(returnType, out var coercedAwaitableInfo))
10131031
{
10141032
returnType = coercedAwaitableInfo.AwaitableInfo.ResultType;
1033+
isAwaitable = true;
10151034
}
10161035

10171036
// Skip void returns and IResults. IResults might implement IEndpointMetadataProvider but otherwise we don't know what it might do.
1018-
if (returnType == typeof(void) || typeof(IResult).IsAssignableFrom(returnType))
1037+
if (!isAwaitable && (returnType == typeof(void) || typeof(IResult).IsAssignableFrom(returnType)))
10191038
{
10201039
return;
10211040
}
10221041

10231042
if (returnType == typeof(string))
10241043
{
1025-
builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(type: null, statusCode: 200, PlaintextContentType));
1044+
builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(type: typeof(string), statusCode: 200, PlaintextContentType));
10261045
}
10271046
else
10281047
{
@@ -1556,8 +1575,10 @@ private static Expression BindParameterFromProperties(ParameterInfo parameter, R
15561575
{
15571576
var parameterInfo =
15581577
new PropertyAsParameterInfo(parameters[i].PropertyInfo, parameters[i].ParameterInfo, factoryContext.NullabilityContext);
1559-
constructorArguments[i] = CreateArgument(parameterInfo, factoryContext);
1578+
Debug.Assert(parameterInfo.Name != null, "Parameter name must be set for parameters resolved from properties.");
1579+
constructorArguments[i] = CreateArgument(parameterInfo, factoryContext, out var hasTryParse, out var hasBindAsync, out var _);
15601580
factoryContext.Parameters.Add(parameterInfo);
1581+
factoryContext.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata(parameterInfo.Name, parameterInfo, hasTryParse: hasTryParse, hasBindAsync: hasBindAsync, isOptional: parameterInfo.IsOptional));
15611582
}
15621583

15631584
initExpression = Expression.New(constructor, constructorArguments);
@@ -1579,8 +1600,10 @@ private static Expression BindParameterFromProperties(ParameterInfo parameter, R
15791600
if (properties[i].CanWrite && properties[i].GetSetMethod(nonPublic: false) != null)
15801601
{
15811602
var parameterInfo = new PropertyAsParameterInfo(properties[i], factoryContext.NullabilityContext);
1582-
bindings.Add(Expression.Bind(properties[i], CreateArgument(parameterInfo, factoryContext)));
1603+
Debug.Assert(parameterInfo.Name != null, "Parameter name must be set for parameters resolved from properties.");
1604+
bindings.Add(Expression.Bind(properties[i], CreateArgument(parameterInfo, factoryContext, out var hasTryParse, out var hasBindAsync, out var _)));
15831605
factoryContext.Parameters.Add(parameterInfo);
1606+
factoryContext.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata(parameterInfo.Name, parameterInfo, hasTryParse: hasTryParse, hasBindAsync: hasBindAsync, isOptional: parameterInfo.IsOptional));
15841607
}
15851608
}
15861609

0 commit comments

Comments
 (0)