Skip to content

Commit 26d174a

Browse files
committed
Emit diagnostics and add PropertyAsParameterInfo implementation
1 parent 0f9c534 commit 26d174a

20 files changed

+720
-246
lines changed

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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
230230
.Select((endpoints, _) =>
231231
{
232232
var requiresMetadataHelperTypes = endpoints.Any(endpoint => endpoint!.EmitterContext.RequiresMetadataHelperTypes);
233+
var hasAsParameters = endpoints.Any(endpoint => endpoint!.EmitterContext.HasAsParameters);
233234

234235
using var stringWriter = new StringWriter(CultureInfo.InvariantCulture);
235236
using var codeWriter = new CodeWriter(stringWriter, baseIndent: 0);
@@ -239,6 +240,11 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
239240
codeWriter.WriteLine(RequestDelegateGeneratorSources.ContentMetadataTypes);
240241
}
241242

243+
if (hasAsParameters)
244+
{
245+
codeWriter.WriteLine(RequestDelegateGeneratorSources.PropertyAsParameterInfoClass);
246+
}
247+
242248
return stringWriter.ToString();
243249
});
244250

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

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,100 @@ public void InvalidFormRequestBody(string parameterTypeName, string parameterNam
363363
}
364364
""";
365365

366+
public static string PropertyAsParameterInfoClass = """
367+
file class PropertyAsParameterInfo : ParameterInfo
368+
{
369+
private readonly PropertyInfo _underlyingProperty;
370+
private readonly ParameterInfo? _constructionParameterInfo;
371+
372+
public PropertyAsParameterInfo(bool isOptional, PropertyInfo propertyInfo)
373+
{
374+
Debug.Assert(null != propertyInfo);
375+
376+
AttrsImpl = (ParameterAttributes)propertyInfo.Attributes;
377+
NameImpl = propertyInfo.Name;
378+
MemberImpl = propertyInfo;
379+
ClassImpl = propertyInfo.PropertyType;
380+
381+
// It is not a real parameter in the delegate, so,
382+
// not defining a real position.
383+
PositionImpl = -1;
384+
385+
_underlyingProperty = propertyInfo;
386+
IsOptional = isOptional;
387+
}
388+
389+
public PropertyAsParameterInfo(bool isOptional, PropertyInfo property, ParameterInfo? parameterInfo)
390+
: this(isOptional, property)
391+
{
392+
_constructionParameterInfo = parameterInfo;
393+
}
394+
395+
public override bool HasDefaultValue
396+
=> _constructionParameterInfo is not null && _constructionParameterInfo.HasDefaultValue;
397+
public override object? DefaultValue
398+
=> _constructionParameterInfo?.DefaultValue;
399+
public override int MetadataToken => _underlyingProperty.MetadataToken;
400+
public override object? RawDefaultValue
401+
=> _constructionParameterInfo?.RawDefaultValue;
402+
403+
public override object[] GetCustomAttributes(Type attributeType, bool inherit)
404+
{
405+
var attributes = _constructionParameterInfo?.GetCustomAttributes(attributeType, inherit);
406+
407+
if (attributes == null || attributes is { Length: 0 })
408+
{
409+
attributes = _underlyingProperty.GetCustomAttributes(attributeType, inherit);
410+
}
411+
412+
return attributes;
413+
}
414+
415+
public override object[] GetCustomAttributes(bool inherit)
416+
{
417+
var constructorAttributes = _constructionParameterInfo?.GetCustomAttributes(inherit);
418+
419+
if (constructorAttributes == null || constructorAttributes is { Length: 0 })
420+
{
421+
return _underlyingProperty.GetCustomAttributes(inherit);
422+
}
423+
424+
var propertyAttributes = _underlyingProperty.GetCustomAttributes(inherit);
425+
426+
// Since the constructors attributes should take priority we will add them first,
427+
// as we usually call it as First() or FirstOrDefault() in the argument creation
428+
var mergedAttributes = new object[constructorAttributes.Length + propertyAttributes.Length];
429+
Array.Copy(constructorAttributes, mergedAttributes, constructorAttributes.Length);
430+
Array.Copy(propertyAttributes, 0, mergedAttributes, constructorAttributes.Length, propertyAttributes.Length);
431+
432+
return mergedAttributes;
433+
}
434+
435+
public override IList<CustomAttributeData> GetCustomAttributesData()
436+
{
437+
var attributes = new List<CustomAttributeData>(
438+
_constructionParameterInfo?.GetCustomAttributesData() ?? Array.Empty<CustomAttributeData>());
439+
attributes.AddRange(_underlyingProperty.GetCustomAttributesData());
440+
441+
return attributes.AsReadOnly();
442+
}
443+
444+
public override Type[] GetOptionalCustomModifiers()
445+
=> _underlyingProperty.GetOptionalCustomModifiers();
446+
447+
public override Type[] GetRequiredCustomModifiers()
448+
=> _underlyingProperty.GetRequiredCustomModifiers();
449+
450+
public override bool IsDefined(Type attributeType, bool inherit)
451+
{
452+
return (_constructionParameterInfo is not null && _constructionParameterInfo.IsDefined(attributeType, inherit)) ||
453+
_underlyingProperty.IsDefined(attributeType, inherit);
454+
}
455+
456+
public new bool IsOptional { get; }
457+
}
458+
""";
459+
366460
public static string GetGeneratedRouteBuilderExtensionsSource(string genericThunks, string thunks, string endpoints, string helperMethods, string helperTypes) => $$"""
367461
{{SourceHeader}}
368462

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 parameterized 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>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 parameter</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 HasAsParameters { get; set; }
1415
public bool RequiresLoggingHelper { get; set; }
1516
public bool RequiresMetadataHelperTypes { get; set; }
1617
public bool HasEndpointMetadataProvider { get; set; }

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ internal static class EmitterExtensions
1313
{
1414
EndpointParameterSource.Header => "header",
1515
EndpointParameterSource.Query => "query string",
16+
EndpointParameterSource.Route => "route",
1617
EndpointParameterSource.RouteOrQuery => "route or query string",
1718
EndpointParameterSource.FormBody => "form",
1819
EndpointParameterSource.BindAsync => endpointParameter.BindMethod == BindabilityMethod.BindAsync
@@ -37,7 +38,7 @@ public static bool IsSerializableJsonResponse(this EndpointResponse endpointResp
3738
public static string EmitArgument(this EndpointParameter endpointParameter) => endpointParameter.Source switch
3839
{
3940
EndpointParameterSource.JsonBody or EndpointParameterSource.Route or EndpointParameterSource.RouteOrQuery or EndpointParameterSource.JsonBodyOrService or EndpointParameterSource.FormBody => endpointParameter.IsOptional ? endpointParameter.EmitHandlerArgument() : $"{endpointParameter.EmitHandlerArgument()}!",
40-
EndpointParameterSource.Unknown => throw new Exception("Unreachable!"),
41+
EndpointParameterSource.Unknown => throw new NotImplementedException("Unreachable!"),
4142
_ => endpointParameter.EmitHandlerArgument()
4243
};
4344
}

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,14 +229,17 @@ internal static void EmitBindAsyncPreparation(this EndpointParameter endpointPar
229229
{
230230
var unwrappedType = endpointParameter.Type.UnwrapTypeSymbol(unwrapNullable: true);
231231
var unwrappedTypeString = unwrappedType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
232+
var resolveParameterInfo = endpointParameter.IsProperty
233+
? endpointParameter.PropertyAsParameterInfoConstruction
234+
: $"parameters[{endpointParameter.Ordinal}]";
232235

233236
switch (endpointParameter.BindMethod)
234237
{
235238
case BindabilityMethod.IBindableFromHttpContext:
236-
codeWriter.WriteLine($"var {endpointParameter.EmitTempArgument()} = await BindAsync<{unwrappedTypeString}>(httpContext, parameters[{endpointParameter.Ordinal}]);");
239+
codeWriter.WriteLine($"var {endpointParameter.EmitTempArgument()} = await BindAsync<{unwrappedTypeString}>(httpContext, {resolveParameterInfo});");
237240
break;
238241
case BindabilityMethod.BindAsyncWithParameter:
239-
codeWriter.WriteLine($"var {endpointParameter.EmitTempArgument()} = await {unwrappedTypeString}.BindAsync(httpContext, parameters[{endpointParameter.Ordinal}]);");
242+
codeWriter.WriteLine($"var {endpointParameter.EmitTempArgument()} = await {unwrappedTypeString}.BindAsync(httpContext, {resolveParameterInfo});");
240243
break;
241244
case BindabilityMethod.BindAsync:
242245
codeWriter.WriteLine($"var {endpointParameter.EmitTempArgument()} = await {unwrappedTypeString}.BindAsync(httpContext);");

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,6 @@ public Endpoint(IInvocationOperation operation, WellKnownTypes wellKnownTypes, S
9595

9696
Parameters = parameters;
9797

98-
EmitterContext.HasEndpointParameterMetadataProvider = Parameters.Any(p => p.IsEndpointParameterMetadataProvider);
99-
EmitterContext.HasEndpointMetadataProvider = Response!.IsEndpointMetadataProvider || Parameters.Any(p => p.IsEndpointMetadataProvider || p.IsEndpointParameterMetadataProvider);
10098
EmitterContext.RequiresLoggingHelper = !Parameters.All(parameter =>
10199
parameter.Source == EndpointParameterSource.SpecialType ||
102100
parameter is { IsArray: true, ElementType.SpecialType: SpecialType.System_String, Source: EndpointParameterSource.Query });

0 commit comments

Comments
 (0)