Skip to content
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

Simplify service registration and use MSDI abstractions throughout. #8653

Closed
JimBobSquarePants opened this issue Aug 18, 2020 · 232 comments
Closed
Assignees

Comments

@JimBobSquarePants
Copy link
Contributor

JimBobSquarePants commented Aug 18, 2020

I don't know how to phrase this better but drop the tight coupling with LightInject like a hot stone for the .NET Core implementation.

This was discussed at great length pre V8.

https://issues.umbraco.org/issue/U4-11427

There was even an attempted PR. Unfortunately V8 was launched before it could be completed. In my opinion it also did not go far enough to simplify registration of services.

#3955

The current DI implementation hurts extensibility through it's complexity. Package development for V8 still undeniably suffers for a lack of ecosystem. There were slack channels created by package developers specifically to discuss navigating this complexity.

Using a custom LightInjectContainer is never a good idea; neither is running multiple containers.

CC/ @lars-erik

P.S I'm absolutely putting my hand up here to help design a more simplified approach.

@lars-erik
Copy link
Contributor

I'm sorry to say I haven't been able to look much at the core port yet. Hopefully I suddenly will.
In any case, I wholeheartedly support the notion that Umbraco should rely on MS.DI abstractions alone for the core port.
I don't mind whether the current composition abstraction stays on top, as long as it exposes the same possibilities. It will at least keep Umbraco code resilient from future container changes. It may well be it deserves an overhaul to better align with MS.DI tho.
Whether the default container or lightinject is used behind the scenes ootb I don't care, but it has to work with -, and support changing to any container supporting MS.DI.
WFIW, I insist. 🤣

@lars-erik
Copy link
Contributor

@JimBobSquarePants
Copy link
Contributor Author

I say kill IRegister and IFactory.

@lars-erik
Copy link
Contributor

lars-erik commented Aug 18, 2020

Not sure about IFactory, but IRegister can certainly become IServiceCollection. 👍
I'd expect the MS.DI factory abstraction would be accessible through a prop or method on IFactory tho.

@JimBobSquarePants
Copy link
Contributor Author

JimBobSquarePants commented Aug 18, 2020

At no point should there need an explicit concrete reference to ServiceCollection, only IServiceCollection. IFactory exists to service a container which shouldn't ever be required either.

IMO all you should ever care about is consuming IServiceCollection, and IConfiguration and adding extension methods to the former or using an IUmbracoBuilder implementation or suchlike to allow simplified registration of certain types.

// Startup.cs ConfigureServices
IUmbracoBuilder builder =  services.AddUmbraco(this.IConfiguration);

// Startup.cs Configure
app.UseUmbraco(...);

@lars-erik
Copy link
Contributor

Abstraction miss fixed. 👼
All I'm saying is that registered factory methods should have access to an IFactory.
I believe MS.DI provides one, so by all means, it could be it.

(A bit sluggish on the APIs atm. Still mostly hacking framework stuff with V8.)

@JimBobSquarePants
Copy link
Contributor Author

All I'm saying is that registered factory methods should have access to an IFactory.

I don't follow you here. The IFactory is used to define a conforming container. You don't need or want to be referencing a container implementation anywhere in the codebase. Any custom container (Not recommended by MS) implementation is wired up at ConfigureServices the final caller.

Perhaps you mean IServiceScopeFactory? This can be injected any time (but should be considered a tool of last resort).

@Shazwazza
Copy link
Contributor

Hi all! Glad this has been resurrected. It's was an item on the original RFC for netcore to look into once we were a little further on with development. There's still a long way to go but prob a good time to make some decisions and come up with a design for how we want this to all work. As you both know there's all sorts of things to think about for this refactor so it's great that you were both very much involved with the orig discussions :)

I'm all for simplification and removing LI and don't think anyone would object to that 😄 so just need to figure out where/how to start with this and what the end goal is. The main thing to keep in mind is that the Umbraco.Core project isn't meant to have any dependencies (clean architecture) and contain only our own abstractions. This is where things like IFactory, etc... are defined. So a big decision would need to be made:

  1. Do we keep this guideline, don't take any dependencies in Umbraco.Core, keep/modify our own abstractions
  2. Do we break this guideline and takes a dependency on Microsoft.Extensions.DependencyInjection in Umbraco.Core for it's DI abstractions
  3. Do we keep this guideline, don't take any dependencies in Umbraco.Core, and move anything that needs a reference to MSDI abstractions to the Umbraco.Infrastructure project (I'm not sure if this is at all possible FYI, just thinking out loud

I believe Lars's previous work and suggestion is to keep option #1 and then implement the abstractions with MSDI. I think this would also mean a lot less code to change. Maybe there's a 4rd/5th/etc... option too?

I guess the main goals here would be to:

  • Hopefully not take any dependencies in Umbraco.Core
  • Remove LI
  • Try to modify as little as possible especially currently used public APIs like the composer/components and collections

What do you think?

And how do you think it can be tackled so we can review/approve in small iterations so we don't end up with someone spending a huge amount of effort for it to be left out? Perhaps once a direction is decided (or needs to be proved) a small POC can be made (if possible) to help showcase the intention.

Cheers!

@JimBobSquarePants
Copy link
Contributor Author

The main thing to keep in mind is that the Umbraco.Core project isn't meant to have any dependencies (clean architecture) and contain only our own abstractions.

I do not understand this sentiment. You're already taking dependencies on all the System.*. Having a dependency doesn't make things unclean.

That said....

Looking at the codebase (Infrastructure) it looks like you are creating your own container instance and then passing that instance around via IFactory. You should never have to do that. The only time a container should be created is at the topmost caller level (Web App creation) or during tests. Any service registration should be done via IServiceCollection. Everything else should be blissfully unaware.

Try to modify as little as possible especially currently used public APIs like the composer/components and collections

This is where you will never win me over. It's so overly complicated and unnecessary. There are already abstractions available for registration via IServiceCollection. Any syntactic sugar can be provided as extension methods upon that abstraction.

@Shazwazza
Copy link
Contributor

Hi @JimBobSquarePants,

I'm wondering if it would be easiest to do a zoom call about all this stuff since there's probably a lot of questions/answers/concerns/reasons underpinning the current state of things and where we want to go. Would you and @lars-erik be available some time next (any) week preferably either Mon or Thur (since those are my late days but happy to do any other day too!)?

Regarding the 'Clean Architecture', that's just referring to the direction the codebase has been restructured (still ongoing), see https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html where the general rule is that the Domain/Core has no external dependencies. In this case though - in my personal opinion - I don't think that referencing Microsoft.Extensions.DependencyInjection is necessarily regarded as an 'external dependency'.

Try to modify as little as possible especially currently used public APIs like the composer/components and collections

Let me try to re-phrase this since I don't think the way I worded that was very clear - also why I'd love to have a call about all this stuff :) What I'm trying to say is that one goal of this release is to reduce the friction for Umbraco developers to make the transition from Umbraco 8 to the Umbraco netcore version and as such we are trying to maintain (where possible) the public APIs and patterns that exist in Umbraco 8. For developers to interact with Umbraco's startup, container configuration, plugins, collection builders etc... we have our own IComposer/IComponent which we need to keep. If changes to these types of things are necessary that is fine but we want to minimize the changes where possible. So it's totally fine we make changes but the goal is to minimize the transition for Umbraco devs from v8 to the netcore version.

I'm sure you'll have a ton of questions as to 'why' if you are perusing the code, there are reasons for everything, both good and bad and things are in flux. I just think a call to do a recap of the current state would be easiest, else we'll be writing responses this long or longer for quite some time 😂

We're totally open for suggestions and if possible would love to get to a point to see a proof of concept for desired changes.
Cheers!

@JimBobSquarePants
Copy link
Contributor Author

Hey @Shazwazza happy to make time for a chat. Just concerned the conversation will be lost to the public.

Regarding IComponent.... That's a worrying API for several reasons.

  • It passes a concrete Composer instance which tightly couples implementation to that type.
  • Composer has a public method CreateFactory which returns the container!

You should never, ever have to return the container instance anywhere in your API.

This is why I get worried when I see a major goal repeated that V8 API compatibility.

@lars-erik
Copy link
Contributor

Happy to make time as well. Mostly free, but unsure how my free time matches aussie timezone.

Here's a slide from my UK Fest 2019 talk on wishes for DI in Umbraco. Note the package dev guidance. 👼
image

(All slides here: https://1drv.ms/p/s!AnYHs3nuLdwBlKxbsi6BCj42REN7vw?e=uhB4SZ)

@Shazwazza
Copy link
Contributor

Hey @JimBobSquarePants + @lars-erik , sorry for the delay, was off Friday to get a tooth implant so I can stop looking like a hillbilly 😂 (long story!)

Just to recap on some stuff:

Just concerned the conversation will be lost to the public.

We had a plan to do an RFC for this exact phase at some point so we might as well start that now with the help of you both and whoever else might like to join. My proposal is to have a first call soon just to go through questions/concerns/current/future, I'll take notes and post them back here and get an RFC draft started and you can help fill in any info, etc... if you want.

Regarding changes and cleanup to the code - We're all for it! It's not about "V8 API compatibility" 1:1 or anything like that, it's just about maintaining similar concepts. If people need to change code that's fine but it will be much friendlier if that can be minimized and perhaps even done with a find/replace.

What do you think about next week for timing? If you can do Monday Aug 31 I would propose a 30 min chat (longer if we want it) anytime between Sydney 19:00-23:00 (CPH 11:00-15:00) onwards. I can make that same time frame work from Mon-Thur next week.

https://www.timeanddate.com/worldclock/meetingtime.html?day=31&month=8&year=2020&p1=240&p2=69&iv=0

Let me know what you think :)

@lars-erik
Copy link
Contributor

@Shazwazza the timeframe works for me, although I might be 10-15 min late in order to fetch lunch and make eating sounds for the first 20 min. 👼

@JimBobSquarePants
Copy link
Contributor Author

@Shazwazza That works for me, looking forward to it!

@Shazwazza
Copy link
Contributor

Shazwazza commented Aug 31, 2020

@lars-erik + @JimBobSquarePants

Sorry should have posted this on Friday but better late than never :) Here's the meeting invite details for tonight (morning for you!). If anything has changed and you guys can't make it, no problemo and we can reschedule.

Topic: Umbraco netcore MSDI chat
Time: Aug 31, 2020 07:00 PM Canberra, Melbourne, Sydney

Join Zoom Meeting
https://us02web.zoom.us/j/88446112341

Meeting ID: 884 4611 2341
One tap mobile
+13462487799,,88446112341# US (Houston)
+16465588656,,88446112341# US (New York)

Dial by your location
+1 346 248 7799 US (Houston)
+1 646 558 8656 US (New York)
+1 669 900 6833 US (San Jose)
+1 253 215 8782 US (Tacoma)
+1 301 715 8592 US (Germantown)
+1 312 626 6799 US (Chicago)
Meeting ID: 884 4611 2341
Find your local number: https://us02web.zoom.us/u/kcxhWnBIKx

@Shazwazza
Copy link
Contributor

@lars-erik + @JimBobSquarePants we'll reschedule to Thur if James can make it. Have pinged you on Twitter and we can setup some cal invites. Cheers!

@p-m-j
Copy link
Contributor

p-m-j commented Sep 1, 2020

and whoever else might like to join

I'd love to sit in on this, is this open to all?

@Shazwazza
Copy link
Contributor

Hi @rustybox yep for sure, here's the zoom information for tomorrow:

Shannon Deminick is inviting you to a scheduled Zoom meeting.

Topic: Umbraco netcore MSDI chat
Time: Sep 3, 2020 07:00 PM Canberra, Melbourne, Sydney

Join Zoom Meeting
https://us02web.zoom.us/j/85617048741

Meeting ID: 856 1704 8741
One tap mobile
+13462487799,,85617048741# US (Houston)
+16465588656,,85617048741# US (New York)

Dial by your location
+1 346 248 7799 US (Houston)
+1 646 558 8656 US (New York)
+1 669 900 6833 US (San Jose)
+1 253 215 8782 US (Tacoma)
+1 301 715 8592 US (Germantown)
+1 312 626 6799 US (Chicago)
Meeting ID: 856 1704 8741
Find your local number: https://us02web.zoom.us/u/kfJyWYds7

@Shazwazza
Copy link
Contributor

@Shazwazza
Copy link
Contributor

@Shazwazza
Copy link
Contributor

Thanks!!! community members for joining and helping move this along, that was fantastic, much appreciated for your time ❤️

Here's the raw notes (will type them up later in a nicer format)


Umbraco netcore + MSDI

  • We know we want to:

    • Remove LightInject
    • Simplify
    • Cleanup
    • Use MSDI!
  • We know we need to:

    • Keep the same concepts as what Umbraco currently has: IComposer, IComponent, plugins, Collection Builder, "Unique" registrations
      • Some of these things are 'interesting' because they don't work 1:1 with how MSDI container works
        • IFileSystem - named service? It's using Activator/reflection? It's possible to use MS's new 'Activator' (can't remember what it's called... ActivatorUtilities?)... can we use DI?
      • ASPNETCORE - has 'replace' method, could be used in place of "Unique"?
  • Main questions

    1. Do we use MSDI abstractions for the DI abstraction layer
      • Use extension methods to keep our existing API methods for ease of upgrade/usage
    2. Do we continue to use our own DI abstractions and MSDI is the default shipped implementation
  • Notes:

    • Have discussed internally and taking on MSDI dependencies in the "Core" project = OK
    • DI Abstractions package only in Core
  • Current state...

    • Current
    • Startup
    • More...
  • RFC

  • Other

    • IFileSystem refactor - it is coupled too strongly to physical file system, its not async!
      • We would need a way to 'fetch' the file and not rely on static file handler - this allows for authorization
  • Improvements:

    • Clear direction on what we're changing
    • Naming - use correct naming so that people know exactly what they are doing
    • Instead of relying on 'magic' like "RegisterUnique" would be like "SetAppCaches"
    • Provide really simple ways to set specific services
    • Current conforming container removed - everything is then extensions on IServiceContainer ... or IUmbracoBuilder type of thing
    • IComposer is really just for plugins
    • IUmbracoBuilder used for end user developers for adding/replacing
    • Current ... WHY? (This causes more problems than it's worth), Views - we need it here in v8 but in netcore we don't
    • IFileSystem - named service? It's using Activator/reflection? It's possible to use MS's new 'Activator' (can't remember what it's called... ActivatorUtilities?)... can we use DI? How do we really want
      • There doesn't need to be multiple IFileSystem ... there is just a single IFileSystem which is injected into Css, Script, Media 'providers', if people want to replace these, then they use factory methods for these service
      • ShadowFileSystem ... how does this affect what we want ... could be done with DI Interceptors, but that's not in MSDI
  • How?

    • will be done in a different feature branch
    • IComposer is simplified, it's really just a wrapper on IServiceContainer
    • Umbraco cannot rely on any 'special' container features, it needs to work with MSDI's default implementation
    • what type of phases could/should done to get to where we are?
      • Could start by renaming methods on IRegister to equivalent method names on IServiceContainer, that would allow transitioning in an easier way
      • POC - a first go to just see what happens, and list questions/concerns, etc... - this can be a PR for comments - whether it gets merged doesn't matter but it is a first.
    • Get rid of Current, is it easy/doable? At the end of this process, let's check!

@lars-erik
Copy link
Contributor

lars-erik commented Sep 3, 2020

Indeed that was awesome! We were way more aligned than I expected, and that makes me super happy. :)

Just thought I'd elaborate on the steps I suggest for renaming the IRegister methods.

Most of the methods and extensions on IRegister boil down to the concrete method Register(Type serviceType, Type implementingType, Lifetime lifetime). (

public void Register(Type serviceType, Type implementingType, Lifetime lifetime = Lifetime.Transient)
)
That method pretty much mirrors a private extension method in MSDI:
https://github.com/dotnet/runtime/blob/1789775d0722bd2344fc026e9111d2a49adb0da0/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/ServiceCollectionServiceExtensions.cs#L650

So I think a good way forward would be to create a new static class a'la TempMsdiExtensions and move the LightInjectContainer.Register method to an extension method with the same signature, except IRegister as the instance argument. It can well be renamed to Add to match the private MSDI one. After that the remaining Register methods on IRegister should be possible to move into extension methods all pointing to that initial Add method, and mirroring the respective extensions in ServiceCollectionServiceExtensions. The final step being just replacing the using directive around Umbraco with the MSDI abstractions namespace instead.

While glancing over that interface now, I'm also reminded about the RegisterFor<> concept we introduced. It lets you register unique dependencies for given services. I think it amongst other things was indended for fixing IFileSystem. You could say .RegisterFor<IFileSystem, IMediaService>(typeof(StorageFileSystem)). 👼
However RegisterFor, like "uniques", do some magic on the container build step. AFAIK. there's nothing supporting that in MSDI, so we need to mind those as well.

Looking forward to see what @rustybox comes up with! 🤘

@JimBobSquarePants
Copy link
Contributor Author

I created services.AddServiceFor in my NamedServices experiments years ago. As far as I recall it "Just worked ™️"

The signature looks like this.

public static IServiceCollection AddServiceFor<TService, TImplementation, TTarget>(
    this IServiceCollection serviceCollection,
    ServiceLifetime serviceLifetime,
    ServiceLifetime targetLifetime)
    where TImplementation : class
    where TTarget : class

@Shazwazza
Copy link
Contributor

IMO if IFileSystem gets in the way of the initial POC I would suggest ignoring RegisterFor and how it is currently put together and we can come up with a simpler approach like we discussed instead of trying to shoe-horn in something that doesn't feel quite right.

@p-m-j
Copy link
Contributor

p-m-j commented Sep 4, 2020

RuntimeTests.Boot_Core_Runtime passed -- Was a bit concerned I might have lost my mind before I saw that message.

@lars-erik
Copy link
Contributor

lars-erik commented Sep 4, 2020

@Shazwazza

IMO if IFileSystem gets in the way of the initial POC I would suggest ignoring RegisterFor and how it is currently put together and we can come up with a simpler approach like we discussed instead of trying to shoe-horn in something that doesn't feel quite right.

I think RegisterFor is pretty much what we talked about. I think usage for filesystems were left as an intention. ;)
Both issues has to be addressed never the less, but I totally agree it can be postponed for now.

@rustybox

RuntimeTests.Boot_Core_Runtime passed -- Was a bit concerned I might have lost my mind before I saw that message.

🤪🤘👍 (GJ)

@p-m-j
Copy link
Contributor

p-m-j commented Sep 4, 2020

Any ideas why I might get the following during Setup in
Umbraco.Core.Persistence.DbConnectionExtensions.IsAvailable for all subclasses of UmbracoIntegrationTest

"Connection Timeout Expired. The timeout period elapsed during the post-login phase. The connection could have timed out while waiting for server to complete the login process and respond; Or it could have timed out while attempting to create multiple active connections. The duration spent while attempting to connect to this server was - [Pre-Login] initialization=1; handshake=0; [Login] initialization=0; authentication=0; [Post-Login] complete=14046;

Edit: The good news (if you can call it that) is that I get the same exception with head @ 775360e which is odd as they ran fine before I made any changes( with the exception of 5 tests that failed)

@p-m-j
Copy link
Contributor

p-m-j commented Nov 13, 2020

Presumably the mandate not to change everything stems from

@p-m-j
Copy link
Contributor

p-m-j commented Nov 14, 2020

I have had 2 epiphanies before my morning coffee.

  1. Anyone that really needs to not do stuff in a component (or elsewhere) if running in install mode can take a dependency on IRuntimeState and use it as a feature flag.

  2. I have no idea why the RuntimeLevel attribute exists, I made an assumption that it was about Components but I don't know how I came to that conclusion, it's not documented anywhere.

I disabled the filtering in Composers.GetRequirements and did an install followed by a run and messed around in the back office.

Guess what, the world didn't explode.

tl;dr I have no idea of the intended purpose of RuntimeLevel attribute.

@Shazwazza
Copy link
Contributor

Hi all (slowly getting back), amazing thread btw and a ton of great ideas and discussion 👏

Some quick answers to some things:

Anyone that really needs to not do stuff in a component (or elsewhere) if running in install mode can take a dependency on IRuntimeState and use it as a feature flag.

Absolutely!

The RuntimeLevel attribute should probably die and I agree with what everyone has (I think) agreed with here: Everything should always be registered in DI regardless of the runtime state. If code needs to do different things it can just check the IRuntimeState

The third party packages or user things are not ever supposed to run during umbraco installation are they?

Yes, some packages will do things differently depending on the runtime state but as above, packages will need to check for that. Things like Deploy, and others execute things on the installation state.

Components do currently initialize in order and terminate in reverse order, I don't know why I just know that's what it does right now :)

I don't see any reason why they need to terminate in reverse order. I have no idea why Terminate exists and it wasn't IDisposable from the beginning.

Is there a suggestion for no-code support (my personal suggestion would be don't support no-code, this is a CMS framework with a built in backoffice UI not am OOTB ready to go CMS) but I don't think that will fly :)

no-code support will be required. Perhaps it's possible after all of this stuff that a developer can opt-out of that in their Startup and manually initialize each package's startup code. I think that's for another discussion though.

ComposeAfter and ComposeBefore? They’re used for controlling service registration order? If so absolutely not!

It's an ugly way to control a couple things:

a) as a package dev I want to execute code after/before another packages IComponent.Initialize
b) as a package dev I want to be able to replace a service from another package

I don't like it at all and I think a lot of this recent discussion is directly related to how to solve a) and b).

I think a) can be solved with events and I think that is what people are agreeing to here? If there would be a simple way for a package to have a way to register services and execute code on startup/shutdown and somehow automatically behind the scenes events are raised for it's startup/shutdown sequence (before/after) then if required another package can do things before/after startup/shutdown of any other package easily and ideally without taking a dependency on that package.

I think we're only missing a clarification on how to control that serviceCollection.Replace<Something, MyThing>() is ran after serviceCollection.Register(). Without composition order, how do we solve that?

So this is b). Can this be solved with events too? I realize that if 2x different packages want to replace a services from package xyz then it's not possible to know which of the 2x other packages would 'win' but that's the same case as we have today with ComposeAfter so I'm not sure we need to really worry about that. If some edge case requires this then it would be up to the end user to adjust the final Replace in their Startup. But perhaps there's other ways too?

In general, I think the changes you are discussing now, are so extensive, we will need to write the proposed changes in an RFC, and have comments from a wider range of the community before we can decide to do it. I fear there are some use cases we are not aware of.

I think that will be ok though? I think so long as whatever a package dev needs to do to register services and execute code on startup/shutdown is simple (maybe more simple that what we currently have?) then it still shouldn't be too much code for them to change, at least that's how it seems to me. We could even have some shim of some sort later if we felt it was important enough to make. Having some POC code to demonstrate how to do the following as a package dev would be quite helpful:

  • register services
  • execute code on startup/shutdown
  • execute code before/after another package
  • execute code based on IRuntimeState

I think that covers everything being talked about?

I think I’m done with all this tbh. It’s not worth the effort.

Please no, this thread has come so far, I don't think there's much more to go. 🙏

@Shazwazza
Copy link
Contributor

I'm unsure if this is helpful or not but I found some blog posts detailing composition in v8 from the early days just in case there might be some other info in there about what is in v8:

@p-m-j
Copy link
Contributor

p-m-j commented Nov 16, 2020

So this is b). Can this be solved with events too?

In theory, however not with mediatr, with Udi's sample you can add an event handler without making use of the container, we could pass the instance around to composers (this would require temporal coupling to set ServiceProvider after initial construction), but in general what we were talking about was the EventAggregator service locating handlers from the container when events are fired.

This wouldn't be useful for working out when someone else has finished adding services as the only time the handlers could be triggered is after the ServiceProvider has been built at which point no one can add services anymore.

@p-m-j
Copy link
Contributor

p-m-j commented Nov 16, 2020

In summary.

  1. RuntimeLevel attribute can be removed
  2. Anyone that needs to conditionally exec code can check against RuntimeState.Level

That change alone (plus a fix against the setup for BackOfficeUserManager and possibly just a couple more) gets us to a point where container validation can be turned back on, I'll gladly put a PR in to resolve this.

  • Any enhancements around killing ComposeAfter/ComposeBefore can come at a later date.
  • As can discussions around killing IComponent entirely.
  • EventAggregator discussions can evolve in Netcore: Remove static events #9397

Outstanding question, is Umbraco.Web.Common.Builder.UmbracoBuilder intended to replace Composition or did you expect them both to exist?


Having some POC code to demonstrate how to do the following as a package dev would be quite helpful:

Would this go on https://github.com/umbraco/UmbracoDocs? v9 branch?

@Shazwazza
Copy link
Contributor


Outstanding question, is Umbraco.Web.Common.Builder.UmbracoBuilder intended to replace Composition or did you expect them both to exist?

Yes IIRC (sorry still getting back into the groove of things and trying to remember everything). The example that @JimBobSquarePants provides above #8653 (comment) is along the lines of I think where we want to go.

In above discussions like: #8653 (comment) we said

IUmbracoBuilder used for end user developers for adding/replacing

I know we have the generic ability to IServiceCollection.Replace any service but I think the intention/end goal was to make extension methods on IUmbracoBuilder to be very explicit to make it super easy/clear for developers to do things. Like builder.SetRuntimeMinifier<MyRuntimeMinfier>(); (as per james' example).

There's a few of other questions you had above in relation to some of this too

Is the intent that all Umbraco Composers ~35 disappear and become extensions on UmbracoBuilder?

Yes i think so, above I mentioned "But with IUmbracoBuilder perhaps the idea of Umbraco itself using composer/components might not make sense and these end up only being used by packages - though maybe that refactor if we want to do that can come later once the primary goals are reached."

Are IComposers just hanging around just for package devs and end-users?

Yes I think so, I don't think there's any benefit of having core 'IComposers' apart from just logicially separating code that registers different types of services but this can just as easily be done with ext methods. I would suggest even end-users just use their Startup code and not IComposers, so those should just be for packages.

If all core stuff is just registered by us, then IComposer's (or as James says to be renamed IUmbracoComposer) would execute then the whole ordering of ICoreComposer vs IUserComposer insanity doesn't need to exist.


Would this go on https://github.com/umbraco/UmbracoDocs? v9 branch?

I was only referring to POC/demo code for this discussion just to start getting a better understanding for all of us as to where we want to head in a more ideal startup approach. Like James' example but to include all 4 of these:

  • register services
  • execute code on startup/shutdown
  • execute code before/after another package
  • execute code based on IRuntimeState

Could even be a gist or something that is editable so it doesn't get lost in the abyss


I've been thinking about the ordering of service registrations:

I think we're only missing a clarification on how to control that serviceCollection.Replace<Something, MyThing>() is ran after serviceCollection.Register(). Without composition order, how do we solve that?

So this is b). Can this be solved with events too?

Maybe this just unnecessary and can be simplified with a callback or similar. Maybe something like:

public class MyComposer : IComposer
{
	public void ConfigureServices(IUmbracoBuilder builder)
        {
            // add callback to execute before the ServiceProvider is built but after all ConfigureServices calls are done
            // and before the end user's ConfigureServices is done
            builder.OnContainerBuilding(c => {
                 // will execute just before the container is built providing a final opportunity for packages
                 // to replace services from other packages
                 c.Replace<Something, MyThing>();
            });
        }
}

Since there's no way currently in v8 to have 2x different packages trying to replace a service of another package and knowing which one will win, then it doesn't actually make sense to control 'order', it only makes sense for a package to declare some code to replace services at a 'final' stage. This would achieve the same thing and doesn't mean we need to have packages listening for when other packages do things regarding composition.

If there's some crazy edge case where 2x packages are trying to replace another package's service or core service then it will be up to the end-user to define the replacement. These callbacks would need to execute 'before' the absolute final stage of when the end-user configure's services. The end-user's code should in Startup should be the absolute final place to replace/configure services.

That seems pretty simple to me but maybe I've totally missed/forgot something, let me know what you think

@p-m-j
Copy link
Contributor

p-m-j commented Nov 17, 2020

Examples

Register services

In startup

// Startup.cs
ConfigureServices(IServiceCollection services) 
{
   services.AddTransient<IFoo,Bar>();
}

In a composer

public class MyComposer : IUmbracoComposer
{
    public void Compose(UmbracoBuilder builder)
    {
        builder.Services.AddTransient<IFoo,Bar>();
    }
}

Execute code on startup/shutdown (+ RuntimeState, omit if not required)

Depends on event aggregator talks, if we go down that route there's no need for IComponent anymore

public class MyComponent : IComponent
{
    private readonly IRuntimeState _state;

    public MyComponent(IRuntimeState state)
    {
        _state = state;
    }

    // Maybe this happens in ctor instead if IComponent survives
    public void Initialize()
    {
        if(_state.Level < RuntimeLevel.Run)
            return;

        // Do Stuff
    }
}

With event aggregator that looks something like

// If this class is a singleton and requires shutdown code it just implements 
// INotificationHandler<UmbracoApplicationStopping> also
public class MyComponent : INotificationHandler<UmbracoApplicationStarting>
{
    public Task Handle(UmbracoApplicationStarting notification, CancellationToken cancellationToken)
    {
        if (notification.RuntimeLevel < RuntimeLevel.Run)
            return Task.CompletedTask;

        // Do Stuff
    }
}

Execute code before/after another package

At the moment this is handled by Composition order, I'm not sure whats happening here.

Do we actually have concrete examples and requirements for this?

@p-m-j
Copy link
Contributor

p-m-j commented Nov 17, 2020

Replacing composition with UmbracoBuilder and removing CoreComposers isn't too bad, it's just boring and begging for conflicts.
IUmbracoBuilder presumably needs to move out of Umbraco.Web.Common.Builder and into Core

What happens to collection builders are they still around?

Are we saying ComposeAfter Compose before would be gone entirely?

Services are then just configured in order of

  1. Umbraco internal builder stuff
  2. IUmbracoComposer (for plugins, ran in order of type finder discovery)
  3. Actions added by composers for OnContainerBuilding

@JimBobSquarePants
Copy link
Contributor Author

The state wouldn’t be a constructor parameter. Any requires information should be passed to the handler via Handle()

@p-m-j
Copy link
Contributor

p-m-j commented Nov 17, 2020

So RuntimeLevel would live as a prop on UmbracoApplicationStarting, sure.

Edit: updated example to match

@JimBobSquarePants
Copy link
Contributor Author

JimBobSquarePants commented Nov 17, 2020

Yep...

In startup I would always recommend using the UmbracoBuilder instance there also.

var builder = services.AddUmbraco();

builder.SetFileSystem<MyFileSystem>();

Though UmbracoBuilder would have to have a Build() or Complete() method to ensure auto registration occured.

@Shazwazza
Copy link
Contributor

What happens to collection builders are they still around?

yes these should remain as-is. Collections will need to be exposed one way or another on the IUmbracoBuilder just like they are currently on Composition. Later on/ideally we have nicer typed syntax for dealing with the different collections.

Depends on event aggregator talks, if we go down that route there's no need for IComponent anymore

I can see some benefits of this approach but I'm unsure they outweigh the amount of effort involved for us to do it and then to get all package developers to change since IComponent (or IUmbracoComponent) seems ok to me and it already exists/works without too many changes for both us and package devs. But is the consensus that event aggregator is better/simpler than IComponent (with IDisposable?) Can you inject things into that class? If so then why not just inject IRuntimeState instead of having to add that as part of UmbracoApplicationStarting? If you can't inject things into it, how do you access all the services you might need?

At the moment this is handled by Composition order, I'm not sure whats happening here. Do we actually have concrete examples and requirements for this?

Most concrete examples are package running things after certain core components but if all core services are registered ourselves before any package components are run then this scenario doesn't ever occur. I don't have a concrete example of a package that relies on another package using ComposeAfter but i'm sure it exists someplace, but like I said above, that would be solved in a simpler way by defining a callback during composition like the OnContainerBuilding example. Then ordering doesn't matter at all.

Services are then just configured in order of
Umbraco internal builder stuff
IUmbracoComposer (for plugins, ran in order of type finder discovery)
Actions added by composers for OnContainerBuilding

I think this needs to be further defined, something like:

  • end-user's ConfigureServices (user can add things before all of this)
  • Umbraco internal builder stuff
  • IUmbracoComposer (for plugins, ran in order of type finder discovery)
  • Actions added by composers for OnContainerBuilding
  • end-user can then replace things after all of this
// Startup.cs
ConfigureServices(IServiceCollection services) 
{
   // user can have custom services before Umbraco
   services.AddTransient<IFoo,Bar>();

  // will add all of Umbraco's services
   var umbracoBuilder = services.AddUmbraco();

   // does this makes sense to initiate the IComposers or maybe that just automatically happens in AddUmbraco
   // or there's some options that can be passed into AddUmbraco to control what it does.
   // this could run all IComposers and then run all OnContainerBuilding callbacks after
   umbracoBuilder.AddPackages();
   // Or I think this is the phase James wanted to be called Build()? 
 
   // user can replace Umbraco and package services and collections
  builder.SetFileSystem<MyFileSystem>();
  builder.OEmbedProviders().Append<Spotify>();

   // user can have custom services after Umbraco
   services.AddTransient<IHello, World>();
}

Just an example/idea, if it can be made better/simpler or if I'm way off please post :)

@p-m-j
Copy link
Contributor

p-m-j commented Nov 18, 2020

I can see some benefits of this approach but I'm unsure they outweigh the amount of effort involved for us to do it and then to get all package developers to change since IComponent (or IUmbracoComponent) seems ok to me and it already exists/works without too many changes for both us and package devs

The real win here isn't that we can 1:1 replace components with big event handlers, it's that most components just hook ContentService.Saving etc, if we were to replace the static events then you don't need a component in the first place as you can just add an extra handler for ContentSaving event right into service collection from a composer.

The ApplicationStarting event is more for something like creating the media directory if not exists (CoreInitialComponent) or setting up the file system watchers (ManifestWatcherComponent).

Can you inject things into that class? If so then why not just inject IRuntimeState instead of having to add that as part of UmbracoApplicationStarting?

Absolutely, the handlers are resolved from container, @JimBobSquarePants point was that in the case of ApplicationStarting, RuntimeLevel is relevant data for the event. It's not that you cant take a dep on RuntimeState, more that you shouldn't have to in this particular case.

end-user's ConfigureServices (user can add things before all of this)
end-user can then replace things after all of this

Yeah all the concern has been around no-code support.

If you have ability to edit Startup.cs (or replace it entirely) then you can add code at the beginning or end of ConfigureServices.

@JimBobSquarePants
Copy link
Contributor Author

JimBobSquarePants commented Nov 18, 2020

@Shazwazza I think CollectionBuilderBase and any derived types need a thorough review. As-is they seem to be responsible for resolving items which they shouldn't. I think there's great potential for simplification and optimization there.

@Shazwazza
Copy link
Contributor

@JimBobSquarePants yep sounds great, there's probably a lot of legacy/etc... in there! There's stuff in there that shouldn't even exist in v8 (i.e. LazyCollectionBuilderBase) do we want a call about that and/or Is it worth starting a diff ticket specifically about this?

@p-m-j
Copy link
Contributor

p-m-j commented Nov 19, 2020

I have the site working for install & run (backoffice good post install) with CoreRuntimeBootstrapper removed.

The tests aren't very happy but this is huge.

RuntimeState.Level determined as part of Startup.Configure instead of Startup.ConfigureServices.

We can drop this BS

public static IUmbracoBuilder AddUmbracoCore(...
  Func<GlobalSettings, ConnectionStrings, IUmbracoVersion, IIOHelper, ILoggerFactory, IProfiler, IHostingEnvironment, IBackOfficeInfo, ITypeFinder, AppCaches, IDbProviderFactoryCreator, CoreRuntimeBootstrapper> getRuntimeBootstrapper)

We never have to call BuildServiceProvider, not even once! (if we fix a couple more little bits)


And tests passing, there's still more cleanup to do but this is so much better 8b97ce6 (and my wife will kill me if I don't step away from PC)

@JimBobSquarePants
Copy link
Contributor Author

LEGEND!

@p-m-j
Copy link
Contributor

p-m-j commented Nov 20, 2020

It's gone, the crazy func is gone!!!

Only calls to BuildServiceProvider are in Test projects (and Umbraco.Web but that doesn't count)

@p-m-j
Copy link
Contributor

p-m-j commented Nov 24, 2020

I believe that once #9415 is merged we could close off this issue?

#9397 & #9446 document the further desirable enhancements

Possibly require an extra issue for some of the syntactic sugar extensions on UmbracoBuilder but I think it's probably better if someone else types that up @JimBobSquarePants?

@JimBobSquarePants
Copy link
Contributor Author

Agreed. I'll have a look at raising a specific issue.

@p-m-j
Copy link
Contributor

p-m-j commented Nov 24, 2020

Oh and collection builder review issue?

@Shazwazza
Copy link
Contributor

Merged 🎉 🚀 bloody amazing work everyone 😍 and yep I agree I think we should close this issue now and discuss further on #9397 & #9446 + created issues re: UmbracoBuilder enhancements and collection builders

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants