This document lists coding and design guidelines that should be followed when working on ServiceControl. In case of conflicts with other coding and design guidelines, this document should take precedence when working on 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, we prefer to use it in preference to any other abstraction (including one of our own).
This makes it easier for us to isolate the application from third party dependencies by relying on relatively stable abstractions. It also helps to keep parts of the application isolated from each other (e.g. running the embedded database in maintenance mode without starting the NServiceBus endpoint).
For example:
- Prefer
IHostBuilderextensions over NServiceBus features: Unless a new feature specifically alters the NServiceBus endpoint, it should be added as an extension toIHostBuilderrather than as a NServiceBusFeatureimplementation. Some existing ServiceControl features may still be using theFeatureabstraction to register components. If these features do not modify the NServiceBus endpoint, they should be migrated 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. In general, we prefer to use theIHostedServiceabstraction which is tied to the lifecycle of the host application. NOTE:IHostedServiceimplementations are started in the order that they are registered, which provides more control over the startup sequence. They are shut down in the reverse order.FeatureStartupTaskimplementations are started in an order that is based on the order ofFeatureactivation. This means that controlling startup sequence has to be done by configuring feature dependencies.FeatureStartupTaskimplementations are also shut down in reverse order. - Prefer
IServiceCollectionoverIConfigureComponentsandIContainerBuilder: Where possible, we use the Microsoft DI abstraction (IServiceCollection) rather than the NServiceBus one (IConfigureComponents) or the Autofac one (IContainerBuilder).- When registering components from within an NServiceBus Feature, it still makes sense to use
IConfigureComponents, but we should consider whether it makes sense to move the code out of an NServiceBus feature.IConfigureComponentswas deprecated in NServiceBus version 8. - When relying on Autofac-specific features, it makes sense to use
IContainerBuilder. We prefer to implement features in 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, it still makes sense to use
- Prefer
IServiceProvideroverILifetimeServiceandIContainer: As above, we prefer to use the Microsoft DI abstraction where possible and only fall backILifetimeServicewhere strictly necessary.- When relying on a Autofac-specific feature,
ILifetimeServiceshould be used. IContainershould never be used. It is functionally equivalent toIServiceProviderand was deprecated in NServiceBus version 8.
- When relying on a Autofac-specific feature,
There is one exception to the preference for Microsoft abstractions
- Use NServiceBus Logging abstractions over Microsoft or NLog abstractions: The existing ServiceControl code uses the static
LogManagerclasses to get access to the logging infrastructure and all new code should follow this pattern. In the future we are likely to switch to the Microsoft abstraction but until then we want to maintain consistency.
Where possible, we prefer explicit container registration for services instead of convention-based registration. This provides better visibility of which classes belong to which ServiceControl components or to which part of the ServiceControl infrastructure. It also gives us more explicit control over which services are available within the container, which helps to reduce 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.
There are a few things that are still registered using convention. Note that these are all registered using Autofac and not the Microsoft DI abstractions.
- API Controllers
- Scatter-Gather API components
Additionally, because NServiceBus does a type-scan at startup it will automatically register any implementations of Feature and IHandleMessage<>. We have chosen to leave this alone as we would be fighting with NServiceBus in order to turn this off.
Use a direct data-store method when all inputs for a persistence operation fit in one method call. The method should own and dispose its EF Core scope/context or RavenDB session, accept a CancellationToken, and commit before returning. Returned entities are detached snapshots; callers must not be required to mutate tracked entities as an implicit persistence command.
Atomic operations that can encounter concurrency conflicts should 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 operation's normal contract.
Reserve a specialized unit of work for cases where a caller genuinely composes several writes into one atomic batch. New unit-of-work APIs should consistently 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.
Although the Autofac container can be configured to allow property injection, we prefer to avoid it. There is no way to specify property injection using the Microsoft DI abstractions, and the default IServiceProvider implementation does not support it. Where possible, use constructor injection instead.
There are a few places where property injection is still used. These are all registered using Autofac, and not the Microsoft DI abstractions.
- API Controllers
- Scatter-Gather API components