Skip to content

Commit 0ea0112

Browse files
authored
Assign primary handler before other configuration (#2445)
1 parent 84a4fcb commit 0ea0112

File tree

13 files changed

+157
-56
lines changed

13 files changed

+157
-56
lines changed

Directory.Packages.props

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<OpenTelemetryIntergationPackageVersion>1.8.1</OpenTelemetryIntergationPackageVersion>
1010
<OpenTelemetryGrpcPackageVersion>1.8.0-beta.1</OpenTelemetryGrpcPackageVersion>
1111
<MicrosoftExtensionsPackageVersion>8.0.0</MicrosoftExtensionsPackageVersion>
12-
<MicrosoftExtensions6PackageVersion>6.0.0</MicrosoftExtensions6PackageVersion>
12+
<MicrosoftExtensionsLtsPackageVersion>6.0.0</MicrosoftExtensionsLtsPackageVersion>
1313
</PropertyGroup>
1414
<ItemGroup>
1515
<!-- ASP.NET Core -->
@@ -24,6 +24,7 @@
2424
<PackageVersion Include="Microsoft.AspNetCore.Grpc.Swagger" Version="0.8.0" />
2525

2626
<!-- Extensions -->
27+
<PackageVersion Include="Microsoft.Extensions.Http" Version="$(MicrosoftExtensionsPackageVersion)" />
2728
<PackageVersion Include="Microsoft.Extensions.Logging" Version="$(MicrosoftExtensionsPackageVersion)" />
2829
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsPackageVersion)" />
2930
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="$(MicrosoftExtensionsPackageVersion)" />

src/Grpc.Net.Client/Grpc.Net.Client.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
</PropertyGroup>
2424

2525
<ItemGroup>
26-
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" VersionOverride="$(MicrosoftExtensions6PackageVersion)" />
26+
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" VersionOverride="$(MicrosoftExtensionsLtsPackageVersion)" />
2727
</ItemGroup>
2828

2929
<ItemGroup>

src/Grpc.Net.Client/GrpcChannel.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,10 @@ private HttpMessageInvoker CreateInternalHttpInvoker(HttpMessageHandler? handler
489489
// Decision to dispose invoker is controlled by _shouldDisposeHttpClient.
490490
if (handler == null)
491491
{
492-
handler = HttpHandlerFactory.CreatePrimaryHandler();
492+
if (!HttpHandlerFactory.TryCreatePrimaryHandler(out handler))
493+
{
494+
throw HttpHandlerFactory.CreateUnsupportedHandlerException();
495+
}
493496
}
494497
else
495498
{

src/Grpc.Net.ClientFactory/Grpc.Net.ClientFactory.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,6 @@
2626
<ItemGroup>
2727
<!-- PrivateAssets set to None to ensure the build targets/props are propagated to parent project -->
2828
<ProjectReference Include="..\Grpc.Net.Client\Grpc.Net.Client.csproj" PrivateAssets="None" />
29-
<PackageReference Include="Microsoft.Extensions.Http" VersionOverride="$(MicrosoftExtensions6PackageVersion)" />
29+
<PackageReference Include="Microsoft.Extensions.Http" VersionOverride="$(MicrosoftExtensionsLtsPackageVersion)" />
3030
</ItemGroup>
3131
</Project>

src/Grpc.Net.ClientFactory/GrpcClientServiceExtensions.cs

Lines changed: 41 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
#endregion
1818

19+
using System;
1920
using System.Diagnostics.CodeAnalysis;
2021
using Grpc.Net.ClientFactory;
2122
using Grpc.Net.ClientFactory.Internal;
@@ -289,9 +290,7 @@ private static IHttpClientBuilder AddGrpcClientCore<
289290
// because we access it by reaching into the service collection.
290291
services.TryAddSingleton(new GrpcClientMappingRegistry());
291292

292-
IHttpClientBuilder clientBuilder = services.AddGrpcHttpClient<TClient>(name);
293-
294-
return clientBuilder;
293+
return services.AddGrpcHttpClient<TClient>(name);
295294
}
296295

297296
/// <summary>
@@ -306,25 +305,22 @@ private static IHttpClientBuilder AddGrpcHttpClient<
306305
{
307306
ArgumentNullThrowHelper.ThrowIfNull(services);
308307

309-
services
310-
.AddHttpClient(name)
311-
.ConfigurePrimaryHttpMessageHandler(() =>
312-
{
313-
// Set PrimaryHandler to null so we can track whether the user
314-
// set a value or not. If they didn't set their own handler then
315-
// one will be created by PostConfigure.
316-
return null!;
317-
});
308+
var builder = services.AddHttpClient(name);
318309

319-
services.PostConfigure<HttpClientFactoryOptions>(name, options =>
310+
builder.Services.AddTransient<TClient>(s =>
320311
{
321-
options.HttpMessageHandlerBuilderActions.Add(builder =>
312+
var clientFactory = s.GetRequiredService<GrpcClientFactory>();
313+
return clientFactory.CreateClient<TClient>(name);
314+
});
315+
316+
// Insert primary handler before other configuration so there is the opportunity to override it.
317+
// This should run before ConfigureDefaultHttpClient so the handler can be overriden in defaults.
318+
var configurePrimaryHandler = ServiceDescriptor.Singleton<IConfigureOptions<HttpClientFactoryOptions>>(new ConfigureNamedOptions<HttpClientFactoryOptions>(name, options =>
319+
{
320+
options.HttpMessageHandlerBuilderActions.Add(b =>
322321
{
323-
if (builder.PrimaryHandler == null)
322+
if (HttpHandlerFactory.TryCreatePrimaryHandler(out var handler))
324323
{
325-
// This will throw in .NET Standard 2.0 with a prompt that a user must set a handler.
326-
// Because it throws it should only be called in PostConfigure if no handler has been set.
327-
var handler = HttpHandlerFactory.CreatePrimaryHandler();
328324
#if NET5_0_OR_GREATER
329325
if (handler is SocketsHttpHandler socketsHttpHandler)
330326
{
@@ -336,37 +332,34 @@ private static IHttpClientBuilder AddGrpcHttpClient<
336332
}
337333
#endif
338334

339-
builder.PrimaryHandler = handler;
335+
b.PrimaryHandler = handler;
336+
}
337+
else
338+
{
339+
b.PrimaryHandler = UnsupportedHttpHandler.Instance;
340340
}
341341
});
342-
});
343-
344-
var builder = new DefaultHttpClientBuilder(services, name);
342+
}));
343+
services.Insert(0, configurePrimaryHandler);
345344

346-
builder.Services.AddTransient<TClient>(s =>
345+
// Some platforms don't have a built-in handler that supports gRPC.
346+
// Validate that a handler was set by the app to after all configuration has run.
347+
services.PostConfigure<HttpClientFactoryOptions>(name, options =>
347348
{
348-
var clientFactory = s.GetRequiredService<GrpcClientFactory>();
349-
return clientFactory.CreateClient<TClient>(builder.Name);
349+
options.HttpMessageHandlerBuilderActions.Add(builder =>
350+
{
351+
if (builder.PrimaryHandler == UnsupportedHttpHandler.Instance)
352+
{
353+
throw HttpHandlerFactory.CreateUnsupportedHandlerException();
354+
}
355+
});
350356
});
351357

352358
ReserveClient(builder, typeof(TClient), name);
353359

354360
return builder;
355361
}
356362

357-
private class DefaultHttpClientBuilder : IHttpClientBuilder
358-
{
359-
public DefaultHttpClientBuilder(IServiceCollection services, string name)
360-
{
361-
Services = services;
362-
Name = name;
363-
}
364-
365-
public string Name { get; }
366-
367-
public IServiceCollection Services { get; }
368-
}
369-
370363
private static void ReserveClient(IHttpClientBuilder builder, Type type, string name)
371364
{
372365
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
384377

385378
registry.NamedClientRegistrations[name] = type;
386379
}
380+
381+
private sealed class UnsupportedHttpHandler : HttpMessageHandler
382+
{
383+
public static readonly UnsupportedHttpHandler Instance = new UnsupportedHttpHandler();
384+
385+
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
386+
{
387+
return Task.FromException<HttpResponseMessage>(HttpHandlerFactory.CreateUnsupportedHandlerException());
388+
}
389+
}
387390
}

src/Grpc.Net.ClientFactory/Internal/GrpcClientMappingRegistry.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#region Copyright notice and license
1+
#region Copyright notice and license
22

33
// Copyright 2019 The gRPC Authors
44
//
@@ -16,7 +16,6 @@
1616

1717
#endregion
1818

19-
2019
namespace Grpc.Net.ClientFactory.Internal;
2120

2221
internal class GrpcClientMappingRegistry

src/Shared/HttpHandlerFactory.cs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,43 +16,53 @@
1616

1717
#endregion
1818

19+
using System.Diagnostics.CodeAnalysis;
1920
using Grpc.Net.Client;
2021

2122
namespace Grpc.Shared;
2223

2324
internal static class HttpHandlerFactory
2425
{
25-
public static HttpMessageHandler CreatePrimaryHandler()
26+
public static bool TryCreatePrimaryHandler([NotNullWhen(true)] out HttpMessageHandler? primaryHandler)
2627
{
2728
#if NET5_0_OR_GREATER
2829
// If we're in .NET 5 and SocketsHttpHandler is supported (it's not in Blazor WebAssembly)
2930
// then create SocketsHttpHandler with EnableMultipleHttp2Connections set to true. That will
3031
// allow a gRPC channel to create new connections if the maximum allow concurrency is exceeded.
3132
if (SocketsHttpHandler.IsSupported)
3233
{
33-
return new SocketsHttpHandler
34+
primaryHandler = new SocketsHttpHandler
3435
{
3536
EnableMultipleHttp2Connections = true
3637
};
38+
return true;
3739
}
3840
#endif
3941

4042
#if NET462
4143
// Create WinHttpHandler with EnableMultipleHttp2Connections set to true. That will
4244
// allow a gRPC channel to create new connections if the maximum allow concurrency is exceeded.
43-
return new WinHttpHandler
45+
primaryHandler = new WinHttpHandler
4446
{
4547
EnableMultipleHttp2Connections = true
4648
};
49+
return true;
4750
#elif !NETSTANDARD2_0
48-
return new HttpClientHandler();
51+
primaryHandler = new HttpClientHandler();
52+
return true;
4953
#else
54+
primaryHandler = null;
55+
return false;
56+
#endif
57+
}
58+
59+
public static Exception CreateUnsupportedHandlerException()
60+
{
5061
var message =
5162
$"gRPC requires extra configuration on .NET implementations that don't support gRPC over HTTP/2. " +
5263
$"An HTTP provider must be specified using {nameof(GrpcChannelOptions)}.{nameof(GrpcChannelOptions.HttpHandler)}." +
5364
$"The configured HTTP provider must either support HTTP/2 or be configured to use gRPC-Web. " +
5465
$"See https://aka.ms/aspnet/grpc/netstandard for details.";
55-
throw new PlatformNotSupportedException(message);
56-
#endif
66+
return new PlatformNotSupportedException(message);
5767
}
5868
}

test/FunctionalTests/Linker/LinkerTests.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
// Skip running load running tests in debug configuration
2020
#if !DEBUG
2121

22+
using System.Globalization;
2223
using System.Reflection;
2324
using System.Runtime.InteropServices;
2425
using Grpc.AspNetCore.FunctionalTests.Linker.Helpers;
@@ -86,7 +87,17 @@ private async Task RunWebsiteAndCallWithClient(bool publishAot)
8687
websiteProcess.Start(BuildStartPath(linkerTestsWebsitePath, "LinkerTestsWebsite"), arguments: null);
8788
await websiteProcess.WaitForReadyAsync().TimeoutAfter(Timeout);
8889

89-
clientProcess.Start(BuildStartPath(linkerTestsClientPath, "LinkerTestsClient"), arguments: websiteProcess.ServerPort!.ToString());
90+
string? clientArguments = null;
91+
if (websiteProcess.ServerPort is {} serverPort)
92+
{
93+
clientArguments = serverPort.ToString(CultureInfo.InvariantCulture);
94+
}
95+
else
96+
{
97+
throw new InvalidOperationException("Website server port not available.");
98+
}
99+
100+
clientProcess.Start(BuildStartPath(linkerTestsClientPath, "LinkerTestsClient"), arguments: clientArguments);
90101
await clientProcess.WaitForExitAsync().TimeoutAfter(Timeout);
91102
}
92103
finally

test/Grpc.Net.Client.Tests/Grpc.Net.Client.Tests.csproj

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,11 @@
4141
<PackageReference Include="Google.Protobuf" />
4242
<PackageReference Include="Grpc.Tools" PrivateAssets="All" />
4343
<PackageReference Include="Microsoft.Extensions.Logging.Testing" />
44-
<PackageReference Include="Microsoft.Extensions.Logging" VersionOverride="5.0.0" />
44+
<PackageReference Include="Microsoft.Extensions.Logging" />
4545
</ItemGroup>
4646

4747
<ItemGroup Condition="'$(TargetFramework)'=='net462'">
4848
<Reference Include="System.Net.Http" />
49-
50-
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
5149
</ItemGroup>
5250

5351
</Project>

test/Grpc.Net.ClientFactory.Tests/DefaultGrpcClientFactoryTests.cs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,34 @@ public void CreateClient_Default_DefaultInvokerSet()
5656
Assert.IsInstanceOf(typeof(HttpMessageInvoker), client.CallInvoker.Channel.HttpInvoker);
5757
}
5858

59+
#if NET6_0_OR_GREATER
60+
[Test]
61+
public void CreateClient_Default_PrimaryHandlerIsSocketsHttpHandler()
62+
{
63+
// Arrange
64+
HttpMessageHandler? clientPrimaryHandler = null;
65+
var services = new ServiceCollection();
66+
services
67+
.AddGrpcClient<TestGreeterClient>(o => o.Address = new Uri("http://localhost"))
68+
.ConfigurePrimaryHttpMessageHandler((primaryHandler, _) =>
69+
{
70+
clientPrimaryHandler = primaryHandler;
71+
});
72+
73+
var serviceProvider = services.BuildServiceProvider(validateScopes: true);
74+
75+
var clientFactory = CreateGrpcClientFactory(serviceProvider);
76+
77+
// Act
78+
var client = clientFactory.CreateClient<TestGreeterClient>(nameof(TestGreeterClient));
79+
80+
// Assert
81+
Assert.NotNull(clientPrimaryHandler);
82+
Assert.IsInstanceOf<SocketsHttpHandler>(clientPrimaryHandler);
83+
Assert.IsTrue(((SocketsHttpHandler)clientPrimaryHandler!).EnableMultipleHttp2Connections);
84+
}
85+
#endif
86+
5987
[Test]
6088
public void CreateClient_MatchingConfigurationBasedOnTypeName_ReturnConfiguration()
6189
{
@@ -254,6 +282,54 @@ public void CreateClient_NoPrimaryHandlerNetStandard_ThrowError()
254282
// Assert
255283
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);
256284
}
285+
286+
[Test]
287+
public void CreateClient_ConfigureDefaultAfter_Success()
288+
{
289+
// Arrange
290+
var services = new ServiceCollection();
291+
services
292+
.AddGrpcClient<TestGreeterClient>(o => o.Address = new Uri("https://localhost"));
293+
294+
services.ConfigureHttpClientDefaults(builder =>
295+
{
296+
builder.ConfigurePrimaryHttpMessageHandler(() => new NullHttpHandler());
297+
});
298+
299+
var serviceProvider = services.BuildServiceProvider(validateScopes: true);
300+
301+
var clientFactory = CreateGrpcClientFactory(serviceProvider);
302+
303+
// Act
304+
var client = clientFactory.CreateClient<TestGreeterClient>(nameof(TestGreeterClient));
305+
306+
// Assert
307+
Assert.IsNotNull(client);
308+
}
309+
310+
[Test]
311+
public void CreateClient_ConfigureDefaultBefore_Success()
312+
{
313+
// Arrange
314+
var services = new ServiceCollection();
315+
316+
services.ConfigureHttpClientDefaults(builder =>
317+
{
318+
builder.ConfigurePrimaryHttpMessageHandler(() => new NullHttpHandler());
319+
});
320+
321+
services.AddGrpcClient<TestGreeterClient>(o => o.Address = new Uri("https://localhost"));
322+
323+
var serviceProvider = services.BuildServiceProvider(validateScopes: true);
324+
325+
var clientFactory = CreateGrpcClientFactory(serviceProvider);
326+
327+
// Act
328+
var client = clientFactory.CreateClient<TestGreeterClient>(nameof(TestGreeterClient));
329+
330+
// Assert
331+
Assert.IsNotNull(client);
332+
}
257333
#endif
258334

259335
#if NET5_0_OR_GREATER

0 commit comments

Comments
 (0)