Files
Marc Schier a81d981755 dotnet format sweep (whitespace, IDE, RCS) + code-style enforcer agent fix (#3865)
# Description

Pure code-style sweep across the repo — no behavioral changes. Drives
`dotnet format` info-level diagnostics down on a large slice of the
codebase (Stack, Libraries, Tests, Tools).

Six commits, applied bottom-up in safety order so each batch leaves the
build green:

1. **`style: fix whitespace and final-newline formatting (WHITESPACE,
FINALNEWLINE)`**
   197 hits across ~30 files; pure mechanical cleanup.

2. **`style: apply dotnet format style fixes (IDE rules)`**
IDE-rule batch via `dotnet format style --severity info` —
`var`/explicit type, simplify names, collection expressions, expression
bodies, unused usings, etc.

3. **`style: apply dotnet format analyzers fixes (RCS blank-line
formatting)`**
   Roslynator formatting (RCS0xxx) blank-line and punctuation rules.

4. **`style: convert comments to doc comments and inline local vars
(RCS1181, RCS1124)`**
24 files. RCS1181 promotes `// summary`-style comments above
public/internal members to `/// <summary>` doc comments; RCS1124 inlines
locals used exactly once. Generated `Tools/` files were reverted to keep
the source-generator outputs deterministic.

5. **`docs(agent): correct --diagnostics flag form and verify Roslynator
4.x code fixers`**
Updates `.github/agents/opc-ua-codestyle-enforcer.agent.md` with two
corrections discovered while running this sweep:
- `dotnet format --diagnostics "RULE1 RULE2"` (single quoted,
space-separated) silently fixes nothing on `dotnet format` 10.x — it
parses as one unknown ID and exits 0. The correct form is `--diagnostics
RULE1 --diagnostics RULE2 …`, repeated per rule.
- Roslynator 4.15.0 **does** ship code fixers for
RCS1078/1118/1124/1140/1142/1181/1221/1260 (the previous "no auto-fixer"
list was outdated). RCS1140/1142 default to `Hidden` so need `--severity
info` to surface and fire; RCS1078 requires
`roslynator_empty_string_style = field` (already set in
`.editorconfig`). Only NUnit4002/2046/2010 genuinely lack auto-fixers in
this stack.

6. **`style: format new code from master merge (dotnet format sweep)`**
After merging master into the branch (commit `e3ebc8742`), re-ran the
same three-phase format sweep on the 151 .cs files brought in by the
merge, scoped per project via repeated `--include`. 81 files changed;
same rule families as commits 1–4.
- Hand-fixed `Tests/Opc.Ua.MigrationAnalyzer.Tests/AnalyzerHarness.cs`
for `RCS1174` (redundant `async`/`await` removal) because the auto-fixer
didn't apply for that specific case.
   - Reverted files that the formatter would break:
- `Libraries/Opc.Ua.Server/Server/ServerInternalData.cs` — multi-TFM
analysis disagreement injected `<<<<<<<` / `=======` / `>>>>>>>`
markers.
-
`Tools/Opc.Ua.MigrationAnalyzer.Core/{Core/Stack/Client/ChannelBase.cs,
Core/Stack/Types/IUserIdentityTokenHandler.cs,
Client/Session/Session.cs}` — IDE0005 removed `using
Opc.Ua.Security.Certificates` that IS used (the files are 1.5.378
reference fixtures for the analyzer).
- `Tests/Opc.Ua.Gds.Tests/PushTest.cs` — IDE0002 over-simplified
`OpcUa.ObjectTypeIds` to `ObjectTypeIds`, which then resolves to a
conflicting type.

### What this PR does **not** touch

- No behavioral or semantic logic changes.
- No `.editorconfig` rule promotions (rules stay at their current
severities; this is a backlog-burndown pass, not a rule tightening).
- Generated source under `Tools/` is intentionally excluded.
- Higher-risk rule families (`RCS1090`/`CA2007` `ConfigureAwait`,
`RCS1249` null-forgiving cleanup, `IDE0270`/`IDE0059`) are left for
follow-up sweeps because they need per-TFM build verification or
semantic review.

## Related Issues

_No tracking issue — this is a non-functional style sweep and the
agent-doc correction is a follow-up to the same work. Happy to open one
if the maintainers prefer._

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage. _(N/A — style-only, no
behavioral change)_
- [x] I have added all necessary documentation. _(agent doc updated with
the two corrections)_
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings. _(verified clean on `net10.0` and `net48`; only
pre-existing `NETSDK1005`/`NETSDK1207` failures on Tools/AOT projects)_
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed. _(build
verified on net10 and net48 per batch; full test suite not yet run
locally — relying on CI)_
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

---------

Co-authored-by: agent <agent@local>
2026-06-10 08:35:44 +02:00

851 lines
32 KiB
C#

/* ========================================================================
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Opc.Ua.Client.Subscriptions.MonitoredItems
{
/// <summary>
/// A monitored item that can be extended to add extra
/// information as context in the subscription.
/// </summary>
internal abstract class MonitoredItem : IMonitoredItem, IAsyncDisposable
{
/// <inheritdoc/>
public string Name { get; }
/// <inheritdoc/>
public uint Order => m_currentOptions?.Order ?? 0u;
/// <inheritdoc/>
public uint ServerId { get; private set; }
/// <inheritdoc/>
public bool Created => ServerId != 0;
/// <inheritdoc/>
public ServiceResult Error { get; private set; }
/// <inheritdoc/>
public MonitoringFilterResult? FilterResult { get; private set; }
/// <inheritdoc/>
public MonitoringMode CurrentMonitoringMode { get; internal set; }
/// <inheritdoc/>
public TimeSpan CurrentSamplingInterval { get; private set; }
/// <inheritdoc/>
public uint CurrentQueueSize { get; private set; }
/// <inheritdoc/>
public uint ClientHandle { get; private set; }
/// <summary>
/// Client handle of the item that triggers this item, or
/// <c>0</c> if no triggering relationship has been set. Used
/// by the snapshot path and by the <see cref="TriggeringItem"/>
/// public projection which resolves the handle to the live
/// sibling via <see cref="IMonitoredItemContext"/>.
/// </summary>
internal uint TriggeringItemClientHandle { get; set; }
/// <inheritdoc/>
public IMonitoredItem? TriggeringItem
{
get
{
uint handle = TriggeringItemClientHandle;
if (handle == 0)
{
return null;
}
return Context.TryGetMonitoredItemByClientHandle(
handle, out IMonitoredItem? item) ? item : null;
}
}
/// <summary>
/// Apply server-side identifiers from a saved snapshot to this
/// freshly-created monitored item. Used by
/// <see cref="ISubscriptionManager.LoadAsync"/> when the caller
/// requests <c>transferSubscriptions</c> so the loaded item can
/// be matched to its server-side state via
/// <c>TransferSubscriptions</c>.
/// </summary>
/// <remarks>
/// Re-assigning <see cref="ClientHandle"/> after construction
/// matters because the V2 publish loop dispatches notifications
/// by client handle. The snapshot's client handle is the one the
/// server still uses; the post-construction one is freshly
/// generated and would never match an incoming notification.
/// </remarks>
internal void ApplyTransferState(uint clientHandle, uint serverId)
{
ClientHandle = clientHandle;
ServerId = serverId;
}
/// <summary>
/// Install fully-loaded state from a snapshot during V2
/// transfer-on-load (<see cref="ISubscriptionManager.LoadAsync"/>
/// with <c>transferSubscriptions: true</c>). Unlike
/// <see cref="ApplyTransferState"/>, this also:
/// <list type="bullet">
/// <item><description>Abandons the create-or-modify
/// <see cref="Change"/> the ctor's <see cref="QueuePendingChanges"/>
/// just queued, so the V2 state machine does not issue a
/// redundant <c>CreateMonitoredItems</c> request whose
/// <c>ClientHandle</c> would mismatch the snapshot's
/// (the source of the <c>Debug.Assert</c> failure that
/// motivated this work).</description></item>
/// <item><description>Re-installs the saved triggering links
/// so a subsequent <c>SetTriggering</c> replay reflects the
/// snapshot.</description></item>
/// </list>
/// The caller is responsible for binding the (possibly
/// freshly-revised) server-side state via the
/// <c>TransferSubscriptions</c> / <c>GetMonitoredItems</c> path
/// that runs in <see cref="MonitoredItemManager.TrySynchronizeHandlesAsync"/>.
/// </summary>
internal void ApplyLoadState(MonitoredItemLoadState state)
{
// Abandon any pending changes queued during the ctor.
while (TryGetPendingChange(out Change? change))
{
change.Abandon();
}
// Snapshot the current options so the V2 state machine
// treats the item as "configured to the loaded values" and
// skips the create path until a real change arrives.
m_currentOptions = m_options.CurrentValue;
ClientHandle = state.ClientHandle;
ServerId = state.ServerId;
TriggeringItemClientHandle = state.TriggeringItemClientHandle;
}
/// <summary>
/// The subscription that owns the monitored item.
/// </summary>
protected IMonitoredItemContext Context { get; }
/// <summary>
/// Current monitored item options
/// </summary>
internal IOptionsMonitor<MonitoredItemOptions> Options
{
get => m_options;
set
{
if (m_options != value)
{
m_options = value;
QueuePendingChanges(m_options.CurrentValue, m_currentOptions);
m_changeTracking?.Dispose();
m_changeTracking = m_options.OnChange(
(o, _) => OnOptionsChanged(o));
}
}
}
/// <summary>
/// Create monitored item
/// </summary>
/// <param name="context"></param>
/// <param name="name"></param>
/// <param name="options"></param>
/// <param name="logger"></param>
protected MonitoredItem(IMonitoredItemContext context, string name,
IOptionsMonitor<MonitoredItemOptions> options, ILogger logger)
{
Context = context;
Name = name;
Error = ServiceResult.Good;
ClientHandle = Utils.IncrementIdentifier(ref GlobalClientHandleUint);
m_logger = logger;
m_options = Options = options;
m_logger.LogDebug("{Item} CREATED.", this);
}
/// <inheritdoc/>
public ValueTask DisposeAsync()
{
GC.SuppressFinalize(this);
return DisposeAsync(disposing: true);
}
/// <summary>
/// Capture an immutable snapshot of this item's configuration
/// + identifiers + triggering state.
/// </summary>
public MonitoredItemStateSnapshot Snapshot()
{
return MonitoredItemStateSnapshot.AsOptions(
Name,
m_options.CurrentValue,
ClientHandle,
ServerId,
TriggeringItemClientHandle);
}
/// <inheritdoc/>
public ValueTask ConditionRefreshAsync(CancellationToken ct = default)
{
if (!Created)
{
throw ServiceResultException.Create(
StatusCodes.BadMonitoredItemIdInvalid,
"Monitored item has not been created on the server.");
}
return Context.ConditionRefreshAsync(ServerId, ct);
}
/// <inheritdoc/>
public override string? ToString()
{
StringBuilder sb = new StringBuilder()
.Append(Context)
.Append('#')
.Append(ClientHandle)
.Append('|')
.Append(ServerId)
.Append(" (")
.Append(Name)
.Append(')');
return sb.ToString();
}
/// <summary>
/// Dispose monitored item
/// </summary>
/// <param name="disposing"></param>
protected virtual ValueTask DisposeAsync(bool disposing)
{
if (disposing && !m_disposedValue)
{
while (TryGetPendingChange(out Change? change))
{
change.Abandon();
}
Context.NotifyItemChange(this, true);
m_logger.LogDebug("{Item} REMOVED.", this);
ServerId = 0;
m_changeTracking?.Dispose();
m_disposedValue = true;
}
return default;
}
/// <summary>
/// Called when the subscription state changed
/// </summary>
/// <param name="state"></param>
/// <param name="publishingInterval"></param>
protected internal virtual void OnSubscriptionStateChange(
SubscriptionState state, TimeSpan publishingInterval)
{
MonitoredItemOptions? options = m_currentOptions;
if (options == null ||
(state != SubscriptionState.Created &&
state != SubscriptionState.Modified))
{
return;
}
uint queueSize = options.QueueSize;
if (!options.AutoSetQueueSize)
{
return;
}
if (publishingInterval == TimeSpan.Zero)
{
return;
}
TimeSpan samplingInterval = CurrentSamplingInterval;
if (samplingInterval == TimeSpan.Zero)
{
samplingInterval = options.SamplingInterval;
}
if (samplingInterval <= TimeSpan.Zero)
{
return;
}
queueSize = Math.Max(queueSize, (uint)Math.Ceiling(
publishingInterval.TotalMilliseconds / samplingInterval.TotalMilliseconds)) +
1;
if (queueSize == options.QueueSize)
{
return;
}
OnOptionsChanged(options with { QueueSize = queueSize });
}
/// <summary>
/// Called when the options change
/// </summary>
/// <param name="options"></param>
protected virtual void OnOptionsChanged(MonitoredItemOptions options)
{
QueuePendingChanges(options, m_currentOptions);
Context.NotifyItemChange(this);
}
/// <summary>
/// Notify subscription that the subscription manager has paused or
/// resumed operations.
/// </summary>
/// <param name="paused"></param>
protected internal virtual void NotifySubscriptionManagerPaused(bool paused)
{
// empty
}
/// <summary>
/// Get the current pending change in the change list. The change list
/// collects the changes to be made to the monitored item while the
/// subscription is applying state changes.
/// </summary>
/// <param name="change"></param>
/// <returns></returns>
internal bool TryGetPendingChange([NotNullWhen(true)] out Change? change)
{
return m_pendingChanges.TryPeek(out change);
}
/// <summary>
/// Updates the object with the results of a transfer subscription request.
/// </summary>
/// <param name="clientHandle"></param>
/// <param name="serverHandle"></param>
/// <exception cref="ObjectDisposedException"></exception>
internal void SetTransferResult(uint clientHandle, uint serverHandle)
{
if (m_disposedValue)
{
throw new ObjectDisposedException(GetType().FullName);
}
// ensure the global counter is not duplicating future handle ids
if (clientHandle != ClientHandle)
{
m_logger.LogInformation("{Item}: UPDATE CLIENT ID from {Old} to {New}.",
this, ClientHandle, clientHandle);
ClientHandle = clientHandle;
Utils.SetIdentifierToAtLeast(ref GlobalClientHandleUint, clientHandle);
}
if (serverHandle != ServerId)
{
m_logger.LogInformation("{Item}: UPDATE SERVER ID from {Old} to {New}.",
this, ServerId, serverHandle);
ServerId = serverHandle;
}
}
/// <summary>
/// Reset the monitored item to its initial state for recreation
/// on server side.
/// </summary>
/// <exception cref="ObjectDisposedException"></exception>
internal void Reset()
{
if (m_disposedValue)
{
throw new ObjectDisposedException(GetType().FullName);
}
m_logger.LogDebug("{Item}: RESET.", this);
ServerId = 0;
MonitoredItemOptions? options = m_currentOptions;
while (TryGetPendingChange(out Change? change))
{
change.Abandon();
options = change.Options;
}
m_currentOptions = null;
if (options == null)
{
return;
}
QueuePendingChanges(options, null);
}
/// <summary>
/// Queues changes into the change queue for the item
/// </summary>
/// <param name="options"></param>
/// <param name="currentOptions"></param>
private void QueuePendingChanges(MonitoredItemOptions options,
MonitoredItemOptions? currentOptions)
{
if (currentOptions != null && currentOptions == options)
{
// No changes
Context.NotifyItemChangeResult(this, 0, options, new ServiceResult(
StatusCodes.BadNothingToDo), true, null);
return;
}
if (options.StartNodeId.IsNull)
{
// Not valid
Context.NotifyItemChangeResult(this, 0, options, new ServiceResult(
StatusCodes.BadNodeIdInvalid), true, null);
return;
}
m_currentOptions = options;
m_pendingChanges.Enqueue(new Change(this, options, currentOptions));
}
/// <summary>
/// Complete a change
/// </summary>
/// <param name="change"></param>
/// <returns></returns>
internal bool CompleteChange(Change change)
{
return m_pendingChanges.TryDequeue(out Change? completed) &&
change == completed;
}
/// <summary>
/// Change steps to apply to the monitored items in the
/// subscription context. The change list is a queue of
/// steps to perform inside the subscription.
/// see Subscription.ApplyMonitoredItemChangesAsync for
/// more information
/// </summary>
internal sealed class Change
{
/// <summary>
/// The item on which the changes are applied
/// </summary>
public MonitoredItem Item { get; }
/// <summary>
/// Timestamps to return
/// </summary>
public TimestampsToReturn Timestamps => Options.TimestampsToReturn;
/// <summary>
/// Create request if the item is not yet created
/// </summary>
public MonitoredItemCreateRequest? Create { get; }
/// <summary>
/// Modification request if the item is already created
/// </summary>
public MonitoredItemModifyRequest? Modify { get; private set; }
/// <summary>
/// Monitoring mode change pending
/// </summary>
public MonitoringMode? MonitoringModeChange { get; private set; }
/// <summary>
/// Force recreating the item due to changes in the
/// read item information.
/// </summary>
public bool ForceRecreate { get; private set; }
/// <summary>
/// Current retry count of this change
/// </summary>
public int RetryCount { get; private set; }
/// <summary>
/// Options that are the source of the change
/// </summary>
public MonitoredItemOptions Options { get; }
/// <summary>
/// Create change
/// </summary>
/// <param name="item"></param>
/// <param name="options"></param>
/// <param name="currentOptions"></param>
public Change(MonitoredItem item, MonitoredItemOptions options,
MonitoredItemOptions? currentOptions)
{
Debug.Assert(!options.StartNodeId.IsNull);
Options = options;
Item = item;
var parameters = new MonitoringParameters
{
ClientHandle = item.ClientHandle,
SamplingInterval = (int)options.SamplingInterval.TotalMilliseconds,
QueueSize = options.QueueSize,
DiscardOldest = options.DiscardOldest,
Filter = options.Filter != null ?
new ExtensionObject(options.Filter) : default
};
Create = new MonitoredItemCreateRequest
{
ItemToMonitor = new ReadValueId
{
NodeId = options.StartNodeId,
AttributeId = options.AttributeId,
IndexRange = options.IndexRange,
DataEncoding = options.Encoding ?? QualifiedName.Null
},
MonitoringMode = options.MonitoringMode,
RequestedParameters = parameters
};
if (currentOptions == null ||
currentOptions.StartNodeId != options.StartNodeId ||
currentOptions.AttributeId != options.AttributeId ||
currentOptions.IndexRange != options.IndexRange ||
currentOptions.Encoding != options.Encoding)
{
Modify = null;
MonitoringModeChange = null;
ForceRecreate = true;
}
else
{
Modify = new MonitoredItemModifyRequest
{
MonitoredItemId = item.ServerId,
RequestedParameters = parameters
};
if (currentOptions.MonitoringMode != options.MonitoringMode)
{
MonitoringModeChange = options.MonitoringMode;
// If only monitoring mode changed, no need to modify
if (currentOptions with
{
MonitoringMode = MonitoringModeChange.Value
} == options)
{
Modify = null;
}
}
else
{
MonitoringModeChange = null;
}
}
}
/// <summary>
/// Updates the object with the results of a create monitored item request.
/// </summary>
/// <param name="request"></param>
/// <param name="result"></param>
/// <param name="index"></param>
/// <param name="diagnosticInfos"></param>
/// <param name="responseHeader"></param>
internal void SetCreateResult(MonitoredItemCreateRequest request,
MonitoredItemCreateResult result, int index,
ArrayOf<DiagnosticInfo> diagnosticInfos, ResponseHeader responseHeader)
{
Debug.Assert(request.RequestedParameters.ClientHandle == Item.ClientHandle);
ServiceResult error = ServiceResult.Good;
if (StatusCode.IsBad(result.StatusCode))
{
error = ClientBase.GetResult(result.StatusCode, index,
diagnosticInfos, responseHeader);
}
Item.CurrentMonitoringMode = request.MonitoringMode;
Item.CurrentSamplingInterval = TimeSpan.FromMilliseconds(
request.RequestedParameters.SamplingInterval);
Item.CurrentQueueSize = request.RequestedParameters.QueueSize;
if (ServiceResult.IsGood(error))
{
Item.ServerId = result.MonitoredItemId;
Item.CurrentSamplingInterval =
TimeSpan.FromMilliseconds(result.RevisedSamplingInterval);
Item.CurrentQueueSize = result.RevisedQueueSize;
Item.LogRevisedSamplingRateAndQueueSize(Options, true);
Notify(error, true, result.FilterResult);
return;
}
// Declare final if not communication error which includes success
RetryCount++;
Notify(error, false, result.FilterResult);
}
/// <summary>
/// Updates the object with the results of a modify monitored item request.
/// </summary>
/// <param name="request"></param>
/// <param name="result"></param>
/// <param name="index"></param>
/// <param name="diagnosticInfos"></param>
/// <param name="responseHeader"></param>
internal void SetModifyResult(MonitoredItemModifyRequest request,
MonitoredItemModifyResult result, int index,
ArrayOf<DiagnosticInfo> diagnosticInfos, ResponseHeader responseHeader)
{
Debug.Assert(request.RequestedParameters.ClientHandle == Item.ClientHandle);
ServiceResult error = ServiceResult.Good;
if (StatusCode.IsBad(result.StatusCode))
{
error = ClientBase.GetResult(result.StatusCode, index,
diagnosticInfos, responseHeader);
}
if (ServiceResult.IsGood(error))
{
Item.CurrentSamplingInterval = TimeSpan.FromMilliseconds(
request.RequestedParameters.SamplingInterval);
Item.CurrentQueueSize = request.RequestedParameters.QueueSize;
Item.CurrentSamplingInterval = TimeSpan.FromMilliseconds(
result.RevisedSamplingInterval);
Item.CurrentQueueSize = result.RevisedQueueSize;
if (MonitoringModeChange == null)
{
Item.LogRevisedSamplingRateAndQueueSize(Options, false);
}
// Declare final
Notify(error, MonitoringModeChange == null, result.FilterResult);
return;
}
if (!IsCommunicationError(error))
{
// Do not apply the mode change but force a recreate
MonitoringModeChange = null;
Modify = null;
ForceRecreate = true;
}
if (MonitoringModeChange == null)
{
// Retry the modification request
RetryCount++;
}
Notify(error, false, result.FilterResult);
}
/// <summary>
/// Set monitoring mode result
/// </summary>
/// <param name="monitoringMode"></param>
/// <param name="statusCode"></param>
/// <param name="index"></param>
/// <param name="diagnosticInfos"></param>
/// <param name="responseHeader"></param>
/// <exception cref="NotImplementedException"></exception>
internal void SetMonitoringModeResult(MonitoringMode monitoringMode,
StatusCode statusCode, int index, ArrayOf<DiagnosticInfo> diagnosticInfos,
ResponseHeader responseHeader)
{
ServiceResult error = ServiceResult.Good;
if (StatusCode.IsBad(statusCode))
{
error = ClientBase.GetResult(statusCode, index, diagnosticInfos,
responseHeader);
}
if (ServiceResult.IsGood(error))
{
Item.CurrentMonitoringMode = monitoringMode;
Item.LogRevisedSamplingRateAndQueueSize(Options, false);
Notify(error, true);
return;
}
if (!IsCommunicationError(error))
{
// Reapply the mode change
Modify = null;
ForceRecreate = true;
}
// Retry
RetryCount++;
Notify(error, false);
}
/// <summary>
/// Set result of delete
/// </summary>
/// <param name="statusCode"></param>
/// <param name="index"></param>
/// <param name="diagnosticInfos"></param>
/// <param name="responseHeader"></param>
internal void SetDeleteResult(StatusCode statusCode, int index,
ArrayOf<DiagnosticInfo> diagnosticInfos, ResponseHeader responseHeader)
{
ServiceResult error = ServiceResult.Good;
if (StatusCode.IsBad(statusCode))
{
error = ClientBase.GetResult(statusCode, index, diagnosticInfos,
responseHeader);
}
bool final = Create == null && Modify == null;
if (ServiceResult.IsGood(error) ||
error.StatusCode == StatusCodes.BadMonitoredItemIdInvalid ||
final)
{
Item.ServerId = 0;
ForceRecreate = false;
// Now state is !Created so continue here to recreate
}
else
{
// Retry
RetryCount++;
}
Notify(error, final);
}
/// <summary>
/// Abandon the change
/// </summary>
internal void Abandon()
{
Notify(new ServiceResult(StatusCodes.BadOperationAbandoned), true);
}
/// <summary>
/// Notify and handle retries
/// </summary>
/// <param name="error"></param>
/// <param name="final"></param>
/// <param name="filterResultExtensionObject"></param>
private void Notify(ServiceResult error, bool final,
ExtensionObject? filterResultExtensionObject = null)
{
MonitoringFilterResult? filterResult = null;
if (filterResultExtensionObject.HasValue &&
filterResultExtensionObject.Value.TryGetValue(out MonitoringFilterResult? fr))
{
filterResult = fr;
}
bool stop = Item.Context.NotifyItemChangeResult(
Item, RetryCount, Options, error, final, filterResult);
if (final || stop)
{
Item.CompleteChange(this);
}
Item.Error = error;
Item.FilterResult = filterResult == null ? Item.FilterResult :
(MonitoringFilterResult)filterResult.Clone();
}
/// <summary>
/// Returns true if communication issue and not an error
/// in subscription or even success or uncertain states
/// </summary>
/// <param name="error"></param>
/// <returns></returns>
private static bool IsCommunicationError(ServiceResult error)
{
StatusCode code = error.StatusCode;
return code == StatusCodes.BadCommunicationError ||
code == StatusCodes.BadNotConnected ||
code == StatusCodes.BadSecureChannelClosed;
}
}
/// <summary>
/// Log revised sampling rate and queue size
/// </summary>
/// <param name="options"></param>
/// <param name="created"></param>
public void LogRevisedSamplingRateAndQueueSize(MonitoredItemOptions options,
bool created)
{
if (options.SamplingInterval != CurrentSamplingInterval &&
options.QueueSize != CurrentQueueSize &&
CurrentQueueSize != 0)
{
m_logger.LogInformation(
"{Item}: {Action} SamplingInterval was " +
"revised from {SamplingInterval} to {CurrentSamplingInterval} " +
"and QueueSize from {QueueSize} to {CurrentQueueSize}.",
this, created ? "CREATED" : "UPDATED",
options.SamplingInterval, CurrentSamplingInterval,
options.QueueSize, CurrentQueueSize);
}
else if (options.SamplingInterval != CurrentSamplingInterval)
{
m_logger.LogInformation(
"{Item}: {Action} SamplingInterval was " +
"revised from {SamplingInterval} to {CurrentSamplingInterval}.",
this, created ? "CREATED" : "UPDATED",
options.SamplingInterval, CurrentSamplingInterval);
}
else if (options.QueueSize != CurrentQueueSize && CurrentQueueSize != 0)
{
m_logger.LogInformation(
"{Item}: {Action} QueueSize was " +
"revised from {QueueSize} to {CurrentQueueSize}.",
this, created ? "CREATED" : "UPDATED",
options.QueueSize, CurrentQueueSize);
}
else
{
m_logger.LogDebug(
"{Item}: {Action} with desired configuration.",
this, created ? "CREATED" : "UPDATED");
}
}
private bool m_disposedValue;
private MonitoredItemOptions? m_currentOptions;
#pragma warning disable CA2213 // Disposed in DisposeAsync(bool)
private IDisposable? m_changeTracking;
#pragma warning restore CA2213
private readonly ConcurrentQueue<Change> m_pendingChanges = new();
private readonly ILogger m_logger;
internal static uint GlobalClientHandleUint;
private IOptionsMonitor<MonitoredItemOptions> m_options;
}
}