-
Notifications
You must be signed in to change notification settings - Fork 753
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fixed MVC dependency injection. #3520
Conversation
* Added a HttpModule (ServiceRequestScopeModule) that handles DI's request scope lifetime. * This enables scoped service resolutions for WebForms, MVC and WebApi. * It also stores the created scope inside the HttpContext.items for easy access (for vendors as well). * Setup the ServiceRequestScopeModule as part of the Application_Start initialization. * Register the WebApi controllers in the DI container.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks so much for contributing this Pull Request, it is great to have other developers working towards the collective goal of better Dependency Injection support in DNN.
The work in this Pull Request looks great but my biggest question, that needs to be answered by @mitchelsellers
- Are we comfortable having the Dependency Injection library take a hard dependency on System.Web. This will may create issues when migrating to .NET Core, it may not.
- Since everything is built towards an interface, we should be able to swap it out in theory. I wouldn't be doing my job if I didn't ask this question
Examples:
DnnMvcDependencyResolver
Leverages HttpContext.Current?.GetScope()
instead of building our own scope on the fly. This appears to be the correct way to do this in System.Web but now it generates a hard dependency on it
If we are okay taking a hard dependency on System.Web this looks like a great start to maturing the Dependency Injection to follow correct Scope standards in DI implementations
@dimarobert
If I am understanding the code, is the HTTP Module portion needed?
); | ||
foreach (var controller in controllerTypes) | ||
{ | ||
services.AddScoped(controller); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Controllers were explicitly removed from the Container because of memory leak problems. In earlier versions of Dependency Injection the controllers that got registered in the container would never be collected by the Garbage Collector. This means if you have 100,000 requests to a specific controller, you would have 100,000 instances of the controller in memory.
I am hesitant to add controllers back in unless there is a very good reason for it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see #3520 (comment) for the reasoning behind the memory leak.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
perfect, thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would like to re-do the same testing that was done to validate the fix that was put in place in 9.4.x to ensure that we don't have a leak, but this looks to be the better way regardless
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please note that if the controllers are not registered in the container it will not be able to resolve them. Which means that in 9.4.4 (apart from the provider being always null) the controllers were always resolved from the asp net DefaultControllerFactory _resolver
that was passed in, which only supports parameterless constructors, so no DI for them.
@@ -36,7 +37,8 @@ public DnnDependencyResolver(IServiceProvider serviceProvider) | |||
/// </returns> | |||
public IDependencyScope BeginScope() | |||
{ | |||
return new DnnDependencyResolver(_serviceProvider.CreateScope().ServiceProvider); | |||
var scope = System.Web.HttpContext.Current.GetScope(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This actually looks like it is the correct way to handle the DI Scope in System.Web
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ahoefling I assume this is the note of the dependence on System.Web. I'm ok with this as we are using System.Web now, so it makes sense, and this is internal to our stuff
|
||
namespace DotNetNuke.HttpModules.DependencyInjection | ||
{ | ||
public class ServiceRequestScopeModule : IHttpModule |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this mean you got Dependency Injection working in HTTP Modules or is this needed to access the System.Web Scope?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This HttpModule is the one that handles the entire request scope lifetime. It handles it for all module types (WebForms, MVC, WebApi). For WebForms we need to look at what is injected in the DependecyProvider property of PortalModuleBase. It should be the request scope provider and not the root one.
As stated in the TODOs, this should be the first HttpModule in the pipeline for BeginRequest so that the scope is opened as soon as possible in the request lifetime (all HttpModules after it can access the opened scope through the instance stored in HttpContext.Items - they will not have constructor DI though). The same HttpModule should be the last that executes it's EndRequest so that the scope is disposed as late as possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perfect, thanks for the explanation on what is going on with this code. It helps me a lot and I'm sure it will help others that review this change later.
This sounds like the correct way and I would argue the way I should have implemented this in the first place. I'm actually fine keeping this as an attribute on the class. Something like this shouldn't be configured in the web.config, creates another point of failure on a part of the platform that isn't designed to be plug and play
Here is an attempt at a complete explanation on how the service resolution works in the dotnet core DI system. You register the services in one container (IServiceCollection). The services lifetime can be Singleton, Scoped or Transient. You then build a service provider from that container using The service provider will hold a cache with all the services that it materialized throughout it's lifetime, no mater their lifetime.
When the service provider is disposed it will go through the cache and dispose all services that implement IDisposable. From the root service provider you can create a scope. What that means is that an IServiceScope instance is created from the root. It will contain a newly created service provider that can access the
When the scope is disposed all the services that were stored in it's cache and implement IDisposable will be disposed. Here is a gotcha with this: a Scoped service that is resolved from the root provider will become Singleton. If you think about it, when you resolve a service with Scoped lifetime from the root provider it will create and store it in it's cache, which means that for further resolutions, both the root and any scope providers that are created from it, will always return that cached instance, instead of creating a new one when needed. Another one would be that Transient services resolved from the root provider will pile up and create a memory leak. Because of the first one, the Root Service Provider can be created in two ways:
ConclusionRequest scope is kind of mandatory for a healthy DI architecture. Disclaimer: I am not completely sure if the Transient services resolved, that are NOT implementing IDisposable, are actually cached by the service provider, but the ones that do are definitely stored so that they are properly disposed. |
I'd rather have someone from the core team understand how everything here works in depth before we go forward. There are a couple other things that we can try. For example for WebApi we could create a custom IHttpControllerActivator instead of the IDependencyProvider. This will give us access to the HttpRequestMessage and in turn to the HttpContextBase when resolving controllers (decouple from HttpContext.Current). But then, we would not be able to replace services in the asp net pipeline (examples of services) from the container, if that is desired. |
@ahoefling @bdukes or others, might we setup a quick conference call |
I'm going to be unavailable on vacation for a week. I'm more than happy to do a more thorough review when I get back the week of 2/3/2020 |
To add a bit of business context, DNN Sharp users can't upgrade to DNN 9.4 because of this issue. It would help us if we can tell users an approximate time of release and also if it will be version 9.5 or 9.4.5. |
@bogdan-litescu Can you shed any additional light on the impact here? Were you using dependency injection PRIOR to 9.4? This was a new feature in 9.4.x. I just want to make sure that we aren't chasing a red-herring in your case. |
@mitchelsellers yes we are using our own DI for about 3 years now. |
@bogdan-litescu @dimarobert so is DNN Sharp planning to update their DI to hook into this request scope? |
@bdukes Robert is implementing a layer of abstraction so on DNN <9.4 we use our previous DI library (DryIoc it's called by the way - very powerful, very lean) and on DNN 9.4+ it will use the one that ships with DNN. With DryIoc we already hook into request scope, MVC, Web API, etc. and actually, that's what's causing the conflicts right now. Our goal is to get similar capabilities with the DNN implementation, otherwise, we'll have to downgrade a lot of code. By the way, if there's a chance to adopt DryIoc as the DI provider in DNN we would like to have that conversation. The one in .Net core is pretty basic. |
I've run a few verification tests, this is looking good to me 👍
|
@bdukes I too have tested this in this manner. There is a TODO: Note from the user about moving the inclusion to the web.config, however, given the nature of how this is done etc, I actually think that the safer route is to leave this implementation as-is and use the attribute. Thoughts? Do we merge this in since it has been validated and we know that 9.4.4 is broken? |
Yes, I agree, leaving it as is would be the best approach, and I'd be good with merging it for 9.5 |
/azp run |
No pipelines are associated with this pull request. |
@bdukes Same here....but it appears that a build is in progress...... |
@bdukes @mitchelsellers @ahoefling I've made some changes/improvements by adding the IScopeAccessor that I was talking about #3520 (comment). It gets rid of the System.Web dependency in the MVC and WebApi projects. If that is something that you think is worth it, merge that PR so that the changes reflect here. |
Thanks @dimarobert, that change looks good to me. I'm going to build it and re-run my tests, but I think we can go ahead and include that change. |
@dimarobert the build is failing with three scope related null reference exceptions. Can you look into those? https://dev.azure.com/dotnet/DNN/_build/results?buildId=27962&view=ms.vss-test-web.build-test-results-tab&runId=1085634&resultId=102042&paneView=debug |
Not sure if adding your IScopeAccessor changes will have any effect on those tests, either… |
@bdukes I've made an update to the dimarobert#1 PR to fix the unit tests. The fix was simple due to the IScopeAccessor. |
Thanks @dimarobert! I've updated this PR with those changes. They also passed my manual testing (no errors, no memory leaks). I'm good to merge this @mitchelsellers if you are. |
Awesomeness @dimarobert - great teamwork! |
Thanks everyone! This is merged! |
Fixes #3513
Summary
Added a HttpModule (ServiceRequestScopeModule) that handles DI's request scope lifetime.
Setup the ServiceRequestScopeModule as part of the Application_Start initialization.
Register the WebApi controllers in the DI container.
Used example from dotnet/aspnetcore#3172 as inspiration.
TODOs:
ServiceRequestScopeModule
HttpModule through the web.config, get rid ofPreApplicationStartMethod
assembly attribute inServiceRequestScopeModule
and theMicrosoft.Web.Infrastructure
package. The module should be the first in the pipeline for BeginRequest and the last for EndRequest, so that as much of the request as possible is covered.