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# | |
---|---|
Some things to note:
- It has a
DefaultColor
that can be set directly through the property. ColorBuilderType
is aSystem.Type
that can only be set through a convenience method. Note that theColorBuilderType
is internal, hence modules from other assemblies will only be able to set it through the method.- By convention, Options are named
{ModuleName}Options
.
Let's see the SampleModule
:
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:
- Runtime configs, i.e using
builders
or object references, stuff resolved at runtime. - 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# | |
---|---|
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.
Then, we need to register this class in our module's SetupModule
:
C# | |
---|---|
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
:
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:
- Using custom section name, in case one's been provided.
- Using Options type's full name
- 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 IConfigurationSource
s, 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.