hosting 0.1.0

Dependency injection and application hosting modeled on Microsoft.Extensions.Hosting
Documentation

hosting

Dependency injection and application hosting for Rust, modeled on Microsoft.Extensions.DependencyInjection and Microsoft.Extensions.Hosting from .NET.

Services are plain structs. You describe their dependencies once, register them with a lifetime, and the container builds them on request.

use hosting::{Service, ServiceCollection};
use std::sync::Arc;

#[derive(Service)]
struct Database;

#[derive(Service)]
struct Repository {
    db: Arc<Database>,
}

let mut services = ServiceCollection::new();
services
    .add_singleton::<Database>()
    .add_scoped::<Repository>();
let provider = services.build()?;

let scope = provider.create_scope();
let repository = scope.resolve::<Repository>()?;

Status

Early development. The dependency injection container is usable; the host lifecycle and configuration are not written yet. The API will change and the crates are not published.

Area State
Container: lifetimes, scopes, factories, instances done
#[derive(Service)] and named constructors done
Trait registration and resolve_all done
Build-time validation done
Hosted services, graceful shutdown, signals planned
Configuration sources and typed options planned

Lifetimes

Method One instance per
add_singleton provider
add_scoped scope
add_transient resolution

The lifetime is chosen when registering, not on the type, so the same struct can be a singleton in one application and scoped in another.

A singleton is always built outside any scope. If it depends on a scoped service, resolving it fails with ResolveError::ScopedFromRoot instead of keeping one scope's instance for the life of the process. Transients are built in the scope that asked for them.

Declaring services

By default every field is a dependency and must be Arc<T> or Vec<Arc<T>>. When a struct needs setup, name a constructor instead; its parameters are injected and it may return Self or Result<Self, E>:

#[derive(Service)]
#[service(constructor = connect)]
struct Database {
    url: String,
}

impl Database {
    fn connect(config: Arc<Config>) -> Result<Self, std::io::Error> {
        Ok(Database { url: config.url.clone() })
    }
}

Types from other crates can be registered with a factory or as an existing value:

services
    .add_singleton_with(|r| Ok(Client::new(r.resolve::<Config>()?.timeout)))
    .add_singleton_instance(Config::default());

Traits

Mark a trait with #[abstraction] once, then register implementations of it. A single resolve returns the last registration; resolve_all and Vec<Arc<dyn Trait>> return all of them in registration order.

use hosting::abstraction;

#[abstraction]
trait Notifier: Send + Sync {
    fn notify(&self, message: &str);
}

services
    .add_singleton_as::<dyn Notifier, Email>()
    .add_singleton_as::<dyn Notifier, Sms>();

let last = provider.resolve::<dyn Notifier>()?;     // Sms
let all = provider.resolve_all::<dyn Notifier>()?; // [Email, Sms]

Traits that can't carry the attribute, such as ones from other crates, can be registered through a factory with add_singleton_as_with.

Registering a type again adds a registration rather than replacing one. Use try_add_singleton and its variants to register a default only when nothing is registered yet, as with TryAdd in .NET.

Validation

build() always rejects dependency cycles. Two more checks are opt-in:

services
    .validate_dependencies() // every declared dependency is registered
    .validate_scopes();      // no singleton depends on a scoped service

let err = services.build().unwrap_err();
for issue in err.issues() {
    eprintln!("{issue}");
}
// dependency cycle: app::Ping -> app::Pong -> app::Ping
// dependency not registered: app::Database -> app::Config
// singleton depends on scoped service: app::Cache -> app::Handler -> app::Transaction

Every problem is reported at once. Unlike .NET, these checks are not enabled automatically in development; turn them on where you want them.

Errors

Resolution errors name the types involved and the path that led to the failure, for example service not registered: app::UserService -> app::Repository. A constructor that fails is not cached, so the next resolution tries again.

Crates

Crate Contents
hosting The crate to depend on. Re-exports everything below.
hosting-di Service collection, provider, scopes and resolution.
hosting-macros #[derive(Service)] and #[abstraction].
hosting-core Host and HostBuilder.

Development

The minimum supported Rust version is 1.85.

cargo test --workspace --all-features
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo bench -p hosting-di --bench resolve

Compile-fail tests for macro diagnostics are pinned to one compiler, since error wording changes between releases:

cargo +1.99.0 test -p hosting --test ui -- --ignored

License

MIT. See LICENSE.