The Solution to the Parameter Problem
A few years ago I made a post titled Dependency Injection: The Parameter Problem explaining the difficulty of dealing with variable parameters in dependency injection.
As a quick recap, when creating a transient object via dependency injection, you need some way to pass your parameters to the instance being constructed, but you also want it to get the services it needs from the container. Historically, the most common solution for this has been "Parameter overrides", which looked something like this:
delegate IService ServiceFactory(string name);
container.Register<ServiceFactory>(
name => container.Resolve<IService>(new ParameterOverride("name", name))
);
This is good because unlike directly calling the constructor, if services get added or removed, you don't have to worry about updating anywhere, it's one of the most underrated benefits of dependency injection.
The problem with this however is threefold:
- If you rename, add, or remove parameters, it breaks. And even worse, the error will occur at runtime rather than compile time.
- If you remember that it's gonna break, you still have to go and fix it, adding the new parameter in both the factory delegate definition, and the Resolve call.
- Resolving at runtime either requires some initial cost to do runtime compilation, or does the significantly slower reflection-based construction.
Having Your Cake and Eating It
Last october I was once again conversing with my friend, lamenting the fact that the general advice is to not use parameter overrides, but without them you have manual maintenance.
It just makes me sad that there really isn't a way to have your cake and eat it
It was at that moment when it hit me. What if we use a combination of attributes and code-generation to create the factories for you.
My original draft written clunkily in a message:
class MyClass
{
public MyClass(
[Param] string name,
IService service)
}
// And it generates you a class factory:
class MyClassFactory(IService service)
{
public MyClass Create(string name) {
return new MyClass(name, service);
}
To which my friend replied:
Why would you suggest this near bed time. I'm not gonna sleep now
This solution is stunning in its simplicity and elegance. The sponge is that to make a factory, all you have to do is markup your class with a couple of attributes and immediately you've got a factory created. The icing is that whenever you change the parameters the factory updates without you needing to do anything. And the cherry on top is that due to being simple C# classes, you can easily register them for dependency injection using literally any container you like. Prism, DryIoc, Unity, Autofac, Ninject, it's truly universal.
But how do you actually do code generation?
I have to give a big thanks to Andrew Lock for his great articles going into detail on how to set up source generators. It's not obvious, but thanks to his articles I soon had a working prototype.
One particular issue I ran into was cross contamination of marker attributes, for which he already had an article explaining, he really is 10 steps ahead.
Hello FactoryGenie
After testing and solving the marker attribute issue, I'm finally happy enough with it to release it as a nuget. You can get it and use in your projects right now:
Here's how the final usage looks:
using FactoryGenie;
[GenerateFactory]
public class MyClass
{
public MyClass(string stringParam, [FactoryParam] bool boolParam, double doubleParam)
{
}
public MyClass(int intParam, string stringParam, [FactoryParam] float floatParam)
{
}
}
And here's what a factory it creates looks like:
public class MyClassFactory
{
private readonly string stringParam;
private readonly double doubleParam;
private readonly int intParam;
public MyClassFactory(string stringParam, double doubleParam, int intParam)
{
this.stringParam = stringParam;
this.doubleParam = doubleParam;
this.intParam = intParam;
}
public MyClass Create(bool boolParam)
{
return new MyClass(stringParam, boolParam, doubleParam);
}
public MyClass Create(float floatParam)
{
return new MyClass(intParam, stringParam, floatParam);
}
}
In typical programmer fasion, the name took about as long to come up with than the implementation. He grants your wish to create a factory, and genie contains "gen" as in "codegen". Very amusing.
It's still early days, and there's already many edge cases I can think of, but it's working great for everything I've needed so far. I'm gonna continue using it and triage the most important things to make it more robust, but with the goal of keeping it as simple as possible.
Happy coding!