diff --git a/src/GrpcHttpApi/src/Microsoft.AspNetCore.Grpc.Swagger/Internal/GrpcHttpApiDescriptionProvider.cs b/src/GrpcHttpApi/src/Microsoft.AspNetCore.Grpc.Swagger/Internal/GrpcHttpApiDescriptionProvider.cs index 112236813..cdef8839c 100644 --- a/src/GrpcHttpApi/src/Microsoft.AspNetCore.Grpc.Swagger/Internal/GrpcHttpApiDescriptionProvider.cs +++ b/src/GrpcHttpApi/src/Microsoft.AspNetCore.Grpc.Swagger/Internal/GrpcHttpApiDescriptionProvider.cs @@ -128,6 +128,19 @@ private static ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, ParameterDescriptor = parameterDescriptor! }); } + + foreach (var queryDescription in ServiceDescriptorHelpers.ResolveQueryParameterDescriptors( + routeParameters, methodDescriptor, bodyDescriptor?.Descriptor, bodyDescriptor?.FieldDescriptors)) + { + apiDescription.ParameterDescriptions.Add(new ApiParameterDescription + { + Name = queryDescription.Name, + ModelMetadata = new GrpcModelMetadata( + ModelMetadataIdentity.ForType(MessageDescriptorHelpers.ResolveFieldType(queryDescription.Field))), + Source = BindingSource.Query, + DefaultValue = string.Empty + }); + } return apiDescription; } diff --git a/src/GrpcHttpApi/src/Shared/ServiceDescriptorHelpers.cs b/src/GrpcHttpApi/src/Shared/ServiceDescriptorHelpers.cs index 640603ffe..fee07583f 100644 --- a/src/GrpcHttpApi/src/Shared/ServiceDescriptorHelpers.cs +++ b/src/GrpcHttpApi/src/Shared/ServiceDescriptorHelpers.cs @@ -317,6 +317,65 @@ public static Dictionary> ResolveRouteParameterDes return null; } + + public static IEnumerable<(string Name, FieldDescriptor Field)> ResolveQueryParameterDescriptors( + Dictionary> routeParameters, + MethodDescriptor methodDescriptor, + MessageDescriptor? bodyDescriptor, + List? bodyFieldDescriptors + ) + { + if (methodDescriptor.InputType.Fields.InDeclarationOrder().Count <= routeParameters.Count) + { + yield break; + } + + var allParameters = methodDescriptor.InputType.Fields.InDeclarationOrder().ToList(); + + var allParametersName = methodDescriptor.InputType.Fields + .InDeclarationOrder() + .Select(x => x.Name) + .ToList(); + + foreach (var pathParameter in routeParameters) + { + allParametersName.Remove(pathParameter.Key); + } + + if (bodyDescriptor != null) + { + if (bodyFieldDescriptors != null) + { + // body with field name + foreach (var bodyFieldDescriptor in bodyFieldDescriptors) + { + allParametersName.Remove(bodyFieldDescriptor.Name); + } + } + else + { + // body with wildcard + foreach (var bodyFieldDescriptor in bodyDescriptor.Fields.InDeclarationOrder()) + { + allParametersName.Remove(bodyFieldDescriptor.Name); + } + } + } + + foreach (var parameterName in allParametersName) + { + var field = allParameters + .Where(x => x.Name == parameterName) + .Select(x => x) + .First(); + + allParameters.Remove(field); + + (string Name, FieldDescriptor Field) queryDescription = (Name: parameterName, Field: field); + + yield return queryDescription; + } + } public record BodyDescriptorInfo( MessageDescriptor Descriptor, diff --git a/src/GrpcHttpApi/test/Microsoft.AspNetCore.Grpc.Swagger.Tests/GrpcSwaggerServiceExtensionsTests.cs b/src/GrpcHttpApi/test/Microsoft.AspNetCore.Grpc.Swagger.Tests/GrpcSwaggerServiceExtensionsTests.cs index 865053c30..3d83a0528 100644 --- a/src/GrpcHttpApi/test/Microsoft.AspNetCore.Grpc.Swagger.Tests/GrpcSwaggerServiceExtensionsTests.cs +++ b/src/GrpcHttpApi/test/Microsoft.AspNetCore.Grpc.Swagger.Tests/GrpcSwaggerServiceExtensionsTests.cs @@ -1,9 +1,11 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Linq; using Count; using Greet; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Grpc.Swagger.Tests.Services; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; @@ -86,6 +88,69 @@ public void AddGrpcSwagger_GrpcServiceWithGroupName_FilteredByGroup() Assert.True(swagger.Paths["/v1/greeter/{name}"].Operations.ContainsKey(OperationType.Get)); Assert.True(swagger.Paths["/v1/add/{value1}/{value2}"].Operations.ContainsKey(OperationType.Get)); } + + [Fact] + public void AddGrpcSwagger_GrpcServiceWithQuery_ResolveQueryParameterDescriptorsTest() + { + // Arrange & Act + var services = new ServiceCollection(); + services.AddGrpcSwagger(); + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + services.AddRouting(); + services.AddLogging(); + services.AddSingleton(); + var serviceProvider = services.BuildServiceProvider(); + var app = new ApplicationBuilder(serviceProvider); + + app.UseRouting(); + app.UseEndpoints(c => + { + c.MapGrpcService(); + }); + + var swaggerGenerator = serviceProvider.GetRequiredService(); + var swagger = swaggerGenerator.GetSwagger("v1"); + + // Base Assert + Assert.NotNull(swagger); + + // Assert 1 + var path = swagger.Paths["/v1/parameters1"]; + Assert.True(path.Operations.ContainsKey(OperationType.Get)); + Assert.True(path.Operations.First().Value.Parameters.Count == 2); + Assert.True(path.Operations.First().Value.Parameters.ElementAt(0).In == ParameterLocation.Query); + Assert.True(path.Operations.First().Value.Parameters.ElementAt(1).In == ParameterLocation.Query); + + // Assert 2 + path = swagger.Paths["/v1/parameters2/{parameter_int}"]; + Assert.True(path.Operations.ContainsKey(OperationType.Get)); + Assert.True(path.Operations.First().Value.Parameters.Count == 2); + Assert.True(path.Operations.First().Value.Parameters.ElementAt(0).In == ParameterLocation.Path); + Assert.True(path.Operations.First().Value.Parameters.ElementAt(1).In == ParameterLocation.Query); + + // Assert 3 + path = swagger.Paths["/v1/parameters3/{parameter_one}"]; + Assert.True(path.Operations.ContainsKey(OperationType.Post)); + Assert.True(path.Operations.First().Value.Parameters.Count == 3); + Assert.True(path.Operations.First().Value.Parameters.ElementAt(0).In == ParameterLocation.Path); + Assert.True(path.Operations.First().Value.Parameters.ElementAt(1).In == ParameterLocation.Query); + Assert.True(path.Operations.First().Value.Parameters.ElementAt(2).In == ParameterLocation.Query); + // body with one parameter + Assert.NotNull(path.Operations.First().Value.RequestBody); + Assert.True(swagger.Components.Schemas["RequestBody"].Properties.Count == 1); + + // Assert 4 + path = swagger.Paths["/v1/parameters4/{parameter_two}"]; + Assert.True(path.Operations.ContainsKey(OperationType.Post)); + Assert.True(path.Operations.First().Value.Parameters.Count == 1); + Assert.True(path.Operations.First().Value.Parameters.ElementAt(0).In == ParameterLocation.Path); + // body with four parameters + Assert.NotNull(path.Operations.First().Value.RequestBody); + Assert.True(swagger.Components.Schemas["RequestTwo"].Properties.Count == 4); + } private class TestWebHostEnvironment : IWebHostEnvironment { diff --git a/src/GrpcHttpApi/test/Microsoft.AspNetCore.Grpc.Swagger.Tests/Microsoft.AspNetCore.Grpc.Swagger.Tests.csproj b/src/GrpcHttpApi/test/Microsoft.AspNetCore.Grpc.Swagger.Tests/Microsoft.AspNetCore.Grpc.Swagger.Tests.csproj index de7ef5d5c..8f74834fd 100644 --- a/src/GrpcHttpApi/test/Microsoft.AspNetCore.Grpc.Swagger.Tests/Microsoft.AspNetCore.Grpc.Swagger.Tests.csproj +++ b/src/GrpcHttpApi/test/Microsoft.AspNetCore.Grpc.Swagger.Tests/Microsoft.AspNetCore.Grpc.Swagger.Tests.csproj @@ -16,6 +16,7 @@ + diff --git a/src/GrpcHttpApi/test/Microsoft.AspNetCore.Grpc.Swagger.Tests/Proto/parameters.proto b/src/GrpcHttpApi/test/Microsoft.AspNetCore.Grpc.Swagger.Tests/Proto/parameters.proto new file mode 100644 index 000000000..74a5e0921 --- /dev/null +++ b/src/GrpcHttpApi/test/Microsoft.AspNetCore.Grpc.Swagger.Tests/Proto/parameters.proto @@ -0,0 +1,60 @@ +syntax = "proto3"; + +package params; + +import "google/api/annotations.proto"; + +// HttpRule: https://cloud.google.com/endpoints/docs/grpc-service-config/reference/rpc/google.api#google.api.HttpRule + +service Parameters { + // parameter_int & parameter_string should be query parameters + rpc DemoParametersOne (RequestOne) returns (Response) { + option (google.api.http) = { + get: "/v1/parameters1" + }; + } + + // parameter_string should be query parameters + rpc DemoParametersTwo (RequestOne) returns (Response) { + option (google.api.http) = { + get: "/v1/parameters2/{parameter_int}" + }; + } + + // parameter_two & parameter_three should be query parameters + rpc DemoParametersThree (RequestTwo) returns (Response) { + option (google.api.http) = { + post: "/v1/parameters3/{parameter_one}" + body: "parameter_four" + }; + } + + // no query parameters + rpc DemoParametersFour (RequestTwo) returns (Response) { + option (google.api.http) = { + post: "/v1/parameters4/{parameter_two}" + body: "*" + }; + } +} + +message RequestOne { + int64 parameter_int = 1; + string parameter_string = 2; +} + +message RequestTwo { + + int64 parameter_one = 1; + string parameter_two = 2; + int64 parameter_three = 3; + RequestBody parameter_four = 45; +} + +message RequestBody { + string request_body = 1 ; +} + +message Response { + string message = 1; +} \ No newline at end of file diff --git a/src/GrpcHttpApi/test/Microsoft.AspNetCore.Grpc.Swagger.Tests/Services/ParametersService.cs b/src/GrpcHttpApi/test/Microsoft.AspNetCore.Grpc.Swagger.Tests/Services/ParametersService.cs new file mode 100644 index 000000000..7e6fdd310 --- /dev/null +++ b/src/GrpcHttpApi/test/Microsoft.AspNetCore.Grpc.Swagger.Tests/Services/ParametersService.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using Grpc.Core; +using Params; + +namespace Microsoft.AspNetCore.Grpc.Swagger.Tests.Services; + +public class ParametersService : Parameters.ParametersBase +{ + public override Task DemoParametersOne(RequestOne requestId, ServerCallContext ctx) + { + return Task.FromResult(new Response {Message = "DemoParametersOne Response"}); + } + + public override Task DemoParametersTwo(RequestOne requestId, ServerCallContext ctx) + { + return Task.FromResult(new Response {Message = "DemoParametersTwo Response"}); + } + + public override Task DemoParametersThree(RequestTwo request, ServerCallContext ctx) + { + return Task.FromResult(new Response {Message = "DemoParametersThree Response "}); + } + + public override Task DemoParametersFour(RequestTwo request, ServerCallContext ctx) + { + return Task.FromResult(new Response {Message = "DemoParametersFour Response"}); + } +} \ No newline at end of file