grpc-dotnet
grpc-dotnet copied to clipboard
Support AddAsKeyed on AddGrpcClient
Is your feature request related to a problem? Please describe.
I want to used Keyed Services feature with named grpc clients. Right now i have something like this:
serviceCollection.AddGrpcClient<MyGrpcServiceClient>("custom_name", o =>
{
o.Address = new Uri(someUrl);
});
But to access this named client i have to inject GrpcClientFactory and all Create on it. In .NET 9 IHttpClientBuilder introduced AddAsKeyed that does exactly that for http clients, but it seems to be ignored for grpc clients.
Describe the solution you'd like
What i would love to have is that i can inject my named client directly like that:
app.MapGet("/", ([FromKeyedServices("custom_name")] MyGrpcServiceClient client) => ... );
Also, currently when i register the same client twice with different names both will be registered as Transient at the same service interface: https://github.com/grpc/grpc-dotnet/blob/c9d26719e8b2a8f03424cacbb168540e35a94b0b/src/Grpc.Net.ClientFactory/GrpcClientServiceExtensions.cs#L310
I think this might not be a good behavior
Anybody can help here? Right now i have a need to register the same client twice (but with different set of interceptors) and whenever i do the second registration - the second client is registered as transient on the same type. Even if i use named client - the registration is screwed
If anybody has the same issue and is interested in workaround i figured out this atrocity:
public static IHttpClientBuilder AddCustomGrpcClient<TClient>(this IServiceCollection services,
Action<GrpcClientFactoryOptions> configureClient
)
where TClient : class
{
if (services.Any(s => s.ServiceType == typeof(TClient) && !s.IsKeyedService))
{
throw new InvalidOperationException($"A gRPC client of type {typeof(TClient)} has already been registered.");
}
return services.AddGrpcClient<TClient>(configureClient)
.AddInterceptor<HeadersPassthroughInterceptor>();
}
public static IHttpClientBuilder AddCustomGrpcClient<TClient>(this IServiceCollection services,
string name,
Action<GrpcClientFactoryOptions> configureClient
)
where TClient : class
{
if (services.Any(s => s.ServiceType == typeof(TClient) && s.IsKeyedService && (s.ServiceKey as string) == name))
{
throw new InvalidOperationException($"A gRPC client of type {typeof(TClient)} with the name {name} has already been registered.");
}
var existingNonKeyedClientRegistration = services.FirstOrDefault(s => s.ServiceType == typeof(TClient) && !s.IsKeyedService);
var builder = services.AddGrpcClient<TClient>(name, configureClient)
.AddInterceptor<HeadersPassthroughInterceptor>();
var allClients = services.Where(s => s.ServiceType == typeof(TClient) && !s.IsKeyedService).ToList();
var newNonKeyedClientRegistration = services.First(s => s != existingNonKeyedClientRegistration && s.ServiceType == typeof(TClient) && !s.IsKeyedService);
if (existingNonKeyedClientRegistration is not null)
{
Debug.Assert(allClients.Count == 2);
var oldKeyedClientRegistration = allClients.First(s => s != newNonKeyedClientRegistration);
Debug.Assert(oldKeyedClientRegistration == existingNonKeyedClientRegistration);
}
Debug.Assert(existingNonKeyedClientRegistration != newNonKeyedClientRegistration);
// remove the non-keyed client registration, since this overrides a non-named client registration and we don't want that
services.Remove(newNonKeyedClientRegistration);
// now register the client properly, as a keyed transient
builder.Services.AddKeyedTransient(name, (provider, key) =>
{
var factory = provider.GetRequiredService<GrpcClientFactory>();
return factory.CreateClient<TClient>((string)key!);
});
return builder;
}
This looks awful, but it does what we need - register the named grpc clients as named service (and does not add multiple non-named registrations for named clients)