|
| 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