-
Notifications
You must be signed in to change notification settings - Fork 1
Container
Telefrag's Container is a unique construct that is a hybrid between an IoC container and a entity-component system like you might find in a game engine such as Unity.
- Components are registered to a container by producers
- Components are resolved by consumers to obtain an instance
- Components registrations can be added or removed at any time
- Containers can be nested, and consumers dictate whether they want to resolve a component strictly from the container being queried, or recursively from parent containers as well
- Components can be named, and consumers can resolve components using their name (or not)
- Components can be searched in containers using LINQ-syntax, both by registration (type and name) as well as by instance
Resolving components works as you'd expect:
var widget = Container.Resolve<Widget>();
var another = Container.Resolve(typeof(Widget));You can also specify whether you'd only like to resolve components from the container being queried (as opposed to any container in the hierarchy):
var widget = Container.Resolve<Widget>(thisContainerOnly: true);
var another = Container.Resolve(typeof(Widget), null, true);If components are named, the name can be used to resolve the component as well:
var awoo = Container.Resolve<Command>("awoo"); // Get the Command named 'awoo'A registration is an object that tells a container how to fulfill the resolution of a given type or type-name combination. There are two main types of registration:
- Factory Registration - What is registered is a delegate that emits the object instance when a consumer calls for one.
- Instance Registration - What is registered is a set of parameters that instruct the Container where and when to construct instances of the registered type via construction,
Factory registrations are simple; when a caller attempts to resolve the given type (or type and name), the registered delegate is called and the return value returned to the caller. Register a factory using the RegisterFactory() methods.
Instance registrations can be broken down into two use cases:
-
Registering existing object instances creates a registration that returns an existing object instance upon resolution. Typically the lifetime for the instance is
Singleton(infinite), but it is possible to specify an alternate lifetime (after which the Container abandons the original instance and constructs a new one if queried). Register existing instances using theRegisterInstance()methods. -
Registering types sets up the Container to construct new instances as necessary. How and when instances are constructed is determined by two parameters:
-
Lifetime - The registration lifetime determines the effective lifetime of each instance tracked by the Container; when a Lifetime expires, the Container will abandon the old instance and construct a new one.
-
Scope - Determines whether instances are scoped to where the registration is (typically a parent Container), or to the container they are being resolved on.
-
Registering types can be done via a number of methods:
-
RegisterTransient- Creates a registration with a Transient lifetime (a new object is constructed upon each resolution) -
RegisterSingleton- Creates a registration with a Singleton lifetime (a new object is constructed only once). Allows an alternate Lifetime to be specified. -
RegisterScoped- Creates a registration with a Singleton lifetime that is scoped to the child Container that resolution is being done on. Allows an alternate Lifetime to be specified.
The following methods are used to create a container registration:
- RegisterFactory - Creates a registration that handles its own resolution via a delegate
- RegisterInstance - Registers an existing object instance
- RegisterTransient - Creates a registration that always emits a new object
- RegisterSingleton - Creates a registration that always emits the same object
- RegisterScoped - Creates a registration that always emits the same object per child container
-
Register - Manually registers a
ComponentRegistrationobject that you may have manually created.
Regardless of which method you use, the return value is always the newly registered ComponentRegistration object, which you can use to remove or track the registration.
// Register a component
var reg = Container.RegisterSingleton<TweetScraper>();
// and later on ...
// Unregister the component
Container.Unregister<TweetScraper>();
// or...
Container.FindRegistration<TweetScraper>()?.Remove();You can also construct a ComponentRegistration object yourself and add it to a container manually:
var cr = new ComponentRegistration()
{
LookupType = typeof(IPastry),
ImplementationType = typeof(Croissant),
Singleton = true,
ExistingInstance = MyCroissant
};
Bakery.Components.Register(cr);See the next section for more information on the properties of ComponentRegistration and how the Container handles them.
The following properties exist on the ComponentRegistration class:
| Name | Type | Description |
|---|---|---|
| LookupType | Type | The Type being provided by the consumer in a call to Resolve(); can be a class, struct, record class, record struct, or interface |
| ImplementationType | Type | The concrete Type that the Container will construct if an instance needs to be constructed; can be a class, struct, record class, or record struct |
| Name | string | The name (if any) to be provided by the consumer in a call to Resolve() to separate multiple component registrations of the same type. |
| ExistingInstance | object | Can be used to set the initial instance when a component is being registered and you want the existing instance to be used rather than a new one constructed. |
| Lifetime | ComponentLifetime | Determines if a new instance needs to be constructed for a given Resolve() request, or if the existing instance can be provided to the caller. If there is no existing instance, a new object will always be constructed. See the Lifetime section for more information. |
| TargetContainer | RegistrationTarget | Determines if the registration will be applied to the Container specified, or if the registration will be hoisted to the root Container in the tree. |
| Scoped | bool | If true, instances are scoped to the child Container they were Resolved on, if false, only one instance is tracked per registration. |
| Durable | bool | If true, an exception is thrown if an attempt is made to overwrite the registration with another one. |
| RemoveWithInstance | bool | If true, a call to RemoveInstance will remove the entire registration along with the instance. |
| Construction | ConstructionBehavior | Determines what the Container does when it needs an instance and doesn't have one; either Construct one, NeverConstruct (return null), or ThrowException. |
| Factory | Func<ResolutionContext, object> | When registering a factory, this is the factory method used to obtain component instances. |
The parameters that qualify a ComponentRegistration are immutable, the instances and lifetimes tracked by the ComponentRegistration are not. Registering a conflicting component will overwrite the previous registration (unless it is set as Durable, in which case, it throws an exception).
The ComponentLifetime type can be passed into a call to Register a component and dictates the validity of instances tracked by the Container. Each time the Container constructs an instance, it uses the ComponentLifetime to generate a ComponentInstanceLifetime object which is used to track the validity of that instance. There are several built-in lifetimes which can be accessed as static members on the ComponentLifetime class:
- ComponentLifetime.Singleton - This lifetime never expires; instances are valid indefinitely and will never be replaced.
- ComponentLifetime.Transient - This lifetime is always expired; instances are constructed each time a component is resolved.
- ComponentLifetime.Activity - This lifetime is scoped to the current Activity that is active on the Context. As soon as this Activity is finished (popped off the stack), the instance is expired.
You can easily create a custom lifetime two ways:
// Sample lifetime that expires when the user is no longer
// bob
var lifetime = ComponentLifetime.CreateDynamic(
(context, resContext) => context.User.DisplayName.Contains("bob")
);- Subclass the
ComponentLifetimetype and override theGetInstanceLifetime()function. - In this function, generate objects of type
ComponentInstanceLifetime, either by also subclassing that type, or via theComponentInstanceLifetime.FromDelegate()factory method.
-
Containers can be nested (they can have a parent Container)
-
Registrations exist on a specific node in the Container tree; depending on the value of
TargetContainerduring registration, the registration is either stored on the container thatRegister()was called on (default), or the registration will be done on the root-most container (if specified). -
Each Container has a name (required upon construction), and the
Pathproperty holds a canonical path for the Container in the tree of Containers based on these names -
When a component is
Resolve'ed on a container, if a registration is not found (andthisContainerOnlyis false), then the resolution attempt is invoked recursively on each parent in the tree. If the request gets to the root Container and no registration matches, then the resolution fails andnullis returned (unlessrequiredis true, in which case an exception is thrown). -
Even though the registration itself only lives in one place, it can track several instances (keyed by the container of scope). The container of scope is either the container being resolved or the container the registration lives on, depending on the value of
Scopedin theComponentRegistration.
Consider this example:
RootContainer
ChildA
ChildB
Let's say SomeComponent is registered at the RootContainer level as a singleton:
var reg = RootContainer.RegisterSingleton<SomeComponent>();These two method calls will return the same object instance:
var a = ChildA.Resolve<SomeComponent>();
var b = ChildB.Resolve<SomeComponent>();
Assert.IsTrue(a == b);If you were to look in the debugger, you would see that the value of reg.Instances.Count to be 1, and the ComponentInstance object tracking this instance is keyed off the root container:
var c = reg.Instances[RootContainer].Instance;
Assert.IsTrue(a == b == c); Now let's say that SomeComponent is instead registered at the RootContainer level as scoped:
var reg = RootContainer.RegisterScoped<SomeComponent>();Now executing the same resolution sequence will return two different instances:
var a = ChildA.Resolve<SomeComponent>();
var b = ChildB.Resolve<SomeComponent>();
Assert.IsTrue(a != b);but keep in mind this isn't the same as transient, the instances are persistent:
var a = ChildA.Resolve<SomeComponent>();
var aa = ChildA.Resolve<SomeComponent>();
var aaa = ChildA.Resolve<SomeComponent>();
Assert.IsTrue(a == aa === aaa != b);but if you look inside the ComponentRegistration, you'll see that they're keyed by the scoped child container. reg.Instances.Count will be 2 here.
var c = reg.Instances[ChildA].Instance;
var d = reg.Instances[ChildB].Instance;
Assert.IsTrue(a == c && b == d);