Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In Simple Injector why is it an error for a singleton or scoped service to depend on a transient service

I'm using simple injector 3.0.4

I have a service that with a lifestyle of scoped dependent on a service which has a lifestyle of transient.

When I call container.Verify() I get a diagnostic error about lifestyle mismatch.

The transient service causing the issues is injected into other transient services so before I go ahead and make my whole project scoped I need to ask. Why is a dependency from a scope of any lifestyle to transient an issue? Transients are newed up fresh each time they are injected so nothing else can interfere with it. In essence the lifetime of a transient object is governed by the service it is injected into.

Also I have already read the documentation on this subject from here, and I do understand why you wouldn't want a singleton dependent on a scoped service for instance but surely a dependency on transient is always safe?

like image 762
Twisted Avatar asked Oct 12 '15 12:10

Twisted


1 Answers

Transients are newed up fresh each time they are injected so nothing else can interfere with it.

Transients are newed up every time you request them from the container, but once they are injected into a component, they will stick around for as long as that component lives. So if the consuming component is a singleton, this means that it will drag along all its dependencies, making them practically singletons as well. This behaviour becomes obvious when you look at how you would typically implement dependency injection:

public class SomeComponent
{
    private readonly ILogger logger;
    private readonly IService service;

    public SomeComponent(ILogger logger, IService service) {
        this.logger = logger;
        this.service = service;
    }
}

As you can see, the dependencies are stored in private fields of the component, will cause them to stay alive for as long as SomeComponent lives and SomeComponent will keep using the same dependencies.

In essence the lifetime of a transient object is governed by the service it is injected into.

Exactly; a component's lifestyle will be at least as long as its consumer. However, a dependency can have multiple consumers with different lifestyles, making it really hard to see how long the dependency will live. When injected into consumer 1 it might live for the duration of the request, while another instance of that dependency, injected into consumer 2, will live for as long as the application does.

Just as scoped instances, transient registrations are typically not thread-safe; otherwise you would have registered them as singleton. Keeping transients alive for a longer period of time, can obviously cause concurrency bugs or bugs related to stale data. This is why Simple Injector disallows this by default and throws an exception.

You might be confused by how Autofac defines its lifestyles, compared to Simple Injector. Autofac does not contain a Transient lifestyle. Instead it has an InstancePerDependency lifestyle. Technically this is the same as transient, but the intent is very different. With InstancePerDependency you say: "This component is intended to live as long as its consumer, whatever that lifestyle might be". There might be cases where this makes sense, but by doing this you are actually ignoring the elephant in the room and I've seen the lack of detection to be a common source of bugs. In most cases, if you don’t care about a components lifestyle, it means it should be registered as singleton – not InstancePerDependency.

The reason that Simple Injector doesn't allow transients to be injected into scoped instances is because scoped instances as well can live for a long time (depending on the application) and you can't always assume that transients can safely be injected into scoped consumers.

In the end it is all about communicating the intent of your code. In case a component is stateless or thread-safe, you should register it as singleton. It it's not thread-safe, you register it as scoped or transient. This makes it clear to anyone who reads the configuration how he should handle such component AND it allows Simple Injector to detect any misconfigurations for you.

While Simple Injector detects misconfigurations for you, I came to the conclusion that your DI configuration can be simplified considerably when your system is designed around object graphs that consist purely of singleton components. I expressed these thoughts here. This will remove many of the complexities we face when working with dependency injection, and even exposes SOLID principles violation even more quickly than DI by itself already does.

before I go ahead and make my whole project scoped

That is something I would not advice to do. You would typically see that only some of the 'leaf components' in your application are scoped (such as DbContext). Those scoped components don't depend on many other components. The components that you write yourself should typically be stateless and don't need any caching. So in case making the object graphs singleton is (not yet) an option, I would typically make as much of the object graph transient, and only those few leaf components scoped. Since transients can safely depend on scoped instances, everything will work as expected.

like image 169
Steven Avatar answered Dec 12 '22 10:12

Steven