From 903d54c2622b19662adac6824334c5264a9a5244 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Fri, 10 Jul 2026 10:13:04 +0300 Subject: [PATCH 1/2] docs: add aiogram integration usage page --- docs/integrations/aiogram.md | 198 +++++++++++++++++++++++++++++++++++ mkdocs.yml | 2 + 2 files changed, 200 insertions(+) create mode 100644 docs/integrations/aiogram.md diff --git a/docs/integrations/aiogram.md b/docs/integrations/aiogram.md new file mode 100644 index 0000000..a7bd973 --- /dev/null +++ b/docs/integrations/aiogram.md @@ -0,0 +1,198 @@ +# Usage with `aiogram` + +aiogram has no dependency-injection system of its own, so `modern-di-aiogram` +uses the `@inject` decorator with `FromDI` markers (or `auto_inject=True` to +skip the decorator entirely). `setup_di` opens the root container on +dispatcher startup, closes it on shutdown, and installs middleware that opens +a per-update child container automatically. + +## How to use + +### 1. Install `modern-di-aiogram` + +=== "uv" + + ```bash + uv add modern-di-aiogram + ``` + +=== "pip" + + ```bash + pip install modern-di-aiogram + ``` + +=== "poetry" + + ```bash + poetry add modern-di-aiogram + ``` + +### 2. Apply to your application + +```python +import typing + +from aiogram import Dispatcher +from aiogram.types import Message +from modern_di import Container, Group, Scope, providers +from modern_di_aiogram import FromDI, inject, setup_di + + +class Settings: + def __init__(self) -> None: + self.greeting = "hello" + + +class AppGroup(Group): + settings = providers.Factory(Settings, scope=Scope.APP, cache=True) + + +dispatcher = Dispatcher() +setup_di(dispatcher, Container(groups=[AppGroup], validate=True)) + + +@dispatcher.message() +@inject +async def greet( + message: Message, + settings: typing.Annotated[Settings, FromDI(AppGroup.settings)], +) -> None: + await message.answer(f"{settings.greeting}, {message.from_user.first_name}") +``` + +`setup_di(dispatcher, container)` stores the container on the dispatcher, +registers `dispatcher.startup`/`dispatcher.shutdown` handlers that open/close +it, and installs an update-level outer middleware that builds a per-update +child container. + +## Auto-injecting handlers + +Passing `auto_inject=True` to `setup_di` wraps every handler already +registered on the dispatcher with `@inject` automatically, so individual +handlers don't need the decorator: + +```python +import typing + +from aiogram import Dispatcher, Router +from aiogram.types import Message +from modern_di import Container +from modern_di_aiogram import FromDI, setup_di + + +router = Router() + + +@router.message() +async def greet( + message: Message, + settings: typing.Annotated[Settings, FromDI(AppGroup.settings)], +) -> None: + await message.answer(f"{settings.greeting}, {message.from_user.first_name}") + + +dispatcher = Dispatcher() +dispatcher.include_router(router) +setup_di(dispatcher, Container(groups=[AppGroup], validate=True), auto_inject=True) +``` + +!!! warning "Register handlers before startup" + `auto_inject` wraps handlers on `dispatcher.startup`, which fires from + `dispatcher.emit_startup()` — the call `start_polling()`/`start_webhook()` + makes before serving updates. Only handlers registered (via + `dispatcher.include_router()` or the decorators directly) **before** + `emit_startup()` runs are wrapped; a handler added afterward is invoked + without injection and any `FromDI` parameter on it is left unresolved. + +## Scopes + +The integration creates one `Scope.REQUEST` child container **per update**. +The middleware is installed on `dispatcher.update` as an +[outer middleware](https://docs.aiogram.dev/en/latest/dispatcher/middlewares.html), +so it wraps every update regardless of which router or handler ultimately +processes it. The child container is closed after the handler runs — +including when it raises. + +There is no `Scope.SESSION` for aiogram — each Telegram update is handled +independently; there's no persistent per-chat/per-user connection comparable +to a WebSocket. See [the scope hierarchy](../providers/scopes.md#the-scope-dependency-rule). + +## Sync resolution, async cleanup + +`FromDI` resolves its dependency with `Container.resolve_dependency(...)`, +which is synchronous — modern-di's resolution is always sync, regardless of +the framework. The per-update `Scope.REQUEST` child container that resolution +runs against is nevertheless torn down asynchronously: after the handler +finishes (or raises), the integration awaits `child_container.close_async()`. +So async finalizers on REQUEST-scoped providers run correctly, while the +factories themselves must build synchronously. + +## Framework context objects + +`aiogram.types.Update` and the concrete event it carries (`Message`, +`CallbackQuery`, etc.) are automatically made available by the integration, +so factories can declare them as parameters — see +[Framework Context Objects](../providers/context.md#framework-context-objects) +for how implicit and explicit resolution work. + +The following context providers are also available for explicit import: + +- `aiogram_update_provider` — provides the current `aiogram.types.Update`. +- `aiogram_event_provider` — provides the current `aiogram.types.TelegramObject`, + the concrete event unwrapped from the `Update` (e.g. a `Message` or + `CallbackQuery` instance). + +### Implicit (type-based) usage + +```python +from aiogram.types import TelegramObject, Update +from modern_di import Group, Scope, providers + + +def create_update_info(update: Update, event: TelegramObject) -> dict[str, str]: + return { + "update_id": str(update.update_id), + "event_type": type(event).__name__, + } + + +class AppGroup(Group): + # Update and TelegramObject are resolved by type annotation + update_info = providers.Factory( + create_update_info, + scope=Scope.REQUEST, + ) +``` + +### Explicit (provider-based) usage + +`aiogram_event_provider` is bound to the base `TelegramObject` type, so +narrowing a parameter to a concrete event type (like `Message`) requires +wiring it explicitly with `FromDI`: + +```python +import typing + +from aiogram.types import Message +from modern_di_aiogram import FromDI, aiogram_event_provider, inject + + +@inject +async def log_message( + message: Message, + same_message: typing.Annotated[Message, FromDI(aiogram_event_provider)], +) -> None: + assert message is same_message +``` + +## API + +| Symbol | Description | +|---|---| +| `setup_di(dispatcher, container, *, auto_inject=False)` | Stores the container on the dispatcher, registers `aiogram_update_provider`/`aiogram_event_provider`, wires `dispatcher.startup`/`dispatcher.shutdown` to open/close the container, and installs the per-update middleware. With `auto_inject=True`, also wraps every handler already registered on the dispatcher at startup. | +| `FromDI(dependency)` | Marker (used with `@inject`) that resolves a provider or type from the per-update child container. | +| `inject` | Decorator for an aiogram handler; resolves its `FromDI`-annotated parameters. Not needed when `setup_di(..., auto_inject=True)` is used. | +| `fetch_di_container(dispatcher)` | Returns the root `Container` stored on the dispatcher. | +| `aiogram_update_provider` | `ContextProvider` for the current `aiogram.types.Update` (REQUEST scope). | +| `aiogram_event_provider` | `ContextProvider` for the current `aiogram.types.TelegramObject` (REQUEST scope) — the concrete event unwrapped from the `Update`. | diff --git a/mkdocs.yml b/mkdocs.yml index b385325..9fbf71a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -21,6 +21,7 @@ nav: - Errors and exceptions: providers/errors-and-exceptions.md - Advanced / low-level API: providers/advanced-api.md - Integrations: + - aiogram: integrations/aiogram.md - aiohttp: integrations/aiohttp.md - FastAPI: integrations/fastapi.md - FastStream: integrations/faststream.md @@ -159,6 +160,7 @@ plugins: - providers/errors-and-exceptions.md: The ModernDIError exception hierarchy - providers/advanced-api.md: Low-level extension points for library authors Integrations: + - integrations/aiogram.md: Usage with aiogram - integrations/aiohttp.md: Usage with aiohttp - integrations/fastapi.md: Usage with FastAPI - integrations/faststream.md: Usage with FastStream From 096c8ff9b8e1dafef38873b81d7513961cdd4c74 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Fri, 10 Jul 2026 10:17:47 +0300 Subject: [PATCH 2/2] docs: make aiogram auto-inject example self-contained The auto-inject block referenced Settings/AppGroup without defining them; add the definitions (mirroring the first example) so the block is independently runnable, matching the sibling integration pages. --- docs/integrations/aiogram.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/integrations/aiogram.md b/docs/integrations/aiogram.md index a7bd973..66063a6 100644 --- a/docs/integrations/aiogram.md +++ b/docs/integrations/aiogram.md @@ -77,10 +77,19 @@ import typing from aiogram import Dispatcher, Router from aiogram.types import Message -from modern_di import Container +from modern_di import Container, Group, Scope, providers from modern_di_aiogram import FromDI, setup_di +class Settings: + def __init__(self) -> None: + self.greeting = "hello" + + +class AppGroup(Group): + settings = providers.Factory(Settings, scope=Scope.APP, cache=True) + + router = Router()