Skip to content

Usage with Litestar

modern-di-litestar builds on Litestar's own dependency injection. FromDI returns a Litestar Provide, which you list in a route's dependencies like any other, and the handler declares the parameter with NamedDependency[...]. ModernDIPlugin gives each HTTP request a Scope.REQUEST child container and each websocket connection a Scope.SESSION one, and adds a lifespan hook that closes the root container on shutdown.

More advanced example of usage with Litestar: litestar-sqlalchemy-template

Installation

uv add modern-di-litestar
pip install modern-di-litestar
poetry add modern-di-litestar

Usage

import dataclasses

import litestar
import modern_di_litestar
from litestar.di import NamedDependency
from modern_di import Container, Group, Scope, providers


@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
class Settings:
    service_name: str = "catalog"


@dataclasses.dataclass(kw_only=True, slots=True)
class Report:
    settings: Settings   # APP-scoped, injected by type

    def as_dict(self) -> dict[str, str]:
        return {"service": self.settings.service_name}


class AppGroup(Group):
    settings = providers.Factory(Settings, scope=Scope.APP, cache=True)
    report = providers.Factory(Report, scope=Scope.REQUEST)


@litestar.get("/report", dependencies={"report": modern_di_litestar.FromDI(Report)})
async def get_report(report: NamedDependency[Report]) -> dict[str, str]:
    return report.as_dict()


container = Container(groups=[AppGroup])
app = litestar.Litestar(
    route_handlers=[get_report],
    plugins=[modern_di_litestar.ModernDIPlugin(container)],
)
container.validate()

Mark every injected parameter with NamedDependency[...] from litestar.di. Litestar 2.23 deprecated inferring a dependency from a plain annotation, and Litestar 3.0 removes it.

Call container.validate() after Litestar(...) is constructed. The plugin registers its context providers in on_app_init, which Litestar runs during construction, and a factory that takes a litestar.Request depends on one of them, so validating before that fails with ValidationFailedError.

Injecting into handlers

FromDI takes a type, as above, or a provider such as FromDI(AppGroup.report), and resolves it from the connection's child container. The dependencies mapping can sit on a route, a controller, a router or the app, as with any Litestar dependency.

The plugin also registers a di_container dependency, the connection's child container itself. A handler receives it by declaring di_container: NamedDependency[Container], with no FromDI.

Auto-wiring with autowired_groups

Pass autowired_groups to ModernDIPlugin to register every provider in those groups as a Litestar dependency, keyed by its attribute name. A handler then receives a provider by naming a parameter after it, with no per-route FromDI:

@litestar.get("/autowired-report")
async def get_autowired_report(report: NamedDependency[Report]) -> dict[str, str]:
    return report.as_dict()


autowired_app = litestar.Litestar(
    route_handlers=[get_autowired_report],
    plugins=[modern_di_litestar.ModernDIPlugin(Container(groups=[AppGroup]), autowired_groups=[AppGroup])],
)

If an attribute name appears in more than one group, or matches a dependency the app already has, a UserWarning is emitted and the autowired provider overwrites the earlier one; among groups, the last one wins.

Don't name a provider di_container

An autowired provider named di_container replaces the plugin's own container dependency, which every FromDI depends on. Litestar then fails to build the app with RecursionError.

With autowired_groups set, FromDI on a route can still take a type. Pass a provider instance only for providers outside autowired_groups: Litestar rejects one provider registered under two keys and raises ImproperlyConfiguredException.

Scopes and lifecycle

An HTTP request gets a Scope.REQUEST child container and a websocket connection a Scope.SESSION one; see the scope hierarchy. The child is a Litestar generator dependency, so it is built only for handlers that inject something and closed in Litestar's dependency cleanup.

The root container is open from the moment you construct it. The plugin appends a lifespan hook that reopens it on startup and closes it with close_async() on shutdown. That hook is the only thing that closes the root container, so when the server skips the lifespan, requests still succeed but its finalizers never run. Close it yourself (await container.close_async()) at shutdown in that case.

Websockets

In a websocket handler, di_container is the connection's Scope.SESSION container, which lives as long as the connection. For per-message work, open a Scope.REQUEST child of it:

@litestar.websocket_listener("/ws")
async def report_socket(data: str, di_container: NamedDependency[Container]) -> dict[str, str]:
    async with di_container.build_child_container(scope=Scope.REQUEST) as request_container:
        return request_container.resolve(Report).as_dict()


websocket_app = litestar.Litestar(
    route_handlers=[report_socket],
    plugins=[modern_di_litestar.ModernDIPlugin(Container(groups=[AppGroup]))],
)

Injecting a REQUEST-scoped provider such as FromDI(Report) straight into a websocket handler raises ScopeNotInitializedError, because no REQUEST container exists for the connection.

Framework context objects

The integration makes litestar.Request and litestar.WebSocket available to your factories. See Framework context objects for how implicit and explicit resolution work.

  • litestar_request_provider provides the current litestar.Request (REQUEST scope).
  • litestar_websocket_provider provides the current litestar.WebSocket (SESSION scope).

A factory can receive the request by type, or name the provider in kwargs:

def describe_request(request: litestar.Request) -> dict[str, str]:
    return {"method": request.method, "url": str(request.url)}


class RequestGroup(Group):
    by_type = providers.Factory(describe_request, scope=Scope.REQUEST, bound_type=None)
    by_provider = providers.Factory(
        describe_request,
        scope=Scope.REQUEST,
        bound_type=None,
        kwargs={"request": modern_di_litestar.litestar_request_provider},
    )

See also

API

Symbol Description
ModernDIPlugin(container, autowired_groups=None) Litestar InitPlugin that stores the container on the app state, registers the request and websocket context providers and the di_container dependency, appends a lifespan hook that closes the container on shutdown, and (if autowired_groups is given) exposes each provider in those groups as a Litestar dependency keyed by attribute name.
FromDI(dependency) Returns a Litestar Provide that resolves a provider or type from the connection's child container (REQUEST for HTTP, SESSION for a websocket).
di_container The dependency name the plugin registers for the connection's child container; declare di_container: NamedDependency[Container] to receive it.
fetch_di_container(app) Returns the root Container stored on the Litestar app.
litestar_request_provider ContextProvider for litestar.Request (REQUEST scope), auto-registered.
litestar_websocket_provider ContextProvider for litestar.WebSocket (SESSION scope), auto-registered.