Before reaching for .AddScoped
or new
, take a step back and ask what your class needs to do — not what it needs to create. This should model a functionality from the business domain.
Design Thinking First, DI Later
A well-designed class doesn't scream for injection. It simply:
- Has a clear purpose.
- Exposes what it needs to perform that purpose.
- Delegates responsibilities it shouldn't own.
Let’s unpack that with a practical approach.
Step 1: Identify the Class's Responsibility
Ask: What behavior is this class responsible for? . Let us analyse an order processing functionality. How would you model the behavior and construct the class?
Example:
public class OrderProcessor
{
public void ProcessOrder()
{
// Validate order
// Charge payment
// Send confirmation email
}
}
Looking at the above class, you’ve already spotted the problem: This class does too much. This is linked to the 'S' in SOLID principles, the class should handle only a single-responsibility.
Step 2: Delegate Responsibilities via Abstractions
Break it down. Let other services handle each concern.
public interface IOrderValidator
{
bool Validate(Order order);
}
public interface IPaymentService
{
void Charge(Order order);
}
public interface INotificationService
{
void SendConfirmation(Order order);
}
Now rewrite OrderProcessor
as a coordinator, not a worker:
public class OrderProcessor
{
private readonly IOrderValidator _validator;
private readonly IPaymentService _payment;
private readonly INotificationService _notifier;
public OrderProcessor(
IOrderValidator validator,
IPaymentService payment,
INotificationService notifier)
{
_validator = validator;
_payment = payment;
_notifier = notifier;
}
public void ProcessOrder(Order order)
{
if (!_validator.Validate(order)) return;
_payment.Charge(order);
_notifier.SendConfirmation(order);
}
}
You’ve just designed for Dependency Injection — without thinking about the container.
Step 3: Make It Testable and Flexible
This class can now be tested using mocks:
var mockValidator = new Mock<IOrderValidator>();
mockValidator.Setup(v => v.Validate(It.IsAny<Order>())).Returns(true);
var mockPayment = new Mock<IPaymentService>();
var mockNotifier = new Mock<INotificationService>();
var processor = new OrderProcessor(mockValidator.Object, mockPayment.Object, mockNotifier.Object);
processor.ProcessOrder(new Order());
✅ No real implementations required. ✅ You can test logic in isolation.
Step 4: Let the Container Wire It Up
Only now do you register it in .NET Core
:
builder.Services.AddScoped<IOrderValidator, OrderValidator>();
builder.Services.AddScoped<IPaymentService, StripePaymentService>();
builder.Services.AddScoped<INotificationService, EmailNotifier>();
builder.Services.AddScoped<OrderProcessor>();
Summary: The Design-First DI Checklist
Step | Ask Yourself… | Outcome |
---|---|---|
1 | What does this class need to do? | Define the class’s responsibility |
2 | What can be delegated to collaborators? | Extract abstractions |
3 | How can this class be tested in isolation? | Refactor for injectability |
4 | Where do I register this in DI container? | Configure DI at the edges |
📌 Remember
DI isn’t about using a container.
It’s about designing classes that ask for what they need, instead of building it themselves.
Start from behavior. Inject contracts. Let the container worry about construction.
Top comments (0)