File: Internal\XmlComments\GrpcXmlCommentsOperationFilter.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.Reflection;
using System.Xml.XPath;
using Grpc.AspNetCore.Server;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
 
namespace Microsoft.AspNetCore.Grpc.Swagger.Internal.XmlComments;
 
internal sealed class GrpcXmlCommentsOperationFilter : IOperationFilter
{
    private readonly XPathNavigator _xmlNavigator;
 
    public GrpcXmlCommentsOperationFilter(XPathDocument xmlDoc)
    {
        _xmlNavigator = xmlDoc.CreateNavigator();
    }
 
    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        var grpcMetadata = context.ApiDescription.ActionDescriptor.EndpointMetadata.OfType<GrpcMethodMetadata>().FirstOrDefault();
        if (grpcMetadata == null)
        {
            return;
        }
 
        var methodInfo = grpcMetadata.ServiceType.GetMethod(grpcMetadata.Method.Name);
        if (methodInfo == null)
        {
            return;
        }
 
        // If method is from a constructed generic type, look for comments from the generic type method
        var targetMethod = methodInfo.DeclaringType!.IsConstructedGenericType
            ? methodInfo.GetUnderlyingGenericTypeMethod()
            : methodInfo;
 
        if (targetMethod == null)
        {
            return;
        }
 
        // Base service never has response tags.
        ApplyServiceTags(operation, targetMethod.DeclaringType!);
 
        if (TryApplyMethodTags(operation, targetMethod))
        {
            return;
        }
 
        if (targetMethod.IsVirtual && targetMethod.GetBaseDefinition() is { } baseMethod)
        {
            if (TryApplyMethodTags(operation, baseMethod))
            {
                return;
            }
        }
    }
 
    private void ApplyServiceTags(OpenApiOperation operation, Type controllerType)
    {
        var typeMemberName = XmlCommentsNodeNameHelper.GetMemberNameForType(controllerType);
        var responseNodes = _xmlNavigator.Select($"/doc/members/member[@name='{typeMemberName}']/response");
        ApplyResponseTags(operation, responseNodes);
    }
 
    private bool TryApplyMethodTags(OpenApiOperation operation, MethodInfo methodInfo)
    {
        var methodMemberName = XmlCommentsNodeNameHelper.GetMemberNameForMethod(methodInfo);
        var methodNode = _xmlNavigator.SelectSingleNode($"/doc/members/member[@name='{methodMemberName}']");
 
        if (methodNode == null)
        {
            return false;
        }
 
        var summaryNode = methodNode.SelectSingleNode("summary");
        if (summaryNode != null)
        {
            operation.Summary = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml);
        }
 
        var remarksNode = methodNode.SelectSingleNode("remarks");
        if (remarksNode != null)
        {
            operation.Description = XmlCommentsTextHelper.Humanize(remarksNode.InnerXml);
        }
 
        var responseNodes = methodNode.Select("response");
        ApplyResponseTags(operation, responseNodes);
 
        return true;
    }
 
    private static void ApplyResponseTags(OpenApiOperation operation, XPathNodeIterator responseNodes)
    {
        while (responseNodes.MoveNext())
        {
            var code = responseNodes.Current!.GetAttribute("code", "");
            if (!operation.Responses.TryGetValue(code, out var response))
            {
                operation.Responses[code] = response = new OpenApiResponse();
            }
 
            response.Description = XmlCommentsTextHelper.Humanize(responseNodes.Current.InnerXml);
        }
    }
}