Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#region Copyright notice and license
#region Copyright notice and license

// Copyright 2019 The gRPC Authors
//
Expand Down Expand Up @@ -33,6 +33,9 @@ public CallOptionsConfigurationInvoker(CallInvoker innerInvoker, IList<Action<Ca
_serviceProvider = serviceProvider;
}

// Internal for testing.
internal CallInvoker InnerInvoker => _innerInvoker;

private CallOptions ResolveCallOptions(CallOptions callOptions)
{
var context = new CallOptionsContext(callOptions, _serviceProvider);
Expand Down
75 changes: 73 additions & 2 deletions src/Grpc.Net.ClientFactory/Internal/GrpcCallInvokerFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
#endregion

using System.Collections.Concurrent;
using System.Globalization;
using System.Net.Http.Headers;
using Grpc.Core;
using Grpc.Net.Client;
using Grpc.Shared;
Expand All @@ -39,6 +41,7 @@ internal class GrpcCallInvokerFactory
private readonly IServiceScopeFactory _scopeFactory;
private readonly ConcurrentDictionary<EntryKey, CallInvoker> _activeChannels;
private readonly Func<EntryKey, CallInvoker> _invokerFactory;
private readonly ILogger<GrpcCallInvokerFactory> _logger;

public GrpcCallInvokerFactory(
IServiceScopeFactory scopeFactory,
Expand All @@ -57,6 +60,7 @@ public GrpcCallInvokerFactory(
_scopeFactory = scopeFactory;
_activeChannels = new ConcurrentDictionary<EntryKey, CallInvoker>();
_invokerFactory = CreateInvoker;
_logger = _loggerFactory.CreateLogger<GrpcCallInvokerFactory>();
}

public CallInvoker CreateInvoker(string name, Type type)
Expand All @@ -73,12 +77,58 @@ private CallInvoker CreateInvoker(EntryKey key)
try
{
var httpClientFactoryOptions = _httpClientFactoryOptionsMonitor.Get(name);
var clientFactoryOptions = _grpcClientFactoryOptionsMonitor.Get(name);

// gRPC channel is configured with a handler instead of a client, so HttpClientActions aren't used directly.
// To capture HttpClient configuration, a temp HttpClient is created and configured using HttpClientActions.
// Values from the temp HttpClient are then copied to the gRPC channel.
// Only values with overlap on both types are copied so log a message about the limitations.
if (httpClientFactoryOptions.HttpClientActions.Count > 0)
{
throw new InvalidOperationException($"The ConfigureHttpClient method is not supported when creating gRPC clients. Unable to create client with name '{name}'.");
Log.HttpClientActionsPartiallySupported(_logger, name);

var httpClient = new HttpClient(NullHttpMessageHandler.Instance);
foreach (var applyOptions in httpClientFactoryOptions.HttpClientActions)
{
applyOptions(httpClient);
}

// Copy configuration from HttpClient to GrpcChannel/CallOptions.
// This configuration should be overriden by gRPC specific config methods.
if (clientFactoryOptions.Address == null)
{
clientFactoryOptions.Address = httpClient.BaseAddress;
}

if (httpClient.DefaultRequestHeaders.Any())
{
var defaultHeaders = httpClient.DefaultRequestHeaders.ToList();

// Merge DefaultRequestHeaders with CallOptions.Headers.
// Follow behavior of DefaultRequestHeaders on HttpClient when merging.
// Don't replace or add new header values if the header name has already been set.
clientFactoryOptions.CallOptionsActions.Add(callOptionsContext =>
{
var metadata = callOptionsContext.CallOptions.Headers ?? new Metadata();
foreach (var entry in defaultHeaders)
{
// grpc requires header names are lower case before being added to collection.
var resolvedKey = entry.Key.ToLower(CultureInfo.InvariantCulture);

if (metadata.Get(resolvedKey) == null)
{
foreach (var value in entry.Value)
{
metadata.Add(resolvedKey, value);
}
}
}

callOptionsContext.CallOptions = callOptionsContext.CallOptions.WithHeaders(metadata);
});
}
}

var clientFactoryOptions = _grpcClientFactoryOptionsMonitor.Get(name);
var httpHandler = _messageHandlerFactory.CreateHandler(name);
if (httpHandler == null)
{
Expand Down Expand Up @@ -190,4 +240,25 @@ public override void SetCompositeCredentials(object state, ChannelCredentials ch
{
}
}

private sealed class NullHttpMessageHandler : HttpMessageHandler
{
public static readonly NullHttpMessageHandler Instance = new NullHttpMessageHandler();

protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}

private static class Log
{
private static readonly Action<ILogger, string, Exception?> _httpClientActionsPartiallySupported =
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(1, "HttpClientActionsPartiallySupported"), "The ConfigureHttpClient method is used to configure gRPC client '{ClientName}'. ConfigureHttpClient is partially supported when creating gRPC clients and only some HttpClient properties such as BaseAddress and DefaultRequestHeaders are applied to the gRPC client.");

public static void HttpClientActionsPartiallySupported(ILogger<GrpcCallInvokerFactory> logger, string clientName)
{
_httpClientActionsPartiallySupported(logger, clientName, null);
}
}
}
82 changes: 77 additions & 5 deletions test/Grpc.Net.ClientFactory.Tests/DefaultGrpcClientFactoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

#endregion

using System.Net;
using System.Net.Http.Headers;
using Greet;
using Grpc.Core;
using Grpc.Net.Client.Internal;
Expand Down Expand Up @@ -144,27 +146,93 @@ public void CreateClient_NoAddress_ThrowError()
}

[Test]
public void CreateClient_ConfigureHttpClient_ThrowError()
public async Task CreateClient_ConfigureHttpClient_LogMessage()
{
// Arrange
var testSink = new TestSink();
Uri? requestUri = null;
HttpRequestHeaders? requestHeaders = null;

var services = new ServiceCollection();
services.AddLogging(configure => configure.SetMinimumLevel(LogLevel.Trace));
services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, TestLoggerProvider>(s => new TestLoggerProvider(testSink, true)));
services
.AddGrpcClient<TestGreeterClient>()
.ConfigureHttpClient(options => options.BaseAddress = new Uri("http://contoso"))
.ConfigureHttpClient(options =>
{
options.BaseAddress = new Uri("http://contoso");
options.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", "abc");
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
return TestHttpMessageHandler.Create(async r =>
{
requestUri = r.RequestUri;
requestHeaders = r.Headers;

var streamContent = await ClientTestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout();
return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent);
});
});

var serviceProvider = services.BuildServiceProvider(validateScopes: true);

var clientFactory = CreateGrpcClientFactory(serviceProvider);

// Act
var client = clientFactory.CreateClient<TestGreeterClient>(nameof(TestGreeterClient));
var response = await client.SayHelloAsync(new HelloRequest()).ResponseAsync.DefaultTimeout();

// Assert
Assert.AreEqual("http://contoso", client.CallInvoker.Channel.Address.OriginalString);
Assert.AreEqual(new Uri("http://contoso/greet.Greeter/SayHello"), requestUri);
Assert.AreEqual("bearer abc", requestHeaders!.GetValues("authorization").Single());

Assert.IsTrue(testSink.Writes.Any(w => w.EventId.Name == "HttpClientActionsPartiallySupported"));
}

[Test]
public async Task CreateClient_ConfigureHttpClient_OverridenByGrpcConfiguration()
{
// Arrange
Uri? requestUri = null;
HttpRequestHeaders? requestHeaders = null;

var services = new ServiceCollection();
services
.AddGrpcClient<TestGreeterClient>(o => o.Address = new Uri("http://eshop"))
.ConfigureHttpClient(options =>
{
options.BaseAddress = new Uri("http://contoso");
options.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", "abc");
options.DefaultRequestHeaders.TryAddWithoutValidation("HTTPCLIENT-KEY", "httpclient-value");
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
return new NullHttpHandler();
return TestHttpMessageHandler.Create(async r =>
{
requestUri = r.RequestUri;
requestHeaders = r.Headers;

var streamContent = await ClientTestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout();
return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent);
});
});

var serviceProvider = services.BuildServiceProvider(validateScopes: true);

var clientFactory = CreateGrpcClientFactory(serviceProvider);

// Act
var ex = Assert.Throws<InvalidOperationException>(() => clientFactory.CreateClient<TestGreeterClient>(nameof(TestGreeterClient)))!;
var client = clientFactory.CreateClient<TestGreeterClient>(nameof(TestGreeterClient));
var response = await client.SayHelloAsync(new HelloRequest(), headers: new Metadata { new Metadata.Entry("authorization", "bearer 123"), new Metadata.Entry("call-key", "call-value") }).ResponseAsync.DefaultTimeout();

// Assert
Assert.AreEqual("The ConfigureHttpClient method is not supported when creating gRPC clients. Unable to create client with name 'TestGreeterClient'.", ex.Message);
Assert.AreEqual("http://eshop", client.CallInvoker.Channel.Address.OriginalString);
Assert.AreEqual(new Uri("http://eshop/greet.Greeter/SayHello"), requestUri);
Assert.AreEqual("bearer 123", requestHeaders!.GetValues("authorization").Single());
Assert.AreEqual("httpclient-value", requestHeaders!.GetValues("httpclient-key").Single());
Assert.AreEqual("call-value", requestHeaders!.GetValues("call-key").Single());
}

#if NET462
Expand Down Expand Up @@ -292,6 +360,10 @@ internal class TestGreeterClient : Greeter.GreeterClient
{
public TestGreeterClient(CallInvoker callInvoker) : base(callInvoker)
{
if (callInvoker is CallOptionsConfigurationInvoker callOptionsInvoker)
{
callInvoker = callOptionsInvoker.InnerInvoker;
}
CallInvoker = (HttpClientCallInvoker)callInvoker;
}

Expand Down