Async resources via lifespan¶
Some resources need an await or a running event loop to construct: aiohttp.ClientSession, an asyncpg connection pool, or a client that exchanges a token at startup. modern-di resolves synchronously, so a creator cannot await. Build these resources in the framework's lifespan and hand the live object to the container.
Solution¶
In the lifespan, build the resource and register it on the APP container with container.set_context(SomeType, instance). Declare a ContextProvider(SomeType, scope=Scope.APP) so other factories can depend on the type. With FastAPI:
import contextlib
import dataclasses
from collections.abc import AsyncIterator
import aiohttp
import fastapi
import modern_di_fastapi
from modern_di import Container, Group, Scope, providers
@dataclasses.dataclass
class WeatherApi:
client: aiohttp.ClientSession
async def forecast(self, city: str) -> dict[str, str]:
async with self.client.get(f"https://weather.example.com/{city}") as response:
return await response.json()
class Dependencies(Group):
http_client = providers.ContextProvider(aiohttp.ClientSession, scope=Scope.APP)
weather_api = providers.Factory(WeatherApi, scope=Scope.REQUEST)
container = Container(groups=[Dependencies])
@contextlib.asynccontextmanager
async def lifespan(app: fastapi.FastAPI) -> AsyncIterator[None]:
async with aiohttp.ClientSession() as session:
container.set_context(aiohttp.ClientSession, session)
yield
app = fastapi.FastAPI(lifespan=lifespan)
modern_di_fastapi.setup_di(app, container)
container.validate()
@app.get("/forecast/{city}")
async def forecast(
city: str,
weather_api: WeatherApi = modern_di_fastapi.FromDI(WeatherApi),
) -> dict[str, str]:
return await weather_api.forecast(city)
WeatherApi receives the session through its client: aiohttp.ClientSession annotation. aiohttp.ClientSession needs a running event loop when it is constructed, and the lifespan runs inside the application's loop.
setup_di wraps your lifespan: yours stays the outer context, and the integration opens the container inside it and closes it at shutdown, before your async with block exits. APP-scoped finalizers therefore run while the session is still open, and the session closes after them.
Without an integration, enter the container yourself, after the resource, so it closes first:
import contextlib
from collections.abc import AsyncIterator
import aiohttp
import fastapi
@contextlib.asynccontextmanager
async def lifespan(app: fastapi.FastAPI) -> AsyncIterator[None]:
async with aiohttp.ClientSession() as session, container:
container.set_context(aiohttp.ClientSession, session)
yield
The same pattern fits asyncpg.create_pool(...), which returns a pool that opens its connections only when awaited, and any client that needs an await before it is ready. modern-di has async finalizers but no async initializer, so that await belongs in the lifespan.
Pitfalls¶
set_contextnever propagates between containers; see Context propagation. Here that is fine: theContextProvideris APP-scoped and the value is set on the APP container, so every REQUEST child reaches it.- Don't also wrap the lifespan in
async with container:when you usesetup_di. It is redundant: the integration already closes the container, and the second close runs no finalizers. - The container does not own the lifespan's resource. It was never created by a
Factory, so no finalizer closes it; theasync with aiohttp.ClientSession()block does.
When a sync creator works instead¶
Many async clients construct synchronously: redis.asyncio.Redis.from_url(...), sqlalchemy.ext.asyncio.create_async_engine(...) and httpx.AsyncClient(...) all return without awaiting. For those, use a plain Factory with cache=CacheSettings(finalizer=...) and an async finalizer, and skip the lifespan and set_context. Use this recipe only when construction needs await or a running event loop.
See also¶
- Lifecycle:
close_async()and finalizers. - Context providers:
ContextProviderandset_contextin depth. - Scopes: APP vs SESSION vs REQUEST.
- Async SQLAlchemy recipe: the sync-creator-with-async-finalizer pattern for comparison.