C# Unit of Work Design Pattern(Unit of Work Tasarım Deseni)

Merhaba,

Bu içeriğimizde veritabanı işlemlerini toplu halde gerçekleştirmemizi sağlayan ve olası bir hata durumunda topyekün geri alınabilmesine olanak tanıyan bir kurumsal tasarım kalıbı olan Unit Of Work Design Pattern’ı inceliyor olacağız.

Unit Of Work Design Pattern Nedir?

Unit Of Work tasarım deseni, yazılım uygulamamızda veritabanıyla ilgili her bir aksiyonun anlık olarak veritabanına yansıtılmasını engelleyen ve buna nazaran tüm aksiyonları biriktirip bir bütün olarak bir defada tek bir connection üzerinden gerçekleştirilmesini sağlayan ve böylece veritabanı maliyetlerini oldukça minimize eden bir tasarım desenidir.

Yukarıda veritabanı maliyetlerinin minimize edilmesinden bahsettik. Bu ifadeyi konumuzun esintisi açısından biraz daha açarsak eğer şöyle izahatte bulunabiliriz; .NET çatısı altında kullanılan veritabanı mimarisi Ado.NET yahut herhangi bir ORM(örn; Entity Framework) aracı ile veritabanına yapılan herhangi bir insert, update yahut delete isteği varsayılan olarak bir transaction içerisinde execute edilmektedir. Hal böyleyken her bir istek oldukça maliyetli bir şekilde işlenecektir. Neden mi? Çünkü, transaction mekanizması veritabanında oluşan tüm aktiviteleri takip edebilmekte ve böylece olası bir aksaklık, hata ya da süreçte tarafımızca belirlenmiş şartlar doğrultusunda yapılan tüm işlemleri geri alabilmektedir. Dolayısıyla transaction bu sorumluluğu karşılıksız yapmayacağından dolayı faturayı yüksek maliyet olarak kesmektedir. Bunun yanında her bir sorguya özel bir transaction devreye girdiğini düşünürsek maliyetimiz kat be kat artmakta ve veritabanını inanılmaz ölçüde yormaktadır. İşte nedeni budur!

Peki bu maliyeti minimize edecek çözüm nedir?
Bu sorunun cevabını verebilmek için transaction yapılanmasının genellikle bilinen bir özelliğine tekrar değinmek gerekmektedir. Transaction, içerisinde bir veya birden fazla sorguyu kontrol edebilme kapasitesine sahip mekanizmadır. Dolayısıyla her bir operatif sorgu için transaction devreye sokmaktansa tüm sorgularımızı kapsayacak bir adet transactionın devreye girmesi maliyeti oldukça minimize edecektir. İşte bu çözümün teknik adı Unit Of Work’tür.

Unit Of Work tasarımına tam teferruatlı giriş yapmadan önce ilk olarak ORM araçlarından Entity Framework Core kullanarak gönderdiğimiz sorguları SQL Server Profiler uygulamasından takip edelim ve her istekte oluşturulan ve bize yüksek maliyete sebep olan transactionları Unit Of Work tasarımının kıymetini anlayabilmek için gözlerimizle görelim 🙂

    [ApiController]
    [Route("api/[controller]")]
    public class CompanyController : ControllerBase
    {
        CompanyContext _companyContext;
        public CompanyController(CompanyContext companyContext)
        {
            _companyContext = companyContext;
        }
        public bool DatabaseBusiness()
        {
            _companyContext.Add(new Company
            {
                Name = "GençAy A.Ş"
            });
            _companyContext.SaveChanges();

            _companyContext.Add(new Company
            {
                Name = "NG A.Ş"
            });
            _companyContext.SaveChanges();

            _companyContext.Products.AddRange(new List<Product> {
                new Product{  CompanyId = 1, Name = "X", Quantity = 10 },
                new Product{  CompanyId = 1, Name = "Y", Quantity = 11 },
                new Product{  CompanyId = 1, Name = "Z", Quantity = 12 },
                new Product{  CompanyId = 2, Name = "A", Quantity = 13 },
                new Product{  CompanyId = 2, Name = "B", Quantity = 14 },
            }.ToArray());
            _companyContext.SaveChanges();

            return true;
        }
    }

Yukaridaki örnek “Company(Controller)” sınıfına göz atarsanız eğer “DatabaseBusiness” metodu içerisinde birden fazla veritabanı işlemi gerçekleştirilmekte ve süreçte her bir işlem “SaveChanges” metodu ile execute edilmektedir. İlgili endpointe yapılan request neticesinde veritabanı sunucusunda generate edilen SQL kodlarını izleyebilmek için Microsoft SQL Server Management Studio editörüyle beraber yüklenen SQL Server Profiler uygulamasını açınız ve ardından aşağıdaki görselde belirtilen talimatları uygulayınız.

C# Unit of Work Design Pattern(Unit of Work Tasarım Deseni) Hangi sunucuda çalışıyorsanız ona bağlanınız.
İlgili sunucuda execute edilen sorguların çalıştırıldığı transactionları görebilmek için Profiler uygulamasında transactionlara özel ayarlama yapmamız gerekmektedir. Bunun için karşımıza gelen Trace Properties penceresinde Events Selection sekmesine geçerek sağ altta bulunan Show all events kutucuğunu işaretleyerek tüm özellikleri listeleyiniz ve oradan Transactions kategorisi altındaki uygun alanları seçerek görülebilir hale getiriniz. C# Unit of Work Design Pattern(Unit of Work Tasarım Deseni)
Ve tüm bu işlemler neticesinde aşağıdaki ekran görüntüsünde olduğu gibi sunucuya gelen her bir istek neticesinde transaction oluşturulduğunu gözlemleyiniz.
C# Unit of Work Design Pattern(Unit of Work Tasarım Deseni)

Evet… Yukarıda görüldüğü üzere her bir SaveChanges SQL sunucusunda bir transaction oluşturmakta ve içerisinde işlem yapmaktadır. Nihayetinde bizim amacımız burada şahit olduğumuz maliyeti minimize etmektir. “La hoca yeter artık gir şu Unit Of Work denen zımbırtıya” dediğinizi duyar gibiyim 🙂 Tamam. Malum konuya gireceğiz merak etmeyin 🙂 Biraz daha sabretmenizi rica ediyorum 😉

Yukarıdaki örneğimize geri dönecek olursak, her bir SaveChanges metodunda bir transaction oluşturuyorsak eğer çözüm olarak her bir işlem neticesinde değil tüm işlemler neticesinde tek bir SaveChanges çağırmayı tercih edebiliriz. Bunun için metot içeriğini aşağıdaki gibi düzenleyelim ve ardından test edelim;

    [ApiController]
    [Route("api/[controller]")]
    public class CompanyController : ControllerBase
    {
        CompanyContext _companyContext;
        public CompanyController(CompanyContext companyContext)
        {
            _companyContext = companyContext;
        }
        public bool DatabaseBusiness()
        {
            _companyContext.Add(new Company
            {
                Name = "GençAy A.Ş"
            });

            _companyContext.Add(new Company
            {
                Name = "NG A.Ş"
            });

            _companyContext.Products.AddRange(new List<Product> {
                new Product{  CompanyId = 1, Name = "X", Quantity = 10 },
                new Product{  CompanyId = 1, Name = "Y", Quantity = 11 },
                new Product{  CompanyId = 1, Name = "Z", Quantity = 12 },
                new Product{  CompanyId = 2, Name = "A", Quantity = 13 },
                new Product{  CompanyId = 2, Name = "B", Quantity = 14 },
            }.ToArray());
            _companyContext.SaveChanges();

            return true;
        }
    }

C# Unit of Work Design Pattern(Unit of Work Tasarım Deseni)

Görüldüğü üzere tüm operasyonları tek bir SaveChanges ile execute ettiğimizde tek bir transaction ile kontrol edilmekte ve süreç daha az maliyetli ve sağlıklı bir hale getirilmiş bulunmaktadır. İşte, Unit Of Work desenide aslında bu mantığın ta kendisini benimsemekte ve tüm işlemleri tek seferde tek transaction içerisinde execute etmeye odaklanmaktadır.

Tabi ki de Unit Of Work tasarım deseni kullanılırken bu şekilde bir kullanımdan ziyade daha kompleks bir tasarım içerisine yerleştirilmektedir. O halde şimdi gelin Unit Of Work’ün hangi tasarımlarla nasıl çalıştığını ele alalım…

Unit Of Work Nasıl Çalışır?

C# Unit of Work Design Pattern(Unit of Work Tasarım Deseni)
Unit Of Work, toplu veritabanı işlemlerini tek seferde bir kereye mahsus execute eden ve böylece bu toplu işlem neticesinde kaç kayıtın etkilendiğini rapor olarak sunabilen bir tasarım desenidir. Genellikle Repository Design Pattern ile birlikte kullanılması tercih edilen Unit Of Work, ayrıca (genellikle) transaction kontrolüyle beraber kullanılarak tek bir merkezden tüm sorgu süreçlerini kontrol edebilmektedir. Tüm bunların yanında, kullanıcı tarafından adım adım(zincirleme) yapılan operasyonlarda süreç tam teferruatlı sonlandırılmadan vazgeçildiği taktirde o noktaya kadar yapılan tüm değişikliklerin geriye alınması gerekmektedir. İşte buradaki iş maliyetini Unit Of Work ortadan kaldırmakta ve zincir sonlanmaksızın hiçbir entitynin değerini veritabanında fiziksel olarak değiştirtmemektedir.

Repository Design Pattern İle Unit Of Work Kullanımı?

Biz bu makalemizde Unit Of Work tasarım desenini transaction kontrolüyle birlikte Repository deseniyle bütünleşik olarak ele alacağız.
İlk olarak Repository imzamızı oluşturalım;
Dikkat! Kodu şişirmemek adına basit işlemleri yapan bir Repository tasarladım

    public interface IRepository<T> where T : class
    {
        List<T> Get();
        bool Add(T model);
        bool Add<A>(A model) where A : class;
        bool Remove(T model);
        int Save();
    }

Ardından Unit Of Work imzamızı oluşturalım;

    public interface IUnitOfWork : IDisposable
    {
        bool Commit(bool state = true);
    }

Şimdi Repository sınıfımızı oluşturalım ve gerekli yapılanmanın ardından Unit Of Work stratejisini uygulayalım.

    public class Repository<T> : ControllerBase, IRepository<T>, IUnitOfWork where T : class
    {
        protected CompanyContext _companyContext;
        IDbContextTransaction transaction = null;
        public Repository(CompanyContext companyContext)
        {
            _companyContext = companyContext;
            transaction = _companyContext.Database.BeginTransaction();
        }
        [NonAction]
        public List<T> Get() => _companyContext.Set<T>().ToList();
        [NonAction]
        public bool Add(T model)
        {
            _companyContext.Set<T>().Add(model);
            return true;
        }
        [NonAction]
        public bool Add<A>(A model) where A : class
        {
            _companyContext.Set<A>().Add(model);
            return true;
        }
        [NonAction]
        public bool Remove(T model)
        {
            _companyContext.Set<T>().Remove(model);
            return true;
        }
        [NonAction]
        public int Save() => _companyContext.SaveChanges();
        [NonAction]
        public bool Commit(bool state = true)
        {
            Save();
            if (state)
                transaction.Commit();
            else
                transaction.Rollback();

            Dispose();
            return true;
        }
        public void Dispose()
        {
            _companyContext.Dispose();
        }
    }

Yukarıdaki kod bloğunda 4. satıra odaklanırsanız eğer 8. satırda context üzerinden başlatılarak elde edilen transaction “IDbContextTransaction” tipinden referans tarafından işaretlenmektedir. Entity Framework yapılanmasında context üzerinden ne zaman BeginTransaction() metodu ile bir transaction başlatılsın, bu başlatılan transaction devrede olacağından dolayı sorgu isteklerinde otomatik oluşturulan transactionlar ezilmiş olacaktır. Dolayısıyla süreçte bize eşlik edecek transaction elimizde bulunmaktadır ve tarafımızca kontrol edilebilir bir özelliktedir. 33. satırda ise Commit metodu aracılığıyla hem Save fonksiyonu çalıştırılmakta hemde mevcut transaction optional parameter değerine göre ya Commit edilmekte ya da Rollback ile tüm işlemler geri alınmaktadır.

Bu çalışma neticesinde mevcut Controller sınıflarımızı ilgili Repository sınıfından türetelim ve aşağıdaki çalışmaları gerçekleştirelim.

    [ApiController]
    [Route("api/[controller]")]
    public class CompanyController : Repository<Company>
    {
        public CompanyController(CompanyContext companyContext) : base(companyContext)
        { }
        public bool DatabaseBusiness()
        {
            Add(new Company
            {
                Name = "GençAy A.Ş"
            });
            Add(new Company
            {
                Name = "NG A.Ş"
            });

            Add(new Product { CompanyId = 1, Name = "X", Quantity = 10 });
            Add(new Product { CompanyId = 1, Name = "Y", Quantity = 11 });
            Add(new Product { CompanyId = 1, Name = "Z", Quantity = 12 });

            Add(new Product { CompanyId = 2, Name = "A", Quantity = 13 });
            Add(new Product { CompanyId = 2, Name = "B", Quantity = 14 });

            Commit();

            return true;
        }
    }

C# Unit of Work Design Pattern(Unit of Work Tasarım Deseni)

Görüldüğü üzere Unit Of Work tasarım deseni sayesinde tüm isteklerimiz tek bir seferde veritabanına gönderilmekte ve hepsi manuel bir şekilde başlattığımız transaction ile yönetilmektedir. Böylece maliyet oldukça düşmekte ve dolaylı yoldan gözle görülür bir performans artışı söz konusu olmaktadır. Ayriyetten burada endpointlerin Commit fonksiyonunu çağırma sorumluluğunu üstlenmesi dikkat edilmesi gereken bir husustur. Nihayetinde kaz gelecek yerden tavuk esirgenmez misali, onca performans getirisine o kadarcık zahmet göz ardı edilebilir olsa gerektir 🙂

Son Dedikodular

Unit Of Work tasarımına istinaden internette yapılan araştırmalarda karşınıza özellikle mühimlik teşkil eden iki hususta yorumlar gelebilir. Bu yorumlardan ilki Entity Framework ile Unit Of Work tasarımının kullanılmasının pekte tavsiye edilmemesi üzerinedir. Bunun nedeni, Entity Framework’ün kendi bünyesinde Unit Of Work barındırmasından dolayıdır. Hatta bu konuda ciddi ciddi içerikler oluşturup muhalif olanlar bile vardır. Bu konu üzerine benim nacizane tavsiyem Unit Of Work kullanım olarak Entity Framework’ün bünyesine çok fazla temas etmediği taktirde gönül rahatlığıyla kullanılabilir. Bir diğer husus ise internetteki birçok kaynakta context nesnesinin Dispose edilmesi üzerinedir. Evet, bizde bu içeriğimizde Dispose etmiş bulunmaktayız. Lakin stackoverflow’da ki şu arkadaşında söylediği gibi Entity Framework Core yapılanması Dependency Injection ile elde edilen context nesnesini otomatik dispose etmekte ve bu işlemi bizim yapmamıza gerek kalmamaktadır. Ve bu mantığa istinaden bizler context nesnesini yine dispose etmeyi lakin bu işlemi gerçekleştirirken pür dikkat olmayı unutmamalıyız.

İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…

Bunlar da hoşunuza gidebilir...

1 Cevap

  1. Enes Taha SELEK dedi ki:

    Sadece ve akıcı anlatım için teşekkürler…

Bir cevap yazın

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

*