using System.Buffers.Binary;
using Grpc.Core;
namespace MFiles.Grpc.Samples;
///
/// Wraps the channel's call invoker: adds the M-Files call metadata to every call, turns failures
/// into and records the metadata structure version the server reports.
///
internal sealed class MFilesCallInvoker(CallInvoker inner, MFilesConnection connection) : CallInvoker
{
// [START call-metadata]
///
/// The metadata M-Files expects on every call. gRPC base64-encodes "-bin" values itself.
///
private CallOptions WithMFilesMetadata(CallOptions options)
{
var headers = new Metadata();
foreach (var entry in options.Headers ?? [])
headers.Add(entry);
// The caller is not one of the server's own local processes.
headers.Add("mfiles-is-remote-call", "true");
// A new 16-byte ID per call; it identifies the call in the server's logs.
headers.Add("mfiles-activity-id-bin", Guid.NewGuid().ToByteArray());
// After LogIn: the session ID, unchanged.
if (connection.SessionId is { } sessionId)
headers.Add("mfiles-session-id-bin", sessionId.ToByteArray());
return options.WithHeaders(headers);
}
// [END call-metadata]
public override TResponse BlockingUnaryCall(
Method method, string? host, CallOptions options, TRequest request)
{
using var call = AsyncUnaryCall(method, host, options, request);
return call.ResponseAsync.GetAwaiter().GetResult();
}
public override AsyncUnaryCall AsyncUnaryCall(
Method method, string? host, CallOptions options, TRequest request)
{
var call = inner.AsyncUnaryCall(method, host, WithMFilesMetadata(options), request);
return new AsyncUnaryCall(
CompleteAsync(call.ResponseAsync, call.GetTrailers),
call.ResponseHeadersAsync, call.GetStatus, call.GetTrailers, call.Dispose);
}
public override AsyncClientStreamingCall AsyncClientStreamingCall(
Method method, string? host, CallOptions options)
{
var call = inner.AsyncClientStreamingCall(method, host, WithMFilesMetadata(options));
return new AsyncClientStreamingCall(
new ErrorDecodingWriter(call.RequestStream),
CompleteAsync(call.ResponseAsync, call.GetTrailers),
call.ResponseHeadersAsync, call.GetStatus, call.GetTrailers, call.Dispose);
}
public override AsyncServerStreamingCall AsyncServerStreamingCall(
Method method, string? host, CallOptions options, TRequest request)
{
var call = inner.AsyncServerStreamingCall(method, host, WithMFilesMetadata(options), request);
return new AsyncServerStreamingCall(
new ErrorDecodingReader(call.ResponseStream),
call.ResponseHeadersAsync, call.GetStatus, call.GetTrailers, call.Dispose);
}
public override AsyncDuplexStreamingCall AsyncDuplexStreamingCall(
Method method, string? host, CallOptions options)
{
var call = inner.AsyncDuplexStreamingCall(method, host, WithMFilesMetadata(options));
return new AsyncDuplexStreamingCall(
new ErrorDecodingWriter(call.RequestStream),
new ErrorDecodingReader(call.ResponseStream),
call.ResponseHeadersAsync, call.GetStatus, call.GetTrailers, call.Dispose);
}
private async Task CompleteAsync(
Task response, Func trailers)
{
try
{
var result = await response.ConfigureAwait(false);
RecordMetadataStructureVersion(trailers());
return result;
}
catch (RpcException e)
{
throw MFilesException.FromRpcException(e);
}
}
// [START metadata-version]
///
/// Many successful calls end with the vault's metadata structure version: 4 bytes, a
/// little-endian int32. A new value means that the metadata structure has changed.
///
private void RecordMetadataStructureVersion(Metadata trailers)
{
if (trailers.GetValueBytes("mfiles-metadata-version-bin") is { Length: 4 } bytes)
connection.MetadataStructureVersion = BinaryPrimitives.ReadInt32LittleEndian(bytes);
}
// [END metadata-version]
/// Turns a failure while reading a response stream into an MFilesException.
private sealed class ErrorDecodingReader(IAsyncStreamReader inner) : IAsyncStreamReader
{
public T Current => inner.Current;
public async Task MoveNext(CancellationToken cancellationToken)
{
try
{
return await inner.MoveNext(cancellationToken).ConfigureAwait(false);
}
catch (RpcException e)
{
throw MFilesException.FromRpcException(e);
}
}
}
///
/// Turns a failure while writing a request stream into . A write
/// fails when the server has already ended the call with an error.
///
private sealed class ErrorDecodingWriter(IClientStreamWriter inner) : IClientStreamWriter
{
public WriteOptions? WriteOptions
{
get => inner.WriteOptions;
set => inner.WriteOptions = value;
}
public async Task WriteAsync(T message)
{
try
{
await inner.WriteAsync(message).ConfigureAwait(false);
}
catch (RpcException e)
{
throw MFilesException.FromRpcException(e);
}
}
public Task CompleteAsync() => inner.CompleteAsync();
}
}