Skip to content

Drop the per-sample StandardServer subclasses for a builder-only DI surface - #861

Merged
romanett merged 1 commit into
masterfrom
romanett/di-samples-surface-5e32a5
Sep 4, 2026
Merged

romanett merged 1 commit into
masterfrom
romanett/di-samples-surface-5e32a5

Conversation

@romanett

@romanett romanett commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Proposed changes

The DI surface of the server samples is now one shape, and no sample has a StandardServer subclass any more.

Every Add<Sample>Server composition root is

services.AddSampleServer(
    configurationFile ?? ConfigurationFile,
    server => server.AddNodeManager<BoilerNodeManagerFactory>(),
    configure);

The server behind it is one shared SampleServer in Samples/Hosting - the stack's DependencyInjectionStandardServer, whose only override derives ServerProperties from the configuration. The 13 samples whose server class did nothing but name the product (Boiler, AlarmCondition, DataAccess, DataTypes, Empty, FileTransfer, HistoricalAccess, Methods, NodeManagement, PerfTest, SimpleEvents, StateMachines, Views) lost it outright.

What the other server classes overrode became a registration on the server builder of the stack:

Was Is
CreateRoleManager + AddIdentity (RoleManagement, AliasNames) server.ConfigureRoles(...) of the stack
OnServerStarted registering authenticators (RoleManagement, AliasNames, UserAuthentication, UA sample) server.AddIdentityAuthenticator((sp, validator) => new UserNamePasswordAuthenticator(...))
OnNodeManagerStartedAsync setting the history capabilities (HistoricalEvents) server.AddStartupTask<HistoricalEventsCapabilities>(), an IServerStartupTask
CreateResourceManager translations (UserAuthentication) server.AddStartupTask<UserAuthenticationTranslations>()
server factory adding one node manager per configured endpoint (Aggregation) server.AddNodeManagers(provider => ...), run with the configuration loaded
LoadServerProperties everywhere derived from ApplicationName / ProductUri of the configuration

The samples built around their ApplicationInstance (UA sample server, UA sample client, GDS WinForms server) run their server on the same stack pipeline: SampleApplication implements the stack's IOpcUaApplicationConfigurationProvider, and AddSampleServer(server => ...) without a file hosts the server on it. That removes the IServerBase path, AddSampleNodeManager<T>(), the Func<IServiceProvider, TServer> overloads and two unused options from the hosting library; SampleApplicationHostedService only loads the configuration eagerly for the forms now.

Three AddSampleServer<TServer> users remain, each for a reason stated at the call site: AliasNames (AliasNamesServer : SampleServer registers and materializes the standard TagVariables alias store before the address space is built, which the stack's AddAliasNameStore does too late), ReferenceServer (the quickstart library's own class), GDS (the server takes databases and certificate groups and is registered with AddSingleton(CreateServer) first).

The helpers the stack lacks are one-liners over its public registration types in SampleServerBuilderExtensions (AddNodeManager(instance), AddNodeManagers(factories), AddIdentityAuthenticator(factory), AddStartupTask<T>()). The corresponding SDK gaps are filed as OPCFoundation/UA-.NETStandard#4410; once they land, SampleServer, those helpers and the AliasNamesServer subclass can go.

Behavior changes a reviewer should know about:

  • Server-side reverse connect is gone for the Aggregation and UA sample servers: DependencyInjectionStandardServer is not a ReverseConnectServer. Both configurations had no reverse connect clients enabled; the console aggregation server no longer prints that section.
  • Product names now come from the configuration (ApplicationName, ProductUri) instead of per-sample strings.
  • The UA sample's X509 check is the stack's X509Authenticator against the Users trust list. The UserAuthentication sample lost its dead impersonation stubs (#if TODO, Kerberos placeholders).
  • The reference server entry point lost an unused server factory left over from the previous migration.

Also documented in Samples/Hosting/README.md.

Related Issues

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • Enhancement (non-breaking change which adds functionality)
  • Test enhancement (non-breaking change to increase test coverage)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected, requires version increase of Nuget packages)
  • Documentation Update (if none of the other choices apply)

Checklist

  • I have read the CONTRIBUTING doc.
  • I have signed the CLA.
  • I ran tests locally with my changes, all passed.
  • I fixed all failing tests in the CI pipelines.
  • I fixed all introduced issues with CodeQL and LGTM.
  • I have added tests that prove my fix is effective or that my feature works and increased code coverage.
  • I have added necessary documentation (if appropriate).
  • Any dependent changes have been merged and published in downstream modules.

Further comments

Verified locally: full UA Samples.slnx build with no new warnings; SampleServers.Tests 115 passed (hosts every sample server, the GDS and aggregation consoles included); SampleNodeManagers.Tests 160 passed, 3 skipped as before (roles, alias names, user authentication, event history capabilities); HostedServerBootstrapTests 3 passed, including the new AServerRunsOnTheApplicationInstanceOfTheSample, which covers the application-instance path the UA sample and GDS servers now use.

Alternatives considered: a shared IMainNodeManagerFactory decorator seam in the hosting library for AliasNames (rejected, one sample does not justify an invented seam), and keeping ReverseConnectServer as the shared base (rejected, it would lose the DI hooks of DependencyInjectionStandardServer for everyone).

🤖 Generated with Claude Code

…urface

Every server sample now registers its server as
services.AddSampleServer(file, server => server.AddNodeManager<TFactory>())
on one shared SampleServer (the stack's DependencyInjectionStandardServer
with ServerProperties derived from the configuration). What the deleted
server classes overrode is a registration on the server builder instead:
ConfigureRoles for the role mappings, AddIdentityAuthenticator for the
authenticators, AddStartupTask for the event history capabilities and the
resource translations, AddNodeManagers for the per-endpoint aggregation
node managers. The samples built around their ApplicationInstance
(Server.Net4, Client.Net4, GDS) run their server on the same stack pipeline
through IOpcUaApplicationConfigurationProvider, which removes the IServerBase
path, AddSampleNodeManager and the factory overloads from the hosting library.

AliasNames keeps a subclass of SampleServer for the standard TagVariables
alias store, ReferenceServer uses the quickstart library class, and GDS
registers its server itself before AddSampleServer<TServer>. The seams the
stack still lacks are filed as OPCFoundation/UA-.NETStandard#4410.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@romanett
romanett merged commit 6b85dc5 into master Sep 4, 2026
9 checks passed
romanett added a commit that referenced this pull request Sep 4, 2026
Master #861 dropped the per-sample StandardServer subclasses for a
builder-only DI surface, which touched three of the files this branch changes.

The interesting resolution is Samples/Hosting/SampleApplicationHostedService.cs,
where this branch is taken out entirely in favour of master. This branch had
that service assign the container's ITransportBindingRegistry to the server,
because a server started down the ApplicationInstance path would otherwise fall
back to a private registry that only knows opc.tcp - so an AddHttpsTransport()
registration never reached it. After #861 no sample starts a server that way:
every one of them goes through AddSampleServer(configureServer) and the hosted
server of the stack, which already assigns TransportBindings from dependency
injection (OpcUaServerHostedService.cs:219). The workaround is obsolete and
master's simpler service is correct.

The other three keep master's shape and re-apply the transport registration on
top: the two WinForms entry points move to
AddSampleServer(server => server.AddUaSampleServer()) and the reference server
to AddSampleServer<ReferenceServer>(...) with its node managers on the builder,
each still calling services.AddOpcUa().AddHttpsTransport() so the https base
address in their configuration is actually served.

Everything else merged cleanly. The ITimeProviderProvider seams, the
ModelChangeTracker in the NodeManagement client and the deletions all survive.

All four tiers pass on the merged tree: 227 configuration, 116 server (one more
than before, from master), 165 node manager and 61 client tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
romanett added a commit that referenced this pull request Sep 4, 2026
…ver surface

#861 landed on master while this was open and deleted every per-sample
StandardServer subclass, RoleManagementServer.cs among them, in favour of
registrations on the server builder. The conflict is a real one rather than
textual: this branch had added a Role configuration and an OnServerStarted
override to exactly the class master removed.

Resolved by keeping master's shape and porting the additions onto the new seams:

* the X509Subject rule which grants ConfigureAdmin for the certificate of the
  maintenance workstation joins the other role mappings in
  SampleUsers.ConfigureRoles, as a RoleDefinitionOptions on the well known Role;
  WorkstationCertificateSubject and WorkstationRoleId move there with it.
* the Endpoints filter becomes WorkstationEndpoints, an IServerStartupTask
  registered with AddStartupTask<T>. It still cannot be declared with the rest of
  the role configuration: AddEndpoint refuses an entry with an empty EndpointUrl
  (UA-.NETStandard#4412), so the endpoints the server advertises have to be
  copied, and those are only known once it has started. Declaring the wildcard in
  RoleConfigurationOptions would be worse than the startup task, because the
  stack applies those entries without looking at what AddEndpoint answered - the
  Role would end up granted on every endpoint.

The tier 1.5 fixture follows the constant to SampleUsers; the README and the node
manager comment follow the code.

Tier 1 116 passed, tier 1.5 170 passed with 4 skipped (one of them the recorded
audit known issue), tier 2 63 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant