Dependency Injection

At the heart of Expanse is a powerful — yet intuitive — dependency injection mechanism that orchestrates every service that your application might need.

Don't let the name fool you, it's actually a simple concept where dependencies needed by classes or functions are injected into them automatically.

Let's take a look at an example:

from expanse.http.response import Response
from expanse.view.view_factory import ViewFactory

from app.models.article import Article
from app.repositories.article_repository import ArticleRepository


class ArticleController:

    def __init__(self, repository: ArticleRepository) -> None:
        self._repository: ArticleRepository = repository

    def show(self, view: ViewFactory, slug: str) -> Response:
        article: Article = self._repository.find_by_slug(slug)

        return view.make("article.show", {"article": article})

Here, the ArticleController needs to retrieve articles from a data source, most likely a database. To do so, we will inject a service in charge of retrieving articles. This is what the ArticleRepository is for, which in turn is likely to use the DatabaseManager service to retrieve the article information. This is where the dependency injection is powerful: since the repository is injected, you can easily replace it by another implementation, this is particularly useful for testing where you can replace it for a mocked version of the repository.

This is the main concept to understand to leverage most of what Expanse has to offer and to start building organized, powerful applications.

Automatic injection

If your services have no dependencies, only depends on concrete services and not interfaces, or depends on Expanse base services, you can start using and injecting them right away without any additional configuration.

To test this, you can put the following code in your routes/web.py file:

from expanse.routing.router import Router


class MessageGenerator:

    def say_hello(self, name: str) -> str:
        return f"Hello {name}"


def display_message(generator: MessageGenerator) -> str:
    return generator.say_hello("John")


def routes(router: Router) -> None:
    router.get("/say-hello", display_message)

Now, if, in your browser, you go to http://localhost:8000/say-hello, the MessageGenerator class will automatically be resolved without having to configure anything, allowing you to focus on what you are building.

If you were to need another service in your MessageGenerator class, you can type-hint it in the constructor of the class:

from expanse.http.response import Response
from expanse.view.view_factory import ViewFactory


class MessageGenerator:

    def __init__(self, view: ViewFactory) -> None:
        self._view = view

    def say_hello(self, name: str) -> Response:
        return self._view.make("hello", {"name": name})

When resolving and injecting the MessageGenerator class, the ViewFactory will be resolved as well so it can be properly injected. This process is called auto-wiring and is used heavily by Expanse itself.

Most of the classes you will create or interact with when using Expanse are already auto-wired and receive their own dependencies automatically, this is the case, for instance, of controllers, middleware, commands, and more.