Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Linq;
using System.Text;

namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel.Emitters;
Expand All @@ -21,8 +22,14 @@ internal static string EmitParameterPreparation(this Endpoint endpoint)
Source: EndpointParameterSource.SpecialType
} => parameter.EmitSpecialParameterPreparation(),
{
Source: EndpointParameterSource.Query,
} => parameter.EmitQueryParameterPreparation(),
Source: EndpointParameterSource.Query or EndpointParameterSource.Header,
} => parameter.EmitQueryOrHeaderParameterPreparation(),
{
Source: EndpointParameterSource.Route,
} => parameter.EmitRouteParameterPreparation(),
{
Source: EndpointParameterSource.RouteOrQuery
} => parameter.EmitRouteOrQueryParameterPreparation(),
{
Source: EndpointParameterSource.JsonBody
} => parameter.EmitJsonBodyParameterPreparationString(),
Expand All @@ -45,4 +52,6 @@ internal static string EmitParameterPreparation(this Endpoint endpoint)

return parameterPreparationBuilder.ToString();
}

public static string EmitArgumentList(this Endpoint endpoint) => string.Join(", ", endpoint.Parameters.Select(p => p.EmitArgument()));
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// 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.Text;

namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel.Emitters;
Expand All @@ -9,22 +10,22 @@ internal static class EndpointParameterEmitter
internal static string EmitSpecialParameterPreparation(this EndpointParameter endpointParameter)
{
return $"""
var {endpointParameter.Name}_local = {endpointParameter.AssigningCode};
var {endpointParameter.EmitHandlerArgument()} = {endpointParameter.AssigningCode};
""";
}

internal static string EmitQueryParameterPreparation(this EndpointParameter endpointParameter)
internal static string EmitQueryOrHeaderParameterPreparation(this EndpointParameter endpointParameter)
{
var builder = new StringBuilder();

// Preamble for diagnostics purposes.
builder.AppendLine($"""
{endpointParameter.EmitParameterDiagnosticComment()}
""");

// Grab raw input from HttpContext.
var assigningCode = endpointParameter.Source is EndpointParameterSource.Header
? $"httpContext.Request.Headers[\"{endpointParameter.Name}\"]"
: $"httpContext.Request.Query[\"{endpointParameter.Name}\"]";
builder.AppendLine($$"""
var {{endpointParameter.Name}}_raw = {{endpointParameter.AssigningCode}};
var {{endpointParameter.EmitAssigningCodeResult()}} = {{assigningCode}};
""");

// If we are not optional, then at this point we can just assign the string value to the handler argument,
Expand All @@ -34,35 +35,103 @@ internal static string EmitQueryParameterPreparation(this EndpointParameter endp
if (endpointParameter.IsOptional)
{
builder.AppendLine($$"""
var {{endpointParameter.HandlerArgument}} = {{endpointParameter.Name}}_raw.Count > 0 ? {{endpointParameter.Name}}_raw.ToString() : null;
var {{endpointParameter.EmitHandlerArgument()}} = {{endpointParameter.EmitAssigningCodeResult()}}.Count > 0 ? {{endpointParameter.EmitAssigningCodeResult()}}.ToString() : null;
""");
}
else
{
builder.AppendLine($$"""
if (StringValues.IsNullOrEmpty({{endpointParameter.Name}}_raw))
if (StringValues.IsNullOrEmpty({{endpointParameter.EmitAssigningCodeResult()}}))
{
wasParamCheckFailure = true;
}
var {{endpointParameter.HandlerArgument}} = {{endpointParameter.Name}}_raw.ToString();
var {{endpointParameter.EmitHandlerArgument()}} = {{endpointParameter.EmitAssigningCodeResult()}}.ToString();
""");
}

return builder.ToString();
}

internal static string EmitJsonBodyParameterPreparationString(this EndpointParameter endpointParameter)
internal static string EmitRouteParameterPreparation(this EndpointParameter endpointParameter)
{
var builder = new StringBuilder();
builder.AppendLine($"""
{endpointParameter.EmitParameterDiagnosticComment()}
""");

// Preamble for diagnostics purposes.
// Throw an exception of if the route parameter name that was specific in the `FromRoute`
// attribute or in the parameter name does not appear in the actual route.
builder.AppendLine($$"""
if (options?.RouteParameterNames?.Contains("{{endpointParameter.Name}}", StringComparer.OrdinalIgnoreCase) != true)
{
throw new InvalidOperationException($"'{{endpointParameter.Name}}' is not a route parameter.");
}
""");

var assigningCode = $"httpContext.Request.RouteValues[\"{endpointParameter.Name}\"]?.ToString()";
builder.AppendLine($$"""
var {{endpointParameter.EmitAssigningCodeResult()}} = {{assigningCode}};
""");

if (!endpointParameter.IsOptional)
{
builder.AppendLine($$"""
if ({{endpointParameter.EmitAssigningCodeResult()}} == null)
{
wasParamCheckFailure = true;
}
""");
}
builder.AppendLine($"""
var {endpointParameter.EmitHandlerArgument()} = {endpointParameter.EmitAssigningCodeResult()};
""");

return builder.ToString();
}

internal static string EmitRouteOrQueryParameterPreparation(this EndpointParameter endpointParameter)
{
var builder = new StringBuilder();
builder.AppendLine($"""
{endpointParameter.EmitParameterDiagnosticComment()}
""");

// Grab raw input from HttpContext.
var parameterName = endpointParameter.Name;
var assigningCode = $@"options?.RouteParameterNames?.Contains(""{parameterName}"", StringComparer.OrdinalIgnoreCase) == true";
assigningCode += $@"? new StringValues(httpContext.Request.RouteValues[$""{parameterName}""]?.ToString())";
assigningCode += $@": httpContext.Request.Query[$""{parameterName}""];";

builder.AppendLine($$"""
var (isSuccessful, {{endpointParameter.Name}}_local) = {{endpointParameter.AssigningCode}};
var {{endpointParameter.EmitAssigningCodeResult()}} = {{assigningCode}};
""");

if (!endpointParameter.IsOptional)
{
builder.AppendLine($$"""
if ({{endpointParameter.EmitAssigningCodeResult()}} is StringValues { Count: 0 })
{
wasParamCheckFailure = true;
}
""");
}

builder.AppendLine($"""
var {endpointParameter.EmitHandlerArgument()} = {endpointParameter.EmitAssigningCodeResult()};
""");

return builder.ToString();
}

internal static string EmitJsonBodyParameterPreparationString(this EndpointParameter endpointParameter)
{
var builder = new StringBuilder();
builder.AppendLine($"""
{endpointParameter.EmitParameterDiagnosticComment()}
""");

var assigningCode = $"await GeneratedRouteBuilderExtensionsCore.TryResolveBody<{endpointParameter.Type.ToDisplayString(EmitterConstants.DisplayFormat)}>(httpContext, {(endpointParameter.IsOptional ? "true" : "false")})";
builder.AppendLine($$"""
var (isSuccessful, {{endpointParameter.EmitHandlerArgument()}}) = {{assigningCode}};
""");

// If binding from the JSON body fails, we exit early. Don't
Expand All @@ -88,14 +157,32 @@ internal static string EmitServiceParameterPreparation(this EndpointParameter en
""");

// Requiredness checks for services are handled by the distinction
// between GetRequiredService and GetService in the AssigningCode.
// between GetRequiredService and GetService in the assigningCode.
// Unlike other scenarios, this will result in an exception being thrown
// at runtime.
var assigningCode = endpointParameter.IsOptional ?
$"httpContext.RequestServices.GetService<{endpointParameter.Type}>();" :
$"httpContext.RequestServices.GetRequiredService<{endpointParameter.Type}>()";

builder.AppendLine($$"""
var {{endpointParameter.HandlerArgument}} = {{endpointParameter.AssigningCode}};
var {{endpointParameter.EmitHandlerArgument()}} = {{assigningCode}};
""");

return builder.ToString();
}

private static string EmitParameterDiagnosticComment(this EndpointParameter endpointParameter) =>
$"// Endpoint Parameter: {endpointParameter.Name} (Type = {endpointParameter.Type}, IsOptional = {endpointParameter.IsOptional}, Source = {endpointParameter.Source})";
$"// Endpoint Parameter: {endpointParameter.Name} (Type = {endpointParameter.Type.ToDisplayString(EmitterConstants.DisplayFormat)}, IsOptional = {endpointParameter.IsOptional}, Source = {endpointParameter.Source})";

private static string EmitHandlerArgument(this EndpointParameter endpointParameter) => $"{endpointParameter.Name}_local";
private static string EmitAssigningCodeResult(this EndpointParameter endpointParameter) => $"{endpointParameter.Name}_raw";

public static string EmitArgument(this EndpointParameter endpointParameter) => endpointParameter.Source switch
{
EndpointParameterSource.JsonBody or EndpointParameterSource.Route or EndpointParameterSource.RouteOrQuery => endpointParameter.IsOptional
? endpointParameter.EmitHandlerArgument()
: $"{endpointParameter.EmitHandlerArgument()}!",
EndpointParameterSource.Unknown => throw new Exception("Unreachable!"),
_ => endpointParameter.EmitHandlerArgument()
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel;

internal class Endpoint
{
private string? _argumentListCache;

public Endpoint(IInvocationOperation operation, WellKnownTypes wellKnownTypes)
{
Operation = operation;
Expand Down Expand Up @@ -67,8 +65,6 @@ public Endpoint(IInvocationOperation operation, WellKnownTypes wellKnownTypes)
public string? RoutePattern { get; }
public EndpointResponse? Response { get; }
public EndpointParameter[] Parameters { get; } = Array.Empty<EndpointParameter>();
public string EmitArgumentList() => _argumentListCache ??= string.Join(", ", Parameters.Select(p => p.EmitArgument()));

public List<DiagnosticDescriptor> Diagnostics { get; } = new List<DiagnosticDescriptor>();

public (string File, int LineNumber) Location { get; }
Expand All @@ -91,7 +87,7 @@ public static bool SignatureEquals(Endpoint a, Endpoint b)

for (var i = 0; i < a.Parameters.Length; i++)
{
if (!a.Parameters[i].Equals(b.Parameters[i]))
if (!a.Parameters[i].SignatureEquals(b.Parameters[i]))
{
return false;
}
Expand All @@ -108,7 +104,7 @@ public static int GetSignatureHashCode(Endpoint endpoint)

foreach (var parameter in endpoint.Parameters)
{
hashCode.Add(parameter);
hashCode.Add(parameter.Type, SymbolEqualityComparer.Default);
}

return hashCode.ToHashCode();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,39 +17,50 @@ public EndpointParameter(IParameterSymbol parameter, WellKnownTypes wellKnownTyp
Type = parameter.Type;
Name = parameter.Name;
Source = EndpointParameterSource.Unknown;
HandlerArgument = $"{parameter.Name}_local";

var fromQueryMetadataInterfaceType = wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromQueryMetadata);
var fromServiceMetadataInterfaceType = wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromServiceMetadata);
var fromRouteMetadataInterfaceType = wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromRouteMetadata);
var fromHeaderMetadataInterfaceType = wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromHeaderMetadata);

if (parameter.HasAttributeImplementingInterface(fromQueryMetadataInterfaceType))
if (parameter.HasAttributeImplementingInterface(fromRouteMetadataInterfaceType, out var fromRouteAttribute))
{
Source = EndpointParameterSource.Route;
Name = GetParameterName(fromRouteAttribute, parameter.Name);
IsOptional = parameter.IsOptional();
}
else if (parameter.HasAttributeImplementingInterface(fromQueryMetadataInterfaceType, out var fromQueryAttribute))
{
Source = EndpointParameterSource.Query;
AssigningCode = $"httpContext.Request.Query[\"{parameter.Name}\"]";
IsOptional = parameter.Type is INamedTypeSymbol
{
NullableAnnotation: NullableAnnotation.Annotated
};
Name = GetParameterName(fromQueryAttribute, parameter.Name);
IsOptional = parameter.IsOptional();
}
else if (parameter.HasAttributeImplementingInterface(fromHeaderMetadataInterfaceType, out var fromHeaderAttribute))
{
Source = EndpointParameterSource.Header;
Name = GetParameterName(fromHeaderAttribute, parameter.Name);
IsOptional = parameter.IsOptional();
}
else if (TryGetExplicitFromJsonBody(parameter, wellKnownTypes, out var jsonBodyAssigningCode, out var isOptional))
else if (TryGetExplicitFromJsonBody(parameter, wellKnownTypes, out var isOptional))
{
Source = EndpointParameterSource.JsonBody;
AssigningCode = jsonBodyAssigningCode;
IsOptional = isOptional;
}
else if (parameter.HasAttributeImplementingInterface(fromServiceMetadataInterfaceType))
{
Source = EndpointParameterSource.Service;
IsOptional = parameter.Type is INamedTypeSymbol { NullableAnnotation: NullableAnnotation.Annotated } || parameter.HasExplicitDefaultValue;
AssigningCode = IsOptional ?
$"httpContext.RequestServices.GetService<{parameter.Type}>();" :
$"httpContext.RequestServices.GetRequiredService<{parameter.Type}>()";
}
else if (TryGetSpecialTypeAssigningCode(Type, wellKnownTypes, out var specialTypeAssigningCode))
{
Source = EndpointParameterSource.SpecialType;
AssigningCode = specialTypeAssigningCode;
}
else if (parameter.Type.SpecialType == SpecialType.System_String)
{
Source = EndpointParameterSource.RouteOrQuery;
IsOptional = parameter.IsOptional();
}
else
{
// TODO: Inferencing rules go here - but for now:
Expand All @@ -60,19 +71,12 @@ public EndpointParameter(IParameterSymbol parameter, WellKnownTypes wellKnownTyp
public ITypeSymbol Type { get; }
public EndpointParameterSource Source { get; }

// TODO: If the parameter has [FromRoute("AnotherName")] or similar, prefer that.
// Only used for SpecialType parameters that need
// to be resolved by a specific WellKnownType
internal string? AssigningCode { get; set; }
public string Name { get; }
public string? AssigningCode { get; }
public string HandlerArgument { get; }
public bool IsOptional { get; }

public string EmitArgument() => Source switch
{
EndpointParameterSource.SpecialType or EndpointParameterSource.Query or EndpointParameterSource.Service => HandlerArgument,
EndpointParameterSource.JsonBody => IsOptional ? HandlerArgument : $"{HandlerArgument}!",
_ => throw new Exception("Unreachable!")
};

// TODO: Handle special form types like IFormFileCollection that need special body-reading logic.
private static bool TryGetSpecialTypeAssigningCode(ITypeSymbol type, WellKnownTypes wellKnownTypes, [NotNullWhen(true)] out string? callingCode)
{
Expand Down Expand Up @@ -118,33 +122,34 @@ private static bool TryGetSpecialTypeAssigningCode(ITypeSymbol type, WellKnownTy

private static bool TryGetExplicitFromJsonBody(IParameterSymbol parameter,
WellKnownTypes wellKnownTypes,
[NotNullWhen(true)] out string? assigningCode,
out bool isOptional)
{
assigningCode = null;
isOptional = false;
if (parameter.HasAttributeImplementingInterface(wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromBodyMetadata), out var fromBodyAttribute))
{
foreach (var namedArgument in fromBodyAttribute.NamedArguments)
{
if (namedArgument.Key == "AllowEmpty")
{
isOptional |= namedArgument.Value.Value is true;
}
}
isOptional |= (parameter.NullableAnnotation == NullableAnnotation.Annotated || parameter.HasExplicitDefaultValue);
assigningCode = $"await GeneratedRouteBuilderExtensionsCore.TryResolveBody<{parameter.Type}>(httpContext, {(isOptional ? "true" : "false")})";
return true;
if (!parameter.HasAttributeImplementingInterface(wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromBodyMetadata), out var fromBodyAttribute))
{
return false;
}
return false;
isOptional |= fromBodyAttribute.TryGetNamedArgumentValue<int>("EmptyBodyBehavior", out var emptyBodyBehaviorValue) && emptyBodyBehaviorValue == 1;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this code still handle a custom IFromBodyMetadata attribute when the AllowEmpty property is set in the attribute?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was a little iffy on whether or not we should support both. We use the custom FromBodyAttribute implementation in our RDF tests (ref) but I don't think this is a common pattern in user code.

I ended up adding it back since we're not in a position to support FromBody vs. FromService inference and might need to implement a FromBody attribute in our own source to avoid importing the Mvc namespace.

isOptional |= fromBodyAttribute.TryGetNamedArgumentValue<bool>("AllowEmpty", out var allowEmptyValue) && allowEmptyValue;
isOptional |= (parameter.NullableAnnotation == NullableAnnotation.Annotated || parameter.HasExplicitDefaultValue);
return true;
}

private static string GetParameterName(AttributeData attribute, string parameterName) =>
attribute.TryGetNamedArgumentValue<string>("Name", out var fromSourceName)
? (fromSourceName ?? parameterName)
: parameterName;

public override bool Equals(object obj) =>
obj is EndpointParameter other &&
other.Source == Source &&
other.Name == Name &&
SymbolEqualityComparer.Default.Equals(other.Type, Type);

public bool SignatureEquals(object obj) =>
obj is EndpointParameter other &&
SymbolEqualityComparer.Default.Equals(other.Type, Type);

public override int GetHashCode()
{
var hashCode = new HashCode();
Expand Down
Loading