Skip to Content
Course content

239: Registering Services with Interfaces

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

I once worked with a developer who spent an entire Friday afternoon hunting down every instance of new FileLogger() across a massive project to replace it with new CloudLogger() because the infrastructure team had migrated to Azure. It was a tedious, error-prone slog. He was essentially playing a game of "Find and Replace" with his career on the line, and the worst part? He missed one instance in a legacy utility class, and the app crashed in production because it couldn't find a local directory that no longer existed. If we had registered the logger as an interface, that entire afternoon of misery would have been a single line change in the Program.cs file.

Decoupling the "What" from the "How"

Up until now, you've likely been registering services by their concrete classes. That works fine for simple apps, but it ties your business logic to a specific implementation. When you register a service using an interface, you're telling the .NET Dependency Injection (DI) container: "Whenever a class asks for this interface, give them this specific implementation."

Take a notification system, for example. Your application logic doesn't actually care if a message is sent via SMTP, SendGrid, or a Twilio SMS. It just cares that a message gets sent. By creating an INotificationService interface, you decouple the request (sending a notification) from the execution (the actual API calls to a provider).

public interface INotificationService
{
    Task SendAsync(string recipient, string message);
}

public class EmailNotificationService : INotificationService
{
    public async Task SendAsync(string recipient, string message)
    {
        // Logic to send a real email
        await Task.CompletedTask;
    }
}

Wiring the Implementation in the Container

The magic happens in your Program.cs. Instead of using the shorthand AddTransient<EmailNotificationService>(), you use the overload that accepts both the service type and the implementation type.

var builder = WebApplication.CreateBuilder(args);

// Registering the interface to the concrete implementation
builder.Services.AddTransient<INotificationService, EmailNotificationService>();

var app = builder.Build();

Now, look at how this changes your constructors. You no longer ask for the EmailNotificationService specifically. You ask for the interface.

public class OrderProcessor
{
    private readonly INotificationService _notificationService;

    public OrderProcessor(INotificationService notificationService)
    {
        _notificationService = notificationService;
    }

    public async Task CompleteOrder()
    {
        // Business logic here...
        await _notificationService.SendAsync("customer@example.com", "Your order has shipped!");
    }
}

I love this pattern because it makes unit testing a breeze. You don't want your unit tests sending actual emails to customers every time you run them. Because OrderProcessor depends on INotificationService, you can inject a MockNotificationService during your tests without changing a single line of code in the OrderProcessor class itself.




📋 Practical Task

Exercise: Implementing a Swappable Payment Gateway

You are building a checkout system that currently uses Stripe for payments, but the business wants the flexibility to switch to PayPal if fees change. Your goal is to refactor the payment logic to use an interface so the implementation can be swapped in Program.cs without touching the checkout logic.

Requirements:

  • Create an interface named IPaymentProcessor with a method bool ProcessPayment(decimal amount).
  • Create a concrete class StripePaymentProcessor that implements this interface (return true to simulate success).
  • Create a second concrete class PayPalPaymentProcessor that also implements this interface.
  • Create a class CheckoutService that accepts IPaymentProcessor in its constructor and has a method CompletePurchase(decimal total) that calls the processor.
  • In a mock Program.cs setup, register StripePaymentProcessor as the implementation for IPaymentProcessor.
  • The Test: Change the registration line in Program.cs to use PayPalPaymentProcessor and verify that CheckoutService now uses PayPal without any changes to its own code.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.