.Net DI gotchas, tips and tricks

.Net DI gotchas, tips and tricks

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 / DependencyTransientScopedSingleton
TransientOKOKOK
ScopedOKOKOK
SingletonOKErrorOK

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:

  1. Create scope directly from IServiceProvider.

     using(var scope = serviceProvider.CreateScope())
     {
         var serviceBus = scope.GetRequiredService<IServiceBusService>();
         // do something, receive, process or send msg etc....
     }
    
  2. Create it using IServicesScopeFactory singleton service

     using(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.

💡
You may not know but the container validation mechanism is turned on only on the 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
💡
This technique is actually pretty useful when you need to overwrite some service implementations with your own when you can not change their implementation directly. Like provided by framework ones or some provided by external libraries.

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) { ... }
💡
For better verbosity and more elegant code, I would recommend specifying a delegate type for any function you want to register and use it instead of a simple 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.

This was important for me to understand when adding keyed services functionality to the .Net 7 application, where I wanted to mimic resolution logic.

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.

The class itself can be internal in any case. It just must be visible in the code it is registered in.

Can you intentionally select a constructor for resolution?

Yes and no.

  1. 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.

  2. 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.