| Crates.io | service-rs |
| lib.rs | service-rs |
| version | 0.1.7 |
| created_at | 2025-10-21 07:00:55.725131+00 |
| updated_at | 2025-12-12 10:54:47.148644+00 |
| description | An async-first, lightweight dependency injection container for Rust |
| homepage | |
| repository | https://github.com/SFINXVC/service-rs |
| max_upload_size | |
| id | 1893359 |
| size | 58,984 |
An async-first, lightweight dependency injection (DI) container for Rust, inspired by Microsoft.Extensions.DependencyInjection from .NET.
[dependencies]
service-rs = { git = "https://github.com/SFINXVC/service-rs.git", branch = "main" }
Compiler support: requires rustc 1.85.0 or higher (uses unstable features: unsize, coerce_unsized)
Note: This library is still in development and is not ready for production use.
tokioArc<T> for safe sharing across threads#[derive(Injectable)] macro for automatic constructor injectionuse service_rs::ServiceCollection;
use std::sync::Arc;
#[tokio::main]
async fn main() {
let mut collection = ServiceCollection::new();
collection.add_singleton_with_factory::<i32, _, _>(|_| async {
Ok(Box::new(42) as Box<dyn std::any::Any + Send + Sync>)
});
collection.add_transient_with_factory::<String, _, _>(|_| async {
Ok(Box::new("Hello".to_string()) as Box<dyn std::any::Any + Send + Sync>)
});
let provider = collection.build();
// Singleton - same instance every time
let num1: Arc<i32> = provider.get::<i32>().await.unwrap();
let num2: Arc<i32> = provider.get::<i32>().await.unwrap();
assert_eq!(Arc::as_ptr(&num1), Arc::as_ptr(&num2));
// Transient - different instance every time
let str1: Arc<String> = provider.get::<String>().await.unwrap();
let str2: Arc<String> = provider.get::<String>().await.unwrap();
assert_ne!(Arc::as_ptr(&str1), Arc::as_ptr(&str2));
}
The Injectable derive macro enables automatic dependency injection for structs with dependencies.
use service_rs::{Injectable, ServiceCollection};
use std::sync::Arc;
struct Database {
connection_string: String,
}
#[derive(Injectable)]
struct UserService {
db: Arc<Database>,
}
#[tokio::main]
async fn main() {
let mut collection = ServiceCollection::new();
collection.add_singleton_with_factory::<Database, _, _>(|_| async {
Ok(Box::new(Database {
connection_string: "localhost:5432".to_string(),
}) as Box<dyn std::any::Any + Send + Sync>)
});
collection.add_scoped::<UserService>();
let provider = collection.build();
let scope = provider.create_scope();
let user_service: Arc<UserService> = scope.get::<UserService>().await.unwrap();
// UserService automatically receives the Database dependency
}
Important: All fields in an Injectable struct must be wrapped in Arc<T>.
use service_rs::ServiceCollection;
use std::sync::Arc;
struct RequestContext {
request_id: String,
}
#[tokio::main]
async fn main() {
let mut collection = ServiceCollection::new();
collection.add_scoped_with_factory::<RequestContext, _, _>(|_| async {
Ok(Box::new(RequestContext {
request_id: uuid::Uuid::new_v4().to_string(),
}) as Box<dyn std::any::Any + Send + Sync>)
});
let provider = collection.build();
// First scope
let scope1 = provider.create_scope();
let ctx1a: Arc<RequestContext> = scope1.get::<RequestContext>().await.unwrap();
let ctx1b: Arc<RequestContext> = scope1.get::<RequestContext>().await.unwrap();
assert_eq!(Arc::as_ptr(&ctx1a), Arc::as_ptr(&ctx1b)); // Same within scope
// Second scope
let scope2 = provider.create_scope();
let ctx2: Arc<RequestContext> = scope2.get::<RequestContext>().await.unwrap();
assert_ne!(Arc::as_ptr(&ctx1a), Arc::as_ptr(&ctx2)); // Different across scopes
}
Register implementations for trait objects using the interface methods:
use service_rs::{Injectable, ServiceCollection};
use std::sync::Arc;
trait Logger: Send + Sync {
fn log(&self, message: &str);
}
#[derive(Injectable)]
struct ConsoleLogger;
impl Logger for ConsoleLogger {
fn log(&self, message: &str) {
println!("{}", message);
}
}
#[tokio::main]
async fn main() {
let mut collection = ServiceCollection::new();
collection.add_singleton_interface::<dyn Logger, ConsoleLogger>();
let provider = collection.build();
let logger: Arc<Box<dyn Logger>> = provider.get::<Box<dyn Logger>>().await.unwrap();
logger.log("Hello from trait object!");
}
Factory-based registration:
add_singleton_with_factory<T, F, Fut>(&mut self, factory: F) -> &mut Selfadd_scoped_with_factory<T, F, Fut>(&mut self, factory: F) -> &mut Selfadd_transient_with_factory<T, F, Fut>(&mut self, factory: F) -> &mut SelfInjectable-based registration (requires proc-macro feature):
add_singleton<T: Injectable>(&mut self) -> &mut Selfadd_scoped<T: Injectable>(&mut self) -> &mut Selfadd_transient<T: Injectable>(&mut self) -> &mut SelfInterface registration (requires proc-macro feature):
add_singleton_interface<T: ?Sized, TImpl: Injectable>(&mut self) -> &mut Selfadd_scoped_interface<T: ?Sized, TImpl: Injectable>(&mut self) -> &mut Selfadd_transient_interface<T: ?Sized, TImpl: Injectable>(&mut self) -> &mut SelfBuild:
build(self) -> Arc<ServiceProvider>async fn get<T>(&self) -> Result<Arc<T>, ServiceError>fn create_scope(&self) -> Arc<ScopedServiceProvider>async fn get<T>(&self) -> Result<Arc<T>, ServiceError>The library provides detailed error types via ServiceError:
ServiceNotFound - Service type not registeredServiceAlreadyExists - Service type already registeredServiceResolutionFailed - Failed to resolve serviceServiceInitializationFailed - Factory threw an errorServiceInvalidScope - Attempted to resolve scoped service from root providerproc-macro (default): Enables the Injectable derive macro and interface registration methodsThis project is licensed under the MIT License.