Dependency Injection, SOLID and Unit Testing in C#
Codevity Admin · 10/25/2026
Codevity Admin · 10/25/2026
Even for freshers, interviewers ask whether you can write code that is easy to change and easy to test. Dependency injection, SOLID and unit testing are how that is judged.
A class has a dependency when it needs another object to do its job. Without DI, the class creates it itself (new EmailSender()), which glues the two together: you cannot swap the sender or test the class alone. With dependency injection, the dependency is given to the class from outside, usually through the constructor, and the class depends on an interface.
INotifier notifier = new EmailNotifier();
var service = new OrderService(notifier);
service.Place("A-100"); // Email sent for A-100
public interface INotifier
{
void Send(string message);
}
public class EmailNotifier : INotifier
{
public void Send(string message) => Console.WriteLine($"Email sent for {message}");
}
public class OrderService(INotifier notifier) // primary constructor (C# 12)
{
public void Place(string orderId) => notifier.Send(orderId);
}OrderService knows only the INotifier contract. In a test you pass a fake notifier. In production, you pass a real one. The principle behind this is inversion of control: the caller decides which implementation is used.
In ASP.NET Core, a built-in container creates and injects objects for you. You register services once:
builder.Services.AddTransient<IEmailBuilder, EmailBuilder>();
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddSingleton<IClock, SystemClock>();| Lifetime | A new instance is created... | Typical use |
|---|---|---|
| Transient | Every time the service is requested | Lightweight, stateless services |
| Scoped | Once per scope (in a web app, once per HTTP request) | Database contexts (DbContext), per-request state |
| Singleton | Once for the whole application | Caches, configuration, shared thread-safe services |
A common bug is the captive dependency: a singleton that depends on a scoped service keeps that one scoped instance forever. A service should never depend on one with a shorter lifetime.
| Letter | Principle | Meaning | Violation to spot |
|---|---|---|---|
| S | Single Responsibility | A class has one reason to change | A class that validates, saves and emails |
| O | Open/Closed | Open to extension, closed to modification | A growing switch on type that you edit for every new case |
| L | Liskov Substitution | A derived class must work wherever its base class is expected | A Square that breaks code written for Rectangle |
| I | Interface Segregation | Prefer small, focused interfaces | An with that a robot must implement |
| D | Dependency Inversion | Depend on abstractions, not concrete classes | inside a business class |
A tiny Open/Closed example: add a new discount rule by adding a class, not by editing existing code.
A unit test checks one small piece of behaviour in isolation, quickly and repeatably. In .NET the common frameworks are xUnit (the usual choice for new projects), NUnit and MSTest. Structure every test as Arrange, Act, Assert (AAA):
Good unit tests are:
Testable code and DI go together: because takes an , you can test it without sending any email. See the xUnit test snippet.
IWorkerEat()new SqlRepository()IClockDateTime.NowMethod_Scenario_ExpectedResultOrderServiceINotifiernewPriceCalculatorIDiscount[] rules = { new NoDiscount(), new TenPercentDiscount() };
// Prints 200, then 180.0 (the decimal keeps the scale of 0.9m).
foreach (var rule in rules)
Console.WriteLine(rule.Apply(200m));
public interface IDiscount
{
decimal Apply(decimal price);
}
public class NoDiscount : IDiscount
{
public decimal Apply(decimal price) => price;
}
public class TenPercentDiscount : IDiscount
{
public decimal Apply(decimal price) => price * 0.9m;
}using Xunit;
public class PriceCalculatorTests
{
[Fact]
public void ApplyDiscount_TenPercent_ReducesPrice()
{
// Arrange
var calculator = new PriceCalculator();
// Act
decimal result = calculator.ApplyDiscount(200m, 10);
// Assert
Assert.Equal(180m, result);
}
}
public class PriceCalculator
{
public decimal ApplyDiscount(decimal price, int percent) => price - price * percent / 100m;
}