Autowired Dependency Injection¶
Overview¶
Autowired provides declarative property injection for the Loryth DI container. The mechanism is entirely metadata-driven: annotate attributes with typing.Annotated and the DI runtime will inject resolved dependencies before the instance is used. The new form removes helper factories and legacy markers, keeping annotations concise and type-checker friendly.
from typing import Annotated
from loryth.common.di.injection import Autowired
class OrderService:
repository: Annotated["OrderRepository", Autowired()]
logger: Annotated["LoggerProtocol", Autowired("orders")]
Quick Start¶
from typing import Annotated
from loryth.common.di.bootstrap import BootstrapPlan
from loryth.common.di.bootstrap import BootstrapRuntime
from loryth.common.di.bootstrap import SingletonRegistrationStep
from loryth.common.di.container import Container
from loryth.common.di.injection import Autowired
class Database:
def save(self, entity: object) -> None:
...
class Logger:
def log(self, message: str) -> None:
...
class UserService:
database: Annotated[Database, Autowired()]
audit_logger: Annotated[Logger, Autowired("audit")]
def create_user(self, username: str) -> None:
self.audit_logger.log(f"creating {username}")
self.database.save({"username": username})
container = Container()
plan = BootstrapPlan(
registrations=(
SingletonRegistrationStep(Database, Database),
SingletonRegistrationStep(Logger, Logger, qualifier="audit"),
)
)
BootstrapRuntime().bootstrap(container, plan)
service = container.resolve(UserService)
service.create_user("alice")
Metadata Reference¶
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class Autowired:
qualifier: str | None = None
qualifier(optional): named binding identifier to request a specific registration. Qualifiers are stripped of surrounding whitespace and validated for non-empty strings.
Autowired metadata is only used inside Annotated. No runtime aliasing or helper factories are required.
Usage Patterns¶
Default Injection¶
class EmailService:
sender: Annotated["EmailSender", Autowired()]
When the container resolves EmailService, AutowiredInjector looks up the EmailSender registration and assigns it to sender.
Qualified Injection¶
class ReportingService:
primary_logger: Annotated["Logger", Autowired("primary")]
audit_logger: Annotated["Logger", Autowired("audit")]
Register two loggers with distinct qualifiers. The injector selects the correct binding based on the qualifier metadata.
Combined Constructor and Property Injection¶
class SessionManager:
token_cache: Annotated["Cache", Autowired()]
def __init__(self, clock: "Clock") -> None:
self.clock = clock
Constructor parameters come from the constructor injector, while token_cache is populated by AutowiredInjector.
Best Practices¶
- Annotate every dependency. Unannotated attributes are ignored by the injector.
- Prefer explicit qualifiers when multiple implementations for the same type are registered. Omitting the qualifier relies on the container's primary registration.
- Avoid runtime mutation of injected properties. Treat them as read-only.
- Keep annotations importable. Use forward references (
"TypeName") when the injected class is defined later in the module.
Troubleshooting¶
- Missing dependency:
PropertyInjectionErrorwraps the underlying resolution failure. Confirm the service is registered (and qualified, if applicable). - Qualifier mismatch: Ensure the registration uses the same qualifier string as the annotation metadata.
- Type checking issues:
Annotatedpreserves static typing. Confirm the imported type is the same one used for registration.
Related APIs¶
loryth.common.di.injection.AutowiredInjectorloryth.common.di.di_container_builder.ContainerBuilderloryth.common.di.bootstrap.BootstrapRuntime
For constructor injection and advanced wiring strategies see: - Constructor Injection Guide (coming soon) - Dependency Qualifiers (coming soon)