Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
703069e
Add long-running connection stability test with dynamic token renewal…
Copilot Dec 22, 2025
e6f5bd0
Bump Roslynator.Analyzers from 4.14.1 to 4.15.0 (#3433)
dependabot[bot] Dec 24, 2025
4f44096
Bump NUnit3TestAdapter from 6.0.0 to 6.0.1 (#3432)
dependabot[bot] Dec 24, 2025
56b4818
Bump NUnit.Console from 3.21.0 to 3.21.1 (#3431)
dependabot[bot] Dec 24, 2025
75be609
Bump System.Text.Json from 10.0.0 to 10.0.1 (#3438)
dependabot[bot] Dec 30, 2025
03af645
Bump System.Formats.Asn1 from 10.0.0 to 10.0.1 (#3437)
dependabot[bot] Dec 31, 2025
90156ae
Bump System.Collections.Immutable from 10.0.0 to 10.0.1 (#3436)
dependabot[bot] Dec 31, 2025
0e18d5c
Bump Roslynator.Formatting.Analyzers from 4.14.1 to 4.15.0 (#3435)
dependabot[bot] Dec 31, 2025
1806172
only run test on net 10, only print per node status in debug runs (#3…
romanett Jan 6, 2026
e92e469
[GDS] Add configurable validation limits for trust list file read/wri…
Copilot Jan 6, 2026
7ec6788
Bump NUnit.Console from 3.21.1 to 3.22.0 (#3440)
dependabot[bot] Jan 6, 2026
5c80f9b
[Client & Server] Remove all usages of SoftwareCertificates (#3443)
romanett Jan 8, 2026
a3a6709
Refactor server for full async subscription management (#3442)
romanett Jan 8, 2026
6ab1bf7
fix format string (#3441)
wxmayifei Jan 8, 2026
30f9359
Fix Session.Save to only save specified subscriptions (#3446)
tobiasfrick Jan 9, 2026
05f7023
Fix two bugs in MonitoredItem (client side) (#3447)
tobiasfrick Jan 12, 2026
8b11d2b
[Server] Add several interfaces to improve testability & prepare for …
romanett Jan 12, 2026
9e192c6
Fix ObjectDisposedException in SubscriptionManager background tasks o…
Copilot Jan 12, 2026
f7c3c4e
Fix Server/ServerStatus/State SourceTimestamp. Fix Reference Server c…
romanett Jan 12, 2026
78a5470
Handle BadRequestTimeout gracefully in Publish error handling (#3460)
Copilot Jan 12, 2026
0713e8f
Suppress expected keepalive errors during test shutdown (#3458)
Copilot Jan 12, 2026
bdb8969
Bump Microsoft.AspNetCore.Http from 2.3.0 to 2.3.9 (#3462)
dependabot[bot] Jan 12, 2026
643fe13
Bump NUnit3TestAdapter from 6.0.1 to 6.1.0 (#3467)
dependabot[bot] Jan 13, 2026
21949f8
Bump Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets from 2.3.0…
dependabot[bot] Jan 13, 2026
288f02f
Allow status code variant creation from uint (#3472)
marcschier Jan 14, 2026
773af55
Fix session reconnect handler (#3471)
marcschier Jan 14, 2026
78a9ba2
Set correct target framework for long running test (#3474)
marcschier Jan 14, 2026
0497953
Change license reference in copilot agents file (#3479)
marcschier Jan 15, 2026
e314cf5
Fix SourceTimestamp for variables without explicit timestamps in Base…
Copilot Jan 19, 2026
3561c68
Document certificate validation workflow with chain building and conf…
Copilot Jan 19, 2026
f2ac68f
Bump Microsoft.Extensions.Configuration and Microsoft.Extensions.Conf…
dependabot[bot] Jan 20, 2026
12704af
Bump Microsoft.AspNetCore.Server.Kestrel and Microsoft.AspNetCore.Ser…
dependabot[bot] Jan 20, 2026
4f089ba
Bump System.Collections.Immutable from 10.0.1 to 10.0.2 (#3490)
dependabot[bot] Jan 20, 2026
424f53f
Bump Microsoft.Extensions.Logging from 10.0.1 to 10.0.2 (#3488)
dependabot[bot] Jan 20, 2026
d71ee2a
Add async callback support to TrustList implementation (#3483)
Copilot Jan 20, 2026
d511adc
Extend NodeSet Export (#3491)
koepalex Jan 22, 2026
9f335c3
Change DebugCheck conditional compilation from DEBUG to CHECKED (#3496)
Copilot Jan 24, 2026
8966e1d
[Server] Use Classes instead of Interfaces for NodeManagers to ensure…
romanett Jan 25, 2026
6765151
Fix EndpointIncomingRequest to do serviceLookup at invocation time in…
romanett Jan 27, 2026
c26de8d
Modified error handling to raise publish error events for (#3502)
romanett Jan 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 16 additions & 13 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,23 @@
This is the official OPC UA .NET Standard Stack from the OPC Foundation. It provides a reference implementation of OPC UA (Open Platform Communications Unified Architecture) targeting .NET Standard, allowing development of applications that run on multiple platforms including Windows, Linux, iOS, and Android.

### Key Technologies
- **Language**: C# with LangVersion 13.0
- **SDK**: .net 10 SDK
- **Language**: C# with LangVersion 14.0
- **Target Frameworks**: .NET Standard 2.0/2.1, .NET Framework 4.8, .NET 8.0 (LTS), .NET 9.0, .NET 10.0 (LTS)
- **Project Type**: Class libraries, console applications, and reference implementations
- **Architecture**: OPC UA Stack with Client, Server, Configuration, Complex Types, GDS, and PubSub components

## Code Style and Standards

### General Guidelines
- Follow the `.editorconfig` settings strictly
- Follow the `.editorconfig` settings *strictly*. Fix all warnings, errors and informational messages before proposing a fix.
- Use 4 spaces for indentation in C# files
- Maximum line length: 120 characters
- End-of-line: CRLF
- Always insert final newline
- Trim trailing whitespace
- Use UTF-8 encoding
- Defined in .editorconfig
- Add the OPC Foundation MIT license header to all source files. Exception: All files in Opc.Ua.Core project which have a dual license header.
- Add the OPC Foundation MIT license header to all source files.

### C# Conventions
- **Braces**: Use Allman style (braces on new line for control blocks, types, and methods)
Expand Down Expand Up @@ -55,19 +55,22 @@ This is the official OPC UA .NET Standard Stack from the OPC Foundation. It prov
## Testing Standards

### Test Requirements
- All new features must include unit tests
- All new features must include unit tests. Tests should be simple and cover positive and negative scenarios.
- Maintain or improve code coverage; critical components should have at least 80% coverage.
- Use NUnit framework (match existing test projects)
- Test projects follow naming convention: `<Component>.Tests`
- Tests must be deterministic and pass in CI/CD environment
- Code coverage is monitored via Coverlet
- Run tests with `dotnet test` from solution root
- Use NUnit asserts methods, no other library or classic Nunit asserts.
- Code coverage is monitored via Coverlet and MUST NOT decrease
- Integration tests should be included for critical components
- Run all tests with `dotnet test` from solution root on UA.slnx
- Use NUnit asserts methods (Assert.That), no other library. DO NOT USE the classic Nunit asserts (E.g. Assert.AreEquals).
- When updating tests fix above for the test only.

### Test Organization
- Place tests in the `Tests/` directory
- Place tests in a project that corresponds to the component package under the `Tests/` directory.
- Mirror the structure of the code being tested
- Use descriptive test method names that explain what is being tested
- Do not use _ in test method names; use PascalCase
- DO NOT use _ in test method names; use PascalCase

## Build and Development

Expand Down Expand Up @@ -126,7 +129,7 @@ This is the official OPC UA .NET Standard Stack from the OPC Foundation. It prov
### Package Guidelines
- Use centralized package management (Directory.Packages.props)
- Audit packages for security vulnerabilities (NuGetAudit is enabled)
- Only add necessary dependencies
- Only add necessary dependencies and ask for approval for new dependencies
- Prefer stable, well-maintained packages
- Check compatibility with all target frameworks

Expand Down Expand Up @@ -161,7 +164,7 @@ This is the official OPC UA .NET Standard Stack from the OPC Foundation. It prov
5. Consider backward compatibility

### Certificate Management
- Never commit certificates to the repository
- NEVER commit certificates or secrets of any kind to the repository
- Use the certificate store APIs
- Follow guidelines in `Docs/Certificates.md`
- Test with different certificate configurations
Expand All @@ -178,7 +181,7 @@ This is the official OPC UA .NET Standard Stack from the OPC Foundation. It prov
- OPC UA Specification: https://reference.opcfoundation.org/
- Documentation: See `Docs/` directory
- Samples Repository: https://github.com/OPCFoundation/UA-.NETStandard-Samples
- NuGet Packages:
- NuGet Packages:
- Types: https://www.nuget.org/packages/OPCFoundation.NetStandard.Opc.Ua.Types/
- Core: https://www.nuget.org/packages/OPCFoundation.NetStandard.Opc.Ua.Core/
- Client: https://www.nuget.org/packages/OPCFoundation.NetStandard.Opc.Ua.Client/
Expand Down
80 changes: 80 additions & 0 deletions .github/workflows/stability-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: Connection Stability Test

on:
schedule:
# Run nightly at 2:00 AM UTC
- cron: '0 2 * * *'
workflow_dispatch:
inputs:
duration:
description: 'Test duration in minutes'
required: false
default: '90'
type: string

jobs:
stability-test:
name: Connection Stability Test
runs-on: ubuntu-latest
timeout-minutes: 120 # Allow extra time beyond test duration for setup/teardown

permissions:
contents: read

env:
DOTNET_VERSION: '10.0.x'
TARGET_FRAMEWORK: 'net10.0'
CONFIGURATION: 'Release'
TEST_DURATION_MINUTES: ${{ github.event.inputs.duration || '90' }}

steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Setup .NET ${{ env.DOTNET_VERSION }}
uses: actions/setup-dotnet@v5
with:
dotnet-version: ${{ env.DOTNET_VERSION }}

- name: Set Cloud Version
shell: pwsh
run: ./.azurepipelines/set-version.ps1

- name: Restore dependencies
run: dotnet restore 'UA.slnx'

- name: Build Client Tests
run: dotnet build ./Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj --configuration ${{ env.CONFIGURATION }} --no-restore

- name: Run Connection Stability Test
run: |
echo "Starting connection stability test for ${{ env.TEST_DURATION_MINUTES }} minutes"
dotnet test ./Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj \
--configuration ${{ env.CONFIGURATION }} \
--no-build \
--framework ${{ env.TARGET_FRAMEWORK }} \
--filter "Category=ConnectionStability" \
--logger "console;verbosity=detailed" \
--results-directory ./TestResults
timeout-minutes: 110 # Slightly longer than expected test duration
env:
TEST_DURATION_MINUTES: ${{ env.TEST_DURATION_MINUTES }}

- name: Upload test results
uses: actions/upload-artifact@v6
with:
name: stability-test-results
path: ./TestResults
if: always()

- name: Check test results
if: always()
run: |
if [ -f ./TestResults/*.trx ]; then
echo "Test results found"
cat ./TestResults/*.trx
else
echo "No test results found"
fi
138 changes: 138 additions & 0 deletions Applications/ConsoleReferenceClient/ClientSamples.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1617,6 +1617,144 @@ public void ExportNodesToNodeSet2(ISession session, IList<INode> nodes, string f
stopwatch.ElapsedMilliseconds);
}

/// <summary>
/// Exports nodes to separate NodeSet2 XML files, one per namespace.
/// Excludes OPC Foundation companion specifications (namespaces starting with http://opcfoundation.org/UA/).
/// </summary>
/// <param name="session">The session to use for exporting.</param>
/// <param name="nodes">The list of nodes to export.</param>
/// <param name="outputDirectory">The directory where NodeSet2 XML files will be saved.</param>
/// <param name="cancellationToken">Optional cancellation token.</param>
/// <returns>A dictionary mapping namespace URI to the file path of the exported NodeSet2 file.</returns>
/// <exception cref="ArgumentNullException">Thrown when session, nodes, or outputDirectory is null.</exception>
/// <exception cref="ArgumentException">Thrown when outputDirectory is empty or whitespace.</exception>
public async Task<IReadOnlyDictionary<string, string>> ExportNodesToNodeSet2PerNamespaceAsync(
ISession session,
IList<INode> nodes,
string outputDirectory,
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));
}

m_logger.LogInformation(
"Exporting {Count} nodes to separate NodeSet2 files per namespace in {Directory}...",
nodes.Count,
outputDirectory);

var stopwatch = Stopwatch.StartNew();

// Ensure output directory exists
Directory.CreateDirectory(outputDirectory);

// Group nodes by namespace, excluding OPC Foundation companion specs
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);
// Exclude OPC Foundation companion specifications
return !string.IsNullOrEmpty(namespaceUri) &&
!namespaceUri.StartsWith("http://opcfoundation.org/UA/", StringComparison.OrdinalIgnoreCase);
})
.ToDictionary(
group => group.Key,
group => group.ToList());

var exportedFiles = new Dictionary<string, string>();

// Export each namespace to its own file
foreach (KeyValuePair<ushort, List<INode>> kvp in nodesByNamespace)
{
cancellationToken.ThrowIfCancellationRequested();

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;
}

/// <summary>
/// Creates a safe filename from a namespace URI.
/// </summary>
/// <param name="namespaceUri">The namespace URI.</param>
/// <param name="namespaceIndex">The namespace index (used as fallback).</param>
/// <returns>A safe filename for the NodeSet2 export.</returns>
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";
}

/// <summary>
/// Create the continuation point collection from the browse result
/// collection for the BrowseNext service.
Expand Down
20 changes: 20 additions & 0 deletions Applications/ConsoleReferenceClient/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using Microsoft.Extensions.Logging;
using Opc.Ua;
using Opc.Ua.Client;
Expand Down Expand Up @@ -94,6 +95,7 @@ public static async Task Main(string[] args)
bool leakChannels = false;
bool forever = false;
bool enableDurableSubscriptions = false;
bool exportNodes = false;

var options = new Mono.Options.OptionSet
{
Expand Down Expand Up @@ -183,6 +185,17 @@ public static async Task Main(string[] args)
}
}
},
{
"e|export",
"Export all fetched nodes into Nodeset2 xml per default",
e =>
{
if (e != null)
{
exportNodes = true;
}
}
},
{
"fa|fetchall",
"Fetch all nodes",
Expand Down Expand Up @@ -560,6 +573,13 @@ r is VariableNode v &&
r.NodeId,
uaClient.Session.NamespaceUris))
];

if (exportNodes)
{
await samples
.ExportNodesToNodeSet2PerNamespaceAsync(uaClient.Session, allNodes, Environment.CurrentDirectory)
.ConfigureAwait(false);
}
}

if (jsonvalues && variableIds != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,10 +265,6 @@
<MaxPublishRequestCount>20</MaxPublishRequestCount>
<MaxSubscriptionCount>100</MaxSubscriptionCount>
<MaxEventQueueSize>10000</MaxEventQueueSize>
<DurableSubscriptionsEnabled>true</DurableSubscriptionsEnabled>
<MaxDurableNotificationQueueSize>10000</MaxDurableNotificationQueueSize>
<MaxDurableEventQueueSize>10000</MaxDurableEventQueueSize>
<MaxDurableSubscriptionLifetimeInHours>10</MaxDurableSubscriptionLifetimeInHours>
<!-- see https://opcfoundation-onlineapplications.org/profilereporting/ for list of available profiles -->
<ServerProfileArray>
<ua:String>http://opcfoundation.org/UA-Profile/Server/StandardUA2017</ua:String>
Expand Down Expand Up @@ -305,7 +301,10 @@
<OperationLimits>
<MaxNodesPerRead>2500</MaxNodesPerRead>
<MaxNodesPerHistoryReadData>1000</MaxNodesPerHistoryReadData>
<MaxNodesPerHistoryReadEvents>1000</MaxNodesPerHistoryReadEvents>
<MaxNodesPerWrite>2500</MaxNodesPerWrite>
<MaxNodesPerHistoryUpdateData>1000</MaxNodesPerHistoryUpdateData>
<MaxNodesPerHistoryUpdateEvents>1000</MaxNodesPerHistoryUpdateEvents>
<MaxNodesPerMethodCall>2500</MaxNodesPerMethodCall>
<MaxNodesPerBrowse>2500</MaxNodesPerBrowse>
<MaxNodesPerRegisterNodes>2500</MaxNodesPerRegisterNodes>
Expand All @@ -315,6 +314,10 @@
</OperationLimits>
<AuditingEnabled>true</AuditingEnabled>
<HttpsMutualTls>true</HttpsMutualTls>
<DurableSubscriptionsEnabled>true</DurableSubscriptionsEnabled>
<MaxDurableNotificationQueueSize>10000</MaxDurableNotificationQueueSize>
<MaxDurableEventQueueSize>10000</MaxDurableEventQueueSize>
<MaxDurableSubscriptionLifetimeInHours>10</MaxDurableSubscriptionLifetimeInHours>
</ServerConfiguration>
<Extensions>
<ua:XmlElement>
Expand Down
2 changes: 1 addition & 1 deletion Applications/ConsoleReferenceServer/UAServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ namespace Quickstarts
public class UAServer<T>
where T : StandardServer, new()
{
public ApplicationInstance Application { get; private set; }
public IApplicationInstance Application { get; private set; }

public ApplicationConfiguration Configuration => Application.ApplicationConfiguration;

Expand Down
Loading
Loading