Skip to content

Dependency Injection Nedir? ASP.NET Core ile Uygulamalı Anlatım

Yayınlandı:

Dependency Injection, bir sınıfın ihtiyaç duyduğu bağımlılıkların dışarıdan sağlanması ile daha esnek, test edilebilir ve sürdürülebilir bir mimari oluşturmayı amaçlayan bir design pattern’dır.

Problem ve Çözüm

Yazılım geliştirirken bir kodun sadece “çalışıyor” olması genellikle yeterli değildir. Uzun vadede sürdürülebilir projeler geliştirmek istiyorsak, kodun düzenli, esnek, test edilebilir ve verimli olması da en az o kadar önemlidir.

Bu hedeflere ulaşmak için yazılım dünyasında pek çok yaklaşım benimsenmiştir. Yazılım mimarileri (architecture), design pattern’lar, best practice’ler, clean code prensipleri ve daha pek çoğu. Dependency Injection (DI) da bu yaklaşımlardan biridir ve özellikle bağımlılık yönetimi konusunda modern projelerin vazgeçilmezidir.

DI, sınıflar arasındaki bağımlılıkların daha düzenli ve kontrol edilebilir hale getirilmesini sağlar. Üstelik sadece bir teknolojiye ya da dile özgü değildir. ASP.NET Core, Spring Boot gibi modern framework’lerin çoğu, DI desteğini yerleşik olarak sunar. Aslında, bu framework’lerle çalışan birçok geliştirici, farkında olmasa bile zaten DI kullanıyordur.

Bu blog yazısında amacım, dependency injection kavramını tüm yönleriyle açıklamak ve uygulamalı örneklerle zihnimizde netleşmesini sağlamak.

⚠️ Bağımlılık Sorunu

Geliştirdiğimiz projelerde, bir sınıfın başka sınıflara ihtiyaç duyması çok normal bir durumdur. Bu ihtiyaçlar, çoğu zaman aşağıdaki gibi doğrudan sınıf içerisinde tanımlanarak karşılanır.

// Bad class structure - Tightly Coupled
public class UserService
{
    private readonly EmailService _emailService = new EmailService();

    // Other codes
}

Bu örnekte UserService, doğrudan EmailService sınıfına bağımlıdır. Eğer EmailService sınıfı yoksa, UserService çalışamaz. Bu duruma tightly coupled (sıkı bağlılık) denir. Üstelik EmailService instance’ı sınıfın içinde new anahtar kelimesiyle oluşturulmuş.

Tightly coupled sınıflar geliştiricilerin hiç istemediği bir durumdur.

Çünkü şu sorunlara neden olur:

✅ Çözüm: Dependency Injection ile Loosely Coupled

Amacımız, sınıfların ihtiyaç duyduğu bağımlılıkları kendileri oluşturmaları yerine dışarıdan almasıdır. Bu yaklaşım, bağımlılıkları daha yönetilebilir hale getirir ve sınıflar arasındaki bağlantıyı gevşek hale getirir. Buna da loosely coupled (gevşek bağlılık) denir.

// Good class structure - DI and Loosely Coupled
public class UserService
{
    private readonly IEmailService _emailService;

    public UserService(IEmailService emailService)
    {
        _emailService = emailService;
    }

    // Other codes
}

Yukarıdaki örnekte, DI yöntemi kullanarak UserService sınıfının, IEmailService arayüzünü dışarıdan almasını sağlıyoruz.

Gördüğünüz gibi IEmailService instance’ı new anahtar kelimesiyle içeride oluşturulmuyor. Peki bu instance nerede oluşturuluyor? Hemen oraya gelelim.

ASP.NET Core uygulamalarında, bağımlılıkları uygulama başlatılırken DI Container yardımıyla aşağııdaki gibi register ederiz.

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddTransient<IEmailService, EmailService>();

// Other codes

Bu satırla birlikte, uygulama boyunca IEmailService ihtiyaç duyulduğunda, EmailService sınıfından bir instance oluşturulup enjekte edilir. Yani sorumuzun cevabı, dependency injection mekanizması (DI Container) tarafından instance’lar üretilir. Bu sayede:

Temel Kavramlar

Dependency Injection konusunu daha iyi anlamamız için bilmemiz gereken bazı temel kavramlara değinelim.

KavramAçıklama
Design PatternYazılım geliştirmede sık karşılaşılan problemlere yönelik, defalarca test edilmiş ve etkili olduğu kanıtlanmış çözüm şablonlarıdır.
❗ Design pattern ≠ kod parçası
✔️ Bir yapısal fikir, bir tasarım yaklaşımıdır.
Inversion of Control (IoC)Kod içerisindeki kontrolün sınıfın kendisinden alınıp dış bir yapıya devredilmesidir.
✔️ Dependency Injection, IoC prensibinin en yaygın uygulamalarından biridir.
DI Container (ya da IoC Container)Sınıfların bağımlılıklarını çözümleyen ve yaşam döngülerini yöneten yapıdır. Hangi sınıfa hangi bağımlılığın verileceğini bilir ve bu nesneleri üretir.
Loosely CoupledBileşenlerin birbirine gevşek bağlı olmasıdır. Yani bir sınıftaki değişiklik, diğer sınıfları minimum düzeyde etkiler. Bu durum, bakım ve test süreçlerini kolaylaştırır.
Tightly CoupledBileşenlerin birbirine sıkı bağlı olmasıdır. Bir sınıfta yapılan değişiklik, doğrudan diğerlerini etkileyebilir. Kodun test edilebilirliği ve esnekliği azalır.

Dependency Injection Uygulama Yöntemleri

Dependency Injection, üç farklı şekilde uygulanabilir:

Bu yöntemler arasında en yaygın ve önerilen yaklaşım Constructor Injection’dır. ASP.NET Core’da da varsayılan olarak bu yöntem kullanılır. Diğer iki yöntem ise özel senaryolar dışında pek tercih edilmez.

Constructor Injection

Constructor Injection, bağımlılıkların sınıfın constructor’ı aracılığıyla dışarıdan verilmesidir (yukarıdaki kod parçalarında da yazdığımız gibi). Bu yaklaşım, sınıfın ihtiyacı olan tüm bileşenleri doğrudan belirtmesini sağlar, böylece sınıfın bağımlılıkları daha net olur.

public interface IMessageService
{
    void Send(string message);
}

public class EmailService : IMessageService
{
    public void Send(string message)
    {
        Console.WriteLine($"Email sent: {message}");
    }
}

public class NotificationManager
{
    private readonly IMessageService _messageService;

    // Constructor Injection
    public NotificationManager(IMessageService messageService)
    {
        _messageService = messageService;
    }
}

Yukarıdaki örnekte NotificationManager sınıfı, IMessageService üzerinden soyutlanmış bir servise ihtiyaç duyuyor. Bu yaklaşım sayesinde:

Dependency Lifetime

Bir sınıf nesnesinin (instance) ne kadar süreyle yaşayacağını belirlemek önemlidir. ASP.NET Core’da servisleri IServiceCollection üzerinden DI Container’a kaydederken, bu yaşam süresini de aşağıdaki gibi belirtmemiz gerekir.

services.AddSingleton<IMyService, MyService>();
services.AddScoped<IMyService, MyService>();
services.AddTransient<IMyService, MyService>();

Her bir lifetime türünün davranışı farklıdır.

LifetimeAçıklamaNe Zaman Kullanılır?
SingletonUygulama başlatıldığında bir kez oluşturulur ve her yerde aynı instance kullanılır.Statik veri tutan, paylaşımlı ve thread-safe servislerde tercih edilir.
Örnek: services.AddSingleton<IEmailService, EmailService>();
ScopedHer HTTP isteği için bir instance oluşturulur. Aynı istek içerisinde aynı instance kullanılır.Web API servislerinde yaygın olarak kullanılır.
Örnek: services.AddScoped<IUserService, UserService>();
TransientHer ihtiyaç duyulduğunda (injection talebinde) yeni bir instance oluşturulur.Hafif, state tutmayan ve hızlı nesneler için uygundur.
Örnek: services.AddTransient<IReportGenerator, ReportGenerator>();

DI Uygulaması: Console Projesi

Dependency Injection’ı daha iyi anlamak için sıfırdan kendimiz yazalım. Bu yüzden şimdi basit bir Console uygulaması üzerinden ilerleyelim.

Console projelerinde ASP.NET Core’daki gibi hazır bir DI mekanizması yoktur. Bu yüzden Microsoft.Extensions.DependencyInjection paketini projeye manuel olarak dahil etmemiz gerekir. (Ya da başka bir DI kütüphanesi de tercih edebiliriz ama biz burada Microsoft’un geliştirdiğini kullanacağız.)

Senaryomuz şu şekilde: Kullanıcılara email veya SMS ile bildirim gönderen bir yapı tasarlıyoruz. Gerçek mesaj göndermek yerine, mesajları konsola yazdıracağız.

IMessageSender Arayüzü ve İmplementasyonlar

Amacımız loosely coupled ve kolay değiştirilebilir bir yapı kurmak.

// Senders.cs

public interface IMessageSender
{
    void Send(string message);
}

public class EmailSender : IMessageSender
{
    public void Send(string message)
    {
        Console.WriteLine($"Email sent: {message}");
    }
}

public class SmsSender : IMessageSender
{
    public void Send(string message)
    {
        Console.WriteLine($"SMS sent: {message}");
    }
}

NotificationService Sınıfı

Bu yapı sayesinde, NotificationService yalnızca IMessageSender arayüzünü tanır. Hangi somut (concrete) sınıfın kullanılacağını dışarıdan biz belirleriz. Bu da kodun test edilebilirliğini ve esnekliğini artırır.

// NotificationService.cs

public class NotificationService
{
    private readonly IMessageSender _sender;

    // Constructor Injection
    public NotificationService(IMessageSender sender)
    {
        _sender = sender;
    }

    public void Notify(string message)
    {
        _sender.Send(message);
    }
}

✔️ NotificationService, mesaj gönderme işini kendi yapmıyor. Bunun yerine bu sorumluluğu IMessageSender’a devrediyor. Hangi tip mesaj gönderici kullanılacaksa (Email mi, SMS mi), bu dışarıdan geliyor. Böylece sınıfın tek bir sorumluluğu olmuş oluyor.

Program.cs: DI Container ile Bağlantı

Burada bağımlılıkları kaydediyoruz.

// Program.cs

using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();

// DI registers
services.AddTransient<IMessageSender, EmailSender>(); // Here you could also give SmsSender
services.AddTransient<NotificationService>();

var serviceProvider = services.BuildServiceProvider();

// Get an instance of NotificationService from the DI container.
// No need to manually instantiate it using 'new'.
var notifier = serviceProvider.GetRequiredService<NotificationService>();

notifier.Notify("Hello Dependency Injection!");

✔️ Sadece tek satırı değiştirerek EmailSender yerine SmsSender kullanılmasını sağlayabiliriz. Hiçbir sınıfı değiştirmemize gerek yok. İşte buna loosely coupled diyoruz.

Microsoft.Extensions.DependencyInjection Nedir?

Bu paket, .NET ekosisteminde bağımlılık enjeksiyonu (DI) işlemleri için kullanılan Microsoft tarafından sunulan bir çözümdür.

Bu proje örneğinde, DI mantığını framework bağımsız bir şekilde nasıl uygulayabileceğimizi gördük.

🔗 GitHub – Dependency Injection

Projeyi klonlayarak doğrudan kendi bilgisayarınızda test edebilirsiniz.

DI Uygulaması: ASP.NET Core Projesi

Console uygulamasında DI’ı sıfırdan kurmuştuk. Şimdi ise ASP.NET Core tarafına geçiyoruz. ASP.NET Core projelerinde Dependency Injection yapısı framework sayesinde yerleşik olarak gelir.

Yine aynı senaryodayız: Kullanıcılara email ya da SMS ile bildirim gönderen bir yapı kuruyoruz. Gerçek mesajlar yerine çıktılar sadece konsola yazılacak.

IMessageSender Arayüzü ve Uygulamaları

Amacımız yine loosely coupled ve kolay değiştirilebilir bir yapı kurmak.

// Services/Senders.cs

public interface IMessageSender
{
    void Send(string message);
}


public class EmailSender : IMessageSender
{
    public void Send(string message)
    {
        Console.WriteLine($"Email sent: {message}");
    }
}

public class SmsSender : IMessageSender
{
    public void Send(string message)
    {
        Console.WriteLine($"SMS sent: {message}");
    }
}

NotificationService Sınıfı

Amacımız uygulama hangi yöntemi kullanarak bildirim gönderecekse, onu sadece bir noktada tanımlamak (Program.cs). Diğer sınıflar detayları bilmesin, sadece IMessageSender üzerinden çalışsın.

// Services/NotificationService.cs

public class NotificationService
{
    private readonly IMessageSender _sender;


    // Constructor injection
    public NotificationService(IMessageSender sender)
    {
        _sender = sender;
    }

    public void Notify(string message)
    {
        _sender.Send(message);
    }
}

NotificationService, dışarıdan IMessageSender bekliyor. Hangi sınıf verilecek (Email mi SMS mi vs.), bu sınıfı ilgilendirmiyor. Bu da daha sade, daha test edilebilir bir yapı sağlıyor.

Bağımlılıkların Kayıt Edilmesi (Program.cs)

Aşağıdaki örnekte EmailSender sınıfı kullanılıyor. İleride SmsSender’a geçmek istersek tek yapmamız gereken kayıt satırını değiştirmek.

// Program.cs

var builder = WebApplication.CreateBuilder(args);

// Dependency injection registers
builder.Services.AddTransient<IMessageSender, EmailSender>();
builder.Services.AddTransient<NotificationService>();

builder.Services.AddControllers();

var app = builder.Build();

app.UseHttpsRedirection();
app.MapControllers();

app.Run();

Not: AddTransient, her istek için yeni bir instance oluşturur.

API Controller

Artık servisimizi bir controller üzerinden kullanabiliriz. Dependency Injection sayesinde, NotificationService de otomatik olarak dışarıdan enjekte edilecek. Çünkü daha önce builder.Services.AddTransient<NotificationService>() satırıyla DI container’a ekledik.

// Controllers/NotificationController.cs

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class NotificationController : ControllerBase
{
    private readonly NotificationService _notificationService;

    public NotificationController(NotificationService notificationService)
    {
        _notificationService = notificationService;
    }

    [HttpPost]
    public IActionResult Send([FromBody] string message)
    {
        _notificationService.Notify(message);
        return Ok("Notification sent.");
    }
}

ASP.NET Core projelerinde Dependency Injection yapısını kullanmak son derece doğal ve basittir çünkü altyapı bunu desteklemek üzere inşa edilmiştir. Bu sayede bağımlılık yönetimi kolaylaşır, kod test edilebilirliği artar ve uygulama yapısı daha esnek hale gelir.

Bu örnekte olduğu gibi bir interface üzerinden hareket ederek uygulama içinde hangi somut sınıfın kullanılacağına tek bir noktadan karar veriyoruz. Bu da uygulamayı daha sürdürülebilir ve geliştirilebilir hale getiriyor.

🔗 GitHub – Dependency Injection

Projeyi klonlayarak doğrudan kendi bilgisayarınızda test edebilirsiniz.

Kaynaklar

🔗 .NET Dependency Injection

🔗 Dependency Injection in ASP.NET Core

🔗 ASP.NET Core - Dependency Lifetimes


Sonraki Yazı
Middleware Nedir? ASP.NET Core ile Middleware Geliştirme 101