Skip to content

Commit baeeba6

Browse files
committed
Updated DI documentation and route context
1 parent c776235 commit baeeba6

File tree

5 files changed

+539
-412
lines changed

5 files changed

+539
-412
lines changed

docs/dependency-injection/index.md

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
# Dependency Injection in Controllers
2+
3+
Django Ninja Extra provides powerful dependency injection capabilities using [Injector](https://injector.readthedocs.io/en/latest/). This guide will show you how to effectively use dependency injection in your controllers.
4+
5+
## **Basic Example**
6+
7+
Let's start with a simple example of dependency injection in a controller:
8+
9+
```python
10+
from ninja_extra import api_controller, http_get
11+
from injector import inject
12+
13+
class UserService:
14+
def get_user_count(self) -> int:
15+
return 42 # Example implementation
16+
17+
@api_controller("/users")
18+
class UserController:
19+
@inject
20+
def __init__(self, user_service: UserService): # Type annotation is required
21+
self.user_service = user_service
22+
23+
@http_get("/count")
24+
def get_count(self):
25+
return {"count": self.user_service.get_user_count()}
26+
```
27+
28+
## **Real-World Example: Todo Application**
29+
30+
Let's create a more practical example with a Todo application that demonstrates dependency injection with multiple services.
31+
32+
### 1. Define the Services
33+
34+
```python
35+
from typing import List, Optional
36+
from datetime import datetime
37+
from pydantic import BaseModel
38+
from injector import inject, singleton
39+
40+
# Data Models
41+
class TodoItem(BaseModel):
42+
id: int
43+
title: str
44+
completed: bool = False
45+
created_at: datetime
46+
47+
# Repository Service
48+
class TodoRepository:
49+
def __init__(self):
50+
self._todos: List[TodoItem] = []
51+
self._counter = 0
52+
53+
def add(self, title: str) -> TodoItem:
54+
self._counter += 1
55+
todo = TodoItem(
56+
id=self._counter,
57+
title=title,
58+
created_at=datetime.now()
59+
)
60+
self._todos.append(todo)
61+
return todo
62+
63+
def get_all(self) -> List[TodoItem]:
64+
return self._todos
65+
66+
def get_by_id(self, todo_id: int) -> Optional[TodoItem]:
67+
return next((todo for todo in self._todos if todo.id == todo_id), None)
68+
69+
def toggle_complete(self, todo_id: int) -> Optional[TodoItem]:
70+
todo = self.get_by_id(todo_id)
71+
if todo:
72+
todo.completed = not todo.completed
73+
return todo
74+
75+
# Business Logic Service
76+
class TodoService:
77+
@inject
78+
def __init__(self, repository: TodoRepository):
79+
self.repository = repository
80+
81+
def create_todo(self, title: str) -> TodoItem:
82+
return self.repository.add(title)
83+
84+
def get_todos(self) -> List[TodoItem]:
85+
return self.repository.get_all()
86+
87+
def toggle_todo(self, todo_id: int) -> Optional[TodoItem]:
88+
return self.repository.toggle_complete(todo_id)
89+
```
90+
91+
### 2. Create the Controller
92+
93+
```python
94+
from ninja_extra import api_controller, http_get, http_post, http_put
95+
from ninja import Body
96+
97+
# Request Models
98+
class CreateTodoRequest(BaseModel):
99+
title: str
100+
101+
@api_controller("/todos")
102+
class TodoController:
103+
def __init__(self, todo_service: TodoService):
104+
self.todo_service = todo_service
105+
106+
@http_post("")
107+
def create_todo(self, request: CreateTodoRequest = Body(...)):
108+
todo = self.todo_service.create_todo(request.title)
109+
return todo
110+
111+
@http_get("")
112+
def list_todos(self):
113+
return self.todo_service.get_todos()
114+
115+
@http_put("/{todo_id}/toggle")
116+
def toggle_todo(self, todo_id: int):
117+
todo = self.todo_service.toggle_todo(todo_id)
118+
if not todo:
119+
return {"error": "Todo not found"}, 404
120+
return todo
121+
```
122+
!!! warning
123+
You are not allowed to override your APIController constructor with parameters that don't have type annotations.
124+
The following example demonstrates the correct way to use type annotations in your constructor.
125+
Read more [**Python Injector** ](https://injector.readthedocs.io/en/latest/)
126+
127+
### 3. Register the Services
128+
129+
Create a module to register your services. When registering services, you can specify their scope:
130+
131+
- `singleton`: The service is created once and reused (default). Best for stateless services or services that maintain application-wide state.
132+
- `noscope` (transient): A new instance is created each time the service is requested. Best for services that maintain request-specific state.
133+
134+
```python
135+
from injector import Module, singleton, noscope, Binder
136+
137+
class TodoModule(Module):
138+
def configure(self, binder: Binder) -> None:
139+
# Singleton scope - same instance for entire application
140+
# TodoRepository maintains application state (the todos list)
141+
binder.bind(TodoRepository, to=TodoRepository, scope=singleton)
142+
143+
# Singleton scope - stateless service that only contains business logic
144+
binder.bind(TodoService, to=TodoService, scope=singleton)
145+
146+
# Example of when to use noscope
147+
# binder.bind(RequestContextService, to=RequestContextService, scope=noscope)
148+
```
149+
150+
!!! info
151+
If no scope is specified, services default to `singleton` scope. Choose the appropriate scope based on your service's requirements:
152+
153+
- Use `singleton` for:
154+
- Stateless services (like services that only contain business logic)
155+
- Services that maintain application-wide state
156+
- Services that are expensive to create
157+
- Use `noscope` for:
158+
- Services that maintain request-specific state
159+
- Services that need to be recreated for each request
160+
- Services with request-scoped dependencies
161+
162+
### 4. Configure Settings
163+
Add the module to your Django settings:
164+
165+
```python
166+
NINJA_EXTRA = {
167+
'INJECTOR_MODULES': [
168+
'your_app.modules.TodoModule'
169+
]
170+
}
171+
```
172+
!!! info
173+
Django-Ninja-Extra supports [**django_injector**](https://github.com/blubber/django_injector). If you're using django_injector, no additional configuration is needed in settings.py.
174+
175+
### 5. Register the API
176+
177+
```python
178+
from ninja_extra import NinjaExtraAPI
179+
180+
api = NinjaExtraAPI()
181+
api.register_controllers(TodoController)
182+
```
183+
184+
## **Advanced Usage: Multiple Dependencies**
185+
186+
You can inject multiple services into a controller:
187+
188+
```python
189+
from ninja_extra import api_controller, http_get
190+
from injector import inject
191+
192+
class AuthService:
193+
def is_admin(self) -> bool:
194+
return True # Example implementation
195+
196+
class LoggingService:
197+
def log_access(self, endpoint: str):
198+
print(f"Accessed: {endpoint}") # Example implementation
199+
200+
@api_controller("/admin")
201+
class AdminController:
202+
def __init__(
203+
self,
204+
auth_service: AuthService,
205+
logging_service: LoggingService,
206+
todo_service: TodoService
207+
):
208+
self.auth_service = auth_service
209+
self.logging_service = logging_service
210+
self.todo_service = todo_service
211+
212+
@http_get("/todos")
213+
def get_todos(self):
214+
if not self.auth_service.is_admin():
215+
return {"error": "Unauthorized"}, 403
216+
217+
self.logging_service.log_access("admin/todos")
218+
return self.todo_service.get_todos()
219+
```
220+
221+
## **Using Service Resolver**
222+
223+
Sometimes you might need to resolve services outside of controllers. Django Ninja Extra provides a `service_resolver` utility for this:
224+
225+
```python
226+
from ninja_extra import service_resolver
227+
228+
# Resolve a single service
229+
todo_service = service_resolver(TodoService)
230+
todos = todo_service.get_todos()
231+
232+
# Resolve multiple services
233+
todo_service, auth_service = service_resolver(TodoService, AuthService)
234+
```
235+
236+
## **Best Practices**
237+
238+
1. **Single Responsibility**: Keep your services focused on a single responsibility.
239+
2. **Interface Segregation**: Create specific interfaces for your services rather than large, monolithic ones.
240+
3. **Dependency Inversion**: Depend on abstractions rather than concrete implementations.
241+
4. **Scoping**: Use appropriate scopes for your services:
242+
- Use `singleton` for services that maintain application-wide state
243+
- Use `noscope` (transient) for services that should be created per request
244+
245+
246+
## **Testing with Dependency Injection**
247+
248+
Testing applications that use dependency injection requires special consideration for mocking services and managing test environments. We have a dedicated guide that covers all aspects of testing, including:
249+
250+
- Setting up separate development and testing environments
251+
- Implementing mock services
252+
- Using different testing frameworks (pytest, NinjaExtra TestClient)
253+
- Best practices for test configuration
254+
- Managing service dependencies in tests
255+
256+
For the complete guide on testing with dependency injection, see [Testing with Dependency Injection](testing_with_dependency_injection.md).

0 commit comments

Comments
 (0)