Apply code style formatting and restore partial modifiers on Avalonia views

Linter/formatter pass across the full codebase. Restores required partial
keyword on AXAML code-behind classes that the formatter incorrectly removed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-03-31 07:58:13 -04:00
parent 55ef854612
commit 41a6b66943
221 changed files with 4274 additions and 3823 deletions

View File

@@ -6,7 +6,7 @@ using ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Adapters;
/// <summary>
/// Production implementation that builds a real OPC UA ApplicationConfiguration.
/// Production implementation that builds a real OPC UA ApplicationConfiguration.
/// </summary>
internal sealed class DefaultApplicationConfigurationFactory : IApplicationConfigurationFactory
{
@@ -54,11 +54,9 @@ internal sealed class DefaultApplicationConfigurationFactory : IApplicationConfi
await config.Validate(ApplicationType.Client);
if (settings.AutoAcceptCertificates)
{
config.CertificateValidator.CertificateValidation += (_, e) => e.Accept = true;
}
if (settings.SecurityMode != Models.SecurityMode.None)
if (settings.SecurityMode != SecurityMode.None)
{
var app = new ApplicationInstance
{
@@ -72,4 +70,4 @@ internal sealed class DefaultApplicationConfigurationFactory : IApplicationConfi
Logger.Debug("ApplicationConfiguration created for {EndpointUrl}", settings.EndpointUrl);
return config;
}
}
}

View File

@@ -5,13 +5,14 @@ using Serilog;
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Adapters;
/// <summary>
/// Production endpoint discovery that queries the real server.
/// Production endpoint discovery that queries the real server.
/// </summary>
internal sealed class DefaultEndpointDiscovery : IEndpointDiscovery
{
private static readonly ILogger Logger = Log.ForContext<DefaultEndpointDiscovery>();
public EndpointDescription SelectEndpoint(ApplicationConfiguration config, string endpointUrl, MessageSecurityMode requestedMode)
public EndpointDescription SelectEndpoint(ApplicationConfiguration config, string endpointUrl,
MessageSecurityMode requestedMode)
{
if (requestedMode == MessageSecurityMode.None)
{
@@ -54,9 +55,10 @@ internal sealed class DefaultEndpointDiscovery : IEndpointDiscovery
{
var builder = new UriBuilder(best.EndpointUrl) { Host = requestedUri.Host };
best.EndpointUrl = builder.ToString();
Logger.Debug("Rewrote endpoint host from {ServerHost} to {RequestedHost}", serverUri.Host, requestedUri.Host);
Logger.Debug("Rewrote endpoint host from {ServerHost} to {RequestedHost}", serverUri.Host,
requestedUri.Host);
}
return best;
}
}
}

View File

@@ -5,7 +5,7 @@ using Serilog;
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Adapters;
/// <summary>
/// Production session adapter wrapping a real OPC UA Session.
/// Production session adapter wrapping a real OPC UA Session.
/// </summary>
internal sealed class DefaultSessionAdapter : ISessionAdapter
{
@@ -67,14 +67,14 @@ internal sealed class DefaultSessionAdapter : ISessionAdapter
true,
nodeClassMask);
return (continuationPoint, references ?? new ReferenceDescriptionCollection());
return (continuationPoint, references ?? []);
}
public async Task<(byte[]? ContinuationPoint, ReferenceDescriptionCollection References)> BrowseNextAsync(
byte[] continuationPoint, CancellationToken ct)
{
var (_, nextCp, nextRefs) = await _session.BrowseNextAsync(null, false, continuationPoint);
return (nextCp, nextRefs ?? new ReferenceDescriptionCollection());
return (nextCp, nextRefs ?? []);
}
public async Task<bool> HasChildrenAsync(NodeId nodeId, CancellationToken ct)
@@ -134,26 +134,24 @@ internal sealed class DefaultSessionAdapter : ISessionAdapter
break;
if (result.HistoryData is ExtensionObject ext && ext.Body is HistoryData historyData)
{
allValues.AddRange(historyData.DataValues);
}
continuationPoint = result.ContinuationPoint;
}
while (continuationPoint != null && continuationPoint.Length > 0 && allValues.Count < maxValues);
} while (continuationPoint != null && continuationPoint.Length > 0 && allValues.Count < maxValues);
return allValues;
}
public async Task<IReadOnlyList<DataValue>> HistoryReadAggregateAsync(
NodeId nodeId, DateTime startTime, DateTime endTime, NodeId aggregateId, double intervalMs, CancellationToken ct)
NodeId nodeId, DateTime startTime, DateTime endTime, NodeId aggregateId, double intervalMs,
CancellationToken ct)
{
var details = new ReadProcessedDetails
{
StartTime = startTime,
EndTime = endTime,
ProcessingInterval = intervalMs,
AggregateType = new NodeIdCollection { aggregateId }
AggregateType = [aggregateId]
};
var nodesToRead = new HistoryReadValueIdCollection
@@ -178,9 +176,7 @@ internal sealed class DefaultSessionAdapter : ISessionAdapter
if (!StatusCode.IsBad(result.StatusCode) &&
result.HistoryData is ExtensionObject ext &&
ext.Body is HistoryData historyData)
{
allValues.AddRange(historyData.DataValues);
}
}
return allValues;
@@ -204,10 +200,7 @@ internal sealed class DefaultSessionAdapter : ISessionAdapter
{
try
{
if (_session.Connected)
{
_session.Close();
}
if (_session.Connected) _session.Close();
}
catch (Exception ex)
{
@@ -219,12 +212,12 @@ internal sealed class DefaultSessionAdapter : ISessionAdapter
{
try
{
if (_session.Connected)
{
_session.Close();
}
if (_session.Connected) _session.Close();
}
catch { }
catch
{
}
_session.Dispose();
}
}
}

View File

@@ -5,7 +5,7 @@ using Serilog;
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Adapters;
/// <summary>
/// Production session factory that creates real OPC UA sessions.
/// Production session factory that creates real OPC UA sessions.
/// </summary>
internal sealed class DefaultSessionFactory : ISessionFactory
{
@@ -34,4 +34,4 @@ internal sealed class DefaultSessionFactory : ISessionFactory
Logger.Information("Session created: {SessionName} -> {EndpointUrl}", sessionName, endpoint.EndpointUrl);
return new DefaultSessionAdapter(session);
}
}
}

View File

@@ -5,13 +5,13 @@ using Serilog;
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Adapters;
/// <summary>
/// Production subscription adapter wrapping a real OPC UA Subscription.
/// Production subscription adapter wrapping a real OPC UA Subscription.
/// </summary>
internal sealed class DefaultSubscriptionAdapter : ISubscriptionAdapter
{
private static readonly ILogger Logger = Log.ForContext<DefaultSubscriptionAdapter>();
private readonly Subscription _subscription;
private readonly Dictionary<uint, MonitoredItem> _monitoredItems = new();
private readonly Subscription _subscription;
public DefaultSubscriptionAdapter(Subscription subscription)
{
@@ -33,9 +33,7 @@ internal sealed class DefaultSubscriptionAdapter : ISubscriptionAdapter
item.Notification += (_, e) =>
{
if (e.NotificationValue is MonitoredItemNotification notification)
{
onDataChange(nodeId.ToString(), notification.Value);
}
};
_subscription.AddItem(item);
@@ -75,10 +73,7 @@ internal sealed class DefaultSubscriptionAdapter : ISubscriptionAdapter
item.Notification += (_, e) =>
{
if (e.NotificationValue is EventFieldList eventFields)
{
onEvent(eventFields);
}
if (e.NotificationValue is EventFieldList eventFields) onEvent(eventFields);
};
_subscription.AddItem(item);
@@ -106,6 +101,7 @@ internal sealed class DefaultSubscriptionAdapter : ISubscriptionAdapter
{
Logger.Warning(ex, "Error deleting subscription");
}
_monitoredItems.Clear();
}
@@ -115,7 +111,10 @@ internal sealed class DefaultSubscriptionAdapter : ISubscriptionAdapter
{
_subscription.Delete(true);
}
catch { }
catch
{
}
_monitoredItems.Clear();
}
}
}

View File

@@ -4,12 +4,12 @@ using ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Adapters;
/// <summary>
/// Creates and configures an OPC UA ApplicationConfiguration.
/// Creates and configures an OPC UA ApplicationConfiguration.
/// </summary>
internal interface IApplicationConfigurationFactory
{
/// <summary>
/// Creates a validated ApplicationConfiguration for the given connection settings.
/// Creates a validated ApplicationConfiguration for the given connection settings.
/// </summary>
Task<ApplicationConfiguration> CreateAsync(ConnectionSettings settings, CancellationToken ct = default);
}
}

View File

@@ -3,13 +3,14 @@ using Opc.Ua;
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Adapters;
/// <summary>
/// Abstracts OPC UA endpoint discovery for testability.
/// Abstracts OPC UA endpoint discovery for testability.
/// </summary>
internal interface IEndpointDiscovery
{
/// <summary>
/// Discovers endpoints at the given URL and returns the best match for the requested security mode.
/// Also rewrites the endpoint URL hostname to match the requested URL when they differ.
/// Discovers endpoints at the given URL and returns the best match for the requested security mode.
/// Also rewrites the endpoint URL hostname to match the requested URL when they differ.
/// </summary>
EndpointDescription SelectEndpoint(ApplicationConfiguration config, string endpointUrl, MessageSecurityMode requestedMode);
}
EndpointDescription SelectEndpoint(ApplicationConfiguration config, string endpointUrl,
MessageSecurityMode requestedMode);
}

View File

@@ -3,7 +3,7 @@ using Opc.Ua;
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Adapters;
/// <summary>
/// Abstracts the OPC UA session for read, write, browse, history, and subscription operations.
/// Abstracts the OPC UA session for read, write, browse, history, and subscription operations.
/// </summary>
internal interface ISessionAdapter : IDisposable
{
@@ -17,7 +17,7 @@ internal interface ISessionAdapter : IDisposable
NamespaceTable NamespaceUris { get; }
/// <summary>
/// Registers a keep-alive callback. The callback receives true when the session is healthy, false on failure.
/// Registers a keep-alive callback. The callback receives true when the session is healthy, false on failure.
/// </summary>
void RegisterKeepAliveHandler(Action<bool> callback);
@@ -25,37 +25,39 @@ internal interface ISessionAdapter : IDisposable
Task<StatusCode> WriteValueAsync(NodeId nodeId, DataValue value, CancellationToken ct = default);
/// <summary>
/// Browses forward hierarchical references from the given node.
/// Returns (continuationPoint, references).
/// Browses forward hierarchical references from the given node.
/// Returns (continuationPoint, references).
/// </summary>
Task<(byte[]? ContinuationPoint, ReferenceDescriptionCollection References)> BrowseAsync(
NodeId nodeId, uint nodeClassMask = 0, CancellationToken ct = default);
/// <summary>
/// Continues a browse from a continuation point.
/// Continues a browse from a continuation point.
/// </summary>
Task<(byte[]? ContinuationPoint, ReferenceDescriptionCollection References)> BrowseNextAsync(
byte[] continuationPoint, CancellationToken ct = default);
/// <summary>
/// Checks whether a node has any forward hierarchical child references.
/// Checks whether a node has any forward hierarchical child references.
/// </summary>
Task<bool> HasChildrenAsync(NodeId nodeId, CancellationToken ct = default);
/// <summary>
/// Reads raw historical data.
/// Reads raw historical data.
/// </summary>
Task<IReadOnlyList<DataValue>> HistoryReadRawAsync(NodeId nodeId, DateTime startTime, DateTime endTime, int maxValues, CancellationToken ct = default);
Task<IReadOnlyList<DataValue>> HistoryReadRawAsync(NodeId nodeId, DateTime startTime, DateTime endTime,
int maxValues, CancellationToken ct = default);
/// <summary>
/// Reads processed/aggregate historical data.
/// Reads processed/aggregate historical data.
/// </summary>
Task<IReadOnlyList<DataValue>> HistoryReadAggregateAsync(NodeId nodeId, DateTime startTime, DateTime endTime, NodeId aggregateId, double intervalMs, CancellationToken ct = default);
Task<IReadOnlyList<DataValue>> HistoryReadAggregateAsync(NodeId nodeId, DateTime startTime, DateTime endTime,
NodeId aggregateId, double intervalMs, CancellationToken ct = default);
/// <summary>
/// Creates a subscription adapter for this session.
/// Creates a subscription adapter for this session.
/// </summary>
Task<ISubscriptionAdapter> CreateSubscriptionAsync(int publishingIntervalMs, CancellationToken ct = default);
Task CloseAsync(CancellationToken ct = default);
}
}

View File

@@ -3,12 +3,12 @@ using Opc.Ua;
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Adapters;
/// <summary>
/// Creates OPC UA sessions from a configured endpoint.
/// Creates OPC UA sessions from a configured endpoint.
/// </summary>
internal interface ISessionFactory
{
/// <summary>
/// Creates a session to the given endpoint.
/// Creates a session to the given endpoint.
/// </summary>
/// <param name="config">The application configuration.</param>
/// <param name="endpoint">The configured endpoint.</param>
@@ -24,4 +24,4 @@ internal interface ISessionFactory
uint sessionTimeoutMs,
UserIdentity identity,
CancellationToken ct = default);
}
}

View File

@@ -3,29 +3,30 @@ using Opc.Ua;
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Adapters;
/// <summary>
/// Abstracts OPC UA subscription and monitored item management.
/// Abstracts OPC UA subscription and monitored item management.
/// </summary>
internal interface ISubscriptionAdapter : IDisposable
{
uint SubscriptionId { get; }
/// <summary>
/// Adds a data-change monitored item and returns its client handle for tracking.
/// Adds a data-change monitored item and returns its client handle for tracking.
/// </summary>
/// <param name="nodeId">The node to monitor.</param>
/// <param name="samplingIntervalMs">The sampling interval in milliseconds.</param>
/// <param name="onDataChange">Callback when data changes. Receives (nodeIdString, DataValue).</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>A client handle that can be used to remove the item.</returns>
Task<uint> AddDataChangeMonitoredItemAsync(NodeId nodeId, int samplingIntervalMs, Action<string, DataValue> onDataChange, CancellationToken ct = default);
Task<uint> AddDataChangeMonitoredItemAsync(NodeId nodeId, int samplingIntervalMs,
Action<string, DataValue> onDataChange, CancellationToken ct = default);
/// <summary>
/// Removes a previously added monitored item by its client handle.
/// Removes a previously added monitored item by its client handle.
/// </summary>
Task RemoveMonitoredItemAsync(uint clientHandle, CancellationToken ct = default);
/// <summary>
/// Adds an event monitored item with the given event filter.
/// Adds an event monitored item with the given event filter.
/// </summary>
/// <param name="nodeId">The node to monitor for events.</param>
/// <param name="samplingIntervalMs">The sampling interval.</param>
@@ -33,15 +34,16 @@ internal interface ISubscriptionAdapter : IDisposable
/// <param name="onEvent">Callback when events arrive. Receives the event field list.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>A client handle for the monitored item.</returns>
Task<uint> AddEventMonitoredItemAsync(NodeId nodeId, int samplingIntervalMs, EventFilter filter, Action<EventFieldList> onEvent, CancellationToken ct = default);
Task<uint> AddEventMonitoredItemAsync(NodeId nodeId, int samplingIntervalMs, EventFilter filter,
Action<EventFieldList> onEvent, CancellationToken ct = default);
/// <summary>
/// Requests a condition refresh for this subscription.
/// Requests a condition refresh for this subscription.
/// </summary>
Task ConditionRefreshAsync(CancellationToken ct = default);
/// <summary>
/// Removes all monitored items and deletes the subscription.
/// Removes all monitored items and deletes the subscription.
/// </summary>
Task DeleteAsync(CancellationToken ct = default);
}
}

View File

@@ -4,12 +4,12 @@ using ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Helpers;
/// <summary>
/// Maps the library's AggregateType enum to OPC UA aggregate function NodeIds.
/// Maps the library's AggregateType enum to OPC UA aggregate function NodeIds.
/// </summary>
public static class AggregateTypeMapper
{
/// <summary>
/// Returns the OPC UA NodeId for the specified aggregate type.
/// Returns the OPC UA NodeId for the specified aggregate type.
/// </summary>
public static NodeId ToNodeId(AggregateType aggregate)
{
@@ -24,4 +24,4 @@ public static class AggregateTypeMapper
_ => throw new ArgumentOutOfRangeException(nameof(aggregate), aggregate, "Unknown AggregateType value.")
};
}
}
}

View File

@@ -1,13 +1,13 @@
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Helpers;
/// <summary>
/// Parses and normalizes failover URL sets for redundant OPC UA connections.
/// Parses and normalizes failover URL sets for redundant OPC UA connections.
/// </summary>
public static class FailoverUrlParser
{
/// <summary>
/// Parses a comma-separated failover URL string, prepending the primary URL.
/// Trims whitespace and deduplicates.
/// Parses a comma-separated failover URL string, prepending the primary URL.
/// Trims whitespace and deduplicates.
/// </summary>
/// <param name="primaryUrl">The primary endpoint URL.</param>
/// <param name="failoverCsv">Optional comma-separated failover URLs.</param>
@@ -15,7 +15,7 @@ public static class FailoverUrlParser
public static string[] Parse(string primaryUrl, string? failoverCsv)
{
if (string.IsNullOrWhiteSpace(failoverCsv))
return new[] { primaryUrl };
return [primaryUrl];
var urls = new List<string> { primaryUrl };
foreach (var url in failoverCsv.Split(',', StringSplitOptions.RemoveEmptyEntries))
@@ -24,11 +24,12 @@ public static class FailoverUrlParser
if (!string.IsNullOrEmpty(trimmed) && !urls.Contains(trimmed, StringComparer.OrdinalIgnoreCase))
urls.Add(trimmed);
}
return urls.ToArray();
}
/// <summary>
/// Builds a failover URL set from the primary URL and an optional array of failover URLs.
/// Builds a failover URL set from the primary URL and an optional array of failover URLs.
/// </summary>
/// <param name="primaryUrl">The primary endpoint URL.</param>
/// <param name="failoverUrls">Optional failover URLs.</param>
@@ -36,7 +37,7 @@ public static class FailoverUrlParser
public static string[] Parse(string primaryUrl, string[]? failoverUrls)
{
if (failoverUrls == null || failoverUrls.Length == 0)
return new[] { primaryUrl };
return [primaryUrl];
var urls = new List<string> { primaryUrl };
foreach (var url in failoverUrls)
@@ -45,6 +46,7 @@ public static class FailoverUrlParser
if (!string.IsNullOrEmpty(trimmed) && !urls.Contains(trimmed, StringComparer.OrdinalIgnoreCase))
urls.Add(trimmed);
}
return urls.ToArray();
}
}
}

View File

@@ -4,12 +4,12 @@ using ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Helpers;
/// <summary>
/// Maps between the library's SecurityMode enum and OPC UA SDK MessageSecurityMode.
/// Maps between the library's SecurityMode enum and OPC UA SDK MessageSecurityMode.
/// </summary>
public static class SecurityModeMapper
{
/// <summary>
/// Converts a <see cref="SecurityMode"/> to an OPC UA <see cref="MessageSecurityMode"/>.
/// Converts a <see cref="SecurityMode" /> to an OPC UA <see cref="MessageSecurityMode" />.
/// </summary>
public static MessageSecurityMode ToMessageSecurityMode(SecurityMode mode)
{
@@ -23,7 +23,7 @@ public static class SecurityModeMapper
}
/// <summary>
/// Parses a string to a <see cref="SecurityMode"/> value, case-insensitively.
/// Parses a string to a <see cref="SecurityMode" /> value, case-insensitively.
/// </summary>
/// <param name="value">The string to parse (e.g., "none", "sign", "encrypt", "signandencrypt").</param>
/// <returns>The corresponding SecurityMode.</returns>
@@ -39,4 +39,4 @@ public static class SecurityModeMapper
$"Unknown security mode '{value}'. Valid values: none, sign, encrypt, signandencrypt")
};
}
}
}

View File

@@ -1,13 +1,13 @@
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Helpers;
/// <summary>
/// Converts raw string values into typed values based on the current value's runtime type.
/// Ported from the CLI tool's OpcUaHelper.ConvertValue.
/// Converts raw string values into typed values based on the current value's runtime type.
/// Ported from the CLI tool's OpcUaHelper.ConvertValue.
/// </summary>
public static class ValueConverter
{
/// <summary>
/// Converts a raw string value into the runtime type expected by the target node.
/// Converts a raw string value into the runtime type expected by the target node.
/// </summary>
/// <param name="rawValue">The raw string supplied by the user.</param>
/// <param name="currentValue">The current node value used to infer the target type. May be null.</param>
@@ -29,4 +29,4 @@ public static class ValueConverter
_ => rawValue
};
}
}
}

View File

@@ -1,22 +1,23 @@
using Opc.Ua;
using ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
using BrowseResult = ZB.MOM.WW.LmxOpcUa.Client.Shared.Models.BrowseResult;
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared;
/// <summary>
/// Shared OPC UA client service contract for CLI and UI consumers.
/// Shared OPC UA client service contract for CLI and UI consumers.
/// </summary>
public interface IOpcUaClientService : IDisposable
{
Task<ConnectionInfo> ConnectAsync(ConnectionSettings settings, CancellationToken ct = default);
Task DisconnectAsync(CancellationToken ct = default);
bool IsConnected { get; }
ConnectionInfo? CurrentConnectionInfo { get; }
Task<ConnectionInfo> ConnectAsync(ConnectionSettings settings, CancellationToken ct = default);
Task DisconnectAsync(CancellationToken ct = default);
Task<DataValue> ReadValueAsync(NodeId nodeId, CancellationToken ct = default);
Task<StatusCode> WriteValueAsync(NodeId nodeId, object value, CancellationToken ct = default);
Task<IReadOnlyList<Models.BrowseResult>> BrowseAsync(NodeId? parentNodeId = null, CancellationToken ct = default);
Task<IReadOnlyList<BrowseResult>> BrowseAsync(NodeId? parentNodeId = null, CancellationToken ct = default);
Task SubscribeAsync(NodeId nodeId, int intervalMs = 1000, CancellationToken ct = default);
Task UnsubscribeAsync(NodeId nodeId, CancellationToken ct = default);
@@ -25,12 +26,15 @@ public interface IOpcUaClientService : IDisposable
Task UnsubscribeAlarmsAsync(CancellationToken ct = default);
Task RequestConditionRefreshAsync(CancellationToken ct = default);
Task<IReadOnlyList<DataValue>> HistoryReadRawAsync(NodeId nodeId, DateTime startTime, DateTime endTime, int maxValues = 1000, CancellationToken ct = default);
Task<IReadOnlyList<DataValue>> HistoryReadAggregateAsync(NodeId nodeId, DateTime startTime, DateTime endTime, AggregateType aggregate, double intervalMs = 3600000, CancellationToken ct = default);
Task<IReadOnlyList<DataValue>> HistoryReadRawAsync(NodeId nodeId, DateTime startTime, DateTime endTime,
int maxValues = 1000, CancellationToken ct = default);
Task<IReadOnlyList<DataValue>> HistoryReadAggregateAsync(NodeId nodeId, DateTime startTime, DateTime endTime,
AggregateType aggregate, double intervalMs = 3600000, CancellationToken ct = default);
Task<RedundancyInfo> GetRedundancyInfoAsync(CancellationToken ct = default);
event EventHandler<DataChangedEventArgs>? DataChanged;
event EventHandler<AlarmEventArgs>? AlarmEvent;
event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged;
}
}

View File

@@ -1,9 +1,9 @@
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared;
/// <summary>
/// Factory for creating <see cref="IOpcUaClientService"/> instances.
/// Factory for creating <see cref="IOpcUaClientService" /> instances.
/// </summary>
public interface IOpcUaClientServiceFactory
{
IOpcUaClientService Create();
}
}

View File

@@ -1,7 +1,7 @@
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
/// <summary>
/// Aggregate functions for processed history reads.
/// Aggregate functions for processed history reads.
/// </summary>
public enum AggregateType
{
@@ -22,4 +22,4 @@ public enum AggregateType
/// <summary>Last value in the interval.</summary>
End
}
}

View File

@@ -1,10 +1,30 @@
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
/// <summary>
/// Event data for an alarm or condition notification from the OPC UA server.
/// Event data for an alarm or condition notification from the OPC UA server.
/// </summary>
public sealed class AlarmEventArgs : EventArgs
{
public AlarmEventArgs(
string sourceName,
string conditionName,
ushort severity,
string message,
bool retain,
bool activeState,
bool ackedState,
DateTime time)
{
SourceName = sourceName;
ConditionName = conditionName;
Severity = severity;
Message = message;
Retain = retain;
ActiveState = activeState;
AckedState = ackedState;
Time = time;
}
/// <summary>The name of the source object that raised the alarm.</summary>
public string SourceName { get; }
@@ -28,24 +48,4 @@ public sealed class AlarmEventArgs : EventArgs
/// <summary>The time the event occurred.</summary>
public DateTime Time { get; }
public AlarmEventArgs(
string sourceName,
string conditionName,
ushort severity,
string message,
bool retain,
bool activeState,
bool ackedState,
DateTime time)
{
SourceName = sourceName;
ConditionName = conditionName;
Severity = severity;
Message = message;
Retain = retain;
ActiveState = activeState;
AckedState = ackedState;
Time = time;
}
}
}

View File

@@ -1,30 +1,10 @@
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
/// <summary>
/// Represents a single node in the browse result set.
/// Represents a single node in the browse result set.
/// </summary>
public sealed class BrowseResult
{
/// <summary>
/// The string representation of the node's NodeId.
/// </summary>
public string NodeId { get; }
/// <summary>
/// The display name of the node.
/// </summary>
public string DisplayName { get; }
/// <summary>
/// The node class (e.g., "Object", "Variable", "Method").
/// </summary>
public string NodeClass { get; }
/// <summary>
/// Whether the node has child references.
/// </summary>
public bool HasChildren { get; }
public BrowseResult(string nodeId, string displayName, string nodeClass, bool hasChildren)
{
NodeId = nodeId;
@@ -32,4 +12,24 @@ public sealed class BrowseResult
NodeClass = nodeClass;
HasChildren = hasChildren;
}
}
/// <summary>
/// The string representation of the node's NodeId.
/// </summary>
public string NodeId { get; }
/// <summary>
/// The display name of the node.
/// </summary>
public string DisplayName { get; }
/// <summary>
/// The node class (e.g., "Object", "Variable", "Method").
/// </summary>
public string NodeClass { get; }
/// <summary>
/// Whether the node has child references.
/// </summary>
public bool HasChildren { get; }
}

View File

@@ -1,10 +1,26 @@
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
/// <summary>
/// Information about the current OPC UA session.
/// Information about the current OPC UA session.
/// </summary>
public sealed class ConnectionInfo
{
public ConnectionInfo(
string endpointUrl,
string serverName,
string securityMode,
string securityPolicyUri,
string sessionId,
string sessionName)
{
EndpointUrl = endpointUrl;
ServerName = serverName;
SecurityMode = securityMode;
SecurityPolicyUri = securityPolicyUri;
SessionId = sessionId;
SessionName = sessionName;
}
/// <summary>The endpoint URL of the connected server.</summary>
public string EndpointUrl { get; }
@@ -22,20 +38,4 @@ public sealed class ConnectionInfo
/// <summary>The session name.</summary>
public string SessionName { get; }
public ConnectionInfo(
string endpointUrl,
string serverName,
string securityMode,
string securityPolicyUri,
string sessionId,
string sessionName)
{
EndpointUrl = endpointUrl;
ServerName = serverName;
SecurityMode = securityMode;
SecurityPolicyUri = securityPolicyUri;
SessionId = sessionId;
SessionName = sessionName;
}
}
}

View File

@@ -1,54 +1,54 @@
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
/// <summary>
/// Settings for establishing an OPC UA client connection.
/// Settings for establishing an OPC UA client connection.
/// </summary>
public sealed class ConnectionSettings
{
/// <summary>
/// The primary OPC UA endpoint URL.
/// The primary OPC UA endpoint URL.
/// </summary>
public string EndpointUrl { get; set; } = string.Empty;
/// <summary>
/// Optional failover endpoint URLs for redundancy.
/// Optional failover endpoint URLs for redundancy.
/// </summary>
public string[]? FailoverUrls { get; set; }
/// <summary>
/// Optional username for authentication.
/// Optional username for authentication.
/// </summary>
public string? Username { get; set; }
/// <summary>
/// Optional password for authentication.
/// Optional password for authentication.
/// </summary>
public string? Password { get; set; }
/// <summary>
/// Transport security mode. Defaults to <see cref="Models.SecurityMode.None"/>.
/// Transport security mode. Defaults to <see cref="Models.SecurityMode.None" />.
/// </summary>
public SecurityMode SecurityMode { get; set; } = SecurityMode.None;
/// <summary>
/// Session timeout in seconds. Defaults to 60.
/// Session timeout in seconds. Defaults to 60.
/// </summary>
public int SessionTimeoutSeconds { get; set; } = 60;
/// <summary>
/// Whether to automatically accept untrusted server certificates. Defaults to true.
/// Whether to automatically accept untrusted server certificates. Defaults to true.
/// </summary>
public bool AutoAcceptCertificates { get; set; } = true;
/// <summary>
/// Path to the certificate store. Defaults to a subdirectory under LocalApplicationData.
/// Path to the certificate store. Defaults to a subdirectory under LocalApplicationData.
/// </summary>
public string CertificateStorePath { get; set; } = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"LmxOpcUaClient", "pki");
/// <summary>
/// Validates the settings and throws if any required values are missing or invalid.
/// Validates the settings and throws if any required values are missing or invalid.
/// </summary>
/// <exception cref="ArgumentException">Thrown when settings are invalid.</exception>
public void Validate()
@@ -57,9 +57,10 @@ public sealed class ConnectionSettings
throw new ArgumentException("EndpointUrl must not be null or empty.", nameof(EndpointUrl));
if (SessionTimeoutSeconds <= 0)
throw new ArgumentException("SessionTimeoutSeconds must be greater than zero.", nameof(SessionTimeoutSeconds));
throw new ArgumentException("SessionTimeoutSeconds must be greater than zero.",
nameof(SessionTimeoutSeconds));
if (SessionTimeoutSeconds > 3600)
throw new ArgumentException("SessionTimeoutSeconds must not exceed 3600.", nameof(SessionTimeoutSeconds));
}
}
}

View File

@@ -1,7 +1,7 @@
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
/// <summary>
/// Represents the current state of the OPC UA client connection.
/// Represents the current state of the OPC UA client connection.
/// </summary>
public enum ConnectionState
{
@@ -16,4 +16,4 @@ public enum ConnectionState
/// <summary>Connection was lost and reconnection is in progress.</summary>
Reconnecting
}
}

View File

@@ -1,10 +1,17 @@
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
/// <summary>
/// Event data raised when the client connection state changes.
/// Event data raised when the client connection state changes.
/// </summary>
public sealed class ConnectionStateChangedEventArgs : EventArgs
{
public ConnectionStateChangedEventArgs(ConnectionState oldState, ConnectionState newState, string endpointUrl)
{
OldState = oldState;
NewState = newState;
EndpointUrl = endpointUrl;
}
/// <summary>The previous connection state.</summary>
public ConnectionState OldState { get; }
@@ -13,11 +20,4 @@ public sealed class ConnectionStateChangedEventArgs : EventArgs
/// <summary>The endpoint URL associated with the state change.</summary>
public string EndpointUrl { get; }
public ConnectionStateChangedEventArgs(ConnectionState oldState, ConnectionState newState, string endpointUrl)
{
OldState = oldState;
NewState = newState;
EndpointUrl = endpointUrl;
}
}
}

View File

@@ -3,19 +3,19 @@ using Opc.Ua;
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
/// <summary>
/// Event data for a monitored data value change.
/// Event data for a monitored data value change.
/// </summary>
public sealed class DataChangedEventArgs : EventArgs
{
/// <summary>The string representation of the node that changed.</summary>
public string NodeId { get; }
/// <summary>The new data value from the server.</summary>
public DataValue Value { get; }
public DataChangedEventArgs(string nodeId, DataValue value)
{
NodeId = nodeId;
Value = value;
}
}
/// <summary>The string representation of the node that changed.</summary>
public string NodeId { get; }
/// <summary>The new data value from the server.</summary>
public DataValue Value { get; }
}

View File

@@ -1,10 +1,18 @@
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
/// <summary>
/// Redundancy information read from the server.
/// Redundancy information read from the server.
/// </summary>
public sealed class RedundancyInfo
{
public RedundancyInfo(string mode, byte serviceLevel, string[] serverUris, string applicationUri)
{
Mode = mode;
ServiceLevel = serviceLevel;
ServerUris = serverUris;
ApplicationUri = applicationUri;
}
/// <summary>The redundancy mode (e.g., "None", "Cold", "Warm", "Hot").</summary>
public string Mode { get; }
@@ -16,12 +24,4 @@ public sealed class RedundancyInfo
/// <summary>The application URI of the connected server.</summary>
public string ApplicationUri { get; }
public RedundancyInfo(string mode, byte serviceLevel, string[] serverUris, string applicationUri)
{
Mode = mode;
ServiceLevel = serviceLevel;
ServerUris = serverUris;
ApplicationUri = applicationUri;
}
}
}

View File

@@ -1,7 +1,7 @@
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
/// <summary>
/// Transport security mode for the OPC UA connection.
/// Transport security mode for the OPC UA connection.
/// </summary>
public enum SecurityMode
{
@@ -13,4 +13,4 @@ public enum SecurityMode
/// <summary>Messages are signed and encrypted.</summary>
SignAndEncrypt
}
}

View File

@@ -1,45 +1,42 @@
using System.Text;
using Opc.Ua;
using Serilog;
using ZB.MOM.WW.LmxOpcUa.Client.Shared.Adapters;
using ZB.MOM.WW.LmxOpcUa.Client.Shared.Helpers;
using ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
using BrowseResult = ZB.MOM.WW.LmxOpcUa.Client.Shared.Models.BrowseResult;
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared;
/// <summary>
/// Full implementation of <see cref="IOpcUaClientService"/> using adapter abstractions for testability.
/// Full implementation of <see cref="IOpcUaClientService" /> using adapter abstractions for testability.
/// </summary>
public sealed class OpcUaClientService : IOpcUaClientService
{
private static readonly ILogger Logger = Log.ForContext<OpcUaClientService>();
private readonly IApplicationConfigurationFactory _configFactory;
private readonly IEndpointDiscovery _endpointDiscovery;
private readonly ISessionFactory _sessionFactory;
private ISessionAdapter? _session;
private ISubscriptionAdapter? _dataSubscription;
private ISubscriptionAdapter? _alarmSubscription;
private ConnectionState _state = ConnectionState.Disconnected;
private ConnectionSettings? _settings;
private string[]? _allEndpointUrls;
private int _currentEndpointIndex;
private bool _disposed;
// Track active data subscriptions for replay after failover
private readonly Dictionary<string, (NodeId NodeId, int IntervalMs, uint Handle)> _activeDataSubscriptions = new();
private readonly IApplicationConfigurationFactory _configFactory;
private readonly IEndpointDiscovery _endpointDiscovery;
private readonly ISessionFactory _sessionFactory;
// Track alarm subscription state for replay after failover
private (NodeId? SourceNodeId, int IntervalMs)? _activeAlarmSubscription;
private ISubscriptionAdapter? _alarmSubscription;
private string[]? _allEndpointUrls;
private int _currentEndpointIndex;
private ISubscriptionAdapter? _dataSubscription;
private bool _disposed;
public event EventHandler<DataChangedEventArgs>? DataChanged;
public event EventHandler<AlarmEventArgs>? AlarmEvent;
public event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged;
public bool IsConnected => _state == ConnectionState.Connected && _session?.Connected == true;
public ConnectionInfo? CurrentConnectionInfo { get; private set; }
private ISessionAdapter? _session;
private ConnectionSettings? _settings;
private ConnectionState _state = ConnectionState.Disconnected;
/// <summary>
/// Creates a new OpcUaClientService with the specified adapter dependencies.
/// Creates a new OpcUaClientService with the specified adapter dependencies.
/// </summary>
internal OpcUaClientService(
IApplicationConfigurationFactory configFactory,
@@ -52,7 +49,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
}
/// <summary>
/// Creates a new OpcUaClientService with default production adapters.
/// Creates a new OpcUaClientService with default production adapters.
/// </summary>
public OpcUaClientService()
: this(
@@ -62,6 +59,13 @@ public sealed class OpcUaClientService : IOpcUaClientService
{
}
public event EventHandler<DataChangedEventArgs>? DataChanged;
public event EventHandler<AlarmEventArgs>? AlarmEvent;
public event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged;
public bool IsConnected => _state == ConnectionState.Connected && _session?.Connected == true;
public ConnectionInfo? CurrentConnectionInfo { get; private set; }
public async Task<ConnectionInfo> ConnectAsync(ConnectionSettings settings, CancellationToken ct = default)
{
ThrowIfDisposed();
@@ -80,10 +84,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
session.RegisterKeepAliveHandler(isGood =>
{
if (!isGood)
{
_ = HandleKeepAliveFailureAsync();
}
if (!isGood) _ = HandleKeepAliveFailureAsync();
});
CurrentConnectionInfo = BuildConnectionInfo(session);
@@ -112,11 +113,13 @@ public sealed class OpcUaClientService : IOpcUaClientService
await _dataSubscription.DeleteAsync(ct);
_dataSubscription = null;
}
if (_alarmSubscription != null)
{
await _alarmSubscription.DeleteAsync(ct);
_alarmSubscription = null;
}
if (_session != null)
{
await _session.CloseAsync(ct);
@@ -150,7 +153,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
ThrowIfNotConnected();
// Read current value for type coercion when value is a string
object typedValue = value;
var typedValue = value;
if (value is string rawString)
{
var currentDataValue = await _session!.ReadValueAsync(nodeId, ct);
@@ -161,14 +164,15 @@ public sealed class OpcUaClientService : IOpcUaClientService
return await _session!.WriteValueAsync(nodeId, dataValue, ct);
}
public async Task<IReadOnlyList<Models.BrowseResult>> BrowseAsync(NodeId? parentNodeId = null, CancellationToken ct = default)
public async Task<IReadOnlyList<BrowseResult>> BrowseAsync(NodeId? parentNodeId = null,
CancellationToken ct = default)
{
ThrowIfDisposed();
ThrowIfNotConnected();
var startNode = parentNodeId ?? ObjectIds.ObjectsFolder;
var nodeClassMask = (uint)NodeClass.Object | (uint)NodeClass.Variable | (uint)NodeClass.Method;
var results = new List<Models.BrowseResult>();
var results = new List<BrowseResult>();
var (continuationPoint, references) = await _session!.BrowseAsync(startNode, nodeClassMask, ct);
@@ -180,7 +184,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
var hasChildren = reference.NodeClass == NodeClass.Object &&
await _session.HasChildrenAsync(childNodeId, ct);
results.Add(new Models.BrowseResult(
results.Add(new BrowseResult(
reference.NodeId.ToString(),
reference.DisplayName?.Text ?? string.Empty,
reference.NodeClass.ToString(),
@@ -188,13 +192,9 @@ public sealed class OpcUaClientService : IOpcUaClientService
}
if (continuationPoint != null && continuationPoint.Length > 0)
{
(continuationPoint, references) = await _session.BrowseNextAsync(continuationPoint, ct);
}
else
{
break;
}
}
return results;
@@ -209,10 +209,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
if (_activeDataSubscriptions.ContainsKey(nodeIdStr))
return; // Already subscribed
if (_dataSubscription == null)
{
_dataSubscription = await _session!.CreateSubscriptionAsync(intervalMs, ct);
}
if (_dataSubscription == null) _dataSubscription = await _session!.CreateSubscriptionAsync(intervalMs, ct);
var handle = await _dataSubscription.AddDataChangeMonitoredItemAsync(
nodeId, intervalMs, OnDataChangeNotification, ct);
@@ -229,16 +226,14 @@ public sealed class OpcUaClientService : IOpcUaClientService
if (!_activeDataSubscriptions.TryGetValue(nodeIdStr, out var sub))
return; // Not subscribed, safe to ignore
if (_dataSubscription != null)
{
await _dataSubscription.RemoveMonitoredItemAsync(sub.Handle, ct);
}
if (_dataSubscription != null) await _dataSubscription.RemoveMonitoredItemAsync(sub.Handle, ct);
_activeDataSubscriptions.Remove(nodeIdStr);
Logger.Debug("Unsubscribed from data changes on {NodeId}", nodeId);
}
public async Task SubscribeAlarmsAsync(NodeId? sourceNodeId = null, int intervalMs = 1000, CancellationToken ct = default)
public async Task SubscribeAlarmsAsync(NodeId? sourceNodeId = null, int intervalMs = 1000,
CancellationToken ct = default)
{
ThrowIfDisposed();
ThrowIfNotConnected();
@@ -305,16 +300,18 @@ public sealed class OpcUaClientService : IOpcUaClientService
ThrowIfDisposed();
ThrowIfNotConnected();
var redundancySupportValue = await _session!.ReadValueAsync(VariableIds.Server_ServerRedundancy_RedundancySupport, ct);
var redundancySupportValue =
await _session!.ReadValueAsync(VariableIds.Server_ServerRedundancy_RedundancySupport, ct);
var redundancyMode = ((RedundancySupport)(int)redundancySupportValue.Value).ToString();
var serviceLevelValue = await _session.ReadValueAsync(VariableIds.Server_ServiceLevel, ct);
var serviceLevel = (byte)serviceLevelValue.Value;
string[] serverUris = Array.Empty<string>();
string[] serverUris = [];
try
{
var serverUriArrayValue = await _session.ReadValueAsync(VariableIds.Server_ServerRedundancy_ServerUriArray, ct);
var serverUriArrayValue =
await _session.ReadValueAsync(VariableIds.Server_ServerRedundancy_ServerUriArray, ct);
if (serverUriArrayValue.Value is string[] uris)
serverUris = uris;
}
@@ -323,7 +320,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
// ServerUriArray may not be present when RedundancySupport is None
}
string applicationUri = string.Empty;
var applicationUri = string.Empty;
try
{
var serverArrayValue = await _session.ReadValueAsync(VariableIds.Server_ServerArray, ct);
@@ -354,7 +351,8 @@ public sealed class OpcUaClientService : IOpcUaClientService
// --- Private helpers ---
private async Task<ISessionAdapter> ConnectToEndpointAsync(ConnectionSettings settings, string endpointUrl, CancellationToken ct)
private async Task<ISessionAdapter> ConnectToEndpointAsync(ConnectionSettings settings, string endpointUrl,
CancellationToken ct)
{
// Create a settings copy with the current endpoint URL
var effectiveSettings = new ConnectionSettings
@@ -372,12 +370,13 @@ public sealed class OpcUaClientService : IOpcUaClientService
var requestedMode = SecurityModeMapper.ToMessageSecurityMode(settings.SecurityMode);
var endpoint = _endpointDiscovery.SelectEndpoint(config, endpointUrl, requestedMode);
UserIdentity identity = settings.Username != null
? new UserIdentity(settings.Username, System.Text.Encoding.UTF8.GetBytes(settings.Password ?? ""))
var identity = settings.Username != null
? new UserIdentity(settings.Username, Encoding.UTF8.GetBytes(settings.Password ?? ""))
: new UserIdentity();
var sessionTimeoutMs = (uint)(settings.SessionTimeoutSeconds * 1000);
return await _sessionFactory.CreateSessionAsync(config, endpoint, "LmxOpcUaClient", sessionTimeoutMs, identity, ct);
return await _sessionFactory.CreateSessionAsync(config, endpoint, "LmxOpcUaClient", sessionTimeoutMs, identity,
ct);
}
private async Task HandleKeepAliveFailureAsync()
@@ -392,9 +391,17 @@ public sealed class OpcUaClientService : IOpcUaClientService
// Close old session
if (_session != null)
{
try { _session.Dispose(); } catch { }
try
{
_session.Dispose();
}
catch
{
}
_session = null;
}
_dataSubscription = null;
_alarmSubscription = null;
@@ -405,7 +412,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
}
// Try each endpoint
for (int attempt = 0; attempt < _allEndpointUrls.Length; attempt++)
for (var attempt = 0; attempt < _allEndpointUrls.Length; attempt++)
{
_currentEndpointIndex = (_currentEndpointIndex + 1) % _allEndpointUrls.Length;
var url = _allEndpointUrls[_currentEndpointIndex];
@@ -418,7 +425,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
session.RegisterKeepAliveHandler(isGood =>
{
if (!isGood) { _ = HandleKeepAliveFailureAsync(); }
if (!isGood) _ = HandleKeepAliveFailureAsync();
});
CurrentConnectionInfo = BuildConnectionInfo(session);
@@ -448,7 +455,6 @@ public sealed class OpcUaClientService : IOpcUaClientService
_activeDataSubscriptions.Clear();
foreach (var (nodeIdStr, (nodeId, intervalMs, _)) in subscriptions)
{
try
{
if (_dataSubscription == null)
@@ -462,7 +468,6 @@ public sealed class OpcUaClientService : IOpcUaClientService
{
Logger.Warning(ex, "Failed to replay data subscription for {NodeId}", nodeIdStr);
}
}
}
// Replay alarm subscription
@@ -569,4 +574,4 @@ public sealed class OpcUaClientService : IOpcUaClientService
if (_state != ConnectionState.Connected || _session == null)
throw new InvalidOperationException("Not connected to an OPC UA server.");
}
}
}

View File

@@ -1,7 +1,7 @@
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared;
/// <summary>
/// Default factory that creates <see cref="OpcUaClientService"/> instances with production adapters.
/// Default factory that creates <see cref="OpcUaClientService" /> instances with production adapters.
/// </summary>
public sealed class OpcUaClientServiceFactory : IOpcUaClientServiceFactory
{
@@ -9,4 +9,4 @@ public sealed class OpcUaClientServiceFactory : IOpcUaClientServiceFactory
{
return new OpcUaClientService();
}
}
}

View File

@@ -1,19 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>ZB.MOM.WW.LmxOpcUa.Client.Shared</RootNamespace>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>ZB.MOM.WW.LmxOpcUa.Client.Shared</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client" Version="1.5.378.106" />
<PackageReference Include="Serilog" Version="4.2.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client" Version="1.5.378.106"/>
<PackageReference Include="Serilog" Version="4.2.0"/>
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="ZB.MOM.WW.LmxOpcUa.Client.Shared.Tests" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="ZB.MOM.WW.LmxOpcUa.Client.Shared.Tests"/>
</ItemGroup>
</Project>