Repository: SamWarden/didiator (language: python)

Issue:
Configure pyproject.toml

Add logging for dispatchers
Add logging to CommandDispatcherImpl and QueryDispatcherImpl. Both should have diferrent description for logs.

Alternatively LoggingMiddleware can be added



Discussion / hints:

Fix patch:
diff --git a/LICENSE.rst b/LICENSE.rst
new file mode 100644
index 0000000..19a0607
--- /dev/null
+++ b/LICENSE.rst
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 SamWarden
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..0b30d34
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,149 @@
+========
+Didiator
+========
+
+``didiator`` is an asynchronous library that implements the Mediator pattern and
+uses the `DI <https://www.adriangb.com/di/>`_ library to help you to inject dependencies to called handlers
+
+This library is inspired by the `MediatR <https://github.com/jbogard/MediatR>`_ used in C#
+and follows CQRS principles
+
+Installation
+======
+
+Didiator is available on pypi: https://pypi.org/project/didiator
+
+.. code-block:: bash
+
+    pip install -U didiator[di]
+
+It will install ``didiator`` with its optional DI dependency that is necessary to use ``DiMiddleware`` and ``DiBuilder``
+
+Examples
+========
+
+You can find examples in `this folder <https://github.com/SamWarden/didiator/tree/dev/examples>`_
+
+Create Commands and Queries with handlers for them
+~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: python
+
+    @dataclass
+    class CreateUser(Command[int]):
+        user_id: int
+        username: str
+
+    class CreateUserHandler(CommandHandler[CreateUser, int]):
+        def __init__(self, user_repo: UserRepo) -> None:
+            self._user_repo = user_repo
+
+        async def __call__(self, command: CreateUser) -> int:
+            user = User(id=command.user_id, username=command.username)
+            await self._user_repo.add_user(user)
+            await self._user_repo.commit()
+            return user.id
+
+You can use functions as handlers
+
+.. code-block:: python
+
+    @dataclass
+    class GetUserById(Query[User]):
+        user_id: int
+
+    async def handle_get_user_by_id(query: GetUserById, user_repo: UserRepo) -> User:
+        user = await self._user_repo.get_user_by_id(user)
+        return user
+
+Create DiBuilder
+~~~~~~~~~~~~~~~~
+
+``DiBuilder`` is a facade for Container from DI with caching of `solving <https://www.adriangb.com/di/0.73.0/solving/>`_
+
+``di_scopes`` is a sequence with the order of `scopes <https://www.adriangb.com/di/0.73.0/scopes/>`_
+
+``di_builder.bind(...)`` will `bind <https://www.adriangb.com/di/0.73.0/binds/>`_ ``UserRepoImpl`` type to ``UserRepo`` protocol
+
+.. code-block:: python
+
+    di_scopes = ("request",)
+    di_builder = DiBuilder(Container(), AsyncExecutor(), di_scopes)
+    di_builder.bind(bind_by_type(Dependent(UserRepoImpl, scope="request"), UserRepo))
+
+Create Mediator and register handlers to it
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Create dispatchers with their middlewares and use them to initialize the ``MediatorImpl``
+
+``cls_scope`` is a scope that will be used to bind class Command/Query handlers initialized during request handling
+
+.. code-block:: python
+
+    dispatchers_middlewares = (LoggingMiddleware(), DiMiddleware(di_builder, cls_scope="request"))
+    command_dispatcher = CommandDispatcherImpl(middlewares=dispatchers_middlewares)
+    query_dispatcher = QueryDispatcherImpl(middlewares=dispatchers_middlewares)
+
+    mediator = MediatorImpl(command_dispatcher, query_dispatcher)
+
+    # CreateUserHandler is not initialized during registration
+    mediator.register_command_handler(CreateUser, CreateUserHandler)
+    mediator.register_query_handler(GetUserById, handle_get_user_by_id)
+
+Main usage
+~~~~~~~~~~
+
+Enter the ``"request"`` scope that was registered earlier and create a new Mediator with ``di_state`` bound
+
+Use ``mediator.send(...)`` for commands and ``mediator.query(...)`` for queries
+
+.. code-block:: python
+
+        async with di_builder.enter_scope("request") as di_state:
+            scoped_mediator = mediator.bind(di_state=di_state)
+
+            # It will call CreateUserHandler(...).__call__(...) and inject UserRepoImpl to it
+            user_id = await scoped_mediator.send(CreateUser(1, "Jon"))
+            user = await scoped_mediator.query(GetUserById(user_id))
+            print("User:",  user)
+        # Session of UserRepoImpl will be closed after exiting the "request" scope
+
+⚠️ **Attention: this is a beta version of** ``didiator`` **that depends on** ``DI``, **which is also in beta. Both of them can change their API!**
+
+CQRS
+====
+
+CQRS stands for "`Command Query Responsibility Segregation <https://www.martinfowler.com/bliki/CQRS.html>`_".
+Its idea about splitting the responsibility of commands (writing) and queries (reading) into different models.
+
+``didiator`` have segregated ``.send(command)`` and ``.query(query)`` methods in its ``Mediator`` and
+assumes that you will separate its handlers.
+Use ``CommandMediator`` and ``QueryMediator`` protocols to explicitly define which method you need in ``YourController``
+
+.. code-block:: mermaid
+
+    graph LR;
+        YourController-- Command -->Mediator;
+        YourController-- Query -->Mediator;
+        Mediator-. Command .->CommandDispatcher-.->di1[DiMiddleware]-.->CommandHandler;
+        Mediator-. Query .->QueryDispatcher-.->di2[DiMiddleware]-.->QueryHandler;
+
+``DiMiddleware`` initializes handlers and injects dependencies for them, you can just send a command with the data you need
+
+Why ``didiator``?
+=================
+
+- Easy dependency injection to your business logic
+- Separating dependencies from your controllers. They can just parse external requests and interact with the ``Mediator``
+- CQRS
+- Flexible configuration
+- Middlewares support
+
+Why not?
+========
+
+- You don't need it
+- Maybe too low coupling: navigation becomes more difficult
+- Didiator is in beta now
+- No support for synchronous handlers
+
diff --git a/didiator/__init__.py b/didiator/__init__.py
new file mode 100644
index 0000000..e7875e8
--- /dev/null
+++ b/didiator/__init__.py
@@ -0,0 +1,19 @@
+from .dispatchers import CommandDispatcherImpl, QueryDispatcherImpl
+from .interface.entities import Command, Query
+from .interface.handlers import CommandHandler, QueryHandler
+from .interface.mediator import Mediator
+from .mediator import MediatorImpl
+
+__version__ = '0.1.0'
+
+__all__ = (
+    "__version__",
+    "MediatorImpl",
+    "Mediator",
+    "Command",
+    "CommandHandler",
+    "CommandDispatcherImpl",
+    "Query",
+    "QueryHandler",
+    "QueryDispatcherImpl",
+)
diff --git a/didiator/dispatchers/__init__.py b/didiator/dispatchers/__init__.py
new file mode 100644
index 0000000..0d94012
--- /dev/null
+++ b/didiator/dispatchers/__init__.py
@@ -0,0 +1,9 @@
+from .command import CommandDispatcherImpl
+from .request import RequestDispatcherImpl
+from .query import QueryDispatcherImpl
+
+__all__ = (
+    "CommandDispatcherImpl",
+    "RequestDispatcherImpl",
+    "QueryDispatcherImpl",
+)
diff --git a/didiator/dispatchers/command.py b/didiator/dispatchers/command.py
new file mode 100644
index 0000000..fa2d176
--- /dev/null
+++ b/didiator/dispatchers/command.py
@@ -0,0 +1,21 @@
+from typing import Any, Type, TypeVar
+
+from didiator.interface.entities.command import Command
+from didiator.interface.dispatchers.command import CommandDispatcher
+from didiator.interface.handlers.request import HandlerType
+from didiator.interface.exceptions import CommandHandlerNotFound, HandlerNotFound
+from didiator.dispatchers.request import RequestDispatcherImpl
+
+CRes = TypeVar("CRes")
+C = TypeVar("C", bound=Command[Any])
+
+
+class CommandDispatcherImpl(RequestDispatcherImpl, CommandDispatcher):
+    def register_handler(self, command: Type[C], handler: HandlerType[C, CRes]) -> None:
+        super()._register_handler(command, handler)
+
+    async def send(self, command: Command[CRes], *args: Any, **kwargs: Any) -> CRes:
+        try:
+            return await self._handle(command, *args, **kwargs)
+        except HandlerNotFound:
+            raise CommandHandlerNotFound(f"Command handler for {type(command).__name__} command is not registered", command)
diff --git a/didiator/dispatchers/query.py b/didiator/dispatchers/query.py
new file mode 100644
index 0000000..9e121fa
--- /dev/null
+++ b/didiator/dispatchers/query.py
@@ -0,0 +1,21 @@
+from typing import Any, Type, TypeVar
+
+from didiator.interface.handlers.request import HandlerType
+from didiator.interface.dispatchers.query import QueryDispatcher
+from didiator.interface.entities.query import Query
+from didiator.interface.exceptions import HandlerNotFound, QueryHandlerNotFound
+from didiator.dispatchers.request import RequestDispatcherImpl
+
+QRes = TypeVar("QRes")
+Q = TypeVar("Q", bound=Query[Any])
+
+
+class QueryDispatcherImpl(RequestDispatcherImpl, QueryDispatcher):
+    def register_handler(self, query: Type[Q], handler: HandlerType[Q, QRes]) -> None:
+        super()._register_handler(query, handler)
+
+    async def query(self, query: Query[QRes], *args: Any, **kwargs: Any) -> QRes:
+        try:
+            return await self._handle(query, *args, **kwargs)
+        except HandlerNotFound:
+            raise QueryHandlerNotFound(f"Query handler for {type(query).__name__} query is not registered", query)
diff --git a/didiator/dispatchers/request.py b/didiator/dispatchers/request.py
new file mode 100644
index 0000000..b969370
--- /dev/null
+++ b/didiator/dispatchers/request.py
@@ -0,0 +1,52 @@
+import abc
+import functools
+from typing import Any, Awaitable, Callable, Sequence, Type, TypeVar
+
+from didiator.interface.entities.request import Request
+from didiator.interface.exceptions import HandlerNotFound
+from didiator.interface.handlers import HandlerType
+from didiator.middlewares.base import Middleware
+from didiator.interface.dispatchers.request import Dispatcher, MiddlewareType
+
+RRes = TypeVar("RRes")
+R = TypeVar("R", bound=Request[Any])
+
+DEFAULT_MIDDLEWARES: tuple[MiddlewareType, ...] = (Middleware(),)
+
+
+class RequestDispatcherImpl(Dispatcher, abc.ABC):
+    def __init__(self, middlewares: Sequence[MiddlewareType] = ()) -> None:
+        self._handlers: dict[Type[Request[Any]], HandlerType[Any, Any]] = {}
+        self._middlewares: Sequence[MiddlewareType] = middlewares
+
+    def _register_handler(self, request: Type[R], handler: HandlerType[R, RRes]) -> None:
+        self._handlers[request] = handler
+
+    @property
+    def handlers(self) -> dict[Type[Request[Any]], HandlerType]:
+        return self._handlers
+
+    @property
+    def middlewares(self) -> tuple[MiddlewareType, ...]:
+        return tuple(self._middlewares)
+
+    async def _handle(self, request: Request[RRes], *args: Any, **kwargs: Any) -> RRes:
+        try:
+            handler = self._handlers[type(request)]
+        except KeyError:
+            raise HandlerNotFound(f"Request handler for {type(request).__name__} request is not registered", request)
+
+        # Handler has to be wrapped with at least one middleware to initialize the handler if it is necessary
+        middlewares = self._middlewares if self._middlewares else DEFAULT_MIDDLEWARES
+        wrapped_handler: Callable[..., Awaitable[RRes]] = self._wrap_middleware(middlewares, handler)
+        return await wrapped_handler(request, *args, **kwargs)
+
+    @staticmethod
+    def _wrap_middleware(
+        middlewares: Sequence[MiddlewareType[R, RRes]],
+        handler: HandlerType[R, RRes],
+    ) -> Callable[..., Awaitable[RRes]]:
+        for middleware in reversed(middlewares):
+            handler = functools.partial(middleware, handler)
+
+        return handler
diff --git a/didiator/interface/__init__.py b/didiator/interface/__init__.py
new file mode 100644
index 0000000..c9fed39
--- /dev/null
+++ b/didiator/interface/__init__.py
@@ -0,0 +1,22 @@
+from .dispatchers.command import CommandDispatcher
+from .dispatchers.request import Dispatcher
+from .dispatchers.query import QueryDispatcher
+from .mediator import CommandMediator, Mediator, QueryMediator
+from .entities import Command, Query, Request
+from .handlers import CommandHandler, Handler, QueryHandler
+
+
+__all__ = (
+    "Mediator",
+    "CommandMediator",
+    "QueryMediator",
+    "Dispatcher",
+    "CommandDispatcher",
+    "QueryDispatcher",
+    "Request",
+    "Command",
+    "Query",
+    "Handler",
+    "CommandHandler",
+    "QueryHandler",
+)
diff --git a/didiator/interface/dispatchers/__init__.py b/didiator/interface/dispatchers/__init__.py
new file mode 100644
index 0000000..8f13c4a
--- /dev/null
+++ b/didiator/interface/dispatchers/__init__.py
@@ -0,0 +1,9 @@
+from .command import CommandDispatcher
+from .request import Dispatcher
+from .query import QueryDispatcher
+
+__all__ = (
+    "CommandDispatcher",
+    "Dispatcher",
+    "QueryDispatcher",
+)
diff --git a/didiator/interface/dispatchers/command.py b/didiator/interface/dispatchers/command.py
new file mode 100644
index 0000000..f101183
--- /dev/null
+++ b/didiator/interface/dispatchers/command.py
@@ -0,0 +1,20 @@
+from typing import Any, Protocol, Type, TypeVar
+
+from didiator.interface.entities.command import Command
+from didiator.interface.dispatchers.request import Dispatcher
+from didiator.interface.handlers import HandlerType
+
+C = TypeVar("C", bound=Command[Any])
+CRes = TypeVar("CRes")
+
+
+class CommandDispatcher(Dispatcher, Protocol):
+    @property
+    def handlers(self) -> dict[Type[Command[Any]], HandlerType[Any, Any]]:
+        raise NotImplementedError
+
+    def register_handler(self, command: Type[C], handler: HandlerType[C, CRes]) -> None:
+        raise NotImplementedError
+
+    async def send(self, command: Command[CRes], *args: Any, **kwargs: Any) -> CRes:
+        raise NotImplementedError
diff --git a/didiator/interface/dispatchers/query.py b/didiator/interface/dispatchers/query.py
new file mode 100644
index 0000000..221d629
--- /dev/null
+++ b/didiator/interface/dispatchers/query.py
@@ -0,0 +1,20 @@
+from typing import Any, Protocol, Type, TypeVar
+
+from didiator.interface.dispatchers.request import Dispatcher
+from didiator.interface.entities.query import Query
+from didiator.interface.handlers import HandlerType
+
+Q = TypeVar("Q", bound=Query[Any])
+QRes = TypeVar("QRes")
+
+
+class QueryDispatcher(Dispatcher, Protocol):
+    @property
+    def handlers(self) -> dict[Type[Query[Any]], HandlerType[Any, Any]]:
+        raise NotImplementedError
+
+    def register_handler(self, query: Type[Q], handler: HandlerType[Q, QRes]) -> None:
+        raise NotImplementedError
+
+    async def query(self, query: Query[QRes], *args: Any, **kwargs: Any) -> QRes:
+        raise NotImplementedError
diff --git a/didiator/interface/dispatchers/request.py b/didiator/interface/dispatchers/request.py
new file mode 100644
index 0000000..77934ce
--- /dev/null
+++ b/didiator/interface/dispatchers/request.py
@@ -0,0 +1,19 @@
+from typing import Any, Awaitable, Callable, Protocol, Type, TypeVar
+
+from didiator.interface.entities.request import Request
+from didiator.interface.handlers import HandlerType
+
+R = TypeVar("R", bound=Request[Any])
+RRes = TypeVar("RRes")
+
+MiddlewareType = Callable[[HandlerType[R, RRes], R], Awaitable[RRes]]
+
+
+class Dispatcher(Protocol):
+    @property
+    def handlers(self) -> dict[Type[Request[Any]], HandlerType[Any, Any]]:
+        raise NotImplementedError
+
+    @property
+    def middlewares(self) -> tuple[MiddlewareType[Any, Any], ...]:
+        raise NotImplementedError
diff --git a/didiator/interface/entities/__init__.py b/didiator/interface/entities/__init__.py
new file mode 100644
index 0000000..45302bc
--- /dev/null
+++ b/didiator/interface/entities/__init__.py
@@ -0,0 +1,9 @@
+from .command import Command
+from .request import Request
+from .query import Query
+
+__all__ = (
+    "Command",
+    "Request",
+    "Query",
+)
diff --git a/didiator/interface/entities/command.py b/didiator/interface/entities/command.py
new file mode 100644
index 0000000..fae9714
--- /dev/null
+++ b/didiator/interface/entities/command.py
@@ -0,0 +1,10 @@
+import abc
+from typing import Generic, TypeVar
+
+from didiator.interface.entities.request import Request
+
+CRes = TypeVar("CRes")
+
+
+class Command(Request[CRes], abc.ABC, Generic[CRes]):
+    pass
diff --git a/didiator/interface/entities/query.py b/didiator/interface/entities/query.py
new file mode 100644
index 0000000..469b6db
--- /dev/null
+++ b/didiator/interface/entities/query.py
@@ -0,0 +1,10 @@
+import abc
+from typing import Generic, TypeVar
+
+from didiator.interface.entities.request import Request
+
+QRes = TypeVar("QRes")
+
+
+class Query(Request[QRes], abc.ABC, Generic[QRes]):
+    pass
diff --git a/didiator/interface/entities/request.py b/didiator/interface/entities/request.py
new file mode 100644
index 0000000..ff33f3c
--- /dev/null
+++ b/didiator/interface/entities/request.py
@@ -0,0 +1,8 @@
+import abc
+from typing import Generic, TypeVar
+
+RRes = TypeVar("RRes")
+
+
+class Request(abc.ABC, Generic[RRes]):
+    pass
diff --git a/didiator/interface/exceptions.py b/didiator/interface/exceptions.py
new file mode 100644
index 0000000..bb0da12
--- /dev/null
+++ b/didiator/interface/exceptions.py
@@ -0,0 +1,21 @@
+from didiator.interface import Request
+
+
+class MediatorError(Exception):
+    pass
+
+
+class HandlerNotFound(MediatorError, TypeError):
+    request: Request
+
+    def __init__(self, text: str, request: Request):
+        super().__init__(text)
+        self.request = request
+
+
+class CommandHandlerNotFound(HandlerNotFound):
+    pass
+
+
+class QueryHandlerNotFound(HandlerNotFound):
+    pass
diff --git a/didiator/interface/handlers/__init__.py b/didiator/interface/handlers/__init__.py
new file mode 100644
index 0000000..05f1290
--- /dev/null
+++ b/didiator/interface/handlers/__init__.py
@@ -0,0 +1,10 @@
+from .command import CommandHandler
+from .request import Handler, HandlerType
+from .query import QueryHandler
+
+__all__ = (
+    "CommandHandler",
+    "Handler",
+    "QueryHandler",
+    "HandlerType",
+)
diff --git a/didiator/interface/handlers/command.py b/didiator/interface/handlers/command.py
new file mode 100644
index 0000000..0648ed1
--- /dev/null
+++ b/didiator/interface/handlers/command.py
@@ -0,0 +1,15 @@
+import abc
+from typing import Any, Generic, TypeVar
+
+from didiator.interface.entities.command import Command
+
+from .request import Handler
+
+CRes = TypeVar("CRes")
+C = TypeVar("C", bound=Command[Any])
+
+
+class CommandHandler(Handler[C, CRes], abc.ABC, Generic[C, CRes]):
+    @abc.abstractmethod
+    async def __call__(self, command: C) -> CRes:
+        ...
diff --git a/didiator/interface/handlers/query.py b/didiator/interface/handlers/query.py
new file mode 100644
index 0000000..a8a0921
--- /dev/null
+++ b/didiator/interface/handlers/query.py
@@ -0,0 +1,15 @@
+import abc
+from typing import Any, Generic, TypeVar
+
+from didiator.interface.entities.query import Query
+
+from .request import Handler
+
+QRes = TypeVar("QRes")
+Q = TypeVar("Q", bound=Query[Any])
+
+
+class QueryHandler(Handler[Q, QRes], abc.ABC, Generic[Q, QRes]):
+    @abc.abstractmethod
+    async def __call__(self, query: Q) -> QRes:
+        ...
diff --git a/didiator/interface/handlers/request.py b/didiator/interface/handlers/request.py
new file mode 100644
index 0000000..08a3db3
--- /dev/null
+++ b/didiator/interface/handlers/request.py
@@ -0,0 +1,16 @@
+import abc
+from typing import Any, Awaitable, Callable, Generic, Type, TypeVar
+
+from didiator.interface.entities.request import Request
+
+RRes = TypeVar("RRes")
+R = TypeVar("R", bound=Request[Any])
+
+
+class Handler(abc.ABC, Generic[R, RRes]):
+    @abc.abstractmethod
+    async def __call__(self, request: R) -> RRes:
+        ...
+
+
+HandlerType = Callable[[R], Awaitable[RRes]] | Type[Handler[R, RRes]]
diff --git a/didiator/interface/mediator.py b/didiator/interface/mediator.py
new file mode 100644
index 0000000..164216c
--- /dev/null
+++ b/didiator/interface/mediator.py
@@ -0,0 +1,43 @@
+from typing import Any, Protocol, Type, TypeVar
+
+from didiator.interface.entities.command import Command
+from didiator.interface.entities.query import Query
+from didiator.interface.handlers import HandlerType
+
+Self = TypeVar("Self", bound="BaseMediator")
+C = TypeVar("C", bound=Command)
+CRes = TypeVar("CRes")
+Q = TypeVar("Q", bound=Query)
+QRes = TypeVar("QRes")
+
+
+class BaseMediator(Protocol):
+    @property
+    def extra_data(self) -> dict[str, Any]:
+        raise NotImplementedError
+
+    def bind(self: Self, **extra_data: Any) -> Self:
+        raise NotImplementedError
+
+    def unbind(self: Self, *keys: str) -> Self:
+        raise NotImplementedError
+
+
+class CommandMediator(BaseMediator, Protocol):
+    async def send(self, command: Command[CRes], *args: Any, **kwargs: Any) -> CRes:
+        raise NotImplementedError
+
+    def register_command_handler(self, command: Type[C], handler: HandlerType[C, CRes]) -> None:
+        raise NotImplementedError
+
+
+class QueryMediator(BaseMediator, Protocol):
+    async def query(self, query: Query[QRes], *args: Any, **kwargs: Any) -> QRes:
+        raise NotImplementedError
+
+    def register_query_handler(self, query: Type[Q], handler: HandlerType[Q, QRes]) -> None:
+        raise NotImplementedError
+
+
+class Mediator(CommandMediator, QueryMediator, BaseMediator, Protocol):
+    pass
diff --git a/didiator/mediator.py b/didiator/mediator.py
new file mode 100644
index 0000000..8482560
--- /dev/null
+++ b/didiator/mediator.py
@@ -0,0 +1,55 @@
+from typing import Any, Type, TypeVar
+
+from didiator.dispatchers.command import CommandDispatcherImpl
+from didiator.dispatchers.query import QueryDispatcherImpl
+from didiator.interface.entities.command import Command
+from didiator.interface.dispatchers.command import CommandDispatcher
+from didiator.interface.dispatchers.query import QueryDispatcher
+from didiator.interface.entities.query import Query
+from didiator.interface.handlers import HandlerType
+from didiator.interface.mediator import Mediator
+
+C = TypeVar("C", bound=Command)
+CRes = TypeVar("CRes")
+Q = TypeVar("Q", bound=Query)
+QRes = TypeVar("QRes")
+
+
+class MediatorImpl(Mediator):
+    def __init__(
+        self,
+        command_dispatcher: CommandDispatcher | None = None,
+        query_dispatcher: QueryDispatcher | None = None,
+        *, extra_data: dict[str, Any] | None = None,
+    ):
+        if command_dispatcher is None:
+            command_dispatcher = CommandDispatcherImpl()
+        if query_dispatcher is None:
+            query_dispatcher = QueryDispatcherImpl()
+
+        self._command_dispatcher = command_dispatcher
+        self._query_dispatcher = query_dispatcher
+        self._extra_data = extra_data if extra_data is not None else {}
+
+    @property
+    def extra_data(self) -> dict[str, Any]:
+        return self._extra_data
+
+    def bind(self, **extra_data: Any) -> "MediatorImpl":
+        return MediatorImpl(self._command_dispatcher, self._query_dispatcher, extra_data=self._extra_data | extra_data)
+
+    def unbind(self, *keys: str) -> "MediatorImpl":
+        extra_data = {key: val for key, val in self._extra_data.items() if key not in keys}
+        return MediatorImpl(self._command_dispatcher, self._query_dispatcher, extra_data=extra_data)
+
+    def register_command_handler(self, command: Type[C], handler: HandlerType[C, CRes]) -> None:
+        self._command_dispatcher.register_handler(command, handler)
+
+    def register_query_handler(self, query: Type[Q], handler: HandlerType[Q, QRes]) -> None:
+        self._query_dispatcher.register_handler(query, handler)
+
+    async def send(self, command: Command[CRes], *args: Any, **kwargs: Any) -> CRes:
+        return await self._command_dispatcher.send(command, *args, **kwargs, **self._extra_data)
+
+    async def query(self, query: Query[QRes], *args: Any, **kwargs: Any) -> QRes:
+        return await self._query_dispatcher.query(query, *args, **kwargs, **self._extra_data)
diff --git a/didiator/middlewares/__init__.py b/didiator/middlewares/__init__.py
new file mode 100644
index 0000000..cb24525
--- /dev/null
+++ b/didiator/middlewares/__init__.py
@@ -0,0 +1,5 @@
+from .base import Middleware
+
+__all__ = (
+    "Middleware",
+)
diff --git a/didiator/middlewares/base.py b/didiator/middlewares/base.py
new file mode 100644
index 0000000..caec1b6
--- /dev/null
+++ b/didiator/middlewares/base.py
@@ -0,0 +1,30 @@
+from typing import Any, TypeVar
+
+from didiator.interface.entities.request import Request
+from didiator.interface.handlers import HandlerType
+
+RRes = TypeVar("RRes")
+R = TypeVar("R", bound=Request[Any])
+
+
+class Middleware:
+    async def __call__(
+        self,
+        handler: HandlerType[R, RRes],
+        request: R,
+        *args: Any,
+        **kwargs: Any,
+    ) -> RRes:
+        return await self._call(handler, request, *args, **kwargs)
+
+    async def _call(
+        self,
+        handler: HandlerType[R, RRes],
+        request: R,
+        *args: Any,
+        **kwargs: Any,
+    ) -> RRes:
+        if isinstance(handler, type):
+            handler = handler()
+
+        return await handler(request, *args, **kwargs)
diff --git a/didiator/middlewares/di.py b/didiator/middlewares/di.py
new file mode 100644
index 0000000..ccf54d4
--- /dev/null
+++ b/didiator/middlewares/di.py
@@ -0,0 +1,72 @@
+from dataclasses import dataclass
+from typing import Any, TypeVar
+
+from di.api.scopes import Scope
+from di.container import ContainerState
+
+from didiator.interface.entities.request import Request
+from didiator.interface.handlers import HandlerType
+from didiator.middlewares import Middleware
+from didiator.utils.di_builder import DiBuilder
+
+RRes = TypeVar("RRes")
+R = TypeVar("R", bound=Request)
+DEFAULT_STATE_KEY = "di_state"
+
+
+@dataclass(frozen=True)
+class MediatorDiScope:
+    cls_handler: Scope
+    func_handler: Scope
+
+
+class DiMiddleware(Middleware):
+    def __init__(
+        self, di_builder: DiBuilder,
+        *, cls_scope: Scope = ..., func_scope: Scope = "mediator_request", state_key: str = DEFAULT_STATE_KEY,
+    ) -> None:
+        self._di_builder = di_builder
+
+        mediator_scope = MediatorDiScope(func_scope if cls_scope is ... else cls_scope, func_scope)
+        self._mediator_scope = mediator_scope
+        self._di_builder.di_scopes = self._get_di_scopes(tuple(self._di_builder.di_scopes), mediator_scope)
+
+        self._state_key = state_key
+
+    def _get_di_scopes(self, di_scopes: tuple[Scope, ...], mediator_scope: MediatorDiScope) -> tuple[Scope, ...]:
+        if mediator_scope.cls_handler not in di_scopes:
+            di_scopes += (mediator_scope.cls_handler,)
+        if mediator_scope.func_handler not in di_scopes:
+            di_scopes += (mediator_scope.func_handler,)
+        return di_scopes
+
+    async def _call(
+        self,
+        handler: HandlerType[R, RRes],
+        request: R,
+        *args: Any,
+        **kwargs: Any,
+    ) -> RRes:
+        di_state: ContainerState | None = kwargs.pop(self._state_key, None)
+
+        if isinstance(handler, type):
+            return await self._call_class_handler(handler, request, di_state, *args, **kwargs)
+        return await self._call_func_handler(handler, request, di_state)
+
+    async def _call_class_handler(
+        self, handler: HandlerType[R, RRes], request: R, di_state: ContainerState | None,
+        *args: Any, **kwargs: Any,
+    ) -> RRes:
+        async with self._di_builder.enter_scope(self._mediator_scope.func_handler, di_state) as scoped_di_state:
+            handler = await self._di_builder.execute(
+                handler, self._mediator_scope.cls_handler, state=scoped_di_state, values={type(request): request},
+            )
+            return await handler(request, *args, **kwargs)
+
+    async def _call_func_handler(
+        self, handler: HandlerType[R, RRes], request: R, di_state: ContainerState | None,
+    ) -> RRes:
+        async with self._di_builder.enter_scope(self._mediator_scope.func_handler, di_state) as scoped_di_state:
+            return await self._di_builder.execute(
+                handler, self._mediator_scope.func_handler, state=scoped_di_state, values={type(request): request},
+            )
diff --git a/didiator/middlewares/logging.py b/didiator/middlewares/logging.py
new file mode 100644
index 0000000..4d38347
--- /dev/null
+++ b/didiator/middlewares/logging.py
@@ -0,0 +1,48 @@
+from typing import Any, Protocol, TypeVar
+import logging
+
+from didiator import Command, Query
+from didiator.interface.entities.request import Request
+from didiator.interface.handlers import HandlerType
+from didiator.middlewares import Middleware
+
+RRes = TypeVar("RRes")
+R = TypeVar("R", bound=Request)
+
+
+class Logger(Protocol):
+    def log(self, level: int, msg: str, *, extra: dict[str, Any] | None = None) -> None:
+        ...
+
+
+class LoggingMiddleware(Middleware):
+    def __init__(self, logger: Logger | str = __name__, level: int | str = logging.DEBUG):
+        if isinstance(logger, str):
+            logger = logging.getLogger(logger)
+
+        self._logger: Logger = logger
+        self._level: int = logging.getLevelName(level) if isinstance(level, str) else level
+
+    async def __call__(
+        self,
+        handler: HandlerType[R, RRes],
+        request: R,
+        *args: Any,
+        **kwargs: Any,
+    ) -> RRes:
+        if isinstance(request, Command):
+            self._logger.log(self._level, f"Send {type(request).__name__} command", extra={"command": request})
+        elif isinstance(request, Query):
+            self._logger.log(self._level, f"Make {type(request).__name__} query", extra={"query": request})
+        else:
+            self._logger.log(self._level, f"Execute {type(request).__name__} request", extra={"request": request})
+
+        res = await self._call(handler, request, *args, **kwargs)
+        if isinstance(request, Command):
+            self._logger.log(self._level, f"Command {type(request).__name__} sent. Result: {res}", extra={"result": res})
+        elif isinstance(request, Query):
+            self._logger.log(self._level, f"Query {type(request).__name__} made. Result: {res}", extra={"result": res})
+        else:
+            self._logger.log(self._level, f"Request {type(request).__name__} executed. Result: {res}", extra={"result": res})
+
+        return res
diff --git a/didiator/utils/__init__.py b/didiator/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/didiator/utils/di_builder.py b/didiator/utils/di_builder.py
new file mode 100644
index 0000000..602e3a4
--- /dev/null
+++ b/didiator/utils/di_builder.py
@@ -0,0 +1,46 @@
+from collections.abc import Sequence
+from typing import Any, ContextManager, Mapping, TypeVar
+
+from di._utils.types import FusedContextManager
+from di.api.executor import SupportsAsyncExecutor
+from di.api.providers import DependencyProvider, DependencyProviderType
+from di.api.scopes import Scope
+from di.api.solved import SolvedDependent
+from di.container import BindHook, Container, ContainerState
+from di.dependent import Dependent
+
+T = TypeVar("T")
+
+
+class DiBuilder:
+    def __init__(
+        self, di_container: Container, di_executor: SupportsAsyncExecutor, di_scopes: Sequence[Scope] = (),
+    ) -> None:
+        self._di_container = di_container
+        self._di_executor = di_executor
+        self.di_scopes = di_scopes
+
+        self._solved_dependencies: dict[Scope, dict[DependencyProviderType[Any], SolvedDependent[Any]]] = {}
+
+    def bind(self, hook: BindHook) -> ContextManager[None]:
+        return self._di_container.bind(hook)
+
+    def enter_scope(self, scope: Scope, state: ContainerState | None = None) -> FusedContextManager[ContainerState]:
+        return self._di_container.enter_scope(scope, state)
+
+    async def execute(
+        self, call: DependencyProviderType[T], scope: Scope,
+        *, state: ContainerState | None = None, values: Mapping[DependencyProvider, Any] | None = None,
+    ) -> T:
+        solved_dependency = self.get_solved(call, scope)
+        return await self._di_container.execute_async(
+            solved_dependency, self._di_executor, state=state, values=values,
+        )
+
+    def get_solved(self, call: DependencyProviderType[T], scope: Scope) -> SolvedDependent[T]:
+        try:
+            solved_dependency = self._solved_dependencies.setdefault(scope, {})[call]
+        except KeyError:
+            solved_dependency = self._di_container.solve(Dependent(call, scope=scope), scopes=self.di_scopes)
+            self._solved_dependencies[scope][call] = solved_dependency
+        return solved_dependency
diff --git a/examples/__init__.py b/examples/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/examples/aiogram_with_sqlalchemy.py b/examples/aiogram_with_sqlalchemy.py
new file mode 100644
index 0000000..ce278a3
--- /dev/null
+++ b/examples/aiogram_with_sqlalchemy.py
@@ -0,0 +1,256 @@
+import asyncio
+import os
+from collections.abc import AsyncGenerator
+from dataclasses import dataclass
+from typing import Any, Awaitable, Callable, Protocol
+import logging
+
+import aiogram
+import aiogram.filters
+import aiogram.types as tg
+from di.container import bind_by_type, Container, ContainerState
+from di.dependent import Dependent
+from di.executors import AsyncExecutor
+from sqlalchemy.exc import IntegrityError
+from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine
+from sqlalchemy.orm import declarative_base, Mapped, mapped_column, sessionmaker
+
+from didiator import Command, CommandHandler, Mediator, Query, QueryDispatcherImpl, QueryHandler
+from didiator.dispatchers.command import CommandDispatcherImpl
+from didiator.mediator import MediatorImpl
+from didiator.middlewares.di import DiMiddleware
+from didiator.middlewares.logging import LoggingMiddleware
+from didiator.utils.di_builder import DiBuilder
+
+logger = logging.getLogger(__name__)
+
+# This is an example of usage didiator with DI, aiogram, and SQLAlchemy.
+# To run this script, install them by running this command:
+# pip install -U --pre aiogram SQLAlchemy DI
+# and set the value of TG_BOT_TOKEN to your token
+
+TG_BOT_TOKEN = os.getenv("TG_BOT_TOKEN", "")
+
+# SQLAlchemy declaration
+BaseModel = declarative_base()
+
+
+# User entity
+@dataclass
+class User:
+    id: int
+    username: str
+
+
+class UserRepo(Protocol):
+    def add_user(self, user: User) -> None:
+        ...
+
+    async def get_user_by_id(self, user_id: int) -> User:
+        ...
+
+    async def commit(self) -> None:
+        ...
+
+    async def rollback(self) -> None:
+        ...
+
+
+# Create user command and its handler
+@dataclass(frozen=True)
+class CreateUser(Command[int]):
+    user_id: int
+    username: str
+
+
+class UserAlreadyExists(RuntimeError):
+    pass
+
+
+class CreateUserHandler(CommandHandler[CreateUser, int]):
+    def __init__(self, user_repo: UserRepo):
+        self._user_repo = user_repo
+
+    async def __call__(self, command: CreateUser) -> int:
+        user = User(id=command.user_id, username=command.username)
+        self._user_repo.add_user(user)
+        try:
+            await self._user_repo.commit()
+        except IntegrityError:
+            await self._user_repo.rollback()
+            raise UserAlreadyExists
+
+        return user.id
+
+
+# Get user query and its handler
+@dataclass(frozen=True)
+class GetUserById(Query[User]):
+    user_id: int
+
+
+class GetUserByIdHandler(QueryHandler[GetUserById, User]):
+    def __init__(self, user_repo: UserRepo):
+        self._user_repo = user_repo
+
+    async def __call__(self, query: GetUserById) -> User:
+        user = await self._user_repo.get_user_by_id(query.user_id)
+        return user
+
+
+class UserModel(BaseModel):
+    __tablename__ = "users"
+
+    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=False)
+    username: Mapped[str]
+
+
+class UserRepoImpl(UserRepo):
+    def __init__(self, session: AsyncSession):
+        self._session = session
+
+    def add_user(self, user: User) -> None:
+        self._session.add(UserModel(id=user.id, username=user.username))
+
+    async def get_user_by_id(self, user_id: int) -> User:
+        user_model = await self._session.get(UserModel, user_id)
+        return User(user_model.id, user_model.username)
+
+    async def commit(self) -> None:
+        await self._session.commit()
+
+    async def rollback(self) -> None:
+        await self._session.rollback()
+
+
+@dataclass(frozen=True)
+class Config:
+    db_uri: str
+    bot_token: str
+
+
+def build_config() -> Config:
+    return Config(
+        db_uri="sqlite+aiosqlite://",
+        bot_token=TG_BOT_TOKEN,
+    )
+
+
+async def build_sa_engine(config: Config) -> AsyncGenerator[AsyncEngine, None]:
+    engine = create_async_engine(config.db_uri, future=True)
+    async with engine.begin() as conn:
+        await conn.run_sync(BaseModel.metadata.create_all)
+
+    yield engine
+
+    await engine.dispose()
+
+
+def build_sa_pool(engine: AsyncEngine) -> sessionmaker:
+    return sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False, autoflush=False)
+
+
+async def build_sa_session(pool: sessionmaker) -> AsyncGenerator[AsyncSession, None]:
+    async with pool() as session:
+        yield session
+
+
+def build_repo(session: AsyncSession) -> UserRepoImpl:
+    return UserRepoImpl(session)
+
+
+def build_tg_bot(config: Config) -> aiogram.Bot:
+    return aiogram.Bot(config.bot_token)
+
+
+def build_tg_dispatcher() -> aiogram.Dispatcher:
+    return aiogram.Dispatcher()
+
+
+def build_mediator(di_builder: DiBuilder) -> Mediator:
+    dispatchers_middlewares = (LoggingMiddleware(level=logging.INFO), DiMiddleware(di_builder, cls_scope="tg_update"))
+    command_dispatcher = CommandDispatcherImpl(middlewares=dispatchers_middlewares)
+    query_dispatcher = QueryDispatcherImpl(middlewares=dispatchers_middlewares)
+
+    mediator = MediatorImpl(command_dispatcher, query_dispatcher)
+    mediator.register_command_handler(CreateUser, CreateUserHandler)
+    mediator.register_query_handler(GetUserById, GetUserByIdHandler)
+
+    return mediator
+
+
+def setup_di_builder() -> DiBuilder:
+    di_container = Container()
+    di_executor = AsyncExecutor()
+    di_scopes = ("app", "tg_update",)
+    di_builder = DiBuilder(di_container, di_executor, di_scopes=di_scopes)
+
+    di_builder.bind(bind_by_type(Dependent(lambda *args: di_builder, scope="app"), DiBuilder))
+    di_builder.bind(bind_by_type(Dependent(build_config, scope="app"), Config))
+    di_builder.bind(bind_by_type(Dependent(build_mediator, scope="app"), Mediator))
+    di_builder.bind(bind_by_type(Dependent(build_tg_bot, scope="app"), aiogram.Bot))
+    di_builder.bind(bind_by_type(Dependent(build_sa_engine, scope="app"), AsyncEngine))
+    di_builder.bind(bind_by_type(Dependent(build_sa_pool, scope="app"), sessionmaker))
+    di_builder.bind(bind_by_type(Dependent(build_sa_session, scope="tg_update"), AsyncSession))
+    di_builder.bind(bind_by_type(Dependent(build_repo, scope="tg_update"), UserRepo))
+    return di_builder
+
+
+class MediatorMiddleware(aiogram.BaseMiddleware):
+    def __init__(
+        self, mediator: Mediator, di_builder: DiBuilder, di_state: ContainerState | None = None,
+    ):
+        self._mediator = mediator
+        self._di_builder = di_builder
+        self._di_state = di_state
+
+    async def __call__(
+        self,
+        handler: Callable[[tg.TelegramObject, dict[str, Any]], Awaitable[Any]],
+        event: tg.TelegramObject,
+        data: dict[str, Any],
+    ) -> None:
+        async with self._di_builder.enter_scope("tg_update", self._di_state) as di_state:
+            mediator = self._mediator.bind(di_state=di_state)
+            data["mediator"] = mediator
+            result = await handler(event, data)
+            del data["mediator"]
+        return result
+
+
+async def echo_handler(message: tg.Message, mediator: Mediator) -> None:
+    logger.info(f"Message received: message_id={message.message_id}, text={message.text}")
+    try:
+        # It will call CreateUserHandler(UserRepoImpl()).__call__(command)
+        # UserRepoImpl() created and injected automatically
+        await mediator.send(CreateUser(message.from_user.id, message.from_user.username))
+    except UserAlreadyExists:
+        logger.info("User already exists")
+
+    # It will call GetUserByIdHandler(user_repo).__call__(query)
+    # UserRepoImpl created earlier will be reused in this scope
+    user = await mediator.query(GetUserById(message.from_user.id))
+    await message.answer(f"Hello, {user=}")
+    logger.info("Message sent")
+
+
+async def main() -> None:
+    logging.basicConfig(
+        level=logging.INFO,
+        format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
+    )
+    di_builder = setup_di_builder()
+
+    async with di_builder.enter_scope("app") as di_state:
+        mediator = await di_builder.execute(Mediator, "app", state=di_state)
+
+        dp = await di_builder.execute(aiogram.Dispatcher, "app", state=di_state)
+        dp.update.middleware(MediatorMiddleware(mediator, di_builder, di_state))
+        dp.message.register(echo_handler)
+
+        bot = await di_builder.execute(aiogram.Bot, "app", state=di_state)
+        await dp.start_polling(bot)
+
+
+if __name__ == "__main__":
+    asyncio.run(main())
diff --git a/examples/simple.py b/examples/simple.py
new file mode 100644
index 0000000..b15d5ab
--- /dev/null
+++ b/examples/simple.py
@@ -0,0 +1,128 @@
+import asyncio
+from dataclasses import dataclass
+from typing import Protocol
+import logging
+
+from di.container import bind_by_type, Container
+from di.dependent import Dependent
+from di.executors import AsyncExecutor
+
+from didiator import Command, CommandHandler, Mediator, Query, QueryDispatcherImpl, QueryHandler
+from didiator.dispatchers.command import CommandDispatcherImpl
+from didiator.mediator import MediatorImpl
+from didiator.middlewares.di import DiMiddleware
+from didiator.middlewares.logging import LoggingMiddleware
+from didiator.utils.di_builder import DiBuilder
+
+logger = logging.getLogger(__name__)
+
+
+# User entity
+@dataclass
+class User:
+    id: int
+    username: str
+
+
+class UserRepo(Protocol):
+    async def add_user(self, user: User) -> None:
+        ...
+
+    async def get_user_by_id(self, user_id: int) -> User:
+        ...
+
+    async def commit(self) -> None:
+        ...
+
+
+# Create user command and its handler
+@dataclass(frozen=True)
+class CreateUser(Command[int]):
+    user_id: int
+    username: str
+
+
+class CreateUserHandler(CommandHandler[CreateUser, int]):
+    def __init__(self, user_repo: UserRepo):
+        self._user_repo = user_repo
+
+    async def __call__(self, command: CreateUser) -> int:
+        user = User(id=command.user_id, username=command.username)
+        await self._user_repo.add_user(user)
+        await self._user_repo.commit()
+        return user.id
+
+
+# Get user query and its handler
+@dataclass(frozen=True)
+class GetUserById(Query[User]):
+    user_id: int
+
+
+async def handle_get_user_by_id(query: GetUserById, user_repo: UserRepo) -> User:
+    user = await user_repo.get_user_by_id(query.user_id)
+    return user
+
+
+class UserRepoImpl(UserRepo):
+    def __init__(self):
+        self._db_mock: dict[int, User] = {}
+
+    async def add_user(self, user: User) -> None:
+        self._db_mock[user.id] = user
+
+    async def get_user_by_id(self, user_id: int) -> User:
+        if user_id not in self._db_mock:
+            raise ValueError("User with given id doesn't exist")
+        return self._db_mock[user_id]
+
+    async def commit(self) -> None:
+        ...
+
+
+def build_mediator(di_builder: DiBuilder) -> Mediator:
+    dispatchers_middlewares = (LoggingMiddleware(level=logging.INFO), DiMiddleware(di_builder, cls_scope="request"))
+    command_dispatcher = CommandDispatcherImpl(middlewares=dispatchers_middlewares)
+    query_dispatcher = QueryDispatcherImpl(middlewares=dispatchers_middlewares)
+
+    mediator = MediatorImpl(command_dispatcher, query_dispatcher)
+    mediator.register_command_handler(CreateUser, CreateUserHandler)
+    mediator.register_query_handler(GetUserById, handle_get_user_by_id)
+    return mediator
+
+
+def setup_di_builder() -> DiBuilder:
+    di_scopes = ("app", "request",)
+    di_builder = DiBuilder(Container(), AsyncExecutor(), di_scopes=di_scopes)
+    di_builder.bind(bind_by_type(Dependent(lambda *args: di_builder, scope="app"), DiBuilder))
+    di_builder.bind(bind_by_type(Dependent(build_mediator, scope="app"), Mediator))
+    di_builder.bind(bind_by_type(Dependent(UserRepoImpl, scope="request"), UserRepo))
+    return di_builder
+
+
+async def main() -> None:
+    logging.basicConfig(
+        level=logging.INFO,
+        format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
+    )
+    di_builder = setup_di_builder()
+
+    async with di_builder.enter_scope("app") as di_state:
+        mediator = await di_builder.execute(Mediator, "app", state=di_state)
+
+        async with di_builder.enter_scope("request", di_state) as di_state:
+            scoped_mediator = mediator.bind(di_state=di_state)
+
+            # It will call CreateUserHandler(UserRepoImpl()).__call__(command)
+            # UserRepoImpl() created and injected automatically
+            user_id = await scoped_mediator.send(CreateUser(1, "Jon"))
+            logger.info(f"Created a user with id: {user_id}")
+
+            # It will call get_user_by_id(query, user_repo)
+            # UserRepoImpl created earlier will be reused in this scope
+            user = await scoped_mediator.query(GetUserById(user_id))
+            logger.info(f"User: {user}")
+
+
+if __name__ == "__main__":
+    asyncio.run(main())
diff --git a/mypy.ini b/mypy.ini
new file mode 100644
index 0000000..57f383b
--- /dev/null
+++ b/mypy.ini
@@ -0,0 +1,12 @@
+[mypy]
+strict = True
+pretty = True
+python_version = 3.10
+disallow_any_explicit = False
+disallow_any_generics = False
+disallow_subclassing_any = True
+disallow_untyped_defs = True
+check_untyped_defs = True
+warn_unused_ignores = True
+warn_return_any = True
+warn_unreachable = True
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..6255aac
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,341 @@
+[[package]]
+name = "anyio"
+version = "3.6.2"
+description = "High level compatibility layer for multiple asynchronous event loop implementations"
+category = "main"
+optional = true
+python-versions = ">=3.6.2"
+
+[package.dependencies]
+idna = ">=2.8"
+sniffio = ">=1.1"
+
+[package.extras]
+doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"]
+test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"]
+trio = ["trio (>=0.16,<0.22)"]
+
+[[package]]
+name = "attrs"
+version = "22.1.0"
+description = "Classes Without Boilerplate"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[package.extras]
+tests_no_zope = ["cloudpickle", "pytest-mypy-plugins", "mypy (>=0.900,!=0.940)", "pytest (>=4.3.0)", "pympler", "hypothesis", "coverage[toml] (>=5.0.2)"]
+tests = ["cloudpickle", "zope.interface", "pytest-mypy-plugins", "mypy (>=0.900,!=0.940)", "pytest (>=4.3.0)", "pympler", "hypothesis", "coverage[toml] (>=5.0.2)"]
+docs = ["sphinx-notfound-page", "zope.interface", "sphinx", "furo"]
+dev = ["cloudpickle", "pre-commit", "sphinx-notfound-page", "sphinx", "furo", "zope.interface", "pytest-mypy-plugins", "mypy (>=0.900,!=0.940)", "pytest (>=4.3.0)", "pympler", "hypothesis", "coverage[toml] (>=5.0.2)"]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+category = "dev"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+
+[[package]]
+name = "di"
+version = "0.73.0"
+description = "Dependency injection toolkit"
+category = "main"
+optional = true
+python-versions = ">=3.7,<4"
+
+[package.dependencies]
+anyio = {version = ">=3.5.0", optional = true, markers = "extra == \"anyio\""}
+graphlib2 = ">=0.4.1,<0.5.0"
+
+[package.extras]
+anyio = ["anyio (>=3.5.0)"]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.0.1"
+description = "Backport of PEP 654 (exception groups)"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+test = ["pytest (>=6)"]
+
+[[package]]
+name = "graphlib2"
+version = "0.4.7"
+description = "Rust port of the Python stdlib graphlib modules"
+category = "main"
+optional = true
+python-versions = ">=3.7"
+
+[[package]]
+name = "idna"
+version = "3.4"
+description = "Internationalized Domain Names in Applications (IDNA)"
+category = "main"
+optional = true
+python-versions = ">=3.5"
+
+[[package]]
+name = "iniconfig"
+version = "1.1.1"
+description = "iniconfig: brain-dead simple config-ini parsing"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "mypy"
+version = "0.991"
+description = "Optional static typing for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+mypy-extensions = ">=0.4.3"
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+typing-extensions = ">=3.10"
+
+[package.extras]
+dmypy = ["psutil (>=4.0)"]
+install-types = ["pip"]
+python2 = ["typed-ast (>=1.4.0,<2)"]
+reports = ["lxml"]
+
+[[package]]
+name = "mypy-extensions"
+version = "0.4.3"
+description = "Experimental type system extensions for programs checked with the mypy typechecker."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "packaging"
+version = "21.3"
+description = "Core utilities for Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
+
+[[package]]
+name = "pluggy"
+version = "1.0.0"
+description = "plugin and hook calling mechanisms for python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+testing = ["pytest-benchmark", "pytest"]
+dev = ["tox", "pre-commit"]
+
+[[package]]
+name = "pyparsing"
+version = "3.0.9"
+description = "pyparsing module - Classes and methods to define and execute parsing grammars"
+category = "dev"
+optional = false
+python-versions = ">=3.6.8"
+
+[package.extras]
+diagrams = ["jinja2", "railroad-diagrams"]
+
+[[package]]
+name = "pytest"
+version = "7.2.0"
+description = "pytest: simple powerful testing with Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+attrs = ">=19.2.0"
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
+iniconfig = "*"
+packaging = "*"
+pluggy = ">=0.12,<2.0"
+tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
+
+[package.extras]
+testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
+
+[[package]]
+name = "pytest-asyncio"
+version = "0.20.3"
+description = "Pytest support for asyncio"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+pytest = ">=6.1.0"
+
+[package.extras]
+docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
+testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
+
+[[package]]
+name = "sniffio"
+version = "1.3.0"
+description = "Sniff out which async library your code is running under"
+category = "main"
+optional = true
+python-versions = ">=3.7"
+
+[[package]]
+name = "tomli"
+version = "2.0.1"
+description = "A lil' TOML parser"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "typing-extensions"
+version = "4.4.0"
+description = "Backported and Experimental Type Hints for Python 3.7+"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[extras]
+di = ["di"]
+
+[metadata]
+lock-version = "1.1"
+python-versions = "^3.10,<4"
+content-hash = "2456a71230de6050a0b2e87d98940218be04ca0d0a415a4e9d8b9fb6a702f3fa"
+
+[metadata.files]
+anyio = [
+    {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"},
+    {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"},
+]
+attrs = [
+    {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
+    {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
+]
+colorama = [
+    {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+    {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+di = [
+    {file = "di-0.73.0-py3-none-any.whl", hash = "sha256:8ded24a6efd540eafb7df7258fabc985cf97733da4f31b1637c48d876e2212fb"},
+    {file = "di-0.73.0.tar.gz", hash = "sha256:d11341d81d42f23841b54a8ae917a0a8a7616e4713ec73dd9d6f3560f57ce3de"},
+]
+exceptiongroup = [
+    {file = "exceptiongroup-1.0.1-py3-none-any.whl", hash = "sha256:4d6c0aa6dd825810941c792f53d7b8d71da26f5e5f84f20f9508e8f2d33b140a"},
+    {file = "exceptiongroup-1.0.1.tar.gz", hash = "sha256:73866f7f842ede6cb1daa42c4af078e2035e5f7607f0e2c762cc51bb31bbe7b2"},
+]
+graphlib2 = [
+    {file = "graphlib2-0.4.7-cp37-abi3-macosx_10_7_x86_64.whl", hash = "sha256:483710733215783cdc76452ccde1247af8f697685c9c1dfd9bb9ff4f52d990ee"},
+    {file = "graphlib2-0.4.7-cp37-abi3-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:3619c7d3c5aca95e6cbbfc283aa6bf42ffa5b59d7f39c8d0ad615bce65dc406f"},
+    {file = "graphlib2-0.4.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b19f1b91d0f22ca3d1cfb2965478db98cf5916a5c6cea5fdc7caf4bf1bfbc33"},
+    {file = "graphlib2-0.4.7-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:624020f6808ee21ffbb2e455f8dd4196bbb37032a35aa3327f0f5b65fb6a35d1"},
+    {file = "graphlib2-0.4.7-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6efc6a197a619a97f1b105aea14b202101241c1db9014bd100ad19cf29288cbf"},
+    {file = "graphlib2-0.4.7-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d7cc38b68775cb2cdfc487bbaca2f7991da0d76d42a68f412c2ca61461e6e026"},
+    {file = "graphlib2-0.4.7-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b06bed98d42f4e10adfe2a8332efdca06b5bac6e7c86dd1d22a4dea4de9b275a"},
+    {file = "graphlib2-0.4.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c9ec3a5645bdf020d8bd9196b2665e26090d60e523fd498df29628f2c5fbecc"},
+    {file = "graphlib2-0.4.7-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:824df87f767471febfd785a05a2cc77c0c973e0112a548df827763ca0aa8c126"},
+    {file = "graphlib2-0.4.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2de5e32ca5c0b06d442d2be4b378cc0bc335c5fcbc14a7d531a621eb8294d019"},
+    {file = "graphlib2-0.4.7-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:13a23fcf07c7bef8a5ad0e04ab826d3a2a2bcb493197005300c68b4ea7b8f581"},
+    {file = "graphlib2-0.4.7-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:15a8a6daa28c1fb5c518d387879f3bbe313264fbbc2fab5635b718bc71a24913"},
+    {file = "graphlib2-0.4.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:0cb6c4449834077972c3cea4602f86513b4b75fcf2d40b12e4fe4bf1aa5c8da2"},
+    {file = "graphlib2-0.4.7-cp37-abi3-win32.whl", hash = "sha256:31b40cea537845d80b69403ae306d7c6a68716b76f5171f68daed1804aadefec"},
+    {file = "graphlib2-0.4.7-cp37-abi3-win_amd64.whl", hash = "sha256:d40935a9da81a046ebcaa0216ad593ef504ae8a5425a59bdbd254c0462adedc8"},
+    {file = "graphlib2-0.4.7-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:9cef08a50632e75a9e11355e68fa1f8c9371d0734642855f8b5c4ead1b058e6f"},
+    {file = "graphlib2-0.4.7-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aeecb604d70317c20ca6bc3556f7f5c40146ad1f0ded837e978b2fe6edf3e567"},
+    {file = "graphlib2-0.4.7-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb4ae9df7ed895c6557619049c9f73e1c2e6d1fbed568010fd5d4af94e2f0692"},
+    {file = "graphlib2-0.4.7-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:3ee3a99fc39df948fef340b01254709cc603263f8b176f72ed26f1eea44070a4"},
+    {file = "graphlib2-0.4.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5873480df8991273bd1585122df232acd0f946c401c254bd9f0d661c72589dcf"},
+    {file = "graphlib2-0.4.7-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297c817229501255cd3a744c62c8f91e5139ee79bc550488f5bc765ffa33f7c5"},
+    {file = "graphlib2-0.4.7-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:853ef22df8e9f695706e0b8556cda9342d4d617f7d7bd02803e824bcc0c30b20"},
+    {file = "graphlib2-0.4.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee62ff1042fde980adf668e30393eca79aee8f1fa1274ab3b98d69091c70c5e8"},
+    {file = "graphlib2-0.4.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b16e21e70938132d4160c2591fed59f79b5f8b702e4860c8933111b5fedb55c2"},
+    {file = "graphlib2-0.4.7.tar.gz", hash = "sha256:a951c18cb4c2c2834eec898b4c75d3f930d6f08beb37496f0e0ce56eb3f571f5"},
+]
+idna = [
+    {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
+    {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
+]
+iniconfig = [
+    {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
+    {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
+]
+mypy = [
+    {file = "mypy-0.991-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab"},
+    {file = "mypy-0.991-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d"},
+    {file = "mypy-0.991-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c8f3be99e8a8bd403caa8c03be619544bc2c77a7093685dcf308c6b109426c6"},
+    {file = "mypy-0.991-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc9ec663ed6c8f15f4ae9d3c04c989b744436c16d26580eaa760ae9dd5d662eb"},
+    {file = "mypy-0.991-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4307270436fd7694b41f913eb09210faff27ea4979ecbcd849e57d2da2f65305"},
+    {file = "mypy-0.991-cp310-cp310-win_amd64.whl", hash = "sha256:901c2c269c616e6cb0998b33d4adbb4a6af0ac4ce5cd078afd7bc95830e62c1c"},
+    {file = "mypy-0.991-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d13674f3fb73805ba0c45eb6c0c3053d218aa1f7abead6e446d474529aafc372"},
+    {file = "mypy-0.991-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c8cd4fb70e8584ca1ed5805cbc7c017a3d1a29fb450621089ffed3e99d1857f"},
+    {file = "mypy-0.991-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:209ee89fbb0deed518605edddd234af80506aec932ad28d73c08f1400ef80a33"},
+    {file = "mypy-0.991-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37bd02ebf9d10e05b00d71302d2c2e6ca333e6c2a8584a98c00e038db8121f05"},
+    {file = "mypy-0.991-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:26efb2fcc6b67e4d5a55561f39176821d2adf88f2745ddc72751b7890f3194ad"},
+    {file = "mypy-0.991-cp311-cp311-win_amd64.whl", hash = "sha256:3a700330b567114b673cf8ee7388e949f843b356a73b5ab22dd7cff4742a5297"},
+    {file = "mypy-0.991-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1f7d1a520373e2272b10796c3ff721ea1a0712288cafaa95931e66aa15798813"},
+    {file = "mypy-0.991-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:641411733b127c3e0dab94c45af15fea99e4468f99ac88b39efb1ad677da5711"},
+    {file = "mypy-0.991-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3d80e36b7d7a9259b740be6d8d906221789b0d836201af4234093cae89ced0cd"},
+    {file = "mypy-0.991-cp37-cp37m-win_amd64.whl", hash = "sha256:e62ebaad93be3ad1a828a11e90f0e76f15449371ffeecca4a0a0b9adc99abcef"},
+    {file = "mypy-0.991-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b86ce2c1866a748c0f6faca5232059f881cda6dda2a893b9a8373353cfe3715a"},
+    {file = "mypy-0.991-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac6e503823143464538efda0e8e356d871557ef60ccd38f8824a4257acc18d93"},
+    {file = "mypy-0.991-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0cca5adf694af539aeaa6ac633a7afe9bbd760df9d31be55ab780b77ab5ae8bf"},
+    {file = "mypy-0.991-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12c56bf73cdab116df96e4ff39610b92a348cc99a1307e1da3c3768bbb5b135"},
+    {file = "mypy-0.991-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:652b651d42f155033a1967739788c436491b577b6a44e4c39fb340d0ee7f0d70"},
+    {file = "mypy-0.991-cp38-cp38-win_amd64.whl", hash = "sha256:4175593dc25d9da12f7de8de873a33f9b2b8bdb4e827a7cae952e5b1a342e243"},
+    {file = "mypy-0.991-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:98e781cd35c0acf33eb0295e8b9c55cdbef64fcb35f6d3aa2186f289bed6e80d"},
+    {file = "mypy-0.991-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6d7464bac72a85cb3491c7e92b5b62f3dcccb8af26826257760a552a5e244aa5"},
+    {file = "mypy-0.991-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c9166b3f81a10cdf9b49f2d594b21b31adadb3d5e9db9b834866c3258b695be3"},
+    {file = "mypy-0.991-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8472f736a5bfb159a5e36740847808f6f5b659960115ff29c7cecec1741c648"},
+    {file = "mypy-0.991-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e80e758243b97b618cdf22004beb09e8a2de1af481382e4d84bc52152d1c476"},
+    {file = "mypy-0.991-cp39-cp39-win_amd64.whl", hash = "sha256:74e259b5c19f70d35fcc1ad3d56499065c601dfe94ff67ae48b85596b9ec1461"},
+    {file = "mypy-0.991-py3-none-any.whl", hash = "sha256:de32edc9b0a7e67c2775e574cb061a537660e51210fbf6006b0b36ea695ae9bb"},
+    {file = "mypy-0.991.tar.gz", hash = "sha256:3c0165ba8f354a6d9881809ef29f1a9318a236a6d81c690094c5df32107bde06"},
+]
+mypy-extensions = [
+    {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
+    {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
+]
+packaging = [
+    {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
+    {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
+]
+pluggy = [
+    {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
+    {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
+]
+pyparsing = [
+    {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
+    {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
+]
+pytest = [
+    {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"},
+    {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"},
+]
+pytest-asyncio = [
+    {file = "pytest-asyncio-0.20.3.tar.gz", hash = "sha256:83cbf01169ce3e8eb71c6c278ccb0574d1a7a3bb8eaaf5e50e0ad342afb33b36"},
+    {file = "pytest_asyncio-0.20.3-py3-none-any.whl", hash = "sha256:f129998b209d04fcc65c96fc85c11e5316738358909a8399e93be553d7656442"},
+]
+sniffio = [
+    {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
+    {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
+]
+tomli = [
+    {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
+    {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
+]
+typing-extensions = [
+    {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
+    {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
+]
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..41a31d4
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,50 @@
+[tool.poetry]
+name = "didiator"
+version = "0.1.0"
+description = "A library that implements the Mediator pattern and uses DI library"
+authors = [
+    "SamWarden <SamWardenSad@gmail.com>",
+]
+maintainers = [
+    "SamWarden <SamWardenSad@gmail.com>",
+]
+license = "MIT"
+readme = "README.rst"
+homepage = "https://github.com/SamWarden/didiator"
+repository = "https://github.com/SamWarden/didiator"
+keywords = [
+    "didiator",
+    "mediatr",
+    "mediator",
+    "CQRS",
+    "DI",
+]
+classifiers = [
+    "License :: OSI Approved :: MIT License",
+    "Typing :: Typed",
+    "Operating System :: OS Independent",
+    "Intended Audience :: Developers",
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+    "Topic :: Software Development :: Libraries :: Python Modules",
+]
+
+[tool.poetry.urls]
+"Bug Tracker" = "https://github.com/SamWarden/didiator/issues"
+
+
+[tool.poetry.dependencies]
+python = "^3.10,<4"
+di = {version = "^0.73.0", extras = ["anyio"], optional = true}
+
+[tool.poetry.extras]
+di = ["di"]
+
+[tool.poetry.dev-dependencies]
+pytest = "^7.2.0"
+mypy = "^0.991"
+pytest-asyncio = "^0.20.3"
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"


Test patch:
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000..c4b0d40
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,4 @@
+[pytest]
+testpaths =
+    tests
+asyncio_mode = auto
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..37f4657
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,16 @@
+import pytest
+
+from didiator.dispatchers.command import CommandDispatcherImpl
+from didiator.dispatchers.query import QueryDispatcherImpl
+
+
+@pytest.fixture()
+def command_dispatcher() -> CommandDispatcherImpl:
+    command_dispatcher = CommandDispatcherImpl()
+    return command_dispatcher
+
+
+@pytest.fixture()
+def query_dispatcher() -> QueryDispatcherImpl:
+    query_dispatcher = QueryDispatcherImpl()
+    return query_dispatcher
diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/integration/test_di_library.py b/tests/integration/test_di_library.py
new file mode 100644
index 0000000..09b560d
--- /dev/null
+++ b/tests/integration/test_di_library.py
@@ -0,0 +1,101 @@
+from functools import partial
+from typing import AsyncGenerator, Callable, Protocol
+
+import pytest
+from di.container import bind_by_type, Container
+from di.dependent import Dependent
+from di.executors import AsyncExecutor
+
+# These tests are for understanding DI library, they're not for didiator
+
+
+class UnitOfWork(Protocol):
+    async def commit(self) -> bool:
+        ...
+
+
+class Repo(Protocol):
+    pass
+
+
+class Handler:
+    def __init__(self, uow: UnitOfWork, repo: Repo):
+        print("Init handler")
+        self._uow = uow
+        self._repo = repo
+
+    async def handle(self, value: bool) -> bool:
+        print("Start handling")
+        assert value
+        assert await self._uow.commit()
+        print("End handling")
+        return True
+
+
+class Session:
+    def __init__(self, uri: str):
+        print("Init session")
+        self.uri = uri
+
+
+class RepoImpl:
+    def __init__(self, session: Session):
+        print("Init repo")
+        self._session = session
+
+
+def get_session_factory(uri: str) -> Callable[..., AsyncGenerator[Session, None]]:
+    async def get_session() -> AsyncGenerator[Session, None]:
+        print("Start session")
+        yield Session(uri)
+        print("Close session")
+    return get_session
+
+
+class UnitOfWorkImpl:
+    def __init__(self, session: Session):
+        print("Init UoW")
+        self._session = session
+
+    async def commit(self) -> bool:
+        print("Commit")
+        return self._session.uri == "psql://uri"
+
+
+class Request(str):
+    pass
+
+
+async def controller(request: Request, uow: UnitOfWork, repo: Repo) -> str:
+    assert request == Request("val")
+    print("Start controller")
+    assert await uow.commit()
+    print("End controller")
+    return "data"
+
+
+@pytest.mark.asyncio
+async def test_class_initialization_with_deps() -> None:
+    container = Container()
+    executor = AsyncExecutor()
+
+    container.bind(bind_by_type(Dependent(UnitOfWorkImpl, scope="request"), UnitOfWork))
+    container.bind(bind_by_type(Dependent(RepoImpl, scope="request"), Repo))
+    container.bind(bind_by_type(Dependent(get_session_factory("psql://uri"), scope="request"), Session))
+
+    solved = container.solve(Dependent(Handler, scope="request"), scopes=["request", "sessio"])
+    solved_func = container.solve(Dependent(partial(controller, Request("val"), uow=1), scope="request"), scopes=["request", "s"])
+    print("Before scope")
+    async with container.enter_scope("sesson") as state:
+        print("State1:", state)
+        async with container.enter_scope("request", state) as state:
+            print("Inside scope")
+            handler = await container.execute_async(solved, executor=executor, state=state)
+            print("After handler initialization")
+            res = await handler.handle(True)
+            controller_res = await container.execute_async(solved_func, executor=executor, state=state, values={lambda: Dependent(Request): Request("val")})
+            assert controller_res == "data"
+            print("Controller res:", controller_res)
+
+    print("Call handler")
+    assert res
diff --git a/tests/mocks/__init__.py b/tests/mocks/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/mocks/middlewares.py b/tests/mocks/middlewares.py
new file mode 100644
index 0000000..a0aac46
--- /dev/null
+++ b/tests/mocks/middlewares.py
@@ -0,0 +1,38 @@
+from typing import Any, TypeVar
+
+from didiator.interface.dispatchers.request import HandlerType
+from didiator.middlewares.base import Middleware
+from didiator.interface.entities.request import Request
+
+RRes = TypeVar("RRes")
+R = TypeVar("R", bound=Request[Any])
+
+
+class DataAdderMiddlewareMock(Middleware):
+    def __init__(self, **kwargs):
+        self._additional_kwargs = kwargs
+
+    async def __call__(
+        self,
+        handler: HandlerType[R, RRes],
+        request: R,
+        *args: Any,
+        **kwargs: Any,
+    ) -> RRes:
+        kwargs |= self._additional_kwargs
+        return await self._call(handler, request, *args, **kwargs)
+
+
+class DataRemoverMiddlewareMock(Middleware):
+    def __init__(self, *args):
+        self._removable_args = args
+
+    async def __call__(
+        self,
+        handler: HandlerType,
+        request: R,
+        *args: Any,
+        **kwargs: Any,
+    ) -> RRes:
+        kwargs = {key: val for key, val in kwargs.items() if key not in self._removable_args}
+        return await self._call(handler, request, *args, **kwargs)
diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/test_di_middleware.py b/tests/unit/test_di_middleware.py
new file mode 100644
index 0000000..a133fbc
--- /dev/null
+++ b/tests/unit/test_di_middleware.py
@@ -0,0 +1,158 @@
+from dataclasses import dataclass
+from typing import Protocol
+
+from di.container import bind_by_type, Container
+from di.dependent import Dependent
+from di.executors import AsyncExecutor
+
+from didiator import Command, CommandHandler, Mediator, Query, QueryDispatcherImpl, QueryHandler
+from didiator.dispatchers.command import CommandDispatcherImpl
+from didiator.mediator import MediatorImpl
+from didiator.middlewares.di import DiMiddleware
+from didiator.utils.di_builder import DiBuilder
+
+
+@dataclass
+class User:
+    user_id: int
+    username: str
+
+
+class Session(Protocol):
+    async def commit(self) -> None:
+        ...
+
+
+class UserRepo(Protocol):
+    async def add_user(self, user: User) -> None:
+        ...
+
+    async def update_user(self, user: User) -> None:
+        ...
+
+    async def get_user_by_id(self, user_id: int) -> User:
+        ...
+
+
+class UnitOfWork(Protocol):
+    user: UserRepo
+
+    async def commit(self) -> None:
+        ...
+
+
+@dataclass
+class GetUserById(Query[User]):
+    user_id: int
+
+
+class GetUserByIdHandler(QueryHandler[GetUserById, User]):
+    def __init__(self, uow: UnitOfWork):
+        print("init get handler")
+
+        self._uow = uow
+
+    async def __call__(self, query: GetUserById) -> User:
+        return await self._uow.user.get_user_by_id(query.user_id)
+
+
+@dataclass
+class CreateUser(Command[int]):
+    user_id: int
+    username: str
+
+
+class CreateUserHandler(CommandHandler[CreateUser, int]):
+    def __init__(self, uow: UnitOfWork):
+        print("init create handler")
+        self._uow = uow
+
+    async def __call__(self, command: CreateUser) -> int:
+        user = User(command.user_id, command.username)
+        await self._uow.user.add_user(user)
+        return user.user_id
+
+
+@dataclass
+class UpdateUser(Command[bool]):
+    user_id: int
+    username: str
+
+
+async def handle_update_user(command: UpdateUser, uow: UnitOfWork) -> bool:
+    print("Handling update user:", command, uow)
+    user = User(command.user_id, command.username)
+    await uow.user.update_user(user)
+    return True
+
+
+class UnitOfWorkImpl:
+    def __init__(self, session: Session, user_repo: UserRepo):
+        self._session = session
+        self.user = user_repo
+
+    async def commit(self) -> None:
+        await self._session.commit()
+
+
+class SessionMock:
+    async def commit(self) -> None:
+        """A standard commit method mock"""
+        pass
+
+
+class UserRepoMock:
+    def __init__(self, session: Session):
+        self._session = session
+        self._db_mock: dict[int, User] = {}
+
+    async def add_user(self, user: User) -> None:
+        self._db_mock[user.user_id] = user
+
+    async def update_user(self, user: User) -> None:
+        self._db_mock[user.user_id] = user
+
+    async def get_user_by_id(self, user_id: int) -> User:
+        if user_id not in self._db_mock:
+            raise ValueError("User with given id doesn't exist")
+        return self._db_mock[user_id]
+
+
+class UserController:
+    def __init__(self, mediator: Mediator):
+        self._mediator = mediator
+
+    async def interact_with_user(self) -> None:
+        assert await self._mediator.send(CreateUser(1, "Jon")) == 1
+        assert await self._mediator.query(GetUserById(1)) == User(1, "Jon")
+        assert await self._mediator.send(UpdateUser(1, "Nick")) is True
+        assert await self._mediator.send(UpdateUser(1, "Sam")) is True
+        assert await self._mediator.query(GetUserById(1)) == User(1, "Sam")
+
+
+class TestDiMiddleware:
+    async def test_di_middleware_with_mediator(self) -> None:
+        di_container = Container()
+        di_executor = AsyncExecutor()
+
+        di_container.bind(bind_by_type(Dependent(UnitOfWorkImpl, scope="mediator"), UnitOfWork))
+        di_container.bind(bind_by_type(Dependent(UserRepoMock, scope="mediator"), UserRepo))
+        di_container.bind(bind_by_type(Dependent(SessionMock, scope="mediator"), Session))
+
+        command_dispatcher = CommandDispatcherImpl(middlewares=(
+            DiMiddleware(DiBuilder(di_container, di_executor, ("mediator", "mediator_request")), cls_scope="mediator"),
+        ))
+        query_dispatcher = QueryDispatcherImpl(middlewares=(
+            DiMiddleware(DiBuilder(di_container, di_executor, ("mediator", "mediator_request")), cls_scope="mediator"),
+        ))
+
+        command_dispatcher.register_handler(CreateUser, CreateUserHandler)
+        command_dispatcher.register_handler(UpdateUser, handle_update_user)
+        query_dispatcher.register_handler(GetUserById, GetUserByIdHandler)
+
+        mediator = MediatorImpl(command_dispatcher, query_dispatcher)
+        async with di_container.enter_scope("mediator") as di_state:
+            scoped_mediator = mediator.bind(di_state=di_state)
+
+            user_controller = UserController(scoped_mediator)
+            await user_controller.interact_with_user()
diff --git a/tests/unit/test_didiator.py b/tests/unit/test_didiator.py
new file mode 100644
index 0000000..3d28ad8
--- /dev/null
+++ b/tests/unit/test_didiator.py
@@ -0,0 +1,5 @@
+from didiator import __version__
+
+
+def test_version() -> None:
+    assert __version__ == '0.1.0'
diff --git a/tests/unit/test_dispatcher.py b/tests/unit/test_dispatcher.py
new file mode 100644
index 0000000..07839a1
--- /dev/null
+++ b/tests/unit/test_dispatcher.py
@@ -0,0 +1,116 @@
+from dataclasses import dataclass
+
+import pytest
+
+from didiator.interface.handlers import CommandHandler
+from didiator.interface.entities.command import Command
+
+from didiator.dispatchers.command import CommandDispatcherImpl
+from didiator.interface.exceptions import CommandHandlerNotFound
+from tests.mocks.middlewares import DataAdderMiddlewareMock, DataRemoverMiddlewareMock
+
+
+@dataclass
+class CreateUserCommand(Command[int]):
+    user_id: int
+    username: str
+
+
+@dataclass
+class UpdateUserCommand(Command[str]):
+    user_id: int
+    username: str
+
+
+class NotCommand:
+    pass
+
+
+class UserId(int):
+    pass
+
+
+class CreateUserHandler(CommandHandler[CreateUserCommand, int]):
+    async def __call__(self, command: CreateUserCommand) -> int:
+        return command.user_id
+
+
+class ExtendedCreateUserHandler(CommandHandler[CreateUserCommand, UserId]):
+    async def __call__(self, command: CreateUserCommand) -> UserId:
+        return UserId(command.user_id)
+
+
+class UpdateUserHandler(CommandHandler[UpdateUserCommand, str]):
+    async def __call__(self, command: UpdateUserCommand, additional_data: str = "") -> str:
+        return additional_data
+
+
+async def handle_update_user(command: UpdateUserCommand, additional_data: str = "") -> str:
+    return additional_data
+
+
+class TestCommandDispatcher:
+    def test_init(self) -> None:
+        command_dispatcher: CommandDispatcherImpl = CommandDispatcherImpl()
+
+        assert isinstance(command_dispatcher, CommandDispatcherImpl)
+        assert command_dispatcher.handlers == {}
+        assert command_dispatcher.middlewares == ()
+
+    async def test_command_handler_registration(self, command_dispatcher: CommandDispatcherImpl) -> None:
+        command_dispatcher.register_handler(CreateUserCommand, CreateUserHandler)
+        assert command_dispatcher.handlers == {CreateUserCommand: CreateUserHandler}
+
+        command_dispatcher.register_handler(UpdateUserCommand, UpdateUserHandler)
+        assert command_dispatcher.handlers == {CreateUserCommand: CreateUserHandler, UpdateUserCommand: UpdateUserHandler}
+
+        command_dispatcher.register_handler(CreateUserCommand, ExtendedCreateUserHandler)
+        assert command_dispatcher.handlers == {CreateUserCommand: ExtendedCreateUserHandler, UpdateUserCommand: UpdateUserHandler}
+
+    async def test_initialization_with_middlewares(self) -> None:
+        middleware1 = DataAdderMiddlewareMock()
+        command_dispatcher = CommandDispatcherImpl(middlewares=(middleware1,))
+        assert command_dispatcher.middlewares == (middleware1,)
+
+        middleware2 = DataRemoverMiddlewareMock()
+        command_dispatcher = CommandDispatcherImpl(middlewares=[middleware1, middleware2])
+        assert command_dispatcher.middlewares == (middleware1, middleware2)
+
+    async def test_command_sending(self, command_dispatcher: CommandDispatcherImpl) -> None:
+        command_dispatcher.register_handler(CreateUserCommand, CreateUserHandler)
+
+        res = await command_dispatcher.send(CreateUserCommand(1, "Jon"))
+        assert res == 1
+
+    async def test_sending_not_registered_command(self, command_dispatcher: CommandDispatcherImpl) -> None:
+        command_dispatcher.register_handler(CreateUserCommand, CreateUserHandler)
+
+        with pytest.raises(CommandHandlerNotFound):
+            await command_dispatcher.send(UpdateUserCommand(1, "Jon"))
+
+    async def test_command_sending_with_middlewares(self) -> None:
+        middleware1 = DataAdderMiddlewareMock(middleware_data="data", additional_data="value")
+        middleware2 = DataRemoverMiddlewareMock("middleware_data")
+        command_dispatcher = CommandDispatcherImpl(middlewares=[middleware1, middleware2])
+
+        command_dispatcher.register_handler(UpdateUserCommand, UpdateUserHandler)
+
+        res = await command_dispatcher.send(UpdateUserCommand(1, "Sam"))
+        assert res == "value"
+
+    async def test_command_sending_with_function_handler(self) -> None:
+        command_dispatcher = CommandDispatcherImpl()
+        command_dispatcher.register_handler(UpdateUserCommand, handle_update_user)
+
+        res = await command_dispatcher.send(UpdateUserCommand(1, "Sam"), additional_data="value")
+        assert res == "value"
+
+    async def test_command_sending_with_middlewares_and_function_handler(self) -> None:
+        middleware1 = DataAdderMiddlewareMock(middleware_data="data", additional_data="value")
+        middleware2 = DataRemoverMiddlewareMock("middleware_data")
+        command_dispatcher = CommandDispatcherImpl(middlewares=[middleware1, middleware2])
+
+        command_dispatcher.register_handler(UpdateUserCommand, handle_update_user)
+
+        res = await command_dispatcher.send(UpdateUserCommand(1, "Sam"))
+        assert res == "value"
diff --git a/tests/unit/test_mediator.py b/tests/unit/test_mediator.py
new file mode 100644
index 0000000..9e0f9d5
--- /dev/null
+++ b/tests/unit/test_mediator.py
@@ -0,0 +1,116 @@
+from dataclasses import dataclass
+
+from didiator.interface.handlers import CommandHandler, QueryHandler
+from didiator.interface.entities.command import Command
+from didiator.dispatchers.command import CommandDispatcherImpl
+from didiator.interface.mediator import CommandMediator, Mediator, QueryMediator
+from didiator.mediator import MediatorImpl
+from didiator.interface.entities.query import Query
+from didiator.dispatchers.query import QueryDispatcherImpl
+from tests.mocks.middlewares import DataRemoverMiddlewareMock
+
+
+@dataclass
+class CommandMock(Command[str]):
+    result: str
+
+
+@dataclass
+class QueryMock(Query[str]):
+    result: str
+
+
+class CommandHandlerMock(CommandHandler[CommandMock, str]):
+    def __init__(self, *args, **kwargs):
+        self._excluded_args = args
+        self._expected_kwargs = kwargs
+
+    async def __call__(self, command: CommandMock, *args, **kwargs) -> str:
+        assert not set(self._excluded_args) & set(kwargs)
+
+        for key, val in self._expected_kwargs.items():
+            assert kwargs[key] == val
+
+        return command.result
+
+
+class QueryHandlerMock(QueryHandler[QueryMock, str]):
+    def __init__(self, *args, **kwargs):
+        self._excluded_args = args
+        self._expected_kwargs = kwargs
+
+    async def __call__(self, query: QueryMock, *args, **kwargs) -> str:
+        assert not set(self._excluded_args) & set(kwargs)
+
+        for key, val in self._expected_kwargs.items():
+            assert kwargs[key] == val
+
+        return query.result
+
+
+class TestMediator:
+    def test_init(self) -> None:
+        mediator: Mediator = MediatorImpl()
+
+        assert isinstance(mediator, MediatorImpl)
+        assert isinstance(MediatorImpl(CommandDispatcherImpl(), QueryDispatcherImpl()), MediatorImpl)
+        assert isinstance(MediatorImpl(CommandDispatcherImpl()), MediatorImpl)
+        assert isinstance(MediatorImpl(
+            command_dispatcher=CommandDispatcherImpl(), query_dispatcher=QueryDispatcherImpl(),
+        ), MediatorImpl)
+
+    async def test_sending_command_through_mediator(self) -> None:
+        command_dispatcher = CommandDispatcherImpl()
+        command_dispatcher.register_handler(CommandMock, CommandHandlerMock)
+
+        mediator: CommandMediator = MediatorImpl(command_dispatcher)
+
+        assert await mediator.send(CommandMock("data")) == "data"
+
+    async def test_query_queries_through_mediator(self) -> None:
+        query_dispatcher = QueryDispatcherImpl()
+        query_dispatcher.register_handler(QueryMock, CommandHandlerMock)
+
+        mediator: QueryMediator = MediatorImpl(query_dispatcher=query_dispatcher)
+
+        assert await mediator.query(QueryMock("data")) == "data"
+
+    async def test_sending_command_through_mediator_with_extra_args(self) -> None:
+        command_dispatcher = CommandDispatcherImpl((DataRemoverMiddlewareMock("middleware_data"),))
+        command_dispatcher.register_handler(CommandMock, CommandHandlerMock(additional_data="arg"))
+
+        mediator = MediatorImpl(command_dispatcher)
+
+        assert await mediator.send(CommandMock("data"), middleware_data="value", additional_data="arg") == "data"
+
+    async def test_extra_data_binding(self) -> None:
+        mediator_without_extra_data = MediatorImpl()
+        assert mediator_without_extra_data.extra_data == {}
+
+        mediator = MediatorImpl(extra_data={"additional_data": "arg"})
+        assert mediator.extra_data == {"additional_data": "arg"}
+
+        mediator2 = mediator.bind(middleware_data="value", some_data=1)
+        assert mediator.extra_data == {"additional_data": "arg"}
+        assert mediator2.extra_data == {"additional_data": "arg", "middleware_data": "value", "some_data": 1}
+
+        mediator3 = mediator2.unbind("some_data", "additional_data")
+        assert mediator.extra_data == {"additional_data": "arg"}
+        assert mediator2.extra_data == {"additional_data": "arg", "middleware_data": "value", "some_data": 1}
+        assert mediator3.extra_data == {"middleware_data": "value"}
+
+        mediator4 = mediator2.bind(another_data=None, some_data=2)
+        assert mediator.extra_data == {"additional_data": "arg"}
+        assert mediator2.extra_data == {"additional_data": "arg", "middleware_data": "value", "some_data": 1}
+        assert mediator3.extra_data == {"middleware_data": "value"}
+        assert mediator4.extra_data == {
+            "additional_data": "arg", "middleware_data": "value", "some_data": 2, "another_data": None,
+        }
+
+    async def test_sending_command_through_mediator_with_extra_data(self) -> None:
+        command_dispatcher = CommandDispatcherImpl()
+        command_dispatcher.register_handler(CommandMock, CommandHandlerMock(additional_data="arg"))
+
+        mediator = MediatorImpl(command_dispatcher, extra_data={"additional_data": "arg"})
+        mediator2 = mediator.bind(middleware_data="value")
+        assert await mediator2.send(CommandMock("data")) == "data"
diff --git a/tests/unit/test_middlewares.py b/tests/unit/test_middlewares.py
new file mode 100644
index 0000000..115174c
--- /dev/null
+++ b/tests/unit/test_middlewares.py
@@ -0,0 +1,77 @@
+from dataclasses import dataclass
+from functools import partial
+
+from didiator.interface.handlers import CommandHandler
+from didiator.interface.entities.command import Command
+from didiator.middlewares.base import Middleware
+from tests.mocks.middlewares import DataAdderMiddlewareMock, DataRemoverMiddlewareMock
+
+
+@dataclass
+class CommandMock(Command[bool]):
+    pass
+
+
+class HandlerMock(CommandHandler[CommandMock, bool]):
+    def __init__(self, *args, **kwargs):
+        self._excluded_args = args
+        self._expected_kwargs = kwargs
+
+    async def __call__(self, command: CommandMock, *args, **kwargs) -> bool:
+        assert not set(self._excluded_args) & set(kwargs)
+
+        for key, val in self._expected_kwargs.items():
+            assert kwargs[key] == val
+
+        return True
+
+
+async def mock_handle_command(command: CommandMock, *args, **kwargs) -> bool:
+    assert "middleware_data" not in kwargs
+    assert kwargs["additional_data"] == "data"
+    return True
+
+
+class TestBaseMiddleware:
+    def test_init(self) -> None:
+        middleware = Middleware()
+
+        assert isinstance(middleware, Middleware)
+
+    async def test_handling_by_middleware(self) -> None:
+        middleware = Middleware()
+
+        assert await middleware(HandlerMock, CommandMock()) is True
+
+    async def test_handling_by_two_middlewares(self) -> None:
+        middleware = Middleware()
+        middleware2 = Middleware()
+
+        assert await middleware(partial(middleware2, HandlerMock), CommandMock()) is True
+
+    async def test_handling_by_middlewares_and_initialized_command_handler(self) -> None:
+        middleware = Middleware()
+        middleware2 = Middleware()
+
+        assert await middleware(partial(middleware2, HandlerMock()), CommandMock()) is True
+
+    async def test_handling_by_middleware_with_params(self) -> None:
+        middleware = DataAdderMiddlewareMock(additional_data="data")
+
+        assert await middleware(HandlerMock(additional_data="data"), CommandMock()) is True
+
+    async def test_handling_by_two_middlewares_with_params(self) -> None:
+        middleware = DataAdderMiddlewareMock(additional_data="data", some_data="value")
+        middleware2 = DataRemoverMiddlewareMock("middleware_data")
+
+        assert await middleware(
+            partial(middleware2, HandlerMock("middleware_data", additional_data="data", some_data="value")),
+            CommandMock(),
+            middleware_data="data",
+        ) is True
+
+    async def test_handling_with_middlewares_and_function_command_handler(self) -> None:
+        middleware = DataAdderMiddlewareMock(additional_data="data")
+        middleware2 = DataRemoverMiddlewareMock("middleware_data")
+
+        assert await middleware(partial(middleware2, mock_handle_command), CommandMock(), middleware_data="data") is True
diff --git a/tests/unit/test_query_dispatcher.py b/tests/unit/test_query_dispatcher.py
new file mode 100644
index 0000000..712fb88
--- /dev/null
+++ b/tests/unit/test_query_dispatcher.py
@@ -0,0 +1,116 @@
+from dataclasses import dataclass
+
+import pytest
+
+from didiator.interface.exceptions import QueryHandlerNotFound
+from didiator.interface.entities.query import Query
+from didiator.interface.handlers import QueryHandler
+
+from didiator.dispatchers.query import QueryDispatcherImpl
+from tests.mocks.middlewares import DataAdderMiddlewareMock, DataRemoverMiddlewareMock
+
+
+@dataclass
+class GetUserQuery(Query[int]):
+    user_id: int
+    username: str
+
+
+@dataclass
+class CollectUserDataQuery(Query[str]):
+    user_id: int
+    username: str
+
+
+class NotQuery:
+    pass
+
+
+class UserId(int):
+    pass
+
+
+class GetUserHandler(QueryHandler[GetUserQuery, int]):
+    async def __call__(self, query: GetUserQuery) -> int:
+        return query.user_id
+
+
+class ExtendedGetUserHandler(QueryHandler[GetUserQuery, UserId]):
+    async def __call__(self, query: GetUserQuery) -> UserId:
+        return UserId(query.user_id)
+
+
+class CollectUserDataHandler(QueryHandler[CollectUserDataQuery, str]):
+    async def __call__(self, query: CollectUserDataQuery, additional_data: str = "") -> str:
+        return additional_data
+
+
+async def handle_update_user(query: CollectUserDataQuery, additional_data: str = "") -> str:
+    return additional_data
+
+
+class TestQueryDispatcher:
+    def test_init(self) -> None:
+        query_dispatcher: QueryDispatcherImpl = QueryDispatcherImpl()
+
+        assert isinstance(query_dispatcher, QueryDispatcherImpl)
+        assert query_dispatcher.handlers == {}
+        assert query_dispatcher.middlewares == ()
+
+    async def test_query_handler_registration(self, query_dispatcher: QueryDispatcherImpl) -> None:
+        query_dispatcher.register_handler(GetUserQuery, GetUserHandler)
+        assert query_dispatcher.handlers == {GetUserQuery: GetUserHandler}
+
+        query_dispatcher.register_handler(CollectUserDataQuery, CollectUserDataHandler)
+        assert query_dispatcher.handlers == {GetUserQuery: GetUserHandler, CollectUserDataQuery: CollectUserDataHandler}
+
+        query_dispatcher.register_handler(GetUserQuery, ExtendedGetUserHandler)
+        assert query_dispatcher.handlers == {GetUserQuery: ExtendedGetUserHandler, CollectUserDataQuery: CollectUserDataHandler}
+
+    async def test_initialization_with_middlewares(self) -> None:
+        middleware1 = DataAdderMiddlewareMock()
+        query_dispatcher = QueryDispatcherImpl(middlewares=(middleware1,))
+        assert query_dispatcher.middlewares == (middleware1,)
+
+        middleware2 = DataRemoverMiddlewareMock()
+        query_dispatcher = QueryDispatcherImpl(middlewares=[middleware1, middleware2])
+        assert query_dispatcher.middlewares == (middleware1, middleware2)
+
+    async def test_query_querying(self, query_dispatcher: QueryDispatcherImpl) -> None:
+        query_dispatcher.register_handler(GetUserQuery, GetUserHandler)
+
+        res = await query_dispatcher.query(GetUserQuery(1, "Jon"))
+        assert res == 1
+
+    async def test_querying_not_registered_query(self, query_dispatcher: QueryDispatcherImpl) -> None:
+        query_dispatcher.register_handler(GetUserQuery, GetUserHandler)
+
+        with pytest.raises(QueryHandlerNotFound):
+            await query_dispatcher.query(CollectUserDataQuery(1, "Jon"))
+
+    async def test_query_querying_with_middlewares(self) -> None:
+        middleware1 = DataAdderMiddlewareMock(middleware_data="data", additional_data="value")
+        middleware2 = DataRemoverMiddlewareMock("middleware_data")
+        query_dispatcher = QueryDispatcherImpl(middlewares=[middleware1, middleware2])
+
+        query_dispatcher.register_handler(CollectUserDataQuery, CollectUserDataHandler)
+
+        res = await query_dispatcher.query(CollectUserDataQuery(1, "Sam"))
+        assert res == "value"
+
+    async def test_query_querying_with_function_handler(self) -> None:
+        query_dispatcher = QueryDispatcherImpl()
+        query_dispatcher.register_handler(CollectUserDataQuery, handle_update_user)
+
+        res = await query_dispatcher.query(CollectUserDataQuery(1, "Sam"), additional_data="value")
+        assert res == "value"
+
+    async def test_query_querying_with_middlewares_and_function_handler(self) -> None:
+        middleware1 = DataAdderMiddlewareMock(middleware_data="data", additional_data="value")
+        middleware2 = DataRemoverMiddlewareMock("middleware_data")
+        query_dispatcher = QueryDispatcherImpl(middlewares=[middleware1, middleware2])
+
+        query_dispatcher.register_handler(CollectUserDataQuery, handle_update_user)
+
+        res = await query_dispatcher.query(CollectUserDataQuery(1, "Sam"))
+        assert res == "value"