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(); } }