Dependency Injection
Lexigram is built on a powerful, lightweight Dependency Injection (DI) container. Instead of your components creating their own dependencies, they are “injected” at runtime, leading to cleaner code, easier testing, and true modularity.
1. The IoC Container
Section titled “1. The IoC Container”The DI container (Inversion of Control) is the central registry where all application services live. You interact with it primarily through Providers or by resolving services directly from the Application instance.
Registration Scopes
Section titled “Registration Scopes”Lexigram supports three primary registration lifetimes:
| Scope | Method | Description |
|---|---|---|
| Singleton | container.singleton() | Only one instance is created for the entire application lifecycle. |
| Scoped | container.scoped() | A new instance is created per request/operation (common in Web controllers). |
| Transient | container.transient() | A new instance is created every time the dependency is requested. |
Container API
Section titled “Container API”from lexigram import Container
container = Container()
# Registration — key: type to resolve, value: what to returncontainer.singleton(PaymentGateway, StripeGateway(api_key)) # protocol → instancecontainer.singleton(UserService, UserService()) # class → pre-built instancecontainer.scoped(DbSession, SqlAlchemySession) # one per scopecontainer.transient(RequestContext, RequestContext) # new instance each resolve
# Resolutionservice = await container.resolve(PaymentGateway) # returns the StripeGateway instanceoptional = await container.resolve_optional(EventBus) # None if not registeredall_impls = await container.resolve_all(BaseHandler) # all subtypes
# Scopingasync with container.scope() as scoped: session = await scoped.resolve(DbSession) # scoped to this block2. Constructor Injection
Section titled “2. Constructor Injection”This is the preferred way to handle dependencies. By simply type-hinting your constructor parameters with a Protocol or Class, Lexigram will automatically resolve and inject the correct instance.
from lexigram.contracts.data.db import DatabaseProviderProtocol
class ProductService: def __init__(self, db: DatabaseProviderProtocol): # The 'db' instance is automatically resolved from the container self.db = db
async def list_products(self): return await self.db.query("SELECT * FROM products")3. DI Decorators
Section titled “3. DI Decorators”Lexigram provides decorators to mark classes for automatic discovery and registration:
| Decorator | Scope | Import From |
|---|---|---|
@singleton | One instance for the app | lexigram or lexigram.di |
@injectable | Transient by default | lexigram or lexigram.di |
@scoped | One instance per request/scope | lexigram or lexigram.di |
@transient | New instance each time | lexigram or lexigram.di |
from lexigram import singleton, injectable, scoped
@singletonclass ConfigService: def __init__(self) -> None: self.settings = load_settings()
@injectable # transient by defaultclass UserService: def __init__(self, config: ConfigService) -> None: self.config = config
@scopedclass RequestContext: def __init__(self) -> None: self.request_id = generate_id()How Auto-Registration Works
Section titled “How Auto-Registration Works”@singletonmarksGreetingServicewith__lexigram_injectable__metadataApplication.discover_providers()scans the package and finds the marked class- At boot, the container registers
GreetingServiceas a singleton - When
HelloControlleris instantiated, the container resolvesGreetingServicefrom the constructor type hints
4. Resolving Manually
Section titled “4. Resolving Manually”While constructor injection is preferred, you can also resolve dependencies manually from the container when necessary.
from lexigram.contracts.core.di import ContainerResolverProtocol
# Within a provider boot() methodasync def boot(self, container: ContainerResolverProtocol): db = await container.resolve(DatabaseProviderProtocol) await db.connect()For
boot()methods, useBootContainerProtocolto also allow service registration. See Container Protocols for the full protocol hierarchy.
5. Named Registrations
Section titled “5. Named Registrations”You can register services with names for more granular resolution:
container.singleton( CacheBackend, RedisCacheBackend(), name="redis")
# Resolve by name using Annotatedfrom typing import Annotatedfrom lexigram.di.annotations import Named
cache = await container.resolve(Annotated[CacheBackend, Named("redis")])6. Testing with Overrides
Section titled “6. Testing with Overrides”In testing scenarios, you can override service registrations:
container = Container(testing_mode=True)
# Override with fakecontainer.override(UserRepository, FakeUserRepository())Note:
override()is only available in containers created withtesting_mode=True.