Open eerhardt opened 3 years ago
Tagging subscribers to this area: @eerhardt, @maryamariyan
See info in area-owners.md if you want to be subscribed.
Description: | See the conversation here: https://github.com/dotnet/runtime/pull/44696/files#r523781023 We are currently looping over the IServiceCollection looking if a ServiceDescriptor has already been added for this ServiceType. Not only are we going through the whole collection for each new service added, but we are also making interface dispatches for each element. Instead, we should introduce a new interface that [`ServiceCollection`](https://github.com/dotnet/runtime/blob/162b33e19eb5a3434867541be691e1082039b130/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceCollection.cs#L12) implements, so we can fast path this check. While introducing this new interface, we should analyze if there are other paths on the startup path that can be optimized as well, in case we need other new methods. cc @davidfowl @stephentoub @benaadams |
---|---|
Author: | eerhardt |
Assignees: | - |
Labels: | `area-Extensions-DependencyInjection`, `untriaged` |
Milestone: | - |
Hi, I would ask some questions if this issue is allowed to improve now, thanks.
Based on conversation, should ServiceCollection add new Dictionary<ServiceType, collection of serviceDescriber> field and keep old List field? This will have synchronous action when calling modified related method for example RemoveAll() ? Or other solution ? Thanks!
@stephentoub In answer to your question https://github.com/dotnet/runtime/pull/44696#discussion_r523781023
How many times are these methods called in a typical startup? The fact that you said it's saving ~750 closures suggests it's called to add that many services, and this is O(N^2) (for each service looking at every other service already added)?
Looking at the startup call counts; I'm going to say yes...
Can see it growing per call (not completely in order)
We should look into this as part of 6.0.0 in the name of startup performance. It'll also require a new API which we should try to get approved.
Assigning to myself to do.
I can think of 2 options here:
ServiceCollection
type to the abstractions package and add the methods there. We would add a type forward to avoid the breaking change and detect the concrete type in the extension methods and optimize there.I prefer option 2 as it avoids interface dispatch and avoids introducing a new type.
namespace Microsoft.Extensions.DependencyInjection
{
public class ServiceCollection
{
+ public void TryAdd(ServiceDescriptor descriptor);
+ public void TryAddEnumerable(ServiceDescriptor descriptor);
+ public void Replace(ServiceDescriptor descriptor);
+ public void RemoveAll(Type serviceType);
}
}
Thoughts @eerhardt and @maryamariyan?
I prefer option 2 as it avoids interface dispatch and avoids introducing a new type.
How common is it for others to implement their own ServiceCollection
?
What do you think about option 3:
abstract class ServiceCollectionBase
in Abstractions that ServiceCollection
inherits from that implements all methods on IServiceCollection
and the above proposed methods.Option 3 sounds fine as well but my hottake is that nobody writes their own IServiceCollection so it's not worth it.
my hottake is that nobody writes their own IServiceCollection
Ok, then Option 2 sounds good to me.
OK, I've updated the proposal to include the API
Ok, then Option 2 sounds good to me.
LGTM. Just some thoughts for API review: Replace
-> TryReplace
?
Even though the existing TryAdd
APIs:
don't return bool, would we want to make these new ones return bool?
LGTM. Just some thoughts for API review: Replace -> TryReplace ?
We could, yes.
don't return bool, would we want to make these new ones return bool?
That sounds good. I'll update.
Option 3 sounds fine as well but my hottake is that nobody writes their own IServiceCollection so it's not worth it.
Outside of unit tests (where we probably don't really care about an inefficient fallback), there is one other third party implementation I am aware of:
Lamar implements IServiceCollection on its main Registry type. They even encourage users to skip the RegisterServices Method in Startup entirely, and just use public void ConfigureContainer(ServiceRegistry services)
, so you can freely mix service registrations based on IServiceCollection extension methods with native registrations that can use its additional features.
It also already has a base class, so it could not derive from some abstract base.
Could also add them as default implementations on the IServiceCollection
interface and then specifically make it more optimized in the implementation for ServiceCollection
On everything but netstandard2.0.
K, but how would the new types on ServiceCollection
be fast pathed? Using is
tests in the IServiceCollection
extension methods? (Which would reduce to limited subset of types)
K, but how would the new types on ServiceCollection be fast pathed? Using is tests in the IServiceCollection extension methods? (Which would reduce to limited subset of types)
Yep. I'd wager that 98% of use cases are ServiceCollection instances. Though I don't have a problem trying to make this work for Lamar as well.
Though I don't have a problem trying to make this work for Lamar as well.
They would have to change their inheritect for ServiceRegistry
to inherit from ServiceCollection
and then rather than being a straight typecheck in the extensions its now an inheritence walk (still faster than O(N²)
that it is currently); and then isn't it essentally making in the IServiceCollection
interface irrelevant as you'd need to always be a strongly typed ServiceCollection
?
OTOH if a default interface was also added (and ServiceCollection
implements the methods); then on .NET Core 3.1+ the IServiceCollection
is still viable (Lamar does have a .NET5.0 build)
public interface IServiceCollection : IList<ServiceDescriptor>
{
#if NETCOREAPP3_1_OR_GREATER
public bool TryAdd(ServiceDescriptor descriptor) => ServiceCollectionDescriptorExtensions.TryAdd(this, descriptor);
public bool TryAddEnumerable(ServiceDescriptor descriptor) => ServiceCollectionDescriptorExtensions.TryAddEnumerable(this, descriptor);
public bool TryReplace(ServiceDescriptor descriptor) => ServiceCollectionDescriptorExtensions.TryReplace(this, descriptor);
public void RemoveAll(Type serviceType) => ServiceCollectionDescriptorExtensions.RemoveAll(this, serviceType);
#endif
}
The extensions can still have a typecheck for ServiceCollection
on pre-netcoreapp3.1 platforms; best of both worlds?
Right, I don't think there's an either or here. We should do the move and the cast anyways. We can use DIMs on IServiceCollection as well on .NET Core specific frameworks. I'm just saying that besides Lamar, I don't think anyone extends this type and it forces them to cross compile for the performance boost anyways. As long as that's fine, we can do both things.
Checking implementations using grep.app serach : IServiceCollection for C# are a few implementations and extensions:
Azure/azure-webjobs-sdk
Microsoft.Azure.WebJobs.Host/Hosting/TrackedServiceCollection.cs
internal class TrackedServiceCollection : IServiceCollection, ITrackedServiceCollection
dotnet/maui
Hosting/IMauiServiceCollection.cs
public interface IMauiServiceCollection : IServiceCollection
Hosting/Internal/MauiServiceCollection.cs
class MauiServiceCollection : IMauiServiceCollection
Hosting/IMauiHandlersCollection.cs
public interface IMauiHandlersCollection : IMauiServiceCollection
Hosting/ImageSources/IImageSourceServiceCollection.cs
public interface IImageSourceServiceCollection : IServiceCollection
etc
OrchardCMS/OrchardCore
OrchardCore/Shell/Builders/FeatureAwareServiceCollection.cs
public class FeatureAwareServiceCollection : IServiceCollection
sevenTiny/Bamboo
SevenTiny.Bantina.Spring/DependencyInjection/ServiceCollection.cs
public class ServiceCollection : IServiceCollection
davidrevoledo/AspNetCore.AutoHealthCheck
AspNetCore.AutoHealthCheck.Abstractions/IAutoHealthCheckBuilder.c
public interface IAutoHealthCheckBuilder : IServiceCollection
blqw/blqw.DI
blqw.DI.Startup/ServiceCollection.cs
internal class ServiceCollection : IServiceCollection
asimmon/Pillar
Pillar.Ioc/ServiceCollection.cs
public class ServiceCollection : IServiceCollection
gmf520/osharp-v4
OSharp.Core/Dependency/ServiceCollection.cs
public class ServiceCollection : IServiceCollection
Also a bunch that don't start : IServiceCollection
but has something in the middle like
: List<ServiceDescriptor>, IServiceCollection
Check their target frameworks as well
Check their target frameworks as well
Ah, get you, you mean if they target netstandard2.0
then they wouldn't get the perf boost from the DIMs as they'd need to also a target netcoreapp3.1+ TFM?
OTH they wouldn't get the performance improvement anyway as they don't inherit from ServiceCollection
🤷♂️
Approved as proposed.
namespace Microsoft.Extensions.DependencyInjection
{
public class ServiceCollection
{
+ public bool TryAdd(ServiceDescriptor descriptor);
+ public bool TryAddEnumerable(ServiceDescriptor descriptor);
+ public bool TryReplace(ServiceDescriptor descriptor);
+ public void RemoveAll(Type serviceType);
}
}
We decided not to add new APIs here. We moved the type and are keeping these internal for now,
Should we close this issue then?
Moving a type to a different assembly is still an "API" change, right?
That's fair.
@davidfowl I am one of those that implement their own IServiceCollection. It is not uncommon when creating non-MS DI integrations. My IServiceCollection wraps my DI's container's builder, to expose 'fake' service registrations out of it, so it 'looks like' services registered outside of MS DI are present.
Because so many MS libraries try to call TryAdd* and then duplicate crap if they don't see it already present.
Without this, I end up with a billion registrations of IOptions stuff, for instance.
If there were Try* methods available to me, I'd implement those directly on my IServiceCollection, instead of exposing a manufactured fake list of stuff.
See the conversation here: https://github.com/dotnet/runtime/pull/44696/files#r523781023
We are currently looping over the IServiceCollection looking if a ServiceDescriptor has already been added for this ServiceType. Not only are we going through the whole collection for each new service added, but we are also making interface dispatches for each element.
Instead, we should introduce a new interface that
ServiceCollection
implements, so we can fast path this check.While introducing this new interface, we should analyze if there are other paths on the startup path that can be optimized as well, in case we need other new methods.
cc @davidfowl @stephentoub @benaadams
Proposed API