## The Moment DI Clicked for Me
I used Dependency Injection for months before I actually understood it. I registered services, injected them via constructors, and it worked — but I couldn't explain why it was better.
Then I had to write a unit test for a service that had hardcoded dependencies. Suddenly, the entire point of DI became viscerally obvious.
## The Core Problem DI Solves
Without DI:
```csharp
public class OrderService
{
private EmailService _email = new EmailService(); // hardcoded
private SmsService _sms = new SmsService(); // hardcoded
private PaymentService _payment = new PaymentService(); // hardcoded
}
```
Problems with this:
- Can't test OrderService without real email/SMS/payment running
- Can't swap EmailService for a different implementation
- OrderService controls the lifetime of its dependencies
With DI:
```csharp
public class OrderService
{
private readonly IEmailService _email;
private readonly IPaymentService _payment;
public OrderService(IEmailService email, IPaymentService payment)
{
_email = email;
_payment = payment;
}
}
```
Now the caller decides what implementation to provide. OrderService depends on contracts (interfaces), not implementations.
## Service Lifetimes — The Part Everyone Gets Wrong
### Transient — New instance every time
```csharp
builder.Services.AddTransient<INotificationService, NotificationService>();
```
Every time something asks for INotificationService, a brand new instance is created.
**Use for:** Lightweight, stateless services. Things that have no shared state.
### Scoped — One instance per HTTP request
```csharp
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
```
One instance created per request. The same instance is shared within that request.
**Use for:** Database repositories, unit of work pattern. Most business services.
### Singleton — One instance for the whole application lifetime
```csharp
builder.Services.AddSingleton<ICacheService, MemoryCacheService>();
```
Created once, lives forever.
**Use for:** Caching, configuration, stateless shared services.
**Warning:** Never inject a Scoped service into a Singleton — the scoped service will behave like a singleton and hold stale data.
## Real Registration from My Portfolio
```csharp
// Program.cs
builder.Services.AddScoped<IExperienceRepository, ExperienceRepository>();
builder.Services.AddScoped<IProjectRepository, ProjectRepository>();
builder.Services.AddScoped<IBlogRepository, BlogRepository>();
builder.Services.AddScoped<ISkillRepository, SkillRepository>();
builder.Services.AddSingleton<IConnectionFactory, SqlConnectionFactory>();
```
## Constructor Injection vs Property Injection vs Method Injection
**Constructor injection** (use this — always):
```csharp
public class ExperienceController : Controller
{
private readonly IExperienceRepository _repo;
public ExperienceController(IExperienceRepository repo) => _repo = repo;
}
```
Constructor injection makes dependencies explicit and required. You can't create the class without providing its dependencies. This is what you want.
Property and method injection exist but are almost never the right choice in ASP.NET Core.
## The Testing Payoff
The entire reason to invest in DI becomes clear when you write tests:
```csharp
[Test]
public async Task GetExperiences_ReturnsOnlyVisibleOnes()
{
// Arrange — provide a fake repository
var mockRepo = new Mock<IExperienceRepository>();
mockRepo.Setup(r => r.GetByPortfolioAsync(1))
.ReturnsAsync(new List<Experience> { ... });
var controller = new ExperienceController(mockRepo.Object);
// Act
var result = await controller.GetExperiences(1);
// Assert
Assert.IsNotNull(result);
}
```
No real database. No real network. Fast, isolated, reliable tests.
## Conclusion
DI is not about syntax — it's about inverting who creates dependencies. Once you internalize that, everything else follows naturally. The syntax is just how you express that inversion in ASP.NET Core.