The service:
public interface IMessageWriter
{
void Write(string message);
}
public class ConsoleMessageWriter:IMessageWriter, IDisposable
{
public void Dispose()
{
Console.WriteLine($"The {GetType().Name} instanse is disposed.");
}
public void Write(string message)
{
Console.WriteLine(message);
}
}
Now I use it in a scope:
using DI_Sandbox.Models;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services
.AddScoped<ConsoleMessageWriter>()
.AddScoped<IMessageWriter>(provider => provider.GetRequiredService<ConsoleMessageWriter>());
var container = services.BuildServiceProvider(validateScopes:true);
using (var serviceScope = container.CreateScope())
{
var messageWriter1 = serviceScope.ServiceProvider.GetRequiredService<IMessageWriter>();
var messageWriter2 = serviceScope.ServiceProvider.GetRequiredService<IMessageWriter>();
Console.WriteLine($"messageWriter1 == messageWriter2: {messageWriter1 == messageWriter2}");
messageWriter1.Write("Hello DI!");
}
Console.Write("THE END");
The output:
messageWriter1 == messageWriter2: True
Hello DI!
The ConsoleMessageWriter instanse is disposed.
The ConsoleMessageWriter instanse is disposed.
THE END
Why the scoped service was disposed twice? How to fix it? I need dispose the service once.
You made two registrations. One for ConsoleMessageWriter
and one for IMessageWriter
:
services.AddScoped<ConsoleMessageWriter>()
services.AddScoped<IMessageWriter>(p =>
p.GetRequiredService<ConsoleMessageWriter>());
While the registration for IMessageWriter
calls back into the ConsoleMessageWriter
registration, MS.DI has no knowledge of this happening. It does know, however, that both registrations result in an IDisposable
object. This happens even though IMessageWriter
does not implement IDisposable
. The check is done at time of resolve. Each registration tracks its returned object, not knowing that this is the same object.
You can consider this to be a design flaw, because at the very least, MS.DI could have made sure that the list of disposable instance was deduplicated before calling Dispose
on them. But that's probably a performance trade off, because:
Would you have done the registration as follows, it would have lead to the Torn Lifestyle problem resulting in two ConsoleMessageWriter
instances within a single scope:
// Incorrect registration. Leads to a Torn Lifestyle
services.AddScoped<ConsoleMessageWriter>()
services.AddScoped<IMessageWriter, ConsoleMessageWriter>();
MS.DI's design is simple (some would say naive) and the only way to prevent components from getting a Torn Lifestyle is making the registration you did in your question:
services.AddScoped<ConsoleMessageWriter>()
services.AddScoped<IMessageWriter>(p =>
p.GetRequiredService<ConsoleMessageWriter>());
In other words: you have a registration that calls back into the container to resolve the actual component you are looking for.
More-mature DI Containers have more sophisticated ways of preventing Torn Lifestyles. Two that come to mind are Unity and Simple Injector. Simple Injector, for instance, would allow you to make the registration as follows while still ensuring one ConsoleMessageWriter
instance per scope:
// Registrations using Simple Injector. No Torn Lifestyle here.
container.Register<ConsoleMessageWriter>(Lifestyle.Scoped);
container.Register<IMessageWriter, ConsoleMessageWriter>(Lifestyle.Scoped);
Unity IoC has similar behavior, while other DI Containers might require a similar construct to what you need with MS.DI.
If the duplicate dispose causes an issue, a way to circumvent this is by creating a non-disposable wrapper class for the IMessageWriter
interface, e.g.:
public sealed class NonDisposableMessageWriterWrapper : IMessageWriter
{
private readonly IMessageWriter decoratee;
public NonDisposableMessageWriterWrapper(IMessageWriter decoratee) =>
this.decoratee = decoratee;
public void Write(string message) => this.decoratee.Write(message);
}
And change the registrations to the following:
services.AddScoped<ConsoleMessageWriter>()
services.AddScoped<IMessageWriter>(p =>
new NonDisposableMessageWriterWrapper(
p.GetRequiredService<ConsoleMessageWriter>()));
This way ConsoleMessageWriter
is no longer disposed of twice.
But is this extra complexity worth it? Probably not.