Skip to content

Commit ca11fe6

Browse files
Copilotromanett
andauthored
Add durable subscription + transfer WinForms sample (#873)
* Plan durable subscription + transfer sample Co-authored-by: romanett <7413710+romanett@users.noreply.github.com> * Add durable subscription + transfer WinForms client sample Co-authored-by: romanett <7413710+romanett@users.noreply.github.com> * Classify recovered values by source timestamp, not keep-alive Co-authored-by: romanett <7413710+romanett@users.noreply.github.com> * Migrate durable subscription client to V2 Opc.Ua.Client.Subscriptions engine Co-authored-by: romanett <7413710+romanett@users.noreply.github.com> * Register durable subscription sample in server/client test coverage Co-authored-by: romanett <7413710+romanett@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>
1 parent e637435 commit ca11fe6

24 files changed

Lines changed: 2141 additions & 2 deletions

‎Tests/SampleClients.Tests/SampleClientTests.cs‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,11 @@ public void UncoveredClientSamplesAreDeclared()
135135
// through a modal dialog, so it cannot be driven by the loop above - it has its
136136
// own fixture in SampleClientFormTests instead. The GDS client hosts no connect
137137
// control either and needs two servers, so it has one in GdsClientTests. The
138-
// aggregation client also needs more than one server and is still open.
139-
string[] expectedGaps = ["Aggregation", "Gds", "Sample"];
138+
// aggregation client also needs more than one server and is still open. The
139+
// durable subscription client hosts no shared connect control and, by design,
140+
// persists its subscription and restarts its own process, which the shared loop
141+
// cannot drive.
142+
string[] expectedGaps = ["Aggregation", "DurableSubscription", "Gds", "Sample"];
140143

141144
IEnumerable<string> uncovered = SampleCatalog.Clients
142145
.Select(sample => sample.Name)

‎Tests/SampleConfiguration.Tests/ConfigurationFileTests.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ public async Task ServerConfigurationHasBaseAddresses(string relativePath)
8686
{
8787
// Samples/ReferenceServer/ReferenceServerHosting.cs
8888
"Samples/ReferenceServer/Quickstarts.ReferenceServer.Config.xml",
89+
// Workshop/DurableSubscription/Server/DurableSubscriptionServerHosting.cs
90+
"Workshop/DurableSubscription/Server/Quickstarts.DurableSubscriptionServer.Config.xml",
8991
// Samples/Server.Net4/Program.cs
9092
"Samples/Server.Net4/Opc.Ua.SampleServer.Config.xml",
9193
// Samples/Client.Net4/Program.cs

‎Tests/Samples.Servers.Hosting/SampleServerFactories.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public static class SampleServerFactories
4646
["Boiler"] = (services, file, configure) => services.AddBoilerServer(file, configure),
4747
["DataAccess"] = (services, file, configure) => services.AddDataAccessServer(file, configure),
4848
["DataTypes"] = (services, file, configure) => services.AddDataTypesServer(file, configure),
49+
["DurableSubscription"] = (services, file, configure) => services.AddDurableSubscriptionServer(file, configure),
4950
["Empty"] = (services, file, configure) => services.AddEmptyServer(file, configure),
5051
["FileTransfer"] = (services, file, configure) => services.AddFileTransferServer(file, configure),
5152
["HistoricalAccess"] = (services, file, configure) => services.AddHistoricalAccessServer(file, configure),

‎Tests/Samples.Servers.Hosting/Samples.Servers.Hosting.csproj‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<ProjectReference Include="..\..\Workshop\Boiler\Server\Boiler Server.csproj" />
2121
<ProjectReference Include="..\..\Workshop\DataAccess\Server\DataAccess Server.csproj" />
2222
<ProjectReference Include="..\..\Workshop\DataTypes\Server\DataTypes Server.csproj" />
23+
<ProjectReference Include="..\..\Workshop\DurableSubscription\Server\DurableSubscription Server.csproj" />
2324
<ProjectReference Include="..\..\Workshop\Empty\Server\Empty Server.csproj" />
2425
<ProjectReference Include="..\..\Workshop\FileTransfer\Server\FileTransfer Server.csproj" />
2526
<ProjectReference Include="..\..\Workshop\HistoricalAccess\Server\HistoricalAccess Server.csproj" />

‎Tests/Samples.Tests.Common/SampleCatalog.cs‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,16 @@ public static class SampleCatalog
130130
ClientEndpointSource = "Workshop/DataTypes/Client/MainForm.cs",
131131
},
132132
new SampleDefinition
133+
{
134+
Name = "DurableSubscription",
135+
ServerProject = "Workshop/DurableSubscription/Server/DurableSubscription Server.csproj",
136+
ServerConfig = "Workshop/DurableSubscription/Server/Quickstarts.DurableSubscriptionServer.Config.xml",
137+
ServerUrl = "opc.tcp://localhost:62581/Quickstarts/DurableSubscriptionServer",
138+
ClientProject = "Workshop/DurableSubscription/Client/DurableSubscription Client.csproj",
139+
ClientConfig = "Workshop/DurableSubscription/Client/Quickstarts.DurableSubscriptionClient.Config.xml",
140+
ClientEndpointSource = "Workshop/DurableSubscription/Client/MainForm.Designer.cs",
141+
},
142+
new SampleDefinition
133143
{
134144
Name = "Empty",
135145
ServerProject = "Workshop/Empty/Server/Empty Server.csproj",

‎UA Quickstart Applications.sln‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NodeManagement Server", "Wo
135135
EndProject
136136
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NodeManagement Client", "Workshop\NodeManagement\Client\NodeManagement Client.csproj", "{D5F29F85-570A-405D-8A8F-E6F991BC3ADB}"
137137
EndProject
138+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DurableSubscription", "DurableSubscription", "{6237BA8C-6D6C-7025-8986-50447A9FC659}"
139+
EndProject
140+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DurableSubscription Server", "Workshop\DurableSubscription\Server\DurableSubscription Server.csproj", "{393EF4FB-D59A-4BCB-8EFA-6AF993D09700}"
141+
EndProject
142+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DurableSubscription Client", "Workshop\DurableSubscription\Client\DurableSubscription Client.csproj", "{294E8E8E-B4E5-40B0-BAA2-21BDE22671FA}"
143+
EndProject
138144
Global
139145
GlobalSection(SolutionConfigurationPlatforms) = preSolution
140146
Debug|Any CPU = Debug|Any CPU
@@ -625,6 +631,30 @@ Global
625631
{D5F29F85-570A-405D-8A8F-E6F991BC3ADB}.Release|x64.Build.0 = Release|Any CPU
626632
{D5F29F85-570A-405D-8A8F-E6F991BC3ADB}.Release|x86.ActiveCfg = Release|Any CPU
627633
{D5F29F85-570A-405D-8A8F-E6F991BC3ADB}.Release|x86.Build.0 = Release|Any CPU
634+
{393EF4FB-D59A-4BCB-8EFA-6AF993D09700}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
635+
{393EF4FB-D59A-4BCB-8EFA-6AF993D09700}.Debug|Any CPU.Build.0 = Debug|Any CPU
636+
{393EF4FB-D59A-4BCB-8EFA-6AF993D09700}.Debug|x64.ActiveCfg = Debug|Any CPU
637+
{393EF4FB-D59A-4BCB-8EFA-6AF993D09700}.Debug|x64.Build.0 = Debug|Any CPU
638+
{393EF4FB-D59A-4BCB-8EFA-6AF993D09700}.Debug|x86.ActiveCfg = Debug|Any CPU
639+
{393EF4FB-D59A-4BCB-8EFA-6AF993D09700}.Debug|x86.Build.0 = Debug|Any CPU
640+
{393EF4FB-D59A-4BCB-8EFA-6AF993D09700}.Release|Any CPU.ActiveCfg = Release|Any CPU
641+
{393EF4FB-D59A-4BCB-8EFA-6AF993D09700}.Release|Any CPU.Build.0 = Release|Any CPU
642+
{393EF4FB-D59A-4BCB-8EFA-6AF993D09700}.Release|x64.ActiveCfg = Release|Any CPU
643+
{393EF4FB-D59A-4BCB-8EFA-6AF993D09700}.Release|x64.Build.0 = Release|Any CPU
644+
{393EF4FB-D59A-4BCB-8EFA-6AF993D09700}.Release|x86.ActiveCfg = Release|Any CPU
645+
{393EF4FB-D59A-4BCB-8EFA-6AF993D09700}.Release|x86.Build.0 = Release|Any CPU
646+
{294E8E8E-B4E5-40B0-BAA2-21BDE22671FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
647+
{294E8E8E-B4E5-40B0-BAA2-21BDE22671FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
648+
{294E8E8E-B4E5-40B0-BAA2-21BDE22671FA}.Debug|x64.ActiveCfg = Debug|Any CPU
649+
{294E8E8E-B4E5-40B0-BAA2-21BDE22671FA}.Debug|x64.Build.0 = Debug|Any CPU
650+
{294E8E8E-B4E5-40B0-BAA2-21BDE22671FA}.Debug|x86.ActiveCfg = Debug|Any CPU
651+
{294E8E8E-B4E5-40B0-BAA2-21BDE22671FA}.Debug|x86.Build.0 = Debug|Any CPU
652+
{294E8E8E-B4E5-40B0-BAA2-21BDE22671FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
653+
{294E8E8E-B4E5-40B0-BAA2-21BDE22671FA}.Release|Any CPU.Build.0 = Release|Any CPU
654+
{294E8E8E-B4E5-40B0-BAA2-21BDE22671FA}.Release|x64.ActiveCfg = Release|Any CPU
655+
{294E8E8E-B4E5-40B0-BAA2-21BDE22671FA}.Release|x64.Build.0 = Release|Any CPU
656+
{294E8E8E-B4E5-40B0-BAA2-21BDE22671FA}.Release|x86.ActiveCfg = Release|Any CPU
657+
{294E8E8E-B4E5-40B0-BAA2-21BDE22671FA}.Release|x86.Build.0 = Release|Any CPU
628658
EndGlobalSection
629659
GlobalSection(SolutionProperties) = preSolution
630660
HideSolutionNode = FALSE
@@ -671,6 +701,8 @@ Global
671701
{F16F0E18-3726-41CA-941B-30A97506EBA1} = {A932037F-857B-D2B6-2036-B9311EB8A26B}
672702
{660D2CB6-6278-4E94-BCE4-18CB598C5A4C} = {B9490B65-C89F-4B63-9816-AE5F6F919FBF}
673703
{D5F29F85-570A-405D-8A8F-E6F991BC3ADB} = {B9490B65-C89F-4B63-9816-AE5F6F919FBF}
704+
{393EF4FB-D59A-4BCB-8EFA-6AF993D09700} = {6237BA8C-6D6C-7025-8986-50447A9FC659}
705+
{294E8E8E-B4E5-40B0-BAA2-21BDE22671FA} = {6237BA8C-6D6C-7025-8986-50447A9FC659}
674706
EndGlobalSection
675707
GlobalSection(ExtensibilityGlobals) = postSolution
676708
SolutionGuid = {2FB5EF9F-9A65-428F-8817-13DF5E39423B}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<configuration>
3+
<system.serviceModel>
4+
<!--
5+
Uncommenting this <diagnostics> block will turn on message logging. The contents and the location of the
6+
log file are specified in the <system.diagnostics> block.
7+
-->
8+
<!--
9+
<diagnostics>
10+
<messageLogging logEntireMessage="true" maxMessagesToLog="3000" logMessagesAtServiceLevel="true" logMalformedMessages="true" logMessagesAtTransportLevel="true"/>
11+
</diagnostics>
12+
-->
13+
</system.serviceModel>
14+
<!--
15+
<system.diagnostics>
16+
<sources>
17+
<source name="System.ServiceModel" switchValue="Verbose, ActivityTracing">
18+
<listeners>
19+
<add type="System.Diagnostics.DefaultTraceListener" name="Default"/>
20+
<add name="ServiceModel Listener"/>
21+
</listeners>
22+
</source>
23+
<source name="System.ServiceModel.MessageLogging">
24+
<listeners>
25+
<add type="System.Diagnostics.DefaultTraceListener" name="Default"/>
26+
<add name="ServiceModel Listener"/>
27+
</listeners>
28+
</source>
29+
</sources>
30+
<sharedListeners>
31+
<add initializeData="Quickstarts.EmptyClient.svclog" type="System.Diagnostics.XmlWriterTraceListener, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" name="ServiceModel Listener" traceOutputOptions="LogicalOperationStack, DateTime, Timestamp, ProcessId, ThreadId, Callstack"/>
32+
</sharedListeners>
33+
</system.diagnostics>
34+
-->
35+
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8"/></startup>
36+
<runtime>
37+
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
38+
<dependentAssembly>
39+
<assemblyIdentity name="System.Numerics.Vectors" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
40+
<bindingRedirect oldVersion="0.0.0.0-4.1.4.0" newVersion="4.1.4.0"/>
41+
</dependentAssembly>
42+
<dependentAssembly>
43+
<assemblyIdentity name="System.Buffers" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
44+
<bindingRedirect oldVersion="0.0.0.0-4.0.3.0" newVersion="4.0.3.0"/>
45+
</dependentAssembly>
46+
<dependentAssembly>
47+
<assemblyIdentity name="System.Security.Cryptography.Cng" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
48+
<bindingRedirect oldVersion="0.0.0.0-4.3.1.0" newVersion="4.3.1.0"/>
49+
</dependentAssembly>
50+
<dependentAssembly>
51+
<assemblyIdentity name="Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets" publicKeyToken="adb9793829ddae60" culture="neutral"/>
52+
<bindingRedirect oldVersion="0.0.0.0-2.2.1.0" newVersion="2.2.1.0"/>
53+
</dependentAssembly>
54+
</assemblyBinding>
55+
</runtime>
56+
</configuration>
3.19 KB
Binary file not shown.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework>net10.0-windows</TargetFramework>
4+
<OutputType>WinExe</OutputType>
5+
<RootNamespace>Quickstarts.DurableSubscriptionClient</RootNamespace>
6+
<AssemblyName>Quickstarts.DurableSubscriptionClient</AssemblyName>
7+
<ApplicationIcon>App.ico</ApplicationIcon>
8+
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
9+
<UseWindowsForms>true</UseWindowsForms>
10+
</PropertyGroup>
11+
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
12+
<OutputPath>..\..\..\bin\Debug\</OutputPath>
13+
</PropertyGroup>
14+
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
15+
<OutputPath>..\..\..\bin\Release\</OutputPath>
16+
</PropertyGroup>
17+
<ItemGroup>
18+
<Content Include="Quickstarts.DurableSubscriptionClient.Config.xml">
19+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
20+
<SubType>Designer</SubType>
21+
</Content>
22+
<EmbeddedResource Include="App.ico">
23+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
24+
</EmbeddedResource>
25+
</ItemGroup>
26+
<ItemGroup>
27+
<ProjectReference Include="..\..\..\Samples\ClientControls.Net4\UA Client Controls.csproj" />
28+
<ProjectReference Include="..\..\..\Samples\Client.Common\Opc.Ua.Samples.Client.csproj" />
29+
<ProjectReference Include="..\..\..\Samples\Hosting\Opc.Ua.Samples.Hosting.csproj" />
30+
</ItemGroup>
31+
<ItemGroup>
32+
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client">
33+
<Version>$(OpcUaNetStandardVersion)</Version>
34+
</PackageReference>
35+
</ItemGroup>
36+
</Project>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/* ========================================================================
2+
* Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
3+
*
4+
* OPC Foundation MIT License 1.00
5+
*
6+
* The complete license agreement can be found here:
7+
* http://opcfoundation.org/License/MIT/1.00/
8+
* ======================================================================*/
9+
10+
using Microsoft.Extensions.DependencyInjection.Extensions;
11+
using Quickstarts.DurableSubscriptionClient.Model;
12+
13+
namespace Microsoft.Extensions.DependencyInjection
14+
{
15+
/// <summary>
16+
/// The composition root of the durable subscription client sample: the client model
17+
/// its window is built on.
18+
/// </summary>
19+
/// <remarks>
20+
/// The counterpart of the <c>AddDurableSubscriptionServer</c> of the server half. The
21+
/// window takes the model through its constructor and the container creates both, so
22+
/// the entry point of the sample and the tests which drive its window share this one
23+
/// registration.
24+
///
25+
/// The model is a singleton because there is one window and the model is its
26+
/// connection to the server. It is created when the window is - on the thread which
27+
/// owns the message loop - which is what lets the model raise its events there.
28+
/// </remarks>
29+
public static class DurableSubscriptionClientHosting
30+
{
31+
/// <summary>
32+
/// Registers the client model of the durable subscription sample.
33+
/// </summary>
34+
/// <param name="services">The service collection.</param>
35+
public static IServiceCollection AddDurableSubscriptionClient(this IServiceCollection services)
36+
{
37+
services.TryAddSingleton<DurableSubscriptionClientModel>();
38+
39+
return services;
40+
}
41+
}
42+
}

0 commit comments

Comments
 (0)