From 3b03ddba6708fdd7359877e8517b46a4563b94c5 Mon Sep 17 00:00:00 2001 From: aka-nse Date: Sun, 15 Sep 2024 20:33:18 +0900 Subject: [PATCH] Adds new API: GrpcEndpointRouteBuilderExtensions.MapGrpcService with ServerServiceDefinition (#1) * support MapGrpcService with ServerServiceDefinition * support MapGrpcService with ServerServiceDefinition * rename sample solution file * create factory method * remove extra using * fix sample project for ServerServiceDefinition mapping * implement Metadata creation for ServerServiceDefinition * implement unit test for MapGrpcService for ServerServiceDefinition --------- Co-authored-by: aka-nse --- .../Client/Client.csproj | 18 +++ .../Client/Program.cs | 30 ++++ .../GreeterByServiceDefinition.sln | 84 +++++++++++ .../Proto/greet.proto | 33 +++++ .../Server/Program.cs | 36 +++++ .../Server/Server.csproj | 16 +++ .../Server/Services/GreeterService.cs | 41 ++++++ .../Server/appsettings.Development.json | 10 ++ .../Server/appsettings.json | 13 ++ examples/README.md | 10 ++ .../GrpcEndpointRouteBuilderExtensions.cs | 38 +++++ .../GrpcServiceExtensions.cs | 2 + .../ClientStreamingServerCallHandler.cs | 52 ++++++- .../DuplexStreamingServerCallHandler.cs | 35 ++++- .../CallHandlers/ServerCallHandlerBase.cs | 135 +++++++++++++++++- .../ServerStreamingServerCallHandler.cs | 33 ++++- .../CallHandlers/UnaryServerCallHandler.cs | 41 +++++- .../Internal/EndpointServiceBinder.cs | 126 ++++++++++++++++ .../Internal/ServerCallHandlerFactory.cs | 130 ++++++++++++++++- .../Model/Internal/ServiceRouteBuilder.cs | 132 +++++++++++++---- src/Grpc.Core.Api/ServerServiceDefinition.cs | 2 +- .../ClientStreamingServerMethodInvoker.cs | 46 ++++++ .../DuplexStreamingServerMethodInvoker.cs | 46 ++++++ src/Shared/Server/ServerMethodInvokerBase.cs | 33 +++++ .../ServerStreamingServerMethodInvoker.cs | 46 ++++++ src/Shared/Server/UnaryServerMethodInvoker.cs | 46 ++++++ ...GrpcEndpointRouteBuilderExtensionsTests.cs | 41 +++++- .../WithAttribute/GreeterWithAttribute.cs | 7 +- 28 files changed, 1241 insertions(+), 41 deletions(-) create mode 100644 examples/GreeterByServiceDefinition/Client/Client.csproj create mode 100644 examples/GreeterByServiceDefinition/Client/Program.cs create mode 100644 examples/GreeterByServiceDefinition/GreeterByServiceDefinition.sln create mode 100644 examples/GreeterByServiceDefinition/Proto/greet.proto create mode 100644 examples/GreeterByServiceDefinition/Server/Program.cs create mode 100644 examples/GreeterByServiceDefinition/Server/Server.csproj create mode 100644 examples/GreeterByServiceDefinition/Server/Services/GreeterService.cs create mode 100644 examples/GreeterByServiceDefinition/Server/appsettings.Development.json create mode 100644 examples/GreeterByServiceDefinition/Server/appsettings.json create mode 100644 src/Grpc.AspNetCore.Server/Internal/EndpointServiceBinder.cs diff --git a/examples/GreeterByServiceDefinition/Client/Client.csproj b/examples/GreeterByServiceDefinition/Client/Client.csproj new file mode 100644 index 000000000..506f33ee0 --- /dev/null +++ b/examples/GreeterByServiceDefinition/Client/Client.csproj @@ -0,0 +1,18 @@ + + + + Exe + net7.0 + + + + + + + + + + + + + diff --git a/examples/GreeterByServiceDefinition/Client/Program.cs b/examples/GreeterByServiceDefinition/Client/Program.cs new file mode 100644 index 000000000..2b5958daf --- /dev/null +++ b/examples/GreeterByServiceDefinition/Client/Program.cs @@ -0,0 +1,30 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using Greet; +using Grpc.Net.Client; + +using var channel = GrpcChannel.ForAddress("https://localhost:5001"); +var client = new Greeter.GreeterClient(channel); + +var reply = await client.SayHelloAsync(new HelloRequest { Name = "GreeterClient" }); +Console.WriteLine("Greeting: " + reply.Message); + +Console.WriteLine("Shutting down"); +Console.WriteLine("Press any key to exit..."); +Console.ReadKey(); diff --git a/examples/GreeterByServiceDefinition/GreeterByServiceDefinition.sln b/examples/GreeterByServiceDefinition/GreeterByServiceDefinition.sln new file mode 100644 index 000000000..da5ff926b --- /dev/null +++ b/examples/GreeterByServiceDefinition/GreeterByServiceDefinition.sln @@ -0,0 +1,84 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33829.357 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Server", "Server\Server.csproj", "{534AC5F8-2DF2-40BD-87A5-B3D8310118C4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Client", "Client\Client.csproj", "{48A1D3BC-A14B-436A-8822-6DE2BEF8B747}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{32B810EF-93B2-46C2-879A-BBA345A10E71}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grpc.Core.Api", "..\..\src\Grpc.Core.Api\Grpc.Core.Api.csproj", "{BF8BD8C9-70D7-486F-BE4D-9ED2C7EA8CB1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grpc.Net.Common", "..\..\src\Grpc.Net.Common\Grpc.Net.Common.csproj", "{912BCAE2-04D8-4FFE-B9A5-C7FAEA7EF808}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grpc.Net.ClientFactory", "..\..\src\Grpc.Net.ClientFactory\Grpc.Net.ClientFactory.csproj", "{3F49C6CE-D3AC-4609-B416-5E180DA59C7F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grpc.AspNetCore.Server.ClientFactory", "..\..\src\Grpc.AspNetCore.Server.ClientFactory\Grpc.AspNetCore.Server.ClientFactory.csproj", "{B38F8199-FD16-4E02-B1E2-CECEBF29A638}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grpc.AspNetCore.Server", "..\..\src\Grpc.AspNetCore.Server\Grpc.AspNetCore.Server.csproj", "{5E857C51-76FF-4263-9BD7-CCB7997795F9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grpc.AspNetCore", "..\..\src\Grpc.AspNetCore\Grpc.AspNetCore.csproj", "{7D83B407-3C89-4671-BF97-A1B196633B0D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grpc.Net.Client", "..\..\src\Grpc.Net.Client\Grpc.Net.Client.csproj", "{CD4371A4-F789-4752-B1C0-DD95B1D6A090}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {534AC5F8-2DF2-40BD-87A5-B3D8310118C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {534AC5F8-2DF2-40BD-87A5-B3D8310118C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {534AC5F8-2DF2-40BD-87A5-B3D8310118C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {534AC5F8-2DF2-40BD-87A5-B3D8310118C4}.Release|Any CPU.Build.0 = Release|Any CPU + {48A1D3BC-A14B-436A-8822-6DE2BEF8B747}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {48A1D3BC-A14B-436A-8822-6DE2BEF8B747}.Debug|Any CPU.Build.0 = Debug|Any CPU + {48A1D3BC-A14B-436A-8822-6DE2BEF8B747}.Release|Any CPU.ActiveCfg = Release|Any CPU + {48A1D3BC-A14B-436A-8822-6DE2BEF8B747}.Release|Any CPU.Build.0 = Release|Any CPU + {BF8BD8C9-70D7-486F-BE4D-9ED2C7EA8CB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BF8BD8C9-70D7-486F-BE4D-9ED2C7EA8CB1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BF8BD8C9-70D7-486F-BE4D-9ED2C7EA8CB1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BF8BD8C9-70D7-486F-BE4D-9ED2C7EA8CB1}.Release|Any CPU.Build.0 = Release|Any CPU + {912BCAE2-04D8-4FFE-B9A5-C7FAEA7EF808}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {912BCAE2-04D8-4FFE-B9A5-C7FAEA7EF808}.Debug|Any CPU.Build.0 = Debug|Any CPU + {912BCAE2-04D8-4FFE-B9A5-C7FAEA7EF808}.Release|Any CPU.ActiveCfg = Release|Any CPU + {912BCAE2-04D8-4FFE-B9A5-C7FAEA7EF808}.Release|Any CPU.Build.0 = Release|Any CPU + {3F49C6CE-D3AC-4609-B416-5E180DA59C7F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3F49C6CE-D3AC-4609-B416-5E180DA59C7F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3F49C6CE-D3AC-4609-B416-5E180DA59C7F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3F49C6CE-D3AC-4609-B416-5E180DA59C7F}.Release|Any CPU.Build.0 = Release|Any CPU + {B38F8199-FD16-4E02-B1E2-CECEBF29A638}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B38F8199-FD16-4E02-B1E2-CECEBF29A638}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B38F8199-FD16-4E02-B1E2-CECEBF29A638}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B38F8199-FD16-4E02-B1E2-CECEBF29A638}.Release|Any CPU.Build.0 = Release|Any CPU + {5E857C51-76FF-4263-9BD7-CCB7997795F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5E857C51-76FF-4263-9BD7-CCB7997795F9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5E857C51-76FF-4263-9BD7-CCB7997795F9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5E857C51-76FF-4263-9BD7-CCB7997795F9}.Release|Any CPU.Build.0 = Release|Any CPU + {7D83B407-3C89-4671-BF97-A1B196633B0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7D83B407-3C89-4671-BF97-A1B196633B0D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7D83B407-3C89-4671-BF97-A1B196633B0D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7D83B407-3C89-4671-BF97-A1B196633B0D}.Release|Any CPU.Build.0 = Release|Any CPU + {CD4371A4-F789-4752-B1C0-DD95B1D6A090}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CD4371A4-F789-4752-B1C0-DD95B1D6A090}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD4371A4-F789-4752-B1C0-DD95B1D6A090}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CD4371A4-F789-4752-B1C0-DD95B1D6A090}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {BF8BD8C9-70D7-486F-BE4D-9ED2C7EA8CB1} = {32B810EF-93B2-46C2-879A-BBA345A10E71} + {912BCAE2-04D8-4FFE-B9A5-C7FAEA7EF808} = {32B810EF-93B2-46C2-879A-BBA345A10E71} + {3F49C6CE-D3AC-4609-B416-5E180DA59C7F} = {32B810EF-93B2-46C2-879A-BBA345A10E71} + {B38F8199-FD16-4E02-B1E2-CECEBF29A638} = {32B810EF-93B2-46C2-879A-BBA345A10E71} + {5E857C51-76FF-4263-9BD7-CCB7997795F9} = {32B810EF-93B2-46C2-879A-BBA345A10E71} + {7D83B407-3C89-4671-BF97-A1B196633B0D} = {32B810EF-93B2-46C2-879A-BBA345A10E71} + {CD4371A4-F789-4752-B1C0-DD95B1D6A090} = {32B810EF-93B2-46C2-879A-BBA345A10E71} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D22B3129-3BFB-41FA-9FCE-E45EBEF8C2DD} + EndGlobalSection +EndGlobal diff --git a/examples/GreeterByServiceDefinition/Proto/greet.proto b/examples/GreeterByServiceDefinition/Proto/greet.proto new file mode 100644 index 000000000..26d0c794d --- /dev/null +++ b/examples/GreeterByServiceDefinition/Proto/greet.proto @@ -0,0 +1,33 @@ +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package greet; + +// The greeting service definition. +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply); +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; +} diff --git a/examples/GreeterByServiceDefinition/Server/Program.cs b/examples/GreeterByServiceDefinition/Server/Program.cs new file mode 100644 index 000000000..69de37f8c --- /dev/null +++ b/examples/GreeterByServiceDefinition/Server/Program.cs @@ -0,0 +1,36 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using Grpc.Core; +using Server; +using Microsoft.AspNetCore.Builder; + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddGrpc(); + +var app = builder.Build(); +app.MapGrpcService(getGreeterService); + +app.Run(); + +static ServerServiceDefinition getGreeterService(IServiceProvider serviceProvider) +{ + var loggerFactory = serviceProvider.GetRequiredService(); + var service = new GreeterService(loggerFactory); + return Greet.Greeter.BindService(service); +} diff --git a/examples/GreeterByServiceDefinition/Server/Server.csproj b/examples/GreeterByServiceDefinition/Server/Server.csproj new file mode 100644 index 000000000..099b6adbd --- /dev/null +++ b/examples/GreeterByServiceDefinition/Server/Server.csproj @@ -0,0 +1,16 @@ + + + + net9.0 + + + + + + + + + + + + diff --git a/examples/GreeterByServiceDefinition/Server/Services/GreeterService.cs b/examples/GreeterByServiceDefinition/Server/Services/GreeterService.cs new file mode 100644 index 000000000..1ca09856d --- /dev/null +++ b/examples/GreeterByServiceDefinition/Server/Services/GreeterService.cs @@ -0,0 +1,41 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System.Threading.Tasks; +using Greet; +using Grpc.Core; +using Microsoft.Extensions.Logging; + +namespace Server +{ + public class GreeterService : Greeter.GreeterBase + { + private readonly ILogger _logger; + + public GreeterService(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + public override Task SayHello(HelloRequest request, ServerCallContext context) + { + _logger.LogInformation($"Sending hello to {request.Name}"); + return Task.FromResult(new HelloReply { Message = "Hello " + request.Name }); + } + } +} diff --git a/examples/GreeterByServiceDefinition/Server/appsettings.Development.json b/examples/GreeterByServiceDefinition/Server/appsettings.Development.json new file mode 100644 index 000000000..fe20c40cc --- /dev/null +++ b/examples/GreeterByServiceDefinition/Server/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Grpc": "Information", + "Microsoft": "Information" + } + } +} diff --git a/examples/GreeterByServiceDefinition/Server/appsettings.json b/examples/GreeterByServiceDefinition/Server/appsettings.json new file mode 100644 index 000000000..f5f63744b --- /dev/null +++ b/examples/GreeterByServiceDefinition/Server/appsettings.json @@ -0,0 +1,13 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information" + } + }, + "AllowedHosts": "*", + "Kestrel": { + "EndpointDefaults": { + "Protocols": "Http2" + } + } +} diff --git a/examples/README.md b/examples/README.md index 14a93010b..c018c0a0f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -328,3 +328,13 @@ The error example shows how to use a richer error model with `Grpc.StatusProto`. * Error handling * Validation * [`google.rpc.Status`](https://cloud.google.com/apis/design/errors#error_model) + + +## [GreeterByServiceDefinition](./GreeterByServiceDefinition) + +This sample is similar with [Greeter](#greeter), but its service instance for server is mapped by using `ServerServiceDefinition`. + +##### Scenarios: + +* Mapping server service by using `ServerServiceDefinition` +* Unary call diff --git a/src/Grpc.AspNetCore.Server/GrpcEndpointRouteBuilderExtensions.cs b/src/Grpc.AspNetCore.Server/GrpcEndpointRouteBuilderExtensions.cs index f583a2c4c..eb52afe4e 100644 --- a/src/Grpc.AspNetCore.Server/GrpcEndpointRouteBuilderExtensions.cs +++ b/src/Grpc.AspNetCore.Server/GrpcEndpointRouteBuilderExtensions.cs @@ -19,6 +19,7 @@ using System.Diagnostics.CodeAnalysis; using Grpc.AspNetCore.Server.Internal; using Grpc.AspNetCore.Server.Model.Internal; +using Grpc.Core; using Grpc.Shared; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; @@ -45,6 +46,43 @@ public static class GrpcEndpointRouteBuilderExtensions var serviceRouteBuilder = builder.ServiceProvider.GetRequiredService>(); var endpointConventionBuilders = serviceRouteBuilder.Build(builder); + return new GrpcServiceEndpointConventionBuilder(endpointConventionBuilders); + } + + /// + /// Maps incoming requests to the specified instance. + /// + /// The to add the route to. + /// The instance of . + /// A for endpoints associated with the service. + [RequiresUnreferencedCode("Due to type erasure in ServerServiceDefinition, MapGrpcService is incompatible with trimming.")] + public static GrpcServiceEndpointConventionBuilder MapGrpcService(this IEndpointRouteBuilder builder, ServerServiceDefinition serviceDefinition) + { + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + ArgumentNullException.ThrowIfNull(serviceDefinition, nameof(serviceDefinition)); + + var serviceRouteBuilder = builder.ServiceProvider.GetRequiredService(); + var endpointConventionBuilders = serviceRouteBuilder.Build(builder, serviceDefinition); + + return new GrpcServiceEndpointConventionBuilder(endpointConventionBuilders); + } + + /// + /// Maps incoming requests to the instance from the specified factory. + /// + /// The to add the route to. + /// The factory for instance. + /// A for endpoints associated with the service. + [RequiresUnreferencedCode("Due to type erasure in ServerServiceDefinition, MapGrpcService is incompatible with trimming.")] + public static GrpcServiceEndpointConventionBuilder MapGrpcService(this IEndpointRouteBuilder builder, Func getServiceDefinition) + { + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + ArgumentNullException.ThrowIfNull(getServiceDefinition, nameof(getServiceDefinition)); + + var serviceDefinition = getServiceDefinition(builder.ServiceProvider); + var serviceRouteBuilder = builder.ServiceProvider.GetRequiredService(); + var endpointConventionBuilders = serviceRouteBuilder.Build(builder, serviceDefinition); + return new GrpcServiceEndpointConventionBuilder(endpointConventionBuilders); } diff --git a/src/Grpc.AspNetCore.Server/GrpcServiceExtensions.cs b/src/Grpc.AspNetCore.Server/GrpcServiceExtensions.cs index 0f9b8852d..b45dbf396 100644 --- a/src/Grpc.AspNetCore.Server/GrpcServiceExtensions.cs +++ b/src/Grpc.AspNetCore.Server/GrpcServiceExtensions.cs @@ -68,6 +68,7 @@ public static IGrpcServerBuilder AddGrpc(this IServiceCollection services) #endif services.AddOptions(); services.TryAddSingleton(); + services.TryAddSingleton(typeof(ServerCallHandlerFactory)); services.TryAddSingleton(typeof(ServerCallHandlerFactory<>)); services.TryAddSingleton(typeof(IGrpcServiceActivator<>), typeof(DefaultGrpcServiceActivator<>)); services.TryAddSingleton(typeof(IGrpcInterceptorActivator<>), typeof(DefaultGrpcInterceptorActivator<>)); @@ -75,6 +76,7 @@ public static IGrpcServerBuilder AddGrpc(this IServiceCollection services) // Model services.TryAddSingleton(); + services.TryAddSingleton(typeof(ServiceRouteBuilder)); services.TryAddSingleton(typeof(ServiceRouteBuilder<>)); services.TryAddEnumerable(ServiceDescriptor.Singleton(typeof(IServiceMethodProvider<>), typeof(BinderServiceMethodProvider<>))); diff --git a/src/Grpc.AspNetCore.Server/Internal/CallHandlers/ClientStreamingServerCallHandler.cs b/src/Grpc.AspNetCore.Server/Internal/CallHandlers/ClientStreamingServerCallHandler.cs index f6d9bb155..faef4ed05 100644 --- a/src/Grpc.AspNetCore.Server/Internal/CallHandlers/ClientStreamingServerCallHandler.cs +++ b/src/Grpc.AspNetCore.Server/Internal/CallHandlers/ClientStreamingServerCallHandler.cs @@ -24,7 +24,57 @@ namespace Grpc.AspNetCore.Server.Internal.CallHandlers; -internal class ClientStreamingServerCallHandler<[DynamicallyAccessedMembers(GrpcProtocolConstants.ServiceAccessibility)] TService, TRequest, TResponse> : ServerCallHandlerBase +internal class ClientStreamingServerCallHandler : ServerCallHandlerBase + where TRequest : class + where TResponse : class +{ + private readonly ClientStreamingServerMethodInvoker _invoker; + + public ClientStreamingServerCallHandler( + ClientStreamingServerMethodInvoker invoker, + ILoggerFactory loggerFactory) + : base(invoker, loggerFactory) + { + _invoker = invoker; + } + + protected override async Task HandleCallAsyncCore(HttpContext httpContext, HttpContextServerCallContext serverCallContext) + { + // Disable request body data rate for client streaming + DisableMinRequestBodyDataRateAndMaxRequestBodySize(httpContext); + + TResponse? response; + + var streamReader = new HttpContextStreamReader(serverCallContext, MethodInvoker.Method.RequestMarshaller.ContextualDeserializer); + try + { + response = await _invoker.Invoke(httpContext, serverCallContext, streamReader); + } + finally + { + streamReader.Complete(); + } + + if (response == null) + { + // This is consistent with Grpc.Core when a null value is returned + throw new RpcException(new Status(StatusCode.Cancelled, "No message returned from method.")); + } + + // Check if deadline exceeded while method was invoked. If it has then skip trying to write + // the response message because it will always fail. + // Note that the call is still going so the deadline could still be exceeded after this point. + if (serverCallContext.DeadlineManager?.IsDeadlineExceededStarted ?? false) + { + return; + } + + var responseBodyWriter = httpContext.Response.BodyWriter; + await responseBodyWriter.WriteSingleMessageAsync(response, serverCallContext, MethodInvoker.Method.ResponseMarshaller.ContextualSerializer); + } +} + +internal class ClientStreamingServerCallHandler<[DynamicallyAccessedMembers(GrpcProtocolConstants.ServiceAccessibility)]TService, TRequest, TResponse> : ServerCallHandlerBase where TRequest : class where TResponse : class where TService : class diff --git a/src/Grpc.AspNetCore.Server/Internal/CallHandlers/DuplexStreamingServerCallHandler.cs b/src/Grpc.AspNetCore.Server/Internal/CallHandlers/DuplexStreamingServerCallHandler.cs index cc5318511..2cffab5f7 100644 --- a/src/Grpc.AspNetCore.Server/Internal/CallHandlers/DuplexStreamingServerCallHandler.cs +++ b/src/Grpc.AspNetCore.Server/Internal/CallHandlers/DuplexStreamingServerCallHandler.cs @@ -23,7 +23,40 @@ namespace Grpc.AspNetCore.Server.Internal.CallHandlers; -internal class DuplexStreamingServerCallHandler<[DynamicallyAccessedMembers(GrpcProtocolConstants.ServiceAccessibility)] TService, TRequest, TResponse> : ServerCallHandlerBase +internal class DuplexStreamingServerCallHandler : ServerCallHandlerBase + where TRequest : class + where TResponse : class +{ + private readonly DuplexStreamingServerMethodInvoker _invoker; + + public DuplexStreamingServerCallHandler( + DuplexStreamingServerMethodInvoker invoker, + ILoggerFactory loggerFactory) + : base(invoker, loggerFactory) + { + _invoker = invoker; + } + + protected override async Task HandleCallAsyncCore(HttpContext httpContext, HttpContextServerCallContext serverCallContext) + { + // Disable request body data rate for client streaming + DisableMinRequestBodyDataRateAndMaxRequestBodySize(httpContext); + + var streamReader = new HttpContextStreamReader(serverCallContext, MethodInvoker.Method.RequestMarshaller.ContextualDeserializer); + var streamWriter = new HttpContextStreamWriter(serverCallContext, MethodInvoker.Method.ResponseMarshaller.ContextualSerializer); + try + { + await _invoker.Invoke(httpContext, serverCallContext, streamReader, streamWriter); + } + finally + { + streamReader.Complete(); + streamWriter.Complete(); + } + } +} + +internal class DuplexStreamingServerCallHandler<[DynamicallyAccessedMembers(GrpcProtocolConstants.ServiceAccessibility)]TService, TRequest, TResponse> : ServerCallHandlerBase where TRequest : class where TResponse : class where TService : class diff --git a/src/Grpc.AspNetCore.Server/Internal/CallHandlers/ServerCallHandlerBase.cs b/src/Grpc.AspNetCore.Server/Internal/CallHandlers/ServerCallHandlerBase.cs index f4db8601f..2b447a910 100644 --- a/src/Grpc.AspNetCore.Server/Internal/CallHandlers/ServerCallHandlerBase.cs +++ b/src/Grpc.AspNetCore.Server/Internal/CallHandlers/ServerCallHandlerBase.cs @@ -30,7 +30,140 @@ namespace Grpc.AspNetCore.Server.Internal.CallHandlers; -internal abstract class ServerCallHandlerBase<[DynamicallyAccessedMembers(GrpcProtocolConstants.ServiceAccessibility)] TService, TRequest, TResponse> +internal abstract class ServerCallHandlerBase + where TRequest : class + where TResponse : class +{ + private const string LoggerName = "Grpc.AspNetCore.Server.ServerCallHandler"; + + protected ServerMethodInvokerBase MethodInvoker { get; } + protected ILogger Logger { get; } + + protected ServerCallHandlerBase( + ServerMethodInvokerBase methodInvoker, + ILoggerFactory loggerFactory) + { + MethodInvoker = methodInvoker; + Logger = loggerFactory.CreateLogger(LoggerName); + } + + public Task HandleCallAsync(HttpContext httpContext) + { + if (GrpcProtocolHelpers.IsInvalidContentType(httpContext, out var error)) + { + return ProcessInvalidContentTypeRequest(httpContext, error); + } + + if (!GrpcProtocolConstants.IsHttp2(httpContext.Request.Protocol) +#if NET6_0_OR_GREATER + && !GrpcProtocolConstants.IsHttp3(httpContext.Request.Protocol) +#endif + ) + { + return ProcessNonHttp2Request(httpContext); + } + + var serverCallContext = new HttpContextServerCallContext(httpContext, MethodInvoker.Options, typeof(TRequest), typeof(TResponse), Logger); + httpContext.Features.Set(serverCallContext); + + GrpcProtocolHelpers.AddProtocolHeaders(httpContext.Response); + + try + { + serverCallContext.Initialize(); + + var handleCallTask = HandleCallAsyncCore(httpContext, serverCallContext); + + if (handleCallTask.IsCompletedSuccessfully) + { + return serverCallContext.EndCallAsync(); + } + else + { + return AwaitHandleCall(serverCallContext, MethodInvoker.Method, handleCallTask); + } + } + catch (Exception ex) + { + return serverCallContext.ProcessHandlerErrorAsync(ex, MethodInvoker.Method.Name); + } + + static async Task AwaitHandleCall(HttpContextServerCallContext serverCallContext, Method method, Task handleCall) + { + try + { + await handleCall; + await serverCallContext.EndCallAsync(); + } + catch (Exception ex) + { + await serverCallContext.ProcessHandlerErrorAsync(ex, method.Name); + } + } + } + + protected abstract Task HandleCallAsyncCore(HttpContext httpContext, HttpContextServerCallContext serverCallContext); + + /// + /// This should only be called from client streaming calls + /// + /// + protected void DisableMinRequestBodyDataRateAndMaxRequestBodySize(HttpContext httpContext) + { + var minRequestBodyDataRateFeature = httpContext.Features.Get(); + if (minRequestBodyDataRateFeature != null) + { + minRequestBodyDataRateFeature.MinDataRate = null; + } + + var maxRequestBodySizeFeature = httpContext.Features.Get(); + if (maxRequestBodySizeFeature != null) + { + if (!maxRequestBodySizeFeature.IsReadOnly) + { + maxRequestBodySizeFeature.MaxRequestBodySize = null; + } + else + { + // IsReadOnly could be true if middleware has already started reading the request body + // In that case we can't disable the max request body size for the request stream + GrpcServerLog.UnableToDisableMaxRequestBodySize(Logger); + } + } + } + + private Task ProcessNonHttp2Request(HttpContext httpContext) + { + GrpcServerLog.UnsupportedRequestProtocol(Logger, httpContext.Request.Protocol); + + var protocolError = $"Request protocol '{httpContext.Request.Protocol}' is not supported."; + GrpcProtocolHelpers.BuildHttpErrorResponse(httpContext.Response, StatusCodes.Status426UpgradeRequired, StatusCode.Internal, protocolError); + httpContext.Response.Headers[HeaderNames.Upgrade] = GrpcProtocolConstants.Http2Protocol; + return Task.CompletedTask; + } + + private Task ProcessInvalidContentTypeRequest(HttpContext httpContext, string error) + { + // This might be a CORS preflight request and CORS middleware hasn't been configured + if (GrpcProtocolHelpers.IsCorsPreflightRequest(httpContext)) + { + GrpcServerLog.UnhandledCorsPreflightRequest(Logger); + + GrpcProtocolHelpers.BuildHttpErrorResponse(httpContext.Response, StatusCodes.Status405MethodNotAllowed, StatusCode.Internal, "Unhandled CORS preflight request received. CORS may not be configured correctly in the application."); + httpContext.Response.Headers[HeaderNames.Allow] = HttpMethods.Post; + return Task.CompletedTask; + } + else + { + GrpcServerLog.UnsupportedRequestContentType(Logger, httpContext.Request.ContentType); + + GrpcProtocolHelpers.BuildHttpErrorResponse(httpContext.Response, StatusCodes.Status415UnsupportedMediaType, StatusCode.Internal, error); + return Task.CompletedTask; + } + } +} + +internal abstract class ServerCallHandlerBase<[DynamicallyAccessedMembers(GrpcProtocolConstants.ServiceAccessibility)]TService, TRequest, TResponse> where TService : class where TRequest : class where TResponse : class diff --git a/src/Grpc.AspNetCore.Server/Internal/CallHandlers/ServerStreamingServerCallHandler.cs b/src/Grpc.AspNetCore.Server/Internal/CallHandlers/ServerStreamingServerCallHandler.cs index 404822f98..bfcdae173 100644 --- a/src/Grpc.AspNetCore.Server/Internal/CallHandlers/ServerStreamingServerCallHandler.cs +++ b/src/Grpc.AspNetCore.Server/Internal/CallHandlers/ServerStreamingServerCallHandler.cs @@ -23,7 +23,38 @@ namespace Grpc.AspNetCore.Server.Internal.CallHandlers; -internal class ServerStreamingServerCallHandler<[DynamicallyAccessedMembers(GrpcProtocolConstants.ServiceAccessibility)] TService, TRequest, TResponse> : ServerCallHandlerBase +internal class ServerStreamingServerCallHandler : ServerCallHandlerBase + where TRequest : class + where TResponse : class +{ + private readonly ServerStreamingServerMethodInvoker _invoker; + + public ServerStreamingServerCallHandler( + ServerStreamingServerMethodInvoker invoker, + ILoggerFactory loggerFactory) + : base(invoker, loggerFactory) + { + _invoker = invoker; + } + + protected override async Task HandleCallAsyncCore(HttpContext httpContext, HttpContextServerCallContext serverCallContext) + { + // Decode request + var request = await httpContext.Request.BodyReader.ReadSingleMessageAsync(serverCallContext, MethodInvoker.Method.RequestMarshaller.ContextualDeserializer); + + var streamWriter = new HttpContextStreamWriter(serverCallContext, MethodInvoker.Method.ResponseMarshaller.ContextualSerializer); + try + { + await _invoker.Invoke(httpContext, serverCallContext, request, streamWriter); + } + finally + { + streamWriter.Complete(); + } + } +} + +internal class ServerStreamingServerCallHandler<[DynamicallyAccessedMembers(GrpcProtocolConstants.ServiceAccessibility)]TService, TRequest, TResponse> : ServerCallHandlerBase where TRequest : class where TResponse : class where TService : class diff --git a/src/Grpc.AspNetCore.Server/Internal/CallHandlers/UnaryServerCallHandler.cs b/src/Grpc.AspNetCore.Server/Internal/CallHandlers/UnaryServerCallHandler.cs index 499dc6491..4789defb1 100644 --- a/src/Grpc.AspNetCore.Server/Internal/CallHandlers/UnaryServerCallHandler.cs +++ b/src/Grpc.AspNetCore.Server/Internal/CallHandlers/UnaryServerCallHandler.cs @@ -24,7 +24,46 @@ namespace Grpc.AspNetCore.Server.Internal.CallHandlers; -internal class UnaryServerCallHandler<[DynamicallyAccessedMembers(GrpcProtocolConstants.ServiceAccessibility)] TService, TRequest, TResponse> : ServerCallHandlerBase +internal class UnaryServerCallHandler : ServerCallHandlerBase + where TRequest : class + where TResponse : class +{ + private readonly UnaryServerMethodInvoker _invoker; + + public UnaryServerCallHandler( + UnaryServerMethodInvoker invoker, + ILoggerFactory loggerFactory) + : base(invoker, loggerFactory) + { + _invoker = invoker; + } + + protected override async Task HandleCallAsyncCore(HttpContext httpContext, HttpContextServerCallContext serverCallContext) + { + var request = await httpContext.Request.BodyReader.ReadSingleMessageAsync(serverCallContext, MethodInvoker.Method.RequestMarshaller.ContextualDeserializer); + + var response = await _invoker.Invoke(httpContext, serverCallContext, request); + + if (response == null) + { + // This is consistent with Grpc.Core when a null value is returned + throw new RpcException(new Status(StatusCode.Cancelled, "No message returned from method.")); + } + + // Check if deadline exceeded while method was invoked. If it has then skip trying to write + // the response message because it will always fail. + // Note that the call is still going so the deadline could still be exceeded after this point. + if (serverCallContext.DeadlineManager?.IsDeadlineExceededStarted ?? false) + { + return; + } + + var responseBodyWriter = httpContext.Response.BodyWriter; + await responseBodyWriter.WriteSingleMessageAsync(response, serverCallContext, MethodInvoker.Method.ResponseMarshaller.ContextualSerializer); + } +} + +internal class UnaryServerCallHandler<[DynamicallyAccessedMembers(GrpcProtocolConstants.ServiceAccessibility)]TService, TRequest, TResponse> : ServerCallHandlerBase where TRequest : class where TResponse : class where TService : class diff --git a/src/Grpc.AspNetCore.Server/Internal/EndpointServiceBinder.cs b/src/Grpc.AspNetCore.Server/Internal/EndpointServiceBinder.cs new file mode 100644 index 000000000..552a578a9 --- /dev/null +++ b/src/Grpc.AspNetCore.Server/Internal/EndpointServiceBinder.cs @@ -0,0 +1,126 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Logging; +using Grpc.Core; +using Microsoft.AspNetCore.Routing.Patterns; +using Grpc.AspNetCore.Server.Model.Internal; + +namespace Grpc.AspNetCore.Server.Internal; + +/// +/// The service binder to bind into ASP.Net core web application server. +/// +internal class EndpointServiceBinder : ServiceBinderBase +{ + private readonly ServerCallHandlerFactory _serverCallHandlerFactory; + private readonly IEndpointRouteBuilder _routeBuilder; + private readonly ILogger _logger; + public List EndpointConventionBuilders { get; } + public List MethodModels { get; } + + public EndpointServiceBinder( + ServerCallHandlerFactory serverCallHandlerFactory, + IEndpointRouteBuilder routeBuilder, + ILoggerFactory loggerFactory) + { + _serverCallHandlerFactory = serverCallHandlerFactory; + _routeBuilder = routeBuilder; + _logger = loggerFactory.CreateLogger(); + EndpointConventionBuilders = new List(); + MethodModels = new List(); + } + + public override void AddMethod(Method method, UnaryServerMethod? handler) + { + if(handler?.Method.DeclaringType == null) + { + throw new InvalidOperationException($"Instance methods are only allowed as server implementation for Grpc.Core.ServerServiceDefinition."); + } + var serviceType = handler.Method.DeclaringType; + var metadata = CreateMetadata(serviceType, handler); + var callHandler = _serverCallHandlerFactory.CreateUnary(method, handler); + var pattern = RoutePatternFactory.Parse(method.FullName); + AddMethod(new MethodModel(method, pattern, metadata, callHandler.HandleCallAsync)); + } + + public override void AddMethod(Method method, ClientStreamingServerMethod? handler) + { + if (handler?.Method.DeclaringType == null) + { + throw new InvalidOperationException($"Instance methods are only allowed as server implementation for Grpc.Core.ServerServiceDefinition."); + } + var serviceType = handler.Method.DeclaringType; + var metadata = CreateMetadata(serviceType, handler); + var callHandler = _serverCallHandlerFactory.CreateClientStreaming(method, handler); + var pattern = RoutePatternFactory.Parse(method.FullName); + AddMethod(new MethodModel(method, pattern, metadata, callHandler.HandleCallAsync)); + } + + public override void AddMethod(Method method, ServerStreamingServerMethod? handler) + { + if (handler?.Method.DeclaringType == null) + { + throw new InvalidOperationException($"Instance methods are only allowed as server implementation for Grpc.Core.ServerServiceDefinition."); + } + var serviceType = handler.Method.DeclaringType; + var metadata = CreateMetadata(serviceType, handler); + var callHandler = _serverCallHandlerFactory.CreateServerStreaming(method, handler); + var pattern = RoutePatternFactory.Parse(method.FullName); + AddMethod(new MethodModel(method, pattern, metadata, callHandler.HandleCallAsync)); + } + + public override void AddMethod(Method method, DuplexStreamingServerMethod? handler) + { + if (handler?.Method.DeclaringType == null) + { + throw new InvalidOperationException($"Instance methods are only allowed as server implementation for Grpc.Core.ServerServiceDefinition."); + } + var serviceType = handler.Method.DeclaringType; + var metadata = CreateMetadata(serviceType, handler); + var callHandler = _serverCallHandlerFactory.CreateDuplexStreaming(method, handler); + var pattern = RoutePatternFactory.Parse(method.FullName); + AddMethod(new MethodModel(method, pattern, metadata, callHandler.HandleCallAsync)); + } + + private IList CreateMetadata(Type serviceType, Delegate handler) + { + var metadata = new List(); + // Add type metadata first so it has a lower priority + metadata.AddRange(serviceType.GetCustomAttributes(inherit: true)); + // Add method metadata last so it has a higher priority + metadata.AddRange(handler.Method.GetCustomAttributes(inherit: true)); + + // Accepting CORS preflight means gRPC will allow requests with OPTIONS + preflight headers. + // If CORS middleware hasn't been configured then the request will reach gRPC handler. + // gRPC will return 405 response and log that CORS has not been configured. + metadata.Add(new HttpMethodMetadata(new[] { "POST" }, acceptCorsPreflight: true)); + + return metadata; + } + + private void AddMethod(MethodModel method) + { + var endpointBuilder = _routeBuilder.Map(method.Pattern, method.RequestDelegate); + endpointBuilder.Add(ep => + { + ep.DisplayName = $"gRPC - {method.Pattern.RawText}"; + + foreach (var item in method.Metadata) + { + ep.Metadata.Add(item); + } + }); + EndpointConventionBuilders.Add(endpointBuilder); + MethodModels.Add(method); + + var httpMethod = method.Metadata.OfType().LastOrDefault(); + + ServiceRouteBuilderLog.LogAddedServiceMethod( + _logger, + method.Method.Name, + method.Method.ServiceName, + method.Method.Type, + httpMethod?.HttpMethods ?? Array.Empty(), + method.Pattern.RawText ?? string.Empty); + } +} diff --git a/src/Grpc.AspNetCore.Server/Internal/ServerCallHandlerFactory.cs b/src/Grpc.AspNetCore.Server/Internal/ServerCallHandlerFactory.cs index 95d545942..c93354ca0 100644 --- a/src/Grpc.AspNetCore.Server/Internal/ServerCallHandlerFactory.cs +++ b/src/Grpc.AspNetCore.Server/Internal/ServerCallHandlerFactory.cs @@ -28,10 +28,138 @@ namespace Grpc.AspNetCore.Server.Internal; +internal interface IServerCallHandlerFactory +{ + bool IgnoreUnknownServices { get; } + bool IgnoreUnknownMethods { get; } + RequestDelegate CreateUnimplementedMethod(); + RequestDelegate CreateUnimplementedService(); +} + +/// +/// Creates server call handlers for . Provides a place to get services that call handlers will use. +/// +internal partial class ServerCallHandlerFactory : IServerCallHandlerFactory +{ + private readonly ILoggerFactory _loggerFactory; + private readonly GrpcServiceOptions _globalOptions; + + public ServerCallHandlerFactory( + ILoggerFactory loggerFactory, + IOptions globalOptions) + { + _loggerFactory = loggerFactory; + _globalOptions = globalOptions.Value; + } + + // Internal for testing + internal MethodOptions CreateMethodOptions() + { + return MethodOptions.Create(new[] { _globalOptions }); + } + + public UnaryServerCallHandler CreateUnary(Method method, UnaryServerMethod invoker) + where TRequest : class + where TResponse : class + { + var options = CreateMethodOptions(); + var methodInvoker = new UnaryServerMethodInvoker(invoker, method, options); + + return new UnaryServerCallHandler(methodInvoker, _loggerFactory); + } + + public ClientStreamingServerCallHandler CreateClientStreaming(Method method, ClientStreamingServerMethod invoker) + where TRequest : class + where TResponse : class + { + var options = CreateMethodOptions(); + var methodInvoker = new ClientStreamingServerMethodInvoker(invoker, method, options); + + return new ClientStreamingServerCallHandler(methodInvoker, _loggerFactory); + } + + public DuplexStreamingServerCallHandler CreateDuplexStreaming(Method method, DuplexStreamingServerMethod invoker) + where TRequest : class + where TResponse : class + { + var options = CreateMethodOptions(); + var methodInvoker = new DuplexStreamingServerMethodInvoker(invoker, method, options); + + return new DuplexStreamingServerCallHandler(methodInvoker, _loggerFactory); + } + + public ServerStreamingServerCallHandler CreateServerStreaming(Method method, ServerStreamingServerMethod invoker) + where TRequest : class + where TResponse : class + { + var options = CreateMethodOptions(); + var methodInvoker = new ServerStreamingServerMethodInvoker(invoker, method, options); + + return new ServerStreamingServerCallHandler(methodInvoker, _loggerFactory); + } + + public RequestDelegate CreateUnimplementedMethod() + { + var logger = _loggerFactory.CreateLogger(); + + return httpContext => + { + // CORS preflight request should be handled by CORS middleware. + // If it isn't then return 404 from endpoint request delegate. + if (GrpcProtocolHelpers.IsCorsPreflightRequest(httpContext)) + { + httpContext.Response.StatusCode = StatusCodes.Status404NotFound; + return Task.CompletedTask; + } + + GrpcProtocolHelpers.AddProtocolHeaders(httpContext.Response); + + var unimplementedMethod = httpContext.Request.RouteValues["unimplementedMethod"]?.ToString() ?? ""; + Log.MethodUnimplemented(logger, unimplementedMethod); + if (GrpcEventSource.Log.IsEnabled()) + { + GrpcEventSource.Log.CallUnimplemented(httpContext.Request.Path.Value!); + } + GrpcProtocolHelpers.SetStatus(GrpcProtocolHelpers.GetTrailersDestination(httpContext.Response), new Status(StatusCode.Unimplemented, "Method is unimplemented.")); + return Task.CompletedTask; + }; + } + + public bool IgnoreUnknownServices => _globalOptions.IgnoreUnknownServices ?? false; + public bool IgnoreUnknownMethods => false; + + public RequestDelegate CreateUnimplementedService() + { + var logger = _loggerFactory.CreateLogger(); + + return httpContext => + { + // CORS preflight request should be handled by CORS middleware. + // If it isn't then return 404 from endpoint request delegate. + if (GrpcProtocolHelpers.IsCorsPreflightRequest(httpContext)) + { + httpContext.Response.StatusCode = StatusCodes.Status404NotFound; + return Task.CompletedTask; + } + + GrpcProtocolHelpers.AddProtocolHeaders(httpContext.Response); + + var unimplementedService = httpContext.Request.RouteValues["unimplementedService"]?.ToString() ?? ""; + Log.ServiceUnimplemented(logger, unimplementedService); + if (GrpcEventSource.Log.IsEnabled()) + { + GrpcEventSource.Log.CallUnimplemented(httpContext.Request.Path.Value!); + } + GrpcProtocolHelpers.SetStatus(GrpcProtocolHelpers.GetTrailersDestination(httpContext.Response), new Status(StatusCode.Unimplemented, "Service is unimplemented.")); + return Task.CompletedTask; + }; + } +} + /// /// Creates server call handlers. Provides a place to get services that call handlers will use. /// -internal partial class ServerCallHandlerFactory<[DynamicallyAccessedMembers(GrpcProtocolConstants.ServiceAccessibility)] TService> where TService : class +internal partial class ServerCallHandlerFactory<[DynamicallyAccessedMembers(GrpcProtocolConstants.ServiceAccessibility)] TService> : IServerCallHandlerFactory where TService : class { private readonly ILoggerFactory _loggerFactory; private readonly IGrpcServiceActivator _serviceActivator; diff --git a/src/Grpc.AspNetCore.Server/Model/Internal/ServiceRouteBuilder.cs b/src/Grpc.AspNetCore.Server/Model/Internal/ServiceRouteBuilder.cs index dc3613ffa..1b4a4dc35 100644 --- a/src/Grpc.AspNetCore.Server/Model/Internal/ServiceRouteBuilder.cs +++ b/src/Grpc.AspNetCore.Server/Model/Internal/ServiceRouteBuilder.cs @@ -23,10 +23,67 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; -using Log = Grpc.AspNetCore.Server.Model.Internal.ServiceRouteBuilderLog; +using Helper = Grpc.AspNetCore.Server.Model.Internal.ServiceRouteBuilderHelper; namespace Grpc.AspNetCore.Server.Model.Internal; +internal class ServiceRouteBuilder +{ + private readonly ServerCallHandlerFactory _serverCallHandlerFactory; + private readonly ServiceMethodsRegistry _serviceMethodsRegistry; + private readonly ILoggerFactory _loggerFactory; + private readonly ILogger _logger; + + public ServiceRouteBuilder( + ServerCallHandlerFactory callHandlerFactory, + ServiceMethodsRegistry serviceMethodsRegistry, + ILoggerFactory loggerFactory) + { + _serverCallHandlerFactory = callHandlerFactory; + _serviceMethodsRegistry = serviceMethodsRegistry; + _loggerFactory = loggerFactory; + _logger = loggerFactory.CreateLogger(); + } + + [RequiresUnreferencedCode("Due to type erasure in ServerServiceDefinition, Build is incompatible with trimming.")] + internal List Build(IEndpointRouteBuilder endpointRouteBuilder, ServerServiceDefinition serverServiceDefinition) + { + ServiceRouteBuilderLog.DiscoveringServiceMethods(_logger, typeof(ServerServiceDefinition)); + + var serviceBinder = new EndpointServiceBinder(_serverCallHandlerFactory, endpointRouteBuilder, _loggerFactory); + + serverServiceDefinition.BindService(serviceBinder); + var endpointConventionBuilders = serviceBinder.EndpointConventionBuilders; + + if(serviceBinder.MethodModels.Count > 0) + { + foreach(var method in serviceBinder.MethodModels) + { + var serviceMethodAttribute = method.Metadata + .Select(data => data as BindServiceMethodAttribute) + .SingleOrDefault(data => data is not null); + var serviceType = serviceMethodAttribute?.BindType ?? typeof(ServerServiceDefinition); + Helper.AddImplementedEndpoint(_logger, serviceType, endpointConventionBuilders, endpointRouteBuilder, method); + } + } + else + { + ServiceRouteBuilderLog.NoServiceMethodsDiscovered(_logger, typeof(ServerServiceDefinition)); + } + + Helper.CreateUnimplementedEndpoints( + endpointRouteBuilder, + _serviceMethodsRegistry, + _serverCallHandlerFactory, + serviceBinder.MethodModels, + endpointConventionBuilders); + + _serviceMethodsRegistry.Methods.AddRange(serviceBinder.MethodModels); + + return endpointConventionBuilders; + } +} + internal class ServiceRouteBuilder<[DynamicallyAccessedMembers(GrpcProtocolConstants.ServiceAccessibility)] TService> where TService : class { private readonly IEnumerable> _serviceMethodProviders; @@ -48,7 +105,7 @@ public ServiceRouteBuilder( internal List Build(IEndpointRouteBuilder endpointRouteBuilder) { - Log.DiscoveringServiceMethods(_logger, typeof(TService)); + ServiceRouteBuilderLog.DiscoveringServiceMethods(_logger, typeof(TService)); var serviceMethodProviderContext = new ServiceMethodProviderContext(_serverCallHandlerFactory); foreach (var serviceMethodProvider in _serviceMethodProviders) @@ -61,39 +118,15 @@ internal List Build(IEndpointRouteBuilder endpointRo { foreach (var method in serviceMethodProviderContext.Methods) { - var endpointBuilder = endpointRouteBuilder.Map(method.Pattern, method.RequestDelegate); - - endpointBuilder.Add(ep => - { - ep.DisplayName = $"gRPC - {method.Pattern.RawText}"; - - ep.Metadata.Add(new GrpcMethodMetadata(typeof(TService), method.Method)); - foreach (var item in method.Metadata) - { - ep.Metadata.Add(item); - } - }); - - endpointConventionBuilders.Add(endpointBuilder); - - // Report the last HttpMethodMetadata added. It's the metadata used by routing. - var httpMethod = method.Metadata.OfType().LastOrDefault(); - - Log.AddedServiceMethod( - _logger, - method.Method.Name, - method.Method.ServiceName, - method.Method.Type, - httpMethod?.HttpMethods ?? Array.Empty(), - method.Pattern.RawText ?? string.Empty); + Helper.AddImplementedEndpoint(_logger, typeof(TService), endpointConventionBuilders, endpointRouteBuilder, method); } } else { - Log.NoServiceMethodsDiscovered(_logger, typeof(TService)); + ServiceRouteBuilderLog.NoServiceMethodsDiscovered(_logger, typeof(TService)); } - CreateUnimplementedEndpoints( + Helper.CreateUnimplementedEndpoints( endpointRouteBuilder, _serviceMethodsRegistry, _serverCallHandlerFactory, @@ -104,11 +137,48 @@ internal List Build(IEndpointRouteBuilder endpointRo return endpointConventionBuilders; } +} + +internal static class ServiceRouteBuilderHelper +{ + internal static void AddImplementedEndpoint( + ILogger logger, + [DynamicallyAccessedMembers(GrpcProtocolConstants.ServiceAccessibility)] Type serviceType, + List endpointConventionBuilders, + IEndpointRouteBuilder endpointRouteBuilder, + MethodModel method) + { + var endpointBuilder = endpointRouteBuilder.Map(method.Pattern, method.RequestDelegate); + + endpointBuilder.Add(ep => + { + ep.DisplayName = $"gRPC - {method.Pattern.RawText}"; + + ep.Metadata.Add(new GrpcMethodMetadata(serviceType, method.Method)); + foreach (var item in method.Metadata) + { + ep.Metadata.Add(item); + } + }); + + endpointConventionBuilders.Add(endpointBuilder); + + // Report the last HttpMethodMetadata added. It's the metadata used by routing. + var httpMethod = method.Metadata.OfType().LastOrDefault(); + + ServiceRouteBuilderLog.LogAddedServiceMethod( + logger, + method.Method.Name, + method.Method.ServiceName, + method.Method.Type, + httpMethod?.HttpMethods ?? Array.Empty(), + method.Pattern.RawText ?? string.Empty); + } internal static void CreateUnimplementedEndpoints( IEndpointRouteBuilder endpointRouteBuilder, ServiceMethodsRegistry serviceMethodsRegistry, - ServerCallHandlerFactory serverCallHandlerFactory, + IServerCallHandlerFactory serverCallHandlerFactory, List serviceMethods, List endpointConventionBuilders) { @@ -161,7 +231,7 @@ internal static partial class ServiceRouteBuilderLog [LoggerMessage(Level = LogLevel.Trace, EventId = 1, EventName = "AddedServiceMethod", Message = "Added gRPC method '{MethodName}' to service '{ServiceName}'. Method type: {MethodType}, HTTP method: {HttpMethod}, route pattern: '{RoutePattern}'.")] private static partial void AddedServiceMethod(ILogger logger, string methodName, string serviceName, MethodType methodType, string HttpMethod, string routePattern); - public static void AddedServiceMethod(ILogger logger, string methodName, string serviceName, MethodType methodType, IReadOnlyList httpMethods, string routePattern) + public static void LogAddedServiceMethod(ILogger logger, string methodName, string serviceName, MethodType methodType, IReadOnlyList httpMethods, string routePattern) { if (logger.IsEnabled(LogLevel.Trace)) { diff --git a/src/Grpc.Core.Api/ServerServiceDefinition.cs b/src/Grpc.Core.Api/ServerServiceDefinition.cs index cc5f1e6da..255549697 100644 --- a/src/Grpc.Core.Api/ServerServiceDefinition.cs +++ b/src/Grpc.Core.Api/ServerServiceDefinition.cs @@ -38,7 +38,7 @@ internal ServerServiceDefinition(List> addMethodAction /// /// Forwards all the previously stored AddMethod calls to the service binder. /// - internal void BindService(ServiceBinderBase serviceBinder) + public void BindService(ServiceBinderBase serviceBinder) { foreach (var addMethodAction in addMethodActions) { diff --git a/src/Shared/Server/ClientStreamingServerMethodInvoker.cs b/src/Shared/Server/ClientStreamingServerMethodInvoker.cs index efbad4804..b8178685d 100644 --- a/src/Shared/Server/ClientStreamingServerMethodInvoker.cs +++ b/src/Shared/Server/ClientStreamingServerMethodInvoker.cs @@ -25,6 +25,52 @@ namespace Grpc.Shared.Server; +/// +/// Client streaming server method invoker for . +/// +/// Request message type for this method. +/// Response message type for this method. +internal sealed class ClientStreamingServerMethodInvoker : ServerMethodInvokerBase + where TRequest : class + where TResponse : class +{ + private readonly ClientStreamingServerMethod _invoker; + + /// + /// Creates a new instance of . + /// + /// The client streaming method to invoke. + /// The description of the gRPC method. + /// The options used to execute the method. + public ClientStreamingServerMethodInvoker( + ClientStreamingServerMethod invoker, + Method method, + MethodOptions options) + : base(method, options) + { + _invoker = invoker; + + if (Options.HasInterceptors) + { + var interceptorPipeline = new InterceptorPipelineBuilder(Options.Interceptors); + _invoker = interceptorPipeline.ClientStreamingPipeline(_invoker); + } + } + + /// + /// Invoke the client streaming method with the specified . + /// + /// The for the current request. + /// The . + /// The reader. + /// A that represents the asynchronous method. The + /// property returns the message. + public async Task Invoke(HttpContext _, ServerCallContext serverCallContext, IAsyncStreamReader requestStream) + { + return await _invoker(requestStream, serverCallContext); + } +} + /// /// Client streaming server method invoker. /// diff --git a/src/Shared/Server/DuplexStreamingServerMethodInvoker.cs b/src/Shared/Server/DuplexStreamingServerMethodInvoker.cs index e195fb88f..e9b4edc5a 100644 --- a/src/Shared/Server/DuplexStreamingServerMethodInvoker.cs +++ b/src/Shared/Server/DuplexStreamingServerMethodInvoker.cs @@ -25,6 +25,52 @@ namespace Grpc.Shared.Server; +/// +/// Duplex streaming server method invoker for . +/// +/// Request message type for this method. +/// Response message type for this method. +internal sealed class DuplexStreamingServerMethodInvoker : ServerMethodInvokerBase + where TRequest : class + where TResponse : class +{ + private readonly DuplexStreamingServerMethod _invoker; + + /// + /// Creates a new instance of . + /// + /// The duplex streaming method to invoke. + /// The description of the gRPC method. + /// The options used to execute the method. + public DuplexStreamingServerMethodInvoker( + DuplexStreamingServerMethod invoker, + Method method, + MethodOptions options) + : base(method, options) + { + _invoker = invoker; + + if (Options.HasInterceptors) + { + var interceptorPipeline = new InterceptorPipelineBuilder(Options.Interceptors); + _invoker = interceptorPipeline.DuplexStreamingPipeline(_invoker); + } + } + + /// + /// Invoke the duplex streaming method with the specified . + /// + /// The for the current request. + /// The . + /// The reader. + /// The writer. + /// A that represents the asynchronous method. + public async Task Invoke(HttpContext _, ServerCallContext serverCallContext, IAsyncStreamReader requestStream, IServerStreamWriter responseStream) + { + await _invoker(requestStream, responseStream, serverCallContext); + } +} + /// /// Duplex streaming server method invoker. /// diff --git a/src/Shared/Server/ServerMethodInvokerBase.cs b/src/Shared/Server/ServerMethodInvokerBase.cs index b2afd3ef4..b1c1f3a41 100644 --- a/src/Shared/Server/ServerMethodInvokerBase.cs +++ b/src/Shared/Server/ServerMethodInvokerBase.cs @@ -23,6 +23,39 @@ namespace Grpc.Shared.Server; +/// +/// Server method invoker base type for . +/// +/// Request message type for this method. +/// Response message type for this method. +internal abstract class ServerMethodInvokerBase + where TRequest : class + where TResponse : class +{ + /// + /// Gets the description of the gRPC method. + /// + public Method Method { get; } + + /// + /// Gets the options used to execute the method. + /// + public MethodOptions Options { get; } + + /// + /// Creates a new instance of . + /// + /// The description of the gRPC method. + /// The options used to execute the method. + private protected ServerMethodInvokerBase( + Method method, + MethodOptions options) + { + Method = method; + Options = options; + } +} + /// /// Server method invoker base type. /// diff --git a/src/Shared/Server/ServerStreamingServerMethodInvoker.cs b/src/Shared/Server/ServerStreamingServerMethodInvoker.cs index 268e17463..077b5fc1e 100644 --- a/src/Shared/Server/ServerStreamingServerMethodInvoker.cs +++ b/src/Shared/Server/ServerStreamingServerMethodInvoker.cs @@ -25,6 +25,52 @@ namespace Grpc.Shared.Server; +/// +/// Server streaming server method invoker for . +/// +/// Request message type for this method. +/// Response message type for this method. +internal sealed class ServerStreamingServerMethodInvoker : ServerMethodInvokerBase + where TRequest : class + where TResponse : class +{ + private readonly ServerStreamingServerMethod _invoker; + + /// + /// Creates a new instance of . + /// + /// The server streaming method to invoke. + /// The description of the gRPC method. + /// The options used to execute the method. + public ServerStreamingServerMethodInvoker( + ServerStreamingServerMethod invoker, + Method method, + MethodOptions options) + : base(method, options) + { + _invoker = invoker; + + if (Options.HasInterceptors) + { + var interceptorPipeline = new InterceptorPipelineBuilder(Options.Interceptors); + _invoker = interceptorPipeline.ServerStreamingPipeline(_invoker); + } + } + + /// + /// Invoke the server streaming method with the specified . + /// + /// The for the current request. + /// The . + /// The message. + /// The stream writer. + /// A that represents the asynchronous method. + public async Task Invoke(HttpContext _, ServerCallContext serverCallContext, TRequest request, IServerStreamWriter streamWriter) + { + await _invoker(request, streamWriter, serverCallContext); + } +} + /// /// Server streaming server method invoker. /// diff --git a/src/Shared/Server/UnaryServerMethodInvoker.cs b/src/Shared/Server/UnaryServerMethodInvoker.cs index cbd1115fc..cd1608c31 100644 --- a/src/Shared/Server/UnaryServerMethodInvoker.cs +++ b/src/Shared/Server/UnaryServerMethodInvoker.cs @@ -26,6 +26,52 @@ namespace Grpc.Shared.Server; +/// +/// Unary server method invoker for . +/// +/// Request message type for this method. +/// Response message type for this method. +internal sealed class UnaryServerMethodInvoker : ServerMethodInvokerBase + where TRequest : class + where TResponse : class +{ + private readonly UnaryServerMethod _invoker; + + /// + /// Creates a new instance of . + /// + /// The unary method to invoke. + /// The description of the gRPC method. + /// The options used to execute the method. + public UnaryServerMethodInvoker( + UnaryServerMethod invoker, + Method method, + MethodOptions options) + : base(method, options) + { + _invoker = invoker; + + if (Options.HasInterceptors) + { + var interceptorPipeline = new InterceptorPipelineBuilder(Options.Interceptors); + _invoker = interceptorPipeline.UnaryPipeline(_invoker); + } + } + + /// + /// Invoke the unary method with the specified . + /// + /// The for the current request. + /// The . + /// The message. + /// A that represents the asynchronous method. The + /// property returns the message. + public Task Invoke(HttpContext _, ServerCallContext serverCallContext, TRequest request) + { + return _invoker(request, serverCallContext); + } +} + /// /// Unary server method invoker. /// diff --git a/test/Grpc.AspNetCore.Server.Tests/GrpcEndpointRouteBuilderExtensionsTests.cs b/test/Grpc.AspNetCore.Server.Tests/GrpcEndpointRouteBuilderExtensionsTests.cs index 423444573..e5e73f054 100644 --- a/test/Grpc.AspNetCore.Server.Tests/GrpcEndpointRouteBuilderExtensionsTests.cs +++ b/test/Grpc.AspNetCore.Server.Tests/GrpcEndpointRouteBuilderExtensionsTests.cs @@ -101,6 +101,40 @@ public void MapGrpcService_CanBindSubclass_CreatesEndpoints() public void MapGrpcService_CanBindSubSubclass_CreatesEndpoints() { BindServiceCore(); + } + + [Test] + public void MapGrpcService_ServerServiceDefinition_CreateEndPoints() + { + // Arrange + var services = ServicesHelpers.CreateServices(); + + var routeBuilder = CreateTestEndpointRouteBuilder(services.BuildServiceProvider(validateScopes: true)); + + // Act + var service = GreeterWithAttribute.BindService(new GreeterWithAttributeService()); + routeBuilder.MapGrpcService(service); + + // Assert + AssertForBindServiceCore(routeBuilder); + } + + [Test] + public void MapGrpcService_GetServerServiceDefinition_CreateEndPoints() + { + // Arrange + var services = ServicesHelpers.CreateServices(); + + var routeBuilder = CreateTestEndpointRouteBuilder(services.BuildServiceProvider(validateScopes: true)); + + // Act + static ServerServiceDefinition serverServiceDefinition(IServiceProvider provider) + => GreeterWithAttribute.BindService(new GreeterWithAttributeService()); + + routeBuilder.MapGrpcService(serverServiceDefinition); + + // Assert + AssertForBindServiceCore(routeBuilder); } private void BindServiceCore() where TService : class @@ -114,6 +148,11 @@ private void BindServiceCore() where TService : class routeBuilder.MapGrpcService(); // Assert + AssertForBindServiceCore(routeBuilder); + } + + private void AssertForBindServiceCore(IEndpointRouteBuilder routeBuilder) + { var endpoints = routeBuilder.DataSources .SelectMany(ds => ds.Endpoints) .Where(e => e.Metadata.GetMetadata() != null) @@ -128,7 +167,7 @@ private void BindServiceCore() where TService : class var routeEndpoint2 = (RouteEndpoint)endpoints[1]; Assert.AreEqual("/greet.Greeter/SayHellos", routeEndpoint2.RoutePattern.RawText); Assert.AreEqual("POST", routeEndpoint2.Metadata.GetMetadata()?.HttpMethods.Single()); - Assert.AreEqual("/greet.Greeter/SayHellos", routeEndpoint2.Metadata.GetMetadata()?.Method.FullName); + Assert.AreEqual("/greet.Greeter/SayHellos", routeEndpoint2.Metadata.GetMetadata()?.Method.FullName); } [Test] diff --git a/test/Grpc.AspNetCore.Server.Tests/TestObjects/Services/WithAttribute/GreeterWithAttribute.cs b/test/Grpc.AspNetCore.Server.Tests/TestObjects/Services/WithAttribute/GreeterWithAttribute.cs index 08dabb7ca..6a6e2bae8 100644 --- a/test/Grpc.AspNetCore.Server.Tests/TestObjects/Services/WithAttribute/GreeterWithAttribute.cs +++ b/test/Grpc.AspNetCore.Server.Tests/TestObjects/Services/WithAttribute/GreeterWithAttribute.cs @@ -1,4 +1,4 @@ -#region Copyright notice and license +#region Copyright notice and license // Copyright 2019 The gRPC Authors // @@ -57,7 +57,10 @@ public abstract partial class GreeterBase public static ServerServiceDefinition BindService(GreeterBase serviceImpl) { - throw new NotImplementedException(); + return ServerServiceDefinition.CreateBuilder() + .AddMethod(__Method_SayHello, serviceImpl.SayHello) + .AddMethod(__Method_SayHellos, serviceImpl.SayHellos) + .Build(); } public static void BindService(ServiceBinderBase serviceBinder, GreeterBase serviceImpl)