.Net Core, EF Core ve Dependency Injection ile Uçtan Uca Service ve Business Katmanı

Selamlar,

Bu makalede, uçtan uca .Net Core bir projede, service, business ve repostory katmanın nasıl olması gerektiği hakkında tavsiyelerde bulunacağım.

Entity Katmanı:

Öncelikle örnek amaçlı .Net Core Console Application aşağıdaki gibi oluşturulur. Bu yazıda Database First kullanılarak, Northwind database’inden faydalanılmıştır.

Sıra, var olan bir SQL DB’den, DBContext ve Model sınıflarını oluşturmaya geldi. Açılan Entity projesine, EntityFrameworkCore’a ait SqlServer, SqlServer.Design ve Tools kütüphaneleri aşağıdaki gibi eklenir.

Bu uygulamada, VM makinadaki bir Sql Server’a bağlanılıp, Northwind DB’sine ait NorthwindContext sınıfı ve Pocoları aşağıdaki komut ile otomatik olarak oluşturulur. .Net Core projesinde bu işlemin yapılabileceği bir arayüz malesef henüz yoktur. Belki 3th party toollar olabilir. Ama bu makalede, aşağıdaki komut satırılı ile yetinilmiştir. İlgili komut satırı çalıştırıldıktan sonra, proje içinde “Models/DB” şeklinde klasörler oluşturulmuş ve ilgili DBContext ve Poco dosyalar buraya konulmuştur. Bir Sql Server’a uzaktan erişmek için yapılması gerekenler, bu makaleden okunabilir.

Diyelimki otomatik olarak oluşturulan tüm Contexler, ortak bir “BaseEntity()” sınıfından türetilsin. Bunun için ayrı bir partial class’ın yapılması gerekmektedir. Nedeni DB’de eğer bir değişiklik olur ise, tüm DB context’in yeniden oluşturulması yani migration yapılması gerekmektedir. Bu durumda var olan pocolar üzerinde yapılan değişikliklerin silinmemesi için, partial yani harici sınıflar oluşturulmalıdır.

PartialEntity/PartialEntites: Aşağıda görüldüğü gibi tüm Pocolar aynı BaseEntity’den türetilmiştir.

BaseEntity: Aşağıda görüldüğü gibi tüm DbContextlere, “WriteLog()” adında bir method eklenmiştir. Ve kullanıldığı zaman, ekrana kullanım zamanı log olarak basılmaktadır. DB’de karşılığı olmayan kolonların, [NotMapped] olarak işaretlenmesi gerekmektedir. Aksi takdirde hata alınır. Yukarıdaki resimde, bir context’in kullanım anında, aşağıdaki log komut’u çağrılmıştır.

Dikkat edilmesi gereken bir husus da, Northwind databasein’deki Viewlar, az önce belirtilen komut ile otomatik olarak DBContext altına alınamazlar. Bunun için ilgili viewlar, NorthwindContext sınıfına partial olarak aşağıdaki gibi eklenmelidirler.

PartialNorthwindContext: Aşağıda Northwind DB’sine ait tüm viewlar değil, örnek amaçlı sadece bir kaç view eklenmiştir.

Şimdi sıra geldi tüm sınıflar için ortak bir Repository sınıfının yapılmasına:

Öncelikle IRepository Interface’i aşağıdaki gibi oluşturulur:

IRepository: Genel bir Repostory katmanında olması gereken tüm malzemeler :), burada tanımlanmıştır.

GeneralRepository: Tüm sınıflarda ortak olarak yapılan işler, “Generic <T>” tipinde tanımlanmış ve işlem yapılacak entity, GeneralRepository sınıfının Constructorın’da atanmıştır. Böylece, her bir entity için yapılabilecek CRUD işlemler, tekrar tekrar yazılmamıştır.

Exensions: Sorgulamaya eklenebilecek birden fazla joinli tablo için, aşağıdaki include extension’ı size örnek amaçlı yazılmıştır.

Şimdi sıra geldi DBContext’in ve diğer serviceslerimizin, Dependency Injection ile .Net Core bir projede daha en baştan ayağa kaldırılmasına. .Net Core ile Dependency Injection, birlikte yerleşik olarak gelmektedir. Bu neden ile ayrıca bir kütüphane ihtiyaç duyulmamaktadır.

Startup.cs: Aşağıdaki kod ile “NorthwindContext“in, diğer Controllerların constructor’ında tanımlanarak kullanılması sağlanmıştır. Ayrıca diğer tanımlı serviceslerin, örneğin “GeneralRepository“‘inin de, burada tanımlanarak projenin diğer yerlerinde kullanılmasına olanak sağlanmıştır. Son olarak DbContext haricinde Memory Cache Manager, LogService gibi yazılabilecek custom servicesler, gene Dependency Injection ile ayağa kaldırılması için, Startup.cs altında aşağıdaki gibi tanımlanmaktadır.

AddScoped,AddTransient,AddSingleton Nedir: Yukarıda görüldüğü gibi, yaşam döngülerine göre, nesnelerin tanımlamaları farklılık göstermektedir.

  • AddScoped: Üretilme şekli, bir request içinde hep aynıdır. Farklı web requestler’de farklı instancelar oluşturulur. Aynı using() kullanımında olduğu gibi. Şahsen ben Repository sınıflarını bu şekilde ayağa kaldırmayı tercih ediyorum. Kısaca her istekde yaratılıp, dispose edilirler.
  • AddSingleton : Tüm application zamanı boyunca bir kere yaratılıp, her requestde aynı Instance’ın kullanılmasına olanak sağlanır. Mesela herkes için ortak kullanılan nesneler, Singleton yapılar, buna çok uygundur. Örneğin Cache manager gibi.
  • AddTransient : Transient nesneler, her zaman farklıdır. Her nesne çağrımında, yani her bir controller veya her bir service için yeni oluşturulurlar. En hızlısı ve thread safety açısından en güvenlisidir(no locks, pooled db connection gibi..). Örneğin LogServiceler bu şekilde kullanılabilirler. Kötü yani, çok fazla deep dive object oluştururlar. Daha çok monolitik büyük dependency ağacı olan yapılarda tercih edilmelidirler.

Services: Şimdi sıra geldi WebApi ile Entity arasındaki Bussines katmanın yazılacağı Service katmanına.

Tüm servislerden dönecek tip aynı olmalıdır. Aşağıda örnek olabilecek genel bir, dönüş tipi sınıfı tanımlanmıştır.

Service.Response.cs: 

  • “HasExceptionError” : Hata var mı.
  • “ExceptionMessage” : Hata mesajlarının yazıldığı kısım.
  • “Entity” : Tek bir Entity sonucun döndüğü kısım.
  • “List”: Çoklu sonucun tanımlandığı kısım.
  • “IsValid” : Update, Insert işlemler için validation sonucu.
  • “IsSuccessful” : İşlem sonucunun tutulduğu boolen kısım.

Örnek bir Service’den geri dönülecek Order ViewModel’, aşağıdaki gibi tanımlanır:

OrderModel: Ekrana basılacak ViewModel.

IOrderService.cs: Önce IOrderService interface’i ile yapılacak işler aşağıdaki gibi tanımlanır.

OrderService: WebApi services’inden çağrılan, bussines’ın döndüğü kısımdır. Aşağıdaki resim geri dönülen yapının console’a basıldığı result değeridir.

Not: Webapi controller’a bussines konmadan, sadece services ya da GeneralRepostory katmanı buradan çağrılır.

  • “public class OrderService : IOrderService” : IOrderServices’inden implemente edilerek, istenen 4 methodun tanımlanması zorunlu hale getirilir. Bu örnekte sadece “List()” methodu detaylıca kodlanmıştır.
  • “public CustomerService(IRepository<Order> orderRepository)” : Dependency Injection için, tüm pocolarda kullanılacak “IRepository” burada tanımlanmıştır.
    • IRepository<Order>” : Bu tanımlama ile orderRepostory tanımlanmıştır.
  • “List<>” : Methodunda Order, Order’a bağlı Customer ve Employee, Employee’ye bağlı EmployeeTerritories kayıtlarından 3 tane çekilmiştir.
    • “var response = new ServiceResponse<OrderModel>(null)” : her servis için geçerli olan ServiceResponse dönüş tipi tanımlanmıştır.
    • “var query= _orderRepository.Orders.Table.Include(or => or.Customer) .Include(or => or.Employee).ThenInclude(or => or.EmployeeTerritories).Take(3)”: İlgili tablolardan query çekilmiştir.
    • “response.Count = query.Count()” : Toplam kayıt sayısı “ServiceResponse” dönüş tipine ait “Count” değerine atanmıştır.
    • “var list = query.ToList()” : çekilen tüm kayıtlar bir list’e atanmıştır.
    • “foreach (var order in list) { var viewModel = new OrderModel{}”: Gezilen her kayıt “OrderModel” tipindeki ViewModel’e atanmıştır.
    • “response.List.Add(viewModel):Tüm servislerin geri dönüş tipi olan “ServiceResponse” modelnin “List” property’si “List<OrderModel>” ile doldurulmuştur.
  • order.Customer.WriteLog()” : Burada amaç ilgili modelin DB karşılığından farklı olarak BaseEntity‘den dönen “WriteLog()” methodunun çağrılabileceğidir. İlgili method tüm Entitylerde inheritancedan dolayı mevcuttur.

Yukarıdaki query’nin SqlProfile karşılığı aşağıdaki gibidir:

Dikkat: Unutulmaması gereken bir husus da, yukarıda yazılan OrderService’in Dependency Injection için Startup.cs’de aşağıdaki gibi tanıtılmasıdır.

Startup.cs (Ekleme):

Son olarak, geldik bu servisin bir WebApi service’i tarafından çağrılmasına.

OrderController: Aşağıdaki WebApi servisi, tüm makalenin özetlendiği kısımdır. WebApi servisinde, hiçbir bussines işlem yapılmamış, sadece data işlemleri için dışarısı ile sistem arasında Proxy görevi görmüştür. Sistem içinden logService, tokenService, notifyService, errorService  gibi daha birçok yapı çıkarılarak, sizin esas infrastructure’ı anlamanız amaçlanmıştır.

  • “IRepository<Order> _orderRepository” : Özel bir işlem olmadan sadece ID’ye göre datanın çekildiği, yani bussines’ın olmadığı “GetOrderById()” methodu için önceden  yukarıda global tanımlanan Repostory kullanılmıştır.
  • “GetOrders(int rowCount)” : İçinde bussines’ın olduğu Order, Customer ve Employee’nin dönüldüğü bir methoddur. Bundan dolayı “IOrderService” kullanılmıştır. Kısaca içinde bussines geçen yapılar, yukarıda  yazılan custom Service katmanındaki methodları kullanmaktadır. İlgili methoddan toplu kayıt dönüldüğü için, “ServiceResponse.List” propertysi doldurulur. Ayrıca paging amaçlı “response.Count” propertysi doldurulur.
  • “GetOrders()” ve “GetOrderById()” methodları makalenin başında global tanımlanan “ServiceResponse” tipinde bir sınıf dönmektedir. Böylece view her zaman hangi tipde bir model alacağını bilmektedir.
  • “GetOrderById()” : Methodu tek bir kayıt döndüğü için, “Entity” propertysi doldurulur.

Geldik bir makalenin daha sonuna. Bu makalede tepeden tırnağa WebApi, Services ve Entity katmanlarının nasıl yaratılması gerektiği, ve dikkat edilmesi gereken bazı hususlara parmak basmak istedim. Her developer’ın farklı bir yoğurt yiyişi vardır:) Genişletilebilir, gereksiz kod tekrarlarından uzak, her bir entity için farklı dönüş tiplerinin olmadığı, Entity üzerindeki özelleştirmelerin birebir üzerinden yapılmadığı, OOP gereği tüm entitylerin base bir entity’den türetildiği kodlar dilerim.

Not: İstenir ise bu makaleden farklı olarak, herbir entity için farklı Service katmanı yaratılabilir. Bu makalede, bussines gömülmeyen entityler için, “GeneralRepository” katmanının kullanılmasına rağmen, siz isterseniz WebApi servisini sadece Service katman ile muhatap edebilirsiniz.

Yeni bir makalede görüşmek üzere hepinize hoşçakalın.

 

Herkes Görsün:

Bunlar da hoşunuza gidebilir...

3 Cevaplar

  1. furkan yüksel dedi ki:

    Bora abi sen adamın dibisin ya çok teşekkürler. Emeklerine eline sağlık

  2. Ahmet dedi ki:

    Hocam makele için teşekkürler.
    UI katmanı olan web api katmanına orderRepository i alıp getorderbyid metodunu doldurmak bana pek doğru gelmedi. Şu sebeple ben yarın bunu mvc katmanına da vermek istersem gidip orada da getorderbyid metodunu repository den doldurmam gerekir. Aynı kodu iki defa yazmış olurum. İçinde her ne kadar business olmasa bile data katmanından datayı UI da almak doğru mu. Ben business servisinde alır UI katmanlarına business taşıyorum. Sizce bu yaklaşım nasıl olmalı.

  3. Sinan dedi ki:

    İyi günler hocam,
    Ben de projemde repository kullanıyorum. Fakat Command işlemleri T tipinde return içeriyor. Sizin yapınızda return yok.
    Merak ettiğim konu, mesela ben ürün eklerken yeni bir kategori oluşturmak istiyorum aynı pencerede. DTO olarak ürün adı, fiyatı, kategori adı aldım. ProductService içindeki Add metodunda sırasıyla kategoriyi ekle, dönen kategori id’yi al ve bu kategori id’ye ürünü ekle diyorum. Void olduğunda bu işlemi nasıl yapabilirim ?

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir