/* ========================================================================
* 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;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using Microsoft.Extensions.Logging;
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Client.ComplexTypes;
namespace Quickstarts
{
///
/// A client interface which holds an active session.
/// The client handler may reconnect and the Session
/// property may be updated during operation.
///
public interface IUAClient
{
///
/// The session to use.
///
ISession Session { get; }
}
///
/// Sample Session calls based on the reference server node model.
///
public class ClientSamples
{
private const int kMaxSearchDepth = 128;
public ClientSamples(
ITelemetryContext telemetry,
Action? validateResponse,
ManualResetEvent? quitEvent = null,
bool verbose = false)
{
m_telemetry = telemetry;
m_logger = telemetry.CreateLogger();
m_validate = validateResponse;
m_quitEvent = quitEvent;
m_verbose = verbose;
m_desiredEventFields = [];
int eventIndexCounter = 0;
m_desiredEventFields.Add(
eventIndexCounter++,
[QualifiedName.From(BrowseNames.Time)]);
m_desiredEventFields.Add(
eventIndexCounter++,
[QualifiedName.From(BrowseNames.ActiveState)]);
m_desiredEventFields.Add(
eventIndexCounter++,
[QualifiedName.From(BrowseNames.Message)]);
m_desiredEventFields.Add(
eventIndexCounter++,
[QualifiedName.From(BrowseNames.LimitState), QualifiedName.From(BrowseNames.CurrentState)]);
m_desiredEventFields.Add(
eventIndexCounter++,
[QualifiedName.From(BrowseNames.LimitState), QualifiedName.From(BrowseNames.LastTransition)]);
}
///
/// Read a list of nodes from Server
///
public async Task ReadNodesAsync(ISession session, CancellationToken ct = default)
{
if (session == null || !session.Connected)
{
Console.WriteLine("Session not connected!");
return;
}
try
{
// build a list of nodes to be read
ArrayOf nodesToRead =
[
// Value of ServerStatus
new ReadValueId {
NodeId = VariableIds.Server_ServerStatus,
AttributeId = Attributes.Value },
// BrowseName of ServerStatus_StartTime
new ReadValueId
{
NodeId = VariableIds.Server_ServerStatus_StartTime,
AttributeId = Attributes.BrowseName
},
// Value of ServerStatus_StartTime
new ReadValueId
{
NodeId = VariableIds.Server_ServerStatus_StartTime,
AttributeId = Attributes.Value
}
];
// Read the node attributes
Console.WriteLine("Reading nodes...");
// Call Read Service
ReadResponse response = await session.ReadAsync(
null,
0,
TimestampsToReturn.Both,
nodesToRead,
ct).ConfigureAwait(false);
ArrayOf resultsValues = response.Results;
ArrayOf diagnosticInfos = response.DiagnosticInfos;
// Validate the results
ValidateResponse(resultsValues, nodesToRead);
// Display the results.
foreach (DataValue result in resultsValues)
{
Console.WriteLine($"Read Value = {result.WrappedValue} , StatusCode = {result.StatusCode}");
}
// Read Server NamespaceArray
Console.WriteLine("Reading Value of NamespaceArray node...");
DataValue namespaceArray = await session.ReadValueAsync(VariableIds.Server_NamespaceArray, ct)
.ConfigureAwait(false);
// Display the result
Console.WriteLine($"NamespaceArray Value = {namespaceArray}");
}
catch (Exception ex)
{
// Log Error
m_logger.LogError(ex, "Read Nodes Error.");
}
}
///
/// Write a list of nodes to the Server.
///
public async Task WriteNodesAsync(ISession session, CancellationToken ct = default)
{
if (session == null || !session.Connected)
{
Console.WriteLine("Session not connected!");
return;
}
try
{
// Write the configured nodes
ArrayOf nodesToWrite =
[
// Int32 Node - Objects\CTT\Scalar\Scalar_Static\Int32
new WriteValue
{
NodeId = NodeId.Parse("ns=2;s=Scalar_Static_Int32"),
AttributeId = Attributes.Value,
Value = new DataValue(Variant.From(100))
},
// Float Node - Objects\CTT\Scalar\Scalar_Static\Float
new WriteValue
{
NodeId = NodeId.Parse("ns=2;s=Scalar_Static_Float"),
AttributeId = Attributes.Value,
Value = new DataValue(Variant.From(100.5f))
},
// String Node - Objects\CTT\Scalar\Scalar_Static\String
new WriteValue
{
NodeId = NodeId.Parse("ns=2;s=Scalar_Static_String"),
AttributeId = Attributes.Value,
Value = new DataValue(Variant.From("String Test"))
}
];
// Write the node attributes
Console.WriteLine("Writing nodes...");
// Call Write Service
WriteResponse response = await session.WriteAsync(
null,
nodesToWrite,
ct).ConfigureAwait(false);
ArrayOf results = response.Results;
ArrayOf diagnosticInfos = response.DiagnosticInfos;
// Validate the response
ValidateResponse(results, nodesToWrite);
// Display the results.
Console.WriteLine("Write Results :");
foreach (StatusCode writeResult in results)
{
Console.WriteLine($" {writeResult}");
}
}
catch (Exception ex)
{
// Log Error
m_logger.LogInformation(ex, "Write Nodes Error.");
}
}
///
/// Browse Server nodes
///
public async Task BrowseAsync(ISession session, CancellationToken ct = default)
{
if (session == null || !session.Connected)
{
Console.WriteLine("Session not connected!");
return;
}
try
{
// Create a Browser object
var browser = new Browser(session)
{
// Set browse parameters
BrowseDirection = BrowseDirection.Forward,
NodeClassMask = (int)NodeClass.Object | (int)NodeClass.Variable,
ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences,
IncludeSubtypes = true
};
NodeId nodeToBrowse = ObjectIds.Server;
// Call Browse service
Console.WriteLine($"Browsing {nodeToBrowse} node...");
ArrayOf browseResults =
await browser.BrowseAsync(nodeToBrowse, ct).ConfigureAwait(false);
// Display the results
Console.WriteLine($"Browse returned {browseResults.Count} results:");
foreach (ReferenceDescription result in browseResults)
{
Console.WriteLine($" DisplayName = {result.DisplayName.Text}, NodeClass = {result.NodeClass}");
}
}
catch (Exception ex)
{
// Log Error
m_logger.LogError(ex, "Browse Error.");
}
}
///
/// Call UA method
///
public async Task CallMethodAsync(ISession session, CancellationToken ct = default)
{
if (session == null || !session.Connected)
{
Console.WriteLine("Session not connected!");
return;
}
try
{
// Define the UA Method to call
// Parent node - Objects\CTT\Methods
// Method node - Objects\CTT\Methods\Add
var objectId = NodeId.Parse("ns=2;s=Methods");
var methodId = NodeId.Parse("ns=2;s=Methods_Add");
// Define the method parameters
// Input argument requires a Float and an UInt32 value
// Invoke Call service
Console.WriteLine($"Calling UAMethod for node {methodId} ...");
ArrayOf outputArguments = await session.CallAsync(
objectId,
methodId,
ct,
(float)10.5,
(uint)10).ConfigureAwait(false);
// Display results
Console.WriteLine($"Method call returned {outputArguments.Count} output argument(s):");
foreach (Variant outputArgument in outputArguments)
{
Console.WriteLine($" OutputValue = {outputArgument}");
}
}
catch (Exception ex)
{
m_logger.LogError(ex, "Method call error");
}
}
///
/// Call the Start method for Alarming to enable events
///
public async Task EnableEventsAsync(
ISession session,
uint timeToRun,
CancellationToken ct = default)
{
if (session == null || !session.Connected)
{
Console.WriteLine("Session not connected!");
return;
}
try
{
// Define the UA Method to call
// Parent node - Objects\CTT\Alarms
// Method node - Objects\CTT\Alarms\Start
var objectId = NodeId.Parse("ns=7;s=Alarms");
var methodId = NodeId.Parse("ns=7;s=Alarms.Start");
// Define the method parameters
// Input argument requires a Float and an UInt32 value
// Invoke Call service
Console.WriteLine($"Calling UAMethod for node {methodId} ...");
ArrayOf outputArguments = await session.CallAsync(
objectId,
methodId,
ct,
timeToRun).ConfigureAwait(false);
// Display results
Console.WriteLine($"Method call returned {outputArguments.Count} output argument(s):");
foreach (Variant outputArgument in outputArguments)
{
Console.WriteLine($" OutputValue = {outputArgument}");
}
}
catch (Exception ex)
{
m_logger.LogError(ex, "Method call error");
}
}
///
/// Create Subscription and MonitoredItems for DataChanges
///
public async Task SubscribeToDataChangesAsync(
ISession session,
uint minLifeTime,
bool enableDurableSubscriptions,
CancellationToken ct = default)
{
bool isDurable = false;
if (session == null || !session.Connected)
{
Console.WriteLine("Session not connected!");
return isDurable;
}
try
{
// Create a subscription for receiving data change notifications
const int subscriptionPublishingInterval = 1000;
const int itemSamplingInterval = 1000;
uint queueSize = 10;
uint lifetime = minLifeTime;
if (enableDurableSubscriptions)
{
queueSize = 100;
lifetime = 20;
}
// Define Subscription parameters
var subscription = new Subscription(session.DefaultSubscription)
{
DisplayName = "Console ReferenceClient Subscription",
PublishingEnabled = true,
PublishingInterval = subscriptionPublishingInterval,
LifetimeCount = 0,
MinLifetimeInterval = lifetime,
KeepAliveCount = 5
};
session.AddSubscription(subscription);
// Create the subscription on Server side
await subscription.CreateAsync(ct).ConfigureAwait(false);
m_logger.LogInformation(
"New Subscription created with SubscriptionId = {Id}, Sampling Interval {SamplingInterval}, Publishing Interval {PublishingInterval}.",
subscription.Id,
itemSamplingInterval,
subscriptionPublishingInterval);
if (enableDurableSubscriptions)
{
(bool success, uint revisedLifetimeInHours) =
await subscription.SetSubscriptionDurableAsync(1, ct).ConfigureAwait(false);
if (success)
{
isDurable = true;
m_logger.LogInformation(
"Subscription {SubscriptionId} is now durable, Revised Lifetime {Lifetime} in hours.",
subscription.Id,
revisedLifetimeInHours);
}
else
{
m_logger.LogInformation("Subscription {SubscriptionId} failed durable call", subscription.Id);
}
}
// Create MonitoredItems for data changes (Reference Server)
var intMonitoredItem = new MonitoredItem(subscription.DefaultItem)
{
// Int32 Node - Objects\CTT\Scalar\Simulation\Int32
StartNodeId = NodeId.Parse("ns=2;s=Scalar_Simulation_Int32"),
AttributeId = Attributes.Value,
DisplayName = "Int32 Variable",
SamplingInterval = itemSamplingInterval,
QueueSize = queueSize,
DiscardOldest = true
};
intMonitoredItem.Notification += OnMonitoredItemNotification;
subscription.AddItem(intMonitoredItem);
var floatMonitoredItem = new MonitoredItem(subscription.DefaultItem)
{
// Float Node - Objects\CTT\Scalar\Simulation\Float
StartNodeId = NodeId.Parse("ns=2;s=Scalar_Simulation_Float"),
AttributeId = Attributes.Value,
DisplayName = "Float Variable",
SamplingInterval = itemSamplingInterval,
QueueSize = queueSize
};
floatMonitoredItem.Notification += OnMonitoredItemNotification;
subscription.AddItem(floatMonitoredItem);
var stringMonitoredItem = new MonitoredItem(subscription.DefaultItem)
{
// String Node - Objects\CTT\Scalar\Simulation\String
StartNodeId = NodeId.Parse("ns=2;s=Scalar_Simulation_String"),
AttributeId = Attributes.Value,
DisplayName = "String Variable",
SamplingInterval = itemSamplingInterval,
QueueSize = queueSize
};
stringMonitoredItem.Notification += OnMonitoredItemNotification;
subscription.AddItem(stringMonitoredItem);
var eventMonitoredItem = new MonitoredItem(subscription.DefaultItem)
{
StartNodeId = ObjectIds.Server,
AttributeId = Attributes.EventNotifier,
DisplayName = "Event Variable",
SamplingInterval = itemSamplingInterval,
QueueSize = queueSize
};
eventMonitoredItem.Notification += OnMonitoredItemEventNotification;
var filter = new EventFilter();
var simpleAttributeOperands = new List();
foreach (ArrayOf desiredEventField in m_desiredEventFields.Values)
{
simpleAttributeOperands.Add(
new SimpleAttributeOperand
{
AttributeId = Attributes.Value,
TypeDefinitionId = ObjectTypeIds.BaseEventType,
BrowsePath = desiredEventField
});
}
filter.SelectClauses = simpleAttributeOperands;
var whereClause = new ContentFilter();
var existingEventType = new SimpleAttributeOperand
{
AttributeId = Attributes.Value,
TypeDefinitionId = ObjectTypeIds.ExclusiveLevelAlarmType,
BrowsePath = [QualifiedName.From("EventType")]
};
var desiredEventType = new LiteralOperand
{
Value = Variant.From(ObjectTypeIds.ExclusiveLevelAlarmType)
};
whereClause.Push(
FilterOperator.Equals,
Variant.FromStructure(existingEventType),
Variant.FromStructure(desiredEventType));
filter.WhereClause = whereClause;
eventMonitoredItem.Filter = filter;
eventMonitoredItem.NodeClass = NodeClass.Object;
subscription.AddItem(eventMonitoredItem);
// Create the monitored items on Server side
await subscription.ApplyChangesAsync(ct).ConfigureAwait(false);
m_logger.LogInformation(
"MonitoredItems created for SubscriptionId = {SubscriptionId}.",
subscription.Id);
}
catch (Exception ex)
{
m_logger.LogError(ex, "Subscribe error");
}
return isDurable;
}
///
/// Fetch all references and nodes with attributes except values from the server.
///
/// The UAClient with a session to use.
/// The node from which the hierarchical nodes are fetched.
/// Iterate to fetch all nodes in the tree.
/// Adds the root node to the result.
/// Filters nodes from namespace 0 from the result.
/// The list of nodes on the server.
public async Task> FetchAllNodesNodeCacheAsync(
IUAClient uaClient,
NodeId startingNode,
bool fetchTree = false,
bool addRootNode = false,
bool filterUATypes = true,
bool clearNodeCache = true,
CancellationToken ct = default)
{
var stopwatch = new Stopwatch();
var nodeDictionary = new Dictionary();
ArrayOf references = [ReferenceTypeIds.HierarchicalReferences];
ArrayOf nodesToBrowse = [startingNode];
// start
stopwatch.Start();
if (clearNodeCache)
{
// clear NodeCache to fetch all nodes from server
uaClient.Session.NodeCache.Clear();
await FetchReferenceIdTypesAsync(uaClient.Session, ct).ConfigureAwait(false);
}
// add root node
if (addRootNode)
{
INode? rootNode = await uaClient.Session.NodeCache.FindAsync(startingNode, ct)
.ConfigureAwait(false);
if (rootNode == null)
{
m_logger.LogWarning("Root node {NodeId} not found, skipping", startingNode);
}
else
{
nodeDictionary[rootNode.NodeId] = rootNode;
}
}
int searchDepth = 0;
while (nodesToBrowse.Count > 0 && searchDepth < kMaxSearchDepth)
{
if (m_quitEvent?.WaitOne(0) == true)
{
m_logger.LogInformation("Browse aborted.");
break;
}
searchDepth++;
m_logger.LogInformation(
"{Depth}: Find {Count} references after {Duration}ms",
searchDepth,
nodesToBrowse.Count,
stopwatch.ElapsedMilliseconds);
ArrayOf response = await uaClient
.Session.NodeCache.FindReferencesAsync(nodesToBrowse, references, false, true, ct)
.ConfigureAwait(false);
var nextNodesToBrowse = new List();
int duplicates = 0;
int leafNodes = 0;
foreach (INode node in response)
{
if (!nodeDictionary.ContainsKey(node.NodeId))
{
if (fetchTree)
{
bool leafNode = false;
// no need to browse property types
if (node is VariableNode variableNode)
{
IReference? hasTypeDefinition = variableNode.ReferenceTable
.FirstOrDefault(r =>
r.ReferenceTypeId
.Equals(ReferenceTypeIds.HasTypeDefinition));
if (hasTypeDefinition != null)
{
leafNode = hasTypeDefinition.TargetId == VariableTypeIds
.PropertyType;
}
}
if (!leafNode)
{
nextNodesToBrowse.Add(node.NodeId);
}
else
{
leafNodes++;
}
}
if (filterUATypes)
{
if (node.NodeId.NamespaceIndex != 0)
{
// filter out default namespace
nodeDictionary[node.NodeId] = node;
}
}
else
{
nodeDictionary[node.NodeId] = node;
}
}
else
{
duplicates++;
}
}
if (duplicates > 0)
{
m_logger.LogInformation("Find References {Count} duplicate nodes were ignored", duplicates);
}
if (leafNodes > 0)
{
m_logger.LogInformation("Find References {Count} leaf nodes were ignored", leafNodes);
}
nodesToBrowse = nextNodesToBrowse;
}
stopwatch.Stop();
m_logger.LogInformation(
"FetchAllNodesNodeCache found {Count} nodes in {Duration}ms",
nodeDictionary.Count,
stopwatch.ElapsedMilliseconds);
var result = nodeDictionary.Values.ToList();
result.Sort((x, y) => x.NodeId.CompareTo(y.NodeId));
if (m_verbose)
{
foreach (INode node in result)
{
m_logger.LogInformation(
"NodeId {NodeId} {NodeClass} {BrowseName}",
node.NodeId,
node.NodeClass,
node.BrowseName);
}
}
return result;
}
///
/// Browse full address space using the ManagedBrowseMethod, which
/// will take care of not sending to many nodes to the server,
/// calling BrowseNext and dealing with the status codes
/// BadNoContinuationPoint and BadInvalidContinuationPoint.
///
/// The UAClient with a session to use.
/// The node where the browse operation starts.
/// An optional BrowseDescription to use.
public async Task> ManagedBrowseFullAddressSpaceAsync(
IUAClient uaClient,
NodeId startingNode = default,
BrowseDescription? browseDescription = null,
CancellationToken ct = default)
{
ContinuationPointPolicy policyBackup = uaClient.Session.ContinuationPointPolicy;
uaClient.Session.ContinuationPointPolicy = ContinuationPointPolicy.Default;
var stopWatch = new Stopwatch();
stopWatch.Start();
BrowseDirection browseDirection = BrowseDirection.Forward;
NodeId referenceTypeId = ReferenceTypeIds.HierarchicalReferences;
bool includeSubtypes = true;
uint nodeClassMask = 0;
if (browseDescription != null)
{
startingNode = browseDescription.NodeId;
browseDirection = browseDescription.BrowseDirection;
referenceTypeId = browseDescription.ReferenceTypeId;
includeSubtypes = browseDescription.IncludeSubtypes;
nodeClassMask = browseDescription.NodeClassMask;
if (browseDescription.ResultMask != (uint)BrowseResultMask.All)
{
m_logger.LogWarning(
"Setting the BrowseResultMask is not supported by the " +
"ManagedBrowse method. Using '{BrowseResultMask}' instead of " +
"the mask {BrowseDescriptionResultMask} for the result mask",
BrowseResultMask.All,
browseDescription.ResultMask);
}
}
ArrayOf nodesToBrowse = [startingNode.IsNull ? ObjectIds.RootFolder : startingNode];
const int kMaxReferencesPerNode = 1000;
// Browse
var referenceDescriptions = new Dictionary();
int searchDepth = 0;
uint maxNodesPerBrowse = uaClient.Session.OperationLimits.MaxNodesPerBrowse;
var allReferenceDescriptions = new List>();
var newReferenceDescriptions = new List>();
var allServiceResults = new List();
while (nodesToBrowse.Count != 0 && searchDepth < kMaxSearchDepth)
{
searchDepth++;
m_logger.LogInformation(
"{Depth}: Browse {Count} nodes after {Duration}ms",
searchDepth,
nodesToBrowse.Count,
stopWatch.ElapsedMilliseconds);
const bool repeatBrowse = false;
do
{
if (m_quitEvent?.WaitOne(0) == true)
{
m_logger.LogInformation("Browse aborted.");
break;
}
try
{
// the resultMask defaults to "all"
// maybe the API should be extended to
// support it. But that will then also be
// necessary for BrowseAsync
(ArrayOf> descriptions, ArrayOf errors) =
await uaClient
.Session.ManagedBrowseAsync(
null,
null,
nodesToBrowse,
kMaxReferencesPerNode,
browseDirection,
referenceTypeId,
true,
nodeClassMask,
ct)
.ConfigureAwait(false);
allReferenceDescriptions.AddRange(descriptions);
newReferenceDescriptions.AddRange(descriptions);
allServiceResults.AddRange(errors);
}
catch (ServiceResultException sre)
{
// the maximum number of nodes per browse is
// set in the ManagedBrowse from the configuration
// and cannot be influenced from the outside.
// if that's desired it would be necessary to provide
// an additional parameter to the method.
m_logger.LogError(sre, "Browse error");
throw;
}
} while (repeatBrowse);
// Build browse request for next level
var nodesForNextManagedBrowse = new List();
int duplicates = 0;
foreach (ArrayOf referenceCollection in newReferenceDescriptions)
{
foreach (ReferenceDescription reference in referenceCollection)
{
if (!referenceDescriptions.ContainsKey(reference.NodeId))
{
referenceDescriptions[reference.NodeId] = reference;
if (!reference.ReferenceTypeId.Equals(ReferenceTypeIds.HasProperty))
{
nodesForNextManagedBrowse.Add(
ExpandedNodeId.ToNodeId(
reference.NodeId,
uaClient.Session.NamespaceUris));
}
}
else
{
duplicates++;
}
}
}
newReferenceDescriptions.Clear();
nodesToBrowse = nodesForNextManagedBrowse.ToArrayOf();
if (duplicates > 0)
{
m_logger.LogInformation(
"Managed Browse Result {Count} duplicate nodes were ignored.",
duplicates);
}
}
stopWatch.Stop();
var result = new List(referenceDescriptions.Values);
result.Sort((x, y) => x.NodeId.CompareTo(y.NodeId));
m_logger.LogInformation(
"ManagedBrowseFullAddressSpace found {Count} references on server in {Duration}ms.",
result.Count,
stopWatch.ElapsedMilliseconds);
if (m_verbose)
{
foreach (ReferenceDescription reference in result)
{
m_logger.LogInformation(
"NodeId {NodeId} {NodeClass} {BrowseName}",
reference.NodeId,
reference.NodeClass,
reference.BrowseName);
}
}
uaClient.Session.ContinuationPointPolicy = policyBackup;
return result;
}
///
/// Browse full address space.
///
/// The UAClient with a session to use.
/// The node where the browse operation starts.
/// An optional BrowseDescription to use.
public async Task> BrowseFullAddressSpaceAsync(
IUAClient uaClient,
NodeId startingNode = default,
BrowseDescription? browseDescription = null,
CancellationToken ct = default)
{
var stopWatch = new Stopwatch();
stopWatch.Start();
// Browse template
const int kMaxReferencesPerNode = 1000;
BrowseDescription browseTemplate =
browseDescription
?? new BrowseDescription
{
NodeId = startingNode.IsNull ? ObjectIds.RootFolder : startingNode,
BrowseDirection = BrowseDirection.Forward,
ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences,
IncludeSubtypes = true,
NodeClassMask = 0,
ResultMask = (uint)BrowseResultMask.All
};
ArrayOf browseDescriptionCollection
= CreateBrowseDescriptionCollectionFromNodeId(
[startingNode.IsNull ? ObjectIds.RootFolder : startingNode],
browseTemplate);
// Browse
var referenceDescriptions = new Dictionary();
int searchDepth = 0;
uint maxNodesPerBrowse = uaClient.Session.OperationLimits.MaxNodesPerBrowse;
while (browseDescriptionCollection.Count > 0 && searchDepth < kMaxSearchDepth)
{
searchDepth++;
m_logger.LogInformation(
"{Depth}: Browse {Count} nodes after {Duration}ms",
searchDepth,
browseDescriptionCollection.Count,
stopWatch.ElapsedMilliseconds);
var allBrowseResults = new List();
bool repeatBrowse;
ArrayOf browseResultCollection = default;
var unprocessedOperations = new List();
ArrayOf diagnosticsInfoCollection;
do
{
if (m_quitEvent?.WaitOne(0) == true)
{
m_logger.LogInformation("Browse aborted.");
break;
}
if (maxNodesPerBrowse >= browseDescriptionCollection.Count)
{
maxNodesPerBrowse = 0; // Do not slice, take all
}
ArrayOf browseCollection =
maxNodesPerBrowse == 0
? browseDescriptionCollection
: browseDescriptionCollection[..(int)maxNodesPerBrowse];
repeatBrowse = false;
try
{
BrowseResponse browseResponse = await uaClient
.Session.BrowseAsync(
null,
null,
kMaxReferencesPerNode,
browseCollection,
ct)
.ConfigureAwait(false);
browseResultCollection = browseResponse.Results;
diagnosticsInfoCollection = browseResponse.DiagnosticInfos;
ClientBase.ValidateResponse(browseResultCollection, browseCollection);
ClientBase.ValidateDiagnosticInfos(
diagnosticsInfoCollection,
browseCollection);
// separate unprocessed nodes for later
int ii = 0;
foreach (BrowseResult browseResult in browseResultCollection)
{
// check for error.
StatusCode statusCode = browseResult.StatusCode;
if (StatusCode.IsBad(statusCode))
{
// this error indicates that the server does not have enough simultaneously active
// continuation points. This request will need to be resent after the other operations
// have been completed and their continuation points released.
if (statusCode == StatusCodes.BadNoContinuationPoints)
{
unprocessedOperations.Add(browseCollection[ii++]);
continue;
}
}
// save results.
allBrowseResults.Add(browseResult);
ii++;
}
}
catch (ServiceResultException sre)
{
if (sre.StatusCode == StatusCodes.BadEncodingLimitsExceeded ||
sre.StatusCode == StatusCodes.BadResponseTooLarge)
{
// try to address by overriding operation limit
maxNodesPerBrowse =
maxNodesPerBrowse == 0
? (uint)browseCollection.Count / 2
: maxNodesPerBrowse / 2;
repeatBrowse = true;
}
else
{
m_logger.LogError(sre, "Browse error.");
throw;
}
}
} while (repeatBrowse);
browseDescriptionCollection = maxNodesPerBrowse == 0 ?
default :
browseDescriptionCollection[(int)maxNodesPerBrowse..];
// Browse next
ArrayOf continuationPoints = PrepareBrowseNext(browseResultCollection);
while (continuationPoints.Count > 0)
{
if (m_quitEvent?.WaitOne(0) == true)
{
m_logger.LogInformation("Browse aborted.");
}
m_logger.LogInformation("BrowseNext {Count} continuation points.", continuationPoints.Count);
BrowseNextResponse browseNextResult = await uaClient
.Session.BrowseNextAsync(null, false, continuationPoints, ct)
.ConfigureAwait(false);
ArrayOf browseNextResultCollection = browseNextResult.Results;
diagnosticsInfoCollection = browseNextResult.DiagnosticInfos;
ClientBase.ValidateResponse(browseNextResultCollection, continuationPoints);
ClientBase.ValidateDiagnosticInfos(
diagnosticsInfoCollection,
continuationPoints);
allBrowseResults.AddRange(browseNextResultCollection);
continuationPoints = PrepareBrowseNext(browseNextResultCollection);
}
// Build browse request for next level
var browseTable = new List();
int duplicates = 0;
foreach (BrowseResult browseResult in allBrowseResults)
{
foreach (ReferenceDescription reference in browseResult.References)
{
if (!referenceDescriptions.ContainsKey(reference.NodeId))
{
referenceDescriptions[reference.NodeId] = reference;
if (reference.ReferenceTypeId != ReferenceTypeIds.HasProperty)
{
browseTable.Add(
ExpandedNodeId.ToNodeId(
reference.NodeId,
uaClient.Session.NamespaceUris));
}
}
else
{
duplicates++;
}
}
}
if (duplicates > 0)
{
m_logger.LogInformation("Browse Result {Count} duplicate nodes were ignored.", duplicates);
}
browseDescriptionCollection = ArrayOf.Combine(
browseDescriptionCollection,
CreateBrowseDescriptionCollectionFromNodeId(browseTable, browseTemplate),
unprocessedOperations); // add unprocessed nodes if any
}
stopWatch.Stop();
var result = new List(referenceDescriptions.Values);
result.Sort((x, y) => x.NodeId.CompareTo(y.NodeId));
m_logger.LogInformation(
"BrowseFullAddressSpace found {Count} references on server in {Duration}ms.",
referenceDescriptions.Count,
stopWatch.ElapsedMilliseconds);
if (m_verbose)
{
foreach (ReferenceDescription reference in result)
{
m_logger.LogInformation(
"NodeId {NodeId} {NodeClass} {BrowseName}",
reference.NodeId,
reference.NodeClass,
reference.BrowseName);
}
}
return result;
}
///
/// Loads the custom type system of the server in the session.
///
///
/// Outputs elapsed time information for perf testing and lists all
/// types that were successfully added to the session encodeable type factory.
///
///
public async Task LoadTypeSystemAsync(ComplexTypeSystem complexTypeSystem, CancellationToken ct = default)
{
m_logger.LogInformation("Load the server type system.");
var stopWatch = new Stopwatch();
stopWatch.Start();
bool loaded = await complexTypeSystem.LoadAsync(throwOnError: true, ct: ct).ConfigureAwait(false);
stopWatch.Stop();
m_logger.LogInformation(
"Loaded {Count} types took {Duration}ms.",
complexTypeSystem.GetDefinedTypes().Count,
stopWatch.ElapsedMilliseconds);
if (!loaded)
{
throw new ServiceResultException(
StatusCodes.BadTypeMismatch,
"ComplexTypeSystem.LoadAsync did not load all custom types.");
}
if (m_verbose)
{
m_logger.LogInformation("Custom types defined for this session:");
foreach (XmlQualifiedName type in complexTypeSystem.GetDefinedTypes())
{
m_logger.LogInformation("{Namespace}.{TypeName}", type.Namespace, type.Name);
}
m_logger.LogInformation(
"Loaded {Count} dictionaries:", complexTypeSystem.DataTypeSystem.Count);
foreach (KeyValuePair dictionary in complexTypeSystem
.DataTypeSystem)
{
m_logger.LogInformation(" + {DictionaryName}", dictionary.Value.Name);
foreach (KeyValuePair type in dictionary.Value.DataTypes)
{
m_logger.LogInformation(" -- {NodeId}:{BrowseName}", type.Key, type.Value);
}
}
}
}
///
/// Read all ReferenceTypeIds from the server that are not known by the client.
/// To reduce the number of calls due to traversal call pyramid, start with all
/// known reference types to reduce the number of FetchReferences/FetchNodes calls.
///
///
/// The NodeCache needs this information to function properly with subtypes
/// of hierarchical calls.
///
/// The session to use
private static Task FetchReferenceIdTypesAsync(
ISession session,
CancellationToken ct = default)
{
NamespaceTable namespaceUris = session.NamespaceUris;
var referenceTypes = ReferenceTypeIds.Identifiers
.Select(nodeId => NodeId.ToExpandedNodeId(nodeId, namespaceUris))
.ToArrayOf();
return session.FetchTypeTreeAsync(referenceTypes, ct);
}
///
/// Output all values as JSON.
///
public async Task> ReadAllValuesAsync(
IUAClient uaClient,
ArrayOf variableIds,
CancellationToken ct = default)
{
bool retrySingleRead = false;
List? values = null;
List? errors = null;
do
{
try
{
if (retrySingleRead)
{
values = [];
errors = [];
foreach (NodeId variableId in variableIds.ToList())
{
try
{
m_logger.LogInformation("Read {NodeId}", variableId);
DataValue value = await uaClient
.Session.ReadValueAsync(variableId, ct)
.ConfigureAwait(false);
values.Add(value);
errors.Add(value.StatusCode);
if (ServiceResult.IsNotBad(value.StatusCode))
{
string valueString = FormatValueAsJson(
uaClient.Session.MessageContext,
variableId.ToString(),
value,
JsonEncoderOptions.Compact);
m_logger.LogInformation("{Value}", valueString);
}
else
{
m_logger.LogInformation("Error: {StatusCode}", value.StatusCode);
}
}
catch (ServiceResultException sre)
{
m_logger.LogError(sre, "Error");
values.Add(new DataValue(sre.StatusCode));
errors.Add(sre.Result);
}
}
}
else
{
(ArrayOf valueList, ArrayOf errorList) = await uaClient.Session.ReadValuesAsync(variableIds, ct)
.ConfigureAwait(false);
values = valueList.ToList();
errors = errorList.ToList();
int ii = 0;
foreach (DataValue value in values)
{
if (ServiceResult.IsNotBad(errors[ii]))
{
string valueString = FormatValueAsJson(
uaClient.Session.MessageContext,
variableIds[ii].ToString(),
value,
JsonEncoderOptions.Compact);
m_logger.LogInformation("{Value}", valueString);
}
else
{
m_logger.LogInformation("Error: {StatusCode}", value.StatusCode);
}
ii++;
}
}
retrySingleRead = false;
}
catch (ServiceResultException sre) when (sre.StatusCode == StatusCodes
.BadEncodingLimitsExceeded)
{
m_logger.LogInformation("Retry to read the values due to error: {Error}", sre.Message);
retrySingleRead = !retrySingleRead;
}
} while (retrySingleRead);
return ResultSet.From(values!, errors!);
}
///
/// Subscribe to all variables in the list.
///
/// The UAClient with a session to use.
/// The variables to subscribe.
public async Task SubscribeAllValuesAsync(
IUAClient uaClient,
ArrayOf variableIds,
int samplingInterval,
int publishingInterval,
uint queueSize,
uint lifetimeCount,
uint keepAliveCount,
CancellationToken ct = default)
{
if (uaClient.Session == null || !uaClient.Session.Connected)
{
m_logger.LogInformation("Session not connected!");
return;
}
try
{
// Create a subscription for receiving data change notifications
ISession session = uaClient.Session;
// test for deferred ack of sequence numbers
session.PublishSequenceNumbersToAcknowledge += DeferSubscriptionAcknowledge;
// set a minimum amount of three publish requests per session
session.MinPublishRequestCount = 3;
// Define Subscription parameters
var subscription = new Subscription(session.DefaultSubscription)
{
DisplayName = "Console ReferenceClient Subscription",
PublishingEnabled = true,
PublishingInterval = publishingInterval,
LifetimeCount = lifetimeCount,
KeepAliveCount = keepAliveCount,
SequentialPublishing = true,
RepublishAfterTransfer = true,
DisableMonitoredItemCache = true,
MaxNotificationsPerPublish = 1000,
MinLifetimeInterval = (uint)session.SessionTimeout,
FastDataChangeCallback = FastDataChangeNotification,
FastKeepAliveCallback = FastKeepAliveNotification
};
session.AddSubscription(subscription);
// Create the subscription on Server side
await subscription.CreateAsync(ct).ConfigureAwait(false);
m_logger.LogInformation(
"New Subscription created with SubscriptionId = {SubscriptionId}.",
subscription.Id);
// Create MonitoredItems for data changes
foreach (Node item in variableIds)
{
var monitoredItem = new MonitoredItem(subscription.DefaultItem)
{
StartNodeId = item.NodeId,
AttributeId = Attributes.Value,
SamplingInterval = samplingInterval,
DisplayName = item.DisplayName.Text ?? item.BrowseName.Name ?? "unknown",
QueueSize = queueSize,
DiscardOldest = true,
MonitoringMode = MonitoringMode.Reporting
};
subscription.AddItem(monitoredItem);
if (subscription.CurrentKeepAliveCount > 1000)
{
break;
}
}
// Create the monitored items on Server side
await subscription.ApplyChangesAsync(ct).ConfigureAwait(false);
m_logger.LogInformation(
"MonitoredItems {Count} created for SubscriptionId = {SubscriptionId}.",
subscription.MonitoredItemCount,
subscription.Id);
}
catch (Exception ex)
{
m_logger.LogError(ex, "Subscribe error");
}
}
///
/// Create a prettified JSON string of a DataValue.
///
/// The key of the Json value.
/// The DataValue.
/// Use reversible encoding.
public static string FormatValueAsJson(
IServiceMessageContext messageContext,
string name,
DataValue value,
JsonEncoderOptions? jsonEncodingType = null)
{
string textbuffer;
using (var jsonEncoder = new JsonEncoder(messageContext, jsonEncodingType))
{
jsonEncoder.WriteDataValue(name, value);
textbuffer = jsonEncoder.CloseAndReturnText();
}
using var doc = JsonDocument.Parse(textbuffer);
using var stream = new MemoryStream();
using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions
{
Indented = true
}))
{
doc.WriteTo(writer);
}
return Encoding.UTF8.GetString(stream.ToArray());
}
///
/// The fast keep alive notification callback.
///
private void FastKeepAliveNotification(
Subscription subscription,
NotificationData notification)
{
try
{
m_logger.LogInformation(
"Keep Alive : Id={SubscriptionId} PublishTime={PublishTime} SequenceNumber={SequenceNumber}.",
subscription.Id,
notification.PublishTime,
notification.SequenceNumber);
}
catch (Exception ex)
{
m_logger.LogError(ex, "FastKeepAliveNotification error");
}
}
///
/// The fast data change notification callback.
///
private void FastDataChangeNotification(
Subscription subscription,
DataChangeNotification notification,
ArrayOf stringTable)
{
try
{
m_logger.LogInformation(
"Notification: Id={SubscriptionId} PublishTime={PublishTime} SequenceNumber={SequenceNumber} Items={Count}.",
subscription.Id,
notification.PublishTime,
notification.SequenceNumber,
notification.MonitoredItems.Count);
}
catch (Exception ex)
{
m_logger.LogError(ex, "FastDataChangeNotification error");
}
}
///
/// Handle DataChange notifications from Server
///
private void OnMonitoredItemNotification(
MonitoredItem monitoredItem,
MonitoredItemNotificationEventArgs e)
{
try
{
// Log MonitoredItem Notification event
if (e.NotificationValue is not MonitoredItemNotification notification)
{
m_logger.LogWarning(
"Unexpected notification type: {Type}",
e.NotificationValue?.GetType().Name ?? "null");
return;
}
DateTime localTime = notification.Value.SourceTimestamp.ToLocalTime();
m_logger.LogInformation(
"Notification: {SequenceNumber} \"{NodeId}\" and Value = {Value} at [{CurrentTime}].",
notification.Message.SequenceNumber,
monitoredItem.ResolvedNodeId,
notification.Value,
localTime.ToLongTimeString());
}
catch (Exception ex)
{
m_logger.LogError(ex, "OnMonitoredItemNotification error");
}
}
///
/// Handle Requested Event notifications from Server
///
private void OnMonitoredItemEventNotification(
MonitoredItem monitoredItem,
MonitoredItemNotificationEventArgs e)
{
try
{
// Log MonitoredItem Notification event
if (e.NotificationValue is not EventFieldList notification)
{
m_logger.LogWarning(
"Unexpected event notification type: {Type}",
e.NotificationValue?.GetType().Name ?? "null");
return;
}
foreach (KeyValuePair> entry in m_desiredEventFields)
{
Variant field = notification.EventFields[entry.Key];
if (field.TypeInfo.BuiltInType != BuiltInType.Null)
{
var fieldPath = new StringBuilder();
int lastIndex = entry.Value.Count - 1;
for (int index = 0; index < entry.Value.Count; index++)
{
fieldPath.Append(entry.Value[index].Name);
if (index < lastIndex)
{
fieldPath.Append('.');
}
}
string fieldName = fieldPath.ToString();
if (fieldName.Equals("Time", StringComparison.Ordinal))
{
try
{
DateTimeUtc currentTime = field.GetDateTime();
TimeSpan timeSpan = currentTime - m_lastEventTime;
m_lastEventTime = currentTime;
m_processedEvents++;
if (m_processedEvents > 1)
{
m_logger.LogInformation(
"Event Received - total count = {Count}, time since last event = {TimeBetweenEvents} seconds",
m_processedEvents,
timeSpan.Seconds);
}
else
{
m_logger.LogInformation(
"Event Received - total count = {Count}",
m_processedEvents);
}
}
catch (Exception ex)
{
m_logger.LogError(ex,
"Unexpected error retrieving Event Time Field Value");
}
}
m_logger.LogInformation(
"\tField [{Index}] \"{Name}\" = [{Value}]",
entry.Key,
fieldName,
field);
}
}
}
catch (Exception ex)
{
m_logger.LogError(ex, "OnMonitoredItemEventNotification error");
}
}
///
/// Event handler to defer publish response sequence number acknowledge.
///
private static void DeferSubscriptionAcknowledge(
ISession session,
PublishSequenceNumbersToAcknowledgeEventArgs e)
{
// for testing keep the latest sequence numbers for a while
const int ackDelay = 5;
if (e.AcknowledgementsToSend.Count > 0)
{
// defer latest sequence numbers
List deferredItems = [.. e
.AcknowledgementsToSend.OrderByDescending(s => s.SequenceNumber)
.Take(ackDelay)];
e.DeferredAcknowledgementsToSend.AddRange(deferredItems);
foreach (SubscriptionAcknowledgement deferredItem in deferredItems)
{
e.AcknowledgementsToSend.Remove(deferredItem);
}
}
}
///
/// Create a browse description from a node id collection.
///
/// The node id collection.
/// The template for the browse description for each node id.
private static ArrayOf CreateBrowseDescriptionCollectionFromNodeId(
ArrayOf nodeIdCollection,
BrowseDescription template)
{
var browseDescriptionCollection = new List();
foreach (NodeId nodeId in nodeIdCollection)
{
BrowseDescription browseDescription = CoreUtils.Clone(template)!;
browseDescription.NodeId = nodeId;
browseDescriptionCollection.Add(browseDescription);
}
return browseDescriptionCollection;
}
///
/// Exports nodes to a NodeSet2 XML file.
///
/// The session to use for exporting.
/// The list of nodes to export.
/// The path where the NodeSet2 XML file will be saved.
public void ExportNodesToNodeSet2(ISession session, IList nodes, string filePath)
{
m_logger.LogInformation("Exporting {Count} nodes to {FilePath}...", nodes.Count, filePath);
var stopwatch = new Stopwatch();
stopwatch.Start();
using var outputStream = new FileStream(filePath, FileMode.Create);
var systemContext = new SystemContext(m_telemetry)
{
NamespaceUris = session.NamespaceUris,
ServerUris = session.ServerUris
};
CoreClientUtils.ExportNodesToNodeSet2(systemContext, nodes, outputStream);
stopwatch.Stop();
m_logger.LogInformation(
"Exported {Count} nodes to {FilePath} in {Duration}ms",
nodes.Count,
filePath,
stopwatch.ElapsedMilliseconds);
}
///
/// Exports nodes to separate NodeSet2 XML files, one per namespace.
///
///
/// When is supplied and non-empty, only nodes whose
/// namespace URI is in the set are exported. Otherwise, all namespaces except the OPC UA
/// base namespace () are exported, which includes any
/// companion specifications hosted by the server.
///
/// The session to use for exporting.
/// The list of nodes to export.
/// The directory where NodeSet2 XML files will be saved.
///
/// Optional set of namespace URIs to include. When null or empty, all namespaces except
/// the OPC UA base namespace are exported.
///
/// Optional cancellation token.
/// A dictionary mapping namespace URI to the file path of the exported NodeSet2 file.
/// Thrown when session, nodes, or outputDirectory is null.
/// Thrown when outputDirectory is empty or whitespace.
public async Task> ExportNodesToNodeSet2PerNamespaceAsync(
ISession session,
IList nodes,
string outputDirectory,
IReadOnlyCollection? targetNamespaces = null,
CancellationToken cancellationToken = default)
{
if (session == null)
{
throw new ArgumentNullException(nameof(session));
}
if (nodes == null)
{
throw new ArgumentNullException(nameof(nodes));
}
if (string.IsNullOrWhiteSpace(outputDirectory))
{
throw new ArgumentException("Value cannot be null or whitespace.", nameof(outputDirectory));
}
HashSet? targetSet = targetNamespaces is { Count: > 0 }
? new HashSet(targetNamespaces, StringComparer.OrdinalIgnoreCase)
: null;
m_logger.LogInformation(
"Exporting {Count} nodes to separate NodeSet2 files per namespace in {Directory} (filter: {Filter})...",
nodes.Count,
outputDirectory,
targetSet != null ? string.Join(",", targetSet) : "all non-OPC-UA-base");
var stopwatch = Stopwatch.StartNew();
// Ensure output directory exists
Directory.CreateDirectory(outputDirectory);
// Group nodes by namespace, applying the requested filter.
var nodesByNamespace = nodes
.Where(node => node.NodeId.NamespaceIndex > 0) // Skip namespace 0 (OPC UA base)
.GroupBy(node => node.NodeId.NamespaceIndex)
.Where(group =>
{
string? namespaceUri = session.NamespaceUris.GetString(group.Key);
if (string.IsNullOrEmpty(namespaceUri))
{
return false;
}
if (targetSet != null)
{
// Caller asked for a specific set of namespaces. namespaceUri is non-null
// after !IsNullOrEmpty (NotNullWhen attribute not available on net48).
return targetSet.Contains(namespaceUri!);
}
// Default: exclude only the OPC UA base namespace; include companion specs.
return !string.Equals(namespaceUri, Namespaces.OpcUa, StringComparison.OrdinalIgnoreCase);
})
.ToDictionary(
group => group.Key,
group => group.ToList());
var exportedFiles = new Dictionary();
// Export each namespace to its own file
foreach (KeyValuePair> kvp in nodesByNamespace)
{
cancellationToken.ThrowIfCancellationRequested();
// The Where clause above filtered out null/empty namespace URIs.
string namespaceUri = session.NamespaceUris.GetString(kvp.Key)!;
// Create a safe filename from the namespace URI
string fileName = CreateSafeFileName(namespaceUri, kvp.Key);
string filePath = Path.Combine(outputDirectory, fileName);
m_logger.LogInformation(
"Exporting namespace {NamespaceIndex} ({NamespaceUri}): {Count} nodes to {FilePath}",
kvp.Key,
namespaceUri,
kvp.Value.Count,
filePath);
await Task.Run(() =>
{
using var outputStream = new FileStream(filePath, FileMode.Create);
var systemContext = new SystemContext(m_telemetry)
{
NamespaceUris = session.NamespaceUris,
ServerUris = session.ServerUris
};
CoreClientUtils.ExportNodesToNodeSet2(systemContext, kvp.Value, outputStream, NodeSetExportOptions.Complete);
}, cancellationToken).ConfigureAwait(false);
exportedFiles[namespaceUri] = filePath;
}
stopwatch.Stop();
m_logger.LogInformation(
"Exported {NamespaceCount} namespaces ({NodeCount} total nodes) in {Duration}ms",
exportedFiles.Count,
nodes.Count,
stopwatch.ElapsedMilliseconds);
return exportedFiles;
}
///
/// Creates a safe filename from a namespace URI.
///
/// The namespace URI.
/// The namespace index (used as fallback).
/// A safe filename for the NodeSet2 export.
private static string CreateSafeFileName(string namespaceUri, ushort namespaceIndex)
{
// Extract meaningful part from URI
string fileName = namespaceUri
.Replace("http://", string.Empty, StringComparison.OrdinalIgnoreCase)
.Replace("https://", string.Empty, StringComparison.OrdinalIgnoreCase)
.Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase);
// Replace invalid filename characters
foreach (char c in Path.GetInvalidFileNameChars())
{
fileName = fileName.Replace(c, '_');
}
// Additional cleanup for common URI characters
fileName = fileName
.Replace('/', '_')
.Replace('\\', '_')
.Replace(':', '_')
.TrimEnd('_');
// Limit length and ensure uniqueness with namespace index
if (fileName.Length > 200)
{
fileName = fileName[..200];
}
return $"{fileName}_ns{namespaceIndex}.xml";
}
///
/// Create the continuation point collection from the browse result
/// collection for the BrowseNext service.
///
/// The browse result collection to use.
/// The collection of continuation points for the BrowseNext service.
private static ArrayOf PrepareBrowseNext(
ArrayOf browseResultCollection)
{
var continuationPoints = new List();
foreach (BrowseResult browseResult in browseResultCollection)
{
if (!browseResult.ContinuationPoint.IsEmpty)
{
continuationPoints.Add(browseResult.ContinuationPoint);
}
}
return continuationPoints;
}
private void ValidateResponse(
ArrayOf requests,
ArrayOf responses)
{
if (m_validate != null)
{
m_validate(requests.ToArray()!, responses.ToArray()!);
}
else
{
ClientBase.ValidateResponse(responses, requests);
}
}
private readonly Action? m_validate;
private readonly ITelemetryContext m_telemetry;
private readonly ILogger m_logger;
private readonly ManualResetEvent? m_quitEvent;
private readonly bool m_verbose;
private readonly Dictionary> m_desiredEventFields;
private int m_processedEvents;
private DateTimeUtc m_lastEventTime = DateTimeUtc.Now;
}
}