Welcome to ManualDi – the simple, fast and extensible C# dependency injection library.
- Source generation, no reflection - Faster and more memory efficient than most other dependency injection containers.
- Compatible with almost all client and server platforms, including IL2CPP and WASM*
- Supercharge the container with your own custom extensions
- Seamless Unity3D game engine integration
* .Net Compact Framework is not compatible because of an optimization
Benchmark against Microsoft's container
| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated |
|------------ |----------:|----------:|----------:|-------:|-------:|----------:|
| ManualDi | 4.890 us | 0.0969 us | 0.0952 us | 0.2747 | 0.0076 | 13.73 KB |
| MicrosoftDi | 40.287 us | 0.4577 us | 0.4282 us | 2.5024 | 0.6714 | 122.87 KB |
Benchmark against Unity3d compatible containers.
- Plain C#: Install it using Nuget
- Unity3d 2022.3.29 or later
- Install it using Unity Package Manager
- Git URL: https://github.com/PereViader/ManualDi.Unity3d.git
- Installation Phase: Container configuration is defined.
- Building Phase: Container configuration is ingested. Non-lazy Bindings are resolved.
- Startup Phase: Configured startup callbacks are run.
- Resolution Phase: Container is provided and explicit resolutions can be done.
- Disposal Phase: The container and its resources are released.
The container is created using a synchronous fluent Builder
IDiContainer container = new DiContainerBindings() // Create the builder
.InstallSomeFunctionality() // Configure with an extension method implemented in your project
.Install(new SomeOtherInstaller()) // Configure with an instance of `IInstaller` implemented your project
.Build(); // Build the container
SomeService service = container.Resolve<SomeService>();
The configuration of the container is done through Binding extension methods available on DiContainerBindings
and can only be set during the installation phase.
Any alteration by custom means after the container's creation may result in undefined behaviour.
Calling the Bind method provides a fluent interface through TypeBinding<TApparent, TConcrete>
.
- Apparent: It's the type that can be used when resolving the container.
- Concrete: It's type of the actual instance behind the scenes.
By convention, method calls should be done in the following order.
Bind<(TApparent,)? TConcrete>() // TApparent is optional and will be equal to TConcrete if undefined
.Default // Source generated
.[Single*|Transient]
.From[Constructor|Instance|Method|ContainerResolve|SubContainerResolve|...] //Constructor is source generated
.Inject //Empty overload is source generated
.Initialize //Empty overload is source generated
.Dispose
.WithId
.When([InjectedIntoId|InjectedIntoType])
.[Lazy*|NonLazy]
.[Any other custom extension method your project implements]
//* means default, no need to call it
Sample extension method installer:
class A { }
class B { }
interface IC { }
class C : IC { }
static class Installer
{
public static DiContainerBindings InstallSomeFunctionality(this DiContainerBindings b)
{
b.Bind<A>().Default().FromInstance(new A());
b.Bind<B>().Default().FromConstructor();
b.Bind<IC, C>().Default().FromConstructor();
return b;
}
}
The scope of a binding defines the rules for instance creation.
- Single: The container creates and caches one instance of the type; subsequent calls reuse this instance.
- Transient: The container creates an instance every time the type is resolved.
If no scope is specified, Single
is used
These methods define the instance creation strategy
This method is source generated ONLY if there is a single public/internal accessible constructor.
The instance is created using the constructor of the concrete type. The necessary dependencies of the constructor are resolved using the container.
b.Bind<T>().FromConstructor()
No new instances will be created, the instance provided during the binding stage is used directly.
Note: If used with Transient
scope, the container will still return the same instance.
b.Bind<T>().FromInstance(new T())
The instance is created using the delegate provided. The container is provided as a parameter thus allowing it to Resolve required dependencies.
b.Bind<T>().FromMethod(c => new T(c.Resolve<SomeService>()))
Used for apparent type remapping. Commonly used to bind multiple relevant interfaces of some type to the container.
inteface IFirst { }
interface ISecond { }
public class SomeClass : IFirst, ISecond { }
b.Bind<SomeClass>().FromConstructor();
b.Bind<IFirst, SomeClass>().FromContainerResolve();
b.Bind<ISecond, SomeClass>().FromContainerResolve();
The instance is created using a sub container built using the installer parameter. Useful for encapsulating parts of object graph definition into sub-containers.
class SomeService(Dependency dependency) { }
class Dependency { }
b.Bind<SomeService>().FromSubContainerResolve(sub => {
sub.Bind<SomeService>().Default().FromConstructor();
sub.Bind<Dependency>().Default().FromConstructor();
})
The Inject method allows for post-construction injection of types. The injection will happen immediately after the object creation. The injection will be done in reverse resolution order. In other words, injected objects will already be injected themselves. The injection will not happen more than once for any instance. The injection can also be used to run other custom user code during the object creation lifecycle. Any amount of injection callbacks can be registered
b.Bind<object>()
.FromInstance(new object())
.Inject((o, c) => Console.WriteLine("1"))
.Inject((o, c) => Console.WriteLine("2"));
An empty overload of the Inject method will be generated if:
- The type has a single public/internal accessible Inject method. The method may have 0 or more dependencies.
- The type has any amount of public/internal accessible properties that use the Inject attribute
The generated method will first do property injection on the properties and then call the inject method and properties.
public class A
{
public void Inject(B b, C c) { }
}
public class B
{
[Inject] public object Object { get; set; }
}
public class C
{
[Inject] public int Value { get; set; }
}
b.Bind<object>().FromInstance(new object());
b.Bind<int>().FromInstance(3);
b.Bind<A>().FromConstructor().Inject();
b.Bind<B>().FromConstructor().Inject();
b.Bind<C>().FromConstructor().Inject();
In the unity engine, for example, types that derive from UnityEngine.Object
cannot make use of the constructor.
For this reason, derived types will usually resort to this kind of injection
public class SomethingGameRelated : MonoBehaviour // this class derives from UnityEngine.Object
{
[Inject] public SomeService SomeService { get; set; }
private OtherService otherService;
public void Inject(OtherService otherService)
{
this.otherService = otherService;
}
}
Warning: Cyclic dependencies usually highlight a problem in the design of the code. If you find such a problem in your codebase, consider redesigning the code before applying the following proposal.
This will throw a stack trace exception when any of the types involved in the cyclic chain is resolved.
b.Bind<A>().FromMethod(c => new A(c.Resolve<B>()));
b.Bind<B>().FromMethod(c => new B(c.Resolve<A>()));
This will work. As long as a single object in the cyclic chain breaks the chain, the resolution will be able to complete successfully.
b.Bind<A>().FromMethod(c => new A())).Inject((o,c) => o.B = c.Resolve<B>());
b.Bind<B>().FromMethod(c => new B(c.Resolve<A>()));
The Initialize method allows for post-injection initialization and injection of types. The initialization will NOT happen immediately after the object injection. It will be queued and run later. The initialization will be done in reverse resolution order. In other words, injected objects will already be initialized themselves. The initialization will not happen more than once for any instance. The initialization can also be used to hook into the object creation lifecycle and run other custom user code. Any amount of initialization callback can be registered
b.Bind<A>()
.FromInstance(new A())
.Initialize((o, c) => o.Init())
.Initialize((o, c) => Console.WriteLine("After init"));
c.Resolve<A>()
An empty overload of the Initialize method will be generated if:
- The type has a single public/internal accessible Initialize method. The method may have 0 or more dependencies.
public class A
{
public void Initialize() { }
}
public class B
{
public void Initialize(A a) { }
}
b.Bind<A>().FromConstructor().Initialize();
b.Bind<B>().FromConstructor().Initialize();
Objects may implement the IDisposable interface or require custom teardown logic. The Dispose extension method allows defining behavior that will run when the object is disposed. The container will dispose of the objects when itself is disposed. The objects will be disposed in reverse resolution order.
If an object implements the IDisposable interface, it doesn't need to call Dispose, it will be Disposed automatically.
class A : IDisposable
{
public void Dispose() { }
}
class B
{
public B(A a) { }
public void DoCleanup() { }
}
b.Bind<A>().FromConstructor(); // No need to call Dispose because the object is IDisposable
b.Bind<B>().FromConstructor().Dispose((o,c) => o.DoCleanup());
// ...
B b = c.Resolve<B>();
c.Dispose(); // A is the first object disposed, then B
If this extension method is called, the method will not call the IDisposable.Dispose
method.
Any delegate registered to the Dispose method will still be called.
These extension methods allow defining an id, enabling the filtering of elements during resolution.
b.Bind<int>().FromInstance(1).WithId("Potato");
b.Bind<int>().FromInstance(5).WithId("Banana");
// ...
c.Resolve<int>(x => x.Id("Potato")); // returns 1
c.Resolve<int>(x => x.Id("Banana")); // returns 5
Note: This feature can be nice to use, but I usually prefer using it sparingly. This is because it introduces the need for the provider and consumer to share two points of information (Type and Id) instead of just one (Type)
An alternative to it is to register delegates instead. Delegates used this way encode the two concepts into one.
delegate int GetPotatoInt();
b.Bind<GetPotatoInt>().FromInstance(() => 1);
int value = c.Resolve<GetPotatoInt>()();
The id functionality can be used on method and property dependencies by using the Inject attribute and providing a string id to it
class A
{
[Inject("Potato")] public B B { get; set; }
public void Inject(int a, [Inject("Other")] object b) { ... }
}
//Will resolve the property doing
c.Resolve<B>(x => x.Id("Potato"));
//and call the inject method doing
o.Inject(
c.Resolve<int>(),
c.Resolve<object>(x => x.Id("Other"))
)
The When
extension method allows defining filtering conditions as part of the bindings.
Allows filtering bindings using the injected Concrete type
class SomeValue(int Value) { }
class OtherValue(int Value) { }
class FailValue(int Value) { }
b.Bind<int>().FromInstance(1).When(x => x.InjectedIntoType<SomeValue>())
b.Bind<int>().FromInstance(2).When(x => x.InjectedIntoType<OtherValue>())
b.Bind<SomeValue>().Default().FromConstructor(); // will be provided 1
b.Bind<OtherValue>().Default().FromConstructor(); // will be provided 2
b.Bind<FailValue>().Default().FromConstructor(); // will fail at runtime when resolved
Allows filtering bindings using the injected Concrete type
class SomeValue(int Value) { }
b.Bind<int>().FromInstance(1).When(x => x.InjectedIntoId("1"));
b.Bind<int>().FromInstance(2).When(x => x.InjectedIntoId("2"));
b.Bind<SomeValue>().Default().FromConstructor().WithId("1"); // will be provided 1
b.Bind<SomeValue>().Default().FromConstructor().WithId("2"); // will be provided 2
b.Bind<FailValue>().Default().FromConstructor(); // will fail at runtime when resolved
The FromMethod delegate will not be called until the object is actually resolved. By default, bindings are Lazy so calling this method is usually not necessary.
The object will be built simultaneously with the container.
This source generated method is a shorthand for calling Inject and Initialize when they are available without needing to manually update the container bindings.
This means that
public class A { }
public class B {
void Inject(A a) { }
}
public class C {
void Initialize(A a) { }
}
public class D {
void Inject(A a) { }
void Initialize(A a) { }
}
b.Bind<A>().Default().FromConstructor(); // Default does not call anything
b.Bind<B>().Default().FromConstructor(); // Default calls Inject
b.Bind<C>().Default().FromConstructor(); // Default calls Initialize
b.Bind<D>().Default().FromConstructor(); // Default calls Inject and Initialize
Using default is not mandatory, but it helps development by taking the responsibility of updating the bindings away from the developer any time a new Inject / Initialize method is introduced.
Thus, it is recommended to always add it even if the type does not currently have any of the methods.
The source generator will take into account the nullability of the dependencies. If a dependency is nullable, the resolution will not fail if it is missing. If a dependency is not nullable, the resolution will fail if it is missing.
public class A
{
//object MUST be registered on the container
//int may or may not be registered on the container
public A(object obj, int? nullableValue) { }
}
b.Bind<A>().Default().FromConstructor();
The source generator will inject all bound instances of a type if the dependency declared is one of the following types List<T>
IList<T>
IReadOnlyList<T>
IEnumerable<T>
The resolved dependencies will be resolved using ResolveAll<T>
If the whole collection is nullable, the provided collection will always have Count > 0
otherwise null.
If the underlying T
is nullable, the resulting list will be converted from T
to T?
. This is not recommended, the nullable values will never be null.
The differences between TCollection<T?>
and TCollection<T>?
:
TCollection<T?>
does a single resolution but allocates a list every timeTCollection<T>?
does a dry resolution at build time and thus may not allocate the list
public class A
{
[Inject] List<object> ListObj {get; set;}
[Inject] IList<int> IListInt {get; set;}
[Inject] IReadOnlyList<obj> IReadOnlyListObj {get; set;}
[Inject] IEnumerable<int> IEnumerableInt {get; set;}
[Inject] List<object>? NullableList {get; set;} //Either null or Count > 0
[Inject] List<object?> NullableGenericList {get; set;} //Valid but NOT recommended
}
b.Bind<A>().Default().FromConstructor();
The source generator will lazily inject dependencies if the dependency is lazy itself. Lazy dependencies may have nullable contents. Lazy dependencies are also compatible with collection . Lazy Collection dependencies are also supported Nullable lazy will only provide a lazy if resolving the lazy would provide a value Nullable lazy with nullable generic value is not recommended. The nullable values will never be null
The differences between Lazy<T?>
and Lazy<T>?
:
Lazy<T?>
does a single resolution but allocates a lazy every timeLazy<T>?
does a dry resolution at build time and thus may not allocate the lazy
public class A
{
[Inject] Lazy<object> Obj {get; set;}
[Inject] Lazy<object?> NullableObj {get; set;}
[Inject] Lazy<int> Value {get; set;}
[Inject] Lazy<int?> NullableValue {get; set;}
[Inject] Lazy<List<object>> LazyObjectList {get; set;}
[Inject] Lazy<object>? NullableObj {get; set;} //Null lazy when container does not have binding
[Inject] Lazy<object?>? NullableObj {get; set;} //Possible but not recommended
}
b.Bind<A>().Default().FromConstructor();
Notice that resolution can only be done on apparent types, not concrete types. Concrete types are there so the container can provide a type safe fluent API.
If you use the source generated methods, you will usually not interact with the Resolution methods.
Resolutions can be done in the following ways:
Resolve an instance from the container. An exception is thrown if it can't be resolved.
SomeService service = container.Resolve<SomeService>();
Resolve a reference type instance from the container. Returns null if it can't be resolved.
SomeService? service = container.ResolveNullable<SomeService>();
Resolve a value type instance from the container. Returns null if it can't be resolved.
int? service = container.ResolveNullableValue<int>();
Resolve an instance from the container. Returns true if found and false if not.
bool found = container.TryResolve<SomeService>(out SomeService someService);
Resolve all the registered instance from the container. If no instances are available the list is empty.
List<SomeService> services = container.ResolveAll<SomeService>();
The container also provides an extension method so it can queue some delegate to be run during the Startups lifecycle of the container. This step is useful to make sure that some object's method is run after all the NonLazy objects have been created and initialized.
class Startup
{
public Startup(...) { ... }
public void Start() { ... }
}
class SomeNonLazy
{
public void Initialize() { ... }
}
b.Bind<SomeNonLazy>().Default().FromConstructor().NonLazy()
b.Bind<Startup>().Default().FromConstructor();
b.WithStartup<Startup>(o => o.Start());
In the snippet above, the following will happen once the container is built:
SomeNonLazy
is createdSomeNonLazy
'sInitialize
method is calledStartup
is createdStartup
'sStart
method is called
When using the container in unity, do not rely on Awake / Start. Instead, rely on Inject / Initialize. You can still use Awake / Start if the classes involved are not injected through the container.
The container provides two specialized Installers
MonoBehaviourInstaller
ScriptableObjectInstaller
This is the idiomatic Unity way to have both the configuration and engine object references in the same place.
These classes just implement the IInstaller
interface, there is no requirement for these classes to be used, so feel free to use IInstaller directly if you want.
public class SomeFeatureInstaller : MonoBehaviourInstaller
{
public Image Image;
public Toggle Toggle;
public Transform Transform;
public override void Install(DiContainerBindings b)
{
b.Bind<Image>().FromInstance(Image);
b.Bind<Toggle>().FromInstance(Toggle);
b.Bind<Transform>().FromInstance(Transform);
}
}
When using the container in the Unity3d game engine the library provides specialized extensions for object construction
FromGameObjectGetComponentInParent
: Retrieves a component from the parent of a GameObject.FromGameObjectGetComponentsInParent
: Retrieves all components of a specific type from the parent of a GameObject.FromGameObjectGetComponentInChildren
: Retrieves a component from the children of a GameObject.FromGameObjectGetComponentsInChildren
: Retrieves all components of a specific type from the children of a GameObject.FromGameObjectGetComponent
: Retrieves a component from the current GameObject.FromGameObjectAddComponent
: Adds a component to the current GameObject.FromGameObjectGetComponents
: Retrieves all components of a specific type from the current GameObject.FromInstantiateComponent
: Instantiates a component and optionally sets a parent.FromInstantiateGameObjectGetComponent
: Instantiates a GameObject and retrieves a specific component from it.FromInstantiateGameObjectGetComponentInChildren
: Instantiates a GameObject and retrieves a component from its children.FromInstantiateGameObjectGetComponents
: Instantiates a GameObject and retrieves all components of a specific type.FromInstantiateGameObjectGetComponentsInChildren
: Instantiates a GameObject and retrieves all components from its children.FromInstantiateGameObjectAddComponent
: Instantiates a GameObject and adds a component to it.FromObjectResource
: Loads an object from a Unity resource file by its path.FromInstantiateGameObjectResourceGetComponent
: Instantiates a GameObject from a resource file and retrieves a component from it.FromInstantiateGameObjectResourceGetComponentInChildren
: Instantiates a GameObject from a resource and retrieves a component from its children.FromInstantiateGameObjectResourceGetComponents
: Instantiates a GameObject from a resource and retrieves all components of a specific type.FromInstantiateGameObjectResourceGetComponentsInChildren
: Instantiates a GameObject from a resource and retrieves all components from its children.FromInstantiateGameObjectResourceAddComponent
: Instantiates a GameObject from a resource and adds a component to it.
Use them like this
public class SomeFeatureInstaller : MonoBehaviourInstaller
{
public Transform canvasTransform;
public string ResourcePath;
public Toggle TogglePrefab;
public GameObject SomeGameObject;
public override Install(DiContainerBindings b)
{
b.Bind<Toggle>().FromInstantiateComponent(TogglePrefab, canvasTransform);
b.Bind<Image>().FromInstantiateGameObjectResourceGetComponent(ResourcePath);
b.Bind<SomeFeature>().FromGameObjectGetComponent(SomeGameObject);
}
}
Most methods have several optional parameters.
Also, special attention to Transform? parent = null
. This one will define the parent transform used when instantiating new instances.
Special attention to bool destroyOnDispose = true
one. This one will be available on creation strategies that create new instances.
If the parameter is left as true
, when the container is disposed, it will first destroy the instance.
This is the necessary default behaviour due to the game likely needing those resources cleaned up for example from shared Additive scenes and wanting the default behaviour to be the safest.
If the scene the resource is created on will then be deleted, there is no need to destroy it during the disposal of the container, so feel free to set the parameter as false
.
An entry point is a place where some context of your application is meant to start. In the case of ManualDi, it is where the object graph is configured and then the container is started.
The last binding of an entry point will usually make use of WithStartable, to run any logic necessary after the container is created.
Root entry points will not depend on any other container. This means that all dependencies will be registered in the main container itself.
Use the appropriate type depending on how you want to structure your application:
MonoBehaviourRootEntryPoint
ScriptableObjectRootEntryPoint
public class Startup
{
public Startup(Dependency1 d1, Dependency2 d2) { ... }
public void Start() { ... }
}
class InitialSceneEntryPoint : MonoBehaviourRootEntryPoint
{
public Dependency1 dependency1;
public Dependency2 dependency2;
public override void Install(DiContainerBindings b)
{
b.Bind<Dependency1>().Default().FromInstance(dependency1);
b.Bind<Dependency2>().Default().FromInstance(dependency2);
b.Bind<Startup>().Default().FromConstructor();
b.WithStartup<Startup>(o => o.Start());
}
}
Subordinate entry points will depend on other container or require other data. This means that these entry points cannot be started by themselves. They need to be started by some other part of your application.
If the data provided to these entry points, implements IInstaller
, then the data will also be installed to the container.
Otherwise, it will just be available through the Data
property of the EntryPoint.
If the subordinate container requires all the dependencies of the parent container, it is recommended to set the parent container on the EntryPointData object.
public class EntryPointData : IInstaller
{
public IDiContainer ParentDiContainer { get; set; }
public void Install(DiContainerBindings b)
{
b.WithParentContainer(ParentDiContainer);
}
}
These entry points may also optionally return a TContext
object resolved from the container.
Doing such a thing is useful to provide a facade to the systems created.
Use the appropriate type depending on how you want to structure your application:
MonoBehaviourSubordinateEntryPoint<TData>
MonoBehaviourSubordinateEntryPoint<TData, TContext>
ScriptableObjectSubordinateEntryPoint<TData>
ScriptableObjectSubordinateEntryPoint<TData, TContext>
public class Startup
{
public Startup(Dependency1? d1, Dependency2 d2) { ... }
public void Start() { ... }
}
public class EntryPointData : IInstaller
{
public Dependency1? Dependency1 { get; set; }
public void Install(DiContainerBindings b)
{
if(Dependency1 is not null)
{
b.Bind<Dependency1>().FromInstance(Dependency1).DontDispose();
}
}
}
public class Facade : MonoBehaviour
{
[Inject] public Dependency1? Dependency1 { get; set; }
[Inject] public Dependency2 Dependency2 { get; set; }
public void DoSomething1()
{
Dependency1?.DoSomething1();
}
public void DoSomething2()
{
Dependency2.DoSomething2();
}
}
class InitialSceneEntryPoint : MonoBehaviourSubordinateEntryPoint<EntryPointData, Facade>
{
public Dependency2 dependency2;
public Facade Facade;
public override void Install(DiContainerBindings b)
{
b.Bind<Dependency2>().Default().FromInstance(dependency2);
b.Bind<Facade>().Default().FromInstance(Facade);
b.Bind<Startup>().Default().FromConstructor();
b.WithStartup<Startup>(o => o.Start());
}
}
And this is how a subordinate entry point on a scene could be started
public class Data
{
public string Name { get; set; }
}
public class SceneFacade
{
[Inject] Data Data { get; set; }
public void DoSomething()
{
Console.WriteLine(Data.Name);
}
}
public class SceneEntryPoint : MonoBehaviourSubordinateEntryPoint<Data, SceneFacade>
{
public override void Install(DiContainerBindings b)
{
b.Bind<Data>().Default().FromInstance(Data);
b.Bind<SceneFacade>().Default().FromConstructor();
}
}
class Example
{
IEnumerator Run()
{
yield return SceneManager.LoadSceneAsync("TheScene", LoadSceneMode.Additive);
var entryPoint = Object.FindObjectOfType<SceneEntryPoint>();
var data = new Data() { Name = "Charles" };
var facade = entryPoint.Initiate(data)
facade.DoSomething();
}
}
and this is an example of how you could use a subordinate prefab
class Example : MonoBehaviour
{
public SceneEntryPoint EntryPoint;
void Start()
{
var data = new Data() { Name = "Charles" };
var facade = entryPoint.Initiate(data)
facade.DoSomething();
}
}
The container provides you with the puzzle pieces necessary. The actual composition of these pieces is up to you to decide. Feel free to ignore the container classes and implement your custom entry points if you have any special need.
Link methods are a great way to interconnect different features right from the container. The library provides a few, but adding your own custom ones for your use cases is a great way to speed up development.
LinkDontDestroyOnLoad
: The object will have don't destroy on load called on it when the container is bound. Behaviour can be customized with the optional parameters
class Installer : MonoBehaviourInstaller
{
public SomeService SomeService;
public override void Install(DiContainerBindings b)
{
b.Bind<SomeService>()
.Default()
.FromInstance(SomeService)
.LinkDontDestroyOnLoad();
}
}
Creation of the container is a synchronous process, if you need to do any kind of asynchronous work you can do the following:
- Simple but with delayed construction: Load the asynchronous data before creating the container, then provide it synchronously.
SomeConfig someConfig = await GetSomeConfig();
IDiContainer container = new DiContainerBindings()
.InstallSomeFunctionality(someConfig)
.Build();
- Avoids delayed construction but complex: Handle asynchronous loading after the object graph is built, the design should take into account that those dependencies are not available from the beginning
IDiContainer container = new DiContainerBindings()
.InstallSomeFunctionality()
.Build();
var initializer = container.Resolve<Initializer>();
await initializer.StartApplication();
The following is experimental and might be removed.
The bindings may also be done with a non type safe interface. This variant should only be used when implementing programmatic driven configuration. Use the type safe variant when all the types involved are known.
List<Type> someTypes = GetSomeTypesWithReflection();
foreach(var type in someTypes)
{
b.Bind(type)....
}
Using reflection to do such bindings will slow down your application due to the runtime Type metadata analysis necessary.
If the reduced performance is not desired, source generation of equivalent code that avoids reflection can be done to do the analysis at build time.
Some platforms may not even be compatible with reflection. If you target any such platform, using source generators is the only approach.
b.InstallSomeTypes(); // This could be source generated to do the same but faster