Dependency Injection daha sürdürülebilir ve bakımı kolay kod yazmanıza yardımcı olan en bilinen tekniklerden biridir. .NET Core ile birlikte Dependency Injection konusunda kapsamlı bir destek gelmişti.
.NET Framework ile DI uygulamak genellikle harici kütüphaneler aracılığıyla yapılırdı. Örneğin Ninject, Autofac veya Unity gibi DI container kullanarak bağımlılıkları yöneten kodlar yazılırdı. Bu yapılar ekstra konfigürasyon gerektirir ve bağımlılıkların nasıl enjekte edileceğine dair manuel yönetim gerektirirdi.
Dependency Injection ihtiyacı nereden doğdu? Sorun neydi ve nasıl bir çözüm geldi?
Bağımlılık Problemi
Küçük bir gereksinim için birçok yerde değişiklik yapmış olabilirsiniz veya varolan bir projede “refactor” işlemi yaparken de zorlanıyor olabilirsiniz. Bunların yanında bir de unit test yazarken de problem yaşıyor olabilirsiniz. Zira bağımlılık sorunu yazılımın temel problemlerinden birisidir.
Bir uygulamanın bileşenleri birbirine fazla sıkı bağlı olduğunda ortaya çıkan tipik bir kod hastalığıdır.
Bir örnek ile basitçe izah edeyim. Bu örnekte iki sınıfımız var: Product ve ProductFeature. Ürünler, ProductManager sınıfı tarafından yönetiliyor. Kod şu şekilde uyarlanabilir:
using System;
using System.Collections.Generic;
namespace ProductManagement
{
public class Product
{
public string ProductId { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public List<ProductFeature> Features { get; set; }
public Product()
{
Features = new List<ProductFeature>();
}
}
public class ProductFeature
{
public string FeatureId { get; set; }
public string Description { get; set; }
}
}ProductManager.cs ise böyle:
using System.Threading.Tasks;
namespace ProductManagement
{
public class ProductManager
{
public async Task<string> HandleAsync(Product product)
{
var productSender = new ProductSender();
return await productSender.SendAsync(product);
}
}
}ProductManager sınıfı, HandleAsync() metodunu uygular. Bu metod, ürün bilgisini başka bir servise göndermek için kullanılır. Burada gönderme işlemi ProductSender sınıfına bırakılmıştır.
Şunun da altını çizelim: Her ProductManager örneği oluşturduğunuzda yeni bir ProductSender oluşturulur. Eğer ProductSender içinde ağır bir işlem varsa (örneğin HttpClient, dosya erişimi, DB context gibi) gereksiz nesne oluşturma maliyeti doğar. ProductManager, ProductSender’ın nasıl oluşturulacağını bilmek zorundadır çünkü manuel olarak yönetiyorsunuz.
Bu da ProductSender.cs dosyamız olsun:
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace ProductManagement
{
public class ProductSender
{
private static readonly HttpClient httpClient = new HttpClient();
public async Task<string> SendAsync(Product product)
{
var jsonProduct = JsonSerializer.Serialize<Product>(product);
var stringContent = new StringContent(jsonProduct, Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync("https://recepserit.com/myendpoint", stringContent);
return await response.Content.ReadAsStringAsync();
}
}
}Burada SendAsync() metodu, Product nesnesini JSON formatına çevirir ve bir POST isteğiyle bir service gönderir. Bu örnekte görebileceğiniz gibi, ProductManager doğrudan ProductSender sınıfına bağımlıdır. Bu durum ileride test veya değişiklik yapmak istediğinizde kodu zorlaştırır.
Peki ya ürün gönderme yöntemini değiştirmek zorunda kalırsak ne olur?
Örneğin ürünleri e-posta ile göndermek veya HTTP yerine TCP servisi üzerinden veri kullanan başka bir servise göndermek isteyelim. ProductManager sınıfı için unit test metodu yazmak ne kadar kolay olur sizce?
Sorunumuz tam olarak bu: ProductManager sınıfı doğrudan ProductSender’a bağımlı olduğu için, birden fazla gönderici türünü desteklemek için her iki sınıfta da değişiklik yapmak zorunda kalırsınız. Alt seviye bileşendeki (ProductSender) değişiklikler, üst seviye bileşeni (ProductManager) etkileyebilir.
Bu sorunu özetlemek ve ihtiyacın nasıl oluştuğunu anlamak için biraz daha başlığımızı genişletelim.
DI İhtiyacı Nereden Doğdu?
Dependency Injection (DI) tekniği önceki nesil .NET uygulamalarında yokken, bağımlılıklar genellikle new() anahtar kelimesiyle doğrudan oluşturuluyordu veya static olarak tanımlamamız gerekirdi. Bu yaklaşım nesnelerin doğrudan sınıfların içinde örneklendiği ve her nesne için bağımlılıkların elle yönetildiği eski yöntemdi.
Bunu biraz DbContext üzerinden örnekleştirelim. Bilindiği gibi bu sınıf genellikle veritabanına erişim sağlayan ve veritabanı ile ilgili işlemleri yöneten bir sınıftır. Eğer DbContext sürekli olarak new() ile yeniden türetiliyorsa bu projede karmaşıklığı ve yönetimi arttıracaktır.
public class ProductService
{
public static DbContext _staticContext = new DbContext();
private readonly DbContext _dbContext;
public MyService()
{
// new() oluşturuluyor
_dbContext = new MyDbContext();
}
public void GetList()
{
var data = _dbContext.Entities.ToList();
var list = _staticContext.Product.ToList();
}
}
Buradaki new DbContext() çağrısı, her ProductService nesnesi yaratıldığında yeni bir DbContext örneği oluşturulmasını sağlar. Ancak bu yaklaşım proje büyüdükçe problemlere yol açacaktır. Her sınıfın DbContext’i manuel olarak yönetmesi gerekecek ve bu da projelerde bağımlılıkları yönetmeyi zorlaştırır.
static DbContext ise direkt olarak kullanılabilir ama bu ciddi bir sorun oluşturacaktır. DbContext nesnesi, multi-threading ortamlarında thread-safe değildir. Eğer aynı DbContext nesnesi birden fazla iş parçacığı tarafından kullanılırsa exception veya crash sorunları yaşanabilir (yıllar önce tecrübe edildi). Çünkü EF, DbContext üzerinde eşzamanlı işlemler yapmaya çalışırken bir iş parçacığının diğerinin durumunu değiştirmesi sebebiyle çakışmalar olabilir.
DbContext’in yaşam süresi oldukça kısadır ve her işlem bittiğinde DbContext otomatik olarak Dispose edilmelidir. Ancak, DbContext’i static yapmak, onu sürekli bir yaşam süresine sokar ve bu da memory leak sorunlarına yol açabilir. Çünkü bu nesne bir kere oluşturulduğunda uygulama süresince var olur ve gereksiz yere bellekte kalır.
Dependency Inversion Prensibi

Görsel: System Softeware Design
Üst seviye modüller, alt seviye modüllere bağımlı olmamalıdır. İş mantığı, doğrudan veri erişim ya da gönderim detaylarını bilmemeli. Her ikisi de soyutlamalara bağımlı olmalıdır. Ortak bir interface veya abstract class kullanılmalı. Örneğin ProductManager içinde kullanılan ProductSender new yaparak türetmemeli, IProductSender’a bağlı olmalı.
Soyutlamalar (abstract sınıflar) detaylara bağımlı olmamalıdır. Interface alt sınıfın iç detaylarına göre değişmemeli. Detaylar soyutlamalara bağımlı olmalıdır. Örneğin “IProductSender” HttpClient veya SMTP sınıflarını bilmez. Örneğin HttpProductSender isimli bir sınıf oluşturursanız buna bağlı olarak “IProductSender” verilmeli.

Bunların hepsi biraz soyut görünebilir. Bu makalede bağımlılıkların azaltılması için kullanılan teknikler hakkında örnekler sunacağım. Buradaki terminoloji her programlama dili için geçerlidir. Bu terimleri ayrıca araştırmanızı öneririm:
- Inversion of Control – IoC: Bağımlılıkların dışarıdan yönetilmesi ve enjekte edilmesi yoluyla bağımlılıkları gevşetir (loose coupling), böylece daha esnek, test edilebilir ve sürdürülebilir bir yazılım mimarisi oluşturulmasına yardımcı olur. Başka bir deyişle bir bileşenin ihtiyaç duyduğu bağımlılıklar, ona dışarıdan enjekte edilir. Bu sayede bileşenler, kendi bağımlılıklarını oluşturmak veya yönetmek yerine, dışarıdan sağlanan soyutlamalara (interface veya abstract class gibi) dayanır.
- Dependency Injection – DI: IoC ilkesini uygulamak için kullanılır. DI, bir sınıfın ihtiyaç duyduğu bağımlılıkları (IProductService gibi) dışarıdan almasını sağlar. Bu sayede üzerinde çalıştığınız sınıflar kendi bağımlılıklarını oluşturmak zorunda kalmaz, bunun yerine bağımlılıklar dışarıdan “enjekte” edilir. Nereden enjekte ediyor? IoC Container aracılığıyla elbette.
- IoC Container: Temel olarak, sınıfların bağımlılıklarını merkezi bir yerden yönetir ve bu bağımlılıkları otomatik olarak enjekte eder.
Dependency Injection
Yukarıda da belirtildiği gibi, Dependency Injection’ın amacı, bir bileşene ihtiyaç duyduğu bağımlılıkları dışarıdan sağlamak ve bileşenin kendi bağımlılıklarını kendi içinde oluşturmamasını sağlamaktır.
Bunu uygulamanın birkaç yolu vardır. Yani alt seviye bileşenlerin örneklerini siz oluşturup, üst seviye bileşenlere enjekte edebilirsiniz.
Constructor Injection: Burada bağımlılık class constructor üzerinden verilir. En güvenli ve kullanılan yöntemdir.
public class ProductManager
{
private readonly IProductSender _sender;
public ProductManager(IProductSender sender)
{
_sender = sender;
}
}Method Injection: Bağımlılık belirli bir metot çağrısına parametre olarak geçirilir. Size esneklik sağlar.
public class ProductManager
{
public void Transmit(Product product, IProductSender sender)
{
sender.Send(product);
}
}Property Injection: Burada bağımlılık, sınıfın bir özelliğine dışarıdan atanır.
public class ProductManager
{
public IProductSender Sender { get; set; }
}.NET Üzerinde DI
Dependency Injection (DI) ile, yukarıda bahsedilen üç yöntemden (constructor, method, property injection) birini veya birkaçını kullanarak kendiniz de uygulayabilirsiniz. .NET Core ile birlikte bunu çok daha kolay hale getiren yerleşik bir IoC (Inversion of Control) Container bulunur. Ninject, Unity gibi ek paketler kurmadan framework desteğiyle bunu rahatlıkla yapabiliyorsunuz.
IoC Container, Dependency Injection işlemlerini otomatik olarak yöneten mekanizmadır. Yani, bağımlılıkları oluşturma, eşleştirme, çözümleme ve yaşam döngüsünü yönetme işlerinden sizin yerinize sorumludur.
Registration (Kayıt): Container, bir bağımlılık istendiğinde hangi sınıfın oluşturulacağını bilmelidir. Bu nedenle türler ile sınıflar arasında bir eşleme yapılır.
services.AddScoped<IProductSender, HttpProductSender>();Böylece IoC Container, IProductSender istendiğinde HttpProductSender nesnesini oluşturacağını bilir.
Resolution (Çözümleme): Bu özellik, IoC Container için gerekli nesneleri otomatik olarak oluşturup enjekte etmesini sağlar. Yani artık new kullanmanız gerekmez, container sizin yerinize gerekli bağımlılığı bulur ve sağlar.
public class ProductManager
{
private readonly IProductSender _sender;
public ProductManager(IProductSender sender)
{
// IoC container tarafından otomatik verilir
_sender = sender;
}
}Disposition (Yaşam Döngüsü Yönetimi): IoC Container, oluşturduğu nesnelerin ne kadar süreyle yaşaması gerektiğini de yönetir. Bu yaşam döngüsü kayıt aşamasında belirlenir:
- AddSingleton: Uygulama boyunca tek bir örnek
- AddScoped: Her istek (request) başına tek örnek
- AddTransient: Her kullanımda yeni örnek
Buradaki görsel ile üçünün arasındaki farkı daha iyi kavrayabilirsiniz.

Küçük bir not paylaşalım, bu mülakat sorunuz olabilir. Arasındaki farklılıkları iyi bilmeniz projedeki teknik becerinizi de gösterecektir.
Microsoft burada DI konusunu güzel bir şekilde açıklamış: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection
.NET’te bulunan yerleşik IoC Container, IServiceProvider arayüzünü uygular. Container tarafından yönetilen tüm bağımlılıklara “service” (servis) der.
Bu servisler ikiye ayrılır:
Framework Servisleri: .NET içinde bulunan ve framework tarafından sağlanan servislerdir. IApplicationBuilder, IConfiguration, ILoggerFactory gibi.
Application Servisleri: Kendi uygulamanızda tanımladığınız özel servislerdir.
IoC Container bunları tanımaz, bu yüzden manuel olarak kaydetmeniz gerekir:
services.AddScoped<IProductSender, HttpProductSender>();
services.AddScoped<ProductManager>();Singleton, Scoped ve Transient Servisleri
Singleton servisler: Bir servis singleton olarak kaydedildiğinde, DI container yaşam süresi boyunca yalnızca bir kez oluşturulur. Yani uygulamanın tüm ömrü anlamına gelir.
Servis ilk kez istendiğinde container onu oluşturur, sonra aynı örnek her yere enjekte edilir. Bu örnekte container yaşam süresi boyunca kullanılabilir kalır, dolayısıyla sık sık dispose edilmez veya garbage collect edilmez.
- Sık kullanılan servislerde, her istek için yeni nesne oluşturmaktan kaçınarak performans kazancı sağlar. Örneğin database repository için çokça tercih edilir.
- Nesne oluşturulması maliyetliyse, yalnızca bir kez oluşturmak verimliliği artırır.
- Thread-safety dikkate alınmalıdır, çünkü aynı nesne birden çok istek tarafından eşzamanlı kullanılabilir.
Başka bir örnek memory cache için singleton daha uygundur. Ancak dikkat edilmeli, tek örnekte çok fazla veri tutuluyorsa memory leak oluşabilir.
Çok az kullanılan ama büyük bellek kullanan servisler için singleton uygun değildir.
Transient servisler: Bir servis transient olarak kaydedildiğinde, container her çözümleme (resolve) işleminde yeni bir örnek oluşturur. Yani bu servisi kullanan her sınıf kendine özgü bir instance alır.
Bu yüzden her sınıfın kendi örneği olduğundan, nesne iç durumu (state) güvenli bir şekilde değiştirilebilir. Başka bir thread ile çakışma durumu olmaz.
Kullanımı ve avantajları böyledir:
- Thread-safe olmayan servisler için uygundur.
- Her çözümlemede yeni bir nesne oluşturulduğu için küçük bir performans maliyeti olabilir.
- Çok sık oluşturulan bu nesneler Garbage Collector tarafından temizlenmelidir.
- En güvenli yaşam süresidir çünkü hiçbir örnek paylaşılmaz.
Scoped servisler: Transient ile singleton arasında bir orta yoldur. Bir scope (örneğin bir HTTP isteği) boyunca yaşar. Örneğin her gelen HTTP isteği için bir scope oluşturulur. Scoped servis, o istek boyunca bir kez oluşturulur ve istek süresince paylaşılır. Yani aynı request içinde birden fazla sınıf bu servisi kullanıyorsa, aynı örnek paylaşılır.
Kullanım avantajlar şöyledir:
- Container her istek için yeni bir örnek oluşturur.
- Aynı istek içinde sıralı olarak kullanıldığı için thread-safe olması gerekmez.
- Bir istek boyunca ortak bağımlılığa ihtiyaç duyuluyorsa uygundur.
Çok güzel bir örnek var. EF Core DbContext varsayılan olarak scoped olarak kayıtlıdır. Böylece bir istek boyunca yapılan tüm işlemler aynı DbContext üzerinden yapılır.
Captive Dependency (Esir Bağımlılıklar) Sorunu
Bağımlılıkların yaşam süresi seçilirken dikkat edilmesi gereken önemli bir konu da “Captive Dependency” problemidir. Kural şudur, bir servis kendisinden daha kısa yaşam süresine sahip bir servise bağımlı olmamalıdır.
Örneğin bir singleton servis, transient servise bağımlı olmamalıdır. Aksi halde, transient servis singleton tarafından “yakalanır” ve uygulama süresince yaşamaya devam eder. Bu da memory leak, thread safety sorunlarına sebebiyet verir.
Singleton bir servisin scoped servise bağımlı olması tehlikeli bir durumdur. Çünkü scoped servis, scope sona erdiğinde dispose edilir ama singleton onu hâlâ kullanmaya çalışabilir. Bu da runtime hatalarına neden olur.
ProductManager örneğimiz üzerinden bu sorunu anlatalım.
using Microsoft.Extensions.DependencyInjection;
using DependencyExample;
var services = new ServiceCollection();
services.AddScoped<IProductSender, ProductSender>();
services.AddSingleton<ProductManager>();
var provider = services.BuildServiceProvider();ProductSender Scoped olarak tanımlandı yani her HTTP isteğinde (veya scope) yeni bir örnek oluşturulması gerekir. Ama ProductManager Singleton olarak kayıtlı. Uygulama boyunca tek örnek var. Bu durumda ProductManager, ilk scope’ta ProductSender örneğini yakalar. Scope sona erdiğinde ProductSender dispose edilir ama ProductManager hala ona referans tutar. Dolayısıyla sonraki scope’larda geçersiz bir nesneye erişmeye çalışır. Bu da size runtime exception çıkarabilir.
Sonuç
Bu makaleyle birlikte Dependency Injection design pattern ile ilgili bazı kavramları netleştirmeye ve bunu destekleyen .NET Core altyapısına odaklanmaya çalıştık.
Genel tanımlardan başlayarak, bir .NET Core uygulamasının nasıl yapılandırılacağını, yerleşik IoC (Inversion of Control) Container sisteminin nasıl kullanılacağını ve framework servislerinin nasıl kaydedileceğini öğrendik.
Bunu bir console application veya ASP.NET projesiyle daha iyi pekiştirebiliriz. Diğer makalelerde farklı örnekler yaparak anlatmaya çalışacağım. Umarım konu anlaşılmıştır.



Yorum bırakın