z4kn4fein / stashbox

A lightweight, fast, and portable dependency injection framework for .NET-based solutions.
https://z4kn4fein.github.io/stashbox
MIT License
140 stars 10 forks source link

Singleton lifetime instances share dependencies #75

Closed schuettecarsten closed 4 years ago

schuettecarsten commented 4 years ago

I register several services with Singleton lifetime. That works so far. But all singletons share their dependencies. Is it possible to register a singleton service and tell Stashbox to use a dedicated, "isolated" scope for this service?

z4kn4fein commented 4 years ago

Yep, there are options to achieve that, you can mark your singletons as "isolated" scopes with the DefinesScope() option, then you can register their dependencies with Scoped lifetime, so the singletons will get their own dependency instances.

Like:

class Singleton1
{
    public SingletonDep Dep { get; set; }    

    public Singleton1(SingletonDep dep) { this.Dep = dep;  }
}

class Singleton2
{
    public SingletonDep Dep { get; set; }

    public Singleton2(SingletonDep dep) { this.Dep = dep;  }
}

container.Register<Singleton1>(c => c.WithSingletonLifetime().DefinesScope())
    .Register<Singleton2>(c => c.WithSingletonLifetime().DefinesScope())
    .RegisterScoped<SingletonDep>();

var isSame = container.Resolve<Singleton1>().Dep == container.Resolve<Singleton2>().Dep; //false

So with this, you will get individual SingletonDep instances bound to the lifetime of your singletons. Is this the thing you are searching for?

Let me know if you have further questions or if this isn't what you need.

schuettecarsten commented 4 years ago

That looks good, but DefinesScope() needs a parameter in my version. Is null valid here or do I need to generate a custom (unique) scope name myself for each registration?

Another question, if I use DefinesScope(), it looks like a custom registration directly on the Container is not resolved. The following registration, that is done at the end of all other registrations, is not used when resolving a service that uses DefinesScope():

container.RegisterTypes(new Type[]
    {
        typeof(IServiceLocator),
        typeof(IServiceProvider),
    }, null, context => context
        .ReplaceExisting()
        .WithInstance(serviceLocator)
        .WithoutDisposalTracking());
z4kn4fein commented 4 years ago

Ah yeah, sorry, the parameterless option is only available in the prerelease version, it's not released yet, you can use a custom scope name with the stable version for now.

As I can see the problem could be that you are using the RegisterTypes() to register interface types, but the way it works is that it only accepts implementations and auto-discovers their implemented interfaces and base classes to register for. So it completely skips this configuration section, because the types you pass are all interfaces.

The thing you want to achieve can be configured like this:

container.Register<IServiceLocator>(serviceLocator.GetType(), context => context
    .WithInstance(serviceLocator)
    .AsServiceAlso<IServiceProvider>()
    .ReplaceExisting()
    .WithoutDisposalTracking());

Just for the note, if the IServiceProvider you are using is from the System namespace it's automatically mapped to the container (the current resolution scope actually), you don't have to register it manually.

Sorry if the behavior caused confusion, maybe the docs should be clearer.

schuettecarsten commented 4 years ago

Even if I use your code, it does not work. My sample was reduced meta-code, here is the original one. The factory method is never called.

container.Register<IServiceLocator>(typeof(StashboxObjectProvider),
    context => context
        .WithFactory(delegate(IDependencyResolver resolver)
        {
            return new StashboxObjectProvider(container, resolver);
        })
        .AsServiceAlso<IObjectProviderRoot>()
        .AsServiceAlso<IObjectProvider>()
        .AsServiceAlso<IServiceProvider>()
        .ReplaceExisting()
        .WithoutDisposalTracking());

Even other versions do not work:

container.Register<StashboxObjectProvider>(context => context
    //.WithInstance(objectProvider)
    .WithFactory(delegate(IDependencyResolver resolver)
    {
        return new StashboxObjectProvider(container, resolver);
    })
    .AsImplementedTypes()
    .ReplaceExisting()
    .WithoutDisposalTracking());
z4kn4fein commented 4 years ago

Ah I see, could you please send me the code of the StashboxObjectProvider and a class which shows the usage of it? Thanks!

z4kn4fein commented 4 years ago

With which type are you referring to this registration? Are you referring to it with IServiceProvider as a dependency? The only thing I could think that the container chooses the current resolution scope as IServiceProvider instead of your implementation (because it implements System.IServiceProvider) and that is why your factory never called. If this is the case then I can make this feature configurable, but have you considered using the current scope as IServiceProvider dependency?

schuettecarsten commented 4 years ago

Just a quick info, this problem is still there with latest 3.1-preview.

schuettecarsten commented 4 years ago

I think I was able to figure out the root cause. I hope I can explain it correctly, as Stashbox is quite complex here and it took me some time to debug and understand what's going on.

The problem is here (ResolutionStrategy.cs, line 29++):

    if (resolutionContext.ResolutionScope.HasScopedInstances)
    {
        var scopedInstance = resolutionContext.ResolutionScope
            .GetScopedInstanceOrDefault(typeInformation.Type, typeInformation.DependencyName);
        if (scopedInstance != null)
            return resolutionContext.CurrentScopeParameter
                .CallMethod(Constants.GetScopedInstanceMethod,
                    typeInformation.Type.AsConstant(),
                    typeInformation.DependencyName.AsConstant())
                .ConvertTo(typeInformation.Type);
    }

The problem is that resolutionContext.ResolutionScope is the current scope that was used to resolve the service from Stashbox. But if the requested service has a singleton or named scope lifetime, then the singleton-/named scope must be used here, not the current scope.

In my code, I create a scope like this:

    this.dependencyResolver = dependencyResolver
    .BeginScope(name, attachToParent)
    .PutInstanceInScope<IServiceLocator>(this, true)
    .PutInstanceInScope<IServiceProvider>(this, true)
    .PutInstanceInScope<IObjectProvider>(this, true)
    .PutInstanceInScope<IObjectProviderScope>(this, true);

I put my IObjectProvider instance into the scope to make sure that it is returned. This works in normal cases, but fails when a different scope is used at resolution time. In this case, Stashbox looks into my scope, finds an IObjectProvider instance and generates the expression. When the expression is executed, it uses the lifetime scope, which was created by Stashbox and does not know anything about an IObjectProvider instance.

schuettecarsten commented 4 years ago

This works with latest changes for #77, so I close this issue.