Skip to content

Commit ce9e1ae

Browse files
authored
Add initial support for parameters with AsParameters attribute (#47914)
1 parent 3ff3207 commit ce9e1ae

File tree

60 files changed

+2681
-837
lines changed

Some content is hidden

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

60 files changed

+2681
-837
lines changed

docs/list-of-diagnostics.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@
7272
| __`RDG002`__ | Unable to resolve endpoint handler |
7373
| __`RDG003`__ | Unable to resolve parameter |
7474
| __`RDG004`__ | Unable to resolve anonymous type |
75+
| __`RDG005`__ | Invalid abstract type |
76+
| __`RDG006`__ | Invalid constructor parameters |
77+
| __`RDG007`__ | No valid constructor found |
78+
| __`RDG008`__ | Multiple public constructors found |
79+
| __`RDG009`__ | Invalid nested AsParameters |
80+
| __`RDG010`__ | Unexpected nullable type |
81+
7582

7683
### SignalR Source Generator (`SSG0000-SSG0110`)
7784

src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RoutePatternParametersDetector.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ static ImmutableArray<ParameterSymbol> ResolvedParametersCore(ISymbol symbol, IS
4343
static string ResolveRouteParameterName(ISymbol parameterSymbol, WellKnownTypes wellKnownTypes)
4444
{
4545
var fromRouteMetadata = wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromRouteMetadata);
46-
if (!parameterSymbol.HasAttributeImplementingInterface(fromRouteMetadata, out var attributeData))
46+
if (!parameterSymbol.TryGetAttributeImplementingInterface(fromRouteMetadata, out var attributeData))
4747
{
4848
return parameterSymbol.Name; // No route metadata attribute!
4949
}

src/Http/Http.Extensions/gen/DiagnosticDescriptors.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,52 @@ internal static class DiagnosticDescriptors
4242
"Usage",
4343
DiagnosticSeverity.Warning,
4444
isEnabledByDefault: true);
45+
46+
public static DiagnosticDescriptor InvalidAsParametersAbstractType { get; } = new(
47+
"RDG005",
48+
new LocalizableResourceString(nameof(Resources.InvalidAsParametersAbstractType_Title), Resources.ResourceManager, typeof(Resources)),
49+
new LocalizableResourceString(nameof(Resources.InvalidAsParametersAbstractType_Message), Resources.ResourceManager, typeof(Resources)),
50+
"Usage",
51+
DiagnosticSeverity.Error,
52+
isEnabledByDefault: true);
53+
54+
public static DiagnosticDescriptor InvalidAsParametersSignature { get; } = new(
55+
"RDG006",
56+
new LocalizableResourceString(nameof(Resources.InvalidAsParametersSignature_Title), Resources.ResourceManager, typeof(Resources)),
57+
new LocalizableResourceString(nameof(Resources.InvalidAsParametersSignature_Message), Resources.ResourceManager, typeof(Resources)),
58+
"Usage",
59+
DiagnosticSeverity.Error,
60+
isEnabledByDefault: true);
61+
62+
public static DiagnosticDescriptor InvalidAsParametersNoConstructorFound { get; } = new(
63+
"RDG007",
64+
new LocalizableResourceString(nameof(Resources.InvalidAsParametersNoConstructorFound_Title), Resources.ResourceManager, typeof(Resources)),
65+
new LocalizableResourceString(nameof(Resources.InvalidAsParametersNoConstructorFound_Message), Resources.ResourceManager, typeof(Resources)),
66+
"Usage",
67+
DiagnosticSeverity.Error,
68+
isEnabledByDefault: true);
69+
70+
public static DiagnosticDescriptor InvalidAsParametersSingleConstructorOnly { get; } = new(
71+
"RDG008",
72+
new LocalizableResourceString(nameof(Resources.InvalidAsParametersSingleConstructorOnly_Title), Resources.ResourceManager, typeof(Resources)),
73+
new LocalizableResourceString(nameof(Resources.InvalidAsParametersSingleConstructorOnly_Message), Resources.ResourceManager, typeof(Resources)),
74+
"Usage",
75+
DiagnosticSeverity.Error,
76+
isEnabledByDefault: true);
77+
78+
public static DiagnosticDescriptor InvalidAsParametersNested { get; } = new(
79+
"RDG009",
80+
new LocalizableResourceString(nameof(Resources.InvalidAsParametersNested_Title), Resources.ResourceManager, typeof(Resources)),
81+
new LocalizableResourceString(nameof(Resources.InvalidAsParametersNested_Message), Resources.ResourceManager, typeof(Resources)),
82+
"Usage",
83+
DiagnosticSeverity.Error,
84+
isEnabledByDefault: true);
85+
86+
public static DiagnosticDescriptor InvalidAsParametersNullable { get; } = new(
87+
"RDG010",
88+
new LocalizableResourceString(nameof(Resources.InvalidAsParametersNullable_Title), Resources.ResourceManager, typeof(Resources)),
89+
new LocalizableResourceString(nameof(Resources.InvalidAsParametersNullable_Message), Resources.ResourceManager, typeof(Resources)),
90+
"Usage",
91+
DiagnosticSeverity.Error,
92+
isEnabledByDefault: true);
4593
}

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,13 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
6969
codeWriter.EndBlockWithComma();
7070
codeWriter.WriteLine("(del, options, inferredMetadataResult) =>");
7171
codeWriter.StartBlock();
72+
codeWriter.WriteLine(@"Debug.Assert(options != null, ""RequestDelegateFactoryOptions not found."");");
73+
codeWriter.WriteLine(@"Debug.Assert(options.EndpointBuilder != null, ""EndpointBuilder not found."");");
7274
codeWriter.WriteLine($"var handler = ({endpoint!.EmitHandlerDelegateType(considerOptionality: true)})del;");
7375
codeWriter.WriteLine("EndpointFilterDelegate? filteredInvocation = null;");
7476
if (endpoint!.EmitterContext.RequiresLoggingHelper || endpoint!.EmitterContext.HasJsonBodyOrService || endpoint!.Response?.IsSerializableJsonResponse(out var _) is true)
7577
{
76-
codeWriter.WriteLine("var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;");
78+
codeWriter.WriteLine("var serviceProvider = options.ServiceProvider ?? options.EndpointBuilder.ApplicationServices;");
7779
}
7880
endpoint!.EmitLoggingPreamble(codeWriter);
7981
endpoint!.EmitRouteOrQueryResolver(codeWriter);
@@ -235,6 +237,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
235237
var hasFormBody = endpoints.Any(endpoint => endpoint!.EmitterContext.HasFormBody);
236238
var hasJsonBody = endpoints.Any(endpoint => endpoint!.EmitterContext.HasJsonBody || endpoint!.EmitterContext.HasJsonBodyOrService);
237239
var hasResponseMetadata = endpoints.Any(endpoint => endpoint!.EmitterContext.HasResponseMetadata);
240+
var requiresPropertyAsParameterInfo = endpoints.Any(endpoint => endpoint!.EmitterContext.RequiresPropertyAsParameterInfo);
238241

239242
using var stringWriter = new StringWriter(CultureInfo.InvariantCulture);
240243
using var codeWriter = new CodeWriter(stringWriter, baseIndent: 0);
@@ -254,6 +257,11 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
254257
codeWriter.WriteLine(RequestDelegateGeneratorSources.ContentTypeConstantsType);
255258
}
256259

260+
if (requiresPropertyAsParameterInfo)
261+
{
262+
codeWriter.WriteLine(RequestDelegateGeneratorSources.PropertyAsParameterInfoClass);
263+
}
264+
257265
return stringWriter.ToString();
258266
});
259267

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

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ private static Task WriteToResponseAsync<T>(T? value, HttpContext httpContext, J
253253
""";
254254

255255
public static string LogOrThrowExceptionHelperClass => $$"""
256-
file class LogOrThrowExceptionHelper
256+
file sealed class LogOrThrowExceptionHelper
257257
{
258258
private readonly ILogger? _rdgLogger;
259259
private readonly bool _shouldThrow;
@@ -397,13 +397,107 @@ public void InvalidFormRequestBody(string parameterTypeName, string parameterNam
397397
}
398398
""";
399399

400+
public static string PropertyAsParameterInfoClass = """
401+
file sealed class PropertyAsParameterInfo : ParameterInfo
402+
{
403+
private readonly PropertyInfo _underlyingProperty;
404+
private readonly ParameterInfo? _constructionParameterInfo;
405+
406+
public PropertyAsParameterInfo(bool isOptional, PropertyInfo propertyInfo)
407+
{
408+
Debug.Assert(propertyInfo != null, "PropertyInfo must be provided.");
409+
410+
AttrsImpl = (ParameterAttributes)propertyInfo.Attributes;
411+
NameImpl = propertyInfo.Name;
412+
MemberImpl = propertyInfo;
413+
ClassImpl = propertyInfo.PropertyType;
414+
415+
// It is not a real parameter in the delegate, so,
416+
// not defining a real position.
417+
PositionImpl = -1;
418+
419+
_underlyingProperty = propertyInfo;
420+
IsOptional = isOptional;
421+
}
422+
423+
public PropertyAsParameterInfo(bool isOptional, PropertyInfo property, ParameterInfo? parameterInfo)
424+
: this(isOptional, property)
425+
{
426+
_constructionParameterInfo = parameterInfo;
427+
}
428+
429+
public override bool HasDefaultValue
430+
=> _constructionParameterInfo is not null && _constructionParameterInfo.HasDefaultValue;
431+
public override object? DefaultValue
432+
=> _constructionParameterInfo?.DefaultValue;
433+
public override int MetadataToken => _underlyingProperty.MetadataToken;
434+
public override object? RawDefaultValue
435+
=> _constructionParameterInfo?.RawDefaultValue;
436+
437+
public override object[] GetCustomAttributes(Type attributeType, bool inherit)
438+
{
439+
var attributes = _constructionParameterInfo?.GetCustomAttributes(attributeType, inherit);
440+
441+
if (attributes == null || attributes is { Length: 0 })
442+
{
443+
attributes = _underlyingProperty.GetCustomAttributes(attributeType, inherit);
444+
}
445+
446+
return attributes;
447+
}
448+
449+
public override object[] GetCustomAttributes(bool inherit)
450+
{
451+
var constructorAttributes = _constructionParameterInfo?.GetCustomAttributes(inherit);
452+
453+
if (constructorAttributes == null || constructorAttributes is { Length: 0 })
454+
{
455+
return _underlyingProperty.GetCustomAttributes(inherit);
456+
}
457+
458+
var propertyAttributes = _underlyingProperty.GetCustomAttributes(inherit);
459+
460+
// Since the constructors attributes should take priority we will add them first,
461+
// as we usually call it as First() or FirstOrDefault() in the argument creation
462+
var mergedAttributes = new object[constructorAttributes.Length + propertyAttributes.Length];
463+
Array.Copy(constructorAttributes, mergedAttributes, constructorAttributes.Length);
464+
Array.Copy(propertyAttributes, 0, mergedAttributes, constructorAttributes.Length, propertyAttributes.Length);
465+
466+
return mergedAttributes;
467+
}
468+
469+
public override IList<CustomAttributeData> GetCustomAttributesData()
470+
{
471+
var attributes = new List<CustomAttributeData>(
472+
_constructionParameterInfo?.GetCustomAttributesData() ?? Array.Empty<CustomAttributeData>());
473+
attributes.AddRange(_underlyingProperty.GetCustomAttributesData());
474+
475+
return attributes.AsReadOnly();
476+
}
477+
478+
public override Type[] GetOptionalCustomModifiers()
479+
=> _underlyingProperty.GetOptionalCustomModifiers();
480+
481+
public override Type[] GetRequiredCustomModifiers()
482+
=> _underlyingProperty.GetRequiredCustomModifiers();
483+
484+
public override bool IsDefined(Type attributeType, bool inherit)
485+
{
486+
return (_constructionParameterInfo is not null && _constructionParameterInfo.IsDefined(attributeType, inherit)) ||
487+
_underlyingProperty.IsDefined(attributeType, inherit);
488+
}
489+
490+
public new bool IsOptional { get; }
491+
}
492+
""";
493+
400494
public static string GetGeneratedRouteBuilderExtensionsSource(string genericThunks, string thunks, string endpoints, string helperMethods, string helperTypes) => $$"""
401495
{{SourceHeader}}
402496
403497
namespace Microsoft.AspNetCore.Builder
404498
{
405499
{{GeneratedCodeAttribute}}
406-
internal class SourceKey
500+
internal sealed class SourceKey
407501
{
408502
public string Path { get; init; }
409503
public int Line { get; init; }

src/Http/Http.Extensions/gen/Resources.resx

Lines changed: 63 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<root>
3-
<!--
4-
Microsoft ResX Schema
5-
3+
<!--
4+
Microsoft ResX Schema
5+
66
Version 2.0
7-
8-
The primary goals of this format is to allow a simple XML format
9-
that is mostly human readable. The generation and parsing of the
10-
various data types are done through the TypeConverter classes
7+
8+
The primary goals of this format is to allow a simple XML format
9+
that is mostly human readable. The generation and parsing of the
10+
various data types are done through the TypeConverter classes
1111
associated with the data types.
12-
12+
1313
Example:
14-
14+
1515
... ado.net/XML headers & schema ...
1616
<resheader name="resmimetype">text/microsoft-resx</resheader>
1717
<resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
2626
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
2727
<comment>This is a comment</comment>
2828
</data>
29-
30-
There are any number of "resheader" rows that contain simple
29+
30+
There are any number of "resheader" rows that contain simple
3131
name/value pairs.
32-
33-
Each data row contains a name, and value. The row also contains a
34-
type or mimetype. Type corresponds to a .NET class that support
35-
text/value conversion through the TypeConverter architecture.
36-
Classes that don't support this are serialized and stored with the
32+
33+
Each data row contains a name, and value. The row also contains a
34+
type or mimetype. Type corresponds to a .NET class that support
35+
text/value conversion through the TypeConverter architecture.
36+
Classes that don't support this are serialized and stored with the
3737
mimetype set.
38-
39-
The mimetype is used for serialized objects, and tells the
40-
ResXResourceReader how to depersist the object. This is currently not
38+
39+
The mimetype is used for serialized objects, and tells the
40+
ResXResourceReader how to depersist the object. This is currently not
4141
extensible. For a given mimetype the value must be set accordingly:
42-
43-
Note - application/x-microsoft.net.object.binary.base64 is the format
44-
that the ResXResourceWriter will generate, however the reader can
42+
43+
Note - application/x-microsoft.net.object.binary.base64 is the format
44+
that the ResXResourceWriter will generate, however the reader can
4545
read any of the formats listed below.
46-
46+
4747
mimetype: application/x-microsoft.net.object.binary.base64
48-
value : The object must be serialized with
48+
value : The object must be serialized with
4949
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
5050
: and then encoded with base64 encoding.
51-
51+
5252
mimetype: application/x-microsoft.net.object.soap.base64
53-
value : The object must be serialized with
53+
value : The object must be serialized with
5454
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
5555
: and then encoded with base64 encoding.
5656
5757
mimetype: application/x-microsoft.net.object.bytearray.base64
58-
value : The object must be serialized into a byte array
58+
value : The object must be serialized into a byte array
5959
: using a System.ComponentModel.TypeConverter
6060
: and then encoded with base64 encoding.
6161
-->
@@ -141,4 +141,40 @@
141141
<data name="UnableToResolveAnonymousReturnType_Title" xml:space="preserve">
142142
<value>Unable to resolve anonymous type</value>
143143
</data>
144+
<data name="InvalidAsParametersAbstractType_Title" xml:space="preserve">
145+
<value>Invalid abstract type</value>
146+
</data>
147+
<data name="InvalidAsParametersAbstractType_Message" xml:space="preserve">
148+
<value>The abstract type '{0}' is not supported.</value>
149+
</data>
150+
<data name="InvalidAsParametersSignature_Title" xml:space="preserve">
151+
<value>Invalid constructor parameters</value>
152+
</data>
153+
<data name="InvalidAsParametersSignature_Message" xml:space="preserve">
154+
<value>The public parameterized constructor must contain only parameters that match the declared public properties for type '{0}'.</value>
155+
</data>
156+
<data name="InvalidAsParametersNoConstructorFound_Title" xml:space="preserve">
157+
<value>No valid constructor found</value>
158+
</data>
159+
<data name="InvalidAsParametersNoConstructorFound_Message" xml:space="preserve">
160+
<value>No public parameterless constructor found for type '{0}'.</value>
161+
</data>
162+
<data name="InvalidAsParametersSingleConstructorOnly_Title" xml:space="preserve">
163+
<value>Multiple public constructors found</value>
164+
</data>
165+
<data name="InvalidAsParametersSingleConstructorOnly_Message" xml:space="preserve">
166+
<value>Only a single public parameterized constructor is allowed for type '{0}'.</value>
167+
</data>
168+
<data name="InvalidAsParametersNested_Title" xml:space="preserve">
169+
<value>Invalid nested AsParameters</value>
170+
</data>
171+
<data name="InvalidAsParametersNested_Message" xml:space="preserve">
172+
<value>Nested AsParametersAttribute is not supported and should be used only for handler parameters.</value>
173+
</data>
174+
<data name="InvalidAsParametersNullable_Title" xml:space="preserve">
175+
<value>Unexpected nullable type</value>
176+
</data>
177+
<data name="InvalidAsParametersNullable_Message" xml:space="preserve">
178+
<value>The nullable type '{0}' is not supported. Mark the parameter as non-nullable.</value>
179+
</data>
144180
</root>

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

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

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
3+
using System;
34
using System.Diagnostics.CodeAnalysis;
45
using Microsoft.AspNetCore.Analyzers.Infrastructure;
56
using Microsoft.CodeAnalysis;
@@ -12,6 +13,7 @@ internal static class EmitterExtensions
1213
{
1314
EndpointParameterSource.Header => "header",
1415
EndpointParameterSource.Query => "query string",
16+
EndpointParameterSource.Route => "route",
1517
EndpointParameterSource.RouteOrQuery => "route or query string",
1618
EndpointParameterSource.FormBody => "form",
1719
EndpointParameterSource.BindAsync => endpointParameter.BindMethod == BindabilityMethod.BindAsync
@@ -30,4 +32,13 @@ public static bool IsSerializableJsonResponse(this EndpointResponse endpointResp
3032
}
3133
return false;
3234
}
35+
36+
public static string EmitHandlerArgument(this EndpointParameter endpointParameter) => $"{endpointParameter.SymbolName}_local";
37+
38+
public static string EmitArgument(this EndpointParameter endpointParameter) => endpointParameter.Source switch
39+
{
40+
EndpointParameterSource.JsonBody or EndpointParameterSource.Route or EndpointParameterSource.RouteOrQuery or EndpointParameterSource.JsonBodyOrService or EndpointParameterSource.FormBody => endpointParameter.IsOptional ? endpointParameter.EmitHandlerArgument() : $"{endpointParameter.EmitHandlerArgument()}!",
41+
EndpointParameterSource.Unknown => throw new NotImplementedException("Unreachable!"),
42+
_ => endpointParameter.EmitHandlerArgument()
43+
};
3344
}

0 commit comments

Comments
 (0)