IoC Container & Auto Configuration

TimeLine IoC, Auto Configuration and IoC attributes documentation

This page explains how the TimeLine IoC container works, how to mark classes with IoC attributes, and how to work with automatically exported IoC configuration.

It is meant for developers and consultants who use the Framework and need to register, extend, or resolve IoC-based components.

What is IoC?

The abbreviation is "Inversion of Control", and basically means that objects will be resolved as the correct type automatically, you do not have to think about it. Only know what you need and ask the container for that type.


1. Core Concepts

1.1 Container & Auto Configuration

  • The global container is TimeLine.Framework.Util.IocContainer.Instance.

  • Auto configuration is handled by IocAutoConfiguration, accessible via:

    var autoConfig = IocContainer.Instance.AutoConfiguration;
  • At build time, a JSON config is generated per assembly. At runtime, IocAutoConfiguration.Load() reads these files and wires types to the container.

  • AutoConfiguration usually does not have to be called manually.

You usually don't interact with the JSON directly. You:

  • Mark types with IoC attributes →

  • IocConfigExporter picks them up →

  • IocAutoConfiguration wires them into TinyIoC.


2. IoC Attributes

All IoC attributes inherit from IocBaseAttribute. Apply them to classes or interfaces to control how they are registered and used.

2.1 Overview

Attribute
Target
Purpose

IocObject

class / interface

Mark type as IoC-managed object (base of most scenarios).

IocExplictImplementationFor

class

Explicit implementation for given interface/abstract base.

IocTag

class / interface

Categorization/tagging for lookup and grouping.

IocRunStatic

class / interface

Run static constructor at startup (without registering type as IoC object).

IocIndependentObject

class

Derived class that should not replace its base in the IoC inheritance chain.

IocDerivedObjectsOnly

class / interface

Base/template type: only concrete children become IoC objects, base itself isn't used.


2.2 IocObjectAttribute

Marks a type as an IoC object.

Key behavior

  • For non-abstract classes:

    • Auto-config will register the type (or its most derived replacement) in the container.

  • For abstract classes / interfaces:

    • Used as base / template; concrete derived types will be picked up.

  • The attribute will automatically be inherited, and does not have to be applied to derived types. (But can, for clarity)

Properties

  • bool RunStatic If true, the static constructor is forced at startup.

  • string Reason Optional explanation for docs/logging.

Example

[IocObject(Reason = "General-purpose helper that should be resolved via IoC.")]
public class SomeHelper
{
    public SomeHelper() { }

    public void DoStuff() { ... }
}

2.3 IocExplictImplementationForAttribute

Marks a concrete class as the explicit implementation for an interface or abstract base.

Key behavior

  • The container will resolve the interface/abstract type to this class.

  • Also implies IoC registration (similar to IocObject).

  • This can be applied to interfaces/types you do not have code access to, e.g. third-party types.

Example

public interface IMyService
{
    void Run();
}

[IocExplictImplementationFor(typeof(IMyService), Reason = "Default implementation used by the framework.")]
public class DefaultMyService : IMyService
{
    public void Run() { ... }
}

2.4 IocTagAttribute

Tags a type for later lookup.

Key behavior

  • Tags show up in auto config and can be queried via GetTagged.

  • Inherited by derived classes if applied on bases.

Example

[IocObject]
[IocTag("ApiController", Reason = "Used to find all API controllers.")]
public class TimeLineApiController
{
    ...
}

public class MyApiController : TimeLineApiController
{
    ...
}

Look up all tagged types:

var taggedControllers = IocContainer.Instance
    .AutoConfiguration
    .GetTagged("ApiController");

2.5 IocRunStaticAttribute

Triggers static construction without registering an IoC object.

Key behavior

  • Use it for static-only classes or global setup that runs during IoC auto-configuration.

  • This setup will run even though the type is not accessed/resolved anywhere in code up to that point.

  • It will automatically load the DLL if it hasn't been loaded yet.

  • Does not automatically register the type for Create<T>.

Example

[IocRunStatic(Reason = "Register global converters.")]
public static class GlobalConversionSetup
{
    static GlobalConversionSetup()
    {
        // This runs once when IoC auto-run executes.
    }
}

If you also want the type as an IoC object, use [IocObject(RunStatic = true) instead.


2.6 IocIndependentObjectAttribute

Marks a derived class as an independent IoC root.

Problem it solves

Normally, the auto config treats inheritance chains as "replacement chains". The most derived type replaces the base in IoC registration. That's great for customizations, but sometimes a derived type should be completely separate, not a replacement.

Behavior

  • Applied to a non-abstract class.

  • Auto config will:

    • Register the class as its own IoC object.

    • Not chain it as a replacement of its base.

    • No inheritance-based "replaced X with Y" warnings.

    • Derived children of this type will be registered as replacements to this type.

Example

[IocObject]
public abstract class ImportHandlerBase
{
    ...
}

[IocObject(Reason = "Default import handler.")]
public class DefaultImportHandler : ImportHandlerBase
{
    ...
}

// Independent, not a replacement:
[IocObject(Reason = "Diagnostics-only handler.")]
[IocIndependentObject(Reason = "Should not replace ImportHandlerBase chain.")]
public class DiagnosticsImportHandler : ImportHandlerBase
{
    ...
}

Result:

  • ImportHandlerBase → DefaultImportHandler replacement chain.

  • DiagnosticsImportHandler is its own IoC root, not part of that chain.


2.7 IocDerivedObjectsOnlyAttribute

Marks a base type where only the derived concrete types are IoC objects.

Problem it solves

Sometimes a base type is just a template, and every concrete subclass should be its own IoC object, not "the latest replacement for the base".

Behavior

  • Applied to a base class or interface.

  • Auto config will:

    • Not register the base itself in IoC.

    • Treat each direct concrete subclass as its own IoC root (no replacement chain starting from the base).

    • Customizations of those subclasses still use normal replacement logic.

Example

[IocObject]
[IocDerivedObjectsOnly(Reason = "Abstract base for export jobs; never resolved directly.")]
public abstract class ExportJobBase
{
    ...
}

[IocObject]
public class CustomerExportJob : ExportJobBase
{
    ...
}

[IocObject]
public class InvoiceExportJob : ExportJobBase
{
    ...
}

Result:

  • ExportJobBase → not registered in IoC.

  • CustomerExportJob → own IoC root.

  • InvoiceExportJob → own IoC root.

A further customization:

[IocObject(Reason = "Project-specific customization of customer export.")]
public class ProjectXCustomerExportJob : CustomerExportJob
{
    ...
}

Now CustomerExportJob has its own replacement chain:

  • CustomerExportJob → ProjectXCustomerExportJob, without affecting InvoiceExportJob.


3. Creating Instances via IocContainer

3.1 Basic creation

Use IocContainer.Instance.Create<T> to create IoC-managed objects.

var helper = IocContainer.Instance.Create<SomeHelper>();

This will:

  • Ensure the type is registered via auto configuration (if it was exported as such).

  • Resolve it from the underlying TinyIoCContainer.

There are also TryCreate variants:

if (IocContainer.Instance.TryCreate(out SomeHelper helper))
{
    // use helper
}

3.2 Singleton pattern with IoC

Typical pattern when you want a singleton that's IoC-aware:

[IocObject]
public class SomeService
{
    private static SomeService _instance;
    public static SomeService Instance =>
        _instance ?? (_instance = IocContainer.Instance.Create<SomeService>());

    protected SomeService() { }

    // members...
}
  • Mark the type with [IocObject].

  • Use a static Instance property that resolves via IocContainer.

  • Keep the constructor protected (or internal) to discourage manual new.

3.2.1 Base class 'Singleton'

There is a shared base class that can be used for the singleton pattern in TimeLine.Client.Framework, called TimeLine.Client.Framework.Singleton<T> that can be used and implements the singleton pattern and [IocObject] tag automatically.


4. IoC Objects with Constructor Arguments

Sometimes you need constructor arguments that can't be auto-resolved.

The container exposes overloads that take named parameters:

// params NamedParameter[]
var obj = IocContainer.Instance.Create<MyType>(
    new NamedParameter("customerId", 123),
    new NamedParameter("mode", ExportMode.Full)
);

// params KeyValuePair<string, object>[]
var obj2 = IocContainer.Instance.Create<MyType>(
    new KeyValuePair<string, object>("customerId", 123),
    new KeyValuePair<string, object>("mode", ExportMode.Full)
);

// params (string name, object value)[]
var obj3 = IocContainer.Instance.Create<MyType>(
    ("customerId", 123),
    ("mode", ExportMode.Full)
);

All of these end up as NamedParameters under the hood and are forwarded to TinyIoC. Usage of the first or third variant is recommended, depending on preference.

Arguments will be passed in right into the constructor, no init step is required. This allows even get-only properties to be set correctly in an IoC object.


5. Working with Auto Configuration

Sometimes you need to inspect or work directly with the IoC metadata.

5.1 Accessing the auto configuration

var autoConfig = IocContainer.Instance.AutoConfiguration;

The key parts:

  • Dictionary<string, Entry> Types All known IoC entries, keyed by fully qualified type name.

  • Dictionary<string, HashSet<Entry>> Tags All tagged entries.

  • Entry this[Type type] Indexer by System.Type.

  • Entry this[string typeName] Indexer by fully qualified name.

5.2 Getting all entries for a tag

var apiControllers = autoConfig.GetTagged("ApiController");

// Example: create all registered controllers
foreach (var entry in apiControllers)
{
    if (!entry.IsRegistered)
        continue;

    var instance = entry.CreateInstance(); // calls AssureRegistration + resolve
    // instance is object; cast as needed
}

GetTagged:

  • Cleans up inheritance chains (returns the most derived applicable entries).

  • Can filter by onlyRegistered if needed.

5.3 Inspecting inheritance / derived types

Each Entry exposes:

  • Entry Base The base entry (config-level, not necessarily the CLR BaseType).

  • Entry Derived The configured replacement / derived entry.

  • Entry MostDerived The last replacement in the chain.

  • bool? Register Whether this entry is configured to be registered.

  • bool IsRegistered Whether the container registration actually happened.

  • Type LoadType() Loads the actual CLR type for the entry.

  • object CreateInstance() Convenience method to create an instance of the most derived implementation.

Example: find the effective implementation for a base type:

var baseEntry = autoConfig[typeof(ExportJobBase)];
var effectiveEntry = baseEntry?.MostDerived;

if (effectiveEntry?.IsRegistered == true)
{
    var jobInstance = effectiveEntry.CreateInstance(); // object
}

Special attributes:

  • IocIndependentObject types will not appear as Derived of their base.

  • IocDerivedObjectsOnly bases will have Register != true, and their direct children will not have Extends pointing to that base.


6. Guidelines & Best Practices

  • Use [IocObject] for:

    • Services, helpers, converters, behaviors, import/export handlers, etc.

    • Any class that is intended to be customizable in customizations.

    • Anything that should be resolved through the framework container instead of new.

  • Use [IocExplictImplementationFor] when:

    • There is exactly one default implementation for an interface/abstract base.

    • You do not have code access to the base type.

  • Use [IocTag] to:

    • Group related components (controllers, jobs, strategies) and load them dynamically.

  • Use [IocRunStatic] or IocObject.RunStatic = true for:

    • Types that need static setup during startup.

  • Use [IocIndependentObject] when:

    • A derived class must not override/replace the base in IoC.

  • Use [IocDerivedObjectsOnly] when:

    • You have a base template and want each concrete child to be its own independent IoC object.

  • When in doubt:

    • Resolve types via IocContainer.Instance.Create<T>() or TryCreate. The container will even resolve if the type was not registered as an IoC object.

    • Avoid new on IoC types at all costs, only create them via the IocContainer.

This is enough for using IoC right in day-to-day work. For anything more exotic, inspect IocAutoConfiguration and the generated JSON, but for most use cases the attributes and the container APIs are all that is needed.

Last updated