mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 22:55:08 +08:00
90fc3cca2f
# Description Completes the UA-.NETStandard implementation of the final OPC UA WoT Binding 1.1 semantics and aligns the WoT Connectivity 1.1 model with xRegistry 0.4.0. The change: - retires `uav:isEvent` and `uav:severity` from current Binding output while preserving unknown members through the generic converter's ordinary residue mechanism; - removes `uav:severity` from the asset Thing Description parser and materializer, so document metadata cannot set occurrence severity; - implements logical EventType definitions, ordered resolution providers, explicit/default selection precedence, materialized member paths, and the built-in `BaseEventType` fallback; - adds deterministic portable identity and browse-path anchoring; - populates bound type declarations instead of creating duplicate siblings, including inherited and closed-content validation; - resolves and validates `uav:externalSchema` without overriding authoritative DataType definitions; - hardens DataType dimensions, native projections, preservation envelopes, URI transmission, projection limits, and delete-policy handling; - synchronizes the xRegistry and WoT Connectivity source-generation inputs; - pins all 27 specification examples, all 97 implementation-owned normative requirements, and all 12 cross-language vector groups to the final specification revision; - adds `samples/WotCon/run-aggregation-demo.ps1` for a verified one-command aggregation client/server round trip and replaces the README topology sketch with Mermaid; - normalizes redirected PowerShell diagnostic presentation in the statement-digest test harness so its semantic assertions are stable on Windows, Linux, and macOS. WoT Connectivity remains model version `1.1`; draft iterations are identified by release labels such as `1.1-draft5`. **Specification dependency:** the evidence assets pin `OPCF-Members/spec-drafts` `main` commit `25db507121b503fd31c8816fbfae67a6822e813b`. The corresponding specification follow-up is `OPCF-Members/spec-drafts#35`. ## Validation - Latest GitHub/Azure CI: 266/266 checks passed. - .NET 10 feedback-focused tests: - WoT Connectivity event/model/traceability: 72 passed - binding capability/traceability: 8 passed - coverage-gate tests: 25 passed - Complete .NET 10 Tools project: 752 passed, 1 intentionally skipped. - .NET Framework 4.8: - WoT Connectivity event/model/traceability: 72 passed - WotCon bindings build: 0 warnings, 0 errors - NativeAOT build: 0 warnings, 0 errors. - WoT aggregation demo: 16 documents uploaded and all 10 mapped values read with Good status; exact processes and isolated PKI stores cleaned up. - Specification traceability: 402 normative statements mapped; all 97 implementation-owned statements have passing test evidence with zero gaps. - Shared conformance vectors: 12/12 groups execute. - Final CI coverage: - project line coverage: 86.83% (floor 70%) - project branch coverage: 76.71% (floor 60%) - patch coverage: 100.00% (2,591/2,591 changed lines) - coverage gate passed ## Related Issues Follow-up to #4375 and alignment with `OPCF-Members/spec-drafts#31` and `OPCF-Members/spec-drafts#35`. No separate tracking issue was opened for this follow-up. ## Checklist - [x] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce new build or analyzer warnings in the changed paths. - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [x] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [x] I have addressed all PR feedback currently received. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6a9f41b1-72f0-4c74-bd7f-26fa531e5738
236 lines
9.5 KiB
C#
236 lines
9.5 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2026 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.Globalization;
|
|
using System.IO;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using Opc.Ua;
|
|
using Opc.Ua.Client;
|
|
using Opc.Ua.WotCon.Bindings;
|
|
using Opc.Ua.WotCon.Bindings.OpcUa;
|
|
using Opc.Ua.WotCon.Server;
|
|
|
|
namespace AggregationServer
|
|
{
|
|
/// <summary>
|
|
/// Builds and runs the reusable generic aggregation host.
|
|
/// </summary>
|
|
public static class AggregationServerHost
|
|
{
|
|
/// <summary>
|
|
/// Builds a host from explicit options.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
public static IHost Build(AggregationServerOptions options)
|
|
{
|
|
if (options is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(options));
|
|
}
|
|
|
|
HostApplicationBuilder builder = Host.CreateApplicationBuilder();
|
|
Configure(builder, options);
|
|
return builder.Build();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds and runs a host from explicit options.
|
|
/// </summary>
|
|
public static async Task RunAsync(
|
|
AggregationServerOptions options,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
using IHost host = Build(options);
|
|
await host.RunAsync(cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds and runs a host from command-line configuration.
|
|
/// </summary>
|
|
public static async Task RunAsync(
|
|
string[] args,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
|
|
var options = new AggregationServerOptions
|
|
{
|
|
EndpointUrl = builder.Configuration["endpoint"],
|
|
Host = builder.Configuration["host"] ?? "localhost",
|
|
Port = ReadPort(builder.Configuration),
|
|
ApplicationName =
|
|
builder.Configuration["applicationName"] ?? "AggregationServer",
|
|
PkiRoot = builder.Configuration["pkiRoot"],
|
|
MaximumDocumentBytes = ReadMaximumDocumentBytes(builder.Configuration)
|
|
};
|
|
Configure(builder, options);
|
|
using IHost host = builder.Build();
|
|
await host.RunAsync(cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
private static void Configure(
|
|
HostApplicationBuilder builder,
|
|
AggregationServerOptions options)
|
|
{
|
|
Validate(options);
|
|
builder.Logging.ClearProviders();
|
|
builder.Logging.AddConsole();
|
|
builder.Services.AddSingleton(options);
|
|
|
|
string endpoint = options.EndpointUrl ??
|
|
$"opc.tcp://{options.Host}:{options.Port}/AggregationServer";
|
|
IOpcUaBuilder opcUa = builder.Services.AddOpcUa();
|
|
opcUa.AddOpcTcpTransport();
|
|
opcUa.AddServer(server =>
|
|
{
|
|
server.ApplicationName = options.ApplicationName;
|
|
server.ApplicationUri =
|
|
$"urn:localhost:OPCFoundation:{options.ApplicationName}";
|
|
server.ProductUri = "uri:opcfoundation.org:AggregationServer";
|
|
if (!string.IsNullOrWhiteSpace(options.PkiRoot))
|
|
{
|
|
server.PkiRoot = options.PkiRoot;
|
|
}
|
|
server.AutoAcceptUntrustedCertificates = true;
|
|
server.IncludeUnsecurePolicyNone = true;
|
|
server.EndpointUrls.Add(endpoint);
|
|
});
|
|
opcUa.AddWotRegistryServer(registry =>
|
|
{
|
|
registry.AutoRefresh = false;
|
|
registry.Bounds.MaxDocumentBytes = options.MaximumDocumentBytes;
|
|
registry.ManagementAccess = new WotManagementAccessPolicy
|
|
{
|
|
MinimumSecurityMode = MessageSecurityMode.None,
|
|
AllowAnonymous = true,
|
|
RequiredRoleId = ObjectIds.WellKnownRole_Anonymous
|
|
};
|
|
});
|
|
|
|
opcUa.AddClient(client =>
|
|
{
|
|
client.ApplicationName = $"{options.ApplicationName}.Upstream";
|
|
client.ApplicationUri =
|
|
$"urn:localhost:OPCFoundation:{options.ApplicationName}.Upstream";
|
|
client.ProductUri = "uri:opcfoundation.org:AggregationServer";
|
|
if (!string.IsNullOrWhiteSpace(options.PkiRoot))
|
|
{
|
|
client.PkiRoot = Path.Combine(options.PkiRoot, "upstream");
|
|
}
|
|
client.AutoAcceptUntrustedCertificates = true;
|
|
client.Session = new ManagedSessionOptions
|
|
{
|
|
SessionName = $"{options.ApplicationName}.Upstream",
|
|
SessionTimeout = TimeSpan.FromSeconds(60)
|
|
};
|
|
}).AddManagedClientPool();
|
|
|
|
opcUa.AddHttpWotBinding();
|
|
opcUa.AddModbusWotBinding();
|
|
|
|
// The aggregation topology deliberately federates source servers that run on the
|
|
// same host as this server, so the loopback gate the default policy applies must be
|
|
// opened explicitly. Keep every other check (scheme, blocked hosts, private ranges)
|
|
// at its secure default.
|
|
opcUa.AddWotEndpointPolicy(new WotEndpointPolicy { AllowLoopback = true });
|
|
|
|
builder.Services.AddSingleton<IWotBindingExecutor>(serviceProvider =>
|
|
{
|
|
IManagedSessionPool pool =
|
|
serviceProvider.GetRequiredService<IManagedSessionPool>();
|
|
return new OpcUaWotBindingExecutor(new OpcUaWotBindingOptions
|
|
{
|
|
DisposeSession = false,
|
|
SessionFactory = async (url, ct) =>
|
|
{
|
|
var endpointDescription = new EndpointDescription
|
|
{
|
|
EndpointUrl = url,
|
|
SecurityMode = MessageSecurityMode.None,
|
|
SecurityPolicyUri = SecurityPolicies.None
|
|
};
|
|
var configuredEndpoint = new ConfiguredEndpoint(
|
|
null,
|
|
endpointDescription,
|
|
null);
|
|
return await pool.GetOrConnectAsync(url, configuredEndpoint, ct)
|
|
.ConfigureAwait(false);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
private static int ReadPort(ConfigurationManager configuration)
|
|
{
|
|
return int.TryParse(
|
|
configuration["port"],
|
|
NumberStyles.Integer,
|
|
CultureInfo.InvariantCulture,
|
|
out int port)
|
|
? port
|
|
: 62550;
|
|
}
|
|
|
|
private static int ReadMaximumDocumentBytes(ConfigurationManager configuration)
|
|
{
|
|
return int.TryParse(
|
|
configuration["maximumDocumentBytes"],
|
|
NumberStyles.Integer,
|
|
CultureInfo.InvariantCulture,
|
|
out int maximumDocumentBytes)
|
|
? maximumDocumentBytes
|
|
: 32 * 1024 * 1024;
|
|
}
|
|
|
|
private static void Validate(AggregationServerOptions options)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(options.ApplicationName))
|
|
{
|
|
throw new ArgumentException("ApplicationName is required.", nameof(options));
|
|
}
|
|
if (options.MaximumDocumentBytes <= 0)
|
|
{
|
|
throw new ArgumentException(
|
|
"MaximumDocumentBytes must be positive.",
|
|
nameof(options));
|
|
}
|
|
if (options.EndpointUrl is null &&
|
|
(string.IsNullOrWhiteSpace(options.Host) || options.Port is < 1 or > 65535))
|
|
{
|
|
throw new ArgumentException("A valid host and port are required.", nameof(options));
|
|
}
|
|
}
|
|
}
|
|
}
|