Compiled some interesting facts about the way .Net dependency injection works behind the scenes. Some you probably already know, others you never bothered about. Knowing some may save you head-scratching in the future and others you may simply find useful. Enjoy!
Scopes
Mixing scopes
While working with .Net DI scopes you probably already know that using scoped dependency in singleton services is a no-go.
But do you know, what happens if you try using scoped dependency in transient service? Or transient dependency in singleton?
Well, actually nothing bad happens. Services will be resolved properly.
I checked all the scope combinations for you and merged them into the simple table:
Service / Dependency | Transient | Scoped | Singleton |
Transient | OK | OK | OK |
Scoped | OK | OK | OK |
Singleton | OK | Error | OK |
It seems the only case, you may keep an eye on is using Scoped
dependency in Singleton
service.
What if I register two instances of the same interface with different lifetimes?
You can register many of the same interface implementations within different lifetimes, but when you try to use them make sure they obey the mixing lifetimes table as above.
So if you refer to the list of all implementations in singleton service and any of them will be registered as scoped, then an exception will be thrown.
Controlling scope
Those of us who are working mainly in ASP.net web development are used to treating a single API request as a scope for all dependencies within it. But what if we have a different type of application, or we work with a service that does not provide scope context per se? Can I have scope in my console service bus events consumer client?
Sure you can. And there are a few ways to do that:
Create scope directly from
IServiceProvider
.using(var scope = serviceProvider.CreateScope()) { var serviceBus = scope.GetRequiredService<IServiceBusService>(); // do something, receive, process or send msg etc.... }
Create it using
IServicesScopeFactory
singleton serviceusing(var scope = scopeFactory.CreateScope()) { var serviceBus = scope.GetRequiredService<IServiceBusService>(); // do something, receive, process or send msg etc.... }
Technically there is no difference in both, as the service provider uses factory implementation internally. The only thing that you might want to keep in mind is that the factory is always a singleton, whereas the scope provider lifetime depends on the context in which it was created.
But why would you do that at all?
Simply said: to isolate a unit of work, but reuse all needed dependencies within it.
An important fact, that plays a pivotal role in the decision to create your own scope is, that every resolved scoped service is disposed of and cleaned up when the scope is not used anymore.
This allows for more efficient memory usage. Without that, all resolved services would live for a lifetime of the app, which may lead to memory leaks.
Development
environment. You can launch it for production by configuring the default service provider.A common case for use own scope is for background jobs (like via BackgroundService
or IHostedService
). Services performing tasks get disposed of, when the job is done.
Registration
If you are like me, you probably mainly register interfaces and classes as their implementations in the DI container. And most likely single instance per interface. But have you tried doing something unusual? Did you wonder if for example:
Can you register different implementations of the interface? Which one will be resolved?
services.AddScoped<IInterface, Implementation1>()
.AddScoped<IInterface, Implementation2>(); // <-- no error here
Yes, you can. There can be multiple implementations of the same interface registered, and you can access all of them or just one. The one that gets resolved is the last one registered.
public MySuperService(IInterface interface) { ... } // <-- Implementation2
How to access other registered implementations?
You simply inject IEnumerable
of registered interfaces to your service. And check the implemented types from there.
// injects: [ Implementation1, Implementation2 ]
public MySuperService(IEnumerablr<IInterface> interface) {
// find the implementation that you want to use
}
Some patterns can help you pick the right one without explicitly checking their type, but this can be a talk for another time.
Can I register and resolve the primitive type or string?
Primitive types no. Structs neither, as they are not reference types. String you can provide and resolve normally as any other service.
On the other side of the spectrum dynamic objects also can not be registered, as their type can not be statically determined.
// !! won't compile
dynamic myDynamicSomething = new { BlahBlah = "Some string" };
builder.Services.AddSingleton(typeof(myDynamicSomething), myDynamicSomething);
// !! neither would this
builder.Services.Add(new ServiceDescriptor(typeof(myDynamicSomething),
myDynamicSomething));
A common way to get around the limitation with primitives would be to wrap them around any DTO class. This is what configuration objects mainly do.
Or you can go to the next question and find another workaround there.
Can I have a function as a dependency and resolve it?
Yes. Simply registering Func
or Action
type and a specific instance of it, you can use it in the constructors like any other type. This is a nice way to bring some functional concepts to your objects.
This way you can register functions returning primitives and other non-registerable types if you want to sneak them to your services.
// Program.cs
builder.Services.AddSingleton<Func<DateTime>>(() => DateTime.UtcNow);
// Service using current time
public MyTimeService(Func<DateTime> getDataTime) { ... }
Func
or Action
. The code looks much cleaner this way in my opinion.// declare delagate
public delegate DateTime GetDateTime();
// some helper
public static class DateTimeHelper
{
public static GetDateTime Local = () => DateTime.Now;
public static GetDateTime Utc = () => DateTime.UtcNow;
}
// register delegate type
builder.Services.AddSingleton<GetDateTime>(DateTimeHelper.Utc);
builder.Services.AddSingleton<GetDateTime>(DateTimeHelper.Local); // either one
// resolve delegate in service
public MyTimeService(GetDateTime getDataTime) { ... }
Resolution
In my services, I tend to have one constructor for all the dependencies. Only in specific cases, there is another default constructor most likely a private one, that is used by an external library (For example DbContext
of Entity Framework).
Still, I did wonder, what is the logic behind constructor resolution. And asked me a few questions:
Which constructor is selected for dependency resolution?
The constructor with the most resolvable parameters takes precedence over others.
If there isn't any, then an exception is thrown.
Implicit default constructor is used only when there is no other constructor declared.
There is an exception to this rule, where you can tell the container which one of the constructors to use. Will get to that later.
What happens if I have two constructors with the same number of parameters?
If both of them have the same number of resolvable dependencies, then an ambiguity exception is thrown while trying to build a service provider.
Must the constructor be public?
Yes, there must be at least one public constructor in service, otherwise, the container builder will fail on startup.
There is an exception to this rule: If the internal class has no constructors, then the internal implicit one is used.
Can you intentionally select a constructor for resolution?
Yes and no.
In .Net 8: Yes by marking it with
[ActivatorUtilitiesConstructor]
attribute. However keep in mind that it is used internally by the container, and might be changed in the future. So use it with caution.In .Net 7 and lower not really: the attribute will work only if the constructor marked with it has more resolvable parameters than any other, which in practice makes it useless.
If we have a constructor with the same number of parameters as another but marked with an attribute, we still get a constructor ambiguity exception.
This was reported as a bug but fixed only in .Net 8. See the details here.
Lessons learned
With .Net DI like with every other tool, there is more to it than meets the eye. Although now you may not use it in any of the cases I presented, it is in my opinion good to know some unusual scenarios beforehand if you happen to stumble upon them in the future. Even better when someone did the testing and research, so you can just quickly read the article ;)
I probably missed lots of these cases here. And If you have any, that is worth mentioning, feel free to share it in the comments.
Thank you and as usual: grab some code pieces from GitHub link here, where I put all the above cases in one project. You can test it yourself.