This document lists the coding and design guidelines for ServiceControl. When it conflicts with other coding and design guidelines, this document takes precedence for ServiceControl.
Microsoft maintains a number of abstractions for common cross-cutting concerns in the framework libraries and in the Microsoft.Extensions.* packages. Where such an abstraction exists, use it instead of any other abstraction, including one of our own.
Relying on relatively stable abstractions makes it easier to isolate the application from third-party dependencies. It also keeps parts of the application isolated from each other, for example running the embedded database in maintenance mode without starting the NServiceBus endpoint.
For example:
- Prefer
IHostBuilderextensions over NServiceBus features: Add a new feature as an extension toIHostBuilderinstead of a NServiceBusFeatureimplementation, unless it specifically alters the NServiceBus endpoint. Some existing ServiceControl features still use theFeatureabstraction to register components. Migrate those that do not modify the NServiceBus endpoint toIHostBuilderextensions over time. - Prefer
IHostedServiceto NServiceBusFeatureStartupTask: ServiceControl hosts many background tasks.IHostedServiceandFeatureStartupTaskare both abstractions for building background tasks.FeatureStartupTaskimplementations are tied to the lifecycle of an endpoint.IHostedServiceis tied to the lifecycle of the host application, and we prefer it in general. NOTE: ServiceControl startsIHostedServiceimplementations in the order of registration, which gives more control over the startup sequence, and shuts them down in the reverse order.FeatureStartupTaskimplementations start in an order based onFeatureactivation, so controlling the startup sequence requires configuring feature dependencies. They also shut down in reverse order. - Prefer
IServiceCollectionoverIConfigureComponentsandIContainerBuilder: Where possible, use the Microsoft DI abstraction (IServiceCollection) instead of the NServiceBus one (IConfigureComponents) or the Autofac one (IContainerBuilder).- When registering components from within an NServiceBus feature, using
IConfigureComponentsis still appropriate. Consider moving the code out of the NServiceBus feature.IConfigureComponentswas deprecated in NServiceBus version 8. - When relying on Autofac-specific features, use
IContainerBuilder. Prefer to implement features in a way that does not rely on Autofac-specific features. In the future, we may decide to remove our dependency on Autofac.
- When registering components from within an NServiceBus feature, using
- Prefer
IServiceProvideroverILifetimeServiceandIContainer: As above, use the Microsoft DI abstraction where possible and useILifetimeServiceonly where strictly necessary.- When relying on an Autofac-specific feature, use
ILifetimeService. - Never use
IContainer. It is functionally equivalent toIServiceProviderand was deprecated in NServiceBus version 8.
- When relying on an Autofac-specific feature, use
Logging is the one exception to the preference for Microsoft abstractions. Use the NServiceBus logging abstractions instead of the Microsoft or NLog abstractions. The existing ServiceControl code uses the static LogManager classes to access the logging infrastructure, and all new code should follow this pattern for consistency. In the future we are likely to switch to the Microsoft abstraction.
Where possible, prefer explicit container registration for services over convention-based registration. Explicit registration shows which classes belong to which ServiceControl components or to which part of the ServiceControl infrastructure. It also controls which services are available within the container, which reduces inappropriate cross-component access. In the future we may be able to make this more explicit by moving ServiceControl components into their own assemblies and keeping non-shared services internal.
A few things are still registered by convention. Autofac registers them, not the Microsoft DI abstractions.
- API controllers
- Scatter-gather API components
NServiceBus also scans types at startup and automatically registers any implementations of Feature and IHandleMessage<>. We leave this unchanged because turning it off would work against NServiceBus.
Use a direct data-store method when all inputs for a persistence operation fit in one method call. The method owns and disposes its EF Core scope or context or its RavenDB session, accepts a CancellationToken, and commits before returning. Returned entities are detached snapshots. Callers must not need to mutate tracked entities as an implicit persistence command.
Atomic operations that can encounter concurrency conflicts document their provider guarantees and translate expected provider exceptions into explicit domain outcomes. For example, a unique-key or optimistic-concurrency conflict should not escape when contention is part of the normal contract of the operation.
Use a specialized unit of work only when a caller composes several writes into one atomic batch. New unit-of-work APIs provide:
- an
I...UnitOfWorkFactory - a
StartNewfactory method - a
Complete(CancellationToken)commit method IAsyncDisposablelifetime ownership- explicit operation-recording methods rather than mutation of tracked return values
- documented commit, abandon, repeated-completion, and concurrency behavior
Do not introduce generic IDataSessionManager-style abstractions or persistence managers with hidden call-order protocols. During review, prefer one explicit store operation unless caller-composed atomicity requires a unit of work.
Avoid property injection, although the Autofac container can be configured to allow it. The Microsoft DI abstractions cannot specify property injection, and the default IServiceProvider implementation does not support it. Where possible, use constructor injection.
A few places still use property injection. Autofac registers them, not the Microsoft DI abstractions.
- API controllers
- Scatter-gather API components