| File: StaticRouteHandlerModel\Endpoint.cs | Web Access |
| Project: src\aspnetcore\src\Http\Http.Extensions\gen\Microsoft.AspNetCore.Http.RequestDelegateGenerator\Microsoft.AspNetCore.Http.RequestDelegateGenerator.csproj (Microsoft.AspNetCore.Http.RequestDelegateGenerator) |
// 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.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Analyzers.Infrastructure; using Microsoft.AspNetCore.App.Analyzers.Infrastructure; using Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel.Emitters; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Operations; namespace Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel; internal class Endpoint { public Endpoint(IInvocationOperation operation, WellKnownTypes wellKnownTypes, SemanticModel semanticModel) { Operation = operation; Location = GetLocation(operation); #pragma warning disable RSEXPERIMENTAL002 // Experimental interceptable location API InterceptableLocation = semanticModel.GetInterceptableLocation((InvocationExpressionSyntax)operation.Syntax)!; #pragma warning restore RSEXPERIMENTAL002 HttpMethod = GetHttpMethod(operation); EmitterContext = new EmitterContext(); if (!operation.TryGetRouteHandlerMethod(semanticModel, out var method)) { Diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.UnableToResolveMethod, Operation.Syntax.GetLocation())); return; } Response = new EndpointResponse(method, wellKnownTypes); Response.EmitRequiredDiagnostics(Diagnostics, Operation.Syntax.GetLocation()); IsAwaitable = Response?.IsAwaitable == true; EmitterContext.HasResponseMetadata = Response is { } response && !(response.IsIResult || response.HasNoResponse); // NOTE: We set this twice. It is possible that we don't have any parameters so we // want this to be true if the response type implements IEndpointMetadataProvider. // Later on we set this to be true if the parameters or the response type // implement the interface. EmitterContext.HasEndpointMetadataProvider = Response!.IsEndpointMetadataProvider; if (method.Parameters.Length == 0) { EmitterContext.RequiresLoggingHelper = false; return; } var parameters = new EndpointParameter[method.Parameters.Length]; for (var i = 0; i < method.Parameters.Length; i++) { var parameterSymbol = method.Parameters[i]; parameterSymbol.EmitRequiredDiagnostics(Diagnostics, Operation.Syntax.GetLocation()); if (Diagnostics.Count > 0) { continue; } var parameter = new EndpointParameter(this, parameterSymbol, wellKnownTypes); switch (parameter.Source) { case EndpointParameterSource.BindAsync: switch (parameter.BindMethod) { case BindabilityMethod.IBindableFromHttpContext: case BindabilityMethod.BindAsyncWithParameter: NeedsParameterArray = true; break; } break; case EndpointParameterSource.Unknown: Diagnostics.Add(Diagnostic.Create( DiagnosticDescriptors.UnableToResolveParameterDescriptor, Operation.Syntax.GetLocation(), parameter.SymbolName)); break; } parameters[i] = parameter; } Parameters = parameters; EmitterContext.RequiresLoggingHelper = !Parameters.All(parameter => parameter is not null && parameter.Source == EndpointParameterSource.SpecialType || parameter is { IsArray: true, ElementType.SpecialType: SpecialType.System_String, Source: EndpointParameterSource.Query }); } public string HttpMethod { get; } public bool IsAwaitable { get; set; } public bool NeedsParameterArray { get; } public string? RoutePattern { get; } public EmitterContext EmitterContext { get; } public EndpointResponse? Response { get; } public EndpointParameter[] Parameters { get; } = Array.Empty<EndpointParameter>(); public List<Diagnostic> Diagnostics { get; } = new List<Diagnostic>(); public (string File, int LineNumber, int CharacterNumber) Location { get; } #pragma warning disable RSEXPERIMENTAL002 // Experimental interceptable location API public InterceptableLocation InterceptableLocation { get; } #pragma warning restore RSEXPERIMENTAL002 public IInvocationOperation Operation { get; } public override bool Equals(object o) => o is Endpoint other && InterceptableLocation == other.InterceptableLocation && SignatureEquals(this, other); public override int GetHashCode() => HashCode.Combine(Location, GetSignatureHashCode(this)); public static bool SignatureEquals(Endpoint a, Endpoint b) { if (!string.Equals(a.Response?.WrappedResponseTypeDisplayName, b.Response?.WrappedResponseTypeDisplayName, StringComparison.Ordinal) || !a.HttpMethod.Equals(b.HttpMethod, StringComparison.Ordinal) || a.Parameters.Length != b.Parameters.Length) { return false; } for (var i = 0; i < a.Parameters.Length; i++) { if (!a.Parameters[i].Equals(b.Parameters[i])) { return false; } } return true; } public static int GetSignatureHashCode(Endpoint endpoint) { var hashCode = new HashCode(); hashCode.Add(endpoint.Response?.WrappedResponseTypeDisplayName); hashCode.Add(endpoint.HttpMethod); foreach (var parameter in endpoint.Parameters) { hashCode.Add(parameter); } return hashCode.ToHashCode(); } private static (string, int, int) GetLocation(IInvocationOperation operation) { // The invocation expression consists of two properties: // - Expression: which is a `MemberAccessExpressionSyntax` that represents the method being invoked. // - ArgumentList: the list of arguments being invoked. // Here, we resolve the `MemberAccessExpressionSyntax` to get the location of the method being invoked. var memberAccessorExpression = ((MemberAccessExpressionSyntax)((InvocationExpressionSyntax)operation.Syntax).Expression); // The `MemberAccessExpressionSyntax` in turn includes three properties: // - Expression: the expression that is being accessed. // - OperatorToken: the operator token, typically the dot separate. // - Name: the name of the member being accessed, typically `MapGet` or `MapPost`, etc. // Here, we resolve the `Name` to extract the location of the method being invoked. var invocationNameSpan = memberAccessorExpression.Name.Span; // Resolve LineSpan associated with the name span so we can resolve the line and character number. var lineSpan = operation.Syntax.SyntaxTree.GetLineSpan(invocationNameSpan); // Resolve the filepath of the invocation while accounting for source mapped paths. var filePath = operation.Syntax.SyntaxTree.GetInterceptorFilePath(operation.SemanticModel?.Compilation.Options.SourceReferenceResolver); // LineSpan.LinePosition is 0-indexed, but we want to display 1-indexed line and character numbers in the interceptor attribute. return (filePath, lineSpan.StartLinePosition.Line + 1, lineSpan.StartLinePosition.Character + 1); } private static string GetHttpMethod(IInvocationOperation operation) { var syntax = (InvocationExpressionSyntax)operation.Syntax; var expression = (MemberAccessExpressionSyntax)syntax.Expression; var name = (IdentifierNameSyntax)expression.Name; var identifier = name.Identifier; return identifier.ValueText; } }