If I have this class...
public class Something : ISomething
{
public string SomeText { get; }
public IAwesomeifier Awesomeifier { get; }
public Something(string someText, IAwesomeifier awesomeifier)
{
this.SomeText = someText ?? throw new ArgumentNullException(nameof(someText));
this.Awesomeifier = awesomeifier ?? throw new ArgumentNullException(nameof(awesomeifier));
}
// ... plus methods that actually do something...
}
... and I want to instantiate it via dependency injection (using Microsoft.Extensions.Hosting
and Microsoft.Extensions.DependencyInjection
), with the value of someText
being "Here's the text to be awesomeified", I can use ActivatorUtilities
to do it like this:
private static IHost BuildHost()
{
var builder = Host.CreateApplicationBuilder();
var services = builder.Services();
services.AddSingleton<IAwesomeifier, Awesomeifier>();
services.AddSingleton<ISomething>(provider =>
ActivatorUtilities.CreateInstance<Something>(
provider,
"Here's the text to be awesomeified"));
return builder.Build();
}
And that works fine. However, it seems fragile and error-prone. Is there some standard thing akin to this that more explicitly ties the arguments to the intended parameters?
For example, I imagine maybe something like:
services.AddSingleton<ISomething>(provider =>
ActivatorUtilities.CreateInstance<Something>(
provider,
someText: "Here's the text to be awesomeified"));
it seems fragile and error-prone. Is there some standard thing akin to this that more explicitly ties the arguments to the intended parameters?
No, unfortunately, there isn't. The only way I can see that is more type safe is fallback to direct constructor invocation and skip Auto-Wiring for this type. For instance:
For example, I imagine maybe something like:
services.AddSingleton<ISomething>(provider =>
new Something(
someText: "Here's the text to be awesomeified",
awesomeifier: provider.GetRequiredService<IAwesomeifier>()));
This registration is completely compile-time safe, but it also means that every change to the Something
constructor needs to be reflected in this registration.
In your code example, you make use of ActivatorUtilities.CreateInstance
. This method automatically figures out for you which dependencies need to be injected. Its behavior is similar to what AddSingleton<ISomething, Something>()
would do, as in: the DI Container automatically figures out which dependencies to inject. This technique is commonly known as Auto-Wiring.
Using Auto-Wiring changes of having to update this registration when dependencies are introduced to Something
go down, which might make your DI configuration more maintainable. The downside is, as you already realized, is loss of compile-time safety.
But there are other options to consider. A small refactoring to Something
completely circumvents the issue:
public record SomethingConfiguration(string SomeText);
public class Something : ISomething
{
public SomethingConfiguration Config { get; }
public IAwesomeifier Awesomeifier { get; }
public Something(SomethingConfiguration config, IAwesomeifier awesomeifier)
{
this.Config = config ?? ...;
this.Awesomeifier = awesomeifier ?? ...;
}
// ... plus methods that actually do something...
}
With this solution, the primitive type string
is wrapped in class that can more precisely defines the meaning for Something
, which is to have configuration values. This allows you to simplify your DI configuration to the following:
services.AddSingleton<IAwesomeifier, Awesomeifier>();
services.AddSingleton(
new SomethingConfiguration(someText: "Here's the text to be awesomeified"));
services.AddSingleton<ISomething, Something>();
This is a technique that I often practice for applying configuration values to my classes. Not only does this simplify my DI configuration, it typically also improves the readability of my code, where configuration values are combined in a single class, and it has a nice benefit of having a application wide configuration object that is composed out of smaller, more specific configuration objects (such as this SomethingConfiguration
).
Note that some classes get their own 'configuration' class, while others might share a configuration class. You might for instance, have certain network settings that are needed by multiple classes. When to share and when to split is not an exact science, but I tend to keep the number of configuration values in a single configuration class small.