C#
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
239: Registering Services with Interfaces
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
IPaymentProcessorwith a methodbool ProcessPayment(decimal amount). - Create a concrete class
StripePaymentProcessorthat implements this interface (returntrueto simulate success). - Create a second concrete class
PayPalPaymentProcessorthat also implements this interface. - Create a class
CheckoutServicethat acceptsIPaymentProcessorin its constructor and has a methodCompletePurchase(decimal total)that calls the processor. - In a mock
Program.cssetup, registerStripePaymentProcessoras the implementation forIPaymentProcessor. - The Test: Change the registration line in
Program.csto usePayPalPaymentProcessorand verify thatCheckoutServicenow uses PayPal without any changes to its own code.
There are no comments for now.