Container¶
AdapterContainer wires dependencies for the application and infrastructure layers. It manages sessions, handlers (CommandHandler[C], QueryHandler[Q]), caches, and ports. Handlers implement CommandPort[C] / QueryPort[Q] and are injected into UseCase fields automatically.
AdapterContainer¶
AdapterContainer is the dependency injection container. It can be used directly without subclassing. Pass port instances as keyword arguments to the constructor.
Constructor¶
AdapterContainer(**fields)
| Parameter | Type | Description |
|---|---|---|
sessions |
set[type[Session] \| type[AsyncSession]] |
Session classes (not instances) to manage. Default: set(). |
handlers |
list[AnyHandler] |
Handler classes to register. Default: []. |
caches |
list[Cache \| AsyncCache] |
Cache instances available through cache_context(). Default: []. |
ports |
dict[type[Port], Port] |
Type-based port resolution fallback. When a port field type is not found by name, the container checks this dict. Default: {}. |
**fields |
Port |
Custom ports registered by field name. Any keyword argument that is a Port instance is registered by name. |
Default Fields¶
| Field | Type | Default |
|---|---|---|
sessions |
set[type[Session] \| type[AsyncSession]] |
set() |
handlers |
list[AnyHandler] |
[] |
caches |
list[Cache] |
[] |
ports |
dict[type[Port], Port] |
{} |
Methods¶
__init__(self, ...)¶
The constructor validates that no duplicate handlers are registered.
get_session(session_cls: type[Session] | type[AsyncSession]) -> Session | AsyncSession¶
Retrieve or instantiate a session class.
| Parameter | Type | Description |
|---|---|---|
session_cls |
type[Session] \| type[AsyncSession] |
The session class to retrieve. |
- If the session class has already been instantiated, returns the cached instance.
- Otherwise, finds a matching class in
self.sessions, instantiates it, caches it in_sessions_needed, and returns it. - Raises
SessionNotFoundErrorif no matching session class is registered.
get_handler(contract: type[Command] | type[Query]) -> CommandHandler | AsyncCommandHandler | QueryHandler | AsyncQueryHandler¶
Find and instantiate a handler for a given contract.
| Parameter | Type | Description |
|---|---|---|
contract |
type[Command] \| type[Query] |
The command or query class. |
- Searches registered handlers for one whose
handle()method accepts the given contract. - Iterates the handler's fields and injects a session instance for each field with a concrete session type annotation.
- Applies matching caches to the handler via
add_cache(). - Raises
HandlerNotFoundErrorif no handler matches the contract.
get_port(name: str) -> Port¶
Find a port implementation by field name.
| Parameter | Type | Description |
|---|---|---|
name |
str |
The field name registered on the container. |
- Looks up
_ports_by_namefor the registered field name. - Returns the field value.
- Raises
PortNotFoundErrorif no port with that name is found.
adapt(operation_cls: type[TOperation], **overrides: Any) -> TOperation¶
Create a use case or projection instance with all dependencies wired automatically. This is the single public entry point for dependency injection -- it dispatches to the appropriate internal method based on the class type.
| Parameter | Type | Description |
|---|---|---|
operation_cls |
type[UseCase \| AsyncUseCase \| ProjectionBase] |
The use case or projection class to instantiate. |
**overrides |
Any |
Optional field overrides for the container copy. |
For use cases:
- Injects matching handler ports (CommandPort[C], QueryPort[Q]) by contract type.
- Injects custom ports by field name, with type-based fallback from ports dict.
- adapt() only injects dependencies. Open cache_context() and Transaction explicitly around execution.
For projections:
- Injects sessions by type annotation for any field with a Session/AsyncSession annotation.
- Injects custom ports by field name, with type-based fallback from ports dict.
When **overrides are provided, creates a container copy before injection.
with_adapters(**overrides: Any) -> Self¶
Create a copy of the container with overridden fields.
| Parameter | Type | Description |
|---|---|---|
**overrides |
Any |
Field values to override in the copy. |
policy_manager() -> PolicyManager¶
Create a PolicyManager from all registered PolicyHandler and AsyncPolicyHandler instances.
container = AdapterContainer(handlers=[OwnerPolicyHandler, AdminPolicyHandler])
manager = container.policy_manager()
manager.enforce(owner_contract)
Port Resolution Order¶
When injecting ports into a use case or projection, the container resolves each port field in this order:
- Named port -- looks up the field name in
_ports_by_name(from container subclass fields or keyword arguments). - Type-based fallback -- looks up the field's type annotation in
self.portsdict. - If neither resolves, raises
PortNotFoundError.
Handler Validation¶
The container enforces:
- No duplicate handler registrations (two handlers for the same contract raise
DuplicateHandlerError). - Each handler is inspected to determine which
CommandorQuerytype itshandle()method accepts.
Cache Activation¶
Cache instances passed via the caches parameter are activated with cache_context():
- Each cache instance carries
CacheKeydefinitions that declare whichQueryandCommandtypes they intercept. cache_context()opens aCacheManagercontext around the operation.- Inside the context, handler-level read-through caching and command-level invalidation happen automatically via
get_cache_context(). - Query read-through values are written immediately. Command invalidations are flushed after a successful transaction commit and discarded on rollback.
Manual usage without container:
from aod.application.cache import CacheManager
cache = RedisCache(keys=[UserById()])
with Transaction(cache=CacheManager(cache)):
result = use_case.run(user_id=1)
Warning:
AsyncCacheinstances are silently ignored in syncUseCase/Projection— cache reads returnNoneand writes are skipped. UseCache(sync) with sync operations andAsyncCacheonly withAsyncUseCase/AsyncReadProjection/AsyncWriteProjection.
Session Caching¶
Once a session is instantiated via get_session(), the same instance is returned on subsequent calls. Handlers and projections register sessions when they use them, so handlers sharing a session type use the same instance.
Multi-Session Support¶
The container supports both sync and async sessions simultaneously. Use Transaction for sync operations and AsyncTransaction for async operations.
Auto-Wiring Logic¶
Use Case Wiring¶
When adapting a UseCase or AsyncUseCase:
| Field | Source |
|---|---|
CommandPort[C] / QueryPort[Q] |
container.get_handler(contract_type) |
| Custom ports | Named ports or ports dict (see Port Resolution Order) |
The container does not create a transaction during adaptation. Use container.transaction(cache=...) or container.async_transaction(cache=...) as convenience factories; transactions do not receive operations.
Projection Wiring¶
When adapting a ProjectionBase subclass:
| Field | Source |
|---|---|
| Session fields | container.get_session(session_type) for each field with a Session/AsyncSession type annotation |
| Custom ports | Named ports or ports dict (see Port Resolution Order) |
Projections do not have a default session field. Each session must be declared as a concrete type annotation (e.g., session: PostgresSession). Multiple session fields are supported.
Override Support¶
container = AdapterContainer(sessions={MySession}, handlers=[MyHandler])
use_case = container.adapt(
MyUseCase,
logger=SpyLogger(),
)
When **overrides are provided:
container.with_adapters(**overrides)creates a temporary container with overridden fields.- Injection proceeds using the overridden container.
Async Use Case Injection¶
Async use cases are wired identically to sync use cases:
from aod.application.async_ import UseCase
class MyAsyncUseCase(UseCase):
...
use_case = container.adapt(MyAsyncUseCase)
Use AsyncTransaction(cache=...) around async work, or container.async_transaction(cache=...). The async transaction is selected explicitly because the transaction coordinates the context, not an operation entrypoint.
Common Patterns¶
Base Container (No Subclassing)¶
container = AdapterContainer(
sessions={MySession},
handlers=[MyHandler],
caches=[RedisCache(keys=[UserById()])],
ports={Logger: SpyLogger()},
user_client=MyUserClient(),
)
use_case = container.adapt(CreateUser)
with container.transaction(cache=container.cache_context()):
use_case.run(user_id=42, name="Alice")
Named Ports¶
container = AdapterContainer(
sessions={MySession},
handlers=[MyHandler],
logger=SpyLogger(),
)
use_case = container.adapt(CreateUser)
Testing with Spy Container¶
from aod.testing.doubles import spy_adapter_container
container = spy_adapter_container(AdapterContainer(sessions={MySession}, handlers=[CreateUserHandler]))
container.get_handler_stub(CreateUserHandler).handle.return_value = None
container.stub_use_case(CreateUserUseCase, returns=None)
use_case = container.adapt(CreateUserUseCase)
use_case.run(user_id=42, name="Alice")
assert container.get_handler(CreateUser).handle.called
assert container.get_handler_stub(CreateUserHandler).handle.call_count == 1
container.stub_projection(UserProjection, read_returns=[])
proj = container.adapt(UserProjection)
result = proj.read(model)
Projection Injection¶
class UserProjection(ReadProjection):
session: MySession
def read(self, model: ReadModel) -> list[User]:
return self.session.query("SELECT * FROM users")
container = AdapterContainer(sessions={MySession})
proj = container.adapt(UserProjection)
result = proj.read(ReadModel())