Files
UA-.NETStandard/tests/Opc.Ua.Interop.LegacyPeer/LegacyClientFeatures.cs
c03c128384 Treat code analysis (CA) warnings as errors and fix all surfaced analyzer warnings (#4680)
# Description

`TreatWarningsAsErrors` was already enabled, but
`CodeAnalysisTreatWarningsAsErrors` was `false`, so Microsoft Code
Analysis (`CAxxxx`) warnings were non-fatal. This PR sets it to `true`
in `common.props` and fixes all 55 analyzer warnings the net10.0 build
reported.

**Production code**
- `SamplingGroup.cs`: CA2213 is a false positive here because the
detached `CancellationTokenSource` is disposed by `CancelSamplingLoop`
once the loop completes. It is suppressed with a pragma, a justification
and a TODO, following the existing pattern in
`ServerBase.RequestQueue.cs`.

**Tests**
- CA1861: constant array arguments are moved to `static readonly` fields
or replaced with `ArrayOf<T>` collection expressions. In the legacy
interop server I used locals instead, because those arrays become node
values that writes can change in place.
- CA2025: tasks that finish before disposal (`Assert.ThrowsAsync`, or
awaited before `using` scope exit) are suppressed with justification
comments, following existing test patterns.
- CA1812: classes created by the DI container are suppressed with a
comment and a TODO.
- CA2208: benchmark switch defaults now throw
`InvalidOperationException`.
- CA1845: `Substring(0, 8)` is replaced with `[..8]`.
- CA1307: `string.Replace` is replaced with `Split`/`Join`, which also
works on net48.
- CA5394: `Random` is replaced with `RandomNumberGenerator`.

**Docs**
- `docs/DeveloperGuide.md` now says CA warnings also fail the build, and
that any suppression needs a comment explaining why plus a TODO.

**Validation**
- I fixed the 55 warnings from a net10.0 build of `UA.slnx`
(`CustomTestTarget=net10.0`). That build ran before the setting was
turned on, and I have not rebuilt net10.0 since the fixes.
- `CustomTestTarget=net48` build of `UA.slnx`: 0 warnings, 0 errors.
- I also started a full build across all target frameworks with the
setting on. Its log showed no errors or warnings while it ran, but I
couldn't read its final result.
- No tests have been run yet; CI should confirm the multi-TFM build and
the tests.

## Related Issues

- N/A

## 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.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [ ] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] 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: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>
Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
2026-10-06 18:54:28 +02:00

736 lines
36 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.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using Opc.Ua.Client;
namespace Opc.Ua.Interop.LegacyPeer
{
/// <summary>
/// The event, subscription, identity and service checks of the 1.5.x
/// client against a server with the Quickstarts reference server address
/// space (TestData and Alarms node managers included).
/// </summary>
public static partial class LegacyClientChecks
{
public const string AlarmsNamespace = "http://test.org/UA/Alarms/";
private static readonly TimeSpan s_eventWait = TimeSpan.FromSeconds(10);
private static readonly int[] s_partialInt32Array = [20, 30];
/// <summary>
/// Subscribes to BaseEventType events of the Server object and writes the
/// reference server's event trigger node, which reports one event.
/// </summary>
private static async Task EventSubscriptionAsync(ClientContext c)
{
var events = new List<EventFieldList>();
Subscription subscription = await CreateEventSubscriptionAsync(c, ObjectIds.Server, events).ConfigureAwait(false);
try
{
WriteResponse write = await c.Session
.WriteAsync(null, new WriteValueCollection { Write(c.Id("NodeIds_Events_TriggerNode01"), 1) }, c.Ct)
.ConfigureAwait(false);
Require(StatusCode.IsGood(write.Results[0]), "writing the event trigger returned " + write.Results[0]);
EventFieldList received = await WaitForEventAsync(events, e =>
(e.EventFields[4].Value as LocalizedText)?.Text?.Contains("Trigger event", StringComparison.Ordinal) == true).ConfigureAwait(false);
Require(received != null, $"no trigger event in {s_eventWait.TotalSeconds} s; received {events.Count} events");
Require(received.EventFields[0].Value is byte[] id && id.Length > 0, "the event has no EventId");
Require(received.EventFields[5].Value is ushort severity && severity > 0, "the event has no Severity");
}
finally
{
await DeleteSubscriptionAsync(c, subscription).ConfigureAwait(false);
}
}
/// <summary>
/// Calls ConditionRefresh for an event subscription and expects the
/// RefreshStartEvent and RefreshEndEvent pair.
/// </summary>
private static async Task ConditionRefreshAsync(ClientContext c)
{
var events = new List<EventFieldList>();
Subscription subscription = await CreateEventSubscriptionAsync(c, ObjectIds.Server, events).ConfigureAwait(false);
try
{
CallMethodResult result = await CallAsync(c, ObjectTypeIds.ConditionType,
MethodIds.ConditionType_ConditionRefresh, new Variant(subscription.Id)).ConfigureAwait(false);
Require(StatusCode.IsGood(result.StatusCode), "ConditionRefresh returned " + result.StatusCode);
EventFieldList end = await WaitForEventAsync(events,
e => Equals(e.EventFields[1].Value, ObjectTypeIds.RefreshEndEventType)).ConfigureAwait(false);
Require(end != null, "no RefreshEndEvent received");
Require(events.Any(e => Equals(e.EventFields[1].Value, ObjectTypeIds.RefreshStartEventType)),
"no RefreshStartEvent received");
}
finally
{
await DeleteSubscriptionAsync(c, subscription).ConfigureAwait(false);
}
}
/// <summary>
/// Starts the alarm simulation of the reference server, waits for an
/// unacknowledged condition event and acknowledges it.
/// </summary>
private static async Task AlarmAcknowledgeAsync(ClientContext c)
{
ushort alarmsNs = (ushort)c.Session.NamespaceUris.GetIndex(AlarmsNamespace);
Require(alarmsNs != ushort.MaxValue, "the Alarms namespace is missing");
var folder = new NodeId("Alarms", alarmsNs);
var events = new List<EventFieldList>();
Subscription subscription = await CreateEventSubscriptionAsync(c, folder, events, condition: true)
.ConfigureAwait(false);
try
{
CallMethodResult started = await CallAsync(c, folder, new NodeId("Alarms.Start", alarmsNs),
new Variant(30u)).ConfigureAwait(false);
Require(StatusCode.IsGood(started.StatusCode), "Alarms.Start returned " + started.StatusCode);
// Fields: 0 EventId, 1 EventType, 2 SourceNode, 3 Time, 4 Message,
// 5 Severity, 6 ConditionId, 7 AckedState/Id, 8 Retain.
EventFieldList unacked = await WaitForEventAsync(events, e =>
e.EventFields[6].Value is NodeId && Equals(e.EventFields[7].Value, false) &&
Equals(e.EventFields[8].Value, true), TimeSpan.FromSeconds(20)).ConfigureAwait(false);
if (unacked == null)
{
EventFieldList sample;
lock (events)
{
sample = events.LastOrDefault();
}
throw new InvalidOperationException(
$"no unacknowledged condition in 20 s; received {events.Count} events, last: " +
(sample == null ? "none" : string.Join(" | ", sample.EventFields.Select(f => $"{f.TypeInfo}:{f.Value}"))));
}
CallMethodResult ack = await CallAsync(c, (NodeId)unacked.EventFields[6].Value,
MethodIds.AcknowledgeableConditionType_Acknowledge,
new Variant((byte[])unacked.EventFields[0].Value),
new Variant(new LocalizedText("acknowledged by the interop test"))).ConfigureAwait(false);
Require(StatusCode.IsGood(ack.StatusCode), "Acknowledge returned " + ack.StatusCode);
}
finally
{
await CallAsync(c, folder, new NodeId("Alarms.End", alarmsNs)).ConfigureAwait(false);
await DeleteSubscriptionAsync(c, subscription).ConfigureAwait(false);
}
}
private static async Task<Subscription> CreateEventSubscriptionAsync(
ClientContext c,
NodeId notifier,
List<EventFieldList> events,
bool condition = false)
{
var subscription = new Subscription(c.Session.DefaultSubscription)
{
PublishingInterval = 100,
KeepAliveCount = 10,
LifetimeCount = 100
};
c.Session.AddSubscription(subscription);
await subscription.CreateAsync(c.Ct).ConfigureAwait(false);
var filter = new EventFilter();
filter.AddSelectClause(ObjectTypeIds.BaseEventType, BrowseNames.EventId);
filter.AddSelectClause(ObjectTypeIds.BaseEventType, BrowseNames.EventType);
filter.AddSelectClause(ObjectTypeIds.BaseEventType, BrowseNames.SourceNode);
filter.AddSelectClause(ObjectTypeIds.BaseEventType, BrowseNames.Time);
filter.AddSelectClause(ObjectTypeIds.BaseEventType, BrowseNames.Message);
filter.AddSelectClause(ObjectTypeIds.BaseEventType, BrowseNames.Severity);
if (condition)
{
filter.SelectClauses.Add(new SimpleAttributeOperand
{
TypeDefinitionId = ObjectTypeIds.ConditionType,
BrowsePath = [],
AttributeId = Attributes.NodeId
});
// AckedState/Id as a two element browse path.
filter.SelectClauses.Add(new SimpleAttributeOperand
{
TypeDefinitionId = ObjectTypeIds.AcknowledgeableConditionType,
BrowsePath = [new QualifiedName(BrowseNames.AckedState), new QualifiedName(BrowseNames.Id)],
AttributeId = Attributes.Value
});
filter.AddSelectClause(ObjectTypeIds.ConditionType, BrowseNames.Retain);
}
filter.WhereClause.Push(FilterOperator.OfType, new LiteralOperand(
new Variant(condition ? ObjectTypeIds.AcknowledgeableConditionType : ObjectTypeIds.BaseEventType)));
var item = new MonitoredItem(subscription.DefaultItem)
{
StartNodeId = notifier,
AttributeId = Attributes.EventNotifier,
SamplingInterval = 0,
QueueSize = 100,
Filter = filter
};
item.Notification += (_, e) =>
{
if (e.NotificationValue is EventFieldList fields)
{
lock (events)
{
events.Add(fields);
}
}
};
subscription.AddItem(item);
await subscription.ApplyChangesAsync(c.Ct).ConfigureAwait(false);
Require(ServiceResult.IsGood(item.Status.Error), "event monitored item " + item.Status.Error);
return subscription;
}
private static async Task<EventFieldList> WaitForEventAsync(
List<EventFieldList> events,
Func<EventFieldList, bool> match,
TimeSpan? timeout = null)
{
var deadline = DateTime.UtcNow + (timeout ?? s_eventWait);
while (DateTime.UtcNow < deadline)
{
lock (events)
{
EventFieldList found = events.FirstOrDefault(match);
if (found != null)
{
return found;
}
}
await Task.Delay(100).ConfigureAwait(false);
}
return null;
}
private static async Task DeleteSubscriptionAsync(ClientContext c, Subscription subscription)
{
try
{
await subscription.DeleteAsync(true, c.Ct).ConfigureAwait(false);
await c.Session.RemoveSubscriptionAsync(subscription, c.Ct).ConfigureAwait(false);
}
catch (ServiceResultException)
{
// The check already reported its outcome.
}
}
private static async Task<CallMethodResult> CallAsync(ClientContext c, NodeId objectId, NodeId methodId,
params Variant[] arguments)
{
CallResponse response = await c.Session.CallAsync(null, new CallMethodRequestCollection
{
new CallMethodRequest
{
ObjectId = objectId,
MethodId = methodId,
InputArguments = new VariantCollection(arguments)
}
}, c.Ct).ConfigureAwait(false);
return response.Results[0];
}
/// <summary>
/// Absolute and percent deadbands on the static AnalogItem
/// DataAccess_AnalogType_Double (EURange 0..100): changes inside the
/// deadband are not reported, larger ones are.
/// </summary>
private static async Task DeadbandFilterAsync(ClientContext c)
{
NodeId analog = c.Id("DataAccess_AnalogType_Double");
foreach (DeadbandType type in new[] { DeadbandType.Absolute, DeadbandType.Percent })
{
await WriteValueAsync(c, analog, 0.0).ConfigureAwait(false);
var values = new List<double>();
Subscription subscription = await CreateDataSubscriptionAsync(c, analog, values, item =>
{
item.Filter = new DataChangeFilter
{
Trigger = DataChangeTrigger.StatusValue,
DeadbandType = (uint)type,
DeadbandValue = 10 // 10 units, or 10 % of the EURange 0..100
};
}).ConfigureAwait(false);
try
{
await Task.Delay(500).ConfigureAwait(false);
foreach (double v in new[] { 5.0, 20.0, 25.0, 40.0 })
{
await WriteValueAsync(c, analog, v).ConfigureAwait(false);
await Task.Delay(400).ConfigureAwait(false);
}
await Task.Delay(800).ConfigureAwait(false);
double[] seen;
lock (values)
{
seen = [.. values];
}
string list = string.Join(", ", seen);
Require(seen.Contains(20.0) && seen.Contains(40.0), $"{type}: 20 and 40 not reported ({list})");
Require(!seen.Contains(5.0) && !seen.Contains(25.0), $"{type}: changes inside the deadband reported ({list})");
}
finally
{
await DeleteSubscriptionAsync(c, subscription).ConfigureAwait(false);
}
}
}
/// <summary>
/// A queue of 2 with DiscardOldest and a slow publishing interval: five
/// quick writes deliver the last two values, with the Overflow bit set.
/// </summary>
private static async Task QueueOverflowAsync(ClientContext c)
{
NodeId node = c.Id("Scalar_Static_Int32");
await WriteValueAsync(c, node, 0).ConfigureAwait(false);
var values = new List<DataValue>();
Subscription subscription = await CreateDataSubscriptionAsync(c, node, null, item =>
{
item.SamplingInterval = 0;
item.QueueSize = 2;
item.DiscardOldest = true;
}, publishingInterval: 2000, rawValues: values).ConfigureAwait(false);
try
{
// Let the initial value go out first.
await Task.Delay(2500).ConfigureAwait(false);
lock (values)
{
values.Clear();
}
for (int ii = 1; ii <= 5; ii++)
{
await WriteValueAsync(c, node, ii).ConfigureAwait(false);
}
await Task.Delay(3000).ConfigureAwait(false);
DataValue[] seen;
lock (values)
{
seen = [.. values];
}
string list = string.Join(", ", seen.Select(v => $"{v.Value}:{v.StatusCode}"));
Require(seen.Length == 2, $"expected the last 2 of 5 values, got {seen.Length} ({list})");
Require(Equals(seen[0].Value, 4) && Equals(seen[1].Value, 5), $"expected 4 and 5, got {list}");
Require(seen.Any(v => v.StatusCode.Overflow), $"no value carries the Overflow bit ({list})");
}
finally
{
await DeleteSubscriptionAsync(c, subscription).ConfigureAwait(false);
}
}
/// <summary>
/// A sampling item linked to a reporting item by SetTriggering reports
/// only when the triggering item reports.
/// </summary>
private static async Task TriggeringAsync(ClientContext c)
{
var subscription = new Subscription(c.Session.DefaultSubscription) { PublishingInterval = 100 };
c.Session.AddSubscription(subscription);
await subscription.CreateAsync(c.Ct).ConfigureAwait(false);
try
{
int linkedReports = 0;
var trigger = new MonitoredItem(subscription.DefaultItem)
{
StartNodeId = c.Id("Scalar_Static_Int32"),
SamplingInterval = 50,
MonitoringMode = MonitoringMode.Reporting
};
var linked = new MonitoredItem(subscription.DefaultItem)
{
StartNodeId = c.Id("Scalar_Static_String"),
SamplingInterval = 50,
MonitoringMode = MonitoringMode.Sampling
};
linked.Notification += (_, e) =>
{
if (e.NotificationValue is MonitoredItemNotification)
{
Interlocked.Increment(ref linkedReports);
}
};
subscription.AddItem(trigger);
subscription.AddItem(linked);
await subscription.ApplyChangesAsync(c.Ct).ConfigureAwait(false);
SetTriggeringResponse set = await c.Session.SetTriggeringAsync(null, subscription.Id,
trigger.Status.Id, new UInt32Collection { linked.Status.Id }, new UInt32Collection(), c.Ct).ConfigureAwait(false);
Require(StatusCode.IsGood(set.AddResults[0]), "SetTriggering returned " + set.AddResults[0]);
await Task.Delay(500).ConfigureAwait(false);
int before = Volatile.Read(ref linkedReports);
await WriteValueAsync(c, c.Id("Scalar_Static_String"), "linked " + Guid.NewGuid()).ConfigureAwait(false);
await Task.Delay(500).ConfigureAwait(false);
Require(Volatile.Read(ref linkedReports) == before, "the sampling item reported without its trigger");
await WriteValueAsync(c, c.Id("Scalar_Static_Int32"), Environment.TickCount).ConfigureAwait(false);
await Task.Delay(1000).ConfigureAwait(false);
Require(Volatile.Read(ref linkedReports) > before, "the triggered item did not report");
}
finally
{
await DeleteSubscriptionAsync(c, subscription).ConfigureAwait(false);
}
}
/// <summary>
/// Republish of a sequence number the subscription never sent returns
/// BadMessageNotAvailable, and of an unknown subscription
/// BadSubscriptionIdInvalid. (Republishing a sent message is racy: the
/// client acknowledges it with its next Publish and the server may drop
/// acknowledged messages.)
/// </summary>
private static async Task RepublishAsync(ClientContext c)
{
NodeId node = c.Id("Scalar_Static_Int32");
Subscription subscription = await CreateDataSubscriptionAsync(c, node, null, _ => { }).ConfigureAwait(false);
try
{
StatusCode notSent = await RepublishStatusAsync(c, subscription.Id, subscription.SequenceNumber + 1000)
.ConfigureAwait(false);
Require(notSent == StatusCodes.BadMessageNotAvailable,
"Republish of an unsent message returned " + StatusCodes.GetBrowseName(notSent.Code));
StatusCode unknown = await RepublishStatusAsync(c, subscription.Id + 100_000, 1).ConfigureAwait(false);
Require(unknown == StatusCodes.BadSubscriptionIdInvalid,
"Republish of an unknown subscription returned " + StatusCodes.GetBrowseName(unknown.Code));
}
finally
{
await DeleteSubscriptionAsync(c, subscription).ConfigureAwait(false);
}
}
private static async Task<StatusCode> RepublishStatusAsync(ClientContext c, uint subscriptionId, uint sequence)
{
try
{
RepublishResponse response = await c.Session.RepublishAsync(null, subscriptionId, sequence, c.Ct)
.ConfigureAwait(false);
return response.ResponseHeader.ServiceResult;
}
catch (ServiceResultException sre)
{
return sre.StatusCode;
}
}
/// <summary>
/// Transfers a subscription to a second session of the same user, which
/// then receives its data changes.
/// </summary>
private static async Task TransferSubscriptionAsync(ClientContext c)
{
NodeId node = c.Id("Scalar_Static_Int32");
Subscription subscription = await CreateDataSubscriptionAsync(c, node, null, _ => { }).ConfigureAwait(false);
ISession target = await c.NewSessionAsync(c.Session.Identity).ConfigureAwait(false);
try
{
SubscriptionCollection transferred = new SubscriptionCollection { subscription }.CloneSubscriptions(false);
foreach (Subscription s in transferred)
{
target.AddSubscription(s);
}
int notifications = 0;
foreach (MonitoredItem item in transferred[0].MonitoredItems)
{
item.Notification += (_, _) => Interlocked.Increment(ref notifications);
}
bool ok = await target.TransferSubscriptionsAsync(transferred, true, c.Ct).ConfigureAwait(false);
Require(ok, "TransferSubscriptions failed");
await Task.Delay(300).ConfigureAwait(false);
int before = Volatile.Read(ref notifications);
WriteResponse write = await target.WriteAsync(null,
new WriteValueCollection { Write(node, Environment.TickCount) }, c.Ct).ConfigureAwait(false);
Require(StatusCode.IsGood(write.Results[0]), "write returned " + write.Results[0]);
var deadline = DateTime.UtcNow + s_eventWait;
while (Volatile.Read(ref notifications) == before && DateTime.UtcNow < deadline)
{
await Task.Delay(100).ConfigureAwait(false);
}
Require(Volatile.Read(ref notifications) > before, "the transferred subscription reported nothing");
}
finally
{
await DeleteSubscriptionAsync(c, subscription).ConfigureAwait(false);
target.DeleteSubscriptionsOnClose = true;
await target.CloseAsync(c.Ct).ConfigureAwait(false);
target.Dispose();
}
}
private static async Task<Subscription> CreateDataSubscriptionAsync(
ClientContext c,
NodeId node,
List<double> values,
Action<MonitoredItem> configure,
int publishingInterval = 100,
List<DataValue> rawValues = null)
{
var subscription = new Subscription(c.Session.DefaultSubscription)
{
PublishingInterval = publishingInterval,
KeepAliveCount = 10,
LifetimeCount = 100
};
c.Session.AddSubscription(subscription);
await subscription.CreateAsync(c.Ct).ConfigureAwait(false);
var item = new MonitoredItem(subscription.DefaultItem)
{
StartNodeId = node,
SamplingInterval = 50,
QueueSize = 10
};
configure(item);
item.Notification += (_, e) =>
{
if (e.NotificationValue is MonitoredItemNotification n)
{
if (values != null && n.Value.Value is IConvertible)
{
lock (values)
{
values.Add(Convert.ToDouble(n.Value.Value, System.Globalization.CultureInfo.InvariantCulture));
}
}
if (rawValues != null)
{
lock (rawValues)
{
rawValues.Add(n.Value);
}
}
}
};
subscription.AddItem(item);
await subscription.ApplyChangesAsync(c.Ct).ConfigureAwait(false);
Require(ServiceResult.IsGood(item.Status.Error), "monitored item " + item.Status.Error);
return subscription;
}
private static async Task WriteValueAsync(ClientContext c, NodeId node, object value)
{
WriteResponse response = await c.Session.WriteAsync(null, new WriteValueCollection { Write(node, value) }, c.Ct)
.ConfigureAwait(false);
Require(StatusCode.IsGood(response.Results[0]), $"write of {node} returned {response.Results[0]}");
}
/// <summary>
/// A second session with a wrong password is rejected.
/// </summary>
private static async Task WrongPasswordRejectedAsync(ClientContext c)
{
try
{
ISession session = await c.NewSessionAsync(
new UserIdentity("user1", System.Text.Encoding.UTF8.GetBytes("wrong password"))).ConfigureAwait(false);
await session.CloseAsync(c.Ct).ConfigureAwait(false);
session.Dispose();
}
catch (ServiceResultException sre)
{
Require(sre.StatusCode == StatusCodes.BadUserAccessDenied || sre.StatusCode == StatusCodes.BadIdentityTokenRejected,
"the wrong password was rejected with " + StatusCodes.GetBrowseName(sre.StatusCode));
return;
}
throw new InvalidOperationException("a session with a wrong password was activated");
}
/// <summary>
/// A second session with an X509 user identity token (a self-signed
/// user certificate) reads the server state.
/// </summary>
private static async Task X509UserTokenAsync(ClientContext c)
{
X509Certificate2 user = CertificateFactory
.CreateCertificate("CN=InteropUser, O=OPC Foundation")
.SetRSAKeySize(2048)
.CreateForRSA();
ISession session = await c.NewSessionAsync(new UserIdentity(user)).ConfigureAwait(false);
try
{
Require(session.Identity.TokenType == UserTokenType.Certificate,
"the session identity is " + session.Identity.TokenType);
DataValue state = await session.ReadValueAsync(VariableIds.Server_ServerStatus_State, c.Ct).ConfigureAwait(false);
Require(StatusCode.IsGood(state.StatusCode), "reading with the X509 user returned " + state.StatusCode);
}
finally
{
await session.CloseAsync(c.Ct).ConfigureAwait(false);
session.Dispose();
}
}
/// <summary>
/// Registers two nodes, reads through the registered ids, unregisters.
/// </summary>
private static async Task RegisterNodesAsync(ClientContext c)
{
var nodes = new NodeIdCollection { c.Id("Scalar_Static_Int32"), c.Id("Scalar_Static_String") };
RegisterNodesResponse registered = await c.Session.RegisterNodesAsync(null, nodes, c.Ct).ConfigureAwait(false);
Require(registered.RegisteredNodeIds.Count == 2, "RegisterNodes returned " + registered.RegisteredNodeIds.Count + " ids");
ReadResponse read = await c.Session.ReadAsync(null, 0, TimestampsToReturn.Neither,
new ReadValueIdCollection(registered.RegisteredNodeIds.Select(Value)), c.Ct).ConfigureAwait(false);
Require(read.Results.All(r => StatusCode.IsGood(r.StatusCode)),
"reading registered nodes returned " + string.Join(", ", read.Results.Select(r => r.StatusCode)));
await c.Session.UnregisterNodesAsync(null, registered.RegisteredNodeIds, c.Ct).ConfigureAwait(false);
}
/// <summary>
/// Reads the raw history of a historizing variable of the last 3 hours.
/// </summary>
private static async Task HistoryReadRawAsync(ClientContext c)
{
var details = new ReadRawModifiedDetails
{
StartTime = DateTime.UtcNow.AddHours(-3),
EndTime = DateTime.UtcNow,
NumValuesPerNode = 10,
ReturnBounds = false
};
HistoryReadResponse response = await c.Session.HistoryReadAsync(null, new ExtensionObject(details),
TimestampsToReturn.Source, false,
new HistoryReadValueIdCollection { new HistoryReadValueId { NodeId = c.Id("Scalar_Static_Double") } }, c.Ct)
.ConfigureAwait(false);
HistoryReadResult result = response.Results[0];
Require(StatusCode.IsGood(result.StatusCode), "HistoryRead returned " + result.StatusCode);
var data = ExtensionObject.ToEncodeable(result.HistoryData) as HistoryData;
Require(data != null && data.DataValues.Count > 0, "HistoryRead returned no values");
Require(data.DataValues.Count <= 10, $"{data.DataValues.Count} values despite NumValuesPerNode 10");
if (result.ContinuationPoint?.Length > 0)
{
await c.Session.HistoryReadAsync(null, new ExtensionObject(details), TimestampsToReturn.Source, true,
new HistoryReadValueIdCollection
{
new HistoryReadValueId { NodeId = c.Id("Scalar_Static_Double"), ContinuationPoint = result.ContinuationPoint }
}, c.Ct).ConfigureAwait(false);
}
}
/// <summary>
/// Adds an object below the Objects folder, finds it by browsing and
/// deletes it again.
/// </summary>
private static async Task NodeManagementAsync(ClientContext c)
{
string name = "InteropAdded_" + Guid.NewGuid().ToString("N")[..8];
var requested = new NodeId(name, c.Ns);
AddNodesResponse added = await c.Session.AddNodesAsync(null, new AddNodesItemCollection
{
new AddNodesItem
{
ParentNodeId = ObjectIds.ObjectsFolder,
ReferenceTypeId = ReferenceTypeIds.Organizes,
RequestedNewNodeId = requested,
BrowseName = new QualifiedName(name, c.Ns),
NodeClass = NodeClass.Object,
NodeAttributes = new ExtensionObject(new ObjectAttributes
{
DisplayName = new LocalizedText(name),
SpecifiedAttributes = (uint)NodeAttributesMask.DisplayName
}),
TypeDefinition = ObjectTypeIds.BaseObjectType
}
}, c.Ct).ConfigureAwait(false);
Require(StatusCode.IsGood(added.Results[0].StatusCode), "AddNodes returned " + added.Results[0].StatusCode);
NodeId nodeId = added.Results[0].AddedNodeId;
ReferenceDescriptionCollection refs = await BrowseAsync(c, ObjectIds.ObjectsFolder, 0).ConfigureAwait(false);
Require(refs.Any(r => r.BrowseName.Name == name), "the added node is not organized by the Objects folder");
DeleteNodesResponse deleted = await c.Session.DeleteNodesAsync(null, new DeleteNodesItemCollection
{
new DeleteNodesItem { NodeId = nodeId, DeleteTargetReferences = true }
}, c.Ct).ConfigureAwait(false);
Require(StatusCode.IsGood(deleted.Results[0]), "DeleteNodes returned " + deleted.Results[0]);
}
/// <summary>
/// Writes and reads parts of an array with an IndexRange.
/// </summary>
private static async Task IndexRangeAsync(ClientContext c)
{
NodeId node = c.Id("Scalar_Static_Arrays_Int32");
await WriteValueAsync(c, node, Enumerable.Range(0, 10).ToArray()).ConfigureAwait(false);
WriteResponse write = await c.Session.WriteAsync(null, new WriteValueCollection
{
new WriteValue
{
NodeId = node,
AttributeId = Attributes.Value,
IndexRange = "2:3",
Value = new DataValue(new Variant(s_partialInt32Array))
}
}, c.Ct).ConfigureAwait(false);
Require(StatusCode.IsGood(write.Results[0]), "writing the index range 2:3 returned " + write.Results[0]);
ReadResponse read = await c.Session.ReadAsync(null, 0, TimestampsToReturn.Neither, new ReadValueIdCollection
{
new ReadValueId { NodeId = node, AttributeId = Attributes.Value, IndexRange = "1:4" }
}, c.Ct).ConfigureAwait(false);
Require(StatusCode.IsGood(read.Results[0].StatusCode), "reading the index range 1:4 returned " + read.Results[0].StatusCode);
Require(read.Results[0].Value is int[] part && part.SequenceEqual(new[] { 1, 20, 30, 4 }),
"index range 1:4 read " + string.Join(",", (read.Results[0].Value as int[]) ?? []));
}
/// <summary>
/// FindServers lists the server and GetEndpoints offers the endpoint
/// of this session.
/// </summary>
private static async Task FindServersAsync(ClientContext c)
{
using DiscoveryClient client = await DiscoveryClient
.CreateAsync(c.Config, new Uri(c.Url), DiagnosticsMasks.None, c.Ct)
.ConfigureAwait(false);
ApplicationDescriptionCollection servers = await client.FindServersAsync(null, c.Ct).ConfigureAwait(false);
string serverUri = c.Session.Endpoint.Server.ApplicationUri;
Require(servers.Any(s => s.ApplicationUri == serverUri),
$"FindServers does not list {serverUri}: {string.Join(", ", servers.Select(s => s.ApplicationUri))}");
EndpointDescriptionCollection endpoints = await client.GetEndpointsAsync(null, c.Ct).ConfigureAwait(false);
Require(endpoints.Any(e => e.SecurityPolicyUri == c.Session.Endpoint.SecurityPolicyUri &&
e.SecurityMode == c.Session.Endpoint.SecurityMode),
"GetEndpoints does not offer the session's endpoint");
}
/// <summary>
/// Reconnects the session on a new secure channel (ActivateSession on
/// the new channel) and keeps reading.
/// </summary>
private static async Task SessionReconnectAsync(ClientContext c)
{
NodeId before = c.Session.SessionId;
await ((Session)c.Session).ReconnectAsync(c.Ct).ConfigureAwait(false);
Require(c.Session.SessionId == before, $"the session id changed from {before} to {c.Session.SessionId}");
DataValue state = await c.Session.ReadValueAsync(VariableIds.Server_ServerStatus_State, c.Ct).ConfigureAwait(false);
Require(StatusCode.IsGood(state.StatusCode), "reading after the reconnect returned " + state.StatusCode);
}
}
}