File: Internal\GrpcJsonTranscodingDescriptionProvider.cs
Web Access
Project: src\src\Grpc\JsonTranscoding\src\Microsoft.AspNetCore.Grpc.Swagger\Microsoft.AspNetCore.Grpc.Swagger.csproj (Microsoft.AspNetCore.Grpc.Swagger)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Linq;
using System.Text;
using Google.Api;
using Google.Protobuf.Reflection;
using Grpc.AspNetCore.Server;
using Grpc.Shared;
using Microsoft.AspNetCore.Grpc.JsonTranscoding;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Routing;
 
namespace Microsoft.AspNetCore.Grpc.Swagger.Internal;
 
internal sealed class GrpcJsonTranscodingDescriptionProvider : IApiDescriptionProvider
{
    private readonly EndpointDataSource _endpointDataSource;
    private readonly DescriptorRegistry _descriptorRegistry;
 
    public GrpcJsonTranscodingDescriptionProvider(EndpointDataSource endpointDataSource, DescriptorRegistry descriptorRegistry)
    {
        _endpointDataSource = endpointDataSource;
        _descriptorRegistry = descriptorRegistry;
    }
 
    // Executes after ASP.NET Core
    public int Order => -900;
 
    public void OnProvidersExecuting(ApiDescriptionProviderContext context)
    {
        var endpoints = _endpointDataSource.Endpoints;
 
        foreach (var endpoint in endpoints)
        {
            if (endpoint is RouteEndpoint routeEndpoint)
            {
                var grpcMetadata = endpoint.Metadata.GetMetadata<GrpcJsonTranscodingMetadata>();
 
                if (grpcMetadata != null)
                {
                    var httpRule = grpcMetadata.HttpRule;
                    var methodDescriptor = grpcMetadata.MethodDescriptor;
 
                    if (ServiceDescriptorHelpers.TryResolvePattern(grpcMetadata.HttpRule, out var pattern, out var verb))
                    {
                        var apiDescription = CreateApiDescription(routeEndpoint, httpRule, methodDescriptor, pattern, verb);
                        context.Results.Add(apiDescription);
 
                        _descriptorRegistry.RegisterFileDescriptor(grpcMetadata.MethodDescriptor.File);
                    }
                }
            }
        }
    }
 
    private static ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, HttpRule httpRule, MethodDescriptor methodDescriptor, string pattern, string verb)
    {
        var apiDescription = new ApiDescription();
        apiDescription.HttpMethod = verb;
        apiDescription.ActionDescriptor = new ActionDescriptor
        {
            RouteValues = new Dictionary<string, string?>
            {
                // Swagger uses this to group endpoints together.
                // Group methods together using the service name.
                ["controller"] = methodDescriptor.Service.Name
            },
            EndpointMetadata = routeEndpoint.Metadata.ToList()
        };
        apiDescription.SupportedRequestFormats.Add(new ApiRequestFormat { MediaType = "application/json" });
 
        var responseBodyDescriptor = ServiceDescriptorHelpers.ResolveResponseBodyDescriptor(httpRule.ResponseBody, methodDescriptor);
        var responseType = responseBodyDescriptor != null ? MessageDescriptorHelpers.ResolveFieldType(responseBodyDescriptor) : methodDescriptor.OutputType.ClrType;
        apiDescription.SupportedResponseTypes.Add(new ApiResponseType
        {
            ApiResponseFormats = { new ApiResponseFormat { MediaType = "application/json" } },
            ModelMetadata = new GrpcModelMetadata(ModelMetadataIdentity.ForType(responseType)),
            StatusCode = 200
        });
        apiDescription.SupportedResponseTypes.Add(new ApiResponseType
        {
            ApiResponseFormats = { new ApiResponseFormat { MediaType = "application/json" } },
            ModelMetadata = new GrpcModelMetadata(ModelMetadataIdentity.ForType(typeof(Google.Rpc.Status))),
            IsDefaultResponse = true
        });
        var explorerSettings = routeEndpoint.Metadata.GetMetadata<ApiExplorerSettingsAttribute>();
        if (explorerSettings != null)
        {
            apiDescription.GroupName = explorerSettings.GroupName;
        }
 
        var methodMetadata = routeEndpoint.Metadata.GetMetadata<GrpcMethodMetadata>()!;
        var httpRoutePattern = HttpRoutePattern.Parse(pattern);
        var routeParameters = ServiceDescriptorHelpers.ResolveRouteParameterDescriptors(httpRoutePattern.Variables, methodDescriptor.InputType);
 
        apiDescription.RelativePath = ResolvePath(httpRoutePattern, routeParameters);
 
        foreach (var routeParameter in routeParameters)
        {
            var field = routeParameter.Value.DescriptorsPath.Last();
            var parameterName = ServiceDescriptorHelpers.FormatUnderscoreName(field.Name, pascalCase: true, preservePeriod: false);
            var propertyInfo = field.ContainingType.ClrType.GetProperty(parameterName);
 
            // If from a property, create model as property to get its XML comments.
            var identity = propertyInfo != null
                ? ModelMetadataIdentity.ForProperty(propertyInfo, MessageDescriptorHelpers.ResolveFieldType(field), field.ContainingType.ClrType)
                : ModelMetadataIdentity.ForType(MessageDescriptorHelpers.ResolveFieldType(field));
 
            apiDescription.ParameterDescriptions.Add(new ApiParameterDescription
            {
                Name = routeParameter.Value.JsonPath,
                ModelMetadata = new GrpcModelMetadata(identity),
                Source = BindingSource.Path,
                DefaultValue = string.Empty
            });
        }
 
        var bodyDescriptor = ServiceDescriptorHelpers.ResolveBodyDescriptor(httpRule.Body, methodMetadata.ServiceType, methodDescriptor);
        if (bodyDescriptor != null)
        {
            // If from a property, create model as property to get its XML comments.
            var identity = bodyDescriptor.PropertyInfo != null
                ? ModelMetadataIdentity.ForProperty(bodyDescriptor.PropertyInfo, bodyDescriptor.PropertyInfo.PropertyType, bodyDescriptor.PropertyInfo.DeclaringType!)
                : ModelMetadataIdentity.ForType(bodyDescriptor.Descriptor.ClrType);
 
            // Or if from a parameter, create model as parameter to get its XML comments.
            var parameterDescriptor = bodyDescriptor.ParameterInfo != null
                ? new ControllerParameterDescriptor { ParameterInfo = bodyDescriptor.ParameterInfo }
                : null;
 
            apiDescription.ParameterDescriptions.Add(new ApiParameterDescription
            {
                Name = "Input",
                ModelMetadata = new GrpcModelMetadata(identity),
                Source = BindingSource.Body,
                ParameterDescriptor = parameterDescriptor!
            });
        }
 
        var queryParameters = ServiceDescriptorHelpers.ResolveQueryParameterDescriptors(routeParameters, methodDescriptor, bodyDescriptor?.Descriptor, bodyDescriptor?.FieldDescriptor);
        foreach (var queryDescription in queryParameters)
        {
            var field = queryDescription.Value;
            var propertyInfo = field.ContainingType.ClrType.GetProperty(field.PropertyName);
 
            // If from a property, create model as property to get its XML comments.
            var identity = propertyInfo != null
                ? ModelMetadataIdentity.ForProperty(propertyInfo, MessageDescriptorHelpers.ResolveFieldType(field), field.ContainingType.ClrType)
                : ModelMetadataIdentity.ForType(MessageDescriptorHelpers.ResolveFieldType(field));
 
            apiDescription.ParameterDescriptions.Add(new ApiParameterDescription
            {
                Name = queryDescription.Key,
                ModelMetadata = new GrpcModelMetadata(identity),
                Source = BindingSource.Query,
                DefaultValue = string.Empty
            });
        }
 
        return apiDescription;
    }
 
    private static string ResolvePath(HttpRoutePattern httpRoutePattern, Dictionary<string, RouteParameter> routeParameters)
    {
        var sb = new StringBuilder();
        for (var i = 0; i < httpRoutePattern.Segments.Count; i++)
        {
            if (sb.Length > 0)
            {
                sb.Append('/');
            }
            var routeParameter = routeParameters.SingleOrDefault(kvp => kvp.Value.RouteVariable.StartSegment == i).Value;
            if (routeParameter != null)
            {
                sb.Append('{');
                sb.Append(routeParameter.JsonPath);
                sb.Append('}');
 
                // Skip segments if variable is multiple segment.
                i = routeParameter.RouteVariable.EndSegment - 1;
            }
            else
            {
                sb.Append(httpRoutePattern.Segments[i]);
            }
        }
        if (httpRoutePattern.Verb != null)
        {
            sb.Append(':');
            sb.Append(httpRoutePattern.Verb);
        }
        return sb.ToString();
    }
 
    public void OnProvidersExecuted(ApiDescriptionProviderContext context)
    {
        // no-op
    }
}