b3ae200b11
Adds the value-bulk SDK surface and CLI subcommands that lived on the
divergent branch (commit f220908) but were never merged into main.
HEAD's MxGatewaySession only had the subscribe-style bulks (AddItem /
Advise / Remove / UnAdvise / Subscribe / Unsubscribe). The proto
contract already defined ReadBulkCommand / WriteBulkCommand /
Write2BulkCommand / WriteSecuredBulkCommand / WriteSecured2BulkCommand
/ BulkReadReply / BulkWriteReply, so this is purely a client-side
addition.
SDK (MxGatewaySession.cs):
- WriteBulkAsync(serverHandle, IReadOnlyList<WriteBulkEntry>, ct)
- Write2BulkAsync(serverHandle, IReadOnlyList<Write2BulkEntry>, ct)
- WriteSecuredBulkAsync(serverHandle, IReadOnlyList<WriteSecuredBulkEntry>, ct)
- WriteSecured2BulkAsync(serverHandle, IReadOnlyList<WriteSecured2BulkEntry>, ct)
- ReadBulkAsync(serverHandle, IReadOnlyList<string> tagAddresses, TimeSpan timeout, ct)
Per-entry secured user ids live on each WriteSecured(2)BulkEntry — they
are NOT lifted to ctor args because the proto field shape allows distinct
ids per row.
CLI (MxGatewayClientCli.cs):
- read-bulk / write-bulk / write2-bulk / write-secured-bulk / write-secured2-bulk
routed through the existing dispatch table, with --type, --values,
--item-handles, --timeout-ms, --current-user-id, --verifier-user-id,
--timestamp flags matching the cross-language CLI surface.
- bench-read-bulk benchmark harness: warmup + steady-state ReadBulk loop
with p50/p95/p99/max/mean latency, emitting the shared JSON schema so
scripts/bench-read-bulk.ps1 collates the .NET line alongside the four
other clients.
The new subcommands flow through the existing batch dispatcher without
further changes.
Verification: dotnet build clean (0 warnings / 0 errors);
dotnet test 59/59 passing. Manual smoke against the live gateway
on localhost:5120: read-bulk returned 2 BulkReadResult entries with
wasSuccessful=true, wasCached=true; write-bulk on int32 returned
wasSuccessful=true; close-session returned SESSION_STATE_CLOSED.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
845 lines
33 KiB
C#
845 lines
33 KiB
C#
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.Client;
|
|
|
|
/// <summary>
|
|
/// Represents one gateway-backed MXAccess session.
|
|
/// </summary>
|
|
public sealed class MxGatewaySession : IAsyncDisposable
|
|
{
|
|
private readonly MxGatewayClient _client;
|
|
private readonly SemaphoreSlim _closeLock = new(1, 1);
|
|
private CloseSessionReply? _closeReply;
|
|
|
|
/// <summary>
|
|
/// Initializes a new session backed by the given MXAccess gateway client.
|
|
/// </summary>
|
|
/// <param name="client">The gateway client used for commands and events.</param>
|
|
/// <param name="openSessionReply">The server's session creation response.</param>
|
|
internal MxGatewaySession(
|
|
MxGatewayClient client,
|
|
OpenSessionReply openSessionReply)
|
|
{
|
|
_client = client ?? throw new ArgumentNullException(nameof(client));
|
|
OpenSessionReply = openSessionReply ?? throw new ArgumentNullException(nameof(openSessionReply));
|
|
}
|
|
|
|
/// <summary>
|
|
/// The session ID assigned by the gateway.
|
|
/// </summary>
|
|
public string SessionId => OpenSessionReply.SessionId;
|
|
|
|
/// <summary>
|
|
/// The server's session creation response containing metadata.
|
|
/// </summary>
|
|
public OpenSessionReply OpenSessionReply { get; }
|
|
|
|
/// <summary>
|
|
/// Closes the session on the gateway. Idempotent.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The server's close-session reply.</returns>
|
|
public async Task<CloseSessionReply> CloseAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
if (_closeReply is not null)
|
|
{
|
|
return _closeReply;
|
|
}
|
|
|
|
await _closeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
try
|
|
{
|
|
if (_closeReply is not null)
|
|
{
|
|
return _closeReply;
|
|
}
|
|
|
|
_closeReply = await _client.CloseSessionRawAsync(
|
|
new CloseSessionRequest { SessionId = SessionId },
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
return _closeReply;
|
|
}
|
|
finally
|
|
{
|
|
_closeLock.Release();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers a client with the MXAccess session, returning a ServerHandle.
|
|
/// </summary>
|
|
/// <param name="clientName">Name to register.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The server handle assigned to the registered client.</returns>
|
|
public async Task<int> RegisterAsync(
|
|
string clientName,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
MxCommandReply reply = await RegisterRawAsync(clientName, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.Register?.ServerHandle ?? reply.ReturnValue.Int32Value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers a client with the MXAccess session without error checking.
|
|
/// </summary>
|
|
/// <param name="clientName">Name to register.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The raw server reply.</returns>
|
|
public Task<MxCommandReply> RegisterRawAsync(
|
|
string clientName,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(clientName);
|
|
|
|
return InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.Register,
|
|
Register = new RegisterCommand { ClientName = clientName },
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds an item to the MXAccess session, returning an ItemHandle.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemDefinition">The item tag address.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The item handle assigned to the new item.</returns>
|
|
public async Task<int> AddItemAsync(
|
|
int serverHandle,
|
|
string itemDefinition,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
MxCommandReply reply = await AddItemRawAsync(
|
|
serverHandle,
|
|
itemDefinition,
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.AddItem?.ItemHandle ?? reply.ReturnValue.Int32Value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds an item to the MXAccess session without error checking.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemDefinition">The item tag address.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The raw server reply.</returns>
|
|
public Task<MxCommandReply> AddItemRawAsync(
|
|
int serverHandle,
|
|
string itemDefinition,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(itemDefinition);
|
|
|
|
return InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.AddItem,
|
|
AddItem = new AddItemCommand
|
|
{
|
|
ServerHandle = serverHandle,
|
|
ItemDefinition = itemDefinition,
|
|
},
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds an item with context to the MXAccess session, returning an ItemHandle.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemDefinition">The item tag address.</param>
|
|
/// <param name="itemContext">Additional context for the item.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The item handle assigned to the new item.</returns>
|
|
public async Task<int> AddItem2Async(
|
|
int serverHandle,
|
|
string itemDefinition,
|
|
string itemContext,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
MxCommandReply reply = await AddItem2RawAsync(
|
|
serverHandle,
|
|
itemDefinition,
|
|
itemContext,
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.AddItem2?.ItemHandle ?? reply.ReturnValue.Int32Value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds an item with context to the MXAccess session without error checking.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemDefinition">The item tag address.</param>
|
|
/// <param name="itemContext">Additional context for the item.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The raw server reply.</returns>
|
|
public Task<MxCommandReply> AddItem2RawAsync(
|
|
int serverHandle,
|
|
string itemDefinition,
|
|
string itemContext,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(itemDefinition);
|
|
|
|
return InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.AddItem2,
|
|
AddItem2 = new AddItem2Command
|
|
{
|
|
ServerHandle = serverHandle,
|
|
ItemDefinition = itemDefinition,
|
|
ItemContext = itemContext ?? string.Empty,
|
|
},
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Subscribes to events for an item (advises in MXAccess terminology).
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
public async Task AdviseAsync(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
MxCommandReply reply = await AdviseRawAsync(serverHandle, itemHandle, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Subscribes to events for an item without error checking.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The raw server reply.</returns>
|
|
public Task<MxCommandReply> AdviseRawAsync(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.Advise,
|
|
Advise = new AdviseCommand
|
|
{
|
|
ServerHandle = serverHandle,
|
|
ItemHandle = itemHandle,
|
|
},
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unsubscribes from events for an item (unadvises in MXAccess terminology).
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
public async Task UnAdviseAsync(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
MxCommandReply reply = await UnAdviseRawAsync(serverHandle, itemHandle, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unsubscribes from events for an item without error checking.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The raw server reply.</returns>
|
|
public Task<MxCommandReply> UnAdviseRawAsync(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.UnAdvise,
|
|
UnAdvise = new UnAdviseCommand
|
|
{
|
|
ServerHandle = serverHandle,
|
|
ItemHandle = itemHandle,
|
|
},
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes an item from the MXAccess session.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
public async Task RemoveItemAsync(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
MxCommandReply reply = await RemoveItemRawAsync(serverHandle, itemHandle, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes an item from the MXAccess session without error checking.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The raw server reply.</returns>
|
|
public Task<MxCommandReply> RemoveItemRawAsync(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.RemoveItem,
|
|
RemoveItem = new RemoveItemCommand
|
|
{
|
|
ServerHandle = serverHandle,
|
|
ItemHandle = itemHandle,
|
|
},
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds multiple items to the MXAccess session in a single command.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="tagAddresses">The item tag addresses to add.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>Per-item subscription results.</returns>
|
|
public async Task<IReadOnlyList<SubscribeResult>> AddItemBulkAsync(
|
|
int serverHandle,
|
|
IReadOnlyList<string> tagAddresses,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(tagAddresses);
|
|
|
|
AddItemBulkCommand command = new() { ServerHandle = serverHandle };
|
|
command.TagAddresses.Add(tagAddresses);
|
|
|
|
MxCommandReply reply = await InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.AddItemBulk,
|
|
AddItemBulk = command,
|
|
},
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.AddItemBulk?.Results.ToArray() ?? [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Advises multiple items in a single command.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandles">The ItemHandles to advise.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>Per-item subscription results.</returns>
|
|
public async Task<IReadOnlyList<SubscribeResult>> AdviseItemBulkAsync(
|
|
int serverHandle,
|
|
IReadOnlyList<int> itemHandles,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(itemHandles);
|
|
|
|
AdviseItemBulkCommand command = new() { ServerHandle = serverHandle };
|
|
command.ItemHandles.Add(itemHandles);
|
|
|
|
MxCommandReply reply = await InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.AdviseItemBulk,
|
|
AdviseItemBulk = command,
|
|
},
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.AdviseItemBulk?.Results.ToArray() ?? [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes multiple items in a single command.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandles">The ItemHandles to remove.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>Per-item subscription results.</returns>
|
|
public async Task<IReadOnlyList<SubscribeResult>> RemoveItemBulkAsync(
|
|
int serverHandle,
|
|
IReadOnlyList<int> itemHandles,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(itemHandles);
|
|
|
|
RemoveItemBulkCommand command = new() { ServerHandle = serverHandle };
|
|
command.ItemHandles.Add(itemHandles);
|
|
|
|
MxCommandReply reply = await InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.RemoveItemBulk,
|
|
RemoveItemBulk = command,
|
|
},
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.RemoveItemBulk?.Results.ToArray() ?? [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unadvises multiple items in a single command.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandles">The ItemHandles to unadvise.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>Per-item subscription results.</returns>
|
|
public async Task<IReadOnlyList<SubscribeResult>> UnAdviseItemBulkAsync(
|
|
int serverHandle,
|
|
IReadOnlyList<int> itemHandles,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(itemHandles);
|
|
|
|
UnAdviseItemBulkCommand command = new() { ServerHandle = serverHandle };
|
|
command.ItemHandles.Add(itemHandles);
|
|
|
|
MxCommandReply reply = await InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.UnAdviseItemBulk,
|
|
UnAdviseItemBulk = command,
|
|
},
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.UnAdviseItemBulk?.Results.ToArray() ?? [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds and advises multiple items in a single command.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="tagAddresses">The item tag addresses to add and advise.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>Per-item subscription results.</returns>
|
|
public async Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
|
|
int serverHandle,
|
|
IReadOnlyList<string> tagAddresses,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(tagAddresses);
|
|
|
|
SubscribeBulkCommand command = new() { ServerHandle = serverHandle };
|
|
command.TagAddresses.Add(tagAddresses);
|
|
|
|
MxCommandReply reply = await InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.SubscribeBulk,
|
|
SubscribeBulk = command,
|
|
},
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.SubscribeBulk?.Results.ToArray() ?? [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unadvises and removes multiple items in a single command.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandles">The ItemHandles to unsubscribe.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>Per-item subscription results.</returns>
|
|
public async Task<IReadOnlyList<SubscribeResult>> UnsubscribeBulkAsync(
|
|
int serverHandle,
|
|
IReadOnlyList<int> itemHandles,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(itemHandles);
|
|
|
|
UnsubscribeBulkCommand command = new() { ServerHandle = serverHandle };
|
|
command.ItemHandles.Add(itemHandles);
|
|
|
|
MxCommandReply reply = await InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.UnsubscribeBulk,
|
|
UnsubscribeBulk = command,
|
|
},
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.UnsubscribeBulk?.Results.ToArray() ?? [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulk Write — sequential MXAccess Write per entry on the worker's STA.
|
|
/// Per-item failures appear as <see cref="BulkWriteResult"/> entries with
|
|
/// <c>WasSuccessful = false</c>; the call never throws on per-item errors.
|
|
/// Protocol-level failures still throw via EnsureProtocolSuccess.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="entries">Per-item write entries; each carries the item handle, value, and user id.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>One <see cref="BulkWriteResult"/> per requested entry, in request order.</returns>
|
|
public async Task<IReadOnlyList<BulkWriteResult>> WriteBulkAsync(
|
|
int serverHandle,
|
|
IReadOnlyList<WriteBulkEntry> entries,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(entries);
|
|
|
|
WriteBulkCommand command = new() { ServerHandle = serverHandle };
|
|
command.Entries.Add(entries);
|
|
|
|
MxCommandReply reply = await InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.WriteBulk,
|
|
WriteBulk = command,
|
|
},
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.WriteBulk?.Results.ToArray() ?? [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulk Write2 — sequential MXAccess Write2 (timestamped) per entry.
|
|
/// Per-item failures appear as <see cref="BulkWriteResult"/> entries with
|
|
/// <c>WasSuccessful = false</c>; the call never throws on per-item errors.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="entries">Per-item write entries; each carries the item handle, value, timestamp, and user id.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>One <see cref="BulkWriteResult"/> per requested entry, in request order.</returns>
|
|
public async Task<IReadOnlyList<BulkWriteResult>> Write2BulkAsync(
|
|
int serverHandle,
|
|
IReadOnlyList<Write2BulkEntry> entries,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(entries);
|
|
|
|
Write2BulkCommand command = new() { ServerHandle = serverHandle };
|
|
command.Entries.Add(entries);
|
|
|
|
MxCommandReply reply = await InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.Write2Bulk,
|
|
Write2Bulk = command,
|
|
},
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.Write2Bulk?.Results.ToArray() ?? [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulk WriteSecured — sequential MXAccess WriteSecured per entry.
|
|
/// Credential-sensitive values must never reach logs; the client mirrors
|
|
/// the single-item WriteSecured redaction contract.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="entries">Per-item write entries; each carries the item handle, value, current user id, and verifier user id.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>One <see cref="BulkWriteResult"/> per requested entry, in request order.</returns>
|
|
public async Task<IReadOnlyList<BulkWriteResult>> WriteSecuredBulkAsync(
|
|
int serverHandle,
|
|
IReadOnlyList<WriteSecuredBulkEntry> entries,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(entries);
|
|
|
|
WriteSecuredBulkCommand command = new() { ServerHandle = serverHandle };
|
|
command.Entries.Add(entries);
|
|
|
|
MxCommandReply reply = await InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.WriteSecuredBulk,
|
|
WriteSecuredBulk = command,
|
|
},
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.WriteSecuredBulk?.Results.ToArray() ?? [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulk WriteSecured2 — sequential MXAccess WriteSecured2 (timestamped) per entry.
|
|
/// Same redaction rules as <see cref="WriteSecuredBulkAsync"/>.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="entries">Per-item write entries; each carries the item handle, value, timestamp, current user id, and verifier user id.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>One <see cref="BulkWriteResult"/> per requested entry, in request order.</returns>
|
|
public async Task<IReadOnlyList<BulkWriteResult>> WriteSecured2BulkAsync(
|
|
int serverHandle,
|
|
IReadOnlyList<WriteSecured2BulkEntry> entries,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(entries);
|
|
|
|
WriteSecured2BulkCommand command = new() { ServerHandle = serverHandle };
|
|
command.Entries.Add(entries);
|
|
|
|
MxCommandReply reply = await InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.WriteSecured2Bulk,
|
|
WriteSecured2Bulk = command,
|
|
},
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.WriteSecured2Bulk?.Results.ToArray() ?? [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulk Read — snapshot the current value for each requested tag.
|
|
/// Returns the cached OnDataChange value when the tag is already advised
|
|
/// (<c>WasCached = true</c>), otherwise the worker takes the full AddItem +
|
|
/// Advise + wait + UnAdvise + RemoveItem snapshot lifecycle. Per-tag
|
|
/// failures (timeout, invalid tag) appear as <see cref="BulkReadResult"/>
|
|
/// entries with <c>WasSuccessful = false</c>; the call never throws on
|
|
/// per-tag errors.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="tagAddresses">Tag addresses to read (one per result).</param>
|
|
/// <param name="timeout">Per-call timeout for the snapshot lifecycle path; <see cref="TimeSpan.Zero"/> uses the gateway default.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>One <see cref="BulkReadResult"/> per requested tag, in request order.</returns>
|
|
public async Task<IReadOnlyList<BulkReadResult>> ReadBulkAsync(
|
|
int serverHandle,
|
|
IReadOnlyList<string> tagAddresses,
|
|
TimeSpan timeout,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(tagAddresses);
|
|
|
|
ReadBulkCommand command = new()
|
|
{
|
|
ServerHandle = serverHandle,
|
|
TimeoutMs = timeout <= TimeSpan.Zero ? 0u : (uint)Math.Min(timeout.TotalMilliseconds, uint.MaxValue),
|
|
};
|
|
command.TagAddresses.Add(tagAddresses);
|
|
|
|
MxCommandReply reply = await InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.ReadBulk,
|
|
ReadBulk = command,
|
|
},
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.ReadBulk?.Results.ToArray() ?? [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a value to an item on the MXAccess server.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
/// <param name="value">The value to write.</param>
|
|
/// <param name="userId">User ID context for the write.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
public async Task WriteAsync(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
MxValue value,
|
|
int userId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
MxCommandReply reply = await WriteRawAsync(serverHandle, itemHandle, value, userId, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a value to an item on the MXAccess server without error checking.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
/// <param name="value">The value to write.</param>
|
|
/// <param name="userId">User ID context for the write.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The raw server reply.</returns>
|
|
public Task<MxCommandReply> WriteRawAsync(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
MxValue value,
|
|
int userId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(value);
|
|
|
|
return InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.Write,
|
|
Write = new WriteCommand
|
|
{
|
|
ServerHandle = serverHandle,
|
|
ItemHandle = itemHandle,
|
|
Value = value,
|
|
UserId = userId,
|
|
},
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a value and timestamp to an item on the MXAccess server.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
/// <param name="value">The value to write.</param>
|
|
/// <param name="timestampValue">The timestamp to write with the value.</param>
|
|
/// <param name="userId">User ID context for the write.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
public async Task Write2Async(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
MxValue value,
|
|
MxValue timestampValue,
|
|
int userId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
MxCommandReply reply = await Write2RawAsync(
|
|
serverHandle,
|
|
itemHandle,
|
|
value,
|
|
timestampValue,
|
|
userId,
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a value and timestamp to an item on the MXAccess server without error checking.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
/// <param name="value">The value to write.</param>
|
|
/// <param name="timestampValue">The timestamp to write with the value.</param>
|
|
/// <param name="userId">User ID context for the write.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The raw server reply.</returns>
|
|
public Task<MxCommandReply> Write2RawAsync(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
MxValue value,
|
|
MxValue timestampValue,
|
|
int userId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(value);
|
|
ArgumentNullException.ThrowIfNull(timestampValue);
|
|
|
|
return InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.Write2,
|
|
Write2 = new Write2Command
|
|
{
|
|
ServerHandle = serverHandle,
|
|
ItemHandle = itemHandle,
|
|
Value = value,
|
|
TimestampValue = timestampValue,
|
|
UserId = userId,
|
|
},
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invokes an MXAccess command on this session.
|
|
/// </summary>
|
|
/// <param name="request">The command request.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The raw server reply.</returns>
|
|
public Task<MxCommandReply> InvokeAsync(
|
|
MxCommandRequest request,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
return _client.InvokeAsync(request, cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Streams events from the worker for this session, optionally starting after a given sequence number.
|
|
/// </summary>
|
|
/// <param name="afterWorkerSequence">The sequence number to stream from. Defaults to 0.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>An async enumerable of events.</returns>
|
|
public IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
|
ulong afterWorkerSequence = 0,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return _client.StreamEventsAsync(
|
|
new StreamEventsRequest
|
|
{
|
|
SessionId = SessionId,
|
|
AfterWorkerSequence = afterWorkerSequence,
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Closes the session and releases resources.
|
|
/// </summary>
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
await CloseAsync().ConfigureAwait(false);
|
|
_closeLock.Dispose();
|
|
}
|
|
|
|
private Task<MxCommandReply> InvokeCommandAsync(
|
|
MxCommand command,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
return _client.InvokeAsync(
|
|
new MxCommandRequest
|
|
{
|
|
SessionId = SessionId,
|
|
ClientCorrelationId = Guid.NewGuid().ToString("N"),
|
|
Command = command,
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
}
|