-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Description
By the principles of IoC, classes should not own, or have any special knowledge of, their dependencies. Attempts to work around this tend to cause problems and usually result in questionable design.
Following this rule, containers must own instances from creation until disposal.
The container already knows when to dispose of most services: singletons are disposed at the end of the container’s lifetime, and scoped services at the end of the scope’s lifetime. The only unresolved case is transient services, as the container has no reliable signal indicating when they are no longer in use and can be released.
To provide this information to the provider, we would need to change how services are consumed, because the consumer is the one supplying (or not supplying) that information. Changing anything at registration time does not help, as the consumer has no visibility into it. For example, a service may register a transient dependency that it later consumes, but at the point of consumption it cannot know whether the registration was modified, or even whether the service is transient. That would violate the rule stated above.
API Proposal
To avoid breaking changes, this would require introducing a new interface to support the feature, together with an extension method on IServiceProvider that checks whether the provider implements the new interface.
public readonly struct Lease<T> : IDisposable
{
public T Service { get; }
public void Dispose() { throw null; }
}
public interface ILeaseServiceProvider
{
Lease<T> LeaseService<T>();
}
public static class DependencyInjectionExtensions
{
public static Lease<T> LeaseService<T>(this IServiceProvider provider)
{
if (provider is not ILeaseServiceProvider leaseProvider)
{
throw NotSupportedException ("The container doesn't support leases.")
}
return leaseProvider.LeaseService<T>();
}
}The mechanism should guarantee thread-unsafe idempotency on disposal, not using the service after disposal is called is on the consumer side as we can't guarantee it at all.
Leasing doesn't make much sense for dependency resolution as the end of life of the service is a natural end of life of the leased service too.
Alternatives
Of course, the most obvious alternative is to require users to use a custom factory (likely obtained from DI) to acquire instances they want to own and manage explicitly, especially when those instances are used heavily or in ways that put pressure on the container.
That said, an alternative to the API described above would be to use a class-based lease:
public class Lease<T> : IDisposable
{
public T Service { get; }
public void Dispose() { throw null; }
}Using a class would guarantee synchronisation, as it is passed by reference. That would allow disposal idempotence to be enforced by the lease item itself.
Moreover, most allocation scenarios could be mitigated by making the lease class resettable and allowing users to optionally supply it. They have the necessary context and can guarantee the correctness of the control flow. Typical hot-path scenarios involving transient services are short-lived and well-encapsulated, so storing a lease instance in a thread-static variable would likely be sufficient in practice.
public interface ILeaseServiceProvider
{
Lease<T> LeaseService<T>(Lease<T>? lease);
} <>
public static class DependencyInjectionExtensions
{
public static Lease<T> LeaseService<T>(this IServiceProvider provider, Lease<T>? lease) { throw null; }
}Another consideration is whether the lease should implement IAsyncDisposable to handle services that implement this interface. I do not think it should, as disposing the lease merely informs the container that the service is no longer in use, which is not an asynchronous operation. How the service is ultimately disposed is a concern of the container, not of the lease itself. That said, I am open to discussion.
Non-viable alternatives
There are a couple of other alternatives I can see, with what seem to be straightforward reasons why they are not viable. That said, I might be wrong, and I am happy to discuss any of them:
- Add the Lease class to DI as an open generic. However, since the mechanism would need to cooperate with the container’s internal workings, this approach would likely come with many caveats and edge cases. At a minimum, the container would need to inspect each service to determine whether it is of this type and handle it differently. Moreover, this could easily lead to complicated situations when multiple container backends are involved.
- Instead of returning a type, add another method to ILeaseServiceProvider and let users simply return the service when it is no longer needed. The problem is that there would be nothing to anchor this to: the returned value would just be an instance. While it would at least originate from the provider, we would still need a way to identify what action to take with it. In addition, services are typically instantiated through scopes, and there is no guarantee that users would return the service to the correct scope. This would require an additional mechanism to synchronise scopes, which feels significantly more complex and risky.
- Use WeakReference and let users stop using the service. This would require a handle to the weak reference, and each weak reference is itself a class, resulting in one or two additional allocations. More importantly, we would still need to keep a reference in order to dispose the instance, and relying on finalisers does not guarantee that they will run or that disposal will actually happen.