Build a Modern, Extensible, Type-Safe gRPC Client Factory in C#
Build a Modern, Extensible, Type-Safe gRPC Client Factory in C#
Overview
In modern distributed systems, managing gRPC clients efficiently and safely is critical. This article presents a clean and powerful design for a gRPC client factory in C# that is:
This solution is ideal for leaders and architects who want reusable infrastructure without unnecessary complexity.
The Goal
We want a factory that:
Interface Contracts
We define two interfaces:
public interface IGRPCClient {}
public interface ISelfGRPCClient
{
static abstract IGRPCClient Create(string fullAddress);
}
Example Client Implementation
💡 Note: Your gRPC client implementations can include constructors that accept parameters (e.g., DI services or shared components). These can be used with the delegate overload of the factory to construct clients with custom dependencies.
public class GrpcEntityDiscoveryClient : EntityDiscoveryProtoService.EntityDiscoveryProtoServiceClient, ISelfGRPCClient, IGRPCClient
{
private GrpcEntityDiscoveryClient(string fullAddress): base(GrpcChannel.ForAddress(grpcAddress))
{
}
public static IGRPCClient Create(string fullAddress)
{
return new GrpcEntityDiscoveryClient(fullAddress);
}
}
You can optionally retain a reference to the channel if you intend to implement channel management logic.
public class GrpcEntityDiscoveryClient : EntityDiscoveryProtoService.EntityDiscoveryProtoServiceClient, ISelfGRPCClient, IGRPCClient
{
private readonly GrpcChannel _channel;
private static GrpcChannel CreateChannel(string address, out GrpcChannel channel)
{
channel = GrpcChannel.ForAddress(address);
return channel;
}
private GrpcEntityDiscoveryClient(string grpcAddress) : base(CreateChannel(grpcAddress, out var ch))
{
_channel = ch;
}
public static IGRPCClient Create(string grpcAddress)
{
return new GrpcEntityDiscoveryClient(grpcAddress);
}
}
Example Client Implementation with Method Overload
Recommended by LinkedIn
public class GrpcMonitorClient : MonitorService.MonitorServiceClient, IGRPCClient, ISelfGRPCClient
{
public GrpcMonitorClient() // This is going to be used by the factory to instantiale an instance which can call GetClient
{
}
private GrpcMonitorClient(string grpcAddress) : base(GrpcChannel.ForAddress(grpcAddress))
{
}
public async Task<bool> MonitorAsync(string fqdn, CancellationToken token = default) // Optional overloaded method if that adds a benefit like removing code redunduncy. This approach is also composition over inheritance
{
try
{
var request = new MonitorRequest { FullyQualifiedName = fqdn };
var response = await base.MonitorAsync(request, cancellationToken: token);
if (!response.Success)
{
throw new InvalidOperationException($"Monitor call failed: {response.Message}");
}
return true;
}
catch (RpcException rpcEx)
{
// Optional: handle gRPC-specific errors
throw new InvalidOperationException($"gRPC Monitor call failed: {rpcEx.Status.Detail}", rpcEx);
}
}
public static IGRPCClient Create(string grpcAddress)
{
return new GrpcMonitorClient(grpcAddress);
}
}
Once implemented like this, the client is ready to be used by the factory — no registration required.
The Factory Implementation
public interface ICustomGrpcClientFactory
{
TClient GetClient<TClient>(string host)
where TClient : ISelfGRPCClient, IGRPCClient;
TClient GetClient<TClient>(string host, Func<string, TClient> factory)
where TClient : IGRPCClient;
}
Concrete Factory
public class CustomGrpcClientFactory : ICustomGrpcClientFactory
{
private readonly ConcurrentDictionary<(Type, string), IGRPCClient> _instances = new();
private readonly IClusterConfiguration _clusterConfiguration;
public CustomGrpcClientFactory(IClusterConfiguration clusterConfiguration)
{
_clusterConfiguration = clusterConfiguration;
}
public TClient GetClient<TClient>(string host)
where TClient : ISelfGRPCClient, IGRPCClient
{
var key = (typeof(TClient), host);
return (TClient)_instances.GetOrAdd(key, _ =>
{
var fullAddress = $"http://{host}:{_clusterConfiguration.GRPCPort}";
return TClient.Create(fullAddress);
});
}
public TClient GetClient<TClient>(string host, Func<string, TClient> factory)
where TClient : IGRPCClient
{
var key = (typeof(TClient), host);
return (TClient)_instances.GetOrAdd(key, _ =>
{
var fullAddress = $"http://{host}:{_clusterConfiguration.GRPCPort}";
return factory(fullAddress);
});
}
}
The factory automatically creates gRPC clients by calling a static Create method defined in each client that implements the ISelfGRPCClient interface. This design allows clients to self-register through interface implementation—no manual registration or configuration is needed—enabling the factory to instantiate, cache, and return the correct client based solely on its type and host.
🔥 Why You Should Care
This design offers:
Whether you're leading a team or building infrastructure for others, this factory is a production-ready solution that minimizes friction and maximizes clarity.
Summary
You now have a clean, modern, and extensible gRPC factory that:
This approach fits perfectly in systems where reliability, reusability, and developer experience are top priorities.
Lastly, feel free to expand the factory logic for better client management — this is just a starting point.
Research Engineer | Operational Research | Graph Theory
1wInteresting!
Microsoft | Sr. Embedded Escalation Engineer | Software engineering | Performance Testing.
1wFeel free to expand the factory logic for better client management, this is just a starting point.