Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
207 changes: 207 additions & 0 deletions docs/integrations/aiogram.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
# 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, 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()


@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`. |
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down