Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
lineNumber);
}
""");
}
}

return code.ToString();
});
Expand Down
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}>(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})";

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,56 @@ 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 = fromRouteAttribute.TryGetNamedArgumentValue<string>("Name", out var fromRouteName)
? fromRouteName
: 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 = fromQueryAttribute.TryGetNamedArgumentValue<string>("Name", out var fromQueryName)
? fromQueryName
: parameter.Name;
IsOptional = parameter.IsOptional();
}
else if (parameter.HasAttributeImplementingInterface(fromHeaderMetadataInterfaceType, out var fromHeaderAttribute))
{
Source = EndpointParameterSource.Header;
Name = fromHeaderAttribute.TryGetNamedArgumentValue<string>("Name", out var fromHeaderName)
? fromHeaderName
: 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 +77,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.
// Omly 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,25 +128,16 @@ 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 |= (parameter.NullableAnnotation == NullableAnnotation.Annotated || parameter.HasExplicitDefaultValue);
return true;
}

public override bool Equals(object obj) =>
Expand All @@ -145,6 +146,10 @@ obj is EndpointParameter other &&
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