castleproject / Windsor

Castle Windsor is a best of breed, mature Inversion of Control container available for .NET

Home Page:http://www.castleproject.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Integration for Microsoft.Extensions.DependencyInjection (ASP.NET Core)

cwe1ss opened this issue · comments

Hi,
I created an integration for Microsoft.Extensions.DependencyInjection.

You can find the code here: https://gist.github.com/cwe1ss/050a531e2711f5b62ab0

The code is based on the following links:

I adjusted the code for RC1 and fixed some issues:

  • Components were added with .OnlyNewServices() which resulted in services that are supposed to exist multiple times not being added
  • GetService<IEnumerable>() was not resolved properly. I had to add special handling for IEnumerable<>
  • ASP.NET Core doesn't expect GetService() to return an exception if the component wasn't registered. (they use it for optional dependencies). For this reason, I added a check to Kernel.HasComponent()

I'm not sure why the call to BeginScope() in Startup.cs is necessary - if it's missing, the first request fails. Seems like ASP.NET Core doesn't start the scope for the first request or some Scoped service is requested before it does.

best regards!

I moved the code into a repository. you can find it here.

@cwe1ss sorry for not replying to your original post, I've had some personal things going on over the last couple of months which have really pulled me away from Castle.

Firstly thanks for building the integration. Unfortunately I've done zero with ASP.NET Core and the new DI framework built into it so can't really comment as to the problems you've encountered.

I assume you'd like Windsor to ship with this integration? You pasted links to code and problems, followed by "best regards", unfortunately it isn't going to get into Windsor without someone contributing the work. Your gist doesn't have a license, while your repo is licensed under the MIT license, contributions to Windsor must be licensed under the Apache 2 license.

At this point in the lifecycle of ASP.NET/DNX/CLI/etc I think it is probably best if you maintain the integration until things settle down and stabilise. My focus at the moment is on Castle Core and getting it ported to .NET Core. Windsor will obviously come next, but at the moment the build system of Windsor is completely incompatible with project.json so our learnings from Castle Core will flow into fixing that up too along with porting to .NET Core so we can accept this integration without it being just for the .NET Framework. Thoughts, opinions, feedback?

Hi @jonorossi,
we've had some serious issues with the integration during the last weeks/months - mostly related to how scopes and sub-containers are handled in Microsoft's DI framework. I'm not sure if Windsor is really compatible with their container. Seems like other DI vendors (especially SimpleInjector) don't even want to create an adapter.

I don't know much about the Windsor internals, so I didn't want to invest too much time. Instead, we decided to remove Windsor and go with the integrated DI from Microsoft for now (albeit it has a lot less features).

This means I don't really have a motivation to maintain the integration. So it's up to you to decide if and when to do something in this area!

Feel free to fork my repository, in case you need the files for future reference. I will delete the repository in a few weeks, since I believe it's broken and therefore not really helpful for anyone except you.

Hi @cwe1ss

I'm using Castle Windsor in aspnet boilerplate (http://www.aspnetboilerplate.com/) and now want to move to AspNet Core. I faced to the same problem with you. It seems the biggest problem is the Scope implementation.
Did you try to use WindsorContainer.AddChildContainer method to return a ServiceScope?

No. As far as I understand them, windsor child containers are used at service registration time and not at resolve time. ( http://mikehadlow.blogspot.co.at/2010/05/10-advanced-windsor-tricks-13-use.html ). This could be wrong though - I don't know much about Windsor.

Hmmm... yes, I'm creating some unit tests and it's like that.
I created a child container, added to main container (thus I can resolve classes registered to the main container), resolved some transient objects from child container then disposed the child container but it did not release resolved objects.

I'm investigating docs and source code of Windsor and I suppose we should implement a custom LifeStyleManager (https://github.com/castleproject/Windsor/blob/master/docs/lifestyles.md#custom-lifestyles). This manager should return the same instance for same scope and destroy all instances when IServiceScope.Dispose() is called by AspNet.

I'm not sure if this will be enough. ASP.NET creates a scope (= a child ServiceProvider) per request. You can have transient and scoped services in such a scope so they have a different lifestyle (scoped = once per request). All of them have to be disposed on scope.Dispose().

but feel free to fork my repository and give it a go :)

OK, I created an adapter library based on your solution and it's passing all unit tests: https://github.com/volosoft/castle-windsor-ms-adapter
Can you check it? I will publish it to nuget but there is a problem with nuget.exe with AspNet RC2 packages currently.

I guess the usage of ThreadStatic might be problematic - AFAIK it's not compatible with the new async/await world. You might have to store this in CallContext, AsyncLocal ?

Also, what would happen if there were multiple child scopes within one request?

Hi,

I know ThreadStatic has problem with async/await. But I used it just while resolving dependencies and Windsor's resolving operation does not include async/await. Whole resolving progress is performed in a single thread (Windsor's resolve mechanism has also similar ThreadStatic mechanism) So, it should not be a problem.

It supports multiple scope (unit tests also have such cases). Also, there is no child scope actually in MS's DI. There can be multiple scopes nested, but they don't have child/parent relation. While this does not effect it, I just wanted to notice.

I suppose finally it's working. You can try it from nuget: https://github.com/volosoft/castle-windsor-ms-adapter#how-to-use

But surely it not works for .NET Core, it working for full .NET Framework since .NET Core is not supported by Windsor yet.

I won't pretend I know anything about how the DI container works in ASP.NET Core and I haven't looked at @hikalkan's implementation, however have you guys looked at how the current per web request lifestyle works (i.e. ScopedLifestyleManager used by all scoped lifestyles and WebRequestScopeAccessor)?

We can not use webrequestscopeaccesor, because Microsoft.Extensions.DependencyInjection's scope approach is different and related to an IServiceScope instance.

We can not use webrequestscopeaccesor, because Microsoft.Extensions.DependencyInjection's scope approach is different and related to an IServiceScope instance.

@hikalkan I didn't mean to imply that you could use WebRequestScopeAccessor, I'm aware that HttpContext.Current is no longer present in ASP.NET Core, I meant have you looked at how they are implemented.

Scoped lifestyles in Windsor 3.0+ already implement the interfaces you've implemented yourself. You'll see the LifetimeScopeAccessor will keep track of the scope via CallContext (and only fallback to ThreadStatic on Silverlight). For ASP.NET you need to use CallContext rather than ThreadStatic (as HttpContext does) because IIS performs thread agility where it reuses OS threads while a request is blocked on I/O.

Using the Windsor scoped implementation should simply the implementation to very little, unless I'm missing something fundamental with how the ASP.NET Core DI works, which I may be as I can't find any documentation.

Nancy's Windsor integration might help to show how they are using LifetimeScopeAccessor (and WebRequestScopeAccessor) to handle non-ASP.NET pipeline (and ASP.NET pipeline requests): https://github.com/NancyFx/Nancy.Bootstrappers.Windsor. You obviously don't care about the WebRequestScopeAccessor bit but the other half.

Hi,

Surely, I opened Windsor's source code in my Visual Studio and investigated reated classes including WebRequestScopeAccessor for hours :) I also investigated source code of Microsoft.Extensions.DependencyInjection.

CallContext can not work since AspNet Core's lifetime does not depend on callcontext. You can create arbitrary scope objects (IServiceScope), store and use them whenever you need. ThreadStatic is worse since it can not work with async pattern. Even there is a HttpContext, we don't rely on it. Also, Microsoft.Extensions.DependencyInjection package is independent from web pipeline.
Surely, I would use a standard mechanism of Windsor if it's possible, but I suppose it's not. My implementation is the most similar implementation to AspNet's, I suppose :)

@hikalkan ah, I didn't realise the IServiceScope was just an arbitrary scope object, thought they were created and destroyed in the call chain. Could you implement Windsor's IScopeAccessor using a ConcurrentDictionary<IServiceScope, ILifetimeScope> rather than a ThreadStatic?

I would not want to use ThreadStatic at all actually. I used it just while resolving an object to understand which scope is being used in this resolving operation. If I use that dictionary, how I will transfer the scope information to the resolving code to ensure they use the same lifetimescope. Resolving code can be something like that:

var myService = _serviceScope.ServiceProvider.GetService(typeof(MyService));

myService instance and all dependencies and all their dependencies should be related to the _serviceScope instance and all they should be released when _serviceScope.Dispose() is called by AspNet. Without a threadstatic, how can I relate this serviceScope with the all objects resolved in this particular GetService method call?

If you need something that works with TPL, check AsyncLocal<T>, or more error prone LogicalCallContext.

threadstatic/async stuff is not a problem, believe me. Because, Windsor's Resolve code is single threaded (it even use ThreadStatic internally, it won't work if this was a problem).

And also, https://github.com/volosoft/castle-windsor-ms-adapter works properly as I tested. But would be good to get some code review.

Any news about this one?

@Kralizek - Please see #283. There is a wealth of info there.

I have not touched on this for a while because there were other things that took priority. I honestly don't believe supporting the abstractions for Microsoft.Extensions.DependencyInjection is viable because of impedance mismatches in the design. It leads to behaviours that are difficult to maintain.

It has also earned Windsor the title of a non-conforming container. Not even sure what that means. I am planning to dust off the work I have been pursuing on this in the next few weeks.

How is people supposed to use Castle Windsor in ASP.NET Core applications? Should we do all the built-in registrations again?
Also, many library creators are creating setup helpers based on IServiceCollection.

Non supporting the de-facto standard in .NET (Core) will eventually mean pushing Castle Windsor into oblivion. And that would be sad.

Non supporting the de-facto standard in .NET (Core) will eventually mean pushing Castle Windsor into oblivion. And that would be sad.

Windsor relies on contributions, @fir3pho3nixx isn't saying Windsor won't support it, more just that we haven't worked out how within the constraints of what we've got on both sides. I encourage you to continue the discussion, I personally don't have the time to work on this in the short or medium term, but would like to see those that have spent energy on this already get together and nut out or workaround remaining issues so Windsor can support this.

Don't get me wrong. I have extreme respect for the people who pulled off the .NET Core support.

But my comment was about attitude, intentions and future plans. When people write "I believe X is not viable" and X is the biggest thing happened in the .NET space in the last couple of years, you are putting yourself in the wrong mindset.

Let me write it explicitly again, my criticism is not about "not having done something because lack of resources", rather "CW is a great product (it really is or i wouldn't be here) and we define our own standards, people will follow us".

The focus should be giving users the possibility to jump on CW from a dotnet newed project in three steps. And those three steps, like it oe not, are necessarily passing by supporting Microsoft.Extensions.DependencyInjection. Then it's just about deciding the level of support.

Again, I'm a "dedicated customer" of CW. Dedicated enough to have googled for a solution for those three steps. I found "https://github.com/volosoft/castle-windsor-ms-adapter" but the vibe i get here is that it's compiling solution rather than a sustainable one (aka, can i go live with it?). If i weren't so committed into using CW, I would have already jumped ship.

@Kralizek https://github.com/volosoft/castle-windsor-ms-adapter is maintained by us and it's used in production by our customers. It's not something just compiles. We have spent significiant time to adapt Windsor to MS DI (it was hard). So, I think you should give a chance to it :)

Great!

@Kralizek I'm not writing on behalf of the project and/or the people working on it atm. This is a personal opinion.

But as a long time OSS contributor, this is the kind of behavior that pisses off everyone working it.

We (oss devs) don't get paid for our work, so you're not a customer (download it or using in an very importante/production project also doesn't make you a customer). Also, we don't care if you googled a solution to fix an issue for the people who do pay your salary.

So, next time (unless you're willing to contribute with an actual solution that involves contributing source code), please: save the smarts thoughts/criticisms about long term strategy and priorities to yourself.

I do believe an implementation around Missing Core DI Extensions is the best way to go in terms of maintainability for the Castle Windsor Project. I will list advantages and disadvantages below.

@hikalkan feel free to weigh in, you and I have done heaps of work on the Microsoft.Extensions.DependencyInjection.Abstractions space and I feel you are the leading authority on this approach. Counter arguments from you are most welcome indeed.

Advantages:

  • Castle Windsor has a first class mature registration API. The registration API extension's for ServiceCollections are incredibly naive from my point of view. Also it is woefully opinionated for the AspNet team to expect container's to forgo their years of hardening and design around this and to merely adapt to their desired service state configuration for a container. Missing-DI-Extensions promotes the use of the underlying containers public registration API's. This gives access to things like IWindsorInstaller's and not to mention all the namespace registration slicing, conventional registration power along with things like specifying interceptors using Castle Core. By comparison their registration API is absolute garbage.

  • We do scoping properly, they simply don't. Our scopes are a single responsibility which applies to the lifestyle of a dependency under certain registration conditions with predictable burden based decommissioning concerns(not including parent/child containers in this statement). Their scopes blend in a service locator pattern which mutates lifestyle outcomes depending on whether it was consumed inside or outside of a scope but also imposes constraints on singletons. It also requires the new'ing up of child container(not in the windsor sense) which is just plain rubbish when it comes to memory management and compared to how Windsor does it today.

  • Singleton's live on, transients consumed by them get disposed. Eh? What? Yeah, with the Missing-DI-Extensions approach you wont have this problem.

  • You don't have to figure out the internals of AspNet to adapt their desired service state configuration to Windsor via the bizarre Microsoft.Extensions.DependencyInjection.Abstractions suggestion. @hikalkan you conquered this, but already I can tell you have to mutate lifestyles to fall back on to other lifestyles achieve this.

  • AspNet do what they need to do and you are in control of your container using the native functionality. It is unfortunately a world of two halves. You just get to pick the one you want for your stuff without the lacking API's or blatant inefficiencies.

  • Another advantage or something Windsor tries to support is disposing things in the right order. This might be a little counter intuitive when implementing the adapter.

Disadvantages

  • You have to register framework services/extensions yourself. This can be problematic if you misunderstand what they needed/meant or they did not bother to document before releasing stuff. I personally don't believe this to be a big problem at all.

  • You are effectively running 2 containers. The AspNet internal one which you are side stepping, and the consumer one which is your choice. This could be considered bad, although I would like to hear elaborations from people on this thread as to the severity.

Summary

After walking @hikalkan's path myself, I can honestly say if you want a registration API that relies on ServiceCollection extensions then you should definitely use that.

If you want to use Windsor, go all in ... that includes the Windsor Registration API along with all the buttery goodness it has to offer.

I finally have time to sit down and read where this conversation is going.

I am really sorry that my intentions were misinterpreted and I here offer my deepest apologies for the tone of the posts. In no occasion I wanted to be disrespectful to the work put into this amazing project by every single contributor. I understand that the tone used in my comments was heavy and ungrateful but, believe me, I never wanted this to be the point.

I might have been swinged away by comments written before things were actually the way they are right now, but this is not a good excuse for using such a tone in my posts.

Again, if any of you felt insulted, I am deeply sorry for that.

@Kralizek - We all get peeved sometimes seriously not a problem. If you have been following the notifications for this project you would have noticed that I have been doing a lot of research in this space over the past few months. I honestly cannot take the credit for everything. @dotnetjunkie did a lot of leg work pursuing this with the aspnet team. It was only when I started bombarding the same team with a multitude of issue's that I was pointed towards this issue.

My problem with supporting the adapter is the aspnet team gives you a suite of tests that you are supposed to make pass for your container, this unfortunately dictates really bad things in terms of design. It also reveals how inferior their design is when comparing it to something like Castle Windsor.

Anyone supporting this is either hacking around their malformed design(which the aspnet team are not interested in fixing) or actually implementing IoC incorrectly. I believe Windsor is better than that. I would literally have to break Windsor to support this nonsense.

** Edited **

I am willing to support AspNet Core, but only where Castle Windsor is a first class citizen.

@fir3pho3nixx

I would l to respond to your list of advantages and disadvantages:

The registration API extension's for ServiceCollections are incredibly naive from my point of view. [...] their registration API is absolute garbage.

Do note that it has never been Microsoft's intention to have a registration API that completely replaces that of a third-party container. In that case it would be rather useless to use a 3rd party container. The abstraction is merely meant to be able to push all the framework and 3rd party component registrations into a third-party container and to let application developers use their container’s native API on top of that, so they can have features like batch-registration and interception. So the idea always was that application developers registered their application components using the native API.

Problems however arise because there are now two APIs that might have completely different (and sometimes incompatible) behavior. When an application developer uses the MS API, can he expect the registrations to behave the same as when done through the native API? And what if object graphs are composed of both MS API registrations and native API registrations? Which rules should apply? Native rules or MS rules? Differences between the two could be very subtle.

Singleton's live on, transients consumed by them get disposed.

I tested this last week with Core v2. Transients, injected into singletons get disposed after their singleton gets disposed (i.e. they're scoped to the container). I'm unsure whether this behavior was different before.

You have to register framework services/extensions yourself. This can be problematic if you misunderstand what they needed/meant or they did not bother to document before releasing stuff. I personally don't believe this to be a big problem at all.

This is actually the hardest part in the framework/application container split. A big warning here: Do not advice your users to re-register framework components in Castle Windsor. This leads to extraordinary amounts of pain, because users will have to redo complete lists of registrations for object graphs for what they are only interested in getting the top-most object. This leads to large amounts of code duplication–for each framework component or third-party component they are using, and this block of registration needs to be updated with each new version of such component. This would lead to a lot of work and strange bugs after upgrading to a new version. On top of that, since there might be subtle differences in the registrations APIs, it might lead to weird bugs if users try to do this themselves. Having the writer of a DI Container create a integration package however, makes little sense, because it would force a tremendous burden on the maintainers of such DI Container.

Another bug that you might run into when doing this is that of Torn Lifestyles. This can happen when a framework or third-party component is registered as Scoped, and we re-register it again as Scoped in Castle Windsor. This might lead to having 2 instances of that component during the same request.

Instead, framework and third-party components should be cross-wired (this is a term I coined for the occasion). Cross-wiring means that we register a delegate in Castle Windsor that redirects the call to the MS container to resolve it for us. This way, the MS container is in control over the composition of that component and all its dependencies. The only thing we do is get it from the MS container and inject it into our object graph.

I always tell my users that their application should have very little dependencies on those application or third-party components, and that the amount of cross-wired dependencies should be very low. This means that it shouldn’t be a problem to cross-wire the few dependencies that exist.

Although I still feel this way, that statement however has proven to be a little problematic for application developers. A good example is when Identity Framework Core is used and especially when mixed with its templates that are installed in Visual Studio. To get the default template working with a non-conforming container is quite annoying.

Another problem with this approach is, that while cross-wiring, you need to be aware of the lifestyle of a component in the MS container, to prevent running into Captive Dependencies. A better approach would be to register these dependencies as Func<T> singletons that resolve from the MS container at runtime instead of at time of composition. Although I do endorse this model, it confuses developers, again because of the default Identity Visual Studio template, which needs to be refactored quite a bit.

Because of this, the Simple Injector community has been experimenting with some ‘auto cross-wire’ feature for Simple Injector (see code). This will try to cross-wire a registration automatically when the container encounters a missing registration in the Simple Injector container.

You might think this reintroduces the Conforming container, but this is not the case, because it completely circumvents the MS registration API, and only makes use of its Resolve functionality (so it’s a Service Locator at most–not a Conforming Container). It does however require some knowledge on what the meaning is of the IServiceCollection, because it needs to check whether or not a registration exists, and it also skips resolving collections, because behavior of collections is incompatible between MS and Simple Injector, even when just resolving collections.

This is still in the experimentation phase however. Although some users reported great success, it’s still a bit unclear the exact downsides of this approach.

One thing I would really like to see is such extension to be moved in a Microsoft assembly. In an ideal world, even non-conforming containers should need no integration packages what so ever. Castle Windsor should only have a piece of documentation explaining how to plug in the container into the Microsoft pipeline using the Microsoft supplied code. This is one of the promises that Microsoft made when building their Conforming Container. They wanted to end the need for endless permutation of integration libraries.

Unfortunately, that’s not the world we live in (and I don’t think we have to expect too much from Microsoft in this respect), so we are forced to still create our own integration packages for each new framework on top of .NET Core.

You are effectively running 2 containers. The AspNet internal one which you are side stepping, and the consumer one which is your choice.

Do note that out-of-the-box, ASP.NET Core already runs with 2 container instances. From the initial set of registrations a ‘hosting container’ is built. Later on, that same set of registrations is used to build a container that contains the registrations for MVC and your application components. This happens even if you use the built-in MS container, and since those containers aren’t cross-wiring, it can already lead to Torn Lifestyles.

This could be considered bad, although I would like to hear elaborations from people on this thread as to the severity.

I don’t agree with this. I always try to explain to developers that, from their perspective, there is only one container, which is their application container (in your case: Castle Windsor). The other thing is not a container, it is the framework’s configuration system. Older .NET frameworks had their own configuration system as well (ASP.NET Forms mainly XML based, and MVC and Web API contained their own Services collections that you could add and remove stuff to/from). In ASP.NET Core the configuration system is more complex, has more features, but it is still a configuration system. In the past we had our own application container for our own application components. As I see it, if we use an non-conforming container, this is the exact same model.

But even if you see the framework’s configuration system as a container, there’s nothing wrong in having two separate containers. You achieve a pretty nice separation between the components you are interested in (your application components) and the things you are typically not interested in (framework infrastructure). By separating these two worlds, your application container is not polluted with hundreds of registrations that are not of your concern. Keeping things separate is IMO the most sensible thing to do.

@dotnetjunkie

Been out for a while and just finished the AspNet framework facilities for Windsor.

Thanks for the "auto cross wiring" example very useful indeed. At best we should try to make this a "must have" feature for the AspNet Core facility. I also did not think about the implications of any third party integrations. This is definitely something I will investigate further to be sure.

Think my next move here is to try and get all of this coded up in a sample github repo, where we start emulating the auto cross wiring. I will also experiment with the identity integration and report back on how things went.

If anyone else has suggestions on features they use, I can try add them in as we go. For now I will start by tackling the basics and after which we can start ramping it up a bit.

@dotnetjunkie

I managed to achieve the "auto cross wiring" feature using a sub dependency resolver. This is a rather tidy result for injecting framework dependencies.

I noticed I did not need the factory method you use to avoid "Captive Dependencies", I was wondering if you had examples of how this happens.

My proposed startup class now looks like this

If anyone has comments for https://github.com/fir3pho3nixx/Windsor.AspNetCore it would be handy if you could jump in before I test and port to Windsor. I will wait till the end of the week before raising the PR.

I noticed I did not need the factory method you use to avoid "Captive Dependencies", I was wondering if you had examples of how this happens.

You won’t need that factory method in case you copy the lifestyle from IServiceCollection, but that implies a few things:

  • You are able to safely determine the correct ServiceDescriptor that will be used to get the service.
  • Lifestyles of Microsoft.Extensions.DI work identical to your container.

I created a factory-method registration because without analyzing the IServiceCollection, you have no idea what the lifestyle will be (and even if you know, it might change over time), and this means that the only safe way to resolve instances without the chance of getting Captive Dependencies is using a factory method.

You won’t need that factory method in case you copy the lifestyle from IServiceCollection

Interesting, I think the lifestyle conundrum is by passed by the fact that the dependency is sub resolved using the ServiceProvider over here without any knowledge of how it is configured.

It is almost like Windsor gobbled up the vanilla ASPNET Core ServiceProvider and uses it for dependencies it deems to be resolvable from the framework configuration container without the application container ever needing it registered! This is great because Microsoft can change this as much as they like and it wont cause breaking changes in the facility or any application developers code that uses it.

To clarify further I used your example of injecting the ViewBufferScope into the HomeController but you wont find any registration for it in the Windsor Container on Startup. Think we are good!

Thanks for clarifying :)

Closing re: #376

Please re-open if you need to discuss anything else. Release is imminent.

Thanks to all for participating! :)

@dotnetjunkie

I noticed our first need for cross wiring. It was actually @generik0 that exposed the issue on our side. We ended up embedding callbacks(factory methods) to the IServiceCollection that resolved from our container. This put us back on top with our registration API.

I will tag you in our issue tracker if I think we find something relevant that might need some consideration. :)

This is the formula we used to crack "two way" resolves:

Look forward to your feedback. You were right :)

Hi @fir3pho3nixx,

I'm a bit worried about your FrameworkDependencyResolver. You seem to be building a new ServiceProvider (on line 43). This can easily lead to dependencies having a Torn Lifestyle. Because you will create a second M.E.DI container instance, with its own dependency cache.

When a singleton registration is made, for instance, both the framework-built, and your FrameworkDependencyResolver-built instance will get its own instance of that registration. This could lead to very subtle, and hard to trace bugs, which might only appear in production.

@fir3pho3nixx @dotnetjunkie
This is really interesting. I have just a question for brainstorming,
Could it be an idea to set the app service provider, to the castle frameworks service provider?
Then we might not need to cross wire for @Inject either ?
I.e. under:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.ApplicationServices = //CFrameworks service provider
                //or
           app.UseCastleWindsorServiceProvider();

It would of cause mean that eeryting in the service collection and builtin provider needs to be present in the container.?

image

Please note, this is just a thought. Don't shoot me if I am completely wrong about it all....

I did a test. not good code, but stil a test if the FrameworkDependencyResolver could replace the ApplicationServices.

It does seam to work...!!! App spins up, everything is resolving
Any thoughs?

image

I'm a bit worried about your FrameworkDependencyResolver. You seem to be building a new ServiceProvider (on line 43). This can easily lead to dependencies having a Torn Lifestyle. Because you will create a second M.E.DI container instance, with its own dependency cache.

@dotnetjunkie Correct!

You are snap-shotting the dependency graph when you build the service provider. This leads to all sorts of problems.

First is an ordering/timing problem of when things are cross wired vs when the service provider gets created. I tried to push service provider creation all the way down to the first resolve using null coalescence for the reference as a singleton. A "just in time" kind of thing. I don't think this has entirely solved the problem but it does minimise collatoral damage when you start integrating 3rd party libraries or start using things like @Inject in your razor views(nasty!).

Second is creation of the service provider is memory intensive. If you go all in and keep recreating it for every resolve you end up thrashing the absolute hell out of the process's memory. By making service provider creation as late bound as possible and not re-creating it reduced our memory footprint from 1+GB to 300MB or such.

I also tried auto cross wiring, but that can have some disastrous consequences depending on when things happen and for performance. I noticed torn lifestyles then and immediately did a 180 and backed out of that idea. It also prompted me to start reading up on some of the simple injector issues. Not easy at all.

I think we need to firm up the definition of what a "cross wire" is and document it from there. It would also hopefully inform the working knowledge of parent/child containers.

Lastly thank you for being the bastion of calm and sanity in this. I am rather peeved at how this has gone and all the time that of all of the developers that have wasted on this.

@generik0

What have you found? I did think in the back of my mind that the ApplicationBuilder was the reference point for a service provider.

When I rolled this test I explicitly left this unused variable behind: https://github.com/castleproject/Windsor/pull/394/files#diff-9cd8e6d1d0ec81f11c73998ce083a4e0R42

@jonorossi I think we need a branch called aspnet-core-windsor, so we can explore this further with more experimental PR's.

Hi @fir3pho3nixx
I probably found nothing. I think it was more of a question. As far as i could read from @dotnetjunkie 's post he was worries about the FrameworkDependencyResolver having a service provider which ment there was more than one service provider in play.

So my question was just, cant we replace the app service provider with the FrameworkDependencyResolver at app configure. I tried it, (adding an interface FrameworkDependencyResolver that exposes the service provider and registered it to the container,[for testing]) and it appears that the application does at least not crash and does spin up fine. But needs much more testing. as the existing service provider does have a lot of resolved and registered services the FrameworkDependencyResolver.ServiceProvider does not..

Its just a though, and a question, Could this path resolve @dotnetjunkie 's worries? Or is it just another hack to get this facility to work? (Not blaiming the Facility, you have done a really great job, blaiming MS for making something that is so hard to plug into.)

image

@fir3pho3nixx, waait a second. did I read @dotnetjunkie message incorrect. Is the message not about having 2 x ServiceProviders, but having a service provider, and a windsor container?
Meaning we have 2 Service Providers, and 1 Windsor Container? And this can lead to torn lifestyles?

@generik0,

In case you add a singleton registration to the service collection, every service provider you create from that collection will get its own cache and thus its own 'singleton' instance. Although not always a problem, it does mean the singleton gets torn, meaning that you lose the guarantee that there is only one instance.

This same problem exists when you have both a service provider and a Windsor container, if you copy registrations instead of cross-wiring them; the effect is the same.

@dotnetjunkie

Do you know anything about who owns responsibility of creating the ServiceProvider? As @generik0 spotted before you can dereference it from the app builder but I am almost certain it will become a timing issue of when or where one tries to dereference it.

@fir3pho3nixx,

In an ASP.NET Core application it is done by the framework by one of the classes that gets called in the Main method. I think that's the AppBuilder.

I'm unsure that dereferencing is a solution, because by that time instances might have been resolved already. That is probably what you are referring to as "a timing issue".

@dotnetjunkie and @generik0

I spent the afternoon reading the code in the aspnet/Hosting repo (specifically how the WebHostBuilder works) and preparing a new branch which demonstrates in code what @dotnetjunkie is saying.

Please see ServiceProvidersAreStupidTestCase.

[TestFixture]
public class ServiceProvidersAreStupidTestCase
{
	/// <summary>
	/// A test that proves service provider's are absolutely lethal when it comes to singleton's that manage shared state.
	/// At the time of writing there were 14 references to this in the aspnet/Hosting repo. :/
	/// https://github.com/aspnet/Hosting/search?utf8=%E2%9C%93&q=BuildServiceProvider&type=
	/// </summary>

	[Test]
	public void ServiceProviders_should_be_rubbish_when_it_comes_to_singletons()
	{
		var serviceCollection = ServiceCollectionBuilder.New();

		serviceCollection.AddSingleton<MyFakeSingleton>();

		var firstServiceProvider = serviceCollection.BuildServiceProvider();

		var singletonA = firstServiceProvider.GetService<MyFakeSingleton>();

		var secondServiceProvider = serviceCollection.BuildServiceProvider();

		var singletonB = secondServiceProvider.GetService<MyFakeSingleton>();

		Assert.That(singletonA, Is.Not.EqualTo(singletonB));
	}

	public class MyFakeSingleton { }
}

At the time of writing there were 14 references to BuildServiceProvider in the aspnet/Hosting repo. This demonstrates that even Microsoft are vulnerable to this problem. I gave up reading researching their code when I found this.

The really awful conclusion I came to here was that as a developer that extends containers, one would have to track and try and expose a resolution chain for singleton's by trying to keep track of each and every service provider out there which is public and then try and resolve singletons from a list of the public ones you can get your hands on in the order in which they were created. This is nasty business.

Also after doing performance testing using Jetbrains DotMemory is also noticed that BuildServiceProvider is a hog that thrashes memory badly.

The flaw exists upstream and has manifested itself in hosting already. What I do know is that some framework singletons can be salvaged from the web host provider by creating rubbish code like this in the FrameworkDependencyResolver.

public object Resolve(CreationContext context, ISubDependencyResolver contextHandlerResolver, ComponentModel model, DependencyModel dependency)
{
	try
	{
		var tornSingletonWebHostServiceProvider = WindsorWebHostProvider.GetWebHost();
		return tornSingletonWebHostServiceProvider.Services.GetService(dependency.TargetType);
	}
	catch
	{
		// If not here in the hosting application service provider, then we need to create our own.
		// This will make singletons registered here https://github.com/aspnet/Hosting/blob/dev/src/Microsoft.AspNetCore.Hosting/WebHostBuilder.cs#L266 resolve correctly through Castle Windsor
		// This catch statement will get hit for things like Controllers, ViewComponents and TagHelpers because once a service provider 
		// get's created snapshots the service collection and houses any singltons that have already been resolved in a cache. 
	}

	// Less than ideal, but we have to fall back on to our own managed instance of the ServiceProvider.
	serviceProvider = serviceProvider ?? serviceCollection.BuildServiceProvider();
	return serviceProvider.GetService(dependency.TargetType);
}

This really can be optimised for types like:

I think a little later on I will look at how I can optimise to cater for this but it would mean that I would have to maintain and ordered cache of singleton's and it's co-respective ServiceProvider's to untear lifestyles for singletons that are commonly used. Nasty!

I don't see any other clean alternative right now.

But if you cache singletons outside one container instance, how do you know when to dispose them? Besides, I think that if you reuse singletons and share them across container instances, you'd even be violating the (very implicit) .NET Core DI Abstraction contract.

Sorry wrote that wrong. Did not mean to say cache singletons. I meant cache the service provider instances in an ordered list only. Then interrogate to the IServiceCollection for descriptors that are singleton, and then try and resolve them one at a time from the ordered list of service provider instances.

I would like to see singletons be IServiceProvider agnostic using a static instance cache that floats between them. It would really not be a lot of work and yet it would yield very high value for non-conforming containers. It would also help start fixing all the problems I found in aspnet/hosting.

Should I try and raise this with @davidfowl? Maybe @shanselman can arbitrate.

@shanselman Houston, we have a problem ....

#LetsFixSingletonsInDependencyInjection

Please note that the problem exists with Scoped registrations also.

For anyone interested in a debuggable solution I have one here: https://github.com/fir3pho3nixx/Windsor.AspNetCore/tree/aspnet-hosting

@dotnetjunkie My gut feel right is we need to start looking at hosting. Creating our own IServiceProvider'less hosting provider wrapper is potentially an option?

I firstly want to make it clear to everyone on this thread that singleton violation's with improperly tracked disposables are an absolute no no. We have enough of that going on with our very own typed factories.

Singletons to me are a process wide instance that shares state and is always threadsafe. Saying no to this creates a configuration nightmare.

I have found a thread-safe way to track "process wide" singletons with better tests for the abstractions.
I am hoping anyone from the hosting project would dive in. I raised this PR against my fork.
https://github.com/fir3pho3nixx/DependencyInjection/pull/2.

Notice how I changed all tests to not track singletons for disposables: https://github.com/fir3pho3nixx/DependencyInjection/pull/2/files#diff-cf49ec75a3a58a8b15b902816b32ce28R690

I also added some negative testing for singletons when it comes to not tracking disposables here: https://github.com/fir3pho3nixx/DependencyInjection/pull/2/files#diff-4185789735b0c69cda3bd684d54a5828R148

Now that we have removed all the disposable tracking, I then came up with this rubbish idea of a singleton tracker which I added to the CallSiteRuntimeResolver. Instead of making singleton fallback on to scoped, I opted for using the singleton tracker here:https://github.com/fir3pho3nixx/DependencyInjection/pull/2/files#diff-df85ffd78647d080ca2e02c3ab6b4de7R6).

It allowed me to test for singletons between scopes and non-scopes like this: https://github.com/fir3pho3nixx/DependencyInjection/pull/2/files#diff-cf49ec75a3a58a8b15b902816b32ce28R444

I really did not like the idea of exposing options that tracked singletons, but I am sure there is a boffin in the house that could help us turn this into a valuable PR.

Common gents/ladies at Microsoft, surely we can work this one out?

@fir3pho3nixx can you stop tagging the entire team here. It's not productive. What's the goal here? The thread is long and the latest issue might be something specific and unrelated to the original problem.

@davidfowl Very much connected to the original problem. Let me explain(although I had hoped you read downward from the first time I tagged everyone).

Creating 2 service providers, will produce 2 separate singletons respectively. This leaves me with torn lifestyles if I manage my own service provider from within the FrameworkDependencyResolver.

I think I have a way of fixing this but it would involve submitting a PR: https://github.com/fir3pho3nixx/DependencyInjection/pull/2

It would then help unblock the original PR for this issue: #394

Creating 2 service providers, will produce 2 separate singletons respectively. This leaves me with torn lifestyles if I manage my own service provider from within the FrameworkDependencyResolver.

Yes, hosting does this and it's awful, what services are breaking for you specifically?

I think I have a way of fixing this but it would involve submitting a PR: fir3pho3nixx/DependencyInjection#2

That fix is in the wrong place. Hosting is the one in the "wrong" not the DI implementation itself so that's not the right place to tackle the issue.

It would then help unblock the original PR for this issue: #394

Is this basically trying to make castle work with ASP.NET Core without being a "conforming" container?

Hosting is the one in the "wrong"

It also looks like I have made the same mistake. What is right way to do this? What are you guys planning to do with hosting? This would probably set me on the path to a proper fix.

Is this basically trying to make castle work with ASP.NET Core without being a "conforming" container?

In a word yes. We simply don’t treat scopes and singletons as the same thing. I tried “conforming” this container almost a year ago but the impedance mismatch in life cycles alone make it impossible. I am still hopeful we can make this work.

That fix is in the wrong place. Hosting is the one in the "wrong" not the DI implementation itself so that's not the right place to tackle the issue.

Curious, it solves the abuse that has already happened in hosting(and might be proliferated through all sorts of third party libraries). After falling victim to this myself I would like see some safety built into dependency injection. Would you reconsider?

The thing is, today the effect is that you get different instances because there are different universes. I still haven't looked closely enough to see what is different about castle core's implementation that makes it a deal breaker. That isn't to say they isn't a problem but it's been there since the beginning so we just have to understand why it matters more now.

Curious, it solves the abuse that has already happened in hosting(and might be proliferated through all sorts of third party libraries). After falling victim to this myself I would like see some safety built into dependency injection. Would you reconsider?

Maybe but the bar is extremely high for making changes to our default container. It's pretty much frozen unless something is outright broken (and wouldn't be a breaking change to fix).

The thing is, today the effect is that you get different instances because there are different universes

Are scopes universes also?

I still haven't looked closely enough to see what is different about castle core's implementation that makes it a deal breaker.

I would love to explore this a little more. Scopes are completely incompatible between our containers.

That isn't to say they isn't a problem but it's been there since the beginning so we just have to understand why it matters more now.

It always mattered. Just getting to it now.

Maybe but the bar is extremely high for making changes to our default container. It's pretty much frozen unless something is outright broken (and wouldn't be a breaking change to fix).

Please bear with me. The least that can come out of this is that I will have a better understanding.

I am incredibly interested in the CallSiteRuntimeResolver right now. Is there any way I could override the VisitSingleton method to use a static collection of resolved services? This would probably sort the problems in hosting out and also not be a breaking change. Hope you are open to discussing this.

Are scopes universes also?

No. Scopes are very simple. The scoped IServiceProvider is just a disposable root for scoped and transient services. If something is declared as scoped, then it's a singleton within the scope container it gets resolved from (instance per lifetime scope in autofac speak).

I would love to explore this a little more. Scopes are completely incompatible between our containers.

public interface IThing  { }

public class Thing : IThing, IDisposable
{
   public void Dispose() => Console.WriteLine("Thing.Dispose()");
}

var sp = new ServiceCollection().AddScoped<IThing, Thing>()
                                                       .BuildServiceProvider();

using (var scope = sp.CreateScope())
{
   var thing = scope.ServiceProvider.GetService<IThing>();
   var thingAgain = scope.ServiceProvider.GetService<IThing>();
   
   // These are the same instance since they were resolved from the same scope container
}

When I say different universes I literally mean we create different instances of IServiceProvider:

var universe1 = new ServiceCollection().AddSingleton<IFoo, Foo>().BuildServiceProvider();

var universe2 = new ServiceCollection().AddSingleton<IFoo, Foo>().BuildServiceProvider();

Those are 2 different worlds even if they share the same service descriptors. This is what hosting does because they are stages.

It always mattered. Just getting to it now.

When I say it didn't matter, what I meant is that you get different instances at each stage which is by design. It means if you add a singleton in the hosting container, you get 2 different versions because they are 2 different containers. That's the quirk. It surfaces when you resolve a service from the Startup constructor and then from the Configure method.

I am incredibly interested in the CallSiteRuntimeResolver right now. Is there any way I could override the VisitSingleton method to use a static collection of resolved services? This would probably sort the problems in hosting out and also not be a breaking change. Hope you are open to discussing this.

I don't see that as a viable option to change how the default service provider works to plug in a 3rd party container. As far as I'm concerned those are implementation details of the default container.

No. Scopes are very simple. The scoped IServiceProvider is just a disposable root for scoped and transient services. If something is declared as scoped, then it's a singleton within the scope container it gets resolved from (instance per lifetime scope in autofac speak).

Good explanation.

In Windsor a service that is registered with a scoped lifestyle will throw an exception if it is resolved from the container(or root scope) unless a scope is created first. Scoped instances live for exactly as long as the scope until it gets disposed. Child scopes yield new scoped lifestyle instances. Transient instances will be instantiated N+1 times dependending how many different service types consumed them as dependencies and their disposables are scoped tracked.

Singletons however are scope agnostic. I think this is a sticking point for me.

When I say different universes I literally mean we create different instances of IServiceProvider:

Two containers. Got it.

Those are 2 different worlds even if they share the same service descriptors. This is what hosting does because they are stages.

Stages of what? We have 2 containers here.

When I say it didn't matter, what I meant is that you get different instances at each stage which is by design.

Why have N+1 containers? We tried parent/child containers, it exposed all sorts of problems on our end.

It means if you add a singleton in the hosting container, you get 2 different versions because they are 2 different containers. That's the quirk.

Yup but do new scopes yield new singletons? This is not right.

I don't see that as a viable option to change how the default service provider works to plug in a 3rd party container. As far as I'm concerned those are implementation details of the default container.

https://en.wikipedia.org/wiki/Singleton_pattern

quote end

Hope you understand that there are some fundamentally different things happening when it comes to memory management here. It affects downstream performance heavily if you keep reinstantiating singletons that pull config in the cloud. Hope you will reconsider this approach.

Why have N+1 containers?

One use case could be integration testing. You might have 1 container for services used by the test code, and 1 container for services used by the application code. So having 2 "singleton" instances would then make sense since you don't pollute the application environment with components registered for the test environment.

One use case could be integration testing. You might have 1 container for services used by the test code, and 1 container for services used by the application code. So having 2 "singleton" instances would then make sense since you don't pollute the application environment with components registered for the test environment.

This happens in aspnet/hosting so this is not test code.

@davidfowl

I believe we can fix this by passing the root scope to the call run-time resolver for singletons.

Singletons however are scope agnostic. I think this is a sticking point for me.

They have to be, if they weren't, singletons would be the same as scoped. Put another way singletons are scoped within the root container.

Stages of what? We have 2 containers here.

If you look at how hosting wires things up there are 2 phases to Startup:

public class Startup
{
   public Startup(ILogger<Startup> logger) // Startup gets resolved from the first container (the hosting container)
   {
   }

   public void ConfigureServices(IServiceCollection services) // This is the second container definition (the application container)
   {
   }  
   
   public void Configure(IApplicationBuilder app, ILogger<Startup> logger2) { } // We resolve services from the second container here (the application container)
}

Yup but do new scopes yield new singletons? This is not right.

It's not a new scope, its an entirely new container. There's the hosting container which is used to resolve services in the Startup constructor and there's the application container which uses the service collection provided in ConfigureServices to be created.

Hope you understand that there are some fundamentally different things happening when it comes to memory management here. It affects downstream performance heavily if you keep reinstantiating singletons that pull config in the cloud. Hope you will reconsider this approach.

I'm not sure what you mean.

They have to be, if they weren't, singletons would be the same as scoped. Put another way singletons are scoped within the root container.

But your call site run-time resolver says you resolve singletons as scoped. https://github.com/aspnet/DependencyInjection/blob/dev/src/DI/ServiceLookup/CallSiteRuntimeResolver.cs#L39

It's not a new scope, its an entirely new container

New scope = new container?

New scope = new container?

No, a new scope is not a new container. We should stop conflating these things.

This is a new container:

var sp1 = new ServiceCollection()
           .AddScoped<ScopedService>()
           .BuildServiceProvider();

var sp2 = new ServiceCollection()
           .AddSingleton<SingletonService>()
           .BuildServiceProvider();

sp1 and sp2 are different containers, they don't share service descriptors, they are independent of each other. They are both distinct top level root containers and can each create their own scopes independently.

But your call site run-time resolver says you resolve singletons as scoped. https://github.com/aspnet/DependencyInjection/blob/dev/src/DI/ServiceLookup/CallSiteRuntimeResolver.cs#L39

Lets not discuss the implementation details of the default container. That just makes the conversation more confusing.

Let me try to explain again:

  • Scoped services are singletons within a specific scoped container instance.
  • Singleton services are the same instance across all scoped containers.

Example:

var sp  = new ServiceCollection()
             .AddSingleton<SingletonService>()
             .BuildServiceProvider();

var singleton1 = sp.GetService<SingletonService>();

using (var scoped = sp.CreateScope())
{
    var singleton2 = scoped.ServiceProvider.GetService<SingletonService>();
    Assert.Same(singleton1, singleton2);
}

In the above code snippet, the singleton object is the exact same object even though it singleton1 was resolved from the root container (sp) and singleton2 was resolved from the scoped container.

Here's an example of scoped services:

var sp  = new ServiceCollection()
             .AddScoped<ScopedService>()
             .BuildServiceProvider();

using (var scoped = sp.CreateScope())
{
    var scoped1 = scoped.ServiceProvider.GetService<ScopedService>();
    var scoped2 = scoped.ServiceProvider.GetService<ScopedService>();
    Assert.Same(scoped1, scoped2);
}

using (var scoped = sp.CreateScope())
{
    // New instance
    var scoped3 = scoped.ServiceProvider.GetService<ScopedService>();
    var scoped4 = scoped.ServiceProvider.GetService<ScopedService>();
    Assert.Same(scoped3, scoped4);
}

// Resolving a scoped service from the root container will throw by default in ASP.NET Core because
// ValidateScopes is on (a feature of the default container)
sp.GetService<ScopedService>(); 

Stages of what? We have 2 containers here.

If you look at how hosting wires things up there are 2 phases to Startup:

public class Startup
{
   // Startup gets resolved from the first container (the hosting container)
   public Startup(ILogger<Startup> logger)

   // This is the second container definition (the application container)
   public void ConfigureServices(IServiceCollection services)

Sorry for kicking in, but to make sure - so typical WebHost creation like

            return WebHost.CreateDefaultBuilder(args)
                    .UseConfiguration(configuration)
                    .UseStartup<Startup>()
                    .Build();

creates and uses "the first container (the hosting container)" just to help in creating the Startup object, and then (more or less) forgets the container and then, only after creation of the instance of Startup, we lets the Startup bootstrap its own, new, second container instance "(the application container)" thats later used through out the app's lifetime?

That would explain some confusing things I've seen when I tried to setup some common logging for app startup and app runtime.. Ok, but then, once application starts, what happens to the first container? is it totally forgotten and left for GC to cleanup, or is it kept and later something other than Startup is resolved from it as well?

creates and uses "the first container (the hosting container)" just to help in creating the Startup object, and then (more or less) forgets the container and then, only after creation of the instance of Startup, we lets the Startup bootstrap its own, new, second container instance "(the application container)" thats later used through out the app's lifetime?

@quetzalcoatl that's absolutely correct.

That would explain some confusing things I've seen when I tried to setup some common logging for app startup and app runtime..

Yes, it's a PITA.

Ok, but then, once application starts, what happens to the first container? is it totally forgotten and left for GC to cleanup, or is it kept and later something other than Startup is resolved from it as well?

It sticks around until the WebHost is disposed but nothing else is ever resolved from it (it could really be disposed earlier).

@davidfowl

You have cleared up quite a bit, I managed to clear up quite a few things in my PR as a result. Disposables are working properly and I don't have any issues with singletons anymore. The last thing I need to look at is why enabling scope validation in the facility is triggering exceptions.

Everything you have said so far is making perfect sense. I not sure what stopped us from conforming the first time, we had all sorts of problems making those tests pass but that was a while ago. I might take another look at this later on. Thanks for explaining. 👍

@davidfowl

We are going ahead with this cross-wiring non-conforming thing for now (I do know there is a problem with scopes just not clear on why yet). I do think our next issue needs to be a conforming one. I am not going to try and address this here because this is an issue already has a long tail. Thank you for your input, look forward to to making this happen.

@dotnetjunkie

I think we have cleared up most of the misconceptions I have had. Thank you for your input.

@ALL

If you can see something I missed @ PR: #394 please chime in. @generik0 this includes you.

Latest artifacts are here: https://ci.appveyor.com/project/castleproject/windsor/build/0.0.423/artifacts

If you need any more help or clarification let me know. Castle Windsor is a popular container and it would be huge if an adapter could be built.

one of the reasons that was preventing me
from taking brown field projects (upgrading from mvc5 to core)
is not being able to use castle windsor.

it seems that it's going to be possible now. i'm excited
thank you guys!. :)

@hikalkan looking a your impl, it seems as though scoping is the hardest thing to implement right? A cursory glance tells me there's no built in unit of work style scope that isn't tied to some ambient state (call context, async local etc). I haven't looked but I'm also guessing there's no support for child containers or nested containers in windsor? This is the model that most closely matches the semantics of Microsoft.Extensions.DependencyInjection:

A cursory glance tells me there's no built in unit of work style scope that isn't tied to some ambient state (call context, async local etc)

We use async local for scoped ambient state here: https://github.com/castleproject/Windsor/blob/master/src/Castle.Windsor/MicroKernel/Lifestyle/Scoped/CallContextLifetimeScope.cs#L48

I haven't looked but I'm also guessing there's no support for child containers or nested containers in windsor

We have them: https://github.com/castleproject/Windsor/blob/master/src/Castle.Windsor/Windsor/IWindsorContainer.cs#L56

They behave wierdly in some cases. Wont pollute with detail.

We use async local for scoped ambient state here: https://github.com/castleproject/Windsor/blob/master/src/Castle.Windsor/MicroKernel/Lifestyle/Scoped/CallContextLifetimeScope.cs#L48

Right, I was playing around with it last night and there seems to be a few gaps with the build in scope that https://github.com/volosoft/castle-windsor-ms-adapter/blob/master/src/Castle.Windsor.MsDependencyInjection/MsLifeTimeScope.cs#L32 tries to address:

  • Disposing transients, transient disposable services resolved in a scope are disposed (is that configurable?)
  • Disposable order

It's using async local because there's no way to set enough state from the call site of the resolution itself so that you can get it back when the custom scope infrastructure calls you back. Here's my super basic example of one of the problems:

using System;
using Castle.MicroKernel.Context;
using Castle.MicroKernel.Lifestyle;
using Castle.MicroKernel.Lifestyle.Scoped;
using Castle.MicroKernel.Registration;
using Castle.Windsor;

namespace windsor_tests
{
    class Program
    {
        static void Main(string[] args)
        {
            var container = new WindsorContainer();
            container.Register(Component.For<IFoo>().ImplementedBy<Foo>().LifestyleCustom<LifetimeObjectScopeLifestyleManager>());

            // This object represents the lifetime of resolved services. Anything disposable component associated with this lifetimte object
            // should be disposed when it is disposed
            
            using (var lifetime = new DefaultLifetimeScope())
            {
                container.Resolve<IFoo>();
            }
        }
    }
    public class LifetimeObjectScopeLifestyleManager : ScopedLifestyleManager
    {
        public LifetimeObjectScopeLifestyleManager() : base(new LifetimeObjectScopeAccessor())
        {
        }
    }

    public class LifetimeObjectScopeAccessor : IScopeAccessor
    {
        public void Dispose()
        {
            // TODO: Dispose the lifetime instance
        }

        public ILifetimeScope GetScope(CreationContext context)
        {
            // How do I get the lifetime instance here?
        }
    }

    class Foo : IFoo, IDisposable
    {
        public Foo()
        {
            System.Console.WriteLine("Created Foo");
        }

        public void Dispose()
        {
            System.Console.WriteLine("Disposing Foo");
        }
    }

    interface IFoo
    {
    }
}

Ideally, I could shove the DefaultLifetimeScope into the CreationContext so that it was accessible in LifetimeObjectScopeAccessor.GetScope. It would also need to be available when disposing.

I'm sure this isn't the only problem, and I don't really know that much about Castle Windsor so feel free to call me out on anything I misunderstood.

Hi @davidfowl, @fir3pho3nixx and all others :)

it seems as though scoping is the hardest thing to implement right?

Yes, definitely. Because Windsor does not have such a scoping feature natively. I implemented it using AsyncLocal and created ScopedWindsorServiceProvider. The secret code is:

https://github.com/volosoft/castle-windsor-ms-adapter/blob/80964d709cd48db208387c04ecf664605f7f2ff6/src/Castle.Windsor.MsDependencyInjection/ScopedWindsorServiceProvider.cs#L43-L69

This ensures to relate the resolved transient and scoped services to the current service scope, so it can dispose them when the service scope is disposed.

@hikalkan Of course! Modelling the provider impedance mismatches was definitely the right way to go. You really kicked some ass leading this(I followed the same path and reached the same conclusion, I do know this is not the complexity Castle wants to own). Could you kindly provide some more context around your code quote?

using (MsLifetimeScope.Using(OwnMsLifetimeScope))
{
	var isAlreadyInResolving = IsInResolving;

	if (!isAlreadyInResolving)
	{
		IsInResolving = true;
	}

	object instance = null;
	try
	{
		return instance = ResolveInstanceOrNull(serviceType, isOptional);
	}
	finally
	{
		if (!isAlreadyInResolving)
		{
			if (instance != null)
			{
				OwnMsLifetimeScope?.AddInstance(instance);
			}

			IsInResolving = false;
		}
	}
}

@davidfowl I need more time to respond to this (sorry man I know what you are asking for). Please bear with me.

@davidfowl I need more time to respond to this (sorry man I know what you are asking for). Please bear with me.

@fir3pho3nixx it's cool. I have a suggestion on how to fix it after you respond but I haven't dug into the code to see how hard it would be to implement.

@hikalkan Of course! Modelling the provider impedance mismatches was definitely the right way to go. You really kicked some ass leading this(I followed the same path and reached the same conclusion, I do know this is not the complexity Castle wants to own). Could you kindly provide some more context around your code quote?

The code probably doesn't need to be that complex. For example there is no nested scope requirement. Scopes can be at a single level they don't need to support overlapping. Because of the ambient state model though I see why you feel the need to handle it. Consider this:

using (var lifetime = new DefaultLifetimeScope())
using (var lifetime = new DefaultLifetimeScope())
{
    container.Resolve<IFoo>();
}

There's no way to resolve from a specific scope there. The last one will win because it's the most recent. This might be an edge case though but somebody could run into it.

Guys/Gals, I am closing this in light of PR: #394

@jonorossi Thanks for reviewing and cleaning up all my misconceptions about Windsor. You are a fucking legend.

@cwe1ss Thank you for raising the issue.

@hikalkan Thanks for leading the way on https://github.com/volosoft/castle-windsor-ms-adapter. Seriously man, good work.

@dotnetjunkie You informed and led this process from eons ago. Thank you.

@davidfowl Thank you for engaging us. Hope you don't mind if we call on you in the future. Seriously thank you.

This issue only took 2 years, 4 months, 18 days excluding the end date to solve(kind of).