-
Notifications
You must be signed in to change notification settings - Fork 5.5k
Add observability hooks to IServiceCollection (OnAdded, OnReplaced, OnRemoved) #128166
-
Hi everyone,
I'd like to open a discussion about introducing observability features to IServiceCollection to allow developers to hook into service registration mutations.
The Problem
When building comprehensive, modular frameworks or custom enterprise architectures, it is often necessary to react dynamically to dependency injection registrations. Currently, if we want to intercept when a ServiceDescriptor is added, replaced, or removed, we have to create a custom wrapper around IServiceCollection (by implementing IList<ServiceDescriptor>). While this workaround functions, it can be cumbersome, introduces unnecessary allocations, and a standardized, low-overhead approach within the base abstraction would be significantly cleaner.
Proposed Solution
It would be highly beneficial to have built-in hooks or events—such as OnAdded, OnReplaced, and OnRemoved—that trigger during the container configuration phase (before the IServiceProvider is built).
Having native observability would allow framework authors to easily:
- Auto-register dependencies: Automatically append decorators, interceptors, or companion services the moment a specific core type is registered.
- Enforce architectural boundaries: Validate registrations on the fly to ensure that core framework services aren't unintentionally replaced, removed, or registered with the wrong lifetime.
Caveats and Considerations
I am not entirely sure if there are strict dependency injection specifications, backward-compatibility constraints (since IServiceCollection is heavily relied upon as a standard IList), or underlying architectural rules that limit this kind of API addition. I am just putting this concept forward to see if it aligns with the runtime's design philosophy and to gather feedback from the team and community.
Would love to hear your thoughts on the feasibility of this, or if there is already an endorsed pattern for achieving this without wrapping the collection!
Beta Was this translation helpful? Give feedback.
All reactions
Replies: 2 comments
-
IServiceCollection is not a concrete implementation. It's just an interface like IList<T>. The request is about an implementation similar to ObservableCollection<T>.
In this case, a Collection<ServiceDescriptor> based implementation is simply sufficient:
class ObservableServiceCollection : Collection<ServiceDescriptor>, IServiceCollection { public event Action<ServiceDescriptor>? OnAdded; public event Action<ServiceDescriptor, ServiceDescriptor>? OnReplaced; public event Action<ServiceDescriptor>? OnRemoved; protected override void InsertItem(int index, ServiceDescriptor item) { OnAdded?.Invoke(item); base.InsertItem(index, item); } protected override void SetItem(int index, ServiceDescriptor item) { OnReplaced?.Invoke(this[index], item); base.SetItem(index, item); } protected override void RemoveItem(int index) { OnRemoved?.Invoke(this[index]); base.RemoveItem(index); } }
Beta Was this translation helpful? Give feedback.
All reactions
-
Do you have an example of what you've done so far that has led you to want a feature like this?
Working with an IServiceCollection instance is very abstract, via a builder-like interface. It's a means to describe services that may or may not get used at run-time, but are not coupled to each other at design-time. Expecting to interact with the collection while it's being constructed seems at odds with the purpose of dependency injection/inversion. i.e., the collection is simply a registration of types each with a lifecycle, so that they can be resolved when necessary. One of the reasons for this is to isolate the order of usage from the order of registration to further enable that loose coupling. Coupling by an event at time of registration is tighter temporal coupling that a DI container would normally have. There are existing frameworks like Scutor that can implement things like decorators to effectively wrap other registrations without these sorts of hooks. I think the extensibility point you're looking for is a custom service provider factory. One that validates service relationships after all registrations have been made and the provider is created. For example:
/// <summary> /// A service provider factory that validates service registrations to ensure no captive dependencies exist. /// </summary> /// <remarks> /// This factory is designed to enforce best practices in dependency injection by /// detecting and preventing the use of captive dependencies, which can lead to /// unintended behavior in applications. /// </remarks> public class ValidatingServiceProviderFactory : IServiceProviderFactory<IServiceCollection> { public IServiceProvider CreateServiceProvider(IServiceCollection services) { ThrowIfCaptiveDependencies(services); return new DefaultServiceProviderFactory().CreateServiceProvider(services); } private static void ThrowIfCaptiveDependencies(IServiceCollection services) { foreach (var descriptor in services.Where(s => s.ImplementationType is not null && s.Lifetime == ServiceLifetime.Singleton)) { var q = (from p in descriptor.ImplementationType! .GetConstructors() .SelectMany(p => p.GetParameters()) join s in services on p.ParameterType equals s.ServiceType select new {s.Lifetime, s.ServiceType, s.ImplementationType}).ToArray(); if (q.Length != 0) { throw new InvalidOperationException($"{descriptor.ImplementationType} with a Singleton lifetime depends on " + $"{string.Join(" and ", q.Select(g => $"{g.ImplementationType} with a {g.Lifetime} lifetime"))}."); } } } IServiceCollection IServiceProviderFactory<IServiceCollection>.CreateBuilder(IServiceCollection services) => services; }
Beta Was this translation helpful? Give feedback.