mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 22:55:08 +08:00
Add async read/write hooks to BaseVariableState and AsyncCustomNodeManager
Introduces an end-to-end async path for variable Value-attribute reads and writes that does not require holding lock(this) while the user callback executes: * New AttributeReadResult, AttributeSimpleReadResult and AttributeWriteResult readonly record structs in NodeState carry the typed result of an async Value-attribute hook (no ref/out params across await). * Four new async delegates (NodeValueEventHandlerAsync, NodeValueSimpleEventHandlerAsync, NodeValueWriteEventHandlerAsync, NodeValueSimpleWriteEventHandlerAsync) sit alongside the existing sync hooks. * NodeState gains virtual ReadAttributeAsync / WriteAttributeAsync methods. The default implementation wraps the synchronous call in lock(this) so callers that have not opted into async hooks see bit-identical behaviour. * BaseVariableState exposes OnReadValueAsync, OnSimpleReadValueAsync, OnWriteValueAsync and OnSimpleWriteValueAsync slots and overrides the new virtuals to dispatch to them WITHOUT holding the lock during the await. On simple-read the framework still applies index range, data encoding and copy policy; on simple-write the cached value/status/timestamp are updated under lock after the hook completes. * AsyncCustomNodeManager.ReadAsync and WriteAsync now await the new ReadAttributeAsync / WriteAttributeAsync entry points instead of taking lock(source) around the synchronous call. Existing per-source locking semantics are preserved by the default async wrappers. * The fluent INodeBuilder surface gains four new async OnRead/OnWrite overloads that wire to the new BaseVariableState slots, with the same ThrowIfSlotOccupied + null-check pattern as the sync overloads. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -93,6 +93,31 @@ namespace Opc.Ua.Server.Fluent
|
||||
/// </summary>
|
||||
INodeBuilder OnWrite(NodeValueSimpleEventHandler handler);
|
||||
|
||||
/// <summary>
|
||||
/// Wires <see cref="BaseVariableState.OnReadValueAsync"/>. Use this
|
||||
/// overload when the read source performs I/O — the handler runs
|
||||
/// without holding <c>lock(this)</c>, so the call can <c>await</c>
|
||||
/// freely.
|
||||
/// </summary>
|
||||
INodeBuilder OnRead(NodeValueEventHandlerAsync handler);
|
||||
|
||||
/// <summary>
|
||||
/// Wires <see cref="BaseVariableState.OnSimpleReadValueAsync"/>.
|
||||
/// The framework still applies index-range / data-encoding /
|
||||
/// copy-policy post-processing to the returned value.
|
||||
/// </summary>
|
||||
INodeBuilder OnRead(NodeValueSimpleEventHandlerAsync handler);
|
||||
|
||||
/// <summary>
|
||||
/// Wires <see cref="BaseVariableState.OnWriteValueAsync"/>.
|
||||
/// </summary>
|
||||
INodeBuilder OnWrite(NodeValueWriteEventHandlerAsync handler);
|
||||
|
||||
/// <summary>
|
||||
/// Wires <see cref="BaseVariableState.OnSimpleWriteValueAsync"/>.
|
||||
/// </summary>
|
||||
INodeBuilder OnWrite(NodeValueSimpleWriteEventHandlerAsync handler);
|
||||
|
||||
/// <summary>
|
||||
/// Wires <see cref="MethodState.OnCallMethod2"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -104,6 +104,42 @@ namespace Opc.Ua.Server.Fluent
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public INodeBuilder OnRead(NodeValueEventHandlerAsync handler)
|
||||
{
|
||||
BaseVariableState v = RequireVariable("OnReadAsync");
|
||||
ThrowIfSlotOccupied(v.OnReadValueAsync, "OnReadAsync");
|
||||
v.OnReadValueAsync = handler ?? throw new ArgumentNullException(nameof(handler));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public INodeBuilder OnRead(NodeValueSimpleEventHandlerAsync handler)
|
||||
{
|
||||
BaseVariableState v = RequireVariable("OnSimpleReadAsync");
|
||||
ThrowIfSlotOccupied(v.OnSimpleReadValueAsync, "OnSimpleReadAsync");
|
||||
v.OnSimpleReadValueAsync = handler ?? throw new ArgumentNullException(nameof(handler));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public INodeBuilder OnWrite(NodeValueWriteEventHandlerAsync handler)
|
||||
{
|
||||
BaseVariableState v = RequireVariable("OnWriteAsync");
|
||||
ThrowIfSlotOccupied(v.OnWriteValueAsync, "OnWriteAsync");
|
||||
v.OnWriteValueAsync = handler ?? throw new ArgumentNullException(nameof(handler));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public INodeBuilder OnWrite(NodeValueSimpleWriteEventHandlerAsync handler)
|
||||
{
|
||||
BaseVariableState v = RequireVariable("OnSimpleWriteAsync");
|
||||
ThrowIfSlotOccupied(v.OnSimpleWriteValueAsync, "OnSimpleWriteAsync");
|
||||
v.OnSimpleWriteValueAsync = handler ?? throw new ArgumentNullException(nameof(handler));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public INodeBuilder OnCall(GenericMethodCalledEventHandler2 handler)
|
||||
{
|
||||
|
||||
@@ -1962,15 +1962,18 @@ namespace Opc.Ua.Server
|
||||
DataValue value = values[handle.Index];
|
||||
|
||||
// update the attribute value.
|
||||
lock (source)
|
||||
{
|
||||
errors[handle.Index] = source.ReadAttribute(
|
||||
context,
|
||||
nodeToRead.AttributeId,
|
||||
nodeToRead.ParsedIndexRange,
|
||||
nodeToRead.DataEncoding,
|
||||
value);
|
||||
}
|
||||
// Async path — the default NodeState.ReadAttributeAsync re-takes
|
||||
// `lock(this)` around the synchronous fallback when no async
|
||||
// hook is set, so behaviour is preserved bit-for-bit. When a
|
||||
// BaseVariableState async hook is set the lock is dropped on
|
||||
// the await, allowing true async I/O.
|
||||
errors[handle.Index] = await source.ReadAttributeAsync(
|
||||
context,
|
||||
nodeToRead.AttributeId,
|
||||
nodeToRead.ParsedIndexRange,
|
||||
nodeToRead.DataEncoding,
|
||||
value,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2372,15 +2375,13 @@ namespace Opc.Ua.Server
|
||||
|
||||
WriteValue nodeToWrite = nodesToWrite[handle.Index];
|
||||
|
||||
lock (source)
|
||||
{
|
||||
// write the attribute value.
|
||||
errors[handle.Index] = source.WriteAttribute(
|
||||
context,
|
||||
nodeToWrite.AttributeId,
|
||||
nodeToWrite.ParsedIndexRange,
|
||||
nodeToWrite.Value);
|
||||
}
|
||||
// Async path — see ReadAsync for the locking rationale.
|
||||
errors[handle.Index] = await source.WriteAttributeAsync(
|
||||
context,
|
||||
nodeToWrite.AttributeId,
|
||||
nodeToWrite.ParsedIndexRange,
|
||||
nodeToWrite.Value,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// updates to source finished - report changes to monitored items.
|
||||
source.ClearChangeMasks(context, false);
|
||||
|
||||
@@ -33,6 +33,8 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Opc.Ua.Types;
|
||||
|
||||
@@ -561,6 +563,45 @@ namespace Opc.Ua
|
||||
/// </summary>
|
||||
public NodeValueEventHandler OnWriteValue;
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronous sibling of <see cref="OnReadValue"/>. When set, the
|
||||
/// async path through
|
||||
/// <see cref="ReadAttributeAsync(ISystemContext, uint, NumericRange, QualifiedName, DataValue, CancellationToken)"/>
|
||||
/// invokes this delegate without holding the <c>lock(this)</c>
|
||||
/// taken by the synchronous fallback — the handler owns its own
|
||||
/// thread-safety. Has the same "I am the read source"
|
||||
/// semantics as <see cref="OnReadValue"/>: the framework does
|
||||
/// <em>not</em> apply post-processing such as
|
||||
/// <see cref="ApplyIndexRangeAndDataEncoding"/> on the returned
|
||||
/// value.
|
||||
/// </summary>
|
||||
public NodeValueEventHandlerAsync OnReadValueAsync;
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronous sibling of <see cref="OnSimpleReadValue"/>. The
|
||||
/// returned value is post-processed by the framework
|
||||
/// (<see cref="ApplyIndexRangeAndDataEncoding"/> and copy policy)
|
||||
/// just like the synchronous path.
|
||||
/// </summary>
|
||||
public NodeValueSimpleEventHandlerAsync OnSimpleReadValueAsync;
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronous sibling of <see cref="OnWriteValue"/>. When set,
|
||||
/// the async path through
|
||||
/// <see cref="WriteAttributeAsync(ISystemContext, uint, NumericRange, DataValue, CancellationToken)"/>
|
||||
/// invokes this delegate without holding <c>lock(this)</c>; on
|
||||
/// success the framework updates the cached value, status code
|
||||
/// and timestamp.
|
||||
/// </summary>
|
||||
public NodeValueWriteEventHandlerAsync OnWriteValueAsync;
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronous sibling of <see cref="OnSimpleWriteValue"/>. Index
|
||||
/// range writes are not supported through this hook (just like
|
||||
/// the synchronous one).
|
||||
/// </summary>
|
||||
public NodeValueSimpleWriteEventHandlerAsync OnSimpleWriteValueAsync;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the DataType attribute is read.
|
||||
/// </summary>
|
||||
@@ -1737,6 +1778,315 @@ namespace Opc.Ua
|
||||
return ServiceResult.Good;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronous override of
|
||||
/// <see cref="NodeState.ReadAttributeAsync(ISystemContext, uint, NumericRange, QualifiedName, DataValue, CancellationToken)"/>.
|
||||
/// Dispatches to <see cref="OnReadValueAsync"/> or
|
||||
/// <see cref="OnSimpleReadValueAsync"/> when set; otherwise falls
|
||||
/// through to the synchronous read flow under <c>lock(this)</c>
|
||||
/// (preserving today's locking semantics for code that has not
|
||||
/// opted into async hooks).
|
||||
/// </summary>
|
||||
public override async ValueTask<ServiceResult> ReadAttributeAsync(
|
||||
ISystemContext context,
|
||||
uint attributeId,
|
||||
NumericRange indexRange,
|
||||
QualifiedName dataEncoding,
|
||||
DataValue value,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
NodeValueEventHandlerAsync onReadValueAsync = OnReadValueAsync;
|
||||
NodeValueSimpleEventHandlerAsync onSimpleReadValueAsync = OnSimpleReadValueAsync;
|
||||
|
||||
if (attributeId != Attributes.Value ||
|
||||
(onReadValueAsync == null && onSimpleReadValueAsync == null))
|
||||
{
|
||||
return await base.ReadAttributeAsync(
|
||||
context, attributeId, indexRange, dataEncoding, value, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (value == null)
|
||||
{
|
||||
return ServiceResult.Create(
|
||||
StatusCodes.BadStructureMissing,
|
||||
"DataValue missing");
|
||||
}
|
||||
|
||||
ServiceResult result;
|
||||
Variant valueToRead;
|
||||
DateTimeUtc sourceTimestamp;
|
||||
|
||||
try
|
||||
{
|
||||
// snapshot access levels / timestamp / status code under the lock.
|
||||
uint accessLevel;
|
||||
byte userAccessLevel;
|
||||
StatusCode cachedStatusCode;
|
||||
DateTimeUtc cachedTimestamp;
|
||||
// TODO: introduce a dedicated private lock object on NodeState
|
||||
// — today's sync flow synchronises through `lock(source)` taken
|
||||
// by external callers (e.g. CustomNodeManager2.Read), so the
|
||||
// async path must lock on the same instance to preserve
|
||||
// mutual exclusion. Switching to a private lock object
|
||||
// requires updating every external `lock(source)` site.
|
||||
#pragma warning disable CA2002 // Do not lock on objects with weak identity
|
||||
lock (this)
|
||||
#pragma warning restore CA2002
|
||||
{
|
||||
accessLevel = m_accessLevel;
|
||||
userAccessLevel = m_userAccessLevel;
|
||||
cachedStatusCode = m_statusCode;
|
||||
cachedTimestamp = m_timestamp;
|
||||
}
|
||||
|
||||
if ((accessLevel & AccessLevels.CurrentRead) == 0)
|
||||
{
|
||||
result = StatusCodes.BadNotReadable;
|
||||
valueToRead = Variant.Null;
|
||||
sourceTimestamp = DateTimeUtc.MinValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
OnReadUserAccessLevel?.Invoke(context, this, ref userAccessLevel);
|
||||
|
||||
if ((userAccessLevel & AccessLevels.CurrentRead) == 0)
|
||||
{
|
||||
result = StatusCodes.BadUserAccessDenied;
|
||||
valueToRead = Variant.Null;
|
||||
sourceTimestamp = DateTimeUtc.MinValue;
|
||||
}
|
||||
else if (onReadValueAsync != null)
|
||||
{
|
||||
// full async read — user owns timestamp / status code.
|
||||
AttributeReadResult readResult = await onReadValueAsync(
|
||||
context, this, indexRange, dataEncoding, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
result = readResult.Result;
|
||||
valueToRead = readResult.Value;
|
||||
sourceTimestamp = readResult.SourceTimestamp == DateTimeUtc.MinValue
|
||||
? DateTimeUtc.Now
|
||||
: readResult.SourceTimestamp;
|
||||
|
||||
// mirror sync OnReadValue status-code fixup.
|
||||
if (ServiceResult.IsGood(result) &&
|
||||
readResult.StatusCode != StatusCodes.Good)
|
||||
{
|
||||
result = readResult.StatusCode;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// simple async read — framework owns post-processing.
|
||||
AttributeSimpleReadResult simpleResult = await onSimpleReadValueAsync(
|
||||
context, this, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
result = simpleResult.Result;
|
||||
valueToRead = simpleResult.Value;
|
||||
sourceTimestamp = cachedTimestamp == DateTimeUtc.MinValue
|
||||
? DateTimeUtc.Now
|
||||
: cachedTimestamp;
|
||||
|
||||
if (ServiceResult.IsGood(result))
|
||||
{
|
||||
ServiceResult rangeResult = ApplyIndexRangeAndDataEncoding(
|
||||
context, indexRange, dataEncoding, ref valueToRead);
|
||||
|
||||
if (ServiceResult.IsBad(rangeResult))
|
||||
{
|
||||
result = rangeResult;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (CopyPolicy is VariableCopyPolicy.CopyOnRead or VariableCopyPolicy.Always)
|
||||
{
|
||||
valueToRead = CoreUtils.Clone(valueToRead);
|
||||
}
|
||||
|
||||
if (cachedStatusCode != StatusCodes.Good)
|
||||
{
|
||||
result = cachedStatusCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
result = ServiceResult.Create(
|
||||
e,
|
||||
StatusCodes.BadUnexpectedError,
|
||||
"Failed to read value attribute from node.");
|
||||
valueToRead = Variant.Null;
|
||||
sourceTimestamp = DateTimeUtc.MinValue;
|
||||
}
|
||||
|
||||
// commit to the supplied DataValue, mirroring NodeState.ReadAttribute(DataValue).
|
||||
value.SourceTimestamp = sourceTimestamp;
|
||||
value.SourcePicoseconds = 0;
|
||||
|
||||
if (result != null && result != ServiceResult.Good)
|
||||
{
|
||||
value.StatusCode = result.StatusCode;
|
||||
}
|
||||
else
|
||||
{
|
||||
value.StatusCode = StatusCodes.Good;
|
||||
}
|
||||
|
||||
value.WrappedValue = StatusCode.IsBad(value.StatusCode) ? Variant.Null : valueToRead;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronous override of
|
||||
/// <see cref="NodeState.WriteAttributeAsync(ISystemContext, uint, NumericRange, DataValue, CancellationToken)"/>.
|
||||
/// Dispatches to <see cref="OnWriteValueAsync"/> or
|
||||
/// <see cref="OnSimpleWriteValueAsync"/> when set; otherwise falls
|
||||
/// through to the synchronous write flow under <c>lock(this)</c>.
|
||||
/// </summary>
|
||||
public override async ValueTask<ServiceResult> WriteAttributeAsync(
|
||||
ISystemContext context,
|
||||
uint attributeId,
|
||||
NumericRange indexRange,
|
||||
DataValue value,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
NodeValueWriteEventHandlerAsync onWriteValueAsync = OnWriteValueAsync;
|
||||
NodeValueSimpleWriteEventHandlerAsync onSimpleWriteValueAsync = OnSimpleWriteValueAsync;
|
||||
|
||||
if (attributeId != Attributes.Value ||
|
||||
(onWriteValueAsync == null && onSimpleWriteValueAsync == null))
|
||||
{
|
||||
return await base.WriteAttributeAsync(
|
||||
context, attributeId, indexRange, value, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (value == null)
|
||||
{
|
||||
return ServiceResult.Create(
|
||||
StatusCodes.BadStructureMissing,
|
||||
"DataValue missing");
|
||||
}
|
||||
|
||||
if (value.ServerTimestamp != DateTimeUtc.MinValue)
|
||||
{
|
||||
return ServiceResult.Create(
|
||||
StatusCodes.BadWriteNotSupported,
|
||||
"Cannot write to server timestamp");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// snapshot access levels under the lock.
|
||||
uint accessLevel;
|
||||
byte userAccessLevel;
|
||||
// TODO: introduce a dedicated private lock object on NodeState
|
||||
// — see the sibling note in ReadAttributeAsync for the rationale.
|
||||
#pragma warning disable CA2002 // Do not lock on objects with weak identity
|
||||
lock (this)
|
||||
#pragma warning restore CA2002
|
||||
{
|
||||
accessLevel = m_accessLevel;
|
||||
userAccessLevel = m_userAccessLevel;
|
||||
}
|
||||
|
||||
if ((accessLevel & AccessLevels.CurrentWrite) == 0)
|
||||
{
|
||||
return StatusCodes.BadNotWritable;
|
||||
}
|
||||
|
||||
OnReadUserAccessLevel?.Invoke(context, this, ref userAccessLevel);
|
||||
|
||||
if ((userAccessLevel & AccessLevels.CurrentWrite) == 0)
|
||||
{
|
||||
return StatusCodes.BadUserAccessDenied;
|
||||
}
|
||||
|
||||
Variant valueToWrite = value.WrappedValue;
|
||||
StatusCode statusCode = value.StatusCode;
|
||||
DateTimeUtc sourceTimestamp = value.SourceTimestamp;
|
||||
|
||||
if (onWriteValueAsync != null)
|
||||
{
|
||||
AttributeWriteResult writeResult = await onWriteValueAsync(
|
||||
context, this, indexRange, valueToWrite, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (ServiceResult.IsBad(writeResult.Result))
|
||||
{
|
||||
return writeResult.Result;
|
||||
}
|
||||
|
||||
DateTimeUtc effectiveTimestamp = sourceTimestamp == DateTimeUtc.MinValue
|
||||
? DateTimeUtc.Now
|
||||
: sourceTimestamp;
|
||||
|
||||
#pragma warning disable CA2002 // Do not lock on objects with weak identity
|
||||
lock (this)
|
||||
#pragma warning restore CA2002
|
||||
{
|
||||
m_value = valueToWrite;
|
||||
m_statusCode = statusCode;
|
||||
m_timestamp = effectiveTimestamp;
|
||||
ChangeMasks |= NodeStateChangeMasks.Value;
|
||||
}
|
||||
|
||||
return writeResult.Result;
|
||||
}
|
||||
|
||||
// simple async write path mirrors OnSimpleWriteValue:
|
||||
// index-range writes are not supported through this hook.
|
||||
if (!indexRange.IsNull)
|
||||
{
|
||||
return StatusCodes.BadIndexRangeInvalid;
|
||||
}
|
||||
|
||||
if (sourceTimestamp == DateTimeUtc.MinValue)
|
||||
{
|
||||
sourceTimestamp = DateTimeUtc.Now;
|
||||
}
|
||||
|
||||
if (CopyPolicy is VariableCopyPolicy.CopyOnWrite or VariableCopyPolicy.Always)
|
||||
{
|
||||
valueToWrite = CoreUtils.Clone(valueToWrite);
|
||||
}
|
||||
|
||||
AttributeWriteResult simpleResult = await onSimpleWriteValueAsync(
|
||||
context, this, valueToWrite, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (ServiceResult.IsBad(simpleResult.Result))
|
||||
{
|
||||
return simpleResult.Result;
|
||||
}
|
||||
|
||||
#pragma warning disable CA2002 // Do not lock on objects with weak identity
|
||||
lock (this)
|
||||
#pragma warning restore CA2002
|
||||
{
|
||||
m_value = valueToWrite;
|
||||
m_statusCode = statusCode;
|
||||
m_timestamp = sourceTimestamp;
|
||||
ChangeMasks |= NodeStateChangeMasks.Value;
|
||||
}
|
||||
|
||||
return simpleResult.Result;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return ServiceResult.Create(
|
||||
e,
|
||||
StatusCodes.BadUnexpectedError,
|
||||
"Failed to write value attribute.");
|
||||
}
|
||||
}
|
||||
|
||||
private Variant m_value;
|
||||
private DateTimeUtc m_timestamp;
|
||||
private bool m_valueTouched;
|
||||
|
||||
@@ -32,6 +32,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using Opc.Ua.Types;
|
||||
|
||||
@@ -3696,6 +3697,50 @@ namespace Opc.Ua
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronous sibling of
|
||||
/// <see cref="ReadAttribute(ISystemContext, uint, NumericRange, QualifiedName, DataValue)"/>.
|
||||
/// The default implementation simply wraps the synchronous call
|
||||
/// inside a <c>lock(this)</c> so behaviour is bit-identical for
|
||||
/// every <see cref="NodeState"/> that does not override it. Derived
|
||||
/// types (notably <see cref="BaseVariableState"/>) can override
|
||||
/// this method to dispatch to true asynchronous read hooks without
|
||||
/// blocking a thread.
|
||||
/// </summary>
|
||||
/// <param name="context">The context for the current operation.</param>
|
||||
/// <param name="attributeId">The attribute id.</param>
|
||||
/// <param name="indexRange">The index range.</param>
|
||||
/// <param name="dataEncoding">The data encoding.</param>
|
||||
/// <param name="value">The value to populate.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>
|
||||
/// An instance of the <see cref="ServiceResult"/> containing the
|
||||
/// status code and diagnostic info for the operation.
|
||||
/// </returns>
|
||||
public virtual ValueTask<ServiceResult> ReadAttributeAsync(
|
||||
ISystemContext context,
|
||||
uint attributeId,
|
||||
NumericRange indexRange,
|
||||
QualifiedName dataEncoding,
|
||||
DataValue value,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ServiceResult result;
|
||||
// TODO: introduce a dedicated private lock object on NodeState —
|
||||
// today's sync flow synchronises through `lock(source)` taken by
|
||||
// external callers (e.g. CustomNodeManager2.Read), so the async
|
||||
// path must lock on the same instance to preserve mutual
|
||||
// exclusion. Switching to a private lock object requires
|
||||
// updating every external `lock(source)` site.
|
||||
#pragma warning disable CA2002 // Do not lock on objects with weak identity
|
||||
lock (this)
|
||||
#pragma warning restore CA2002
|
||||
{
|
||||
result = ReadAttribute(context, attributeId, indexRange, dataEncoding, value);
|
||||
}
|
||||
return new ValueTask<ServiceResult>(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the value for any non-value attribute.
|
||||
/// </summary>
|
||||
@@ -3999,6 +4044,40 @@ namespace Opc.Ua
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronous sibling of
|
||||
/// <see cref="WriteAttribute(ISystemContext, uint, NumericRange, DataValue)"/>.
|
||||
/// The default implementation wraps the synchronous call inside a
|
||||
/// <c>lock(this)</c> so behaviour is bit-identical for every
|
||||
/// <see cref="NodeState"/> that does not override it. Derived
|
||||
/// types (notably <see cref="BaseVariableState"/>) can override
|
||||
/// this method to dispatch to true asynchronous write hooks
|
||||
/// without blocking a thread.
|
||||
/// </summary>
|
||||
/// <param name="context">The context for the current operation.</param>
|
||||
/// <param name="attributeId">The attribute id.</param>
|
||||
/// <param name="indexRange">The index range.</param>
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public virtual ValueTask<ServiceResult> WriteAttributeAsync(
|
||||
ISystemContext context,
|
||||
uint attributeId,
|
||||
NumericRange indexRange,
|
||||
DataValue value,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ServiceResult result;
|
||||
// TODO: introduce a dedicated private lock object on NodeState —
|
||||
// see the sibling note in ReadAttributeAsync for the rationale.
|
||||
#pragma warning disable CA2002 // Do not lock on objects with weak identity
|
||||
lock (this)
|
||||
#pragma warning restore CA2002
|
||||
{
|
||||
result = WriteAttribute(context, attributeId, indexRange, value);
|
||||
}
|
||||
return new ValueTask<ServiceResult>(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write the value for any non-value attribute.
|
||||
/// </summary>
|
||||
@@ -5340,6 +5419,99 @@ namespace Opc.Ua
|
||||
ref StatusCode statusCode,
|
||||
ref DateTimeUtc timestamp);
|
||||
|
||||
/// <summary>
|
||||
/// Result returned by an asynchronous full Value-attribute read hook
|
||||
/// (<see cref="NodeValueEventHandlerAsync"/>). Carries the value plus
|
||||
/// its status code and source timestamp; the framework writes all three
|
||||
/// onto the supplied <see cref="DataValue"/> when the operation
|
||||
/// succeeds. Allocated on the stack — no per-call allocation.
|
||||
/// </summary>
|
||||
/// <param name="Result">
|
||||
/// Operation result. <see cref="ServiceResult.IsBad(ServiceResult)"/>
|
||||
/// short-circuits the rest of the read pipeline.
|
||||
/// </param>
|
||||
/// <param name="Value">The value to return to the caller.</param>
|
||||
/// <param name="StatusCode">Status code to attach to the value.</param>
|
||||
/// <param name="SourceTimestamp">
|
||||
/// Source timestamp to attach. <see cref="DateTimeUtc.MinValue"/>
|
||||
/// causes the framework to substitute the current UTC time, mirroring
|
||||
/// the synchronous <see cref="NodeValueEventHandler"/> behavior.
|
||||
/// </param>
|
||||
public readonly record struct AttributeReadResult(
|
||||
ServiceResult Result,
|
||||
Variant Value,
|
||||
StatusCode StatusCode,
|
||||
DateTimeUtc SourceTimestamp);
|
||||
|
||||
/// <summary>
|
||||
/// Result returned by an asynchronous simple Value-attribute read hook
|
||||
/// (<see cref="NodeValueSimpleEventHandlerAsync"/>). Carries only the
|
||||
/// value; the framework reuses the variable's cached status code and
|
||||
/// timestamp and runs the standard index-range / data-encoding
|
||||
/// post-processing — matching the synchronous
|
||||
/// <see cref="NodeValueSimpleEventHandler"/> pipeline.
|
||||
/// </summary>
|
||||
/// <param name="Result">Operation result.</param>
|
||||
/// <param name="Value">The value to return to the caller.</param>
|
||||
public readonly record struct AttributeSimpleReadResult(
|
||||
ServiceResult Result,
|
||||
Variant Value);
|
||||
|
||||
/// <summary>
|
||||
/// Result returned by an asynchronous Value-attribute write hook
|
||||
/// (<see cref="NodeValueWriteEventHandlerAsync"/> /
|
||||
/// <see cref="NodeValueSimpleWriteEventHandlerAsync"/>).
|
||||
/// </summary>
|
||||
/// <param name="Result">Operation result.</param>
|
||||
public readonly record struct AttributeWriteResult(
|
||||
ServiceResult Result);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronous sibling of <see cref="NodeValueEventHandler"/> for the
|
||||
/// read direction. Returns a <see cref="AttributeReadResult"/> instead
|
||||
/// of writing to <c>ref</c> parameters because <c>ref</c> cannot cross
|
||||
/// an <c>await</c>.
|
||||
/// </summary>
|
||||
public delegate ValueTask<AttributeReadResult> NodeValueEventHandlerAsync(
|
||||
ISystemContext context,
|
||||
NodeState node,
|
||||
NumericRange indexRange,
|
||||
QualifiedName dataEncoding,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronous sibling of <see cref="NodeValueSimpleEventHandler"/>
|
||||
/// for the read direction.
|
||||
/// </summary>
|
||||
public delegate ValueTask<AttributeSimpleReadResult> NodeValueSimpleEventHandlerAsync(
|
||||
ISystemContext context,
|
||||
NodeState node,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronous sibling of <see cref="NodeValueEventHandler"/> for the
|
||||
/// write direction. The hook receives the value being written and
|
||||
/// returns only a status; on success the framework updates the
|
||||
/// variable's cached value and timestamp to mirror the synchronous
|
||||
/// path.
|
||||
/// </summary>
|
||||
public delegate ValueTask<AttributeWriteResult> NodeValueWriteEventHandlerAsync(
|
||||
ISystemContext context,
|
||||
NodeState node,
|
||||
NumericRange indexRange,
|
||||
Variant value,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronous sibling of <see cref="NodeValueSimpleEventHandler"/>
|
||||
/// for the write direction.
|
||||
/// </summary>
|
||||
public delegate ValueTask<AttributeWriteResult> NodeValueSimpleWriteEventHandlerAsync(
|
||||
ISystemContext context,
|
||||
NodeState node,
|
||||
Variant value,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Stores a reference from a node in the instance hierarchy.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user