Bald Bearded Builder
aspnetcore/dependency-injection-mistakes-you-need-to-avoid

Dependency Injection Mistakes You Need to Avoid!

Five ASP.NET Core dependency injection failures cover missing registrations, circular dependencies, lifetime mismatches, ambiguous constructors, and scoped services used by background work.

Loads from YouTube when you press play

Transcript 7 topics

Missing service registrations

0:00

Today, we're playing everyone's favorite ASP dot NET Core mini game. Why is my dependency injection throwing shade? Let's talk about five common mistakes that bite beginners and surprise veterans. We'll talk about what they are, how to fix them, and even add in some dot net tooling that makes them run smoother than my beard shaver and battery saver mode. First up is the I could swear I added that service error.

You injected an interface into a controller, ran the app, and then boom, unable to resolve service for type. That's basically dependency injection politely saying I wasn't invited to this party. For instance, let's look at this interface that always returns true. We've got an interface there. We've got a class that implements that that's returning the true.

We come to our controller and we inject it into that controller, right? Everything looks straightforward. But if we run this, we're gonna get that error that it's unable to resolve the service. Luckily for us, this is an easy fix. All we have to do is head over to the program CS and we'll say builder.services.

I'm gonna add scoped. That's a great choice for web apps. But if you need to know what type of scope you need for your scenario, whether it's scoped, singleton or transient, we've already recorded a video about that, that explains the differences in each and when you'd wanna use them. So check that out. But in this case, I'm gonna say, I'm gonna add a I category not added service, and I want category not added service class to implement it.

But I can run this and all is well with the world. But here's a pro tip for you. You also have the option to add in these values. Now validate scopes here is defaulted to true. So you don't necessarily have to add that, but this validate on build option does let you see when those things aren't registered at build time rather than getting that error at runtime.

Break service cycles

1:58

Next is the you call me, I'll call you service duet. Product service relies on category service which relies on product service. It's an endless friendship that will result in endless exceptions. The correct fix for this is actually at the design level. We need to split out responsibilities so that the cycle disappears.

For example, extract out a read only abstraction that one side needs. Now our product service only relies on an I category reader and our category service doesn't need a product service at all. No cycle, more testable code. As a last resort, you could use func of tea or lazy of tea to defer that creation, but don't use that as a way to cover up bad design. Hidden cycles are just like beard dandruff.

Avoid singleton scope misuse

2:45

Still there, just less visible. Our third mistake is the classic cannot consume a scope service from a singleton exception. Singletons last for the entire lifetime of the app. Whereas a scope service only lasts for the lifetime of one request. So when a singleton requests a scope service, which one is it getting?

Exactly. And in our case, if we try to inject that scope into the singleton, the app's gonna protest. The right way is to create a scope only when you need a scope service. So we could modify that category singleton service to use a scope service factory that generates that scope service when we need it and then gets rid of it when we're done. Then all that's needed is to come into the program CS, register those appropriately with scoped and singleton and voila, we have that scoped registered.

And when the singleton needs it, it will be able to find it and create a new one and do what it needs to do. A real world example here would be options. IOptions snapshot is scoped whereas iOptions monitor is singleton friendly. So if you have a singleton service that needs configuration that could change, prefer the iOptions monitor.

Resolve ambiguous constructors

3:58

Your future self will buy you a coffee. Fourth on our list are multiple public constructors with resolvable parameters. The container can't guess which one you want, so it throws a constructors are ambiguous exception. Now we have three options to get around this. One, we can add an attribute to the constructor we want it to use with activator utilities constructor, if I'll type it right.

Then all we have to do is register it and the container will handle everything else. It'll use that specific constructor to create new ones. Now, B would be not to use that attribute, but instead in our builder, use a factory to explicitly use the one we want. Works like a champ. Again, it's registered and just runs.

Use keyed implementations

4:50

But our third option is for when we have multiple implementations of the same interface and need to predictably choose which one we're using in our controller. In that case in .net eight and above, we can come in and register that with builder.services.adheedscoped. And then we'll say, I category service. This is not the correct service, but whatever. The specific version we want and then give it a name like fancy.

Then we need to go into our controller and specify which flavor we want. So on that attribute of the constructor, would say from keyed services and give it the name that we provided that specific implementation. And now that controller will get that specific flavor of the interface. So our last one deals with scope lifetime and threading.

Handle background scopes

5:56

We've already mentioned that scope services only live as long as the requests lifetime. But what happens if we pick up that service and try to use it again later in another thread? We may end up poking a disposed object. That's like reheating yesterday's coffee and calling it fresh. Here's a simple example to show what I'm talking about.

In this case, we're actually logging two timestamps, one in the controller and one five seconds later in a background thread. But we all know now this won't work. Our fix here will be to create a new scope inside of our background work so that we don't rely on the one passed from the controller. But here's the pro tip, don't fire and forget inside of controllers. If you need to perform background work, build some type of background service with channels and queues.

The benefit with those hosted services is that you can cleanly control those scopes and avoid those timing issues when the HTTP request ends. And you know what, just for sticking around through those five mistakes, here are five defensive settings and patterns you can follow to make your APIs safer and snappy.

Defensive DI defaults

7:02

First validate on build. We showed that code earlier, think on mistake number one, go put that in your settings and you'll know what build when you haven't registered something appropriately. Secondly, if you're building a library for others to use, use triad or triad enumerable so that you don't inadvertently override user registrations. Third, prefer constructor injection and if possible, a single public constructor. If you're in multiple implementation scenarios, factories, named options, or if you're in dot net eight, keyed services are your friend.

And then finally for configs in singletons, iOptions Monitor beats out iOptions Snapshot every day. Remember, dependency injection is here to simplify your architecture, not complicate it. So keep your lifetimes clear, your responsibilities focused, and your constructors unambiguous. Your services and your debugger will thank you for it. Until next time.

Sign in to join in. Reading needs nothing.