Wednesday, Jul 13, 2016
Ninject ambient scope and deterministic dispose
When working with Ninject, one can specify standard lifetime designators like InSingletonScope and InTransientScope in the bindings section. There are two important situations when one might want to define a custom scope - to reuse instances within the lifetime of a scope (ambient instances) and to specify a deterministic dispose of resources when a scope lifetime ends.
The magic of InRequestScope
When working with Ninject in a web environment and using the Web extension, its special InRequestScope binding comes very handy. It defines a custom scope mechanism which achieves both aforementioned specific requirements - ambient instances and deterministic dispose. Something bound as InRequestScope
would be reused during the duration of the request and disposed when the request ends. One might wonder how to achieve this in a non web app. A simple code representation of the desired functionality would be something in the line of the following:
using (var scope = new SomeScope())
{
var ambientReusable = GetSomethingThatIsASingleInstanceDuringScopeLifeTime>();
var ambientDisposable = GetSomethingDisposable>();
} //ambientDisposable gets disposed here
I deliberately separated the two requirements as they should be separate and depending on one’s needs, something may be required to be ambient reusable or ambient disposable or both.
ActivationBlock
Ninject has a type called ActivationBlock
that can be used in a manner similar to the one described above:
using (var activationBlock = kernel.BeginBlock())
{
//activationBlock.Get<Something>();
}
This still exists in Ninject, but it’s deprecated due to the various problems it introduces. It was simply trying to do too much implicitly, so for example, anything bound as InSingletonScope
would behave as singleton but only within the lifetime of the ActivationBlock, i.e. it would transform singleton bindings temporarily to ambient bindings. Now, that’s just silly, I don’t want anyone to imply what my bindings should be on my behalf, I want that decision fully under my control. This makes the whole thing practically unusable.
NamedScope extension
Ninject provides several implementations of custom scopes in its NamedScope extension. The logic behind all of them is that scope is something that’s also been bound. It initiates the creation of the whole underlying object chain and propagates itself through the chain via injection context so that types bound to the custom scope would be aware of its existence and tied to it during instantiation. The InNamedScope
binding is more powerful than the InCallScope
and InParentScope
, since it also works with deferred creation via factories, but only if the ContextPreservation extension is installed. Here’s a simple xUnit test proving that it works:
public class Foo
{
private readonly BarFactory barFactory;
public Foo(BarFactory barFactory)
{
this.barFactory = barFactory;
}
public void AssertAmbientInstance()
{
var bar1 = barFactory.CreateBar();
var bar2 = barFactory.CreateBar();
Assert.Equal(bar1, bar2);
}
}
public class BarFactory
{
private readonly IResolutionRoot resolutionRoot;
public BarFactory(IResolutionRoot resolutionRoot)
{
this.resolutionRoot = resolutionRoot;
}
public Bar CreateBar()
{
return resolutionRoot.Get<Bar>();
}
}
public class Bar
{
}
[Fact]
public void Should_named_scope_injection_work()
{
IKernel kernel = new StandardKernel();
kernel.Bind<Foo>().ToSelf().DefinesNamedScope("FooScope");
kernel.Bind<BarFactory>().ToSelf();
kernel.Bind<Bar>().ToSelf().InNamedScope("FooScope");
var foo = kernel.Get<Foo>();
foo.AssertAmbientInstance();
}
However, let’s work with an additional example where Bar
is used in many different scenarios. Let’s say that we also need it in a Goo
class, which looks the same as Foo
and is also creating a Bar
in some method and doing something with it. The only way we could make it work is to use conditional binding from the Context Preservation
extension:
[Fact]
public void Should_named_scope_injection_work()
{
IKernel kernel = new StandardKernel();
kernel.Bind<Foo>().ToSelf().DefinesNamedScope("FooScope");
kernel.Bind<Goo>().ToSelf().DefinesNamedScope("GooScope");
kernel.Bind<BarFactory>().ToSelf();
kernel.Bind<Bar>().ToSelf().When(request => request.ParentRequest.ParentRequest.Service == typeof(Foo)).InNamedScope("FooScope");
kernel.Bind<Bar>().ToSelf().When(request => request.ParentRequest.ParentRequest.Service == typeof(Goo)).InNamedScope("GooScope");
var foo = kernel.Get<Foo>();
foo.AssertAmbientInstance();
var goo = kernel.Get<Goo>();
goo.AssertAmbientInstance();
}
Let’s say we later realize that we need to use both Foo
and Goo
in Moo
, and Bar
needs to be a single instance within the whole Moo
- what do we do then? Introduce another set of complex conditional bindings? Forget defining NamedScope
on Foo
and Goo
and move it all the way up to Moo
?
The answer of course depends on our needs at that time, but the point is that in some non web scenario the bounds of the ambient lifetime of Bar
may not be known in advance like it is with InRequestScope
. It may change during development time causing us to go back and forth changing the bindings and rethinking the strategy. Maybe it’s just me, but I would find it easier in a lot of scenarios to work with a custom ambient scope disentangled from the injection and binding changes all together. If we define Bar
binding like this:
kernel.Bind<Bar>().ToSelf().InAmbientScope()
it would be nice to be able to use it like this
using (var scope = new AmbientScope())
{
//`Foo` or `Goo` or `Moo` or whatever, no mather what `Bar` is a single instance within this using clause
}
Custom ambient scope and context propagation
In order for the whole thing to work in the InNamedScope
example above, BarFactory
(or in a real life situation, a really complex chain of objects) needs to be injected by the parent scoping type (Foo
or Goo
or Moo
) because that’s under Ninject’s control and that’s the only way it can pass the contextual parent scope information down to the injection chain. As soon as one moves away from the NamedScope
extension way of doing things, the biggest challenge in accomplishing Ninject to be aware of a custom scope is not wiring it in the bindings, but actually propagating its context in an async/await environment.
using (var scope = new AmbientScope())
{
var ambientInstance = kernel.Get<AmbientBoundType>();
Task.Run(() => {
//Different thread needs to be aware of the ambient scope
var ambientInstance = kernel.Get<AmbientBoundType>();
}).Wait();
}
The propagation of any kind of context through the async/await chain is a complex topic, Jon Skeet wrote a great post about it. My colleague Kristijan Horvat first encountered this a couple of years ago when trying to mimic the InRequestScope
in a separate process. He was inspired by another great article from Stephen Cleary - Implicit Async Context (“AsyncLocal”), where he used the CallContext.Logical...
methods and Immutable Collections
to make this async/await propagation of contextual information work in the .NET 4.5. This is his working version of the AmbientScope
:
/// <summary>
/// Ninject ambient scope used to create a scope directly in code.
/// </summary>
/// <remarks>Nested scopes are supported, but multi-threading in nested scopes is not supported.</remarks>
public class NinjectAmbientScope : IDisposable
{
#region Fields
private static ImmutableStack<string> scopeStack
{
get
{
return CallContext.LogicalGetData("scopes") as ImmutableStack<string>;
}
set
{
CallContext.LogicalSetData("scopes", value);
}
}
#endregion Fields
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="NinjectAmbientScope" /> class.
/// </summary>
public NinjectAmbientScope()
: this(Guid.NewGuid().ToString())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="NinjectAmbientScope" /> class.
/// </summary>
/// <param name="id">The scope identifier.</param>
public NinjectAmbientScope(string id)
{
Id = id;
if (scopeStack == null)
{
scopeStack = ImmutableStack.Create<string>(id);
}
else
{
scopeStack = scopeStack.Push(id);
}
}
#endregion Constructors
#region Properties
/// <summary>
/// Gets the current ambient scope.
/// </summary>
/// <value>The current ambient scope.</value>
public static object Current
{
get
{
if (scopeStack == null || scopeStack.IsEmpty)
{
return null;
}
return scopeStack.Peek();
}
}
/// <summary>
/// Gets or sets the scope identifier.
/// </summary>
/// <value>The scope identifier.</value>
public string Id { get; set; }
#endregion Properties
#region IDisposable Members
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public virtual void Dispose()
{
if (!scopeStack.IsEmpty)
{
scopeStack = scopeStack.Pop();
}
if (scopeStack.IsEmpty)
{
CallContext.FreeNamedDataSlot("scopes");
}
}
#endregion IDisposable Members
}
and the extension for the binding, where he needed it to use InRequestScope
in a web environment and yet having the whole unchanged code to also work with ambient scope in a non web environment:
/// <summary>
/// Defines extension method that specifies InAmbientOrRequestScope.
/// </summary>
public static class AmbientOrRequestScopeExtensionMethod
{
#region Methods
/// <summary>
/// Sets the scope to ambient or request scope.
/// </summary>
/// <typeparam name="T">The type of the service.</typeparam>
/// <param name="syntax">The syntax.</param>
/// <returns>The syntax to define more information.</returns>
public static IBindingNamedWithOrOnSyntax<T> InAmbientOrRequestScope<T>(this IBindingInSyntax<T> syntax)
{
return syntax.InScope(GetScope);
}
/// <summary>
/// Gets the scope.
/// </summary>
/// <param name="ctx">The context.</param>
/// <returns>The scope.</returns>
private static object GetScope(IContext ctx)
{
var scope = NinjectAmbientScope.Current;
if (scope != null)
{
return scope;
}
//Note: Copied from Ninject (https://github.com/ninject/Ninject.Web.Common/blob/master/src/Ninject.Web.Common/RequestScopeExtensionMethod.cs) subject to changes (out of sync with Ninject)
var nscope = ctx.Kernel.Components.GetAll<INinjectHttpApplicationPlugin>().Select(c => c.GetRequestScope(ctx)).FirstOrDefault(s => s != null);
return nscope;
}
#endregion Methods
}
Deterministic dispose
So far, I haven’t mentioned anything about deterministic dispose. My colleague didn’t need it in his implementation of the ambient scope above. One may wonder why it is even important, most of the times we could leave it to the GC … well … most of the times. We may argue about how frequent it happens, but every now and then a scenario emerges when one has disposable objects that hold heavy resources down the chain and wants to dispose them during the app’s runtime when knowing they are not needed anymore. In a DI/IOC world, the implementation of certain abstracted interface may change over time or depends on the environment, so it’s not a good practice to publicly define whether our abstraction is IDisposable
, we should leave the disposal to the DI framework. Since in Ninject a bound scope object defines the lifetime of an instance, it’s obvious that deterministic dispose is related to it.
Amazingly, googling Ninject deterministic dispose gives various confusing posts, so one may need to dig further to find a coherent answer. I found it nicely explained in this old article - Cache-and-Collect Lifecycle Management in Ninject 2.0. It seems that the only way to have a deterministic dispose of an instance bound to a custom scope is to have that scope implement the Ninject’s INotifyWhenDisposed
interface. It will immediately cause the disposal of any IDisposable
instance associated with the scope object when scope’s Dispose
method is invoked. The key thing is to raise the INotifyWhenDisposed.Disposed
event within the Dispose
method. Here’s the code for the universal disposable scope:
/// <summary>
/// Ninject disposable scope
/// </summary>
public class NinjectDisposableScope : IDisposable, INotifyWhenDisposed
{
#region Events
public event EventHandler Disposed;
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="NinjectDisposableScope" /> class.
/// </summary>
public NinjectDisposableScope()
{
}
#endregion
#region Properties
/// <summary>
/// Gets whether the scope is disposed.
/// </summary>
public bool IsDisposed
{
get;
private set;
}
#endregion
#region IDisposable Members
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public virtual void Dispose()
{
this.IsDisposed = true;
if (this.Disposed != null)
{
this.Disposed(this, EventArgs.Empty);
}
}
#endregion IDisposable Members
}
Regardless of whether one uses the NamedScope
extensions or a custom scope implementation, inheriting from this class or implementing INotifyWhenDisposed
is the only way to enable deterministic dispose of all the instances bound to the scope.
AmbientScope in .NET 4.6
The CallContext
and Immutable Collections
recipe used earlier in the NinjectAmbientScope
can be simplified a lot in .NET 4.6, since a new way of propagating context through async/await chain was introduced - AsyncLocal. Here’s the complete version that provides both ambient instances and deterministic dispose:
/// <summary>
/// Ninject ambient scope.
/// </summary>
/// <remarks>
/// Nested scopes are supported, but multi-threading in nested scopes is not supported.
/// </remarks>
public class NinjectAmbientScope : NinjectDisposableScope
{
#region Fields
private static readonly AsyncLocal<NinjectAmbientScope> scopeHolder = new AsyncLocal<NinjectAmbientScope>();
#endregion Fields
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="NinjectAmbientScope" /> class.
/// </summary>
public NinjectAmbientScope()
{
scopeHolder.Value = this;
}
#endregion Constructors
#region Properties
/// <summary>
/// Gets the current ambient scope.
/// </summary>
public static NinjectAmbientScope Current
{
get { return scopeHolder.Value; }
}
#endregion Properties
}
and the binding extension
/// <summary>
/// Defines ambient scope extension methods.
/// </summary>
public static class NinjectAmbientScopeExtensions
{
#region Methods
/// <summary>
/// Sets the scope to ambient scope.
/// </summary>
/// <typeparam name="T">The type of the service.</typeparam>
/// <param name="syntax">The syntax.</param>
/// <returns>The syntax to define more information.</returns>
public static IBindingNamedWithOrOnSyntax<T> InAmbientScope<T>(this IBindingInSyntax<T> syntax)
{
return syntax.InScope(GetAmbientScope);
}
private static object GetAmbientScope(IContext ctx)
{
var scope = NinjectAmbientScope.Current;
if (scope != null)
{
return scope;
}
throw new ApplicationException("No ambient scope defined");
}
#endregion Methods
}
Ambient disposable vs transient disposable
Finally, there is one important remaining issue and that is the separation between the 2 features that the ambient scope provides - reuse of ambient instance and deterministic dispose. What if only a deterministic dispose is desired, without the instance reuse? In other words, let’s consider an example where a factory creates a multitude of different (transient) instances within the ambient scope and all those instances need to be disposed when the ambient scope is disposed. Ninject doesn’t associate any scope with instances bound in InTransientScope
way, so we’d need to associate some disposable scope, but not our ambient scope, since we don’t want the instance reuse. The solution I came up with was to introduce another binding extension called InAmbientScopeAsTransient
and expand NinjectAmbientScope
to be able to create a new DisposableScope
for each requested transient instance, keep track of it and dispose all those scopes when the ambient scope itself is disposed. Here’s the complete code:
/// <summary>
/// Ninject ambient scope.
/// </summary>
/// <remarks>
/// Nested scopes are supported, but multi-threading in nested scopes is not supported.
/// </remarks>
public class NinjectAmbientScope : NinjectDisposableScope
{
#region Fields
private static readonly AsyncLocal<NinjectAmbientScope> scopeHolder = new AsyncLocal<NinjectAmbientScope>();
#endregion Fields
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="NinjectAmbientScope" /> class.
/// </summary>
public NinjectAmbientScope()
{
TransientScopes = new ConcurrentQueue<NinjectDisposableScope>();
scopeHolder.Value = this;
}
#endregion Constructors
#region Properties
/// <summary>
/// Gets the current ambient scope.
/// </summary>
public static NinjectAmbientScope Current
{
get { return scopeHolder.Value; }
}
/// <summary>
/// Gets the DisposableScope queue used for transient injection during the ambient scope lifetime.
/// </summary>
private ConcurrentQueue<NinjectDisposableScope> TransientScopes { get; set; }
#endregion Properties
#region Methods
public NinjectDisposableScope CreateTransientScope()
{
if (IsDisposed)
{
throw new ApplicationException("Ambient scope is disposed");
}
var scope = new NinjectDisposableScope();
TransientScopes.Enqueue(scope);
return scope;
}
#endregion
#region IDisposable Members
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public override void Dispose()
{
base.Dispose();
foreach (var transientScope in this.TransientScopes)
{
transientScope.Dispose();
}
}
#endregion IDisposable Members
}
and the binding extensions
/// <summary>
/// Defines ambient scope extension methods.
/// </summary>
public static class NinjectExtensions
{
#region Methods
/// <summary>
/// Sets the scope to ambient scope.
/// </summary>
/// <typeparam name="T">The type of the service.</typeparam>
/// <param name="syntax">The syntax.</param>
/// <returns>The syntax to define more information.</returns>
public static IBindingNamedWithOrOnSyntax<T> InAmbientScope<T>(this IBindingInSyntax<T> syntax)
{
return syntax.InScope(GetAmbientScope);
}
private static object GetAmbientScope(IContext ctx)
{
var scope = NinjectAmbientScope.Current;
if (scope != null)
{
return scope;
}
throw new ApplicationException("No ambient scope defined");
}
/// <summary>
/// Sets the scope to ambient scope and inject as transient within the current ambient scope.
/// </summary>
/// <typeparam name="T">The type of the service.</typeparam>
/// <param name="syntax">The syntax.</param>
/// <returns>The syntax to define more information.</returns>
public static IBindingNamedWithOrOnSyntax<T> InAmbientScopeAsTransient<T>(this IBindingInSyntax<T> syntax)
{
return syntax.InScope(GetAmbientScopeAsTransient);
}
private static object GetAmbientScopeAsTransient(IContext ctx)
{
var scope = NinjectAmbientScope.Current.CreateTransientScope();
if (scope != null)
{
return scope;
}
throw new ApplicationException("No ambient scope defined");
}
#endregion Methods
}
xUnit Tests
A couple of xUnit tests (with Shouldly assertions) which demonstrate the functionality of the AmbientScope from above.
The following test shows that when Foo
is bound as InAmbientScope
:
- Foo can’t be instantiated without the existence of ambient scope
- invoking foo.Bar() within
using
works - invoking foo.Bar() outside of
using
throwsObjectDisposedException
- instantiating another Foo within
using
and even in different thread returns the same instance asfoo
- two
Foo
instances instantiated in two different scopes are different
public class Foo : IDisposable
{
bool isDisposed;
public void Bar()
{
if (isDisposed)
{
throw new ObjectDisposedException(typeof(Foo).Name);
}
}
public void Dispose()
{
isDisposed = true;
}
}
[Fact]
public void Should_ambient_scope_injection_work_correctly()
{
IKernel kernel = new StandardKernel();
kernel.Bind<Foo>().ToSelf().InAmbientScope();
Should.Throw<Exception>(() =>
{
kernel.Get<Foo>();
});
ConcurrentDictionary<int, Foo> dict = new ConcurrentDictionary<int, Foo>();
Parallel.For(0, 2, (i) =>
{
Foo foo;
using (var scope = new NinjectAmbientScope())
{
foo = kernel.Get<Foo>();
dict.TryAdd(i, foo);
Should.NotThrow(() =>
{
foo.Bar();
});
Task.Run(() =>
{
foo.ShouldBeSameAs(kernel.Get<Foo>());
}).Wait();
}
Should.Throw<ObjectDisposedException>(() =>
{
foo.Bar();
});
});
dict[0].ShouldNotBeSameAs(dict[1]);
}
The following test shows everything the same as the test above, except that, when Foo
is bound as InAmbientScopeAsTransient
, instantiating another Foo
within using
will return a different instance than the previously created foo
. Disposing still works as expected.
[Fact]
public void Should_ambient_scope_as_transient_injection_work_correctly()
{
IKernel kernel = new StandardKernel();
kernel.Bind<Foo>().ToSelf().InAmbientScopeAsTransient();
Should.Throw<Exception>(() =>
{
kernel.Get<Foo>();
});
Parallel.For(0, 2, (i) =>
{
Foo foo;
using (var scope = new NinjectAmbientScope())
{
foo = kernel.Get<Foo>();
Should.NotThrow(() =>
{
foo.Bar();
});
Task.Run(() =>
{
foo.ShouldNotBeSameAs(kernel.Get<Foo>());
}).Wait();
}
Should.Throw<ObjectDisposedException>(() =>
{
foo.Bar();
});
});
}