A simplistic example of utilizing Pure DI in ASP.NET Core 5.0 Web API project.
The Composition Root is implemented in a custom IControllerActivator and IMiddlewareFactory. Main problem solved here is how to dispose of IDisposable dependencies, including objects with both singleton and request-scoped lifestyle.
Try to build and run the application in console, e.g. on Linux with
Debug | Any CPU
configuration:
$ dotnet build
$ ./AspNetCorePureDiApi/bin/Debug/netcoreapp5.0/AspNetCorePureDiApi
Open a web browser, and navigate to
http://localhost:5000/api/hello
. Refresh the page several times. In
the console, observe how scoped dependencies get disposed after each
request. The controller response includes ids of its dependencies (one
singleton and one scoped). A middleware then adds some extra
information to the response, reporting its own dependencies.
When finished, press Ctrl+C
in the console to shut down the
application to see that singleton dependencies get disposed as well.
Implementation replaces the default ControllerActivator and
MiddlewareFactory with a custom class which serves as the Composition
Root. The CompositionRoot
class is registered in default ASP.NET
Core's DI container (IServicesCollection
) as singleton and gets
disposed when application shuts down. This in turn, disposes all of
the singleton dependencies held by the CompositionRoot
. Scoped
controller dependencies are registered for disposal in
ControllerContext.HttpContext.Response
and get automatically
disposed by the framework along with the response. Scoped middleware
dependencies are disposed in a similar manner.
Note that despite the fact that all the DI logic is implemented in the
CompositionRoot
, I would probably delegate the responsibility of
creating dependency graphs to another class or classes in a real life
project.
See comments in the code for more information.
With the current implementation, when using IMiddlewareFactory to create request-scoped middleware, there are two scopes created per web request: one for middleware's dependencies and one for controller's. I.e. if for example both UserMiddleware and UserController depend on IUserRepository, and IUserRepository should have request scoped lifetime, then there will be two separate instances of IUserRepository created.
It is potentially possible to workaround this issue by using
AsyncLocal<IDictionary<Type, object>>
field that gets populated with
dependencies in IMiddlewareFactory.Create
method and consumed by
IControllerActivator.Create
. However we are loosing compile-time
checking of dependencies in the latter method (when creating a
controller), which is the whole point of Pure DI. Solving this problem
also moves us close to implementing our own DI container - not
something we aim to do here.
A compromise could be to only allow singleton middlewares, as in classic Microsoft.Owin pipeline but that of course poses other limitations.
It is entirely possible that there is a better way to do this, or that such implementation has potential problems which I don't see (except the separate middleware / controller request-scoped dependencies). Comments are welcome in the Issues section or in my Code Review question on StackExchange.