Summary
Modernize the sample and workshop applications to bootstrap OPC UA clients and
servers through the Microsoft.Extensions.DependencyInjection surface that the
v2 UA-.NETStandard libraries already expose, combined with the fluent
IOpcUaBuilder pattern. This is not just about the configuration builder — the
v2 stack ships a full DI surface (services.AddOpcUa() → IOpcUaBuilder →
.AddServer(...) / .AddClient(...) / .AddNodeManager<T>() / ConfigureApplication(...),
DI‑resolved ITelemetryContext, and IHostedService lifetime via the .NET Generic
Host). The samples should consume that surface directly so they demonstrate the
idiomatic, supported way to wire up the stack.
Reference: docs/DependencyInjection.md
in UA-.NETStandard.
Motivation
Every sample today repeats the same imperative bootstrap. For example
Workshop/Boiler/Server/Program.cs and Samples/ReferenceServer/Program.cs:
public sealed class ConsoleTelemetry : TelemetryContextBase
{
public ConsoleTelemetry()
: base(LoggerFactory.Create(builder =>
{
builder.SetMinimumLevel(LogLevel.Information);
builder.AddConsole();
}))
{ }
}
static void Main()
{
ApplicationInstance.MessageDlg = new ApplicationMessageDlg();
ApplicationInstance application = new ApplicationInstance(m_telemetry);
application.ApplicationType = ApplicationType.Server;
application.ConfigSectionName = "BoilerServer";
application.LoadApplicationConfigurationAsync(false).AsTask().Wait();
application.CheckApplicationInstanceCertificatesAsync(false).AsTask().Wait();
application.StartAsync(new BoilerServer(m_telemetry)).Wait();
// ...
}
Problems:
- Duplicated boilerplate across ~30 client/server programs
(Workshop/**/Program.cs, Samples/ReferenceClient, Samples/ReferenceServer,
Samples/GDS/**, Samples/LDS).
- Hand-rolled telemetry via a per-sample
ConsoleTelemetry : TelemetryContextBase
instead of the DI-provided ITelemetryContext (ServiceProviderTelemetryContext)
that resolves the host's ILoggerFactory.
- Blocking async (
.AsTask().Wait() / .Result) in entry points — poor
practice for code people copy as a starting point.
- No dependency injection — servers, node managers, and even WinForms forms are
new-ed up directly, so nothing can be composed, replaced, or unit-tested.
- The samples do not exercise the v2 DI surface at all, so they fail to show
users the recommended pattern.
Proposal
Rebootstrap the samples on the existing v2 DI surface (do not reinvent it):
- Root + generic host. Use
Host.CreateApplicationBuilder(args) and
builder.Services.AddOpcUa(). Server features register an IHostedService
(OpcUaServerHostedService) so the Generic Host owns lifetime, certificate
setup, and Ctrl+C/SIGTERM.
- Fluent feature registration. Use
.AddServer(o => ...) /
.AddClient(o => ...) and .AddNodeManager<T>() / .AddSyncNodeManager<T>()
for the quickstart node managers, and ConfigureApplication(...) where a shared
application identity/certificate lifecycle is needed.
- DI-resolved telemetry/logging. Delete the per-sample
ConsoleTelemetry
classes; configure logging via builder.Logging.AddConsole() /
.AddLogging(b => b.AddConsole()) and let ITelemetryContext come from DI.
- Async all the way.
static async Task Main + await builder.Build().RunAsync()
instead of .Wait()/.Result.
Illustrative console server (Boiler)
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Logging.AddConsole();
builder.Services
.AddOpcUa()
.AddServer(o =>
{
o.ApplicationName = "Boiler Server";
o.ApplicationUri = "urn:localhost:Boiler";
o.EndpointUrls.Add("opc.tcp://localhost:51210/Boiler");
o.AutoAcceptUntrustedCertificates = false;
})
.AddNodeManager<BoilerNodeManagerFactory>();
await builder.Build().RunAsync();
Illustrative console client
builder.Services
.AddOpcUa()
.AddClient(o =>
{
o.ApplicationName = "Boiler Client";
o.Session = new ManagedSessionOptions { SessionName = "BoilerClient" };
});
WinForms samples — use constructor injection
The WinForms samples (Samples/ReferenceServer, Samples/ReferenceClient,
Samples/GDS/Client, Samples/Server.Net4, Samples/Client.Net4,
Controls.Net4/ServerControls.Net4) should also move to DI. Today the forms
are constructed by hand, e.g.:
Application.Run(new ServerForm(application, m_telemetry, showCertificateValidationDialog));
Forms already take their dependencies through their constructors
(ServerForm(ApplicationInstance, ITelemetryContext, ...)), so they are a natural
fit for constructor injection. Build the container, register the form, and
resolve it from the provider instead of new-ing it:
using IHost host = builder.Build();
// forms resolve ApplicationInstance / ITelemetryContext / options via their ctor
using var scope = host.Services.CreateScope();
var form = scope.ServiceProvider.GetRequiredService<ServerForm>();
// (or ActivatorUtilities.CreateInstance<ServerForm>(host.Services) if the form
// is not itself registered)
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(form);
This removes the manual ApplicationInstance/ConsoleTelemetry plumbing from the
WinForms Program.cs while preserving the interactive UI behavior.
Scope
Workshop/**/Client/Program.cs and Workshop/**/Server/Program.cs
(Aggregation, AlarmCondition, Boiler, DataAccess, DataTypes, Empty,
HistoricalAccess, HistoricalEvents, Methods, PerfTest, SimpleEvents,
UserAuthentication, Views).
Samples/ReferenceClient, Samples/ReferenceServer (WinForms).
Samples/GDS/** (Client, Server, ConsoleServer) — AddGdsServer(...) /
AddGdsClient(...).
Samples/LDS — AddLdsServer(...).
- WinForms samples via constructor injection (see above).
Considerations / preserve behavior
- Keep config section names, certificate handling, extension configuration
(e.g. ReferenceServerConfiguration / ShowCertificateValidationDialog), and
interactive ServerForm/UI behavior intact.
- Where a sample must retain the manual constructors, that's fine — the DI surface
is additive (new ApplicationInstance(telemetry) etc. remain supported); prefer
DI for the entry points.
Acceptance criteria
Open questions
- Should a shared registration helper (e.g. under
Workshop/Common) wrap the
common AddOpcUa()... chain for the Workshop samples, or should each Program.cs
show the full chain for didactic clarity?
- For WinForms, register each
Form in the container, or resolve via
ActivatorUtilities.CreateInstance without registration?
- Do we standardize all console samples on
Host.CreateApplicationBuilder (hosted
services), or use a lighter ServiceCollection + manual provider where the sample
is intentionally minimal?
Summary
Modernize the sample and workshop applications to bootstrap OPC UA clients and
servers through the
Microsoft.Extensions.DependencyInjectionsurface that thev2
UA-.NETStandardlibraries already expose, combined with the fluentIOpcUaBuilderpattern. This is not just about the configuration builder — thev2 stack ships a full DI surface (
services.AddOpcUa()→IOpcUaBuilder→.AddServer(...)/.AddClient(...)/.AddNodeManager<T>()/ConfigureApplication(...),DI‑resolved
ITelemetryContext, andIHostedServicelifetime via the .NET GenericHost). The samples should consume that surface directly so they demonstrate the
idiomatic, supported way to wire up the stack.
Reference:
docs/DependencyInjection.mdin
UA-.NETStandard.Motivation
Every sample today repeats the same imperative bootstrap. For example
Workshop/Boiler/Server/Program.csandSamples/ReferenceServer/Program.cs:Problems:
(
Workshop/**/Program.cs,Samples/ReferenceClient,Samples/ReferenceServer,Samples/GDS/**,Samples/LDS).ConsoleTelemetry : TelemetryContextBaseinstead of the DI-provided
ITelemetryContext(ServiceProviderTelemetryContext)that resolves the host's
ILoggerFactory..AsTask().Wait()/.Result) in entry points — poorpractice for code people copy as a starting point.
new-ed up directly, so nothing can be composed, replaced, or unit-tested.users the recommended pattern.
Proposal
Rebootstrap the samples on the existing v2 DI surface (do not reinvent it):
Host.CreateApplicationBuilder(args)andbuilder.Services.AddOpcUa(). Server features register anIHostedService(
OpcUaServerHostedService) so the Generic Host owns lifetime, certificatesetup, and Ctrl+C/SIGTERM.
.AddServer(o => ...)/.AddClient(o => ...)and.AddNodeManager<T>()/.AddSyncNodeManager<T>()for the quickstart node managers, and
ConfigureApplication(...)where a sharedapplication identity/certificate lifecycle is needed.
ConsoleTelemetryclasses; configure logging via
builder.Logging.AddConsole()/.AddLogging(b => b.AddConsole())and letITelemetryContextcome from DI.static async Task Main+await builder.Build().RunAsync()instead of
.Wait()/.Result.Illustrative console server (
Boiler)Illustrative console client
WinForms samples — use constructor injection
The WinForms samples (
Samples/ReferenceServer,Samples/ReferenceClient,Samples/GDS/Client,Samples/Server.Net4,Samples/Client.Net4,Controls.Net4/ServerControls.Net4) should also move to DI. Today the formsare constructed by hand, e.g.:
Forms already take their dependencies through their constructors
(
ServerForm(ApplicationInstance, ITelemetryContext, ...)), so they are a naturalfit for constructor injection. Build the container, register the form, and
resolve it from the provider instead of
new-ing it:This removes the manual
ApplicationInstance/ConsoleTelemetryplumbing from theWinForms
Program.cswhile preserving the interactive UI behavior.Scope
Workshop/**/Client/Program.csandWorkshop/**/Server/Program.cs(Aggregation, AlarmCondition, Boiler, DataAccess, DataTypes, Empty,
HistoricalAccess, HistoricalEvents, Methods, PerfTest, SimpleEvents,
UserAuthentication, Views).
Samples/ReferenceClient,Samples/ReferenceServer(WinForms).Samples/GDS/**(Client, Server, ConsoleServer) —AddGdsServer(...)/AddGdsClient(...).Samples/LDS—AddLdsServer(...).Considerations / preserve behavior
(e.g.
ReferenceServerConfiguration/ShowCertificateValidationDialog), andinteractive
ServerForm/UI behavior intact.is additive (
new ApplicationInstance(telemetry)etc. remain supported); preferDI for the entry points.
Acceptance criteria
Host.CreateApplicationBuilder+services.AddOpcUa().AddServer/AddClient(...)and run under the Generic Host..AddNodeManager<T>()/.AddSyncNodeManager<T>()rather than manual server construction.ConsoleTelemetryclasses are removed;ITelemetryContextandlogging come from DI.
injection (
GetRequiredService<T>()/ActivatorUtilities.CreateInstance)..Wait()/.Resultin sample entry points replaced withasync/await.behavioral regressions.
Open questions
Workshop/Common) wrap thecommon
AddOpcUa()...chain for the Workshop samples, or should eachProgram.csshow the full chain for didactic clarity?
Formin the container, or resolve viaActivatorUtilities.CreateInstancewithout registration?Host.CreateApplicationBuilder(hostedservices), or use a lighter
ServiceCollection+ manual provider where the sampleis intentionally minimal?