Dependency Injection (IoC Container)
Explore ZCore's custom Inversion of Control container, constructor auto-wiring, signature caching, and lifecycle management.
ZCore does not rely on FastAPI's native Depends() chains for internal service wiring. Instead, it implements a high-performance IoCContainer designed for constructor auto-wiring and clean architecture separation.
1. The Problem with Nested Depends()
In standard FastAPI, if a route depends on a service, the service depends on a repository, and the repository depends on an AsyncSession, you end up with deeply nested, tightly coupled Depends() chains in your HTTP layer:
# Standard FastAPI (High coupling & boilerplate)
@router.post("/tasks")
def create_task(
service: TaskService = Depends(get_task_service) # get_task_service must resolve repo & session
):
passThis couples web routes directly to internal persistence details, degrades IDE type-hinting, and makes unit testing difficult.
2. The Inject[T] Abstraction
ZCore uses Python's Annotated metadata system to seamlessly bridge FastAPI's route dependency injection with ZCore's IoC container:
# ZCore Approach (Clean & Decoupled)
from zcore import Inject
@router.post("/tasks")
async def create_task(service: Inject[TaskService]):
await service.create(...)Compilation Magic: When you annotate a parameter with service: Inject[TaskService], the Inject class intercepts the __class_getitem__ dunder method. It compiles this into:
Annotated[TaskService, Depends(Injector(TaskService))].
This preserves 100% compatibility with FastAPI's OpenAPI generation while delegating full dependency graph assembly to ZCore's IoC container.
3. Lifecycle Management
The IoCContainer coordinates three distinct instance lifecycles:
- Singleton: Instantiated once for the entire application lifespan and stored in
container._singletons(e.g.,EventDispatcher,Settings). - Scoped: Instantiated once per ASGI HTTP request and bound to the coroutine context via
ContextVar(_scoped_instances). Managed and automatically purged byScopedDependencyMiddlewareafter the response is sent (e.g.,AsyncSession). - Transient: A fresh, independent instance is created every time
container.resolve()is executed (e.g., stateless helper utilities).
4. Auto-Wiring & Signature Caching
Inside your classes and services, you do not need decorators. Declare dependencies in the constructor via standard type hints:
class TaskService:
# ZCore inspects the type hint and resolves TaskRepo automatically
def __init__(self, repository: TaskRepo):
self.repository = repositoryWhen container.resolve(TaskService) is invoked:
- The container inspects
TaskService.__init__usinginspect.signatureandtyping.get_type_hints. - Signature Caching: To eliminate the runtime performance overhead of Python reflection, ZCore caches the constructor parameter dependencies in
_dependency_signature_cacheand constructor references in_constructor_cache. - Subsequent resolutions bypass reflection completely, reading directly from the warm signature cache to construct instances with near-zero latency.
5. Circular Dependency Detection
If ServiceA depends on ServiceB, and ServiceB depends on ServiceA, a naive container would enter infinite recursion and crash the interpreter.
ZCore tracks the active resolution graph using an internal _stack set during auto-wiring. If a class is encountered that already exists in the active stack, it raises a readable CircularDependencyError:
CircularDependencyError: Circular dependency detected: ServiceA -> ServiceB -> ServiceAConstructor vs Route Usage:
- In Class Constructors (
__init__): Use plain Python type hints (def __init__(self, repo: TaskRepo):). - In FastAPI Route Endpoints: Use
Inject[T](async def route(service: Inject[TaskService]):).
6. Background Tasks & Isolated Scope (background_scope, @background_task)
When an HTTP request completes, the ASGI middleware disposes of the request-scoped database session and purges _scoped_instances. Running background routines after the request finishes requires an independent scope.
ZCore provides two utilities for safe background processing:
A. The @background_task Decorator
Decorate any asynchronous coroutine or standard synchronous function. The decorator:
- Spawns an isolated
background_scope(). - Automatically allocates a fresh, dedicated
AsyncSession. - Inspects the function's signature and auto-resolves any missing type-annotated dependencies via
container.resolve(). - Executes synchronous functions in a dedicated worker threadpool via
anyio.to_thread.run_sync.
from zcore import background_task
from tasks.services import TaskService
@background_task
async def send_welcome_email(user_id: uuid.UUID, service: TaskService):
# 'service' and its underlying AsyncSession are automatically resolved in an isolated scope
await service.notify(user_id)Avoid Passing Active Request Sessions: Do not pass an existing AsyncSession from an HTTP route directly into a background task. The request session will be closed when the HTTP response completes. Let @background_task inject an isolated session automatically; passing an active session triggers a runtime logger warning.
B. The background_scope Context Manager
For manual execution blocks, background_scope provides explicit boundary control:
from zcore import background_scope
async with background_scope(inherit_context=True, custom_flag="worker"):
# Clean, isolated IoC and DB session lifecycle
...Configuration & Context Management
Deep dive into how ZCore handles application settings via lazy proxies and isolates request-scoped state using contextvars.
Kernel & Plugin Orchestration
Understand how ZCore orchestrates application modularity, topological startup ordering, and deterministic lifespans.