diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 8dace7b8f..b9e727bb2 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -15,7 +15,7 @@ jobs:
build:
name: Basic Tests
- runs-on: ubuntu-latest
+ runs-on: ubuntu-22.04
steps:
- name: Check out code
@@ -36,7 +36,7 @@ jobs:
grpc_web:
name: gRPC-Web Tests
- runs-on: ubuntu-latest
+ runs-on: ubuntu-22.04
steps:
- name: Check out code
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 1c82cda73..35b4940a7 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -33,7 +33,7 @@
-
+
diff --git a/examples/GreeterByServiceDefinition/Client/Client.csproj b/examples/GreeterByServiceDefinition/Client/Client.csproj
new file mode 100644
index 000000000..8256c46ea
--- /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..b19669e10
--- /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/examples/Spar/Server/ClientApp/package-lock.json b/examples/Spar/Server/ClientApp/package-lock.json
index 7cf245d58..b80027769 100644
--- a/examples/Spar/Server/ClientApp/package-lock.json
+++ b/examples/Spar/Server/ClientApp/package-lock.json
@@ -3837,10 +3837,11 @@
"dev": true
},
"node_modules/elliptic": {
- "version": "6.6.0",
- "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.0.tgz",
- "integrity": "sha512-dpwoQcLc/2WLQvJvLRHKZ+f9FgOdjnq11rurqwekGQygGPsYSK29OMMD2WalatiqQ+XGFDglTNixpPfI+lpaAA==",
+ "version": "6.6.1",
+ "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz",
+ "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"bn.js": "^4.11.9",
"brorand": "^1.1.0",
@@ -12413,9 +12414,9 @@
"dev": true
},
"elliptic": {
- "version": "6.6.0",
- "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.0.tgz",
- "integrity": "sha512-dpwoQcLc/2WLQvJvLRHKZ+f9FgOdjnq11rurqwekGQygGPsYSK29OMMD2WalatiqQ+XGFDglTNixpPfI+lpaAA==",
+ "version": "6.6.1",
+ "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz",
+ "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==",
"dev": true,
"requires": {
"bn.js": "^4.11.9",
diff --git a/nuget.config b/nuget.config
index e4d73b867..d5ff7157f 100644
--- a/nuget.config
+++ b/nuget.config
@@ -3,7 +3,6 @@
-
diff --git a/src/Grpc.AspNetCore.Server/GrpcEndpointRouteBuilderExtensions.cs b/src/Grpc.AspNetCore.Server/GrpcEndpointRouteBuilderExtensions.cs
index f583a2c4c..3be616df8 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;
@@ -48,6 +49,43 @@ public static class GrpcEndpointRouteBuilderExtensions
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);
+ }
+
private static void ValidateServicesRegistered(IServiceProvider serviceProvider)
{
var marker = serviceProvider.GetService(typeof(GrpcMarkerService));
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 99437edc9..0272ac6bd 100644
--- a/src/Grpc.AspNetCore.Server/Internal/CallHandlers/ClientStreamingServerCallHandler.cs
+++ b/src/Grpc.AspNetCore.Server/Internal/CallHandlers/ClientStreamingServerCallHandler.cs
@@ -24,6 +24,56 @@
namespace Grpc.AspNetCore.Server.Internal.CallHandlers;
+internal sealed 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 sealed class ClientStreamingServerCallHandler<[DynamicallyAccessedMembers(GrpcProtocolConstants.ServiceAccessibility)] TService, TRequest, TResponse> : ServerCallHandlerBase
where TRequest : class
where TResponse : class
diff --git a/src/Grpc.AspNetCore.Server/Internal/CallHandlers/DuplexStreamingServerCallHandler.cs b/src/Grpc.AspNetCore.Server/Internal/CallHandlers/DuplexStreamingServerCallHandler.cs
index 2649b1fb5..b7811e131 100644
--- a/src/Grpc.AspNetCore.Server/Internal/CallHandlers/DuplexStreamingServerCallHandler.cs
+++ b/src/Grpc.AspNetCore.Server/Internal/CallHandlers/DuplexStreamingServerCallHandler.cs
@@ -23,6 +23,39 @@
namespace Grpc.AspNetCore.Server.Internal.CallHandlers;
+internal sealed 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 sealed class DuplexStreamingServerCallHandler<[DynamicallyAccessedMembers(GrpcProtocolConstants.ServiceAccessibility)] TService, TRequest, TResponse> : ServerCallHandlerBase
where TRequest : class
where TResponse : 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 da49752c9..0b315677f 100644
--- a/src/Grpc.AspNetCore.Server/Internal/CallHandlers/ServerStreamingServerCallHandler.cs
+++ b/src/Grpc.AspNetCore.Server/Internal/CallHandlers/ServerStreamingServerCallHandler.cs
@@ -23,6 +23,37 @@
namespace Grpc.AspNetCore.Server.Internal.CallHandlers;
+internal sealed 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 sealed class ServerStreamingServerCallHandler<[DynamicallyAccessedMembers(GrpcProtocolConstants.ServiceAccessibility)] TService, TRequest, TResponse> : ServerCallHandlerBase
where TRequest : class
where TResponse : class
diff --git a/src/Grpc.AspNetCore.Server/Internal/CallHandlers/UnaryServerCallHandler.cs b/src/Grpc.AspNetCore.Server/Internal/CallHandlers/UnaryServerCallHandler.cs
index f0d3047cd..557036d18 100644
--- a/src/Grpc.AspNetCore.Server/Internal/CallHandlers/UnaryServerCallHandler.cs
+++ b/src/Grpc.AspNetCore.Server/Internal/CallHandlers/UnaryServerCallHandler.cs
@@ -24,6 +24,45 @@
namespace Grpc.AspNetCore.Server.Internal.CallHandlers;
+internal sealed 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 sealed class UnaryServerCallHandler<[DynamicallyAccessedMembers(GrpcProtocolConstants.ServiceAccessibility)] TService, TRequest, TResponse> : ServerCallHandlerBase
where TRequest : class
where TResponse : 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..23ef15e6f
--- /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