Skip to Content
Course content

79: Dependency Injection in .NET

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've noticed a recurring theme when I mentor junior devs: they tend to think Dependency Injection (DI) is some "magic" feature of the .NET framework that requires a complex library or a PhD in architectural patterns to understand. They see the IServiceCollection and the various lifetimes (Transient, Scoped, Singleton) and assume DI is a tool provided by Microsoft to make simple things complicated.

DI is a "Framework Feature" vs. DI is just passing arguments

Here is the biggest secret about DI: you can do it without any framework at all. At its core, Dependency Injection is just a fancy name for "passing an object into a class instead of letting the class create it."

Look at this common mistake. Imagine you're building a system to send user notifications:

public class NotificationService 
{
    private readonly EmailSender _sender;

    public NotificationService() 
    {
        // This is the "Wrong Way"
        _sender = new EmailSender(); 
    }

    public void Notify(string message) => _sender.Send(message);
}

This looks fine until you try to write a unit test. To test NotificationService, you are forced to actually trigger EmailSender, which probably tries to connect to a real SMTP server. Your tests will be slow, flaky, and will probably spam your boss with emails. You've "tightly coupled" the service to a specific implementation.

Now, let's do actual Dependency Injection. No magic, just a constructor:

public interface IMessageSender { void Send(string message); }

public class NotificationService 
{
    private readonly IMessageSender _sender;

    // This is DI. I'm just asking for what I need.
    public NotificationService(IMessageSender sender) 
    {
        _sender = sender;
    }

    public void Notify(string message) => _sender.Send(message);
}

Now, I can pass in a real EmailSender in production, or a MockSender in my tests. I didn't use a single line of .NET framework code to achieve this; I just stopped using the new keyword inside my class.

Manually Wiring Objects vs. Using the ServiceProvider

If you have two classes, doing it manually is easy. If you have two hundred, it's a nightmare. You'd end up with a "Main" method that's a thousand lines of new ClassA(new ClassB(new ClassC...)). This is where the .NET built-in DI container comes in. It's essentially a giant dictionary that knows how to create your objects for you.

In a modern .NET app, you register your dependencies in Program.cs. This tells the container: "Whenever someone asks for IMessageSender, give them an instance of EmailSender."

var builder = WebApplication.CreateBuilder(args);

// Registration
builder.Services.AddTransient<IMessageSender, EmailSender>();
builder.Services.AddTransient<NotificationService>();

But you'll see three different methods for adding services. I've seen people guess which one to use, but there's a very logical reason for each:

  • Transient: Created every single time they are requested. Use this for lightweight, stateless services.
  • Scoped: Created once per client request (e.g., one per HTTP request). This is critical for things like DbContext, where you want the same database connection used across different classes during a single web request, but not shared with other users.
  • Singleton: Created once and lives for the entire life of the application. Use this for things like a cache or a configuration wrapper. Be careful here—if a Singleton holds onto a Scoped service, you've got a memory leak or a concurrency bug waiting to happen.

When the framework sees NotificationService in a controller constructor, it looks at the registrations, sees it needs an IMessageSender, creates the EmailSender, injects it, and hands you the fully constructed service. That's the "magic"—it's just automated constructor calls.




📋 Practical Task

Refactoring the OrderProcessor to use an IDiscountService

You have been handed a legacy OrderProcessor class that is currently impossible to unit test because it hard-codes its dependency on a specific discount logic class. Your goal is to refactor this to use Dependency Injection.

Current Code:

public class OrderProcessor 
{
    private readonly BlackFridayDiscountCalculator _calculator;

    public OrderProcessor() 
    {
        _calculator = new BlackFridayDiscountCalculator();
    }

    public decimal ProcessOrder(decimal amount) 
    {
        return _calculator.ApplyDiscount(amount);
    }
}

public class BlackFridayDiscountCalculator 
{
    public decimal ApplyDiscount(decimal amount) => amount * 0.5m;
}

Your Task:

  1. Create an interface named IDiscountCalculator that defines the ApplyDiscount method.
  2. Modify BlackFridayDiscountCalculator to implement this interface.
  3. Refactor OrderProcessor to accept IDiscountCalculator through its constructor instead of instantiating it internally.
  4. Write the line of code you would add to Program.cs (the IServiceCollection) to register BlackFridayDiscountCalculator as a Transient service for the IDiscountCalculator interface.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.