This library contains all of the fundamental abstractions for defining configuration options.
This crate provides the following features:
The options pattern uses classes to provide strongly typed access to groups of related settings. Options also provide a mechanism to validate configuration data. For more information, see the Options validation section.
The preferred way to read related configuration values is using the options pattern. For example, to read the following configuration values:
json
"Position": {
"Title": "Editor",
"Name": "Joe Smith"
}
Create the following PositionOptions
struct:
```rust
pub struct PositionOptions { pub title: String, pub name: String, } ```
An options struct:
Default
trait; otherwise a custom OptionsFactory<TOptions>
is required.The following code:
ConfigurationBinder.bind
to bind the PositionOptions
class to the Position
section.Position
configuration data.```rust
pub struct PositionOptions { pub title: String, pub name: String, }
pub TestModel<'a> { config: &'a dyn Configuration }
impl<'a> TestModel<'a> { pub new(config: &dyn Configuration) -> Self { Self { config: config } }
pub get(&self) -> String {
let mut options = PositionOptions::default();
let section = self.config.section("Position").bind(&mut options);
format!("Title: {}\nName: {}", options.title, options.name)
}
} ```
ConfigurationBinder.reify<T>
binds and returns the specified type. ConfigurationBinder.reify<T>
may be more convenient than using ConfigurationBinder.bind
. The following code shows how to use ConfigurationBinder.reify<T>
with the PositionOptions struct:
```rust pub TestModel<'a> { config: &'a dyn Configuration }
impl<'a> TestModel<'a> { pub new(config: &dyn Configuration) -> Self { Self { config: config } }
pub get(&self) -> String {
let options: PositionOptions = self.config.section("Position").reify();
format!("Title: {}\nName: {}", options.title, options.name)
}
} ```
An alternative approach when using the options pattern is to bind the
Position
section and add it to the dependency injection service container.
In the following code, PositionOptions
is added to the service container with
configure
and bound to configuration:
```rust
pub TestModel {
options: Rc
impl TestModel {
pub new(options: Rc
pub get(&self) -> String {
let value = self.options.value();
format!("Title: {}\nName: {}", value.title, value.name)
}
}
fn main() {
let path = PathBuf::from("./appsettings.json");
let config = Rc::from(
DefaultConfigurationBuilder::new()
.addjsonfile(&path)
.build()
.asconfig(),
);
let provider = ServiceCollection::new()
.add(transientasself::
println!("{}", model.get())
} ```
Options<TOptions>
:
- Does not support:
- Reading of configuration data after the app has started.
- Is registered as a Singleton
and can be injected into any service lifetime.
OptionsSnapshot<TOptions>
:
- Is useful in scenarios where options should be recomputed on every request.
- Is registered as Scoped
and therefore can't be injected into a Singleton
service.
OptionsMonitor<TOptions>
:
- Is used to retrieve options and manage options notifications for TOptions
instances.
- Is registered as a Singleton
and can be injected into any service lifetime.
- Supports:
- Change notifications
- Reloadable configuration
- Selective options invalidation (OptionsMonitorCache<TOptions>
)
Post-configuration scenarios enable setting or changing
options after all ConfigureOptions<TOptions>
configuration occurs.
OptionsFactory<TOptions>
is responsible for creating new options instances. It has a single
create
method. The default implementation takes all registered ConfigureOptions<TOptions>
and PostConfigureOptions<TOptions>
and runs all the configurations first, followed by the
post-configuration.
OptionsMonitorCache<TOptions>
is used by OptionsMonitor<TOptions>
to cache TOptions
instances.
The OptionsMonitorCache<TOptions>
invalidates options instances in the monitor so that the value
is recomputed (try_remove
). Values can be manually introduced with try_add
. The clear
method
is used when all named instances should be recreated on demand.
Using OptionsSnapshot<TOptions>
:
Scoped
service and is recomputed per request.The difference between OptionsMonitor<TOptions>
and OptionsSnapshot<TOptions>
is that:
OptionsMonitor<TOptions>
is a Singleton
service that retrieves current option values at any time, which is especially useful in singleton dependencies.OptionsSnapshot<TOptions>
is a Scoped
service and provides a snapshot of the options at the time the OptionsSnapshot<TOptions>
object is constructed. Options snapshots are designed for use with transient and scoped dependencies.The following code uses OptionsSnapshot<TOptions>
:
```rust
pub TestSnapModel {
snapshot: Rc
impl TestSnapModel {
pub new(snapshot: Rc
pub get(&self) -> String {
let options = self.snapshot.get(None);
format!("Option1: {}\nOption2: {}", options.option1, options.option2)
}
}
fn main() {
let path = PathBuf::from("./appsettings.json");
let config = Rc::from(
DefaultConfigurationBuilder::new()
.addjsonfile(&path)
.build()
.asconfig(),
);
let provider = ServiceCollection::new()
.add(transientasself::
println!("{}", model.get())
} ```
The following code registers a configuration instance which MyOptions
binds against:
```rust
pub TestMonitorModel {
monitor: Rc
impl TestMonitorModel {
pub new(monitor: Rc
pub get(&self) -> String {
let options = self.monitor.get(None);
format!("Option1: {}\nOption2: {}", options.option1, options.option2)
}
}
fn main() {
let path = PathBuf::from("./appsettings.json");
let config = Rc::from(
DefaultConfigurationBuilder::new()
.addjsonfile(&path)
.build()
.asconfig(),
);
let provider = ServiceCollection::new()
.add(transientasself::
println!("{}", model.get())
} ```
Services can be accessed from dependency injection while configuring options in two ways:
rust
services.add_options::<MyOptions>()
.configure(|options| options.count = 1);
services.configure_options::<MyAltOptions>(|options| options.count = 1);
services.add_named_options::<MyOtherOptions>("name")
.configure5(
|options,
s2: Rc<Service2>,
s1: Rc<Service1>,
s3: Rc<Service3>,
s4: Rc<Service4>
s4: Rc<Service5>| {
options.property = do_something_with(s1, s2, s3, s4, s5);
});
ConfigureOptions<TOptions>
trait and register it as a serviceIt is recommended to pass a configuration closure to one of the configure
functions
since creating a struct is more complex. Creating a struct is equivalent to what the
framework does when calling any of the configure
functions. Calling one of the
configure
functions registers a transient ConfigureOptions<TOptions>
, which
initializes with the specified service types.
| Function | Description |
| ------------ | ----------- |
| configure
| Configures the options without using any services |
| configure1
| Configures the options using a single dependency |
| configure2
| Configures the options using 2 dependencies |
| configure3
| Configures the options using 3 dependencies |
| configure4
| Configures the options using 4 dependencies |
| configure5
| Configures the options using 5 dependencies |
| post_configure
| Post-configures the options without using any services |
| post_configure1
| Post-configures the options using a single dependency |
| post_configure2
| Post-configures the options using 2 dependencies |
| post_configure3
| Post-configures the options using 3 dependencies |
| post_configure4
| Post-configures the options using 4 dependencies |
| post_configure5
| Post-configures the options using 5 dependencies |
| validate
| Validates the options without using any services |
| validate1
| Validates the options using a single dependency |
| validate2
| Validates the options using 2 dependencies |
| validate3
| Validates the options using 3 dependencies |
| validate4
| Validates the options using 4 dependencies |
| validate5
| Validates the options using 5 dependencies |
Options validation enables option values to be validated.
Consider the following appsettings.json
file:
json
{
"MyConfig": {
"Key1": "My Key One",
"Key2": 10,
"Key3": 32
}
}
The following code:
add_options
to get an OptionsBuilder<TOptions>
that binds to the
MyConfigOptions
struct.rust
fn main() {
let path = PathBuf::from("./appsettings.json");
let config = Rc::from(
DefaultConfigurationBuilder::new()
.add_json_file(&path)
.build()
.as_config(),
);
let provider = ServiceCollection::new()
.apply_config_at::<MyConfigOptions>(config, "MyConfig")
.validate(
|options| options.key2 == 0 || options.key3 > options.key2,
"Key3 must be > than Key2.")
.build_provider()
.unwrap();
}
It is recommended to validate options via a closure as opposed to implementing
ValidateOptions<TOptions>
directly. The default validation consumers, such as
OptionsFactory<TOptions>
, panic if there are any validation errors as the
application is considered to be in an invalid state.
ValidateOptions<TOptions>
The following struct implements ValidateOptions<TOptions>
:
```rust
struct MyConfigValidation;
impl ValidationOptions
if options.key2 < 0 || options.key2 > 1000 {
failures.push(format!("{} doesn't match Range 0 - 1000", options.key2));
}
if config.key3 <= config.key2 {
failures.push("Key3 must be > than Key2");
}
if failures.is_empty() {
ValidationOptionsResult::success()
} else {
ValidationOptionsResult::fail_many(failures)
}
}
} ```
ValidateOptions
enables moving the validation code out of a closure and into a struct.
Using the preceding code, validation is enabled with the following code:
rust
fn main() {
let path = PathBuf::from("./appsettings.json");
let config = Rc::from(
DefaultConfigurationBuilder::new()
.add_json_file(&path)
.build()
.as_config(),
);
let provider = ServiceCollection::new()
.apply_config_at::<MyOptions>(config, "MyOptions")
.add(
transient::<dyn ValidateOptions<MyConfigOptions>, MyConfigValidation>()
.from(|_| Rc::new(MyConfigValidation::default())))
.build_provider()
.unwrap();
let options = provider.get_required::<MyConfigOptions>();
}
Set post-configuration with PostConfigureOptions<TOptions>
. Post-configuration
runs after all ConfigureOptions<TOptions>
configuration occurs:
rust
fn main() {
let provider = ServiceCollection::new()
.post_configure_options::<TestOptions>(|options| options.enabled = true)
.build_provider()
.unwrap();
}
post_configure_options
applies to all instances. To apply a named configuration use
post_configure_named_options
.
This project is licensed under the [MIT license].