Dependency Injection

An alternative approach when using the options pattern is to bind an entire configuration or section of it through a dependency injection (DI) service container. Dependency injection extensions are provided by the more-di crate.

In the following code, PositionOptions is added to the service container with apply_config and bound to loaded configuration:

use config::{*, ext::*};
use options::{Options, ext::*};
use serde::Deserialize;
use std::rc::Rc;

#[derive(Default, Deserialize)]
#[serde(rename_all(deserialize = "PascalCase"))]
pub struct PositionOptions {
    pub title: String,
    pub name: String,
}

pub TestModel {
    options: Rc<dyn Options<Position>>
}

impl TestModel {
    pub new(options: Rc<dyn Options<Position>>) -> Self {
        Self { options }
    }

    pub get(&self) -> String {
        let value = self.options.value();
        format!("Title: {}\nName: {}", value.title, value.name)
    }
}

fn main() {
    let config = Rc::from(
        DefaultConfigurationBuilder::new()
            .add_json_file("appsettings.json")
            .build()
            .unwrap()
            .as_config(),
    );
    let provider = ServiceCollection::new()
        .add(transient_as_self::<TestModel>())
        .apply_config::<PositionOptions>(config)
        .build_provider()
        .unwrap();
    let model = provider.get_required::<TestModel>();

    println!("{}", model.get())
}

Options Configuration

Services can be accessed from dependency injection while configuring options in two ways:

  • Pass a configuration function
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);
            });

It 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, which initializes with the specified service types.

FunctionDescription
configureConfigures the options without using any services
configure1Configures the options using a single dependency
configure2Configures the options using 2 dependencies
configure3Configures the options using 3 dependencies
configure4Configures the options using 4 dependencies
configure5Configures the options using 5 dependencies

Options Post-Configuration

Set post-configuration with PostConfigureOptions. Post-configuration runs after all ConfigureOptions configuration occurs.

Services can be accessed from dependency injection while configuring options in two ways:

  • Pass a configuration function
services.add_options::<MyOptions>()
        .post_configure(|options| options.count = 1);
services.post_configure_options::<MyAltOptions>(|options| options.count = 1);
services.add_named_options::<MyOtherOptions>("name")
        .post_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);
            });

post_configure_options applies to all instances. To apply a named configuration use post_configure_named_options. It is recommended to pass a configuration closure to one of the post_configure functions since creating a struct is more complex. Creating a struct is equivalent to what the framework does when calling any of the post_configure functions. Calling one of the post_configure functions registers a transient PostConfigureOptions, which initializes with the specified service types.

FunctionDescription
post_configurePost-configures the options without using any services
post_configure1Post-configures the options using a single dependency
post_configure2Post-configures the options using 2 dependencies
post_configure3Post-configures the options using 3 dependencies
post_configure4Post-configures the options using 4 dependencies
post_configure5Post-configures the options using 5 dependencies

Options Validation

Validation is performed with ValidateOptions. Validation runs after all ConfigureOptions and PostConfigureOptions occurs.

Services can be accessed from dependency injection while validating options in two ways:

  • Pass a validation function
services.add_options::<MyOptions>()
        .configure(|options| options.count = 1)
        .validate(|options| options.count > 0, "Count must be greater than 0.");
services.add_named_options::<MyOtherOptions>("name")
        .configure(|options| options.count = 1)
        .validate5(
            |options,
            s2: Rc<Service2>,
            s1: Rc<Service1>,
            s3: Rc<Service3>,
            s4: Rc<Service4>
            s4: Rc<Service5>| do_complex_validation(s1, s2, s3, s4, s5));

It is recommended to pass a validation closure to one of the validate functions since creating a struct is more complex. Creating a struct is equivalent to what the framework does when calling any of the validate functions. Calling one of the validate functions registers a transient ValidateOptions, which initializes with the specified service types.

FunctionDescription
validateValidates the options without using any services
validate1Validates the options using a single dependency
validate2Validates the options using 2 dependencies
validate3Validates the options using 3 dependencies
validate4Validates the options using 4 dependencies
validate5Validates the options using 5 dependencies