Skip to content

Use Dependency Injection + Fluent Builder pattern in the samples #772

Description

@romanett

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):

  1. 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.
  2. 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.
  3. 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.
  4. 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/LDSAddLdsServer(...).
  • 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

  • Console samples bootstrap via Host.CreateApplicationBuilder +
    services.AddOpcUa().AddServer/AddClient(...) and run under the Generic Host.
  • Quickstart node managers are registered with .AddNodeManager<T>() /
    .AddSyncNodeManager<T>() rather than manual server construction.
  • Per-sample ConsoleTelemetry classes are removed; ITelemetryContext and
    logging come from DI.
  • WinForms samples resolve their forms from the DI container using constructor
    injection (GetRequiredService<T>() / ActivatorUtilities.CreateInstance).
  • Blocking .Wait()/.Result in sample entry points replaced with
    async/await.
  • All converted samples build and run against a compatible peer with no
    behavioral regressions.
  • READMEs/comments referencing the old bootstrap flow are updated.

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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions