Context Providers¶
Often, scopes are connected with external events: HTTP requests, messages from a queue, callbacks from a framework. These events can be represented by objects which can be used for dependency creation.
ContextProvider is a provider type that injects runtime context values — framework objects like
requests or websockets, or your own custom context — into dependencies, extracting them from the
container's context registry at resolve time.
In integrations, some context objects (like fastapi.Request, litestar.WebSocket, etc.) are
automatically provided — see Framework Context Objects below.
ContextProvider(context_type, *, scope=Scope.APP, bound_type=UNSET) — context_type may also be
passed as a keyword (context_type=).
Basic Usage¶
Declare a ContextProvider for your context type, supply the value when you build the child container, and any Factory that takes that type as a parameter receives it automatically:
from modern_di import Group, Container, Scope, providers
# Custom context type
class CustomContext:
def __init__(self, user_id: str, tenant_id: str) -> None:
self.user_id = user_id
self.tenant_id = tenant_id
def create_user_info(custom_context: CustomContext) -> dict[str, str]:
return {
"user_id": custom_context.user_id,
"tenant_id": custom_context.tenant_id,
}
class Dependencies(Group):
# Manually defined ContextProvider for custom context
custom_context = providers.ContextProvider(CustomContext, scope=Scope.REQUEST)
# Factory uses the custom context
user_info = providers.Factory(
create_user_info,
scope=Scope.REQUEST,
)
# Provide custom context when building the child container
container = Container(groups=[Dependencies])
custom_context = CustomContext(user_id="123", tenant_id="abc")
request_container = container.build_child_container(
scope=Scope.REQUEST,
context={CustomContext: custom_context}
)
# Now resolve the factory — it will receive the custom context automatically
user_info = request_container.resolve_provider(Dependencies.user_info)
# {"user_id": "123", "tenant_id": "abc"}
The provider is bound to a scope (here Scope.REQUEST) and the value is supplied via
build_child_container(context={...}).
When no value is set¶
A ContextProvider reads its value from the context of the container at its bound scope. If nothing was supplied, behavior depends on the call path:
- Resolving it directly (
container.resolve(CustomContext)) raisesContextValueNotSetError(see Migration: direct resolve of an unsetContextProviderraises). - Injecting it into a
Factoryparameter that is notOptional/defaulted raisesArgumentResolutionError.
Annotate the consuming parameter as X | None (or give it a default) if the value can legitimately be absent. See ContextProvider has no value.
Context propagation¶
Context never propagates between containers. A ContextProvider reads the context registry of the container at the provider's own scope — build order is irrelevant.
Scope determines which container is read, not timing
Setting context on a parent container never reaches a child-scoped provider, regardless of when you call set_context:
# ❌ Broken: a REQUEST-scoped provider reads the REQUEST container's registry.
# Setting it on the APP parent has no effect.
app_container = Container()
app_container.set_context(CustomContext, value) # ignored for REQUEST-scoped providers
request_container = app_container.build_child_container(scope=Scope.REQUEST)
For a REQUEST-scoped ContextProvider, set the value on the request container:
# Option A: pass context directly when building the child
request_container = app_container.build_child_container(
scope=Scope.REQUEST, context={CustomContext: value}
)
# Option B: set on the request container after building it
request_container = app_container.build_child_container(scope=Scope.REQUEST)
request_container.set_context(CustomContext, value)
Setting context on the parent only works when the ContextProvider's scope matches the parent's scope.
Framework Context Objects¶
Every framework integration auto-registers ContextProviders for its own request/websocket-like
objects — you never declare a ContextProvider for these yourself. Each integration builds a
per-request (or per-message, or per-connection) child container and sets the framework object as
context on it before your code resolves anything from it. There are two ways to consume that value:
Implicit usage (type-based resolution). Annotate a factory parameter with the framework's
type; because the integration already registered a matching ContextProvider, modern-di resolves
it automatically — the same mechanism as Basic Usage above, just with the
ContextProvider declared by the integration instead of by you. With
FastAPI, the fastapi.Request is injected into each per-request
child container automatically:
from modern_di import Group, Container, Scope, providers
import fastapi
import modern_di_fastapi
def create_request_info(request: fastapi.Request) -> dict[str, str]:
return {"method": request.method, "url": str(request.url)}
class Dependencies(Group):
# Factory uses the request from context (automatically provided by the integration)
request_info = providers.Factory(
create_request_info,
scope=Scope.REQUEST,
)
ALL_GROUPS = [Dependencies]
app = fastapi.FastAPI()
container = Container(groups=ALL_GROUPS)
modern_di_fastapi.setup_di(app, container)
# setup_di() registers fastapi.Request's ContextProvider, so the graph is complete
# from here on — call validate() after this line, not before.
container.validate()
# The integration creates a REQUEST-scoped child container per request and
# injects the fastapi.Request into its context, so `request` is the real object
# at runtime.
Nothing validates automatically, so the ordering above is what matters: fastapi.Request's
ContextProvider only exists once setup_di() has registered it, so calling
container.validate() before that line would raise
ValidationFailedError — its .errors would
carry an ArgumentResolutionError for the
required request parameter, since the provider isn't there yet. Call validate() after
setup_di(), as above, and a required parameter validates cleanly. See Writing an
integration for the same rule from the
integration author's side.
If you need to validate the rest of the graph before setup_di() runs — e.g. as part of a
narrower, construction-time check — make the parameter optional instead
(request: fastapi.Request | None = None), so validate() skips it regardless of whether the
connection provider is registered yet; at runtime the integration still injects the real
Request, since it always sets the per-request context before resolving. A defaulted Factory
parameter keeps its own disposition here too: ContextValueNotSetError (see When no value is
set above) affects only a direct resolve of an unset context type, not
a defaulted parameter, which still falls back to its default when no context is set.
Explicit usage (provider-based resolution). Every integration also exports the underlying
ContextProvider object itself (e.g. fastapi_request_provider, litestar_request_provider,
aiohttp_request_provider, faststream_message_provider) so you can wire it through kwargs
instead of relying on type-based resolution — useful with skip_creator_parsing=True, or when the
parameter name doesn't match the type:
Each integration's own page has its exact provider names, scopes, and API table: FastAPI, Litestar, Starlette, FastStream, aiohttp.
See also¶
- Factories — how factories receive injected context values.
- Scopes — choosing the scope a
ContextProvideris bound to. - Container —
build_child_containerandset_context. - FastAPI integration — framework-provided context objects.