Getting Started
Architecture
NServiceBus
Transports
Persistence
ServiceInsight
ServicePulse
ServiceControl
Monitoring
Samples

Features

Component: NServiceBus
NuGet Package: NServiceBus (9.x)

While NServiceBus provides interfaces to plug in code at certain steps in the lifecycle, Features offer a more complete approach to writing and distributing custom extensions.

Features allow:

  • Configuring required dependencies.
  • Enabling or disabling via configuration or based on conditions and dependencies.
  • Accessing the pipeline, settings and dependency injection.

Feature API

To create a new feature create a class which inherits from Feature. This offers two basic extensibility points:

  • The constructor of the class will always be executed and should be used to determine whether to enable or disable the feature and configure default settings.
  • The Setup method is called if all the defined conditions are met and the feature is marked as enabled. Use this method to configure and initialize all required components for the feature such as startup tasks.
public class MinimalFeature :
    Feature
{
    protected override void Setup(FeatureConfigurationContext context)
    {
    }
}

Dependencies

Features can depend on each other to form more complex functionality.

public class ComponentAFeature :
    Feature
{
    public ComponentAFeature()
    {
        DependsOn<ComponentBFeature>();
        // Assuming type names are
        // Namespace.ComponentCFeature
        // Namespace.ComponentDFeature
        // Namespace.ComponentEFeature
        DependsOn("Namespace.ComponentCFeature");
        DependsOnAtLeastOne("Namespace.ComponentDFeature", "Namespace.ComponentEFeature");
    }

A feature might use either strongly or loosely typed API when declaring dependencies (the latter can be useful if a dependency is declared in an external assembly).

The feature name is derived from the name of the type. The Feature suffix must not be removed and dependency strings must be prefixed with the namespace of the target Feature.

The API also allows declaring optional dependencies on one or more listed features.

Enabling, disabling and activation

For a feature to take part in the endpoint construction, it has to first be enabled and only then does it qualify for activation. By default, features are disabled unless explicitly enabled.

This can be overridden and a feature can be enabled by default, like most of NServiceBus's features are:

public class FeatureEnabledByDefault :
    Feature
{
    public FeatureEnabledByDefault()
    {
        EnableByDefault();
    }

    protected override void Setup(FeatureConfigurationContext context)
    {
    }
}

Enabling other features

The list of all the enabled features is built interactively. A feature can enable other features via the defaults mechanism and that fact might trigger another set of defaults to be applied, enabling subsequent features, etc.

Defaults(s => s.EnableFeatureByDefault<OtherFeature>());

Enabling features from the outside

To manually activate or deactivate features, and most of the internal NServiceBus features, use the provided extension methods on the endpoint configuration:

// this is not required if the feature uses EnableByDefault()
endpointConfiguration.EnableFeature<MyFeature>();

// disable features not in use
endpointConfiguration.DisableFeature<Sagas>();

var startableEndpoint = await Endpoint.Create(endpointConfiguration);

Prerequisites

When the enabling algorithm can't find any more features that should be enabled, it begins the second stage which does the prerequisite checking. Each feature can declare its prerequisites as predicates that need to be satisfied for that feature to be able to be activated.

public FeatureWithPrerequisites()
{
    Prerequisite(
        condition: c =>
        {
            var settings = c.Settings;
            return settings.HasExplicitValue("SomeKey");
        },
        description: "The key SomeKey was not present.");
}
The differentiation between explicit settings and default settings becomes useful when determining if a given feature should be activated.

Activation

The final stage is the activation where each feature has its chance to set up the endpoint. The features are activated in order of dependencies which means that when a given feature is activating, all the features it depends on have already been activated.

For a feature to be activated it needs to satisfy the following criteria:

  • It needs to be enabled.
  • All its prerequisites need to be satisfied.
  • All the feature's dependencies need to be enabled.
  • All the prerequisites of all the feature's dependencies need to be satisfied.

Feature setup

Setup has to be implemented in the feature and will be called if the feature is enabled. It supports the configuration of the feature, hooking into the pipeline, or registering services on the container. The FeatureConfigurationContext parameter on the method contains:

  • Settings: read or write settings which should be available to other components or read access settings provided by NServiceBus.
  • Container: register services with dependency injection which can be injected into other components.
  • Pipeline: register a behavior into the message processing pipeline or replace/remove existing ones.
protected override void Setup(FeatureConfigurationContext context)
{
    var configurationValue = context.Settings.Get<string>("Key");

    context.Services.AddSingleton(new MessageHandlerDependency());

    context.Pipeline.Register(new CustomBehavior(configurationValue), "custom pipeline behavior");
}
Features are automatically detected and registered by NServiceBus when the assembly is scanned.

Feature settings

The settings are a good way to share data and configuration between the feature and the endpoint configuration, or various features. Settings can be accessed via the FeatureConfigurationContext.Settings property during the setup phase. Settings can be configured using defaults or EndpointConfiguration.

Defaults

When a feature is found to be enabled, the bootstrapping code applies the defaults defined by that feature to a shared dictionary containing the settings.

public class FeatureWithDefaults :
    Feature
{
    public FeatureWithDefaults()
    {
        Defaults(settings =>
        {
            settings.Set("Key", "Value");
            settings.SetDefault("OtherKey", 42);
        });
    }

    protected override void Setup(FeatureConfigurationContext context)
    {
    }
}

The code above configures the key "Key" to contain value "Value" and the key "OtherKey" to contain value 42 as a default value. The querying API allows distinguishing between these two types of registrations. Usually, for a given key, a feature registers a default value in this way and also exposes an extension method of the endpoint configuration to allow a user to override the default value.

EndpointConfiguration

The settings can already be accessed during endpoint configuration:

var settings = endpointConfiguration.GetSettings();
settings.Set("AnotherKey", new CustomSettingsDto());

Note that defaults have not yet been applied at endpoint configuration time, so they can't be accessed during endpoint configuration. However, irrelevant of the declaration order, custom values will always take precedence over the defaults.

Feature startup tasks

If it's required to execute some feature-related logic after the feature has been started or stopped, this can be done by providing a FeatureStartupTask which comes with an OnStart and OnStop method. The task will always be disposed of after stopping the feature if it implements IDisposable.

class MyStartupTask :
    FeatureStartupTask,
    IDisposable
{
    ManualResetEventSlim resetEvent = new ManualResetEventSlim();

    protected override Task OnStart(IMessageSession session, CancellationToken cancellationToken)
    {
        resetEvent.Set();
        return Task.CompletedTask;
    }

    protected override Task OnStop(IMessageSession session, CancellationToken cnCancellationToken)
    {
        resetEvent.Reset();
        return Task.CompletedTask;
    }

    public void Dispose()
    {
        resetEvent.Dispose();
    }
}

To associate a FeatureStartupTask with the feature, use RegisterStartupTask in the feature's Setup method.

protected override void Setup(FeatureConfigurationContext context)
{
    context.RegisterStartupTask(new MyStartupTask());
    // or
    context.RegisterStartupTask(() => new MyStartupTask());
}

The task will only be created and called if the feature is enabled. The FeatureStartupTasks are activated and started in random order.

Avoid long-running operations which will delay the endpoint startup time.

Accessing the Endpoint Instance

In Versions 6 and above access to the message session, which allows to do basic endpoint operations, inside a FeatureStartupTask is provided via method parameters.

class MyStartupTaskThatUsesMessageSession :
    FeatureStartupTask,
    IDisposable
{
    ManualResetEventSlim resetEvent = new ManualResetEventSlim();

    protected override async Task OnStart(IMessageSession session, CancellationToken cancellationToken = default)
    {
        await session.Publish(new MyEvent(), cancellationToken);
        resetEvent.Set();
    }

    protected override async Task OnStop(IMessageSession session, CancellationToken cancellationToken = default)
    {
        await session.Publish(new MyEvent(), cancellationToken);
        resetEvent.Reset();
    }

    public void Dispose()
    {
        resetEvent.Dispose();
    }
}

Samples