Skip to content

Module Configuration

By default, all Suite Modules includes a system to support the Options Pattern and IConfiguration.

All Modules are encouraged to provide an Options class so that other modules can customize their behavior. Services may use the Options class to support customizing its features.

In order to do so, we make use of the IOptions Pattern.

SuiteModule Options Lifetime

In a normal ASP.Net Core application, IOptions can only be resolved after the Startup.ConfigureServices has executed. At that time, you rely on IConfiguration for customizing the service's registration logic.

In a Suite Framework Application, Modules may use builder.AddOptions<T> at ISuiteModule.SetupModule in order to declare Options classes.

These classes can then be retrieved at ISuiteModule.ConfigureServices through the context.GetModuleOptions<TOptions> method. Through this method, you may inject your module's options or options from other modules that you depend on.

Take the following Options class:

C#
public class SampleModuleOptions {

    public string DefaultColor { get; set; }

    internal Type ColorBuilderType { get; private set; } = typeof(DefaultColorBuilder);

    public SampleModuleOptions SetColorBuilder(Type builderType)
    {
        ColorBuilderType = builderType;
    }
}

Some things to note:

  1. It has a DefaultColor that can be set directly through the property.
  2. ColorBuilderType is a System.Type that can only be set through a convenience method. Note that the ColorBuilderType is internal, hence modules from other assemblies will only be able to set it through the method.
  3. By convention, Options are named {ModuleName}Options.

Let's see the SampleModule:

C#
public class SampleModule : SuiteModule
{
    public override void SetupModule(IModuleBuilder builder)
    {
        builder.AddOptions<SampleModuleOptions>();
    }

    public override void ConfigureServices(IServiceCollection services,
        ModuleConfigurationContext context)
    {
        // Use this convenience method to obtain the options.
        // You can also obtain options from other modules **that you depend on**.
        var opts = context.GetModuleOptions<SampleModuleOptions>().Value;

        // Services to be registered can be changed based off the options.
        services.AddTransient(typeof(IColorBuilder), opts.ColorBuilderType);

        // Module Options are promoted to the Application Services automatically.
        // They can be injected on services by using the IOptions<SampleModuleOptions>
    }
}

As you may notice, we initialize Options earlier than regular .NET applications so that you can inject them at Modules ConfigureServices time, which allows you to modify dependency injection registrations based on your module’s Options, which may in turn come from IConfiguration sources (e.g. config files, config store, etc).

This is the biggest difference with the IOptions pattern used in vanilla ASP.Net Core apps: you can provide Options to each module you depend on, which allows you to change how services are registered at the DI container.

By using an Options class, we can mainly achieve two things:

  1. Runtime configs, i.e using builders or object references, stuff resolved at runtime.
  2. Static configs, i.e properties that come from IConfiguration.

Configuring dependant module's options

As shown in the example below, Suite Modules will usually use Module Options to configure DI.

When depending on another module, we may configure its options by using the builder.DependsOn<TModule, TModuleOptions>() variant, which receives a callback for configuring the module's option:

C#
1
2
3
4
5
6
7
8
9
public class OtherModule : SuiteModule
{
    public override void SetupModule(IModuleBuilder builder)
    {
        builder.DependsOn<SampleModule, SampleModuleOptions>(opts=> {
            opts.SetColorBuilder(typeof(CustomColorBuilder));
        });
    }
}

Using my module's options to configure dependant module's

Sometimes, we need to use our own options to configure the modules we depend on. This is usually the case for higher level modules that depend on the Framework Modules. In order to do so, the Module Options support the IConfigureOptions pattern.

We need to create a class implementing IConfigureOptions<TOptions> where TOptions is the class of the Module Options that we want to configure. For example, if we depend on MassTransitModule and we want to configure its options, we would create a class implementing IConfigureOptions<MassTransitModuleOptions> where we can inject our own module's options in order to configure MassTransitModuleOptions.

Important

IConfigureOptions added through AddOptionsConfigurator can only inject other Module Options. Nothing else is able to be injected since the Application Services are not yet built.

C#
internal class SampleModuleOptionsConfigurator
    : IConfigureOptions<SampleModuleOptions>
{
    private readonly IOptions<OtherModuleOptions> otherModuleOptions;

    public OptionsConfigurator(IOptions<OtherModuleOptions> otherModuleOptions)
    {
        this.otherModuleOptions = otherModuleOptions;
    }

    public void Configure(SampleModuleOptions options)
    {
        var userProvidedType = this.otherModuleOptions.Value.RegisteredType;

        var finalType = typeof(MyGenericThing<>).MakeGenericType(userProvidedType);
        options.SetColorBuilder(finalType);
    }
}

Then, we need to register this class in our module's SetupModule:

C#
1
2
3
4
5
6
7
8
9
public class OtherModule : SuiteModule
{
    public override void SetupModule(IModuleBuilder builder)
    {
        builder.DependsOn<SampleModule>();
        builder.AddOptions<OtherModuleOptions>();
        builder.AddOptionsConfigurator<SampleModuleOptionsConfigurator, SampleModuleOptions>();
    }
}

Injecting at runtime

Finally, since the provided Options are registered against the app's service collection you can also inject IOptions<SampleModuleOptions> at any services or other symbols resolved by DI after the application has initialized. That includes the module's Initialize as well.

Options best practices

Even though the Module Options are plain C# objects, there is still a need to provide a set of ground rules to work with these objects, in order to have a consistent behavior and experience throughout the whole framework.

Give module's Options a suitable name

The module's Option must be named as {ModuleName}Options. This is the default and recommended naming convention, making them easier to find.

Place Options along with module in the ITsynch.Suite.App.Modules namespace

By placing all modules along with their Options in the same namespace, you are not required to include additional namespaces to get access to module and options symbols required to setup and configure your module. Please note that, if you provide extension methods over Options, that become part of the public API, you must put them also in the same namespace as module and Options.

Initialize option properties with default values

Give the option's properties an initial, or default, value whenever is possible, specially if those attributes are collections.

Avoid the usage of nullables in properties

Avoid the usage of nullables in those cases where you can supply an initial value upfront. Nullables can cause confusion due to its nullability state. It's preferable to have a default value which is going to be overwritten later on by other module, or the application itself, rather than leave the value in its undefined state.

Option validation

Options, as any other DTO dealing with incoming data, must provide a minimum level of consistency. To do so you can take different approaches:

Using DataAnnotations

Using DataAnnotations from [System.ComponentModel.DataAnnotations](https://docs.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations) you can provide basic field validation, which can be enough for primary field validations. In case you'd need to fulfill more advanced validations scenarios, you'll need take a different approach out of the one explained as follows. DataAnnotations validation is enabled by default for every module options registered through IModuleBuilder's AddOptions<TOptions> method.

Using IValidateOptions<TOptions>

By creating a type that implements IValidateOptions<TOptions> and register it to DI, you can apply more complex validation rules, also taking advantage of DI to inject any other dependency to provide validation.

Using PostConfigure method

Due to the fact that PostConfigure is going to be called after all Configure methods are called, there is no need to use PostConfigure other than ensure that the option instance has been properly configured, or in such cases where complementary attributes must been set based on other values supplied to the options instance during the bootstrap process.

IConfiguration support

By invoking the builder.AddOptions<TOptions>() method, your Module can provide Options (which other dependant modules can configure) that are resolved before ConfigureServices and react accordingly.

All Options are by default initialized from IConfiguration, so you can configure them using any source, like a config file usually the appsettings.json.

When no configuration's section name is specified, the TOptions type's full name or type's name are used.

For example, we can set this at our appsettings.json:

JSON
1
2
3
4
5
{
    "ITsynch.Suite.App.Modules.SampleModuleOptions": {
        "DefaultColor": "Blue"
    }
}
C#
public class SampleModule : SuiteModule
{
    public override void SetupModule(IModuleBuilder builder)
    {
        builder.AddOptions<SampleModuleOptions>();
    }

    public override void ConfigureServices(IServiceCollection services,
        ModuleConfigurationContext context)
    {
        var ops = context.GetModuleOptions<SampleModuleOptions>().Value;

        // ops.DefaultColor is "Blue"
    }
}

Note

When no section name is supplied to the AddOptions<TOption> method, the Suite Framework will use the full name or the name of TOption's type to locate the configuration section to bind to.

The order in which configuration sections to bind Options to are selected is:

  1. Using custom section name, in case one's been provided.
  2. Using Options type's full name
  3. Using Options type's name

Adding IConfiguration Sources

Modules that require custom configuration sources, like custom files etc may may contribute to the App's IConfiguration without the need for the application to call methods on the HostBuilder.

Instead, Modules that need to add IConfigurationSources, may call builder.ContributeAppConfiguration(IConfigurationBuilder builder) at SetupModule time.

After you have provided the configuration sources, you can access the config as you would in any .NET application,

The recommended approach is to always use the IOptions pattern, specifically the Module Options mentioned on the previous section.

Example

C#
internal class SampleModule : SuiteModule
{
    public override void SetupModule(IModuleBuilder builder)
    {
        // This adds a file as an IConfigurationSource
        // Any section or IOptions may be configured from that file.
        builder.ContributeAppConfiguration(config =>
        {
            appConfig.AddJsonFile("sample-module.config.json");
        });
    }