About Konstruktor lifetime scopes

Konstruktor supports just only one single lifetime concept. To clarify the implications and why I think it covers most use cases, I’ll try to explain what a lifetime scope is:

Lifetime Scopes

In Konstruktor, all lifetime scopes together form a tree, a hierarchical structure with one single root.

When an instance needs to be resolved, either by explicitly calling the IScope.resolve() method or by a constructor argument of a dependent object, the scope uses the following resolution algorithm:

  • If an instance of the requested type is existing in the scope, it is immediately returned. This behavior is similar to Autofac’s InstancePerLifetimeScope()
  • If an instance of the requested type is not existing, the parent scope is asked for an instance.
  • If there is an instance in an ancestor scope, this instance is returned.
  • If there is no instance found, a new instance is created by the Builder and is stored in the scope that initially requested the instance.

Effectively, a newly created instance’s lifetime extends over the scope in which it was first resolved and all its nested branches.

In Konstruktor, there can be only one instance of the same type in one scope.

The implications are:

  1. As soon an instance is stored in a scope, no instances of this type can be created in the same or nested scope.
  2. The only possibility to create multiple instances of the same type is to resolve them in scopes of which their common ancestors contain no instance of that type.

So it seems to be hard to create transient instances. This is mainly because Konstruktor is focused towards a more service-oriented design, in which transient instance creation is exceptional.

The creation of the transient instances can be made explicit by injecting factories:

Injecting Factories

Every constructor that refers a Func<> or Func<,> typed argument, gets injected with a factory method that creates a new nested lifetime scope and stores a new instance of the result type in it. So even when an ancestor scope already contains an instance of the same type, a fresh instance is created, potentially hiding a parent one.

All other instances that are requested by dependencies are resolved by the standard rules outlined above.

Deterministic Disposal

Until the previous version of Konstruktor, is was not possible to dispose the scopes that were generated by factories.

Now, in the most recent version, scopes and instances that have been created by a factory can now be disposed explicitly. This is solved by requesting Func<Owned<T>> instead of Func<T>.

To access the instance of the return type T, Owned<T>.Value can be used. To dispose the scope and all dependent objects, call Owned<T>.Dispose().

There is a unit test that shows this concept in more detail.

This idea was inspired by Autofac’s Owned<T> mechanism.