8023eccfa6
Client.Dotnet-001: MapRpcException typed only Unauthenticated and PermissionDenied; every other gRPC status collapsed to an untyped exception with the status code discarded. Added a nullable StatusCode to MxGatewayException, extracted the duplicated mappers into a shared RpcExceptionMapper that records the code for every status, and documented it. Client.Dotnet-002: the production retry branch (MxGatewayException wrapping RpcException) was never exercised. FakeGatewayTransport gained a MapTransportExceptions mode that runs thrown RpcExceptions through RpcExceptionMapper exactly as the production transport does. Client.Dotnet-003: MxGatewaySession.DisposeAsync disposed _closeLock while a concurrent CloseAsync could be parked in WaitAsync. DisposeAsync now drains in-flight CloseAsync callers before disposing the semaphore; the client's _disposed flag is accessed via Interlocked. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
178 lines
5.4 KiB
C#
178 lines
5.4 KiB
C#
using Grpc.Core;
|
|
using MxGateway.Contracts.Proto;
|
|
|
|
namespace MxGateway.Client;
|
|
|
|
/// <summary>
|
|
/// gRPC implementation of IMxGatewayClientTransport.
|
|
/// </summary>
|
|
internal sealed class GrpcMxGatewayClientTransport(
|
|
MxGatewayClientOptions options,
|
|
MxAccessGateway.MxAccessGatewayClient rawClient) : IMxGatewayClientTransport
|
|
{
|
|
/// <summary>
|
|
/// Gets the gateway client options.
|
|
/// </summary>
|
|
public MxGatewayClientOptions Options { get; } = options;
|
|
|
|
/// <summary>
|
|
/// Gets the underlying gRPC client.
|
|
/// </summary>
|
|
public MxAccessGateway.MxAccessGatewayClient RawClient { get; } = rawClient;
|
|
|
|
/// <inheritdoc />
|
|
MxAccessGateway.MxAccessGatewayClient? IMxGatewayClientTransport.RawClient => RawClient;
|
|
|
|
/// <inheritdoc />
|
|
public async Task<OpenSessionReply> OpenSessionAsync(
|
|
OpenSessionRequest request,
|
|
CallOptions callOptions)
|
|
{
|
|
try
|
|
{
|
|
return await RawClient.OpenSessionAsync(request, callOptions)
|
|
.ResponseAsync
|
|
.ConfigureAwait(false);
|
|
}
|
|
catch (RpcException exception)
|
|
{
|
|
throw RpcExceptionMapper.Map(exception, callOptions.CancellationToken);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<CloseSessionReply> CloseSessionAsync(
|
|
CloseSessionRequest request,
|
|
CallOptions callOptions)
|
|
{
|
|
try
|
|
{
|
|
return await RawClient.CloseSessionAsync(request, callOptions)
|
|
.ResponseAsync
|
|
.ConfigureAwait(false);
|
|
}
|
|
catch (RpcException exception)
|
|
{
|
|
throw RpcExceptionMapper.Map(exception, callOptions.CancellationToken);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<MxCommandReply> InvokeAsync(
|
|
MxCommandRequest request,
|
|
CallOptions callOptions)
|
|
{
|
|
try
|
|
{
|
|
return await RawClient.InvokeAsync(request, callOptions)
|
|
.ResponseAsync
|
|
.ConfigureAwait(false);
|
|
}
|
|
catch (RpcException exception)
|
|
{
|
|
throw RpcExceptionMapper.Map(exception, callOptions.CancellationToken);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
|
StreamEventsRequest request,
|
|
CallOptions callOptions,
|
|
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
|
|
{
|
|
CancellationToken effectiveCancellationToken = cancellationToken.CanBeCanceled
|
|
? cancellationToken
|
|
: callOptions.CancellationToken;
|
|
|
|
using AsyncServerStreamingCall<MxEvent> call = RawClient.StreamEvents(request, callOptions);
|
|
|
|
IAsyncStreamReader<MxEvent> responseStream = call.ResponseStream;
|
|
while (true)
|
|
{
|
|
MxEvent? gatewayEvent;
|
|
try
|
|
{
|
|
if (!await responseStream.MoveNext(effectiveCancellationToken).ConfigureAwait(false))
|
|
{
|
|
break;
|
|
}
|
|
|
|
gatewayEvent = responseStream.Current;
|
|
}
|
|
catch (RpcException exception)
|
|
{
|
|
throw RpcExceptionMapper.Map(exception, effectiveCancellationToken);
|
|
}
|
|
|
|
yield return gatewayEvent;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
IAsyncEnumerable<MxEvent> IMxGatewayClientTransport.StreamEventsAsync(
|
|
StreamEventsRequest request,
|
|
CallOptions callOptions)
|
|
{
|
|
return StreamEventsAsync(request, callOptions);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<AcknowledgeAlarmReply> AcknowledgeAlarmAsync(
|
|
AcknowledgeAlarmRequest request,
|
|
CallOptions callOptions)
|
|
{
|
|
try
|
|
{
|
|
return await RawClient.AcknowledgeAlarmAsync(request, callOptions)
|
|
.ResponseAsync
|
|
.ConfigureAwait(false);
|
|
}
|
|
catch (RpcException exception)
|
|
{
|
|
throw RpcExceptionMapper.Map(exception, callOptions.CancellationToken);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async IAsyncEnumerable<ActiveAlarmSnapshot> QueryActiveAlarmsAsync(
|
|
QueryActiveAlarmsRequest request,
|
|
CallOptions callOptions,
|
|
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
|
|
{
|
|
CancellationToken effectiveCancellationToken = cancellationToken.CanBeCanceled
|
|
? cancellationToken
|
|
: callOptions.CancellationToken;
|
|
|
|
using AsyncServerStreamingCall<ActiveAlarmSnapshot> call = RawClient.QueryActiveAlarms(request, callOptions);
|
|
|
|
IAsyncStreamReader<ActiveAlarmSnapshot> responseStream = call.ResponseStream;
|
|
while (true)
|
|
{
|
|
ActiveAlarmSnapshot? snapshot;
|
|
try
|
|
{
|
|
if (!await responseStream.MoveNext(effectiveCancellationToken).ConfigureAwait(false))
|
|
{
|
|
break;
|
|
}
|
|
|
|
snapshot = responseStream.Current;
|
|
}
|
|
catch (RpcException exception)
|
|
{
|
|
throw RpcExceptionMapper.Map(exception, effectiveCancellationToken);
|
|
}
|
|
|
|
yield return snapshot;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
IAsyncEnumerable<ActiveAlarmSnapshot> IMxGatewayClientTransport.QueryActiveAlarmsAsync(
|
|
QueryActiveAlarmsRequest request,
|
|
CallOptions callOptions)
|
|
{
|
|
return QueryActiveAlarmsAsync(request, callOptions);
|
|
}
|
|
}
|