Menu
Coddy logo textTech

Dependency Injection Basics

Part of the Object Oriented Programming section of Coddy's C# journey — lesson 46 of 70.

Dependency Injection (DI) is a technique where a class receives its dependencies from the outside rather than creating them itself. This makes code more flexible, testable, and loosely coupled.

Without DI, a class creates its own dependencies, making it hard to change or test:

public class OrderService
{
    private EmailSender sender = new EmailSender();  // Tightly coupled
    
    public void PlaceOrder() => sender.Send("Order placed");
}

With DI, dependencies are passed in through the constructor:

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

public class EmailSender : IMessageSender
{
    public void Send(string message) => Console.WriteLine("Email: " + message);
}

public class OrderService
{
    private IMessageSender sender;
    
    public OrderService(IMessageSender sender)
    {
        this.sender = sender;
    }
    
    public void PlaceOrder() => sender.Send("Order placed");
}

Now OrderService doesn't know or care whether it's using email, SMS, or a mock sender for testing. The dependency is "injected" when the object is created:

IMessageSender emailSender = new EmailSender();
OrderService service = new OrderService(emailSender);
service.PlaceOrder();  // Email: Order placed

This pattern follows the principle of depending on abstractions (interfaces) rather than concrete implementations. It enables swapping implementations without modifying the class that uses them, which is essential for unit testing and maintaining flexible architectures.

challenge icon

Challenge

Easy

Let's build a logging system that demonstrates the power of dependency injection. Instead of hardcoding how logs are written, you'll create a flexible Logger class that can work with any logging destination - console, file simulation, or anything else - without knowing the specifics.

You'll organize your code across three files:

  • ILogWriter.cs: Define an interface called ILogWriter in the Logging namespace. This interface represents the contract for any logging destination and should have a single method Write(string message) that returns a string describing where the message was written.
  • LogWriters.cs: Create two concrete implementations of ILogWriter in the Logging namespace:
    • ConsoleLogWriter - its Write method returns Console: {message}
    • FileLogWriter - takes a filename through its constructor and its Write method returns File[{filename}]: {message}
  • Logger.cs: Create a Logger class in the Logging namespace that receives its dependency through constructor injection. The Logger should:
    • Accept an ILogWriter through its constructor and store it in a private field
    • Have a Log(string message) method that delegates to the injected writer and returns the result
  • Program.cs: Demonstrate dependency injection by creating two different loggers - one with a ConsoleLogWriter and one with a FileLogWriter. Show how the same Logger class behaves differently based on which dependency is injected.

You will receive two inputs:

  • The filename for the file logger
  • The message to log

Create a Logger with a ConsoleLogWriter and log the message. Then create another Logger with a FileLogWriter (using the filename from input) and log the same message. Print each result on its own line.

For example, if the inputs are app.log and System started, the output should be:

Console: System started
File[app.log]: System started

Notice how the Logger class never changes - it works with any ILogWriter implementation. This is the essence of dependency injection: the Logger depends on an abstraction (the interface), and the concrete implementation is "injected" from outside. You could easily add a DatabaseLogWriter or CloudLogWriter later without modifying the Logger class at all!

Cheat sheet

Dependency Injection (DI) is a technique where a class receives its dependencies from the outside rather than creating them itself, making code more flexible, testable, and loosely coupled.

Without DI (tightly coupled):

public class OrderService
{
    private EmailSender sender = new EmailSender();  // Tightly coupled
    
    public void PlaceOrder() => sender.Send("Order placed");
}

With DI (loosely coupled):

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

public class EmailSender : IMessageSender
{
    public void Send(string message) => Console.WriteLine("Email: " + message);
}

public class OrderService
{
    private IMessageSender sender;
    
    public OrderService(IMessageSender sender)
    {
        this.sender = sender;
    }
    
    public void PlaceOrder() => sender.Send("Order placed");
}

Injecting the dependency:

IMessageSender emailSender = new EmailSender();
OrderService service = new OrderService(emailSender);
service.PlaceOrder();  // Email: Order placed

DI follows the principle of depending on abstractions (interfaces) rather than concrete implementations, enabling swapping implementations without modifying the class that uses them.

Try it yourself

using System;
using Logging;

class Program
{
    public static void Main(string[] args)
    {
        // Read inputs
        string filename = Console.ReadLine();
        string message = Console.ReadLine();

        // TODO: Create a Logger with a ConsoleLogWriter and log the message
        // Print the result

        // TODO: Create another Logger with a FileLogWriter (using the filename) and log the same message
        // Print the result
    }
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming