Dependency Injection
One of the core features of Django Ninja Extra APIController is its support dependency injection using Injector
For example, if you have a service called AuthService and you want to use it in your UsersController
class,
you can simply add it as a parameter in the constructor of the class and annotate it with its type.
class UsersController(ControllerBase):
def __init__(self, auth_service: AuthService):
self.auth_service = auth_service
Then in your application config, you can register this service and its scope. By default, services are singleton scoped unless specified.
def configure(binder: Binder) -> Binder:
binder.bind(AuthService, to=AuthServiceImpl, scope=singleton)
You can also specify the scope of the service. This is useful when you want to use different instances of the same service for different requests.
def configure(binder: Binder) -> Binder:
binder.bind(AuthService, to=AuthServiceImpl, scope=noscope)
In this way, you can easily inject services into your controllers and use them throughout your application. This makes it easy to test your controllers as well as to change the implementation of a service without affecting the rest of the application.
Info
Django-Ninja-Extra supports django_injector. There is no extra configuration needed.
Creating a Service
A service refers to a self-contained module or piece of functionality that can be reused across different parts of an application. Services are typically used to encapsulate business logic or to provide access to shared resources, such as databases or external APIs. Services are often implemented as classes, and they can be accessed through an object-oriented interface. Services can be used to perform actions, to interact with external systems, and to perform calculations. They usually have some public methods and properties, which allows other objects to interact with them. Services are usually used to separate the application logic from the infrastructure, this way the application logic can be reused, tested and maintained independently.
Let's create a simple S3 bucket service, create a service.py
in your project and add the cold below
from ninja import File
from ninja.files import UploadedFile
from ninja_extra import NinjaExtraAPI, api_controller, http_post
class BucketFileUploadService:
def upload_file_to_s3(self, file, bucket_name=None, acl="public-read", file_key=None):
pass
def upload_existing_file_to_s3(
self, filepath, file_key, bucket_name=None, acl="public-read", delete_file_afterwards=False,
clean_up_root_limit=None
):
pass
@api_controller('/user_profile')
class UserProfileController:
def __init__(self, upload_service: BucketFileUploadService):
self.upload_service = upload_service
@http_post('/upload')
def upload_profile_pic(self, file: UploadedFile = File(...)):
self.upload_service.upload_file_to_s3(file=file)
return {'message', 'uploaded successfully'}
api = NinjaExtraAPI(title='Injector Test')
api.register_controllers(UserProfileController)
Create a module
In Python Injector, a module is a class or a function that is used to configure the dependency injection container. A module is responsible for binding services to their implementations and for configuring the scope of services.
A module can define a configure(binder: Binder)
function that is used to configure the dependency injection container.
The binder
argument is an instance of the Binder class that is used to bind services to their implementations.
A module can also define one or more provider functions, which are used to create instances of services.
These functions can be decorated with @inject
to specify the dependencies that they need to be resolved,
and they can be decorated with @provider
to indicate that they should be used to create instances of services.
For example:
from injector import Binder, singleton, inject, provider
class MyModule:
def configure(self, binder: Binder) -> Binder:
binder.bind(AuthService, to=AuthServiceImpl, scope=singleton)
@provider
@inject
def provide_user_service(self, auth_service: AuthService) -> UserService:
return UserService(auth_service)
MyModule
class has configure
method which is used to bind the AuthService
and set the scope
as singleton
and provide_user_service
which is decorated with @provider
and @inject
to provide
UserService
and the AuthService
is injected to it as a dependency.
By registering a module in Ninja Extra settings, all the services, providers and configurations defined in the module will be added to the Injector, and these services can be resolved and used throughout the application.
Lets creates a module for the BucketFileUpload
service we created earlier. Create a module.py
in your project and add the code below.
import logging
import os
from typing import cast
from django.conf import Settings
from injector import inject, Module, Binder, singleton
logger = logging.getLogger()
class InMemoryBucketFileUpload(BucketFileUpload):
@inject
def __init__(self, settings: Settings):
logger.info(f"===== Using InMemoryBucketFileUpload =======")
self.settings = settings
assert isinstance(self.settings, Settings)
def upload_file_to_s3(self, file, bucket_name=None, acl="public-read", file_key=None):
logger.info(
f"InMemoryBucketFileUpload ---- "
f"upload_file_to_s3(file={file.filename}, bucket_name{bucket_name}, acl={acl}, file_key={file_key})"
)
if not file_key:
return os.path.join(self.settings.UPLOAD_FOLDER, file.filename)
return os.path.join(self.settings.BASE_DIR, file_key)
def upload_existing_file_to_s3(self, filepath, file_key, bucket_name=None, acl="public-read",
delete_file_afterwards=False, clean_up_root_limit=None):
logger.info(f"InMemoryBucketFileUpload ---- upload_existing_file_to_s3("
f"filepath={filepath}, file_key={file_key}, "
f"bucket_name={bucket_name}, acl={acl}, delete_file_afterwards={delete_file_afterwards})")
return filepath
class FileServiceModule(Module):
def configure(self, binder: Binder) -> None:
binder.bind(BucketFileUpload, to=InMemoryBucketFileUpload, scope=singleton)
FileServiceModule
that binds BucketFileUpload
to InMemoryBucketFileUpload
.
In our application, when BucketFileUpload
is resolved we will get an instance of InMemoryBucketFileUpload
provided for us by the injector.
We also used inject
decorator from injector
to inject django settings to InMemoryBucketFileUpload
service.
The InMemoryBucketFileUpload
concrete class is a simple class for development. In production time, you meant want to write a better service to saves file to your AWS S3 bucket.
Service Scope
A scope defines the lifespan of a service created. There are three major scope when working with dependency injection in a web framework
singleton
scope
A singleton service is created only once and the same instance is reused for the entire lifetime of the application. This is the default scope when no scope is specified.
from injector import Module, Binder, singleton
class FileServiceModule(Module):
def configure(self, binder: Binder) -> None:
binder.bind(BucketFileUpload, to=InMemoryBucketFileUpload, scope=singleton)
transient
scope
A transient service is created each time it's requested. A new instance is created for each request. This is useful for services that do not maintain state or services that should not be shared across multiple requests.
from injector import Module, Binder, noscope
class FileServiceModule(Module):
def configure(self, binder: Binder) -> None:
binder.bind(BucketFileUpload, to=InMemoryBucketFileUpload, scope=noscope)
scoped
A scoped service is created once per request. A new instance is created for each incoming request and is shared among all components that depend on it within the same request. This is useful for services that maintain request-specific state.
Currently, Ninja extra does not support scoped
scope service.
It's important to choose the appropriate scope when registering services with the dependency injection container.
Singleton
services are suitable for services that maintain application-wide state,
transient
services are suitable for services that do not maintain state, and
scoped
services are suitable for services that maintain request-specific state.
Adding Service to Controllers
Ninja Extra controllers constructor (__init__
) are decorated with inject
function from injector
library.
This makes it possible define parameter in a parameter in the constructor with a type annotation and the annotated type gets injected during object instantiation.
We have created a BucketFileUpload
contract and some concrete implementations, lets add it to a controller.
Lets create a controller.py
with the code below
from ninja import File
from ninja.files import UploadedFile
from ninja_extra import NinjaExtraAPI, api_controller, http_post
from .modules import BucketFileUpload, InMemoryBucketFileUpload
@api_controller('/user_profile')
class UserProfileController:
def __init__(self, upload_service: BucketFileUpload):
self.upload_service = upload_service
@http_post('/upload')
def upload_profile_pic(self, file: UploadedFile = File(...)):
self.upload_service.upload_file_to_s3(file=file)
assert isinstance(self.upload_service, InMemoryBucketFileUpload) # True
return {'message', 'uploaded successfully'}
api = NinjaExtraAPI(title='Injector Test')
api.register_controllers(UserProfileController)
Now, we have defined an BucketFileUpload
service dependence to our UserProfileController
.
We need to register FileServiceModule
to settings to avoid getting UnsatisedRequirement
exception from injector when Ninja extra tries to create the object.
Module Registration
There are different ways of registering injector Modules in a Django app.
- django_injector: if you are using django_inject, it has documentation on how to register a module.
- ninja_extra: you can provide module string path in
INJECTOR_MODULES
inNINJA_EXTRA
field as shown below:
Registering based on Ninja Extra
We register modules to INJECTOR_MODULES
key in Ninja Extra settings in django settings.py
NINJA_EXTRA = {
'INJECTOR_MODULES': [
'myproject.app1.modules.SomeModule',
'myproject.app2.modules.SomeAppModule',
]
}
Let's register FileServiceModule
module to the NinjaExtra
settings,
# settings.py
...
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
NinjaExtra = {
'INJECTOR_MODULES': [
'myproject.modules.FileServiceModule'
]
}
...
That's it. We have completely wired BucketFileUpload
service to UserProfileController
.
Warning
You are not only allowed to override your APIController constructor with parameter that don't have annotations Read more Python Injector
Using service_resolver
The service_resolver
is a utility class that help resolves types registered in the injector
instance. I could be in handle when we need a service resolved outside controllers.
For example:
from ninja_extra import service_resolver
from .service import BucketFileUpload
bucket_service = service_resolver(BucketFileUpload)
bucket_service.upload_file_to_s3('/path/to/file')
We can also resolve more than one service at a time and a tuple result will be returned.
from ninja_extra import service_resolver
from .service import BucketFileUpload
bucket_service, service_a, service_b = service_resolver(BucketFileUpload, AnotherServiceA, AnotherServiceB)
bucket_service.upload_file_to_s3('/path/to/file')
service_a.do_something()
service_b.do_something()