.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...

19 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 ?

  4. Samet ÇINAr dedi ki:

    Hocam saygılar. OrderController’da liste çekerken servisten, tek bir nesne çekerken repository’dan almışsın bunun bir sebebi var mı ?

    • borsoft dedi ki:

      Selamlar Samet;

      Valla güzel yakalamışın. Hayır özel bir nedeni yok. Amaç kod çeşitliliğini göstermek.

      Hoşçakal…
      İyi çalışmalar..

  5. Oğuzhan Dinç dedi ki:

    Hocam emeğinize sağlık çok güzel bir anlatım olmuş

  6. hacı yusuf özgelinkaya dedi ki:

    olmuyor ne yaptıysam olmuyor yoruldum ya işten patrondan yöneticilerden buktım illallah geldi teknolojininde allah belasını versin repositorininde bora abi teşekkürler

    • borsoft dedi ki:

      Yusuf Selam,

      Ne yaptın böyle. Bence daha basitten başla. Karışıktan direk başlayınca daralmışın sakin. Önce OOP’dan başla.
      Hatta bence önce bir kahve al git bir parkda otur. Öyle başla :)

      Kolay Gelsin.

    • sinan dedi ki:

      yav yusuf alem adamsın olum gece gece güldürdün bizi sabah ofiste çözeriz.

  7. Ben Murat dedi ki:

    Örnek uygulamanın kodları mevcut mu?

  8. Tolgahan Özgür dedi ki:

    IRepository scoped olarak tanımlandığı için services.AddSingleton(); kısmı da scoped olarak tanımlanmalı onun haricinde güzel yazı emeğinize sağlık :)

  9. Necmettin Yanık dedi ki:

    Bora Abi sen ne mükemmel bir insansın. Gerçekten anlatırken bile o karşı tarafı ezmeden, güzel dille, DotNet Konf’ta bahsettiğin iletişim becerisi ile ne güzel anlatıyorsun. Ellerine sağlık. Çok teşekkür ederim.

  10. Hocam iyi günler, Repository içerisinde update metdolarını tanımlarken doğrusdan savechanges() metodunu çağırmışsınız. Ben şimdiye kadar update yaparken, “_context.Entry(entity).State = EntityState.Modified;” veya “_context.Update(entity);” komutlarını kullanıyorudm. Bu kullanımlar arasında fark var mı ya da en doğru seçenek hangisidir? Teşekkür ederim. Saygılar.

    • borsoft dedi ki:

      Selamlar,

      Evet savechanges() methodu ile _context.SaveChanges() işlemi yapılıyor. En kestirme yolu bu. Sen işler biraz uzatıyorsun. Öncelike eğer uygulamanda Entity’de .AsNoTracking() var ise senin “EntityState.Modified” işe yaramaya bilir. Sen önce benim entitym değişti diye işaretliyorsun. Sonra verdiğin entitynin güncellenmesini istiyorsun.Ben entity’de yapılan değişikliği direk kaydet diyorum. Bende .AsNoTracking() tabi ki kapalı.

      İyi çalışmalar.

  11. Abdullah dedi ki:

    Hocam Merhaba,
    Makale için teşekkür ederim. Ellerinize sağlık. Genericrepository kullanarak transaction yönetimini nasıl sağlayabiliriz.

    • borsoft dedi ki:

      Selamlar,

      Genelde Transaction yönetiminin, Servis katmanında yapılmasını tavsiye ediyorlar. Amma illaki General Repository’de yapacak iseniz, Services katmanında oluşturduğunuz transaction’ı, Repository katmanına parametre olarak gönderin :) Ben öyle çözdüm :)

      İyi çalışmalar.

Bir cevap yazın

E-posta hesabınız yayımlanmayacak.