grpc-dotnet icon indicating copy to clipboard operation
grpc-dotnet copied to clipboard

Support AddAsKeyed on AddGrpcClient

Open jakoss opened this issue 9 months ago • 3 comments

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) => ... );

jakoss avatar Feb 18 '25 13:02 jakoss

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

jakoss avatar Feb 18 '25 13:02 jakoss

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

jakoss avatar Apr 01 '25 11:04 jakoss

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)

jakoss avatar Apr 01 '25 12:04 jakoss