Repository: reagento/dishka
Pull request #58 (2024-02-18 18:00:36)

v0.3


Patch:
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..0c1017c
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,7 @@
+[report]
+exclude_lines = 
+    pragma: not covered
+    @overload
+[run]
+omit =
+    src/dishka/_adaptix/**
\ No newline at end of file
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 0000000..4304498
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,10 @@
+version: 2
+build:
+  os: ubuntu-22.04
+  tools:
+    python: "3.10"
+python:
+  install:
+    - method: pip
+      path: .
+    - requirements: requirements_doc.txt
diff --git a/.ruff.toml b/.ruff.toml
index f102c3b..2c8fb52 100644
--- a/.ruff.toml
+++ b/.ruff.toml
@@ -1,8 +1,9 @@
 line-length = 79
 src = ["src"]
 include = ["src/**.py", "tests/**.py"]
+exclude = ["src/dishka/_adaptix/**"]
 
-select = [
+lint.select = [
     "E",
     "F",
     "BLE",
@@ -19,8 +20,8 @@ select = [
 ]
 
 
-[isort]
+[lint.isort]
 no-lines-before = ["local-folder"]
 
-[flake8-tidy-imports]
+[lint.flake8-tidy-imports]
 ban-relative-imports = "parents"
\ No newline at end of file
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..01e65f5
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS    ?=
+SPHINXBUILD   ?= sphinx-build
+SOURCEDIR     = docs
+BUILDDIR      = docs-build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/README.md b/README.md
index 71c4ed1..b613a0b 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@ Main ideas:
 * **Simple API**. You need minimum of objects to start using library. You can easily integrate it with your task framework, examples provided. 
 * **Speed**. It is fast enough so you not to worry about. It is even faster than many of the analogs.
 
-See more in [technical requirements](docs/technical_requirements.md)
+See more in [technical requirements](https://dishka.readthedocs.io/en/latest/requirements/technical.html)
 
 ### Quickstart
 
@@ -72,7 +72,7 @@ Some of them can live while you application is running, others are destroyed and
 
   `APP` -> `REQUEST` -> `ACTION` -> `STEP`.
 
-You decide when to enter and exit them, but it is done one by one. You set a scope for your dependency when you configure how to create it. If the same dependency is requested multiple time within one scope without leaving it, then the same instance is returned.
+You decide when to enter and exit them, but it is done one by one. You set a scope for your dependency when you configure how to create it. If the same dependency is requested multiple time within one scope without leaving it, then by default the same instance is returned.
 
 If you are developing web application, you would enter `APP` scope on startup, and you would `REQUEST` scope in each HTTP-request.
 
@@ -113,7 +113,7 @@ If `provide` is used with some class then that class itself is treated as a fact
     class MyProvider(Provider):
         a = provide(A, scope=Scope.REQUEST)
     ```
-* Want to create a child class instance when parent is requested? add a `dependency` attribute to `provide` function with a parent class while passing child as a first parameter 
+* Want to create a child class instance when parent is requested? add a `source` attribute to `provide` function with a parent class while passing child as a first parameter
     ```python 
     class MyProvider(Provider):
         a = provide(source=AChild, scope=Scope.REQUEST, provides=A)
@@ -162,3 +162,14 @@ with make_container(MyProvider(), context={App: app}) as container:
 ```python
 with make_container(MyProvider(), OtherProvider()) as container:
 ```
+
+* Tired of providing `scope==` for each depedency? Set it inside your `Provider` class and all dependencies with no scope will use it.
+```python
+
+class MyProvider(Provider):
+   scope=Scope.APP
+
+   @provide
+   async def get_a(self) -> A:
+      return A()
+```
\ No newline at end of file
diff --git a/docs/concepts.rst b/docs/concepts.rst
new file mode 100644
index 0000000..b2ce0aa
--- /dev/null
+++ b/docs/concepts.rst
@@ -0,0 +1,86 @@
+.. include:: <isonum.txt>
+
+Key concepts
+*****************
+
+Dishka is a DI-framework and it is designed to create complex objects following dependency injection principle.
+Let's start with some terms.
+
+Dependency
+==================
+
+**Dependency** is what you need for some part of your code to work.
+If you *database gateway* needs *database connection* to execute SQL queries, then the connection is a dependency for a gateway. If your business logic class requires *database gateway* or some *api client* then *api client* and *database gateway* are dependencies for a business logic. Here the ``Client`` is a dependency, while ``Service`` is a dependant.
+
+.. code-block:: python
+
+    class Service:
+        def __init__(self, client: Client):
+            self.client = client
+
+So, dependency is just object required for some other object. Dependencies can depend on other objects, which are their dependencies.
+
+To follow dependency injection rule, dependent object should receive their dependencies and not request themselves. The same classes can be instantiated with different values of their dependencies. At least in tests.
+
+
+Scope
+===========
+
+**Scope** is a lifespan of a dependency.
+Some dependencies can live while you application is running, others are created and destroyed on each request. In more rare cases you need more short-lived objects. You set a scope for your dependency when you configure how to create it.
+
+Standard scopes are:
+
+    ``APP`` |rarr| ``REQUEST`` |rarr| ``ACTION`` |rarr| ``STEP``
+
+You decide when to enter and exit them, but it is done one by one. If you entered ``APP`` scope then the next step deeper will enter ``REQUEST`` scope.
+
+In dishka dependencies are lazy - they are created when you first request them. If the same dependency is requested multiple time within one scope then the same instance is returned (you can disable it for each dependency separately). A created dependency is kept until you exit the scope. And in that moment it is not just dropped away, but corresponding finalization steps are done. You can enter same scope multiple times concurrently so to have multiple instances of objects you can work simultaneously.
+
+Each object can depend on other objects from the same or previous scopes. So, if you have ``Config`` with scope of *APP* and ``Connection`` with scope of *REQUEST* you cannot have an *APP*-scoped object with requires a connection, but you can have *REQUEST*-scoped object which requires a ``Connection`` or a ``Config`` (or even both).
+
+If you are developing web application, you would enter ``APP`` scope on startup, and you would ``REQUEST`` scope in each HTTP-request.
+
+You can provide your own Scopes class if you are not satisfied with standard flow.
+
+Container
+==================
+
+**Container** is an object you use to get your dependency.
+
+You just call ``container.get(SomeType)`` and it finds a way to get you an instance of that type. It does not create things itself, but manages their lifecycle and caches. It delegates objects creation to providers which are passed during creation.
+
+Each container is assigned to a certain scope. To enter the nested scope you call it and use as a context manager.
+According to scopes order container can be used to get dependencies from its and previous scopes.
+
+.. code-block:: python
+
+    with make_container(provider1, provider2) as app_container:  # enter APP scope
+        config = app_container.get(Config)  # APP-scoped object
+        with container() as request_container:  # enter REQUEST scope
+            connection = request_container.get(Connection)  # REQUEST-scoped object
+            config = request_container.get(Config)  # APP-scoped object
+
+Async container is working in the same manner, but you should use async context manager and await the result of get
+
+Some containers are concurrently safe, others are not: it is configured when you call a context manager. For web applications it is good to have APP-scoped container thread/task-safe, but REQUEST-scoped containers do not it, and it is default behavior.
+
+Provider
+===============
+
+**Provider** is an object which members are used to construct dependencies. It is a normal object, but some attributes must be marked with special decorators so they will be used by a container. To create your own provider you inherit from ``Provider`` class and instantiate it when creating a container:
+
+.. code-block:: python
+
+    class MyProvider(Provider):
+        pass
+
+    with make_container(MyProvider()) as container:
+        pass
+
+
+There are 3 special functions:
+
+* ``@provide`` is used to declare a factory providing a dependency. It can be used with some class or as a method decorator. :ref:`Read more<provide>`
+* ``alias`` is used to allow retrieving of the same object by different type hints. :ref:`Read more<alias>`
+* ``decorate`` is used to modify or wrap an object which is already configured in another ``Provider``. :ref:`Read more<decorate>`
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..8686d2c
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,57 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+# import os
+# import sys
+# sys.path.insert(0, os.path.abspath('.'))
+
+
+# -- Project information -----------------------------------------------------
+
+project = 'dishka'
+copyright = '2022, reagento'
+author = 'Tishka17'
+master_doc = 'index'
+
+# -- General configuration ---------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+    'sphinx.ext.autodoc',
+]
+autodoc_type_aliases = {
+}
+autodoc_typehints = 'description'
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = []
+
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+#
+html_theme = 'furo'
+
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
diff --git a/docs/container/index.rst b/docs/container/index.rst
new file mode 100644
index 0000000..61bc681
--- /dev/null
+++ b/docs/container/index.rst
@@ -0,0 +1,100 @@
+Container
+*******************
+
+**Container** is an object you use to get your dependency.
+
+Basic usage
+======================
+
+
+Container can be synchronous or asynchronous.
+
+* *Async* container can use any type of dependency sources: both sync and async are supported. Sync methods are called directly and no executors are used, so avoid network I/O in synchronous functions
+* *Sync* container can use only synchronous dependency sources.
+
+To create a top level container you should call ``make_container`` (or ``make_async_container``) and use it as a context manager. Pass there one or more providers.
+
+.. code-block:: python
+
+    from dishka import make_container
+    with make_container(provider) as container:
+        ...
+
+And async version correspondingly:
+
+.. code-block:: python
+
+    from dishka import make_container
+    async with make_async_container(provider) as container:
+        ...
+
+If you have not provided your own *scopes* enum, then default one will be used. Root container is attached to the first scope: Scope.APP by default.
+
+To enter the next scope you should call container as a function and enter context manager (same sync or async):
+
+.. code-block:: python
+
+    with container() as nested_container:
+        pass
+
+Container as needed for retrieving objects. To do it you need call ``get(DependencyType)`` (and ``await`` it for async container).
+All retrieved dependencies are stored inside container of corresponding scope until you exit that scope. So, you if you call ``get`` multiple times you will receive the same instance. The rule is followed for indirect dependencies as well. Multiple dependencies of the same scope have their own cache.
+
+.. code-block:: python
+
+    with make_container(provider) as container:
+        a = container.get(A)
+        a = container.get(A)  # same instance
+
+Whe you exit the scope, dependency cache is cleared. Finalization of dependencies is done if you used generator factories
+
+Thread/task safety
+==========================
+
+You can have multiple containers of the same scope simultaneously (except the top level one) - it is safe while you do not have dependencies of previous scope.
+
+For example, if you have declared ``SessionPool`` as an APP-scoped dependency and then you concurrently enter REQUEST scope. Once you request ``SessionPool`` for the first time (directly or for another dependency) you cannot guarantee that only one instance of that object is created.
+
+To prevent such a condition you need to protect any session whose children can be used concurrently: to pass ``lock_factory`` when creating a container. Do not mix up threading and asyncio locks: they are not interchangeable, use the proper one.
+
+
+.. code-block:: python
+
+    import threading
+
+    with make_container(provider, lock_factory=threading.Lock) as container:
+        with container(lock_factory=threading.Lock) as nested_container:
+            ...
+
+
+
+.. code-block:: python
+
+    import asyncio
+
+    async with make_async_container(provider, lock_factory=asyncio.Lock) as container:
+        async with container(lock_factory=asyncio.Lock) as nested_container:
+            ...
+
+.. note::
+    Do not worry, lock is set by default for top level (``Scope.APP``) container. So, if you are not using other scopes concurrently you do not need any changes. (E.g. if you are not using multiple ``Scope.ACTION`` containers at a same time within one ``Scope.REQUEST`` container)
+
+Context data
+====================
+
+Often, you scopes are assigned with some external events: HTTP-requests, message from queue, callbacks from framework. You can use those objects when creating dependencies. The difference from normal factories is that they are not created inside some ``Provder``, but passed to the scope:
+
+.. code-block:: python
+
+    from framework import Request
+
+    class MyProvider:
+        @provide(scope=Scope.REQUEST)
+        def a(self, request: Request) -> A:
+            return A(data=request.contents)
+
+    while True:
+        request = connection.recv()
+        with container(context={Request:request}) as request_container:
+             a = request_container.get(A)
+
diff --git a/docs/di_intro.rst b/docs/di_intro.rst
new file mode 100644
index 0000000..7cd1697
--- /dev/null
+++ b/docs/di_intro.rst
@@ -0,0 +1,175 @@
+Dependency injection
+*************************
+
+What is Dependency Injection?
+==================================
+
+Dependency injection is a simple idea which says that if some object requires another object for its work it should receive it from outside instead of creating or requesting that object itself.
+
+Imagine you have a business logic which uses some remote API client
+
+.. code-block:: python
+
+    class Service:
+        def action(self):
+            client = Client(token)
+            client.get_data()
+
+    service = Service()
+    service.action()
+
+Here, the ``client`` is a **dependency**. Imagine that you have many methods working with same client and each methods knows how to create the client. Than think about these question?
+
+* How do they get the ``token``? Should the every method read it on its own?
+* What if the ``Client`` constructor will require more than only token? Should we copy-paste new parameters to each method?
+* How will we replace ``client`` with mock while testing these methods?
+* How do we know which part of the code can use the ``client``?
+* How can we reuse same client if it becomes stateful? How can we reuse several clients in different cases?
+
+I hope by this moment you've started suspecting that there is a problem. To solve it let's create our **dependency** somewhere outside of this method and **inject** it.
+There are three ways to do it:
+
+1. **Parameter injection**. We can pass client to a method as a parameter.
+
+.. code-block:: python
+
+    class Service:
+        def action(self, client: Client):
+            client.get_data()
+
+    service = Service()
+    client = Client(token="1234567890")
+    service.action(client)
+
+It works well unless you have many methods and they can call each other. Also we change the signature of methods, which can lead to big code changes and impossible to be done if there is an expectation about the interface of an object.
+
+2. **Constructor injection**. We can pass client when constructing service instance. All its methods will have access to it. It is a primary way to do DI
+
+.. code-block:: python
+
+    class Service:
+        def __init__(self, client: Client):
+            self.client = client
+
+        def action(self):
+            self.client.get_data()
+
+    token = os.getenv("TOKEN")
+    client = Client(token)
+    service = Service(client)
+    service.action()
+
+3. **Attribute injection**. We can store an attribute directly (or using additional methods) on the constructed object. It mostly used in combination with constructor injection to change existing objects, or to break cycle references between objects. E.g.
+
+.. code-block:: python
+
+    class Service:
+        client: Client
+
+        def action(self):
+            self.client.get_data()
+
+    service = Service(client)
+    service.client = Client(token)
+    service.action()
+
+Additionally I need to name anti-patterns, which should be avoided as they cannot solve all the problems which are eliminated by DI, though they can solve part of them:
+
+* *Global variable*. Though it allows us to reuse a dependency and configure it outside of our business logic we are limited to one instance forever. Also, we do not control when it is created and finalized: some code can try to use it before it is properly configured
+
+* *Singleton*. It's mostly a variant of global variable. It can add laziness, but other characteristics are the same.
+
+* *Monkey patch*. Or `mock.patch()` as an example. It allows to replace behavior in tests but it also relies on details how the class is imported and used instead of its interface. That make tests more fragile and requires more work to support them
+
+When to inject dependencies?
+===================================
+
+For simple cases it obvious that you have some classes with their requirements and once you start you app you create all of them and wire together. But real applications are more complicated things. They operated dozens or even hundreds of objects in complex hierarchy, they do concurrent processing.
+
+
+It is a good idea to divide the code which uses dependencies and the code which creates them. Usually we want to reduce the knowledge about our dependencies in the code which uses them. But it is not always possible as different objects have different lifespan.
+
+For example, *configuration* is usually loaded during application startup, but *database transactions* (and corresponding *database connections*) should be opened separately for each processing HTTP-request. So it is unavoidable to create and finalize dependencies somewhere inside request processing. Other dependencies will have their own **scopes**, but often there are only two of them: the application lifetime and each request.
+
+For web application it can look like this:
+
+.. code-block:: python
+
+    @app.get("/")
+    def index(request):
+        client = Client(os.getenv("TOKEN"))
+        service = Service(client)
+        service.action()
+
+    @app.get("/foo")
+    def get_foo(request):
+        client = Client(os.getenv("TOKEN"))
+        service = Service(client)
+        service.action()
+
+The trick is how to manager those dependencies when you have a lot of request handlers without losing ability to test them.
+
+* On approach is to create all those dependencies in middleware (it's a special object which is called by your framework on each event). In pseudo-code it will be kind of this:
+
+.. code-block:: python
+
+    def service_creator(request):
+        client = Client(os.getenv("TOKEN"))
+        service = Service(client)
+        request.state.service = Service(client)
+
+    app.setup_middleware(service_creator)
+
+    @app.get("/")
+    def index(request):
+        service = request.state.service
+        service.action()
+
+It works good. You have clean request handlers and you can change middlewares in tests. But it can become a problem if you have lot's of objects which are not cheap to create.
+
+* The second approach is to create some factory (let's call it **container**) and call it within request handler. You can still use middleware to pass it into handler (check also others features of your framework)
+
+.. code-block:: python
+
+    class Container:
+        def get_client(self) -> Client:
+            return Client(os.getenv("TOKEN"))
+
+        def get_service(self) -> Service:
+            return Service(self.get_client())
+
+    container = Container()
+    def container_middleware(request):
+        request.state.container = container
+
+    app.setup_middleware(container_middleware)
+
+    @app.get("/")
+    def index(request):
+        service = container.get_service()
+        service.action()
+
+Comparing to middleware it allows you to create only needed objects. But beware of accessing a container from handlers via global variable - that will make tests more difficult to maintain.
+
+In both approaches you can control whether the instance is created on each request or once per app. Also you can have different middlewares or containers for production and test purposes.
+
+
+What is IoC-container?
+=============================
+
+IoC-container is a special object (or a framework providing such an object) which provides required objects following dependency injection rules and managers their lifetime. DI-framework is another name fur such frameworks.
+
+Common mistake is to treat IoC-container as a single way to inject dependencies. It has nothing common with reality. Dependency injection can be done just by passing one object to another, but in complex application it is not so easy to do. As it was shown above you might want to create a separate object to encapsulate all DI-related logic. ``Container`` in previous example is a an example of hand-written primitive IoC-container.
+
+Bigger is your application, more complex factories you need, more necessary is to automate creation of a container. You do not need to use IoC-container to test one small part of application, but can be essential for launching it in whole. Fortunately, there are frameworks for it. But again, beware of spreading container-related details around your application code with an exception on scope boundaries.
+
+So, talking about IoC-container we can write-down these ideas:
+
+* IoC-container is not necessary for dependency injection
+* It is a useful helper for bigger applications
+* It should be safe to use it in concurrent application
+* It should follow the rules you provide for your dependencies (single or multiple instances, multiple lifetime scopes, etc.)
+
+More about possible requirements you can read in :ref:`technical-requirements`.
+
+So here is the time for **dishka** - an implementation of IoC-container with everything you need.
\ No newline at end of file
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..7431c82
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,21 @@
+dishka
+=============================================
+
+.. toctree::
+   :caption: Contents:
+
+   quickstart
+   di_intro
+   concepts
+   requirements/technical
+   provider/index
+   container/index
+   integrations/index
+
+.. toctree::
+    :hidden:
+    :caption: Project Links
+
+    GitHub <https://github.com/reagento/dishka>
+    PyPI <https://pypi.org/project/dishka>
+    Chat <https://t.me/reagento_ru>
diff --git a/docs/integrations/index.rst b/docs/integrations/index.rst
new file mode 100644
index 0000000..f188ede
--- /dev/null
+++ b/docs/integrations/index.rst
@@ -0,0 +1,6 @@
+Using with frameworks
+*******************************
+
+For several frameworks library contains helper functions so you don't need to constrol scopes yourself, but just annotate handler/view functions and change application startup code
+
+.. toctree::
diff --git a/docs/provider/alias.rst b/docs/provider/alias.rst
new file mode 100644
index 0000000..4cf1d22
--- /dev/null
+++ b/docs/provider/alias.rst
@@ -0,0 +1,17 @@
+.. _alias:
+
+@alias
+****************
+
+``alias`` is used to allow retrieving of the same object by different type hints. E.g. you have configure how to provide ``A`` object and want to use it as AProtocol: ``container.get(A)==container.get(AProtocol)``.
+
+.. code-block:: python
+
+    class MyProvider(Provider):
+        @provide(scope=Scope.REQUEST)
+        def get_a(self) -> A:
+            return A()
+
+        a_proto = alias(source=A, provides=AProtocol)
+
+Additionally, alias has own setting for caching: it caches by default regardless if source is cached. You can disable it providing ``cache=False`` argument.
\ No newline at end of file
diff --git a/docs/provider/decorate.rst b/docs/provider/decorate.rst
new file mode 100644
index 0000000..3b6614f
--- /dev/null
+++ b/docs/provider/decorate.rst
@@ -0,0 +1,28 @@
+.. _decorate:
+
+@decorate
+*********************
+
+``decorate`` is used to modify or wrap an object which is already configured in another ``Provider``.
+
+If you want to apply decorator pattern and do not want to alter existing provide method, then it is a place for ``decorate``. It will construct object using earlie defined provider and then pass it to your decorator before returning from the container.
+
+.. code-block:: python
+
+    class MyProvider(Provider):
+        @decorate
+        def decorate_a(self, a: A) -> A:
+            return ADecorator(a)
+
+Such decorator function can also have additional parameters.
+
+.. code-block:: python
+
+    class MyProvider(Provider):
+        @decorate
+        def decorate_a(self, a: A, b: B) -> A:
+            return ADecorator(a)
+
+The limitation is that you cannot use ``decorate`` in the same provider as you declare factory or alias for dependency. But you won't need it because you can update the factory code.
+
+The idea of ``decorate`` is to postprocess dependencies provided by some external source, when you combine multiple ``Provider`` objects into one container
\ No newline at end of file
diff --git a/docs/provider/index.rst b/docs/provider/index.rst
new file mode 100644
index 0000000..9b97bcc
--- /dev/null
+++ b/docs/provider/index.rst
@@ -0,0 +1,38 @@
+Provider
+****************
+
+**Provider** is an object which members are used to construct dependencies. Providers are needed for container to create dependencies.
+
+To create your own provider you inherit from ``Provider`` class and instantiate it when creating a container:
+
+.. code-block:: python
+
+    class MyProvider(Provider):
+        pass
+
+    with make_container(MyProvider()) as container:
+        pass
+
+You can also set default scope for factories within provider. It will affect only those factories which have no scope set explicitly.
+
+* Inside class:
+
+.. code-block:: python
+
+    class MyProvider(Provider):
+        scope=Scope.APP
+
+* Or when instantiating it. This can be also useful for tests to override provider scope.
+
+.. code-block:: python
+
+    with make_container(MyProvider(scope=Scope.APP)) as container:
+        pass
+
+Though it is a normal object, not all attributes are analyzed by ``Container``, but only those which are marked with special functions:
+
+.. toctree::
+
+   provide
+   alias
+   decorate
diff --git a/docs/provider/provide.rst b/docs/provider/provide.rst
new file mode 100644
index 0000000..e1d9bcf
--- /dev/null
+++ b/docs/provider/provide.rst
@@ -0,0 +1,73 @@
+.. _provide:
+
+@provide
+******************
+
+``provide`` function is used to declare a factory providing a dependency. It can be used with some class or as a method decorator. In second case it can be sync or async method. Also, it can support finalization of dependency if you make it a generator.
+
+If it is used with class analyzes its ``__init__`` typehints to detect its dependencies. If it is used with method, it checks its parameters typehints and a result type. Last one describes what this method is used to create.
+
+``scope`` argument is required to define the lifetime of the created object.
+By default the result is cached within scope. You can disable it providing ``cache=False`` argument.
+
+* For simple case add method and mark it with ``@provide`` decorator.
+
+.. code-block:: python
+
+    class MyProvider(Provider):
+        @provide(scope=Scope.REQUEST)
+        def get_a(self) -> A:
+            return A()
+
+* Want some finalization when exiting the scope? Make that method generator:
+
+.. code-block:: python
+
+    class MyProvider(Provider):
+        @provide(scope=Scope.REQUEST)
+        def get_a(self) -> Iterable[A]:
+            a = A()
+            yield a
+            a.close()
+
+* Do not have any specific logic and just want to create class using its ``__init__``? then add a provider attribute using ``provide`` as function passing that class.
+
+.. code-block:: python
+
+    class MyProvider(Provider):
+        a = provide(A, scope=Scope.REQUEST)
+
+* Want to create a child class instance when parent is requested? add a ``source`` attribute to ``provide`` function with a parent class while passing child as a first parameter
+
+.. code-block:: python
+
+    class MyProvider(Provider):
+        a = provide(source=AChild, scope=Scope.REQUEST, provides=A)
+
+
+* Want to go ``async``? Make provide methods asynchronous. Create async container. Use ``async with`` and await ``get`` calls:
+
+.. code-block:: python
+
+    class MyProvider(Provider):
+       @provide(scope=Scope.APP)
+       async def get_a(self) -> A:
+          return A()
+
+    async with make_async_container(MyProvider()) as container:
+         a = await container.get(A)
+
+* Tired of providing `scope==` for each depedency? Set it inside your `Provider` class and all factories with no scope will use it.
+
+.. code-block:: python
+
+    class MyProvider(Provider):
+       scope=Scope.APP
+
+       @provide  # uses provider scope
+       async def get_a(self) -> A:
+          return A()
+
+       @provide(scope=Scope.REQUEST)  # has own scope
+       async def get_b(self) -> B:
+          return B()
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
new file mode 100644
index 0000000..4e44962
--- /dev/null
+++ b/docs/quickstart.rst
@@ -0,0 +1,74 @@
+Quickstart
+********************
+
+1. Install dishka
+
+.. code-block:: shell
+
+    pip install dishka
+
+
+2. Create Provider subclass.
+
+.. code-block:: python
+
+    from dishka import Provider
+    class MyProvider(Provider):
+       ...
+
+3. Mark methods which actually create dependencies with `@provide` decorator with carefully arranged scopes. Do not forget to place correct typehints for parameters and result.
+Here we describe how to create instances of A and B classes, where B class requires itself an instance of A.
+
+.. code-block:: python
+
+    from dishka import provide, Provider, Scope
+    class MyProvider(Provider):
+       @provide(scope=Scope.APP)
+       def get_a(self) -> A:
+          return A()
+
+       @provide(scope=Scope.REQUEST)
+       def get_b(self, a: A) -> B:
+          return B(a)
+
+4. Create Container instance passing providers, and step into `APP` scope. Or deeper if you need.
+
+.. code-block:: python
+
+    from dishka import make_container
+    with make_container(MyProvider()) as container:  # enter Scope.APP
+         with container() as request_container:   # enter Scope.REQUEST
+              ...
+
+
+5. Call `get` to get dependency and use context manager to get deeper through scopes
+
+.. code-block:: python
+
+    from dishka import make_container
+    with make_container(MyProvider()) as container:
+         a = container.get(A)  # `A` has Scope.APP, so it is accessible here
+         with container() as request_container:
+              b = request_container.get(B)  # `B` has Scope.REQUEST
+              a = request_container.get(A)  # `A` is accessible here too
+
+
+6. If you are using supported framework add decorators and middleware for it.
+
+
+.. code-block:: python
+
+    from dishka.integrations.fastapi import (
+        Depends, inject, DishkaApp,
+    )
+
+    @router.get("/")
+    @inject
+    async def index(a: Annotated[A, Depends()]) -> str:
+        ...
+
+    ...
+    app = DishkaApp(
+        providers=[MyProvider()],
+        app=app,
+    )
\ No newline at end of file
diff --git a/docs/technical_requirements.md b/docs/requirements/technical.rst
similarity index 80%
rename from docs/technical_requirements.md
rename to docs/requirements/technical.rst
index d95d4ae..cedd6b9 100644
--- a/docs/technical_requirements.md
+++ b/docs/requirements/technical.rst
@@ -1,6 +1,10 @@
-## Technical requirements for IoC-container
+.. _technical-requirements:
 
-#### 1. Scopes
+Technical requirements
+*********************************************
+
+1. Scopes
+================
 
 1. Library should support various number of scopes
 2. All dependencies are attached to scopes before any of them can be created
@@ -12,44 +16,51 @@
 8. If the same dependency is requested more than one time within the scope the same instance is returned. Cache is not shared between concurrent instances of same scope
 9. Dependency can require other dependencies of the same or previous scope.
 
-#### 2. Concurrency
+2. Concurrency
+================
 
 1. Containers should be allowed to use with multithreading or asyncio. Not required to support both within same object. 
 2. Dependency creation using async functions should be supported if container is configured to run in asyncio 
 3. Concurrent entrance of scopes must not break requirement of single instance of dependency. Type of concurrency model can be configured when creating container 
 4. User of container may be allowed to switch synchronization on or off for performance tuning
 
-#### 3. Clean dependencies
+3. Clean dependencies
+========================
 
 1. Usage of container must not require modification of objects we are creating
-2. Container must not require to be global variable. 
-4. Container can require code changes on the borders of scopes (e.g. application start, middlewares, request handlers)
+2. Container must not require to be global variable.
+3. Container can require code changes on the borders of scopes (e.g. application start, middlewares, request handlers)
 
-#### 4. Lifecycle
+4. Lifecycle
+================
 
 1. Dependencies which require some cleanup must be cleaned up on the scope exit
 2. Dependencies which do not require cleanup should somehow be supported
 
-#### 5. Context data
+5. Context data
+================
 
 1. It should be allowed to pass some data when entering the scope
 2. Context data must be accessible when creating dependencies
 
-#### 6. Modularity
+6. Modularity
+================
 
 1. There can be multiple containers within same code base for different purposes
 2. There must be a way to assemble a container from some reusable parts.
 3. Assembling of container should be done in runtime in local scope
 
-#### 7. Usability
+7. Usability
+================
 
-1. There should be a way to create dependency based on its `__init__`
+1. There should be a way to create dependency based on its ``__init__``
 2. When creating a dependency there should be a way to decide which subtype is used and request only its dependencies
 3. There should be a way to reuse same object for multiple requested types
 4. There should be a way to decorate dependency just adding new providers
 
-#### 8. Integration
+8. Integration
+================
 
 1. Additional helpers should be provided for some popular frameworks. E.g: flask, fastapi, aiogram, celery, apscheduler
 2. These helpers should be optional
-3. Additional integrations should be done without changing library code
\ No newline at end of file
+3. Additional integrations should be done without changing library code
diff --git a/examples/aiohttp_app.py b/examples/aiohttp_app.py
new file mode 100644
index 0000000..1b3641b
--- /dev/null
+++ b/examples/aiohttp_app.py
@@ -0,0 +1,39 @@
+from typing import Annotated, Protocol
+
+from aiohttp.web import run_app
+from aiohttp.web_app import Application
+from aiohttp.web_response import Response
+from aiohttp.web_routedef import RouteTableDef
+
+from dishka import Provider, Scope, provide
+from dishka.integrations.aiohttp import inject, setup_dishka
+from dishka.integrations.base import Depends
+
+
+class Gateway(Protocol):
+    async def get(self) -> int: ...
+
+
+class MockGateway(Gateway):
+    async def get(self) -> int:
+        return hash(self)
+
+
+class GatewayProvider(Provider):
+    get_gateway = provide(MockGateway, scope=Scope.REQUEST, provides=Gateway)
+
+
+GatewayDepends = Annotated[Gateway, Depends()]
+router = RouteTableDef()
+
+
+@router.get('/')
+@inject
+async def endpoint(request: str, gateway: GatewayDepends) -> Response:
+    data = await gateway.get()
+    return Response(text=f'gateway data: {data}')
+
+app = Application()
+app.add_routes(router)
+setup_dishka(providers=[GatewayProvider()], app=app)
+run_app(app)
diff --git a/examples/real_world/myapp/ioc.py b/examples/real_world/myapp/ioc.py
index 12bf70b..8f24a0a 100644
--- a/examples/real_world/myapp/ioc.py
+++ b/examples/real_world/myapp/ioc.py
@@ -18,14 +18,12 @@ from .use_cases import (
 
 # app dependency logic
 class AdaptersProvider(Provider):
-    users = provide(
-        FakeUserGateway, scope=Scope.REQUEST, provides=UserGateway,
-    )
-    products = provide(
-        FakeProductGateway, scope=Scope.REQUEST, provides=ProductGateway,
-    )
-
-    @provide(scope=Scope.REQUEST)
+    scope = Scope.REQUEST
+
+    users = provide(FakeUserGateway, provides=UserGateway)
+    products = provide(FakeProductGateway, provides=ProductGateway)
+
+    @provide
     def connection(self) -> Iterable[FakeDbConnection]:
         uow = FakeDbConnection()
         yield uow
@@ -39,4 +37,6 @@ class AdaptersProvider(Provider):
 
 
 class InteractorProvider(Provider):
-    product = provide(AddProductsInteractor, scope=Scope.REQUEST)
+    scope = Scope.REQUEST
+
+    product = provide(AddProductsInteractor)
diff --git a/examples/starlette_app.py b/examples/starlette_app.py
new file mode 100644
index 0000000..c9b890b
--- /dev/null
+++ b/examples/starlette_app.py
@@ -0,0 +1,67 @@
+import logging
+from abc import abstractmethod
+from typing import Annotated, Protocol
+
+import uvicorn
+from starlette.applications import Starlette
+from starlette.requests import Request
+from starlette.responses import PlainTextResponse
+from starlette.routing import Route
+
+from dishka import Provider, Scope, provide
+from dishka.integrations.starlette import Depends, inject, DishkaApp
+
+
+# app core
+class DbGateway(Protocol):
+    @abstractmethod
+    def get(self) -> str:
+        raise NotImplementedError
+
+
+class FakeDbGateway(DbGateway):
+    def get(self) -> str:
+        return "Hello from Starlette"
+
+
+class Interactor:
+    def __init__(self, db: DbGateway):
+        self.db = db
+
+    def __call__(self) -> str:
+        return self.db.get()
+
+
+# app dependency logic
+class AdaptersProvider(Provider):
+    @provide(scope=Scope.REQUEST)
+    def get_db(self) -> DbGateway:
+        return FakeDbGateway()
+
+
+class InteractorProvider(Provider):
+    i1 = provide(Interactor, scope=Scope.REQUEST)
+
+
+# presentation layer
+@inject
+async def index(request: Request, *, interactor: Annotated[Interactor, Depends()]) -> PlainTextResponse:
+    result = interactor()
+    return PlainTextResponse(result)
+
+
+def create_app():
+    logging.basicConfig(
+        level=logging.WARNING,
+        format="%(asctime)s  %(process)-7s %(module)-20s %(message)s",
+    )
+
+    app = Starlette(routes=[Route("/", endpoint=index, methods=["GET"])])
+    return DishkaApp(
+        providers=[AdaptersProvider(), InteractorProvider()],
+        app=app,
+    )
+
+
+if __name__ == "__main__":
+    uvicorn.run(create_app(), host="0.0.0.0", port=8000, lifespan="on")
diff --git a/make.bat b/make.bat
new file mode 100644
index 0000000..02a0ce3
--- /dev/null
+++ b/make.bat
@@ -0,0 +1,35 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+	set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=docs
+set BUILDDIR=docs-build
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+	echo.
+	echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+	echo.installed, then set the SPHINXBUILD environment variable to point
+	echo.to the full path of the 'sphinx-build' executable. Alternatively you
+	echo.may add the Sphinx directory to PATH.
+	echo.
+	echo.If you don't have Sphinx installed, grab it from
+	echo.https://www.sphinx-doc.org/
+	exit /b 1
+)
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+
+:end
+popd
diff --git a/pyproject.toml b/pyproject.toml
index 2000c17..4a06966 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,7 +10,7 @@ where = ["src"]
 
 [project]
 name = "dishka"
-version = "0.2.0"
+version = "0.1"
 readme = "README.md"
 authors = [
     { name = "Andrey Tikhonov", email = "17@itishka.org" },
diff --git a/requirements/aiohttp-393.txt b/requirements/aiohttp-393.txt
new file mode 100644
index 0000000..91f3e76
--- /dev/null
+++ b/requirements/aiohttp-393.txt
@@ -0,0 +1,2 @@
+-r test.txt
+aiohttp==3.9.3
diff --git a/requirements/fastapi-0092.txt b/requirements/fastapi-0092.txt
deleted file mode 100644
index 42f7e29..0000000
--- a/requirements/fastapi-0092.txt
+++ /dev/null
@@ -1,2 +0,0 @@
--r asgi.txt
-fastapi==0.92.0
\ No newline at end of file
diff --git a/requirements/fastapi-0096.txt b/requirements/fastapi-0096.txt
new file mode 100644
index 0000000..a3c29ca
--- /dev/null
+++ b/requirements/fastapi-0096.txt
@@ -0,0 +1,2 @@
+-r asgi.txt
+fastapi==0.96.0
\ No newline at end of file
diff --git a/requirements/starlette-0270.txt b/requirements/starlette-0270.txt
new file mode 100644
index 0000000..a87c5d3
--- /dev/null
+++ b/requirements/starlette-0270.txt
@@ -0,0 +1,2 @@
+-r asgi.txt
+starlette==0.27.0
\ No newline at end of file
diff --git a/requirements_doc.txt b/requirements_doc.txt
new file mode 100644
index 0000000..93945bf
--- /dev/null
+++ b/requirements_doc.txt
@@ -0,0 +1,3 @@
+sphinx==7.2.*
+sphinx-autodocgen==1.3
+furo==2024.1.*
diff --git a/src/dishka/_adaptix/common.py b/src/dishka/_adaptix/common.py
new file mode 100644
index 0000000..023a3fc
--- /dev/null
+++ b/src/dishka/_adaptix/common.py
@@ -0,0 +1,22 @@
+from typing import TYPE_CHECKING, Any, Callable, Tuple, Type, TypeVar, Union
+
+K_contra = TypeVar('K_contra', contravariant=True)
+V_co = TypeVar('V_co', covariant=True)
+T = TypeVar('T')
+
+Loader = Callable[[Any], V_co]
+Dumper = Callable[[K_contra], Any]
+Converter = Callable[..., Any]
+Coercer = Callable[[Any], Any]
+
+TypeHint = Any
+
+VarTuple = Tuple[T, ...]
+
+Catchable = Union[Type[BaseException], VarTuple[Type[BaseException]]]
+
+# https://github.com/python/typing/issues/684#issuecomment-548203158
+if TYPE_CHECKING:
+    EllipsisType = ellipsis  # pylint: disable=undefined-variable  # noqa: F821
+else:
+    EllipsisType = type(Ellipsis)
diff --git a/src/dishka/_adaptix/feature_requirement.py b/src/dishka/_adaptix/feature_requirement.py
new file mode 100644
index 0000000..bdb1a1d
--- /dev/null
+++ b/src/dishka/_adaptix/feature_requirement.py
@@ -0,0 +1,178 @@
+import importlib.metadata
+import itertools
+import re
+import sys
+from abc import ABC, abstractmethod
+from typing import Any, Iterable
+
+from .common import VarTuple
+
+
+def _true():
+    return True
+
+
+def _false():
+    return False
+
+
+class Requirement(ABC):
+    __slots__ = ('is_meet', '__bool__', '__dict__')
+
+    def __init__(self):
+        self.is_meet = self._evaluate()
+        self.__bool__ = _true if self.is_meet else _false
+
+    @abstractmethod
+    def _evaluate(self) -> bool:
+        ...
+
+    @property
+    @abstractmethod
+    def fail_reason(self) -> str:
+        ...
+
+
+class PythonVersionRequirement(Requirement):
+    def __init__(self, min_version: VarTuple[int]):
+        self.min_version = min_version
+        super().__init__()
+
+    def _evaluate(self) -> bool:
+        return sys.version_info >= self.min_version
+
+    @property
+    def fail_reason(self) -> str:
+        return f'Python >= {".".join(map(str, self.min_version))} is required'
+
+
+class DistributionRequirement(Requirement):
+    def __init__(self, distribution_name: str):
+        self.distribution_name = distribution_name
+        super().__init__()
+
+    def _evaluate(self) -> bool:
+        try:
+            importlib.metadata.distribution(self.distribution_name)
+        except importlib.metadata.PackageNotFoundError:
+            return False
+        return True
+
+    @property
+    def fail_reason(self) -> str:
+        return f'Installed distribution {self.distribution_name!r} is required'
+
+
+class DistributionVersionRequirement(DistributionRequirement):
+    # Pattern from PEP 440
+    _VERSION_PATTERN = r"""
+        v?
+        (?:
+            (?:(?P<epoch>[0-9]+)!)?                           # epoch
+            (?P<release>[0-9]+(?:\.[0-9]+)*)                  # release segment
+            (?P<pre>                                          # pre-release
+                [-_\.]?
+                (?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
+                [-_\.]?
+                (?P<pre_n>[0-9]+)?
+            )?
+            (?P<post>                                         # post release
+                (?:-(?P<post_n1>[0-9]+))
+                |
+                (?:
+                    [-_\.]?
+                    (?P<post_l>post|rev|r)
+                    [-_\.]?
+                    (?P<post_n2>[0-9]+)?
+                )
+            )?
+            (?P<dev>                                          # dev release
+                [-_\.]?
+                (?P<dev_l>dev)
+                [-_\.]?
+                (?P<dev_n>[0-9]+)?
+            )?
+        )
+        (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
+    """
+
+    _version_regex = re.compile(
+        r"^\s*" + _VERSION_PATTERN + r"\s*$",
+        re.VERBOSE | re.IGNORECASE,
+    )
+
+    def __init__(self, distribution_name: str, min_version: str):
+        self.min_version = min_version
+        super().__init__(distribution_name)
+
+    def _evaluate(self) -> bool:
+        try:
+            distribution = importlib.metadata.distribution(self.distribution_name)
+        except importlib.metadata.PackageNotFoundError:
+            return False
+        return (
+            self._make_comparator(distribution.version)
+            >=
+            self._make_comparator(self.min_version)
+        )
+
+    def _remove_trailing_zeros(self, data: Iterable[int]) -> VarTuple[int]:
+        return tuple(reversed(tuple(itertools.dropwhile(lambda x: x == 0, reversed(tuple(data))))))
+
+    def _make_comparator(self, version: str) -> VarTuple[Any]:
+        match = self._version_regex.match(version)
+        if match is None:
+            raise ValueError
+        return (
+            match.group('epoch') or 0,
+            self._remove_trailing_zeros(int(i) for i in match.group('release').split('.')),
+            match.group('pre') is not None,
+            match.group('dev') is not None,
+        )
+
+    @property
+    def fail_reason(self) -> str:
+        return f'Installed distribution {self.distribution_name!r} of {self.min_version!r} is required'
+
+
+class PythonImplementationRequirement(Requirement):
+    def __init__(self, implementation_name: str):
+        self.implementation_name = implementation_name
+        super().__init__()
+
+    def _evaluate(self) -> bool:
+        return sys.implementation.name == self.implementation_name
+
+    @property
+    def fail_reason(self) -> str:
+        return f'{self.implementation_name} is required'
+
+
+HAS_PY_39 = PythonVersionRequirement((3, 9))
+HAS_ANNOTATED = HAS_PY_39
+HAS_STD_CLASSES_GENERICS = HAS_PY_39
+
+HAS_PY_310 = PythonVersionRequirement((3, 10))
+HAS_TYPE_UNION_OP = HAS_PY_310
+HAS_TYPE_GUARD = HAS_PY_310
+HAS_TYPE_ALIAS = HAS_PY_310
+HAS_PARAM_SPEC = HAS_PY_310
+
+HAS_PY_311 = PythonVersionRequirement((3, 11))
+HAS_NATIVE_EXC_GROUP = HAS_PY_311
+HAS_TYPED_DICT_REQUIRED = HAS_PY_311
+HAS_SELF_TYPE = HAS_PY_311
+HAS_TV_TUPLE = HAS_PY_311
+HAS_UNPACK = HAS_PY_311
+
+HAS_PY_312 = PythonVersionRequirement((3, 12))
+HAS_TV_SYNTAX = HAS_PY_312
+
+HAS_SUPPORTED_ATTRS_PKG = DistributionVersionRequirement('attrs', '21.3.0')
+HAS_ATTRS_PKG = DistributionRequirement('attrs')
+
+HAS_SUPPORTED_SQLALCHEMY_PKG = DistributionVersionRequirement('sqlalchemy', '2.0.0')
+HAS_SQLALCHEMY_PKG = DistributionRequirement('sqlalchemy')
+
+IS_CPYTHON = PythonImplementationRequirement('cpython')
+IS_PYPY = PythonImplementationRequirement('pypy')
diff --git a/src/dishka/_adaptix/type_tools/__init__.py b/src/dishka/_adaptix/type_tools/__init__.py
new file mode 100644
index 0000000..63df61d
--- /dev/null
+++ b/src/dishka/_adaptix/type_tools/__init__.py
@@ -0,0 +1,26 @@
+from .basic_utils import (
+    create_union,
+    get_all_type_hints,
+    is_bare_generic,
+    is_generic,
+    is_generic_class,
+    is_named_tuple_class,
+    is_new_type,
+    is_parametrized,
+    is_protocol,
+    is_subclass_soft,
+    is_typed_dict_class,
+    is_user_defined_generic,
+    strip_alias,
+)
+from .norm_utils import is_class_var, strip_tags
+from .normalize_type import (
+    AnyNormTypeVarLike,
+    BaseNormType,
+    NormParamSpecMarker,
+    NormTV,
+    NormTVTuple,
+    NormTypeAlias,
+    make_norm_type,
+    normalize_type,
+)
diff --git a/src/dishka/_adaptix/type_tools/basic_utils.py b/src/dishka/_adaptix/type_tools/basic_utils.py
new file mode 100644
index 0000000..877e3ef
--- /dev/null
+++ b/src/dishka/_adaptix/type_tools/basic_utils.py
@@ -0,0 +1,180 @@
+import types
+import typing
+from typing import (
+    Any,
+    Dict,
+    ForwardRef,
+    Generic,
+    Iterable,
+    Protocol,
+    TypedDict,
+    TypeVar,
+    Union,
+    get_args,
+    get_origin,
+    get_type_hints,
+)
+
+from ..common import TypeHint, VarTuple
+from ..feature_requirement import HAS_ANNOTATED, HAS_PY_39, HAS_PY_312, HAS_STD_CLASSES_GENERICS
+from .constants import BUILTIN_ORIGIN_TO_TYPEVARS
+
+TYPED_DICT_MCS = type(types.new_class("_TypedDictSample", (TypedDict,), {}))
+
+
+def strip_alias(type_hint: TypeHint) -> TypeHint:
+    origin = get_origin(type_hint)
+    return type_hint if origin is None else origin
+
+
+def is_subclass_soft(cls, classinfo) -> bool:
+    """Acts like builtin issubclass,
+     but returns False instead of rising TypeError
+    """
+    try:
+        return issubclass(cls, classinfo)
+    except TypeError:
+        return False
+
+
+def has_attrs(obj, attrs: Iterable[str]) -> bool:
+    return all(
+        hasattr(obj, attr_name)
+        for attr_name in attrs
+    )
+
+
+def is_new_type(tp) -> bool:
+    return has_attrs(tp, ['__supertype__', '__name__'])
+
+
+def is_typed_dict_class(tp) -> bool:
+    return isinstance(tp, TYPED_DICT_MCS)
+
+
+NAMED_TUPLE_METHODS = ('_fields', '_field_defaults', '_make', '_replace', '_asdict')
+
+
+def is_named_tuple_class(tp) -> bool:
+    return (
+        is_subclass_soft(tp, tuple)
+        and
+        has_attrs(tp, NAMED_TUPLE_METHODS)
+    )
+
+
+def is_protocol(tp):
+    if not isinstance(tp, type):
+        return False
+
+    return Protocol in tp.__bases__
+
+
+def create_union(args: tuple):
+    return Union[args]
+
+
+if HAS_ANNOTATED:
+    def get_all_type_hints(obj, globalns=None, localns=None):
+        return get_type_hints(obj, globalns, localns, include_extras=True)
+else:
+    get_all_type_hints = get_type_hints
+
+
+def is_parametrized(tp: TypeHint) -> bool:
+    return bool(get_args(tp))
+
+
+def get_type_vars(tp: TypeHint) -> VarTuple[TypeVar]:
+    return getattr(tp, '__parameters__', ())
+
+
+if HAS_PY_312:
+    def is_user_defined_generic(tp: TypeHint) -> bool:
+        # pylint: disable=no-member
+        return (
+            bool(get_type_vars(tp))
+            and (
+                is_subclass_soft(strip_alias(tp), Generic)
+                or isinstance(tp, typing.TypeAliasType)  # type: ignore[attr-defined]
+            )
+        )
+else:
+    def is_user_defined_generic(tp: TypeHint) -> bool:
+        return (
+            bool(get_type_vars(tp))
+            and is_subclass_soft(strip_alias(tp), Generic)
+        )
+
+
+def is_generic(tp: TypeHint) -> bool:
+    """Check if the type could be parameterized"""
+    return (
+        bool(get_type_vars(tp))
+        or (
+            strip_alias(tp) in BUILTIN_ORIGIN_TO_TYPEVARS
+            and tp != type
+            and not is_parametrized(tp)
+            and (
+                bool(HAS_STD_CLASSES_GENERICS) or not isinstance(tp, type)
+            )
+        )
+        or (
+            bool(HAS_ANNOTATED)
+            and get_origin(tp) == typing.Annotated
+            and is_generic(tp.__origin__)
+        )
+    )
+
+
+def is_bare_generic(tp: TypeHint) -> bool:
+    """Check if the type could be parameterized, excluding type aliases (list[T] etc.)"""
+    return (
+        (
+            is_generic(strip_alias(tp))
+            # for 3.8 and List (list is not generic)
+            or is_generic(tp)
+            # at 3.8 list is bare_generic but not generic
+            # (this function only needs to create predicate)
+            or tp in BUILTIN_ORIGIN_TO_TYPEVARS
+        )
+        and not is_parametrized(tp)
+    )
+
+
+def is_generic_class(cls: type) -> bool:
+    """Check if the class represents a generic type.
+    This function is faster than ``.is_generic()``, but it is limited to testing only classes
+    """
+    return (
+        cls in BUILTIN_ORIGIN_TO_TYPEVARS
+        or (
+            issubclass(cls, Generic)  # type: ignore[arg-type]
+            and bool(cls.__parameters__)  # type: ignore[attr-defined]
+        )
+    )
+
+
+def get_type_vars_of_parametrized(tp: TypeHint) -> VarTuple[TypeVar]:
+    try:
+        params = tp.__parameters__
+    except AttributeError:
+        return ()
+
+    if isinstance(tp, type):
+        if HAS_STD_CLASSES_GENERICS and isinstance(tp, types.GenericAlias):
+            return params
+        return ()
+    if get_origin(tp) is not None and get_args(tp) == ():
+        return ()
+    return params
+
+
+if HAS_PY_39:
+    def eval_forward_ref(namespace: Dict[str, Any], forward_ref: ForwardRef):
+        # pylint: disable=protected-access
+        return forward_ref._evaluate(namespace, None, frozenset())
+else:
+    def eval_forward_ref(namespace: Dict[str, Any], forward_ref: ForwardRef):
+        # pylint: disable=protected-access
+        return forward_ref._evaluate(namespace, None)  # type: ignore[call-arg]
diff --git a/src/dishka/_adaptix/type_tools/constants.py b/src/dishka/_adaptix/type_tools/constants.py
new file mode 100644
index 0000000..f05a630
--- /dev/null
+++ b/src/dishka/_adaptix/type_tools/constants.py
@@ -0,0 +1,35 @@
+import collections
+import concurrent.futures
+import queue
+import re
+from os import PathLike
+from typing import Mapping, TypeVar
+
+from ..common import VarTuple
+
+_AnyStrT = TypeVar('_AnyStrT', str, bytes)
+_T1 = TypeVar('_T1')
+_T2 = TypeVar('_T2')
+_T1_co = TypeVar('_T1_co', covariant=True)
+_AnyStr_co = TypeVar("_AnyStr_co", str, bytes, covariant=True)
+
+BUILTIN_ORIGIN_TO_TYPEVARS: Mapping[type, VarTuple[TypeVar]] = {
+    re.Pattern: (_AnyStrT, ),  # type: ignore[dict-item]
+    re.Match: (_AnyStrT, ),  # type: ignore[dict-item]
+    PathLike: (_AnyStr_co, ),  # type: ignore[dict-item]
+    type: (_T1,),  # type: ignore[dict-item]
+    list: (_T1,),  # type: ignore[dict-item]
+    set: (_T1,),  # type: ignore[dict-item]
+    frozenset: (_T1_co, ),  # type: ignore[dict-item]
+    collections.Counter: (_T1,),  # type: ignore[dict-item]
+    collections.deque: (_T1,),  # type: ignore[dict-item]
+    dict: (_T1, _T2),  # type: ignore[dict-item]
+    collections.defaultdict: (_T1, _T2),  # type: ignore[dict-item]
+    collections.OrderedDict: (_T1, _T2),  # type: ignore[dict-item]
+    collections.ChainMap: (_T1, _T2),  # type: ignore[dict-item]
+    queue.Queue: (_T1, ),  # type: ignore[dict-item]
+    queue.PriorityQueue: (_T1, ),  # type: ignore[dict-item]
+    queue.LifoQueue: (_T1, ),  # type: ignore[dict-item]
+    queue.SimpleQueue: (_T1, ),  # type: ignore[dict-item]
+    concurrent.futures.Future: (_T1, ),  # type: ignore[dict-item]
+}
diff --git a/src/dishka/_adaptix/type_tools/generic_resolver.py b/src/dishka/_adaptix/type_tools/generic_resolver.py
new file mode 100644
index 0000000..0a07eb5
--- /dev/null
+++ b/src/dishka/_adaptix/type_tools/generic_resolver.py
@@ -0,0 +1,105 @@
+import typing
+from dataclasses import dataclass, replace
+from itertools import chain
+from typing import Callable, Collection, Dict, Generic, Hashable, Mapping, TypeVar, get_args
+
+from ..common import TypeHint
+from ..feature_requirement import HAS_TV_TUPLE, HAS_UNPACK
+from .basic_utils import get_type_vars, get_type_vars_of_parametrized, is_generic, is_parametrized, strip_alias
+from .implicit_params import fill_implicit_params
+from .normalize_type import normalize_type
+
+M = TypeVar('M')
+K = TypeVar('K', bound=Hashable)
+
+
+@dataclass
+class MembersStorage(Generic[K, M]):
+    members: Mapping[K, TypeHint]
+    overriden: Collection[K]
+    meta: M
+
+
+class GenericResolver(Generic[K, M]):
+    def __init__(self, members_getter: Callable[[TypeHint], MembersStorage[K, M]]):
+        self._raw_members_getter = members_getter
+
+    def get_resolved_members(self, tp: TypeHint) -> MembersStorage[K, M]:
+        if is_parametrized(tp):
+            return self._get_members_of_parametrized_generic(tp)
+        if is_generic(tp):
+            return self._get_members_of_parametrized_generic(fill_implicit_params(tp))
+        return self._get_members_by_parents(tp)
+
+    def _get_members_of_parametrized_generic(self, parametrized_generic) -> MembersStorage[K, M]:
+        origin = strip_alias(parametrized_generic)
+        members_storage = self._get_members_by_parents(origin)
+        type_var_to_actual = self._get_type_var_to_actual(
+            get_type_vars(origin),
+            self._unpack_args(get_args(parametrized_generic)),
+        )
+        return replace(
+            members_storage,
+            members={
+                key: self._parametrize_by_dict(type_var_to_actual, tp)
+                for key, tp in members_storage.members.items()
+            }
+        )
+
+    def _unpack_args(self, args):
+        if HAS_UNPACK and any(strip_alias(arg) == typing.Unpack for arg in args):
+            return tuple(arg.source for arg in normalize_type(tuple[args]).args)
+        return args
+
+    def _get_type_var_to_actual(self, type_vars, args):
+        result = {}
+        idx = 0
+        for tv in type_vars:
+            if HAS_TV_TUPLE and isinstance(tv, typing.TypeVarTuple):
+                tuple_len = len(args) - len(type_vars) + 1
+                result[tv] = args[idx:idx + tuple_len]
+                idx += tuple_len
+            else:
+                result[tv] = (args[idx], )
+                idx += 1
+
+        return result
+
+    def _parametrize_by_dict(self, type_var_to_actual, tp: TypeHint) -> TypeHint:
+        if tp in type_var_to_actual:
+            return type_var_to_actual[tp][0]
+
+        params = get_type_vars_of_parametrized(tp)
+        if not params:
+            return tp
+        return tp[tuple(chain.from_iterable(type_var_to_actual[type_var] for type_var in params))]
+
+    def _get_members_by_parents(self, tp) -> MembersStorage[K, M]:
+        members_storage = self._raw_members_getter(tp)
+        if not any(
+            get_type_vars_of_parametrized(tp) or isinstance(tp, TypeVar)
+            for tp in members_storage.members.values()
+        ):
+            return members_storage
+        if not hasattr(tp, '__orig_bases__'):
+            return members_storage
+
+        bases_members: Dict[K, TypeHint] = {}
+        for base in reversed(tp.__orig_bases__):
+            bases_members.update(self.get_resolved_members(base).members)
+
+        return replace(
+            members_storage,
+            members={
+                key: (
+                    bases_members[key]
+                    if (
+                        key in bases_members
+                        and key not in members_storage.overriden
+                        and (is_generic(value) or isinstance(value, TypeVar))
+                    )
+                    else value
+                )
+                for key, value in members_storage.members.items()
+            },
+        )
diff --git a/src/dishka/_adaptix/type_tools/implicit_params.py b/src/dishka/_adaptix/type_tools/implicit_params.py
new file mode 100644
index 0000000..de133c1
--- /dev/null
+++ b/src/dishka/_adaptix/type_tools/implicit_params.py
@@ -0,0 +1,49 @@
+import sys
+import typing
+from typing import Any, ForwardRef, Tuple, TypeVar
+
+from ..common import TypeHint, VarTuple
+from ..feature_requirement import HAS_PARAM_SPEC, HAS_TV_TUPLE
+from .basic_utils import create_union, eval_forward_ref, is_user_defined_generic, strip_alias
+from .constants import BUILTIN_ORIGIN_TO_TYPEVARS
+
+
+class ImplicitParamsGetter:
+    def _process_limit_element(self, type_var: TypeVar, tp: TypeHint) -> TypeHint:
+        if isinstance(tp, ForwardRef):
+            return eval_forward_ref(vars(sys.modules[type_var.__module__]), tp)
+        return tp
+
+    def _process_type_var(self, type_var) -> TypeHint:
+        if HAS_PARAM_SPEC and isinstance(type_var, typing.ParamSpec):
+            return ...
+        if HAS_TV_TUPLE and isinstance(type_var, typing.TypeVarTuple):
+            return typing.Unpack[Tuple[Any, ...]]
+        if type_var.__constraints__:
+            return create_union(
+                tuple(
+                    self._process_limit_element(type_var, constraint)
+                    for constraint in type_var.__constraints__
+                )
+            )
+        if type_var.__bound__ is None:
+            return Any
+        return self._process_limit_element(type_var, type_var.__bound__)
+
+    def get_implicit_params(self, origin) -> VarTuple[TypeHint]:
+        if is_user_defined_generic(origin):
+            type_vars = origin.__parameters__
+        else:
+            type_vars = BUILTIN_ORIGIN_TO_TYPEVARS.get(origin, ())
+
+        return tuple(
+            self._process_type_var(type_var)
+            for type_var in type_vars
+        )
+
+
+def fill_implicit_params(tp: TypeHint) -> TypeHint:
+    params = ImplicitParamsGetter().get_implicit_params(strip_alias(tp))
+    if params:
+        return tp[params]
+    raise ValueError(f'Can not derive implicit parameters for {tp}')
diff --git a/src/dishka/_adaptix/type_tools/norm_utils.py b/src/dishka/_adaptix/type_tools/norm_utils.py
new file mode 100644
index 0000000..a23e1b6
--- /dev/null
+++ b/src/dishka/_adaptix/type_tools/norm_utils.py
@@ -0,0 +1,40 @@
+import typing
+from dataclasses import InitVar
+from typing import ClassVar, Final, TypeVar
+
+from ..feature_requirement import HAS_ANNOTATED, HAS_TYPED_DICT_REQUIRED
+from .normalize_type import BaseNormType
+
+_TYPE_TAGS = [Final, ClassVar, InitVar]
+
+if HAS_ANNOTATED:
+    _TYPE_TAGS.append(typing.Annotated)
+
+if HAS_TYPED_DICT_REQUIRED:
+    _TYPE_TAGS.extend([typing.Required, typing.NotRequired])
+
+
+def strip_tags(norm: BaseNormType) -> BaseNormType:
+    """Removes type hints that do not represent a type
+    and that only indicates metadata
+    """
+    if norm.origin in _TYPE_TAGS:
+        return strip_tags(norm.args[0])
+    return norm
+
+
+N = TypeVar('N', bound=BaseNormType)
+
+
+def strip_annotated(value: N) -> N:
+    if HAS_ANNOTATED and isinstance(value, BaseNormType) and value.origin == typing.Annotated:
+        return strip_annotated(value)
+    return value
+
+
+def is_class_var(norm: BaseNormType) -> bool:
+    if norm.origin == ClassVar:
+        return True
+    if norm.origin in _TYPE_TAGS:
+        return is_class_var(norm.args[0])
+    return False
diff --git a/src/dishka/_adaptix/type_tools/normalize_type.py b/src/dishka/_adaptix/type_tools/normalize_type.py
new file mode 100644
index 0000000..cc1f090
--- /dev/null
+++ b/src/dishka/_adaptix/type_tools/normalize_type.py
@@ -0,0 +1,876 @@
+# pylint: disable=inconsistent-return-statements,comparison-with-callable
+import dataclasses
+import sys
+import types
+import typing
+from abc import ABC, abstractmethod
+from collections import abc as c_abc, defaultdict
+from copy import copy
+from dataclasses import InitVar, dataclass
+from enum import Enum, EnumMeta
+from functools import lru_cache, partial
+from typing import (
+    Any,
+    Callable,
+    ClassVar,
+    DefaultDict,
+    Dict,
+    Final,
+    ForwardRef,
+    Hashable,
+    Iterable,
+    List,
+    Literal,
+    NewType,
+    NoReturn,
+    Optional,
+    Sequence,
+    Tuple,
+    Type,
+    TypeVar,
+    Union,
+    get_args,
+    overload,
+)
+
+from ..common import TypeHint, VarTuple
+from ..feature_requirement import (
+    HAS_ANNOTATED,
+    HAS_PARAM_SPEC,
+    HAS_PY_310,
+    HAS_PY_311,
+    HAS_SELF_TYPE,
+    HAS_TV_SYNTAX,
+    HAS_TV_TUPLE,
+    HAS_TYPE_ALIAS,
+    HAS_TYPE_GUARD,
+    HAS_TYPE_UNION_OP,
+    HAS_TYPED_DICT_REQUIRED,
+    HAS_UNPACK,
+)
+from .basic_utils import create_union, eval_forward_ref, is_new_type, is_subclass_soft, strip_alias
+from .implicit_params import ImplicitParamsGetter
+
+
+class BaseNormType(Hashable, ABC):
+    @property
+    @abstractmethod
+    def origin(self) -> Any:
+        ...
+
+    @property
+    @abstractmethod
+    def args(self) -> VarTuple[Any]:
+        ...
+
+    @property
+    @abstractmethod
+    def source(self) -> TypeHint:
+        ...
+
+
+T = TypeVar('T')
+
+
+class _BasicNormType(BaseNormType, ABC):
+    __slots__ = ('_source', '_args')
+
+    def __init__(self, args: VarTuple[Any], *, source: TypeHint):
+        self._source = source
+        self._args = args
+
+    @property
+    def args(self) -> VarTuple[Any]:
+        return self._args
+
+    @property
+    def source(self) -> TypeHint:
+        return self._source
+
+    def __hash__(self):
+        return hash((self.origin, self._args))
+
+    def __eq__(self, other):
+        if isinstance(other, _BasicNormType):
+            return (
+                self.origin == other.origin
+                and
+                self._args == other._args
+            )
+        if isinstance(other, BaseNormType):
+            return False
+        return NotImplemented
+
+    def __repr__(self):
+        args_str = f" {list(self.args)}," if self.args else ""
+        return f'<{type(self).__name__}({self.origin},{args_str} source={self._source})>'
+
+
+class _NormType(_BasicNormType):
+    __slots__ = _BasicNormType.__slots__ + ('_source',)
+
+    def __init__(self, origin: TypeHint, args: VarTuple[Any], *, source: TypeHint):
+        self._origin = origin
+        super().__init__(args, source=source)
+
+    @property
+    def origin(self) -> Any:
+        return self._origin
+
+
+class _UnionNormType(_BasicNormType):
+    def __init__(self, args: VarTuple[Any], *, source: TypeHint):
+        super().__init__(self._order_args(args), source=source)
+
+    @property
+    def origin(self) -> Any:
+        return Union
+
+    # ensure stable order of args during one interpreter session
+    def _make_orderable(self, obj: object) -> str:
+        if isinstance(obj, BaseNormType):
+            return f"{obj.origin} {[self._make_orderable(arg) for arg in obj.args]}"
+        return str(obj)
+
+    def _order_args(self, args: VarTuple[BaseNormType]) -> VarTuple[BaseNormType]:
+        args_list = list(args)
+        args_list.sort(key=self._make_orderable)
+        return tuple(args_list)
+
+
+def _type_and_value_iter(args):
+    return [(type(arg), arg) for arg in args]
+
+
+LiteralArg = Union[str, int, bytes, Enum]
+
+
+class _LiteralNormType(_BasicNormType):
+    def __init__(self, args: VarTuple[Any], *, source: TypeHint):
+        super().__init__(self._order_args(args), source=source)
+
+    @property
+    def origin(self) -> Any:
+        return Literal
+
+    # ensure stable order of args during one interpreter session
+    def _make_orderable(self, obj: LiteralArg) -> str:
+        return f"{type(obj)}{obj.name}" if isinstance(obj, Enum) else repr(obj)
+
+    def _order_args(self, args: VarTuple[LiteralArg]) -> VarTuple[LiteralArg]:
+        args_list = list(args)
+        args_list.sort(key=self._make_orderable)
+        return tuple(args_list)
+
+    def __eq__(self, other):
+        if isinstance(other, _LiteralNormType):
+            return _type_and_value_iter(self._args) == _type_and_value_iter(other._args)
+        if isinstance(other, BaseNormType):
+            return False
+        return NotImplemented
+
+    __hash__ = _BasicNormType.__hash__
+
+
+class _AnnotatedNormType(_BasicNormType):
+    @property
+    def origin(self) -> Any:
+        return typing.Annotated
+
+    __slots__ = _BasicNormType.__slots__ + ('_hash',)
+
+    def __init__(self, args: VarTuple[Hashable], *, source: TypeHint):
+        super().__init__(args, source=source)
+        self._hash = self._calc_hash()
+
+    # calculate hash even if one of Annotated metadata is not hashable
+    def _calc_hash(self) -> int:
+        lst = [self.origin]
+        for arg in self._args:
+            try:
+                arg_hash = hash(arg)
+            except TypeError:
+                pass
+            else:
+                lst.append(arg_hash)
+        return hash(tuple(lst))
+
+    def __hash__(self):
+        return self._hash
+
+
+class Variance(Enum):
+    INVARIANT = 0
+    COVARIANT = 1
+    CONTRAVARIANT = 2
+    INFERRED = 3
+
+    def __repr__(self):
+        return f'{type(self).__name__}.{self.name}'
+
+
+@dataclass
+class Bound:
+    value: BaseNormType
+
+
+@dataclass
+class Constraints:
+    value: VarTuple[BaseNormType]
+
+
+TypeVarLimit = Union[Bound, Constraints]  # pylint: disable=invalid-name
+
+
+class NormTV(BaseNormType):
+    __slots__ = ('_var', '_limit', '_variance', '_source')
+
+    def __init__(self, var: Any, limit: TypeVarLimit, *, source: TypeHint):
+        self._var = var
+        self._source = source
+        self._limit = limit
+
+        if var.__covariant__:
+            self._variance = Variance.COVARIANT
+        if var.__contravariant__:
+            self._variance = Variance.CONTRAVARIANT
+        if getattr(var, '__infer_variance__', False):
+            self._variance = Variance.INFERRED
+        self._variance = Variance.INVARIANT
+
+    @property
+    def origin(self) -> Any:
+        return self._var
+
+    @property
+    def args(self) -> Tuple[()]:
+        return ()
+
+    @property
+    def source(self) -> TypeHint:
+        return self._source
+
+    @property
+    def name(self) -> str:
+        return self._var.__name__
+
+    @property
+    def variance(self) -> Variance:
+        return self._variance
+
+    @property
+    def limit(self) -> TypeVarLimit:
+        return self._limit
+
+    def __repr__(self):
+        return f'<{type(self).__name__}({self._var})>'
+
+    def __hash__(self):
+        return hash(self._var)
+
+    def __eq__(self, other):
+        if isinstance(other, NormTV):
+            return self._var == other._var
+        if isinstance(other, BaseNormType):
+            return False
+        return NotImplemented
+
+
+class NormTVTuple(BaseNormType):
+    __slots__ = ('_var', '_source')
+
+    def __init__(self, var: Any, *, source: TypeHint):
+        self._var = var
+        self._source = source
+
+    @property
+    def origin(self) -> Any:
+        return self._var
+
+    @property
+    def args(self) -> Tuple[()]:
+        return ()
+
+    @property
+    def source(self) -> TypeHint:
+        return self._source
+
+    @property
+    def name(self) -> str:
+        return self._var.__name__
+
+    def __repr__(self):
+        return f'<{type(self).__name__}({self._var})>'
+
+    def __hash__(self):
+        return hash(self._var)
+
+    def __eq__(self, other):
+        if isinstance(other, NormTVTuple):
+            return self._var == other._var
+        if isinstance(other, BaseNormType):
+            return False
+        return NotImplemented
+
+
+class NormParamSpecMarker(BaseNormType, ABC):
+    __slots__ = ('_param_spec', '_source')
+
+    def __init__(self, param_spec: Any, *, source: TypeHint):
+        self._param_spec = param_spec
+        self._source = source
+
+    @property
+    def param_spec(self) -> NormTV:
+        return self._param_spec
+
+    @property
+    def args(self) -> Tuple[()]:
+        return ()
+
+    @property
+    def source(self) -> TypeHint:
+        return self._source
+
+    def __hash__(self):
+        return hash((self.origin, self.param_spec))
+
+    def __eq__(self, other):
+        if isinstance(other, NormParamSpecMarker):
+            return self.origin == other.origin and self._param_spec == other._param_spec
+        if isinstance(other, BaseNormType):
+            return False
+        return NotImplemented
+
+
+class _NormParamSpecArgs(NormParamSpecMarker):
+    @property
+    def origin(self) -> Any:
+        return typing.ParamSpecArgs
+
+
+class _NormParamSpecKwargs(NormParamSpecMarker):
+    @property
+    def origin(self) -> Any:
+        return typing.ParamSpecKwargs
+
+
+AnyNormTypeVarLike = Union[NormTV, NormTVTuple]
+
+
+class NormTypeAlias(BaseNormType):
+    __slots__ = ('_type_alias', '_args', '_norm_type_vars')
+
+    def __init__(self, type_alias, args: VarTuple[BaseNormType], type_vars: VarTuple[AnyNormTypeVarLike]):
+        self._type_alias = type_alias
+        self._args = args
+        self._type_vars = type_vars
+
+    @property
+    def origin(self) -> Any:
+        return self._type_alias
+
+    @property
+    def args(self) -> VarTuple[BaseNormType]:
+        return self._args
+
+    @property
+    def source(self) -> TypeHint:
+        return self._type_alias
+
+    @property
+    def value(self):
+        return self._type_alias.__value__
+
+    @property
+    def module(self):
+        return self._type_alias.__module__
+
+    @property
+    def type_params(self) -> VarTuple[AnyNormTypeVarLike]:
+        return self._type_vars
+
+    def __eq__(self, other):
+        if isinstance(other, NormTypeAlias):
+            return self._type_alias == other._type_alias and self._args == other._args
+        if isinstance(other, BaseNormType):
+            return False
+        return NotImplemented
+
+    def __hash__(self):
+        return hash(self._type_alias)
+
+
+_PARAM_SPEC_MARKER_TYPES = (typing.ParamSpecArgs, typing.ParamSpecKwargs) if HAS_PARAM_SPEC else ()
+
+
+def make_norm_type(  # noqa: CCR001
+    origin: TypeHint,
+    args: VarTuple[Hashable],
+    *,
+    source: TypeHint
+) -> BaseNormType:
+    if origin == Union:
+        if not all(isinstance(arg, BaseNormType) for arg in args):
+            raise TypeError
+        return _UnionNormType(args, source=source)
+    if origin == Literal:
+        if not all(type(arg) in [int, bool, str, bytes] or isinstance(type(arg), EnumMeta) for arg in args):
+            raise TypeError
+        return _LiteralNormType(args, source=source)
+    if HAS_ANNOTATED and origin == typing.Annotated:
+        return _AnnotatedNormType(args, source=source)
+    if isinstance(origin, TypeVar):
+        raise TypeError
+    if HAS_PARAM_SPEC and (
+        isinstance(origin, _PARAM_SPEC_MARKER_TYPES) or isinstance(source, _PARAM_SPEC_MARKER_TYPES)
+    ):
+        raise TypeError
+    return _NormType(origin, args, source=source)
+
+
+NoneType = type(None)
+ANY_NT = _NormType(Any, (), source=Any)
+
+
+def _create_norm_union(args: VarTuple[BaseNormType]) -> BaseNormType:
+    return _UnionNormType(args, source=create_union(tuple(a.source for a in args)))
+
+
+def _dedup(inp: Iterable[T]) -> Iterable[T]:
+    in_set = set()
+    result = []
+    for item in inp:
+        if item not in in_set:
+            result.append(item)
+            in_set.add(item)
+    return result
+
+
+def _create_norm_literal(args: Iterable):
+    dedup_args = tuple(_dedup(args))
+    return _LiteralNormType(
+        dedup_args,
+        source=Literal[dedup_args],
+    )
+
+
+def _replace_source(norm: BaseNormType, *, source: TypeHint) -> BaseNormType:
+    norm_copy = copy(norm)
+    norm_copy._source = source  # type: ignore[attr-defined]
+    return norm_copy
+
+
+def _replace_source_with_union(norm: BaseNormType, sources: list) -> BaseNormType:
+    return _replace_source(
+        norm=norm,
+        source=create_union(tuple(sources)),
+    )
+
+
+NormAspect = Callable[['TypeNormalizer', Any, Any, tuple], Optional[BaseNormType]]
+
+
+class AspectStorage(List[str]):
+    @overload
+    def add(self, *, condition: object = True) -> Callable[[NormAspect], NormAspect]:
+        ...
+
+    @overload
+    def add(self, func: NormAspect) -> NormAspect:
+        ...
+
+    def add(self, func: Optional[NormAspect] = None, *, condition: object = True) -> Any:
+        if func is None:
+            return partial(self.add, condition=condition)
+
+        if condition:
+            self.append(func.__name__)
+        return func
+
+    def copy(self) -> 'AspectStorage':
+        return type(self)(super().copy())
+
+
+class NotSubscribedError(ValueError):
+    pass
+
+
+N = TypeVar('N', bound=BaseNormType)
+TN = TypeVar('TN', bound='TypeNormalizer')
+
+
+class TypeNormalizer:
+    def __init__(self, implicit_params_getter: ImplicitParamsGetter):
+        self.implicit_params_getter = implicit_params_getter
+        self._namespace: Optional[Dict[str, Any]] = None
+
+    def _with_namespace(self: TN, namespace: Dict[str, Any]) -> TN:
+        # pylint: disable=protected-access
+        self_copy = copy(self)
+        self_copy._namespace = namespace
+        return self_copy
+
+    def _with_module_namespace(self: TN, module_name: str) -> TN:
+        try:
+            module = sys.modules[module_name]
+        except KeyError:
+            return self
+        return self._with_namespace(vars(module))
+
+    @overload
+    def normalize(self, tp: TypeVar) -> NormTV:
+        ...
+
+    @overload
+    def normalize(self, tp: TypeHint) -> BaseNormType:
+        ...
+
+    def normalize(self, tp: TypeHint) -> BaseNormType:
+        origin = strip_alias(tp)
+        args = get_args(tp)
+
+        result = self._norm_forward_ref(tp)
+        if result is not None:
+            return result
+
+        for attr_name in self._aspect_storage:
+            result = getattr(self, attr_name)(tp, origin, args)
+            if result is not None:
+                return result
+
+        raise RuntimeError
+
+    def _norm_forward_ref(self, tp):
+        if self._namespace is not None:
+            if isinstance(tp, str):
+                return _replace_source(
+                    self.normalize(ForwardRef(tp)),
+                    source=tp,
+                )
+            if isinstance(tp, ForwardRef):
+                return _replace_source(
+                    self.normalize(eval_forward_ref(self._namespace, tp)),
+                    source=tp
+                )
+        elif isinstance(tp, (str, ForwardRef)):
+            raise ValueError(f'Can not normalize value {tp!r}, there are no namespace to evaluate types')
+
+    _aspect_storage = AspectStorage()
+
+    def _norm_iter(self, tps: Iterable[Any]) -> VarTuple[BaseNormType]:
+        return tuple(self.normalize(tp) for tp in tps)
+
+    MUST_SUBSCRIBED_ORIGINS = [
+        ClassVar, Final, Literal,
+        Union, Optional, InitVar,
+    ]
+    if HAS_ANNOTATED:
+        MUST_SUBSCRIBED_ORIGINS.append(typing.Annotated)
+    if HAS_TYPE_GUARD:
+        MUST_SUBSCRIBED_ORIGINS.append(typing.TypeGuard)
+    if HAS_TYPED_DICT_REQUIRED:
+        MUST_SUBSCRIBED_ORIGINS.extend([typing.Required, typing.NotRequired])
+
+    @_aspect_storage.add
+    def _check_bad_input(self, tp, origin, args):
+        if tp in self.MUST_SUBSCRIBED_ORIGINS:
+            raise NotSubscribedError(f"{tp} must be subscribed")
+
+        if tp in (NewType, TypeVar):
+            raise ValueError(f'{origin} must be instantiating')
+
+    @_aspect_storage.add
+    def _norm_none(self, tp, origin, args):
+        if origin is None or origin is NoneType:
+            return _NormType(None, (), source=tp)
+
+    @_aspect_storage.add(condition=HAS_ANNOTATED)
+    def _norm_annotated(self, tp, origin, args):
+        if origin == typing.Annotated:
+            return _AnnotatedNormType(
+                (self.normalize(args[0]), *args[1:]),
+                source=tp
+            )
+
+    def _get_bound(self, type_var) -> Bound:
+        return (
+            Bound(ANY_NT)
+            if (type_var.__bound__ is None or type_var.__bound__ is NoneType) else
+            Bound(self.normalize(type_var.__bound__))
+        )
+
+    @_aspect_storage.add
+    def _norm_type_var(self, tp, origin, args):
+        # pylint: disable=protected-access
+        if isinstance(origin, TypeVar):
+            namespaced = self._with_module_namespace(origin.__module__)
+            limit = (
+                Constraints(
+                    tuple(
+                        namespaced._dedup_union_args(
+                            namespaced._norm_iter(origin.__constraints__)
+                        )
+                    )
+                )
+                if origin.__constraints__ else
+                namespaced._get_bound(origin)
+            )
+            return NormTV(var=origin, limit=limit, source=tp)
+
+    @_aspect_storage.add(condition=HAS_TV_TUPLE)
+    def _norm_type_var_tuple(self, tp, origin, args):
+        if isinstance(origin, typing.TypeVarTuple):
+            return NormTVTuple(var=origin, source=tp)
+
+    @_aspect_storage.add(condition=HAS_PARAM_SPEC)
+    def _norm_param_spec(self, tp, origin, args):
+        if isinstance(tp, typing.ParamSpecArgs):
+            return _NormParamSpecArgs(param_spec=self.normalize(origin), source=tp)
+
+        if isinstance(tp, typing.ParamSpecKwargs):
+            return _NormParamSpecKwargs(param_spec=self.normalize(origin), source=tp)
+
+        if isinstance(origin, typing.ParamSpec):
+            namespaced = self._with_module_namespace(origin.__module__)
+            return NormTV(
+                var=origin,
+                limit=namespaced._get_bound(origin),  # pylint: disable=protected-access
+                source=tp,
+            )
+
+    @_aspect_storage.add(condition=HAS_TV_SYNTAX)
+    def _norm_type_alias_type(self, tp, origin, args):
+        if isinstance(origin, typing.TypeAliasType):  # pylint: disable=no-member
+            return NormTypeAlias(
+                origin,
+                self._norm_iter(args),
+                self._norm_iter(tp.__type_params__),
+            )
+
+    @_aspect_storage.add
+    def _norm_init_var(self, tp, origin, args):
+        if isinstance(origin, InitVar):
+            # this origin is InitVar[T]
+            return _NormType(
+                InitVar,
+                (self.normalize(origin.type),),
+                source=tp
+            )
+
+    @_aspect_storage.add
+    def _norm_new_type(self, tp, origin, args):
+        if is_new_type(tp):
+            return _NormType(tp, (), source=tp)
+
+    @_aspect_storage.add
+    def _norm_tuple(self, tp, origin, args):
+        if origin == tuple:
+            if tp in (tuple, Tuple):  # not subscribed values
+                return _NormType(
+                    tuple,
+                    (ANY_NT, ...),
+                    source=tp
+                )
+
+            # >>> Tuple[()].__args__ == ((),)
+            # >>> tuple[()].__args__ == ()
+            if not args or args == ((),):
+                return _NormType(tuple, (), source=tp)
+
+            if args[-1] is Ellipsis:
+                return _NormType(
+                    tuple, (*self._norm_iter(args[:-1]), ...),
+                    source=tp,
+                )
+
+            norm_args = self._norm_iter(args)
+            if HAS_UNPACK:
+                norm_args = self._unpack_tuple_elements(norm_args)
+            return _NormType(tuple, norm_args, source=tp)
+
+    def _unpack_tuple_elements(self, args: VarTuple[BaseNormType]) -> VarTuple[BaseNormType]:
+        # it is necessary to unpack the variable-length tuple as well
+        if len(args) == 1 and args[0].origin == typing.Unpack:
+            inner_tp = args[0].args[0]
+            if inner_tp.origin == tuple:
+                return inner_tp.args
+
+        return self._unpack_generic_elements(args)
+
+    def _unpack_generic_elements(self, args: VarTuple[Any]) -> VarTuple[BaseNormType]:
+        result = []
+        for arg in args:
+            if isinstance(arg, BaseNormType) and arg.origin == typing.Unpack and self._is_fixed_size_tuple(arg.args[0]):
+                result.extend(arg.args[0].args)
+            else:
+                result.append(arg)
+        return tuple(result)
+
+    def _is_fixed_size_tuple(self, tp: BaseNormType) -> bool:
+        return tp.origin == tuple and (not tp.args or tp.args[-1] is not Ellipsis)
+
+    @_aspect_storage.add
+    def _norm_callable(self, tp, origin, args):
+        if origin == c_abc.Callable:
+            if not args:
+                return _NormType(
+                    c_abc.Callable, (..., ANY_NT), source=tp
+                )
+
+            if args[0] is Ellipsis:
+                call_args = ...
+            elif isinstance(args[0], list):
+                call_args = self._norm_iter(args[0])
+                if HAS_TV_TUPLE:
+                    call_args = self._unpack_generic_elements(call_args)
+            else:
+                call_args = normalize_type(args[0])
+            return _NormType(
+                c_abc.Callable, (call_args, self.normalize(args[-1])), source=tp
+            )
+
+    @_aspect_storage.add
+    def _norm_literal(self, tp, origin, args):
+        if origin == Literal:
+            if args == (None,):  # Literal[None] converted to None
+                return _NormType(None, (), source=tp)
+
+            if None in args:
+                args_without_none = list(args)
+                args_without_none.remove(None)
+
+                return _UnionNormType(
+                    (
+                        _NormType(None, (), source=Literal[None]),
+                        _create_norm_literal(args_without_none),
+                    ),
+                    source=tp
+                )
+
+            return _LiteralNormType(args, source=tp)
+
+    def _unfold_union_args(self, norm_args: Iterable[N]) -> Iterable[N]:
+        result: List[N] = []
+        for norm in norm_args:
+            if norm.origin == Union:
+                result.extend(norm.args)
+            else:
+                result.append(norm)
+        return result
+
+    def _dedup_union_args(self, args: Iterable[BaseNormType]) -> Iterable[BaseNormType]:
+        args_to_sources: DefaultDict[BaseNormType, List[Any]] = defaultdict(list)
+
+        for arg in args:
+            args_to_sources[arg].append(arg.source)
+
+        return [
+            _replace_source_with_union(arg, sources)
+            if len(sources) != 1 and isinstance(arg, _NormType)
+            else arg
+            for arg, sources in args_to_sources.items()
+        ]
+
+    def _merge_literals(self, args: Iterable[N]) -> Sequence[N]:
+        result = []
+        lit_args: List[N] = []
+        for norm in args:
+            if norm.origin == Literal:
+                lit_args.extend(norm.args)
+            else:
+                result.append(norm)
+
+        if lit_args:
+            result.append(_create_norm_literal(lit_args))
+        return result
+
+    _UNION_ORIGINS: List[Any] = [Union]
+    if HAS_TYPE_UNION_OP:
+        _UNION_ORIGINS.append(types.UnionType)
+
+    @_aspect_storage.add
+    def _norm_union(self, tp, origin, args):
+        if origin in self._UNION_ORIGINS:
+            norm_args = self._norm_iter(args)
+            unfolded_n_args = self._unfold_union_args(norm_args)
+            unique_n_args = self._dedup_union_args(unfolded_n_args)
+            merged_n_args = self._merge_literals(unique_n_args)
+
+            if len(merged_n_args) == 1:
+                arg = merged_n_args[0]
+                return make_norm_type(origin=arg.origin, args=arg.args, source=tp)
+            return _UnionNormType(tuple(merged_n_args), source=tp)
+
+    @_aspect_storage.add
+    def _norm_type(self, tp, origin, args):
+        if is_subclass_soft(origin, type) and args:
+            norm = self.normalize(args[0])
+
+            if norm.origin == Union:
+                return _UnionNormType(
+                    tuple(
+                        _NormType(type, (arg,), source=Type[arg.source])
+                        for arg in norm.args
+                    ),
+                    source=tp
+                )
+
+    ALLOWED_ZERO_PARAMS_ORIGINS = {Any, NoReturn}
+    if HAS_TYPE_ALIAS:
+        ALLOWED_ZERO_PARAMS_ORIGINS.add(typing.TypeAlias)
+    if HAS_PY_310:
+        ALLOWED_ZERO_PARAMS_ORIGINS.add(dataclasses.KW_ONLY)
+    if HAS_PY_311:
+        ALLOWED_ZERO_PARAMS_ORIGINS.add(typing.Never)
+    if HAS_SELF_TYPE:
+        ALLOWED_ZERO_PARAMS_ORIGINS.add(typing.Self)
+    if HAS_PY_311:
+        ALLOWED_ZERO_PARAMS_ORIGINS.add(typing.LiteralString)
+
+    def _norm_generic_arg(self, arg):
+        if arg is Ellipsis:
+            return Ellipsis
+        if isinstance(arg, tuple):
+            return self._norm_iter(arg)
+        return self.normalize(arg)
+
+    def _norm_implicit_param(self, param):
+        if param is Ellipsis:
+            return Ellipsis
+        return self.normalize(param)
+
+    @_aspect_storage.add
+    def _norm_other(self, tp, origin, args):
+        if args:
+            norm_args = tuple(self._norm_generic_arg(el) for el in args)
+            if HAS_TV_TUPLE:
+                norm_args = self._unpack_generic_elements(norm_args)
+            return _NormType(origin, norm_args, source=tp)
+
+        params = self.implicit_params_getter.get_implicit_params(origin)
+        if not (
+            params
+            or isinstance(origin, type)
+            or origin in self.ALLOWED_ZERO_PARAMS_ORIGINS
+        ):
+            raise ValueError(f'Can not normalize value {tp!r}')
+
+        return _NormType(
+            origin,
+            tuple(self._norm_implicit_param(param) for param in params),
+            source=tp,
+        )
+
+
+_STD_NORMALIZER = TypeNormalizer(ImplicitParamsGetter())
+_cached_normalize = lru_cache(maxsize=128)(_STD_NORMALIZER.normalize)
+
+
+def normalize_type(tp: TypeHint) -> BaseNormType:
+    try:
+        hash(tp)
+    except TypeError:
+        return _STD_NORMALIZER.normalize(tp)
+
+    return _cached_normalize(tp)
diff --git a/src/dishka/async_container.py b/src/dishka/async_container.py
index 1467903..f4272be 100644
--- a/src/dishka/async_container.py
+++ b/src/dishka/async_container.py
@@ -1,6 +1,6 @@
 from asyncio import Lock
 from dataclasses import dataclass
-from typing import Callable, List, Optional, Type, TypeVar
+from typing import Any, Callable, List, Optional, Type, TypeVar
 
 from .dependency_source import Factory, FactoryType
 from .exceptions import ExitExceptionGroup
@@ -71,7 +71,9 @@ class AsyncContainer:
             raise ValueError("No child scopes found")
         return AsyncContextWrapper(self._create_child(context, lock_factory))
 
-    async def _get_from_self(self, factory: Factory) -> T:
+    async def _get_from_self(
+            self, factory: Factory, dependency_type: Any,
+    ) -> T:
         sub_dependencies = [
             await self._get_unlocked(dependency)
             for dependency in factory.dependencies
@@ -79,19 +81,22 @@ class AsyncContainer:
         if factory.type is FactoryType.GENERATOR:
             generator = factory.source(*sub_dependencies)
             self._exits.append(Exit(factory.type, generator))
-            return next(generator)
+            solved =  next(generator)
         elif factory.type is FactoryType.ASYNC_GENERATOR:
             generator = factory.source(*sub_dependencies)
             self._exits.append(Exit(factory.type, generator))
-            return await anext(generator)
+            solved =  await anext(generator)
         elif factory.type is FactoryType.ASYNC_FACTORY:
-            return await factory.source(*sub_dependencies)
+            solved =  await factory.source(*sub_dependencies)
         elif factory.type is FactoryType.FACTORY:
-            return factory.source(*sub_dependencies)
+            solved =  factory.source(*sub_dependencies)
         elif factory.type is FactoryType.VALUE:
-            return factory.source
+            solved =  factory.source
         else:
             raise ValueError(f"Unsupported type {factory.type}")
+        if factory.cache:
+            self.context[dependency_type] = solved
+        return solved
 
     async def get(self, dependency_type: Type[T]) -> T:
         lock = self.lock
@@ -103,13 +108,12 @@ class AsyncContainer:
     async def _get_unlocked(self, dependency_type: Type[T]) -> T:
         if dependency_type in self.context:
             return self.context[dependency_type]
-        provider = self.registry.get_provider(dependency_type)
+        provider = self.registry.get_factory(dependency_type)
         if not provider:
             if not self.parent_container:
                 raise ValueError(f"No provider found for {dependency_type!r}")
             return await self.parent_container.get(dependency_type)
-        solved = await self._get_from_self(provider)
-        self.context[dependency_type] = solved
+        solved = await self._get_from_self(provider, dependency_type)
         return solved
 
     async def close(self):
diff --git a/src/dishka/container.py b/src/dishka/container.py
index 2d688a0..1460d04 100644
--- a/src/dishka/container.py
+++ b/src/dishka/container.py
@@ -1,6 +1,6 @@
 from dataclasses import dataclass
 from threading import Lock
-from typing import Callable, List, Optional, Type, TypeVar
+from typing import Any, Callable, List, Optional, Type, TypeVar
 
 from .dependency_source import Factory, FactoryType
 from .exceptions import ExitExceptionGroup
@@ -71,7 +71,9 @@ class Container:
             raise ValueError("No child scopes found")
         return ContextWrapper(self._create_child(context, lock_factory))
 
-    def _get_from_self(self, factory: Factory) -> T:
+    def _get_from_self(
+            self, factory: Factory, dependency_type: Any,
+    ) -> T:
         sub_dependencies = [
             self._get_unlocked(dependency)
             for dependency in factory.dependencies
@@ -79,13 +81,16 @@ class Container:
         if factory.type is FactoryType.GENERATOR:
             generator = factory.source(*sub_dependencies)
             self._exits.append(Exit(factory.type, generator))
-            return next(generator)
+            solved = next(generator)
         elif factory.type is FactoryType.FACTORY:
-            return factory.source(*sub_dependencies)
+            solved = factory.source(*sub_dependencies)
         elif factory.type is FactoryType.VALUE:
-            return factory.source
+            solved = factory.source
         else:
             raise ValueError(f"Unsupported type {factory.type}")
+        if factory.cache:
+            self.context[dependency_type] = solved
+        return solved
 
     def get(self, dependency_type: Type[T]) -> T:
         lock = self.lock
@@ -97,13 +102,12 @@ class Container:
     def _get_unlocked(self, dependency_type: Type[T]) -> T:
         if dependency_type in self.context:
             return self.context[dependency_type]
-        provider = self.registry.get_provider(dependency_type)
+        provider = self.registry.get_factory(dependency_type)
         if not provider:
             if not self.parent_container:
                 raise ValueError(f"No provider found for {dependency_type!r}")
             return self.parent_container.get(dependency_type)
-        solved = self._get_from_self(provider)
-        self.context[dependency_type] = solved
+        solved = self._get_from_self(provider, dependency_type)
         return solved
 
     def close(self) -> None:
diff --git a/src/dishka/dependency_source.py b/src/dishka/dependency_source.py
index f3988f5..77e8bca 100644
--- a/src/dishka/dependency_source.py
+++ b/src/dishka/dependency_source.py
@@ -1,10 +1,19 @@
-from collections.abc import AsyncIterable, Iterable
+from collections.abc import (
+    AsyncGenerator,
+    AsyncIterable,
+    AsyncIterator,
+    Generator,
+    Iterable,
+    Iterator,
+)
 from enum import Enum
 from inspect import (
     isasyncgenfunction,
     isclass,
     iscoroutinefunction,
+    isfunction,
     isgeneratorfunction,
+    signature,
 )
 from typing import (
     Any,
@@ -18,6 +27,15 @@ from typing import (
     overload,
 )
 
+from ._adaptix.type_tools.basic_utils import (
+    get_all_type_hints,
+    get_type_vars,
+    is_bare_generic,
+)
+from ._adaptix.type_tools.generic_resolver import (
+    GenericResolver,
+    MembersStorage,
+)
 from .scope import BaseScope
 
 
@@ -36,7 +54,7 @@ def _identity(x: Any) -> Any:
 class Factory:
     __slots__ = (
         "dependencies", "source", "provides", "scope", "type",
-        "is_to_bound",
+        "is_to_bound", "cache",
     )
 
     def __init__(
@@ -44,9 +62,10 @@ class Factory:
             dependencies: Sequence[Any],
             source: Any,
             provides: Type,
-            scope: Optional[BaseScope],
+            scope: BaseScope | None,
             type: FactoryType,
             is_to_bound: bool,
+            cache: bool,
     ):
         self.dependencies = dependencies
         self.source = source
@@ -54,8 +73,10 @@ class Factory:
         self.scope = scope
         self.type = type
         self.is_to_bound = is_to_bound
+        self.cache = cache
 
     def __get__(self, instance, owner):
+        scope = self.scope or instance.scope
         if instance is None:
             return self
         if self.is_to_bound:
@@ -66,80 +87,162 @@ class Factory:
             dependencies=self.dependencies,
             source=source,
             provides=self.provides,
-            scope=self.scope,
+            scope=scope,
             type=self.type,
             is_to_bound=False,
+            cache=self.cache,
         )
 
 
+def _get_init_members(tp) -> MembersStorage[str, None]:
+    type_hints = get_all_type_hints(tp.__init__)
+    if "__init__" in tp.__dict__:
+        overriden = frozenset(type_hints)
+    else:
+        overriden = {}
+
+    return MembersStorage(
+        meta=None,
+        members=type_hints,
+        overriden=overriden,
+    )
+
+
+def _guess_factory_type(source):
+    if isasyncgenfunction(source):
+        return FactoryType.ASYNC_GENERATOR
+    elif isgeneratorfunction(source):
+        return FactoryType.GENERATOR
+    elif iscoroutinefunction(source):
+        return FactoryType.ASYNC_FACTORY
+    else:
+        return FactoryType.FACTORY
+
+
+def _clean_result_hint(factory_type: FactoryType, possible_dependency: Any):
+    if factory_type == FactoryType.ASYNC_GENERATOR:
+        origin = get_origin(possible_dependency)
+        if origin is AsyncIterable:
+            return get_args(possible_dependency)[0]
+        elif origin is AsyncIterator:
+            return get_args(possible_dependency)[0]
+        elif origin is AsyncGenerator:
+            return get_args(possible_dependency)[0]
+        else:
+            raise TypeError(
+                f"Unsupported return type {possible_dependency} {origin} "
+                f"for async generator")
+    elif factory_type == FactoryType.GENERATOR:
+        origin = get_origin(possible_dependency)
+        if origin is Iterable:
+            return get_args(possible_dependency)[0]
+        elif origin is Iterator:
+            return get_args(possible_dependency)[0]
+        elif origin is Generator:
+            return get_args(possible_dependency)[1]
+        else:
+            raise TypeError(
+                f"Unsupported return type {possible_dependency} {origin}"
+                f" for generator")
+    return possible_dependency
+
+
 def make_factory(
         provides: Any,
         scope: Optional[BaseScope],
         source: Callable,
+        cache: bool,
 ) -> Factory:
-    if isclass(source):
-        hints = get_type_hints(source.__init__, include_extras=True)
+    if is_bare_generic(source):
+        source = source[get_type_vars(source)]
+
+    if isclass(source) or get_origin(source):
+        # we need to fix concrete generics and normal classes as well
+        # as classes can be children of concrete generics
+        res = GenericResolver(_get_init_members)
+        hints = dict(res.get_resolved_members(source).members)
         hints.pop("return", None)
-        possible_dependency = source
+        dependencies = list(hints.values())
+        if not provides:
+            provides = source
         is_to_bind = False
-    else:
+        factory_type = FactoryType.FACTORY
+    elif isfunction(source) or isinstance(source, classmethod):
+        if isinstance(source, classmethod):
+            params = signature(source.__wrapped__).parameters
+            factory_type = _guess_factory_type(source.__wrapped__)
+        else:
+            params = signature(source).parameters
+            factory_type = _guess_factory_type(source)
+
+        self = next(iter(params.values()))
         hints = get_type_hints(source, include_extras=True)
+        hints.pop(self.name, None)
         possible_dependency = hints.pop("return", None)
+        dependencies = list(hints.values())
+        if not provides:
+            provides = _clean_result_hint(factory_type, possible_dependency)
         is_to_bind = True
-
-    if isclass(source):
-        provider_type = FactoryType.FACTORY
-    elif isasyncgenfunction(source):
-        provider_type = FactoryType.ASYNC_GENERATOR
-        if get_origin(possible_dependency) is AsyncIterable:
-            possible_dependency = get_args(possible_dependency)[0]
-        else:  # async generator
-            possible_dependency = get_args(possible_dependency)[0]
-    elif isgeneratorfunction(source):
-        provider_type = FactoryType.GENERATOR
-        if get_origin(possible_dependency) is Iterable:
-            possible_dependency = get_args(possible_dependency)[0]
-        else:  # generator
-            possible_dependency = get_args(possible_dependency)[1]
-    elif iscoroutinefunction(source):
-        provider_type = FactoryType.ASYNC_FACTORY
+    elif isinstance(source, staticmethod):
+        factory_type = _guess_factory_type(source.__wrapped__)
+        hints = get_type_hints(source, include_extras=True)
+        possible_dependency = hints.pop("return", None)
+        dependencies = list(hints.values())
+        if not provides:
+            provides = _clean_result_hint(factory_type, possible_dependency)
+        is_to_bind = False
+    elif callable(source):
+        factory = make_factory(
+            provides=provides,
+            source=type(source).__call__,
+            cache=cache,
+            scope=scope,
+        )
+        factory_type = factory.type
+        dependencies = factory.dependencies
+        provides = factory.provides
+        is_to_bind = False
     else:
-        provider_type = FactoryType.FACTORY
+        raise TypeError(f"Cannot use {type(source)} as a factory")
 
     return Factory(
-        dependencies=list(hints.values()),
-        type=provider_type,
+        dependencies=dependencies,
+        type=factory_type,
         source=source,
         scope=scope,
-        provides=provides or possible_dependency,
+        provides=provides,
         is_to_bound=is_to_bind,
+        cache=cache,
     )
 
 
 @overload
 def provide(
         *,
-        scope: BaseScope,
+        scope: BaseScope = None,
         provides: Any = None,
+        cache: bool = True,
 ) -> Callable[[Callable], Factory]:
     ...
 
 
 @overload
 def provide(
-        source: Callable | Type,
+        source: Callable | classmethod | staticmethod | Type | None,
         *,
         scope: BaseScope,
         provides: Any = None,
+        cache: bool = True,
 ) -> Factory:
     ...
 
 
 def provide(
-        source: Callable | Type | None = None,
+        source: Callable | classmethod | staticmethod | Type | None = None,
         *,
-        scope: BaseScope,
+        scope: BaseScope | None = None,
         provides: Any = None,
+        cache: bool = True,
 ) -> Factory | Callable[[Callable], Factory]:
     """
     Mark a method or class as providing some dependency.
@@ -159,22 +262,24 @@ def provide(
     :param scope: Scope of the dependency to limit its lifetime
     :param provides: Dependency type which is provided by this factory
     :return: instance of Factory or a decorator returning it
+    :param cache: save created object to scope cache or not
     """
     if source is not None:
-        return make_factory(provides, scope, source)
+        return make_factory(provides, scope, source, cache)
 
     def scoped(func):
-        return make_factory(provides, scope, func)
+        return make_factory(provides, scope, func, cache)
 
     return scoped
 
 
 class Alias:
-    __slots__ = ("source", "provides")
+    __slots__ = ("source", "provides", "cache")
 
-    def __init__(self, source, provides):
+    def __init__(self, source, provides, cache: bool):
         self.source = source
         self.provides = provides
+        self.cache = cache
 
     def as_factory(self, scope: BaseScope) -> Factory:
         return Factory(
@@ -184,6 +289,7 @@ class Alias:
             is_to_bound=False,
             dependencies=[self.source],
             type=FactoryType.FACTORY,
+            cache=self.cache,
         )
 
     def __get__(self, instance, owner):
@@ -194,10 +300,12 @@ def alias(
         *,
         source: Type,
         provides: Type,
+        cache: bool = True,
 ) -> Alias:
     return Alias(
         source=source,
         provides=provides,
+        cache=cache,
     )
 
 
@@ -209,7 +317,7 @@ class Decorator:
         self.provides = factory.provides
 
     def as_factory(
-            self, scope: BaseScope, new_dependency: Any,
+            self, scope: BaseScope, new_dependency: Any, cache: bool,
     ) -> Factory:
         return Factory(
             scope=scope,
@@ -221,6 +329,7 @@ class Decorator:
                 for dep in self.factory.dependencies
             ],
             type=self.factory.type,
+            cache=cache,
         )
 
     def __get__(self, instance, owner):
@@ -249,10 +358,10 @@ def decorate(
         provides: Any = None,
 ) -> Decorator | Callable[[Callable], Decorator]:
     if source is not None:
-        return Decorator(make_factory(provides, None, source))
+        return Decorator(make_factory(provides, None, source, False))
 
     def scoped(func):
-        return Decorator(make_factory(provides, None, func))
+        return Decorator(make_factory(provides, None, func, False))
 
     return scoped
 
diff --git a/src/dishka/integrations/aiogram.py b/src/dishka/integrations/aiogram.py
index fad5868..19e24ae 100644
--- a/src/dishka/integrations/aiogram.py
+++ b/src/dishka/integrations/aiogram.py
@@ -3,7 +3,6 @@ __all__ = [
     "inject",
     "setup_dishka",
 ]
-
 from inspect import Parameter
 from typing import Container, Sequence
 
@@ -13,10 +12,12 @@ from aiogram.types import TelegramObject
 from dishka import Provider, make_async_container
 from .base import Depends, wrap_injection
 
+CONTAINER_NAME = "dishka_container"
+
 
 def inject(func):
     additional_params = [Parameter(
-        name="dishka_container",
+        name=CONTAINER_NAME,
         annotation=Container,
         kind=Parameter.KEYWORD_ONLY,
     )]
@@ -24,7 +25,7 @@ def inject(func):
     return wrap_injection(
         func=func,
         remove_depends=True,
-        container_getter=lambda _, p: p["dishka_container"],
+        container_getter=lambda _, p: p[CONTAINER_NAME],
         additional_params=additional_params,
         is_async=True,
     )
@@ -38,8 +39,8 @@ class ContainerMiddleware(BaseMiddleware):
     async def __call__(
             self, handler, event, data,
     ):
-        async with self.container({TelegramObject: event}) as subcontainer:
-            data["dishka_container"] = subcontainer
+        async with self.container({TelegramObject: event}) as sub_container:
+            data[CONTAINER_NAME] = sub_container
             return await handler(event, data)
 
     async def startup(self):
diff --git a/src/dishka/integrations/aiohttp.py b/src/dishka/integrations/aiohttp.py
new file mode 100644
index 0000000..d863e6f
--- /dev/null
+++ b/src/dishka/integrations/aiohttp.py
@@ -0,0 +1,56 @@
+__all__ = [
+    "Depends", "inject", "setup_dishka",
+]
+
+from typing import Callable, Final, Sequence
+
+from aiohttp import web
+from aiohttp.typedefs import Handler
+from aiohttp.web_app import Application
+from aiohttp.web_request import Request
+from aiohttp.web_response import StreamResponse
+
+from dishka import Provider, make_async_container
+from dishka.async_container import AsyncContainer, AsyncContextWrapper
+from dishka.integrations.base import Depends, wrap_injection
+
+CONTAINER_KEY: Final = web.AppKey('dishka_container', AsyncContainer)
+
+
+def inject(func: Callable) -> Callable:
+    return wrap_injection(
+        func=func,
+        remove_depends=True,
+        container_getter=lambda p, _: p[0][CONTAINER_KEY],
+        is_async=True,
+    )
+
+
+@web.middleware
+async def container_middleware(
+    request: Request, handler: Handler,
+) -> StreamResponse:
+    container = request.app[CONTAINER_KEY]
+    async with container(context={Request: request}) as request_container:
+        request[CONTAINER_KEY] = request_container
+        res = await handler(request)
+    return res
+
+
+def startup(wrapper_container: AsyncContextWrapper):
+    async def wrapper(app: Application) -> None:
+        app[CONTAINER_KEY] = await wrapper_container.__aenter__()
+    return wrapper
+
+
+def shutdown(wrapper_container: AsyncContextWrapper):
+    async def wrapper(app: Application) -> None:
+        await wrapper_container.__aexit__(None, None, None)
+    return wrapper
+
+
+def setup_dishka(providers: Sequence[Provider], app: Application) -> None:
+    wrapper_container = make_async_container(*providers)
+    app.middlewares.append(container_middleware)
+    app.on_startup.append(startup(wrapper_container))
+    app.on_shutdown.append(shutdown(wrapper_container))
diff --git a/src/dishka/integrations/base.py b/src/dishka/integrations/base.py
index 36a7757..32e4189 100644
--- a/src/dishka/integrations/base.py
+++ b/src/dishka/integrations/base.py
@@ -2,13 +2,18 @@ from inspect import Parameter, Signature, signature
 from typing import (
     Annotated,
     Any,
+    Awaitable,
     Callable,
+    Literal,
     Sequence,
+    Type,
     get_args,
     get_origin,
     get_type_hints,
+    overload,
 )
 
+from dishka.async_container import AsyncContainer
 from dishka.container import Container
 
 
@@ -20,12 +25,13 @@ class Depends:
 def default_parse_dependency(
         parameter: Parameter,
         hint: Any,
+        depends_class: Type[Any] = Depends,
 ) -> Any:
     """ Resolve dependency type or return None if it is not a dependency """
     if get_origin(hint) is not Annotated:
         return None
     dep = next(
-        (arg for arg in get_args(hint) if isinstance(arg, Depends)),
+        (arg for arg in get_args(hint) if isinstance(arg, depends_class)),
         None,
     )
     if not dep:
@@ -39,14 +45,41 @@ def default_parse_dependency(
 DependencyParser = Callable[[Parameter, Any], Any]
 
 
+@overload
 def wrap_injection(
+        *,
         func: Callable,
         container_getter: Callable[[tuple, dict], Container],
+        is_async: Literal[False] = False,
         remove_depends: bool = True,
         additional_params: Sequence[Parameter] = (),
-        is_async: bool = False,
         parse_dependency: DependencyParser = default_parse_dependency,
 ) -> Callable:
+    ...
+
+
+@overload
+def wrap_injection(
+        *,
+        func: Callable,
+        container_getter: Callable[[tuple, dict], AsyncContainer],
+        is_async: Literal[True],
+        remove_depends: bool = True,
+        additional_params: Sequence[Parameter] = (),
+        parse_dependency: DependencyParser = default_parse_dependency,
+) -> Awaitable:
+    ...
+
+
+def wrap_injection(
+        *,
+        func: Callable,
+        container_getter,
+        is_async: bool = False,
+        remove_depends: bool = True,
+        additional_params: Sequence[Parameter] = (),
+        parse_dependency: DependencyParser = default_parse_dependency,
+):
     hints = get_type_hints(func, include_extras=True)
     func_signature = signature(func)
 
diff --git a/src/dishka/integrations/starlette.py b/src/dishka/integrations/starlette.py
new file mode 100644
index 0000000..29115cd
--- /dev/null
+++ b/src/dishka/integrations/starlette.py
@@ -0,0 +1,47 @@
+__all__ = ["Depends", "inject", "DishkaApp"]
+
+from starlette.requests import Request
+from starlette.types import ASGIApp, Receive, Scope, Send
+
+from dishka import AsyncContainer
+from ..async_container import AsyncContextWrapper
+from .asgi import BaseDishkaApp
+from .base import Depends, wrap_injection
+
+
+def inject(func):
+    return wrap_injection(
+        func=func,
+        remove_depends=True,
+        container_getter=lambda r, _: r[0].scope["state"]["dishka_container"],
+        additional_params=[],
+        is_async=True,
+    )
+
+
+class ContainerMiddleware:
+    def __init__(self, app: ASGIApp) -> None:
+        self.app = app
+
+    async def __call__(
+            self, scope: Scope, receive: Receive, send: Send,
+    ) -> None:
+        if scope["type"] != "http":
+            return await self.app(scope, receive, send)
+
+        request = Request(scope)
+        async with request.app.state.dishka_container(
+                {Request: request},
+        ) as request_container:
+            request.state.dishka_container = request_container
+            await self.app(scope, receive, send)
+
+
+class DishkaApp(BaseDishkaApp):
+    def _init_request_middleware(
+            self, app, container_wrapper: AsyncContextWrapper,
+    ):
+        app.add_middleware(ContainerMiddleware)
+
+    def _app_startup(self, app, container: AsyncContainer):
+        app.state.dishka_container = container
diff --git a/src/dishka/integrations/telebot.py b/src/dishka/integrations/telebot.py
index cd41f13..d5b6678 100644
--- a/src/dishka/integrations/telebot.py
+++ b/src/dishka/integrations/telebot.py
@@ -13,10 +13,11 @@ from telebot import BaseMiddleware, TeleBot
 from dishka import Container, Provider, make_container
 from .base import Depends, wrap_injection
 
+CONTAINER_NAME = "dishka_container"
 
 def inject(func):
     additional_params = [Parameter(
-        name="dishka_container",
+        name=CONTAINER_NAME,
         annotation=Container,
         kind=Parameter.KEYWORD_ONLY,
     )]
@@ -24,7 +25,7 @@ def inject(func):
     return wrap_injection(
         func=func,
         remove_depends=True,
-        container_getter=lambda _, p: p["dishka_container"],
+        container_getter=lambda _, p: p[CONTAINER_NAME],
         additional_params=additional_params,
         is_async=False,
     )
@@ -39,11 +40,11 @@ class ContainerMiddleware(BaseMiddleware):
 
     def pre_process(self, message, data):
         dishka_container_wrapper = self.container({type(message): message})
-        data["dishka_container_wrapper"] = dishka_container_wrapper
-        data["dishka_container"] = dishka_container_wrapper.__enter__()
+        data[CONTAINER_NAME + "_wrapper"] = dishka_container_wrapper
+        data[CONTAINER_NAME] = dishka_container_wrapper.__enter__()
 
     def post_process(self, message, data, exception):
-        data["dishka_container_wrapper"].__exit__(None, None, None)
+        data[CONTAINER_NAME + "_wrapper"].__exit__(None, None, None)
 
 
 def setup_dishka(providers: Sequence[Provider], bot: TeleBot) -> Container:
diff --git a/src/dishka/provider.py b/src/dishka/provider.py
index 80a327b..1eb34f7 100644
--- a/src/dishka/provider.py
+++ b/src/dishka/provider.py
@@ -3,6 +3,7 @@ from typing import Any, List
 
 from .dependency_source import Alias, Decorator, DependencySource, Factory
 from .exceptions import InvalidGraphError
+from .scope import BaseScope
 
 
 def is_dependency_source(attribute: Any) -> bool:
@@ -22,12 +23,14 @@ class Provider:
     The only intended usage of providers is to pass them when
     creating a container
     """
+    scope: BaseScope | None = None
 
-    def __init__(self):
+    def __init__(self, scope: BaseScope | None = None):
         self.factories: List[Factory] = []
         self.aliases: List[Alias] = []
         self.decorators: List[Decorator] = []
         self._init_dependency_sources()
+        self.scope = self.scope or scope
 
     def _init_dependency_sources(self) -> None:
         processed_types = {}
diff --git a/src/dishka/py.typed b/src/dishka/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/src/dishka/registry.py b/src/dishka/registry.py
index 5bc0412..e8f1ef9 100644
--- a/src/dishka/registry.py
+++ b/src/dishka/registry.py
@@ -1,6 +1,7 @@
 from collections import defaultdict
-from typing import Any, List, NewType, Type
+from typing import Any, List, NewType, Type, TypeVar, get_args, get_origin
 
+from ._adaptix.type_tools.basic_utils import get_type_vars, is_generic
 from .dependency_source import Factory
 from .exceptions import InvalidGraphError
 from .provider import Provider
@@ -14,11 +15,53 @@ class Registry:
         self._factories: dict[Type, Factory] = {}
         self.scope = scope
 
-    def add_provider(self, factory: Factory):
-        self._factories[factory.provides] = factory
+    def add_factory(self, factory: Factory):
+        if is_generic(factory.provides):
+            self._factories[get_origin(factory.provides)] = factory
+        else:
+            self._factories[factory.provides] = factory
 
-    def get_provider(self, dependency: Any) -> Factory:
-        return self._factories.get(dependency)
+    def get_factory(self, dependency: Any) -> Factory | None:
+        try:
+            return self._factories[dependency]
+        except KeyError:
+            origin = get_origin(dependency)
+            if not origin:
+                return None
+            factory = self._factories.get(origin)
+            if not factory:
+                return None
+
+            factory = self._specialize_generic(factory, dependency)
+            self._factories[dependency] = factory
+            return factory
+
+    def _specialize_generic(
+            self, factory: Factory, dependency: Any,
+    ) -> Factory:
+        params_replacement = dict(zip(
+            get_args(factory.provides),
+            get_args(dependency),
+        ))
+        new_dependencies = []
+        for source_dependency in factory.dependencies:
+            if isinstance(source_dependency, TypeVar):
+                source_dependency = params_replacement[source_dependency]
+            elif get_origin(source_dependency):
+                source_dependency = source_dependency[tuple(
+                    params_replacement[param]
+                    for param in get_type_vars(source_dependency)
+                )]
+            new_dependencies.append(source_dependency)
+        return Factory(
+            source=factory.source,
+            provides=dependency,
+            dependencies=new_dependencies,
+            is_to_bound=factory.is_to_bound,
+            type=factory.type,
+            scope=factory.scope,
+            cache=factory.cache,
+        )
 
 
 def make_registries(
@@ -38,7 +81,7 @@ def make_registries(
     for provider in providers:
         for source in provider.factories:
             scope = source.scope
-            registries[scope].add_provider(source)
+            registries[scope].add_factory(source)
         for source in provider.aliases:
             alias_source = source.source
             visited_types = [alias_source]
@@ -52,7 +95,7 @@ def make_registries(
             scope = dep_scopes[alias_source]
             dep_scopes[source.provides] = scope
             source = source.as_factory(scope)
-            registries[scope].add_provider(source)
+            registries[scope].add_factory(source)
         for source in provider.decorators:
             provides = source.provides
             scope = dep_scopes[provides]
@@ -62,12 +105,12 @@ def make_registries(
                 source.provides,
             )
             decorator_depth[provides] += 1
-            old_provider = registry.get_provider(provides)
-            old_provider.provides = undecorated_type
-            registry.add_provider(old_provider)
+            old_factory = registry.get_factory(provides)
+            old_factory.provides = undecorated_type
+            registry.add_factory(old_factory)
             source = source.as_factory(
-                scope, undecorated_type,
+                scope, undecorated_type, old_factory.cache,
             )
-            registries[scope].add_provider(source)
+            registries[scope].add_factory(source)
 
     return list(registries.values())
diff --git a/tox.ini b/tox.ini
index ec47cab..3d6c92b 100644
--- a/tox.ini
+++ b/tox.ini
@@ -4,19 +4,23 @@ requires =
 env_list =
     unit,
     real_world_example,
-    fastapi-{0092,0109},
+    fastapi-{0096,0109},
+    aiohttp-393,
     flask-302,
     litestar-230,
     aiogram-330,
     telebot-415,
+    starlette-0270,
 
 [pytest]
-addopts = --cov=dishka --cov-append -v
+addopts = --cov=dishka --cov-append --cov-report term-missing -v
 
 [testenv]
 deps =
+    aiohttp-393: -r requirements/aiohttp-393.txt
+    aiohttp-latest: -r requirements/aiohttp-latest.txt
     fastapi-latest: -r requirements/fastapi-latest.txt
-    fastapi-0092: -r requirements/fastapi-0092.txt
+    fastapi-0096: -r requirements/fastapi-0096.txt
     fastapi-0109: -r requirements/fastapi-0109.txt
     aiogram-latest: -r requirements/aiogram-latest.txt
     aiogram-330: -r requirements/aiogram-330.txt
@@ -26,13 +30,17 @@ deps =
     flask-302: -r requirements/flask-302.txt
     litestar-latest: -r requirements/litestar-latest.txt
     litestar-230: -r requirements/litestar-230.txt
+    starlette-latest: -r requirements/srarlette-latest.txt
+    starlette-0270: -r requirements/starlette-0270.txt
 
 commands =
+    aiohttp: pytest tests/integrations/aiohttp
     fastapi: pytest tests/integrations/fastapi
     aiogram: pytest tests/integrations/aiogram
     telebot: pytest tests/integrations/telebot
     flask: pytest tests/integrations/flask
     litestar: pytest tests/integrations/litestar
+    starlette: pytest tests/integrations/starlette
 
 package = editable
 


Test patch:
diff --git a/examples/real_world/tests/test_add_products.py b/examples/real_world/tests/test_add_products.py
index 8ec0959..81a15c8 100644
--- a/examples/real_world/tests/test_add_products.py
+++ b/examples/real_world/tests/test_add_products.py
@@ -27,25 +27,27 @@ from myapp.use_cases import (
 
 # app dependency logic
 class AdaptersProvider(Provider):
-    @provide(scope=Scope.APP)
+    scope=Scope.APP
+
+    @provide
     def users(self) -> UserGateway:
         gateway = Mock()
         gateway.get_user = Mock(return_value=User())
         return gateway
 
-    @provide(scope=Scope.APP)
+    @provide
     def products(self) -> ProductGateway:
         gateway = Mock()
         gateway.add_product = Mock()
         return gateway
 
-    @provide(scope=Scope.APP)
+    @provide
     def uow(self) -> UnitOfWork:
         uow = Mock()
         uow.commit = Mock()
         return uow
 
-    @provide(scope=Scope.APP)
+    @provide
     def warehouse(self) -> WarehouseClient:
         warehouse = Mock()
         warehouse.next_product = Mock(return_value=["a", "b"])
diff --git a/requirements/aiohttp-latest.txt b/requirements/aiohttp-latest.txt
new file mode 100644
index 0000000..40baa82
--- /dev/null
+++ b/requirements/aiohttp-latest.txt
@@ -0,0 +1,2 @@
+-r test.txt
+aiohttp
\ No newline at end of file
diff --git a/requirements/starlette-latest.txt b/requirements/starlette-latest.txt
new file mode 100644
index 0000000..2525668
--- /dev/null
+++ b/requirements/starlette-latest.txt
@@ -0,0 +1,2 @@
+-r asgi.txt
+starlette
\ No newline at end of file
diff --git a/tests/integrations/aiohttp/__init__.py b/tests/integrations/aiohttp/__init__.py
new file mode 100644
index 0000000..0e1409f
--- /dev/null
+++ b/tests/integrations/aiohttp/__init__.py
@@ -0,0 +1,3 @@
+import pytest
+
+pytest.importorskip("aiohttp")
diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py
new file mode 100644
index 0000000..3d0cb47
--- /dev/null
+++ b/tests/integrations/aiohttp/test_aiohttp.py
@@ -0,0 +1,86 @@
+from contextlib import asynccontextmanager
+from typing import Annotated
+from unittest.mock import Mock
+
+import pytest
+from aiohttp.test_utils import TestClient, TestServer
+from aiohttp.web_app import Application
+from aiohttp.web_response import Response
+from aiohttp.web_routedef import RouteTableDef
+
+from dishka.integrations.aiohttp import (
+    Depends,
+    inject,
+    setup_dishka,
+)
+from ..common import (
+    APP_DEP_VALUE,
+    REQUEST_DEP_VALUE,
+    AppDep,
+    AppProvider,
+    RequestDep,
+)
+
+
+@asynccontextmanager
+async def dishka_app(view, provider) -> TestClient:
+    app = Application()
+
+    router = RouteTableDef()
+    router.get("/")(inject(view))
+
+    app.add_routes(router)
+    setup_dishka(providers=[provider], app=app)
+    client = TestClient(TestServer(app))
+    await client.start_server()
+    yield client
+    await client.close()
+
+
+async def get_with_app(
+        _,
+        a: Annotated[AppDep, Depends()],
+        mock: Annotated[Mock, Depends()],
+) -> Response:
+    mock(a)
+    return Response(text='passed')
+
+
+@pytest.mark.asyncio
+async def test_app_dependency(app_provider: AppProvider):
+    async with dishka_app(get_with_app, app_provider) as client:
+        await client.get("/")
+        app_provider.mock.assert_called_with(APP_DEP_VALUE)
+        app_provider.app_released.assert_not_called()
+    app_provider.app_released.assert_called()
+
+
+async def get_with_request(
+        _,
+        a: Annotated[RequestDep, Depends()],
+        mock: Annotated[Mock, Depends()],
+) -> Response:
+    mock(a)
+    return Response(text='passed')
+
+
+
+@pytest.mark.asyncio
+async def test_request_dependency(app_provider: AppProvider):
+    async with dishka_app(get_with_request, app_provider) as client:
+        await client.get("/")
+        app_provider.mock.assert_called_with(REQUEST_DEP_VALUE)
+        app_provider.request_released.assert_called_once()
+
+
+@pytest.mark.asyncio
+async def test_request_dependency2(app_provider: AppProvider):
+    async with dishka_app(get_with_request, app_provider) as client:
+        await client.get("/")
+        app_provider.mock.assert_called_with(REQUEST_DEP_VALUE)
+        app_provider.mock.reset_mock()
+        app_provider.request_released.assert_called_once()
+        app_provider.request_released.reset_mock()
+        await client.get("/")
+        app_provider.mock.assert_called_with(REQUEST_DEP_VALUE)
+        app_provider.request_released.assert_called_once()
diff --git a/tests/integrations/fastapi/test_fastapi.py b/tests/integrations/fastapi/test_fastapi.py
index c312423..b9a3cff 100644
--- a/tests/integrations/fastapi/test_fastapi.py
+++ b/tests/integrations/fastapi/test_fastapi.py
@@ -77,3 +77,25 @@ async def test_request_dependency2(app_provider: AppProvider):
         client.get("/")
         app_provider.mock.assert_called_with(REQUEST_DEP_VALUE)
         app_provider.request_released.assert_called_once()
+
+
+@inject
+async def additional(
+        a: Annotated[RequestDep, Depends()],
+        mock: Annotated[Mock, Depends()],
+):
+    mock(a)
+
+
+async def get_with_depends(
+        a: Annotated[None, fastapi.Depends(additional)],
+) -> None:
+    pass
+
+
+@pytest.mark.asyncio
+async def test_fastapi_depends(app_provider: AppProvider):
+    async with dishka_app(get_with_depends, app_provider) as client:
+        client.get("/")
+        app_provider.mock.assert_called_with(REQUEST_DEP_VALUE)
+        app_provider.request_released.assert_called_once()
diff --git a/tests/integrations/starlette/__init__.py b/tests/integrations/starlette/__init__.py
new file mode 100644
index 0000000..c89ddf9
--- /dev/null
+++ b/tests/integrations/starlette/__init__.py
@@ -0,0 +1,3 @@
+import pytest
+
+pytest.importorskip("starlette")
diff --git a/tests/integrations/starlette/test_starlette.py b/tests/integrations/starlette/test_starlette.py
new file mode 100644
index 0000000..d276d37
--- /dev/null
+++ b/tests/integrations/starlette/test_starlette.py
@@ -0,0 +1,83 @@
+from contextlib import asynccontextmanager
+from typing import Annotated
+from unittest.mock import Mock
+
+import pytest
+from asgi_lifespan import LifespanManager
+from starlette.applications import Starlette
+from starlette.requests import Request
+from starlette.responses import PlainTextResponse
+from starlette.routing import Route
+from starlette.testclient import TestClient
+
+from dishka.integrations.starlette import (
+    Depends,
+    DishkaApp,
+    inject,
+)
+from ..common import (
+    APP_DEP_VALUE,
+    REQUEST_DEP_VALUE,
+    AppDep,
+    AppProvider,
+    RequestDep,
+)
+
+
+@asynccontextmanager
+async def dishka_app(view, provider) -> TestClient:
+    app = Starlette(routes=[Route("/", inject(view), methods=["GET"])])
+    app = DishkaApp(
+        providers=[provider],
+        app=app,
+    )
+    async with LifespanManager(app):
+        yield TestClient(app)
+
+
+async def get_with_app(
+        _: Request,
+        a: Annotated[AppDep, Depends()],
+        mock: Annotated[Mock, Depends()],
+) -> PlainTextResponse:
+    mock(a)
+    return PlainTextResponse("passed")
+
+
+@pytest.mark.asyncio
+async def test_app_dependency(app_provider: AppProvider):
+    async with dishka_app(get_with_app, app_provider) as client:
+        client.get("/")
+        app_provider.mock.assert_called_with(APP_DEP_VALUE)
+        app_provider.app_released.assert_not_called()
+    app_provider.app_released.assert_called()
+
+
+async def get_with_request(
+        _: Request,
+        a: Annotated[RequestDep, Depends()],
+        mock: Annotated[Mock, Depends()],
+) -> PlainTextResponse:
+    mock(a)
+    return PlainTextResponse("passed")
+
+
+@pytest.mark.asyncio
+async def test_request_dependency(app_provider: AppProvider):
+    async with dishka_app(get_with_request, app_provider) as client:
+        client.get("/")
+        app_provider.mock.assert_called_with(REQUEST_DEP_VALUE)
+        app_provider.request_released.assert_called_once()
+
+
+@pytest.mark.asyncio
+async def test_request_dependency2(app_provider: AppProvider):
+    async with dishka_app(get_with_request, app_provider) as client:
+        client.get("/")
+        app_provider.mock.assert_called_with(REQUEST_DEP_VALUE)
+        app_provider.mock.reset_mock()
+        app_provider.request_released.assert_called_once()
+        app_provider.request_released.reset_mock()
+        client.get("/")
+        app_provider.mock.assert_called_with(REQUEST_DEP_VALUE)
+        app_provider.request_released.assert_called_once()
diff --git a/tests/unit/container/test_cache.py b/tests/unit/container/test_cache.py
index 32e26d6..5170c1a 100644
--- a/tests/unit/container/test_cache.py
+++ b/tests/unit/container/test_cache.py
@@ -46,18 +46,49 @@ async def test_cache_async():
             assert await state.get(int) == 2
             assert await state.get(int) == 2
 
+def test_nocache_sync():
+    class MyProvider(Provider):
+        value = 0
+
+        @provide(scope=Scope.REQUEST, cache=False)
+        def get_int(self) -> int:
+            self.value += 1
+            return self.value
+
+    with make_container(MyProvider()) as container:
+        with container() as state:
+            assert state.get(int) == 1
+            assert state.get(int) == 2
+
+
+@pytest.mark.asyncio
+async def test_nocache_async():
+    class MyProvider(Provider):
+        value = 0
+
+        @provide(scope=Scope.REQUEST, cache=False)
+        async def get_int(self) -> int:
+            self.value += 1
+            return self.value
+
+    async with make_async_container(MyProvider()) as container:
+        async with container() as state:
+            assert await state.get(int) == 1
+            assert await state.get(int) == 2
+
 
 @pytest.fixture()
 def alias_provider():
     class MyProvider(Provider):
         value = 0
 
-        @provide(scope=Scope.APP)
+        @provide(scope=Scope.APP, cache=False)
         def get_int(self) -> int:
             self.value += 1
             return self.value
 
         float = alias(source=int, provides=float)
+        complex = alias(source=int, provides=complex, cache=False)
 
     return MyProvider()
 
@@ -65,11 +96,17 @@ def alias_provider():
 def test_alias_sync(alias_provider):
     with make_container(alias_provider) as container:
         assert container.get(int) == 1
-        assert container.get(float) == 1
+        assert container.get(float) == 2
+        assert container.get(float) == 2
+        assert container.get(complex) == 3
+        assert container.get(complex) == 4
 
 
 @pytest.mark.asyncio
 async def test_alias_async(alias_provider):
     async with make_async_container(alias_provider) as container:
         assert await container.get(int) == 1
-        assert await container.get(float) == 1
+        assert await container.get(float) == 2
+        assert await container.get(float) == 2
+        assert await container.get(complex) == 3
+        assert await container.get(complex) == 4
diff --git a/tests/unit/container/test_decorator.py b/tests/unit/container/test_decorator.py
index 4ed84de..635796e 100644
--- a/tests/unit/container/test_decorator.py
+++ b/tests/unit/container/test_decorator.py
@@ -33,6 +33,36 @@ def test_simple():
         assert isinstance(a.a, A)
 
 
+def test_decorator():
+    class MyProvider(Provider):
+        a = provide(A, scope=Scope.APP)
+
+    class DProvider(Provider):
+        @decorate()
+        def foo(self, a: A) -> A:
+            return ADecorator(a)
+
+    with make_container(MyProvider(), DProvider()) as container:
+        a = container.get(A)
+        assert isinstance(a, ADecorator)
+        assert isinstance(a.a, A)
+
+
+def test_decorator_with_provides():
+    class MyProvider(Provider):
+        a = provide(A, scope=Scope.APP)
+
+    class DProvider(Provider):
+        @decorate(provides=A)
+        def foo(self, a: A):
+            return ADecorator(a)
+
+    with make_container(MyProvider(), DProvider()) as container:
+        a = container.get(A)
+        assert isinstance(a, ADecorator)
+        assert isinstance(a.a, A)
+
+
 def test_alias():
     class MyProvider(Provider):
         a2 = provide(A2, scope=Scope.APP)
diff --git a/tests/unit/container/test_generic.py b/tests/unit/container/test_generic.py
new file mode 100644
index 0000000..7db169b
--- /dev/null
+++ b/tests/unit/container/test_generic.py
@@ -0,0 +1,101 @@
+from typing import Generic, TypeVar
+
+import pytest
+
+from dishka import Provider, Scope, make_container, provide
+
+T = TypeVar("T")
+U = TypeVar("U")
+
+
+class Base(Generic[T]):
+    def __init__(self, y: T):
+        self.y = y
+
+
+class ReplaceInit(Base[str], Generic[T]):
+    def __init__(self, x: T):
+        super().__init__("hello")
+        self.x = x
+
+
+class A(Generic[T]):
+    def __init__(self, x: T):
+        self.x = x
+
+
+class B(A[U], Generic[U]):
+    pass
+
+
+@pytest.mark.parametrize(
+    "cls", [A, B, ReplaceInit],
+)
+def test_concrete_generic(cls):
+    class MyProvider(Provider):
+        scope = Scope.APP
+
+        @provide
+        def get_int(self) -> int:
+            return 42
+
+        a = provide(cls[int])
+
+    with make_container(MyProvider()) as container:
+        a = container.get(cls[int])
+        assert isinstance(a, cls)
+        assert a.x == 42
+
+
+class C(A[int]):
+    pass
+
+
+def test_concrete_child():
+    class MyProvider(Provider):
+        scope = Scope.APP
+
+        @provide
+        def get_int(self) -> int:
+            return 42
+
+        a = provide(C)
+
+    with make_container(MyProvider()) as container:
+        a = container.get(C)
+        assert isinstance(a, C)
+        assert a.x == 42
+
+
+def test_generic_class():
+    class MyProvider(Provider):
+        scope = Scope.APP
+
+        @provide
+        def get_int(self) -> int:
+            return 42
+
+        a = provide(A)
+
+    with make_container(MyProvider()) as container:
+        a = container.get(A[int])
+        assert isinstance(a, A)
+        assert a.x == 42
+
+
+def test_generic_func():
+    class MyProvider(Provider):
+        scope = Scope.APP
+
+        @provide
+        def get_int(self) -> int:
+            return 42
+
+        @provide
+        def a(self, param: T) -> A[T]:
+            return A(param)
+
+    with make_container(MyProvider()) as container:
+        a = container.get(A[int])
+        assert isinstance(a, A)
+        assert a.x == 42
diff --git a/tests/unit/container/test_resolve.py b/tests/unit/container/test_resolve.py
index 230d018..357f2bc 100644
--- a/tests/unit/container/test_resolve.py
+++ b/tests/unit/container/test_resolve.py
@@ -8,6 +8,7 @@ from dishka import (
     provide,
 )
 from ..sample_providers import (
+    A_VALUE,
     ClassA,
     async_func_a,
     async_gen_a,
@@ -15,6 +16,7 @@ from ..sample_providers import (
     sync_func_a,
     sync_gen_a,
     sync_iter_a,
+    value_factory,
 )
 
 
@@ -66,3 +68,20 @@ async def test_async(factory, closed):
         assert a
         assert a.dep == 100
     assert a.closed == closed
+
+
+def test_value():
+    class MyProvider(Provider):
+        factory = value_factory
+
+    with make_container(MyProvider()) as container:
+        assert container.get(ClassA) is A_VALUE
+
+
+@pytest.mark.asyncio
+async def test_value_async():
+    class MyProvider(Provider):
+        factory = value_factory
+
+    async with make_async_container(MyProvider()) as container:
+        assert await container.get(ClassA) is A_VALUE
diff --git a/tests/unit/sample_providers.py b/tests/unit/sample_providers.py
index abfb3e1..56791d7 100644
--- a/tests/unit/sample_providers.py
+++ b/tests/unit/sample_providers.py
@@ -1,5 +1,8 @@
 from typing import AsyncGenerator, AsyncIterable, Generator, Iterable
 
+from dishka import Scope
+from dishka.dependency_source import Factory, FactoryType
+
 
 class ClassA:
     def __init__(self, dep: int) -> None:
@@ -37,3 +40,15 @@ async def async_gen_a(self, dep: int) -> AsyncGenerator[ClassA, None]:
     a = ClassA(dep)
     yield a
     a.closed = True
+
+
+A_VALUE = ClassA(42)
+value_factory = Factory(
+    provides=ClassA,
+    source=A_VALUE,
+    dependencies=[],
+    type=FactoryType.VALUE,
+    scope=Scope.APP,
+    is_to_bound=False,
+    cache=False,
+)
diff --git a/tests/unit/test_provider.py b/tests/unit/test_provider.py
index 578fe02..1803e05 100644
--- a/tests/unit/test_provider.py
+++ b/tests/unit/test_provider.py
@@ -45,3 +45,81 @@ def test_parse_factory(source, provider_type, is_to_bound):
     assert factory.scope == Scope.REQUEST
     assert factory.source == source
     assert factory.type == provider_type
+
+
+def test_provider_class_scope():
+    class MyProvider(Provider):
+        scope = Scope.REQUEST
+
+        @provide()
+        def foo(self, x: bool) -> str:
+            return f"{x}"
+
+    provider = MyProvider()
+    assert provider.foo.scope == Scope.REQUEST
+
+
+def test_provider_instance_scope():
+    class MyProvider(Provider):
+        @provide()
+        def foo(self, x: bool) -> str:
+            return f"{x}"
+
+    provider = MyProvider(scope=Scope.REQUEST)
+    assert provider.foo.scope == Scope.REQUEST
+
+
+def test_provider_instance_braces():
+    class MyProvider(Provider):
+        @provide
+        def foo(self, x: bool) -> str:
+            return f"{x}"
+
+    provider = MyProvider(scope=Scope.REQUEST)
+    assert provider.foo.scope == Scope.REQUEST
+
+
+def test_self_hint():
+    class MyProvider(Provider):
+        @provide
+        def foo(self: Provider) -> str:
+            return "hello"
+
+    provider = MyProvider(scope=Scope.REQUEST)
+    assert not provider.foo.dependencies
+
+
+def test_staticmethod():
+    class MyProvider(Provider):
+        @provide
+        @staticmethod
+        def foo() -> str:
+            return "hello"
+
+    provider = MyProvider(scope=Scope.REQUEST)
+    assert not provider.foo.dependencies
+
+
+def test_classmethod():
+    class MyProvider(Provider):
+        @provide
+        @classmethod
+        def foo(cls: type) -> str:
+            return "hello"
+
+    provider = MyProvider(scope=Scope.REQUEST)
+    assert not provider.foo.dependencies
+
+
+class MyCallable:
+    def __call__(self: object, param: int) -> str:
+        return "hello"
+
+
+def test_callable():
+    class MyProvider(Provider):
+        foo = provide(MyCallable())
+
+    provider = MyProvider(scope=Scope.REQUEST)
+    assert provider.foo.provides == str
+    assert provider.foo.dependencies == [int]