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();