ASP.NET Core Dependency Injection Nedir? IoC, DIP, DI Rehberi

ASP.NET Core Dependency Injection, modern .NET uygulamalarının temel taşlarından biridir ve DI’yi anlamak için önce neden var olduğunu anlamak gerekir.
Bir sınıfta yapılan değişikliğin diğer sınıfları etkilemesini önlemek, unit test yazılabilirliği artırmak ve veritabanı değiştiğinde tüm projeyi ya da büyük bir kısmını yeniden yazma zorunluluğundan kurtulmak istiyorsanız, bu yazı tam size göre.
IoC (Inversion of Control), DIP (Dependency Inversion Principle) ve Dependency Injection (DI) kavramları bu sorunlara bir takım çözümler sunar. SOLID prensiplerinin temel taşlarından biri olan bu kavramları iyi anlamak gerekmektedir.
Bu yazıda basit bir tightly coupled sınıftan başlayarak loosely coupled, test edilebilir ve genişletilebilir bir ASP.NET Core Dependency Injection mimarisi inşa edeceğiz.
IoC, DIP ve DI Arasındaki Fark
Bu üç kavram çoğu zaman birbiriyle karıştırılır. Bunu bir tabloyla netleştirelim.
| Kavram | Tür | Açıklama |
|---|---|---|
| Inversion of Control (IoC) | Prensip | Kontrolün tersine çevrilmesi, bir sınıf bağımlılıklarını kendisi oluşturmaz |
| Dependency Inversion Principle (DIP) | Prensip | Yüksek seviyeli modüller düşük seviyeli modüller yerine soyutlamalara bağlı olmalıdır |
| Dependency Injection (DI) | Pattern | IoC prensibini uygulamanın en yaygın yöntemi, bağımlılıklar dışarıdan enjekte edilir |
| IoC Container | Framework | DI işlemini otomatikleştiren araçlar (Autofac, Ninject, Unity, ASP.NET Core built-in) |
Basitçe özetlemek gerekirse IoC bir düşünce biçimidir. DIP bu düşüncenin temel kuralını tanımlar. Dependency Injection ise bu kuralı uygulamak için kullanılan bir tekniktir. IoC Container ise uygulamadaki bağımlılıkları otomatik olarak oluşturup yöneten araçtır.
Tightly Coupled Sınıf Nedir?
Tightly Coupled (sıkı sıkıya bağlı), bir sınıfın ihtiyaç duyduğu bağımlılıkları kendi içinde new anahtar kelimesiyle oluşturduğu yapıdır. Bu durum aşağıdaki sorunlara yol açar.
- Test edilemezlik: Gerçek veritabanı veya servis olmadan unit test yazılamaz.
- Değişime kapalılık: Bir implementasyonu değiştirmek için kullanan sınıfı da değiştirmek gerekir.
- Tekrar kullanılamayan kod: Sınıf, sadece belirli bir implementasyonla çalışır.
public class OrderService
{
private readonly SqlOrderRepository _repository = new SqlOrderRepository();
private readonly SmtpEmailService _emailService = new SmtpEmailService();
public void PlaceOrder(Order order)
{
_repository.Save(order);
_emailService.Send(order.CustomerEmail, "Order placed!");
}
}
public class SqlOrderRepository
{
public void Save(Order order)
{
Console.WriteLine($"Saved to SQL: {order.Id}");
}
}
public class SmtpEmailService
{
public void Send(string email, string message)
{
Console.WriteLine($"Sent with SMTP: {email}");
}
}
Yukarıdaki kodun problemlerini ele alacak olursak OrderService, SqlOrderRepository‘e sıkıca bağlı. MongoDB’ye geçmek istersek OrderService‘i de değiştirmek zorundayız. SmtpEmailService yerine SendGrid kullanmak istesek yine OrderService‘e dokunmak gerekir. OrderService için unit test yazılamaz çünkü gerçek bir SQL bağlantısına ihtiyaç duyar.
Factory Pattern ile IoC Implementasyonu
İlk adımda IoC prensibini Factory Pattern kullanarak uygulayalım. Buradaki fikir OrderService bağımlılıklarını new kullanarak oluşturmak yerine bir fabrikadan istiyor.
public static class RepositoryFactory
{
public static SqlOrderRepository CreateRepository()
{
return new SqlOrderRepository();
}
}
public static class EmailServiceFactory
{
public static SmtpEmailService CreateEmailService()
{
return new SmtpEmailService();
}
}
public class OrderService
{
private readonly SqlOrderRepository _repository;
private readonly SmtpEmailService _emailService;
public OrderService()
{
_repository = RepositoryFactory.CreateRepository();
_emailService = EmailServiceFactory.CreateEmailService();
}
public void PlaceOrder(Order order)
{
_repository.Save(order);
_emailService.Send(order.CustomerEmail, "Order placed!");
}
}
Bu örnekte nesneler artık sınıf içinde new ile oluşturulmuyor. Bunun yerine RepositoryFactory ve EmailServiceFactory sınıfları kullanılıyor. Böylece nesnelerin nasıl oluşturulacağı bilgisi OrderService sınıfından ayrılmış oluyor.
Ancak sınıf hala SqlOrderRepository ve SmtpEmailService gibi somut sınıflara doğrudan bağlı. Bu nedenle farklı bir repository veya e-posta servisi kullanmak istersek kodu değiştirmemiz gerekecek; çünkü henüz bir soyutlama (interface gibi) yapmadık. Bu da esnekliği ve test edilebilirliği sınırlamaya devam ediyor.
Abstraction Oluşturarak DIP Implementasyonu
SOLID’in “D” harfi olan Dependency Inversion Principle şunu söyler:
Yüksek seviyeli modüller (
OrderService), düşük seviyeli modüllere (SqlOrderRepository) değil; soyutlamalara (IOrderRepository) bağlı olmalıdır. Bu adımda interface’ler yani soyutlamalar oluşturacağız.
public interface IOrderRepository
{
void Save(Order order);
}
public interface IEmailService
{
void Send(string email, string message);
}
public class SqlOrderRepository : IOrderRepository
{
public void Save(Order order)
{
Console.WriteLine($"Saved to SQL: {order.Id}");
}
}
public class MongoOrderRepository : IOrderRepository
{
public void Save(Order order)
{
Console.WriteLine($"Saved to MongoDB: {order.Id}");
}
}
public class SmtpEmailService : IEmailService
{
public void Send(string email, string message)
{
Console.WriteLine($"Sent with SMTP: {email}");
}
}
public class SendGridEmailService : IEmailService
{
public void Send(string email, string message)
{
Console.WriteLine($"Sent with SendGrid: {email}");
}
}
public class OrderService
{
private readonly IOrderRepository _repository;
private readonly IEmailService _emailService;
public OrderService()
{
_repository = new SqlOrderRepository();
_emailService = new SmtpEmailService();
}
public void PlaceOrder(Order order)
{
_repository.Save(order);
_emailService.Send(order.CustomerEmail, "Order placed!");
}
}
Bu versiyonda OrderService artık somut sınıflar yerine IOrderRepository ve IEmailService gibi arayüzlere bağlıdır. Bu sayede veri kaydetme veya e-posta gönderme işleminin hangi teknoloji ile yapıldığı OrderService için önemli değildir. Örneğin SqlOrderRepository yerine MongoOrderRepository kullanmak istersek, OrderService kodunu değiştirmeden farklı bir implementasyon yazabiliriz.
Ancak bağımlılıkların hangi sınıf olacağı hala OrderService içinde belirlenmektedir. Constructor içinde new kullanıldığı için nesneler yine sınıfın içinde oluşturulmaktadır. Bu durum test yazarken sahte (mock) nesneler kullanmayı zorlaştırır ve bağımlılıkların tamamen dışarıdan yönetilmesini engeller.
Dependency Injection (DI) Implementasyonu
Dependency Injection, bağımlılıkların sınıfa dışarıdan verilmesi prensibidir. En yaygın yöntem Constructor Injection‘dır.
public class OrderService
{
private readonly IOrderRepository _repository;
private readonly IEmailService _emailService;
public OrderService(IOrderRepository repository, IEmailService emailService)
{
_repository = repository;
_emailService = emailService;
}
public void PlaceOrder(Order order)
{
_repository.Save(order);
_emailService.Send(order.CustomerEmail, "Order placed!");
}
}
Artık OrderService tamamen bağımsız. Aşağıda DI’nin üç kullanım yöntemi verilmiştir.
// Constructor Injection
public class OrderService
{
public OrderService(IOrderRepository repository) { ... }
}
// Property Injection
public class OrderService
{
public IOrderRepository Repository { get; set; }
}
// Method Injection
public class OrderService
{
public void PlaceOrder(Order order, IOrderRepository repository) { ... }
}
Yukarıdaki tüm adımları uyguladıktan sonra loosely coupled (gevşek bağlı) bir yapıya kavuşmuş olduk.
| Özellik | Tightly Coupled | Loosely Coupled |
|---|---|---|
| Bağımlılık oluşturma | Sınıf içinde new | Dışarıdan inject edilir |
| Interface kullanımı | Yok | Var |
| Unit test | Zor | Mock |
| Değişime açıklık | Kapalı | Açık |
| Yeniden kullanım | Düşük | Kullanılabilir |
| SOLID uyumu | Yok | Uyumlu |
ASP.NET Core Built-in DI Container Nasıl Kullanılır?
ASP.NET Core Dependency Injection için Microsoft.Extensions.DependencyInjection kütüphanesiyle bir built-in IoC Container sunar. Autofac veya Ninject gibi harici kütüphanelere gerek kalmadan DI kullanılabilir.
Şimdi konuyu örnek bir ASP.NET Core Web API projesiyle pekiştirelim.
dotnet new webapi --name OrderApi
Öncesinde proje genelinde kullanacağımız Order modelini Models klasörüne oluşturalım.
public class Order
{
public int Id { get; set; }
public string CustomerEmail { get; set; } = string.Empty;
public string ProductName { get; set; } = string.Empty;
public decimal Amount { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
public record CreateOrderRequest(string CustomerEmail, string ProductName, decimal Amount);
Sonrasında Services isimli bir klasör oluşturarak içerisine abstraction yapımızı sağlayacak arayüzleri oluşturalım.
public interface IOrderRepository
{
void Save(Order order);
Order? GetById(int id);
IEnumerable<Order> GetAll();
}
public interface IEmailService
{
void Send(string toEmail, string subject, string body);
}
public interface IOrderValidator
{
void Validate(string customerEmail, string productName, decimal amount);
}
public interface IOrderService
{
Order PlaceOrder(string customerEmail, string productName, decimal amount);
IEnumerable<Order> GetOrders();
}
Oluşturduğumuz arayüzleri implemente ederek devam edelim.
public class SqlOrderRepository : IOrderRepository
{
private readonly Guid _instanceId = Guid.NewGuid();
private readonly ILogger<SqlOrderRepository> _logger;
private readonly List<Order> _db = [];
public SqlOrderRepository(ILogger<SqlOrderRepository> logger)
{
_logger = logger;
_logger.LogInformation("SqlOrderRepository instance created. Instance ID: {InstanceId}", _instanceId);
}
public void Save(Order order)
{
order.Id = _db.Count + 1;
_db.Add(order);
_logger.LogInformation("Order saved using SqlOrderRepository instance. Instance ID: {InstanceId}, Order ID: {OrderId}", _instanceId, order.Id);
}
public Order? GetById(int id)
{
var order = _db.FirstOrDefault(o => o.Id == id);
_logger.LogInformation("Order retrieving using SqlOrderRepository instance. Instance ID: {InstanceId}, Order ID: {OrderId}", _instanceId, order.Id);
return order;
}
public IEnumerable<Order> GetAll()
{
_logger.LogInformation("All orders retrieved using SqlOrderRepository instance. Instance ID: {InstanceId}", _instanceId);
return _db;
}
}
public class SmtpEmailService : IEmailService
{
private readonly Guid _instanceId = Guid.NewGuid();
private readonly ILogger<SmtpEmailService> _logger;
public SmtpEmailService(ILogger<SmtpEmailService> logger)
{
_logger = logger;
_logger.LogInformation("SmtpEmailService instance created. Instance ID: {InstanceId}", _instanceId);
}
public void Send(string toEmail, string subject, string body)
{
_logger.LogInformation("Sending email using SmtpEmailService instance. Instance ID: {InstanceId}, To: {ToEmail}, Subject: {Subject}", _instanceId, toEmail, subject);
}
}
public class OrderValidator : IOrderValidator
{
private readonly Guid _instanceId = Guid.NewGuid();
private readonly ILogger<OrderValidator> _logger;
public OrderValidator(ILogger<OrderValidator> logger)
{
_logger = logger;
_logger.LogInformation("OrderValidator instance created. Instance ID: {InstanceId}", _instanceId);
}
public void Validate(string customerEmail, string productName, decimal amount)
{
_logger.LogInformation("Validating order using OrderValidator instance. Instance ID: {InstanceId}", _instanceId);
if (string.IsNullOrWhiteSpace(customerEmail))
throw new ArgumentException("Customer email cannot be empty.");
if (string.IsNullOrWhiteSpace(productName))
throw new ArgumentException("Product name cannot be empty.");
if (amount <= 0)
throw new ArgumentException("Order amount must be greater than zero.");
}
}
Dikkat ettiyseniz her sınıf kendi _instanceId ve _logger implementasyonuna sahip, bu yazının ilerleyen kısımlarında bize sınıfların yaşam döngüleri hakkında bilgi sağlayacak. Şimdi iş kurallarımızı koşturacağımız OrderService sınıfımızı oluşturarak devam edelim.
public class OrderService : IOrderService
{
private readonly Guid _instanceId = Guid.NewGuid();
private readonly ILogger<OrderService> _logger;
private readonly IOrderRepository _repository;
private readonly IEmailService _emailService;
private readonly IOrderValidator _orderValidator;
public OrderService(IOrderRepository repository, IEmailService emailService, IOrderValidator orderValidator, ILogger<OrderService> logger)
{
_repository = repository;
_emailService = emailService;
_orderValidator = orderValidator;
_logger = logger;
_logger.LogInformation("OrderService instance created. Instance ID: {InstanceId}", _instanceId);
}
public Order PlaceOrder(string customerEmail, string productName, decimal amount)
{
_orderValidator.Validate(customerEmail, productName, amount);
var order = new Order
{
CustomerEmail = customerEmail,
ProductName = productName,
Amount = amount
};
_repository.Save(order);
_emailService.Send(
customerEmail,
"Order placed",
$"Your order for {productName} has been placed successfully. Total amount: {amount:C}."
);
_logger.LogInformation("Order placed. Instance ID: {InstanceId}, Order ID: {OrderId}", _instanceId, order.Id);
return order;
}
public IEnumerable<Order> GetOrders()
{
_logger.LogInformation("Retrieving orders. Instance ID: {InstanceId}", _instanceId);
return _repository.GetAll();
}
}
Yukarıda constructor bloğunda tüm bağımlılıkların dışarıdan inject edildiği görülmektedir. Soyutlama işlemi yaptığımız için somut sınıf kimdir bu noktada bilmiyoruz. Sipariş ile ilgili işlemlerimizi dışarıya açacağımız controller sınıfımızı yazalım.
[ApiController, Route("api/[controller]")]
public class OrderController : ControllerBase
{
private readonly IOrderService _orderService;
private readonly IOrderValidator _orderValidator;
public OrderController(IOrderService orderService, IOrderValidator orderValidator)
{
_orderService = orderService;
_orderValidator = orderValidator;
}
[HttpPost]
public IActionResult Create([FromBody] CreateOrderRequest request)
{
_orderValidator.Validate(request.CustomerEmail, request.ProductName, request.Amount);
var order = _orderService.PlaceOrder(
request.CustomerEmail,
request.ProductName,
request.Amount
);
return Ok(order);
}
[HttpGet]
public IActionResult GetAll()
{
var orders = _orderService.GetOrders();
return Ok(orders);
}
}
IOrderService sınıfını da soyutladıktan sonra DI Container kayıtlarına geçebiliriz.
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddOpenApi();
builder.Services.AddControllers();
// Register the services with the dependency injection container
builder.Services.AddSingleton<IEmailService, SmtpEmailService>();
builder.Services.AddTransient<IOrderValidator, OrderValidator>();
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.AddScoped<IOrderService, OrderService>();
// Enable validation of service scopes and dependencies at build time
builder.Host.UseDefaultServiceProvider(options =>
{
options.ValidateScopes = true;
options.ValidateOnBuild = true;
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
ASP.NET Core uygulaması başlatılırken uygulamanın oluşturulması (Build) ve uygulamanın çalıştırılması (Run) olmak üzere iki ana aşamadan geçer.
İlk bölümde WebApplication.CreateBuilder(args) ile uygulamanın yapılandırma süreci başlar. Bu aşamada servisler Dependency Injection container içerisine kaydedilir. Framework’ün kendi servisleri (örneğin AddControllers, AddOpenApi) eklenir. Sonrasında uygulamaya ait servisler tanımlanır. Bu bizim örneğimizde IEmailService, IOrderValidator, IOrderRepository ve IOrderService arayüzleridir ve implementasyonlarıyla birlikte AddScoped, AddSingleton ve AddTransient kullanılarak container’a kaydedilmiştir. Bu kayıtlar sayesinde ASP.NET Core ihtiyaç duyulan sınıfları otomatik olarak oluşturabilir ve bağımlılıklarını enjekte edebilir.
builder.Build() çağrısı yapıldığında uygulamanın çalışma zamanı pipeline’ı oluşturulur. Bu noktadan sonra HTTP isteği geldiğinde nasıl işleneceğini belirleyen middleware ve uygulama ayarları yapılır. Örneğin geliştirme ortamında OpenAPI endpoint’inin açılması, HTTPS yönlendirmesi ve controller route’larının tanımlanması bu aşamada gerçekleşir.
Son olarak app.Run() çağrısı ile uygulama başlatılır ve HTTP isteklerini dinlemeye başlar. Bu noktadan itibaren ASP.NET Core gelen her isteği tanımlanan middleware pipeline’ından geçirerek ilgili controller’a yönlendirir.
Uygulamamızı ayağa kaldırıp sipariş oluşturmaya çalıştığımızda konsolda aşağıdaki çıktıyı göreceğiz.
info: SqlOrderRepository instance created. Instance ID: cb593ecc-7070-4c50-930a-4f6df366373e
info: SmtpEmailService instance created. Instance ID: 2d0c9d88-93d0-4af8-94ff-b08d6c3d3009
info: OrderValidator instance created. Instance ID: 743e18a4-8979-4112-9c2a-f10ba543a905
info: OrderService instance created. Instance ID: 1239455c-add7-4efd-8448-ab5a948ea62a
info: OrderValidator instance created. Instance ID: 891622bc-f6d6-4a2f-8322-70af9cce574b
info: Validating order using OrderValidator instance. Instance ID: 891622bc-f6d6-4a2f-8322-70af9cce574b
info: Validating order using OrderValidator instance. Instance ID: 743e18a4-8979-4112-9c2a-f10ba543a905
info: Order saved using SqlOrderRepository instance. Instance ID: cb593ecc-7070-4c50-930a-4f6df366373e, Order ID: 1
info: Sending email using SmtpEmailService instance. Instance ID: 2d0c9d88-93d0-4af8-94ff-b08d6c3d3009, To: [email protected], Subject: Order placed
info: Order placed. Instance ID: 1239455c-add7-4efd-8448-ab5a948ea62a, Order ID: 1
Yukarıda servislerin yaşam döngüleri açıkça görülebilir. SqlOrderRepository ve OrderService yalnızca bir kez oluşturulmuştur, çünkü aynı HTTP isteği boyunca aynı instance kullanılmaktadır; bu da onların Scoped olarak çalıştığını gösterir. SmtpEmailService ise uygulama boyunca tek bir kez oluşturulmuş ve tekrar kullanılmaktadır, bu nedenle Singleton yaşam döngüsüne sahiptir. OrderValidator için ise iki farklı instance oluştuğu görülmektedir. Bunun nedeni servisin Transient olarak tanımlanmasıdır; her talep edildiğinde container yeni bir nesne üretir.
Singleton, Scoped ve Transient: Farkları ve Ne Zaman Kullanılır?
ASP.NET Core DI Container’da bir servis kayıt edilirken yaşam döngüsü (lifetime) belirlenir. Bu, servis nesnesinin ne zaman oluşturulup ne zaman yok edileceğini belirler.
Singleton
Singleton yaşam döngüsünde servis yalnızca bir kez oluşturulur ve uygulama çalıştığı sürece aynı nesne kullanılır. Aşağıdaki gibi eklenmektedir.
builder.Services.AddSingleton<IEmailService, SmtpEmailService>();
Uygulama başladığında oluşturulan bu nesne, tüm HTTP istekleri ve tüm kullanıcılar tarafından paylaşılır. Bu nedenle içinde kullanıcıya özel veri tutulmaması gerekir. Genellikle konfigürasyon servisleri, cache servisleri veya uygulama boyunca tek bir instance için yeterli olacak servislerce tercih edilir.
Scoped
Scoped yaşam döngüsünde servis her HTTP isteği için bir kez oluşturulur. Aynı istek içerisinde bu servis birden fazla yerde kullanılsa bile aynı nesne paylaşılır. Aşağıdaki gibi eklenmektedir.
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
Ancak yeni bir HTTP isteği geldiğinde yeni bir instance oluşturulur. Web uygulamalarında en yaygın kullanılan yaşam döngüsüdür. Özellikle DbContext, repository’ler ve iş mantığı servisleri genellikle Scoped olarak tanımlanır.
Transient
Transient yaşam döngüsünde servis her talep edildiğinde yeni bir nesne oluşturur. Yani aynı HTTP isteği içinde bile servis farklı yerlerde inject edilirse her seferinde farklı bir instance üretilir. Aşağıdaki gibi kullanılmaktadır.
builder.Services.AddTransient<IOrderValidator, OrderValidator>();
Bu nedenle en kısa ömürlü yaşam döngüsüdür. Genellikle hafif, durum tutmayan (stateless) servislerde veya her kullanımda yeni bir nesne oluşturulmasının istendiği durumlarda tercih edilir.
Aşağıdaki bu üç yaşam döngüleri bir tablo ile karşılaştırılmıştır.
| Singleton | Scoped | Transient | |
| Oluşturulma | Uygulama başlangıcında 1 kez | Her HTTP isteğinde 1 kez | Her inject’te |
| Kapsam | Tüm uygulama | Tek istek | Tek kullanım |
| Bellek Kullanımı | En düşük | Orta | En yüksek |
| Thread-safety | Dikkatli olunmalı | Güvenli | Güvenli |
| Tipik kullanım | Cache, Config | DbContext, Repository | Utility, Helper |
Captive Dependency Uyarısı
Servis lifetime uyumluluğu da önemli konulardan bir tanesidir. Temel prensip, daha uzun ömürlü bir servis, daha kısa ömürlü bir servisi doğrudan tüketmemelidir. Bunun en tipik örneği bir Singleton sınıfın bir Scoped sınıfa bağımlılığıdır. Bu durumda Scoped servis uygulama boyunca tek bir instance’a hapsolur ve Captive Dependency problemi oluşur. ASP.NET Core bu tür hataları çoğu durumda otomatik olarak yakalar.
| Singleton | Scoped | Transient | |
| Singleton | Tüketebilir | Hata | Uyarı |
| Scoped | Tüketebilir | Tüketebilir | Tüketebilir |
| Transient | Tüketebilir | Tüketebilir | Tüketebilir |
Singleton servisler yalnızca Singleton bağımlılıkları güvenle tüketebilir. Bir Singleton servisin Scoped bir servisi tüketmesi hatalıdır ve Captive Dependency problemine yol açar çünkü Scoped servis uygulama boyunca tek instance’a hapsolur ve request bazlı davranış bozulur. Singleton ile Transient kullanımı teknik olarak mümkündür ancak bu durumda Transient servis yalnızca ilk oluşturulduğunda üretilir ve uygulama boyunca aynı instance kullanılır; yani pratikte Singleton gibi davranır. Buna karşılık Scoped ve Transient servisler daha esnektir. Scoped servisler Singleton, Scoped ve Transient bağımlılıkları güvenle tüketebilir çünkü yaşam süreleri request scope’u ile sınırlıdır; Transient servisler de her oluşturulduklarında yeni bir instance üretildiği için tüm lifetime türlerini güvenle tüketebilir ve dependency zincirinde ömür çakışması oluşmaz.
Development ortamında container doğrulaması uygulama başlarken yapılır ve hatalı lifetime kombinasyonu varsa uygulama ayağa kalkmaz. İsterseniz production’da da aynı doğrulamayı açık tutabilirsiniz. Lifetime hatalarının uygulama başlarken yakalanmasını istiyorsanız doğrulama seçeneklerini Program.cs içerisinden aşağıdaki gibi aktif edebilirsiniz.
builder.Host.UseDefaultServiceProvider(options =>
{
options.ValidateScopes = true;
options.ValidateOnBuild = true;
});
Bu ayar sayesinde container tüm dependency graph‘ını uygulama başlarken doğrular ve hatalı lifetime kombinasyonlarını runtime yerine startup aşamasında yakalar.
IoC Container Nedir? (Autofac, Ninject, Unity)
IoC Container (veya DI Container), bağımlılıkların yönetimini otomatikleştiren bir framework’tür. ASP.NET Core’un yerleşik container’ı çoğu proje için yeterlidir, ancak büyük ve karmaşık projelerde harici container’lar tercih edilebilir.
| Container | Açıklama | Öne Çıkan Özellik |
|---|---|---|
| ASP.NET Core Built-in | Microsoft’un yerleşik container’ı | Sıfır bağımlılık, hızlı başlangıç |
| Autofac | En popüler .NET DI container’ı | Modüller, assembly tarama, decorator |
| Ninject | Hafif ve esnek | Koşullu bağlama (contextual binding) |
| Unity | Microsoft’un eski DI container’ı | Enterprise projeler |
| Castle Windsor | Olgun, kurumsal | Interceptor, AOP desteği |
Yeni projelerde ASP.NET Core’un built-in container‘ı ile başlayın. Yalnızca decorator, interceptor veya assembly tarama gibi gelişmiş özelliklere ihtiyaç duyduğunuzda diğer DI Container’ları değerlendirin.
Bu makalede yolculuğumuza Tightly Coupled bir yapıdan başlayarak new ile oluşturulan bağımlılıkların yarattığı esnek olmayan mimariyi gördük. Ardından Factory Pattern (IoC) ile nesne oluşturma sorumluluğunu sınıfların dışına taşıdık, Dependency Inversion Principle (DIP) sayesinde somut sınıflar yerine abstraction’lara bağımlı hale geldik ve Dependency Injection ile bağımlılıkları constructor üzerinden enjekte ettik. Son olarak ASP.NET Core’un yerleşik DI container’ı ile tüm servis kayıtlarını merkezi olarak yönettik ve Singleton, Scoped ve Transient yaşam döngülerini doğru kullanarak daha esnek, test edilebilir ve sürdürülebilir bir mimari oluşturduk.
Altın Kurallar
- Servis sınıflarında
newanahtar kelimesi kullanmaktan kaçının - Her zaman implementation’a değil interface’e programlayın
DbContextverepository‘ler için Scoped lifetime kullanın- Singleton servislere Scoped bağımlılık enjekte etmeyin
- ASP.NET Core projelerinde built-in DI container ile başlayın
Yazı içinde kullanılan projeye buradan ulaşabilirsiniz.
