diff --git a/Directory.Packages.props b/Directory.Packages.props
index c0b910e66..25dd91506 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -9,7 +9,7 @@
1.8.1
1.8.0-beta.1
8.0.0
- 6.0.0
+ 6.0.0
@@ -24,6 +24,7 @@
+
diff --git a/src/Grpc.Net.Client/Grpc.Net.Client.csproj b/src/Grpc.Net.Client/Grpc.Net.Client.csproj
index b97499188..a810fe41d 100644
--- a/src/Grpc.Net.Client/Grpc.Net.Client.csproj
+++ b/src/Grpc.Net.Client/Grpc.Net.Client.csproj
@@ -23,7 +23,7 @@
-
+
diff --git a/src/Grpc.Net.Client/GrpcChannel.cs b/src/Grpc.Net.Client/GrpcChannel.cs
index 0c4cea722..e84459040 100644
--- a/src/Grpc.Net.Client/GrpcChannel.cs
+++ b/src/Grpc.Net.Client/GrpcChannel.cs
@@ -489,7 +489,10 @@ private HttpMessageInvoker CreateInternalHttpInvoker(HttpMessageHandler? handler
// Decision to dispose invoker is controlled by _shouldDisposeHttpClient.
if (handler == null)
{
- handler = HttpHandlerFactory.CreatePrimaryHandler();
+ if (!HttpHandlerFactory.TryCreatePrimaryHandler(out handler))
+ {
+ throw HttpHandlerFactory.CreateUnsupportedHandlerException();
+ }
}
else
{
diff --git a/src/Grpc.Net.ClientFactory/Grpc.Net.ClientFactory.csproj b/src/Grpc.Net.ClientFactory/Grpc.Net.ClientFactory.csproj
index 0c77d633e..13d5b30d7 100644
--- a/src/Grpc.Net.ClientFactory/Grpc.Net.ClientFactory.csproj
+++ b/src/Grpc.Net.ClientFactory/Grpc.Net.ClientFactory.csproj
@@ -26,6 +26,6 @@
-
+
diff --git a/src/Grpc.Net.ClientFactory/GrpcClientServiceExtensions.cs b/src/Grpc.Net.ClientFactory/GrpcClientServiceExtensions.cs
index d7c13eaad..9e3b3bd00 100644
--- a/src/Grpc.Net.ClientFactory/GrpcClientServiceExtensions.cs
+++ b/src/Grpc.Net.ClientFactory/GrpcClientServiceExtensions.cs
@@ -16,6 +16,7 @@
#endregion
+using System;
using System.Diagnostics.CodeAnalysis;
using Grpc.Net.ClientFactory;
using Grpc.Net.ClientFactory.Internal;
@@ -289,9 +290,7 @@ private static IHttpClientBuilder AddGrpcClientCore<
// because we access it by reaching into the service collection.
services.TryAddSingleton(new GrpcClientMappingRegistry());
- IHttpClientBuilder clientBuilder = services.AddGrpcHttpClient(name);
-
- return clientBuilder;
+ return services.AddGrpcHttpClient(name);
}
///
@@ -306,25 +305,22 @@ private static IHttpClientBuilder AddGrpcHttpClient<
{
ArgumentNullThrowHelper.ThrowIfNull(services);
- services
- .AddHttpClient(name)
- .ConfigurePrimaryHttpMessageHandler(() =>
- {
- // Set PrimaryHandler to null so we can track whether the user
- // set a value or not. If they didn't set their own handler then
- // one will be created by PostConfigure.
- return null!;
- });
+ var builder = services.AddHttpClient(name);
- services.PostConfigure(name, options =>
+ builder.Services.AddTransient(s =>
{
- options.HttpMessageHandlerBuilderActions.Add(builder =>
+ var clientFactory = s.GetRequiredService();
+ return clientFactory.CreateClient(name);
+ });
+
+ // Insert primary handler before other configuration so there is the opportunity to override it.
+ // This should run before ConfigureDefaultHttpClient so the handler can be overriden in defaults.
+ var configurePrimaryHandler = ServiceDescriptor.Singleton>(new ConfigureNamedOptions(name, options =>
+ {
+ options.HttpMessageHandlerBuilderActions.Add(b =>
{
- if (builder.PrimaryHandler == null)
+ if (HttpHandlerFactory.TryCreatePrimaryHandler(out var handler))
{
- // This will throw in .NET Standard 2.0 with a prompt that a user must set a handler.
- // Because it throws it should only be called in PostConfigure if no handler has been set.
- var handler = HttpHandlerFactory.CreatePrimaryHandler();
#if NET5_0_OR_GREATER
if (handler is SocketsHttpHandler socketsHttpHandler)
{
@@ -336,17 +332,27 @@ private static IHttpClientBuilder AddGrpcHttpClient<
}
#endif
- builder.PrimaryHandler = handler;
+ b.PrimaryHandler = handler;
+ }
+ else
+ {
+ b.PrimaryHandler = UnsupportedHttpHandler.Instance;
}
});
- });
-
- var builder = new DefaultHttpClientBuilder(services, name);
+ }));
+ services.Insert(0, configurePrimaryHandler);
- builder.Services.AddTransient(s =>
+ // Some platforms don't have a built-in handler that supports gRPC.
+ // Validate that a handler was set by the app to after all configuration has run.
+ services.PostConfigure(name, options =>
{
- var clientFactory = s.GetRequiredService();
- return clientFactory.CreateClient(builder.Name);
+ options.HttpMessageHandlerBuilderActions.Add(builder =>
+ {
+ if (builder.PrimaryHandler == UnsupportedHttpHandler.Instance)
+ {
+ throw HttpHandlerFactory.CreateUnsupportedHandlerException();
+ }
+ });
});
ReserveClient(builder, typeof(TClient), name);
@@ -354,19 +360,6 @@ private static IHttpClientBuilder AddGrpcHttpClient<
return builder;
}
- private class DefaultHttpClientBuilder : IHttpClientBuilder
- {
- public DefaultHttpClientBuilder(IServiceCollection services, string name)
- {
- Services = services;
- Name = name;
- }
-
- public string Name { get; }
-
- public IServiceCollection Services { get; }
- }
-
private static void ReserveClient(IHttpClientBuilder builder, Type type, string name)
{
var registry = (GrpcClientMappingRegistry?)builder.Services.Single(sd => sd.ServiceType == typeof(GrpcClientMappingRegistry)).ImplementationInstance;
@@ -384,4 +377,14 @@ private static void ReserveClient(IHttpClientBuilder builder, Type type, string
registry.NamedClientRegistrations[name] = type;
}
+
+ private sealed class UnsupportedHttpHandler : HttpMessageHandler
+ {
+ public static readonly UnsupportedHttpHandler Instance = new UnsupportedHttpHandler();
+
+ protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ return Task.FromException(HttpHandlerFactory.CreateUnsupportedHandlerException());
+ }
+ }
}
diff --git a/src/Grpc.Net.ClientFactory/Internal/GrpcClientMappingRegistry.cs b/src/Grpc.Net.ClientFactory/Internal/GrpcClientMappingRegistry.cs
index f612cedc2..2dac10123 100644
--- a/src/Grpc.Net.ClientFactory/Internal/GrpcClientMappingRegistry.cs
+++ b/src/Grpc.Net.ClientFactory/Internal/GrpcClientMappingRegistry.cs
@@ -1,4 +1,4 @@
-#region Copyright notice and license
+#region Copyright notice and license
// Copyright 2019 The gRPC Authors
//
@@ -16,7 +16,6 @@
#endregion
-
namespace Grpc.Net.ClientFactory.Internal;
internal class GrpcClientMappingRegistry
diff --git a/src/Shared/HttpHandlerFactory.cs b/src/Shared/HttpHandlerFactory.cs
index 6d0661c7c..fecd71857 100644
--- a/src/Shared/HttpHandlerFactory.cs
+++ b/src/Shared/HttpHandlerFactory.cs
@@ -16,13 +16,14 @@
#endregion
+using System.Diagnostics.CodeAnalysis;
using Grpc.Net.Client;
namespace Grpc.Shared;
internal static class HttpHandlerFactory
{
- public static HttpMessageHandler CreatePrimaryHandler()
+ public static bool TryCreatePrimaryHandler([NotNullWhen(true)] out HttpMessageHandler? primaryHandler)
{
#if NET5_0_OR_GREATER
// If we're in .NET 5 and SocketsHttpHandler is supported (it's not in Blazor WebAssembly)
@@ -30,29 +31,38 @@ public static HttpMessageHandler CreatePrimaryHandler()
// allow a gRPC channel to create new connections if the maximum allow concurrency is exceeded.
if (SocketsHttpHandler.IsSupported)
{
- return new SocketsHttpHandler
+ primaryHandler = new SocketsHttpHandler
{
EnableMultipleHttp2Connections = true
};
+ return true;
}
#endif
#if NET462
// Create WinHttpHandler with EnableMultipleHttp2Connections set to true. That will
// allow a gRPC channel to create new connections if the maximum allow concurrency is exceeded.
- return new WinHttpHandler
+ primaryHandler = new WinHttpHandler
{
EnableMultipleHttp2Connections = true
};
+ return true;
#elif !NETSTANDARD2_0
- return new HttpClientHandler();
+ primaryHandler = new HttpClientHandler();
+ return true;
#else
+ primaryHandler = null;
+ return false;
+#endif
+ }
+
+ public static Exception CreateUnsupportedHandlerException()
+ {
var message =
$"gRPC requires extra configuration on .NET implementations that don't support gRPC over HTTP/2. " +
$"An HTTP provider must be specified using {nameof(GrpcChannelOptions)}.{nameof(GrpcChannelOptions.HttpHandler)}." +
$"The configured HTTP provider must either support HTTP/2 or be configured to use gRPC-Web. " +
$"See https://aka.ms/aspnet/grpc/netstandard for details.";
- throw new PlatformNotSupportedException(message);
-#endif
+ return new PlatformNotSupportedException(message);
}
}
diff --git a/test/FunctionalTests/Linker/LinkerTests.cs b/test/FunctionalTests/Linker/LinkerTests.cs
index 7a78c909a..8cb05d84c 100644
--- a/test/FunctionalTests/Linker/LinkerTests.cs
+++ b/test/FunctionalTests/Linker/LinkerTests.cs
@@ -19,6 +19,7 @@
// Skip running load running tests in debug configuration
#if !DEBUG
+using System.Globalization;
using System.Reflection;
using System.Runtime.InteropServices;
using Grpc.AspNetCore.FunctionalTests.Linker.Helpers;
@@ -86,7 +87,17 @@ private async Task RunWebsiteAndCallWithClient(bool publishAot)
websiteProcess.Start(BuildStartPath(linkerTestsWebsitePath, "LinkerTestsWebsite"), arguments: null);
await websiteProcess.WaitForReadyAsync().TimeoutAfter(Timeout);
- clientProcess.Start(BuildStartPath(linkerTestsClientPath, "LinkerTestsClient"), arguments: websiteProcess.ServerPort!.ToString());
+ string? clientArguments = null;
+ if (websiteProcess.ServerPort is {} serverPort)
+ {
+ clientArguments = serverPort.ToString(CultureInfo.InvariantCulture);
+ }
+ else
+ {
+ throw new InvalidOperationException("Website server port not available.");
+ }
+
+ clientProcess.Start(BuildStartPath(linkerTestsClientPath, "LinkerTestsClient"), arguments: clientArguments);
await clientProcess.WaitForExitAsync().TimeoutAfter(Timeout);
}
finally
diff --git a/test/Grpc.Net.Client.Tests/Grpc.Net.Client.Tests.csproj b/test/Grpc.Net.Client.Tests/Grpc.Net.Client.Tests.csproj
index f612232dc..73a63531b 100644
--- a/test/Grpc.Net.Client.Tests/Grpc.Net.Client.Tests.csproj
+++ b/test/Grpc.Net.Client.Tests/Grpc.Net.Client.Tests.csproj
@@ -41,13 +41,11 @@
-
+
-
-
diff --git a/test/Grpc.Net.ClientFactory.Tests/DefaultGrpcClientFactoryTests.cs b/test/Grpc.Net.ClientFactory.Tests/DefaultGrpcClientFactoryTests.cs
index d2a58d1c4..dc473d6c4 100644
--- a/test/Grpc.Net.ClientFactory.Tests/DefaultGrpcClientFactoryTests.cs
+++ b/test/Grpc.Net.ClientFactory.Tests/DefaultGrpcClientFactoryTests.cs
@@ -56,6 +56,34 @@ public void CreateClient_Default_DefaultInvokerSet()
Assert.IsInstanceOf(typeof(HttpMessageInvoker), client.CallInvoker.Channel.HttpInvoker);
}
+#if NET6_0_OR_GREATER
+ [Test]
+ public void CreateClient_Default_PrimaryHandlerIsSocketsHttpHandler()
+ {
+ // Arrange
+ HttpMessageHandler? clientPrimaryHandler = null;
+ var services = new ServiceCollection();
+ services
+ .AddGrpcClient(o => o.Address = new Uri("http://localhost"))
+ .ConfigurePrimaryHttpMessageHandler((primaryHandler, _) =>
+ {
+ clientPrimaryHandler = primaryHandler;
+ });
+
+ var serviceProvider = services.BuildServiceProvider(validateScopes: true);
+
+ var clientFactory = CreateGrpcClientFactory(serviceProvider);
+
+ // Act
+ var client = clientFactory.CreateClient(nameof(TestGreeterClient));
+
+ // Assert
+ Assert.NotNull(clientPrimaryHandler);
+ Assert.IsInstanceOf(clientPrimaryHandler);
+ Assert.IsTrue(((SocketsHttpHandler)clientPrimaryHandler!).EnableMultipleHttp2Connections);
+ }
+#endif
+
[Test]
public void CreateClient_MatchingConfigurationBasedOnTypeName_ReturnConfiguration()
{
@@ -254,6 +282,54 @@ public void CreateClient_NoPrimaryHandlerNetStandard_ThrowError()
// Assert
Assert.AreEqual(@"gRPC requires extra configuration on .NET implementations that don't support gRPC over HTTP/2. An HTTP provider must be specified using GrpcChannelOptions.HttpHandler.The configured HTTP provider must either support HTTP/2 or be configured to use gRPC-Web. See https://aka.ms/aspnet/grpc/netstandard for details.", ex.Message);
}
+
+ [Test]
+ public void CreateClient_ConfigureDefaultAfter_Success()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ services
+ .AddGrpcClient(o => o.Address = new Uri("https://localhost"));
+
+ services.ConfigureHttpClientDefaults(builder =>
+ {
+ builder.ConfigurePrimaryHttpMessageHandler(() => new NullHttpHandler());
+ });
+
+ var serviceProvider = services.BuildServiceProvider(validateScopes: true);
+
+ var clientFactory = CreateGrpcClientFactory(serviceProvider);
+
+ // Act
+ var client = clientFactory.CreateClient(nameof(TestGreeterClient));
+
+ // Assert
+ Assert.IsNotNull(client);
+ }
+
+ [Test]
+ public void CreateClient_ConfigureDefaultBefore_Success()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+
+ services.ConfigureHttpClientDefaults(builder =>
+ {
+ builder.ConfigurePrimaryHttpMessageHandler(() => new NullHttpHandler());
+ });
+
+ services.AddGrpcClient(o => o.Address = new Uri("https://localhost"));
+
+ var serviceProvider = services.BuildServiceProvider(validateScopes: true);
+
+ var clientFactory = CreateGrpcClientFactory(serviceProvider);
+
+ // Act
+ var client = clientFactory.CreateClient(nameof(TestGreeterClient));
+
+ // Assert
+ Assert.IsNotNull(client);
+ }
#endif
#if NET5_0_OR_GREATER
diff --git a/test/Grpc.Net.ClientFactory.Tests/Grpc.Net.ClientFactory.Tests.csproj b/test/Grpc.Net.ClientFactory.Tests/Grpc.Net.ClientFactory.Tests.csproj
index 50163ccb4..a78247ee4 100644
--- a/test/Grpc.Net.ClientFactory.Tests/Grpc.Net.ClientFactory.Tests.csproj
+++ b/test/Grpc.Net.ClientFactory.Tests/Grpc.Net.ClientFactory.Tests.csproj
@@ -29,6 +29,7 @@
+
diff --git a/testassets/InteropTestsWebsite/Dockerfile b/testassets/InteropTestsWebsite/Dockerfile
index c8f901a95..333c9bbc8 100644
--- a/testassets/InteropTestsWebsite/Dockerfile
+++ b/testassets/InteropTestsWebsite/Dockerfile
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/nightly/sdk:8.0 AS build-env
+FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env
WORKDIR /app
# Copy everything
diff --git a/testassets/LinkerTestsWebsite/Program.cs b/testassets/LinkerTestsWebsite/Program.cs
index eb0c95644..5f026746f 100644
--- a/testassets/LinkerTestsWebsite/Program.cs
+++ b/testassets/LinkerTestsWebsite/Program.cs
@@ -35,5 +35,4 @@
app.MapGrpcService();
-app.Lifetime.ApplicationStarted.Register(() => Console.WriteLine("Application started. Press Ctrl+C to shut down."));
app.Run();