Search code examples
scopeninjectnamed-scopeninject-extensions

Can't figure out why Ninject Named Scope isnt working as expected


I have a console application which I wrote a privately constructed Bootstrapper class, exposing a Default property that gives access to the Bootstrapper with only one public method available. I have an issue where the NamedScope that I can not resolve instances registered as InNamedScope from the NamedScope with the same name. Code follows:

public class Bootstrapper
{
    private const string SCOPENAME = "BOOTSTRAPPED";
    private static object KernelConstructionLocker = new object();
    private StandardKernel Kernel;
    private readonly Bootstrapper defaultBootstrapper = new Bootstrapper();
    private Bootstrapper Default {get { return defaultBootstrapper; }}
    private Bootstrapper() 
    { }

    public GetResolutionRoot()
    {
        if (Kernel == null)
        {
            //Kernel ctor not thread safe
            lock(KernelConstructionLocker)
            {
                //double locked incase thread created while locked
                if (Kernel == null)
                {
                    Kernel = CreateKernel(); 
                }
            }
        }

        return new TaskExecutionScope(Kernel.CreateNamedScope(SCOPENAME));
    }

    private CreateKernel()
    {
        Kernel = new StandardKernel();

        //bindings, etc...
    }
}

    public class TaskExecutionScope : IResolutionRoot, IDisposable
    {
        private readonly NamedScope scope;

        internal TaskExecutionScope(NamedScope scope)
        {
            this.scope = scope;
        }

        public bool CanResolve(Ninject.Activation.IRequest request, bool ignoreImplicitBindings)
        {
            var canResolve = scope.CanResolve(request, ignoreImplicitBindings);

            return canResolve;
        }

        public bool CanResolve(Ninject.Activation.IRequest request)
        {
            var canResolve = scope.CanResolve(request);

            return canResolve;
        }

        public Ninject.Activation.IRequest CreateRequest(Type service, Func<Ninject.Planning.Bindings.IBindingMetadata, bool> constraint, System.Collections.Generic.IEnumerable<Ninject.Parameters.IParameter> parameters, bool isOptional, bool isUnique)
        {
            var request = scope.CreateRequest(service, constraint, parameters, isOptional, isUnique);

            return request;
        }

        public bool Release(object instance)
        {
            var release = scope.Release(instance);

            return release;
        }

        public System.Collections.Generic.IEnumerable<object> Resolve(Ninject.Activation.IRequest request)
        {
            var resolve = scope.Resolve(request);

            return resolve;
        }

        public void Dispose()
        {
            scope.Dispose();
        }
    }

Then, when I attempt to resolve a binding that was registered in the CreateKernel method as

kernel.Bind<IUnitOfWorkService>()
      .To<UnitOfWorkService>()
      .InNamedScope(SCOPENAME);

It fails to resolve with the error:

Error activating IUnitOfWorkService
The scope BOOTSTRAPPED is not known in the current context.
No matching scopes are available, and the type is declared InNamedScope(BOOTSTRAPPED).
Activation path:
  1) Request for IUnitOfWorkService

Suggestions:
  1) Ensure that you have defined the scope BOOTSTRAPPED.
  2) Ensure you have a parent resolution that defines the scope.
  3) If you are using factory methods or late resolution, check that the correct IResolutionRoot is being used.

Stacktrace:

at Ninject.Extensions.NamedScope.NamedScopeExtensionMethods.GetNamedScope(IContext context, String scopeParameterName)
at Ninject.Extensions.NamedScope.NamedScopeExtensionMethods.<>c__DisplayClass1`1.<InNamedScope>b__0(IContext context)
at Ninject.Planning.Bindings.BindingConfiguration.GetScope(IContext context)
at Ninject.Planning.Bindings.Binding.GetScope(IContext context)
at Ninject.Activation.Context.GetScope()
at Ninject.Activation.Context.Resolve()
at Ninject.KernelBase.<>c__DisplayClass15.<Resolve>b__f(IBinding binding)
at System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()
at System.Linq.Enumerable.<CastIterator>d__b1`1.MoveNext()
at System.Linq.Enumerable.Single[TSource](IEnumerable`1 source)
at Ninject.ResolutionExtensions.Get[T](IResolutionRoot root, IParameter[] parameters)
at SomeConsoleApp.Work.ScheduledWork.ScheduledWork.Quartz.IJob.Execute(IJobExecutionContext context) in ***

At this point, I don't know what else to try, any help would be appreciated.

EDIT

Sorry, thought I had how I was using it in here.. Here is a snippet of how it would be used:

using (TaskExecutionScope scope = Bootstrapper.Default.GetResolutionRoot())
{
    var unitOfWork = scope.Get<IUnitOfWorkService>();                   
    //do things with unit of work service
}

This is a bare bones example of how it is used. It fails on that call to Get<IUnitOfWorkService>. Now, while this WAS failing, I was able to get it to work by including the Context Preservation plugin, and changing up the TaskExecutionScope a little bit (only changed the Resolve(IRequest request) method from the above code:

    public System.Collections.Generic.IEnumerable<object> Resolve(Ninject.Activation.IRequest request)
    {
        var attempt = request.ParentContext.GetContextPreservingResolutionRoot().Resolve(request);

        return attempt;
    }

While this works, I hate having something in my code where I don't know WHY I have to have it in my code, and this is one of those examples. I would expect that since before I was calling Resolve directly on the NamedScope, that it should function, because it IS a resolution root - and I can't understand how a resolution root of a NamedScope doesn't even know its own name??? So, I would love to know either-

A) Why did I have to do this to get it to work?

B) This is the worst thing ever - you should do THIS instead!

C) This will work, but there is a small bug...

EDIT 2

So, I have attempted what @BatteryBackupUnit suggested, and it is failing with the same error as before... I added this line to the CreateKernel method:

kernel.Bind<TaskExecutionScope>().ToSelf().DefinesNamedScope(SCOPENAME);

And changed the GetResolutionRoot method to return Kernel.Get<TaskExecutionScope>(); . At this point I'm reverting back to the working code mentioned below.


Solution

  • Normally you would use NamedScope like

    Bind<FooTask>().ToSelf().DefinesNamedScope(SomeScopeName);
    

    Which would not require the creation of a NamedScope instance. However, it seems that you would like it to work for every task. You could switch to .InCallScope(), which would achieve the same in this scenario. Another alternative is to have the TaskExecutionScope created by ninject, and do:

    Bind<TaskExecutionScope>().ToSelf().DefinesNamedScope(SomeScopeName);
    

    since you're allways using the TaskExecutionScope to create a scoped object, right?

    NamedScope and ContextPreservation

    Without ContextPreservation, you can put parameters on the context of a request, but they are only kept until the object and it's dependencies are resolved. Injecting a Func<Foo>? It won't know about the parameters. ContextPreservation changes that and preserves the parametesr of the context, so that Func<Foo> will pass the parameter along to the request of Foo. Now NamedScope is very similar. It's basically a factory. The way it was implemented however, when creating the NamedScope, the NamedScope itself does not know the scope, but rather it's instanciated similarly to Bind<NamedScope>().ToSelf().DefinesNamedScope(SomeScopeName). To be honest i think NamedScope is named and implemented poorly. The scope itself is actually defined by a NamedScopeParameter on the context! If you do Bind<Foo>().ToSelf().DefinesNamedScope("Foo") there's actually no NamedScope object involved. So if there's no context preservation, any object that's created after creating the NamedScope,.. well the named scope definition (NamedScopeParameter) is actually gone. That's why it doesn't work without the ContextPreservation Extension.

    Quartz and TaskScopes

    The easiest way would be to adapt the quartz job factory (see how to inject quartz's job with ninject?). Instead of just doing IResolutionRoot.Get<TTask>(), you should do IResolutionRoot.Get<TTask>(new NamedScopeParameter(scopeName); (also see CreateNamedScope(string scopeName) method).

    Hint: In case your task is creating object by factory - and the objects need to know about scopes - you still will need the ContextPreservation extension.