FactoryGenerator is an IoC container that uses Roslyn to prepare a container for consumption at compile-time. Inspired by, but having little in common with Autofac beyond syntax choices.
- Attribute-based Generation: Simply decorate your code with attributes like
[Inject]
,[Singleton]
,[Self]
and more and your IoC container will be woven together. - Test-Overridability: Need to swap out one injection for another to test something? Simply
[Inject]
a replacement inside your test project for a new container.
FactoryGenerator consists of two seperate Nuget packages. FactoryGenerator and FactoryGenerator.Attributes, the former contains the actual Roslyn generator and the latter contains the attributes and container interfaces which can be used to decorate code.
FactoryGenerator:
References this package to your top-level project, or whichever project will act as the place to "Weave together" your IoC container. If your code fits in a singular project, then just add this nuget package to that. Typically these would be your "Console/WPF Application" and the likes, and not your classlibs.
FactoryGenerator.Attributes:
Reference this package from projects which add things to the IoC container via the [Inject]
attribute, since it is the package that contains said attributes.
For an example regarding which projects are best server by which package, see the following:
graph TD;
App-->Dependency-A;
App-->Dependency-B;
Dependency-B-->Dependency-C;
Dependency-A-->Dependency-C;
In the above graph, the reccomended action would be to add a reference to FactoryGenerator.Attributes to Dependency C as that will in turn allow Dependency A and Dependency B to reference the attributes therein. Additionally, a reference to FactoryGenerator is best added to App, which is where the IoC container will become available and be considered "Complete".
In order to start using FactoryGenerator on a new (single .csproj) project, start by adding the nuget packages by, for example, using the dotnet CLI in your project directory:
dotnet add package FactoryGenerator
dotnet add package FactoryGenerator.Attributes
Afterwards, create an interface or two somewhere within your project, such as the following:
namespace MyProject.Interfaces;
public inteface IWorldGreeter
{
void SayHi();
}
public interface IGreetingProvider
{
string Greeting { get; }
}
These are the contracts that we are interested in resolving from our container.
Next, lets create some implementations, and inject them:
using System;
using MyProject.Interfaces;
using FactoryGenerator.Attributes;
namespace MyProject.Implementations;
[Inject]
public class WorldGreeter(IGreetingProvider provider) : IWorldGreeter
{
void SayHi() => Console.WriteLine(provider.Greeting);
}
[Inject, Singleton]
public class GreetingProvider : IGreetingProvider
{
string Greeting { get; } = "Hello, World!"
}
There, now we have some functions and properties that can run, so, what's next?
Let's consider the entry point Main
as the place where we want to use our generated container. Let's create it there and make it say hi!.
namespace MyProject;
public class Program
{
public static void Main()
{
//Create an instance of our compile-time container.
var container = new Generated.DependencyInjectionContainer();
//Get an instance of our greeter, in this case it will resolve to a new instance of WorldGreeter that uses a singleton instance of GreetingProvider as its' dependency.
var greeter = container.Resolve<IWorldGreeter>();
//Call the method the interface provides, will print "Hello, World!"
greeter.SayHi();
}
}
Of note is perhaps Generated.DependencyInjectionContainer
, this is the Compile-time created implementation of our IoC container, it implements the interface FactoryGenerator.IContainer
.
Attribute | Description | Requires |
---|---|---|
Inject |
Adds any interface directly listed by the type or Method-return-type to the IoC container | |
As<T> |
Explicitly inject the type which holds this attribute as the type specified, in addition to its directly implemented interfaces |
Inject |
ExceptAs<T> |
Do not inject the type which holds this attribute as the type specified | Inject |
InheritInterfaces |
Also includes any interfaces that the type inherits | Inject |
Self |
Also include the type itself directly. | Inject |
Singleton |
Ensures that this type will be resolved exactly once, and any subsequent resolves of the same type will return the same instance |
Inject |
Scoped |
Ensures that this type will be resolved once per created scope, if you do not use IContainer.BeginLifetimeScope(), this behaves like a singleton | Inject |
Boolean(string key) |
Creates a Runtime switch to decide whether this type should be the one that gets resolved (or the best fitting fallback option, otherwise) |
Inject |
Overriding Injections, i.e if in the graph above Dependency C injected an ISomething
instance and that specific implementation of ISomething
will not work for anything that uses Dependency B, then Dependency B can substitute that injection by providing it's own injection of ISomething
. This overriding generally follows the project dependency tree, so if Project A depends on Project B which depends on Project C, A can override both B and C, but B cannot override A.
Note
Overriding Injections will not work if you resolve an IEnumerable<ISomething>
, as that will net your a collection of all ISomething
that have been injected.
What happens if there are some constructor values needed by certain injected implementations, such as command line arguments, that cannot be known at compile time? Well, consider the following
using FactoryGenerator.Attributes;
namespace Somewhere;
[Inject]
public class SomeClass(CommandLineOptions options, IInjectedInterface injected, IAnotherInjectedInterface another) : ISomeInterface
{
/*...Methods and such...*/
}
Here, CommandLineOptions
is a class created by an imaginary Main
method at the start of the program, and not an option for compile-time injection.
Thus, if you were to have an injected class like this, you would find that the new Generated.DependencyInjectionContainer()
looks a little different.
Specifically, the constructor for DependencyInjectionContainer
will now look something like this:
public DependencyInjectionContainer(CommandLineOptions options)
{
/*...Generated Code...*/
}
And will thus require the user to provide the value at container-creation time.
FactoryGenerator is not limited to only allowing for the injection of classes. Consider the following:
using FactoryGenerator.Attributes;
namespace Somewhere;
public interface IResultType;
public interface IProvider
{
[Inject, Singleton]
IResultType Method();
}
[Inject]
public class Provider : IProvider
{
public IResultType Method() => /*Implementation Code*/
}
With this code, it is now possible to do container.Resolve<IResultType>()
, which will effectively return the result of new Provider().Method()
, although, since Method
is [Inject]
ed as a [Singleton]
, the result will be cached and the same instance will be returned at every call to Resolve
as well as shared between all Injected implementations that require a IResultType
.