Skip to content

Add initial support for parameters with AsParameters attribute #47914

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
May 14, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ static ImmutableArray<ParameterSymbol> ResolvedParametersCore(ISymbol symbol, IS
static string ResolveRouteParameterName(ISymbol parameterSymbol, WellKnownTypes wellKnownTypes)
{
var fromRouteMetadata = wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromRouteMetadata);
if (!parameterSymbol.HasAttributeImplementingInterface(fromRouteMetadata, out var attributeData))
if (!parameterSymbol.TryGetAttributeImplementingInterface(fromRouteMetadata, out var attributeData))
{
return parameterSymbol.Name; // No route metadata attribute!
}
Expand Down
48 changes: 48 additions & 0 deletions src/Http/Http.Extensions/gen/DiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,52 @@ internal static class DiagnosticDescriptors
"Usage",
DiagnosticSeverity.Warning,
isEnabledByDefault: true);

public static DiagnosticDescriptor InvalidAsParametersAbstractType { get; } = new(
"RDG005",
new LocalizableResourceString(nameof(Resources.InvalidAsParametersAbstractType_Title), Resources.ResourceManager, typeof(Resources)),
new LocalizableResourceString(nameof(Resources.InvalidAsParametersAbstractType_Message), Resources.ResourceManager, typeof(Resources)),
"Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static DiagnosticDescriptor InvalidAsParametersSignature { get; } = new(
"RDG006",
new LocalizableResourceString(nameof(Resources.InvalidAsParametersSignature_Title), Resources.ResourceManager, typeof(Resources)),
new LocalizableResourceString(nameof(Resources.InvalidAsParametersSignature_Message), Resources.ResourceManager, typeof(Resources)),
"Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static DiagnosticDescriptor InvalidAsParametersNoConstructorFound { get; } = new(
"RDG007",
new LocalizableResourceString(nameof(Resources.InvalidAsParametersNoConstructorFound_Title), Resources.ResourceManager, typeof(Resources)),
new LocalizableResourceString(nameof(Resources.InvalidAsParametersNoConstructorFound_Message), Resources.ResourceManager, typeof(Resources)),
"Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static DiagnosticDescriptor InvalidAsParametersSingleConstructorOnly { get; } = new(
"RDG008",
new LocalizableResourceString(nameof(Resources.InvalidAsParametersSingleConstructorOnly_Title), Resources.ResourceManager, typeof(Resources)),
new LocalizableResourceString(nameof(Resources.InvalidAsParametersSingleConstructorOnly_Message), Resources.ResourceManager, typeof(Resources)),
"Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static DiagnosticDescriptor InvalidAsParametersNested { get; } = new(
"RDG009",
new LocalizableResourceString(nameof(Resources.InvalidAsParametersNested_Title), Resources.ResourceManager, typeof(Resources)),
new LocalizableResourceString(nameof(Resources.InvalidAsParametersNested_Message), Resources.ResourceManager, typeof(Resources)),
"Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static DiagnosticDescriptor InvalidAsParametersNullable { get; } = new(
"RDG010",
new LocalizableResourceString(nameof(Resources.InvalidAsParametersNullable_Title), Resources.ResourceManager, typeof(Resources)),
new LocalizableResourceString(nameof(Resources.InvalidAsParametersNullable_Message), Resources.ResourceManager, typeof(Resources)),
"Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true);
}
6 changes: 6 additions & 0 deletions src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
.Select((endpoints, _) =>
{
var requiresMetadataHelperTypes = endpoints.Any(endpoint => endpoint!.EmitterContext.RequiresMetadataHelperTypes);
var hasAsParameters = endpoints.Any(endpoint => endpoint!.EmitterContext.HasAsParameters);

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

if (hasAsParameters)
{
codeWriter.WriteLine(RequestDelegateGeneratorSources.PropertyAsParameterInfoClass);
}

return stringWriter.ToString();
});

Expand Down
94 changes: 94 additions & 0 deletions src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,100 @@ public void InvalidFormRequestBody(string parameterTypeName, string parameterNam
}
""";

public static string PropertyAsParameterInfoClass = """
file class PropertyAsParameterInfo : ParameterInfo
{
private readonly PropertyInfo _underlyingProperty;
private readonly ParameterInfo? _constructionParameterInfo;

public PropertyAsParameterInfo(bool isOptional, PropertyInfo propertyInfo)
{
Debug.Assert(null != propertyInfo);

AttrsImpl = (ParameterAttributes)propertyInfo.Attributes;
NameImpl = propertyInfo.Name;
MemberImpl = propertyInfo;
ClassImpl = propertyInfo.PropertyType;

// It is not a real parameter in the delegate, so,
// not defining a real position.
PositionImpl = -1;

_underlyingProperty = propertyInfo;
IsOptional = isOptional;
}

public PropertyAsParameterInfo(bool isOptional, PropertyInfo property, ParameterInfo? parameterInfo)
: this(isOptional, property)
{
_constructionParameterInfo = parameterInfo;
}

public override bool HasDefaultValue
=> _constructionParameterInfo is not null && _constructionParameterInfo.HasDefaultValue;
public override object? DefaultValue
=> _constructionParameterInfo?.DefaultValue;
public override int MetadataToken => _underlyingProperty.MetadataToken;
public override object? RawDefaultValue
=> _constructionParameterInfo?.RawDefaultValue;

public override object[] GetCustomAttributes(Type attributeType, bool inherit)
{
var attributes = _constructionParameterInfo?.GetCustomAttributes(attributeType, inherit);

if (attributes == null || attributes is { Length: 0 })
{
attributes = _underlyingProperty.GetCustomAttributes(attributeType, inherit);
}

return attributes;
}

public override object[] GetCustomAttributes(bool inherit)
{
var constructorAttributes = _constructionParameterInfo?.GetCustomAttributes(inherit);

if (constructorAttributes == null || constructorAttributes is { Length: 0 })
{
return _underlyingProperty.GetCustomAttributes(inherit);
}

var propertyAttributes = _underlyingProperty.GetCustomAttributes(inherit);

// Since the constructors attributes should take priority we will add them first,
// as we usually call it as First() or FirstOrDefault() in the argument creation
var mergedAttributes = new object[constructorAttributes.Length + propertyAttributes.Length];
Array.Copy(constructorAttributes, mergedAttributes, constructorAttributes.Length);
Array.Copy(propertyAttributes, 0, mergedAttributes, constructorAttributes.Length, propertyAttributes.Length);

return mergedAttributes;
}

public override IList<CustomAttributeData> GetCustomAttributesData()
{
var attributes = new List<CustomAttributeData>(
_constructionParameterInfo?.GetCustomAttributesData() ?? Array.Empty<CustomAttributeData>());
attributes.AddRange(_underlyingProperty.GetCustomAttributesData());

return attributes.AsReadOnly();
}

public override Type[] GetOptionalCustomModifiers()
=> _underlyingProperty.GetOptionalCustomModifiers();

public override Type[] GetRequiredCustomModifiers()
=> _underlyingProperty.GetRequiredCustomModifiers();

public override bool IsDefined(Type attributeType, bool inherit)
{
return (_constructionParameterInfo is not null && _constructionParameterInfo.IsDefined(attributeType, inherit)) ||
_underlyingProperty.IsDefined(attributeType, inherit);
}

public new bool IsOptional { get; }
}
""";

public static string GetGeneratedRouteBuilderExtensionsSource(string genericThunks, string thunks, string endpoints, string helperMethods, string helperTypes) => $$"""
{{SourceHeader}}

Expand Down
90 changes: 63 additions & 27 deletions src/Http/Http.Extensions/gen/Resources.resx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema

Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes

The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.

Example:

... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
Expand All @@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple

There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the

Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not

The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can

Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.

mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.

mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.

mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
Expand Down Expand Up @@ -141,4 +141,40 @@
<data name="UnableToResolveAnonymousReturnType_Title" xml:space="preserve">
<value>Unable to resolve anonymous type</value>
</data>
<data name="InvalidAsParametersAbstractType_Title" xml:space="preserve">
<value>Invalid abstract type</value>
</data>
<data name="InvalidAsParametersAbstractType_Message" xml:space="preserve">
<value>The abstract type '{0}' is not supported.</value>
</data>
<data name="InvalidAsParametersSignature_Title" xml:space="preserve">
<value>Invalid constructor parameters</value>
</data>
<data name="InvalidAsParametersSignature_Message" xml:space="preserve">
<value>The public parameterized constructor must contain only parameters that match the declared public properties for type '{0}'.</value>
</data>
<data name="InvalidAsParametersNoConstructorFound_Title" xml:space="preserve">
<value>No valid constructor found</value>
</data>
<data name="InvalidAsParametersNoConstructorFound_Message" xml:space="preserve">
<value>No public parameterless constructor found for type '{0}'.</value>
</data>
<data name="InvalidAsParametersSingleConstructorOnly_Title" xml:space="preserve">
<value>Multiple parameterized constructors found</value>
</data>
<data name="InvalidAsParametersSingleConstructorOnly_Message" xml:space="preserve">
<value>Only a single public parameterized constructor is allowed for type '{0}'.</value>
</data>
<data name="InvalidAsParametersNested_Title" xml:space="preserve">
<value>Nested AsParameters</value>
</data>
<data name="InvalidAsParametersNested_Message" xml:space="preserve">
<value>Nested AsParametersAttribute is not supported and should be used only for handler parameters.</value>
</data>
<data name="InvalidAsParametersNullable_Title" xml:space="preserve">
<value>Unexpected nullable parameter</value>
</data>
<data name="InvalidAsParametersNullable_Message" xml:space="preserve">
<value>The nullable type '{0}' is not supported. Mark the parameter as non-nullable.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ internal sealed class EmitterContext
public bool HasBindAsync { get; set; }
public bool HasParsable { get; set; }
public bool HasJsonResponse { get; set; }
public bool HasAsParameters { get; set; }
public bool RequiresLoggingHelper { get; set; }
public bool RequiresMetadataHelperTypes { get; set; }
public bool HasEndpointMetadataProvider { get; set; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Analyzers.Infrastructure;
using Microsoft.CodeAnalysis;
Expand All @@ -12,6 +13,7 @@ internal static class EmitterExtensions
{
EndpointParameterSource.Header => "header",
EndpointParameterSource.Query => "query string",
EndpointParameterSource.Route => "route",
EndpointParameterSource.RouteOrQuery => "route or query string",
EndpointParameterSource.FormBody => "form",
EndpointParameterSource.BindAsync => endpointParameter.BindMethod == BindabilityMethod.BindAsync
Expand All @@ -30,4 +32,13 @@ public static bool IsSerializableJsonResponse(this EndpointResponse endpointResp
}
return false;
}

public static string EmitHandlerArgument(this EndpointParameter endpointParameter) => $"{endpointParameter.SymbolName}_local";

public static string EmitArgument(this EndpointParameter endpointParameter) => endpointParameter.Source switch
{
EndpointParameterSource.JsonBody or EndpointParameterSource.Route or EndpointParameterSource.RouteOrQuery or EndpointParameterSource.JsonBodyOrService or EndpointParameterSource.FormBody => endpointParameter.IsOptional ? endpointParameter.EmitHandlerArgument() : $"{endpointParameter.EmitHandlerArgument()}!",
EndpointParameterSource.Unknown => throw new NotImplementedException("Unreachable!"),
_ => endpointParameter.EmitHandlerArgument()
};
}
Loading