C# & .NET

Understanding Classes, Interfaces, and Dependency Injection

Objects hold behavior. Interfaces express contracts. Dependency injection lets you choose how the pieces connect.

1 min read Beginner 18 min
C#OOPDependency Injection

// you will learn

  • Distinguish an interface from an implementation
  • Inject dependencies through a constructor
  • Choose an appropriate service lifetime

// before you start

  • Variables, methods, and basic C# syntax

Start with behavior, not a framework

A class groups state and behavior. An interface describes what a collaborator can do without deciding how it does it. Dependency injection means receiving a collaborator instead of constructing it inside the class that uses it.

Make the dependency visible

public interface IMessageSender
{
    Task SendAsync(string message, CancellationToken token);
}

public sealed class NotificationService(IMessageSender sender)
{
    public Task NotifyAsync(CancellationToken token) =>
        sender.SendAsync("Your report is ready.", token);
}

// In an ASP.NET Core composition root:
builder.Services.AddScoped<IMessageSender, EmailSender>();
builder.Services.AddScoped<NotificationService>();

EmailSender must implement IMessageSender. The registration belongs in the composition root: the place where the application connects its concrete implementations. The notification service does not need to know SMTP settings, network clients, or provider credentials.

Choose the right lifetime

Transient dependencies are created when requested. Scoped dependencies live for a scope, usually one HTTP request. Singletons live for the application lifetime. Do not capture a scoped database context in a singleton: it outlives the scope it depends on.

Test the contract

Supply a fake sender that records the message. Verify that the notification service sends the expected message once and propagates cancellation. This test requires no mail server.

Try it yourself

  1. Implement a console sender.
  2. Construct the notification service manually without a container.
  3. Replace the sender in a test.
  4. Explain why a container is convenient but not essential to dependency injection.

Do not introduce an interface for every class automatically. Introduce a boundary when it clarifies a responsibility or gives you a meaningful alternative implementation.

// the notebook, by email

Learn alongside me.

Engineering ideas worth understanding.