I have a .NET gRPC client on .net 4.6.2 using Grpc.Core
library.
I've registered gRPC channel as singleton to reuse it following [this recommendation].(https://learn.microsoft.com/en-us/aspnet/core/grpc/performance?view=aspnetcore-7.0#reuse-grpc-channels)
There's how I create an instance of the channel:
public static InvokerAndChannel<T> CreateGrpcCallInvokerAndChannel<T>(
string endpoint, ChannelCredentials credentials, IServiceProvider serviceProvider)
where T : class
{
var licenseCode = serviceProvider.GetRequiredService<ILicenseCodeResolver>().GetLicenseCode();
var channel = new Channel(endpoint, credentials, new ChannelOption(ChannelOptions.MaxSendMessageLength, "1mb").Yield());
var callInvoker = channel
.Intercept(
new ClientTracingInterceptor(
new ClientTracingInterceptorOptions { RecordMessageEvents = false }))
.Intercept(new StaticMetadataInterceptor(licenseCode));
return new InvokerAndChannel<T>(callInvoker, channel);
}
As you can see the pipeline includes StaticMetadataInterceptor
which adds license header to the call. Effectively it's also a singleton.
I have multiple parallel calls to various methods of the same gRPC service. And I see that StaticMetadataInterceptor
receives same ClientInterceptorContext
. At least context.Options.Headers
keeps growing, so after several calls it contains the same header several times. Why?
Here's the code for the interceptor:
internal sealed class StaticMetadataInterceptor : Interceptor
{
private readonly string _licenseCode;
public StaticMetadataInterceptor(string licenseCode)
{
_licenseCode = licenseCode;
}
private void ExtendMetadata<TRequest, TResponse>(ref ClientInterceptorContext<TRequest, TResponse> context)
where TRequest : class
where TResponse : class
{
var metadata = context.Options.Headers;
if (metadata == null)
{
metadata = new Metadata();
context = new ClientInterceptorContext<TRequest, TResponse>(context.Method, context. Host, context.Options.WithHeaders(metadata));
}
metadata.Add(GrpcStandardMetadataKeys.License, _licenseCode);
}
public override TResponse BlockingUnaryCall<TRequest, TResponse>(TRequest request, ClientInterceptorContext<TRequest, TResponse> context, BlockingUnaryCallContinuation<TRequest, TResponse> continuation)
{
ExtendMetadata(ref context);
return continuation(request, context);
}
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(TRequest request, ClientInterceptorContext<TRequest, TResponse> context, AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
{
ExtendMetadata(ref context);
return continuation(request, context);
}
public override AsyncClientStreamingCall<TRequest, TResponse> AsyncClientStreamingCall<TRequest, TResponse>(ClientInterceptorContext<TRequest, TResponse> context, AsyncClientStreamingCallContinuation<TRequest, TResponse> continuation)
{
ExtendMetadata(ref context);
return continuation(context);
}
public override AsyncServerStreamingCall<TResponse> AsyncServerStreamingCall<TRequest, TResponse>(TRequest request, ClientInterceptorContext<TRequest, TResponse> context, AsyncServerStreamingCallContinuation<TRequest, TResponse> continuation)
{
ExtendMetadata(ref context);
return continuation(request, context);
}
public override AsyncDuplexStreamingCall<TRequest, TResponse> AsyncDuplexStreamingCall<TRequest, TResponse>(ClientInterceptorContext<TRequest, TResponse> context, AsyncDuplexStreamingCallContinuation<TRequest, TResponse> continuation)
{
ExtendMetadata(ref context);
return continuation(context);
}
}