This episode teaches dependency injection in .NET: the Microsoft.Extensions.DependencyInjection container, scoped, transient, and singleton lifetimes, service registration patterns, and how to configure services inside the Generic Host. DI is the backbone of modern .NET applications.

In episode 5 you learned about interfaces; episode 8 shows why interfaces matter so much in .NET: they are the core material of dependency injection (DI). DI is a pattern in which objects receive their dependencies from outside rather than creating them themselves — and the .NET DI container manages the whole process.
With DI, code becomes easy to test, easy to replace, and the application structure becomes clear. The Microsoft.Extensions.DependencyInjection container is already embedded in the Generic Host, so every modern .NET application uses it without additional configuration.
The container stores registrations as pairs of contracts and implementations. When a service is requested, the container creates the matching implementation and injects its dependencies. An example service and its contract:
public interface IEmailSender
{
Task KirimAsync(string ke, string pesan);
}
public class SmtpEmailSender : IEmailSender
{
public Task KirimAsync(string ke, string pesan)
{
Console.WriteLine($"Email ke {ke}: {pesan}");
return Task.CompletedTask;
}
}SmtpEmailSender implements IEmailSender. Consumer code depends only on the interface, so the implementation can be replaced without changing callers — for example, from SMTP to a cloud service.
Registration happens on IServiceCollection inside Program.cs:
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddSingleton<IEmailSender, SmtpEmailSender>();
var host = builder.Build();builder.Services.AddSingleton<IEmailSender, SmtpEmailSender>() tells the container: when IEmailSender is requested, return SmtpEmailSender. builder.Build() converts the collection into a ready-to-use container.
The lifetime determines how long an instance lives:
builder.Services.AddTransient<IPesanan, PesananService>();
builder.Services.AddScoped<IKontek, KontekService>();
builder.Services.AddSingleton<ILog, LogService>();AddTransient, AddScoped, and AddSingleton are the three main registration methods. The golden rule: scoped must not be injected into singleton — because the singleton outlives the scope, a reference leak will corrupt behavior and memory.
A guide:
Services are consumed through the constructor — the container fills them automatically:
public class OrderService
{
private readonly IEmailSender _sender;
public OrderService(IEmailSender sender)
{
_sender = sender;
}
public async Task PesanAsync(string email)
{
await _sender.KirimAsync(email, "Pesanan diterima");
}
}OrderService requests IEmailSender through its constructor. The container detects this parameter and injects the matching instance. Constructor injection is the most common and most easily testable form of DI.
For choosing an implementation at runtime, use a factory:
builder.Services.AddSingleton<IEmailSender>(sp =>
{
var config = sp.GetRequiredService<IConfiguration>();
return config["Mail:Provider"] == "SendGrid"
? new SendGridEmailSender()
: new SmtpEmailSender();
});The factory receives IServiceProvider (sp) so it can read other services — here IConfiguration — before deciding on the implementation. This pattern is useful for different providers across environments.
All host services — logging, configuration, and custom ones — are registered through builder.Services:
using var host = Host.CreateApplicationBuilder(args)
.Build();
var sender = host.Services.GetRequiredService<IEmailSender>();
await sender.KirimAsync("admin@example.com", "Test");host.Services.GetRequiredService<IEmailSender>() retrieves a service directly from the container — useful at the application entry point. For ordinary code, rely on constructor injection; accessing the container directly should be kept to a minimum.
Enable scope validation to catch wrong registrations earlier:
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddScoped<IData, DataService>();
builder.Services.AddSingleton<ICache, CacheService>();AddScoped on a service that depends on an AddSingleton is valid; conversely, a singleton depending on a scoped service will trigger an error when scope validation is active. Get used to running your application in development with this validation so lifetime problems are caught before production.
Warning
Watch the lifetime when injecting DbContext. Its default is scoped so that one request uses one context — storing it in a singleton will leak connections and state across requests.
Key takeaways:
In the next episode 9 we will discuss data access and persistence — Entity Framework Core, DbContext, entity mapping, migrations, querying with LINQ and raw SQL, and the SQL Server, PostgreSQL, and SQLite database providers.