CSharp

UnitOfWork Tasarım Modeli (Unit of Work Design Pattern) Kullanımı

UnitOfWork tasarım modeli basitçe veritabanı işlemlerini bir noktadan aktararak/yöneterek farklı veri kaynaklarında veri tutarlılığını (transaction) sağlayan bir yazılım yaklaşımıdır. İşlemleri bir noktada toplamış olması kaynak kullanımını azaltmakta ve performansa katkısı olmaktadır. Uygulaması Depo tasarım modeli (Repository design pattern) ile birlikte gerçekleşir.

Depo tasarım modelinde depo (repository) veritabanında bulunan herhangi bir varlığı ifade etmektedir. Model genelde 2 şekilde uygulanmaktadır.

  • Jenerik (Generic)

Jenerik modelde genel bir depo tanımlaması yapılarak işlemler içerisine eklenir. Bu model her varlık için ayrı ayrı oluşturulsa da bir havuz gibidir. Bir örnek ile tüm varlıklara kaynak teşkil eder.

  • Tekil varlığa özgü (Non-Generic)

Tekil varlığa özgü modelde ise her varlık örneği ayrı ayrı tanımlanır. Her tanımın içerisinde kendine özgü işlemleri vardır. Jenerikten farklı olarak birçok örnek ile birçok kaynağı ifade eder.

Jenerik veya tekil varlığa özgü depo yaklaşımlarından hangisi olursa olsun, depo yaklaşımının birçok faydasının yanında belki bir dezavantaj olarak oluşturulan her varlık kendi içerisinde veritabanı kaynağı/bağlamı (context) içerir. Bu da her varlık için oluşturulan veritabanı kaynağı demektir. Ayrıca birbirini sıralı takip eden işlemlerde hatalı bir işlemin olması durumunda farklı veri yapıları üzerinde geri alma prosedürü oldukça karmaşıktır. Bu durum veri kaynağın tekilleştirmesi sayesinde aşılabilir.

unit of work olmadan

Üstteki şekilde görüleceği gibi kullanıcı ve sipariş yönetimi, kendileri için kurgulanmış repository’lere sahipler ve repository’ler kendi içerisinde DbContext içermektedir.

UnitOfWork yaklaşımında ise alttaki şekilde görüleceği üzere DbContext bir örnek olarak oluşturulmakta ve repository’lere sağlanmaktadır.

unit of work ile

Örnek uygulamamızda UnitOfWorkManager adında bir yapı oluşturarak bazı temel işlemleri içerisine ekleyeceğiz. Oluşturacağımız yapı IUnitOfWorkManager arayüzünden türetilecektir.

  public interface IUnitOfWorkManager
    {
        int SaveChanges();
        Task SaveChangesAsync(CancellationToken cancellationToken = default);
        void BeginTransaction();
        void Commit();
        void Rollback();
        bool IsTransactionContinue();
    }
  public class UnitOfWorkManager : IUnitOfWorkManager
    {
        public DefaultDbContext Context { get; internal set; }
        private bool isTransaction { get; set; }

        public UnitOfWorkManager(DefaultDbContext context)
        {
            Context = context;
        }
        public int SaveChanges()
        {
            return Context.SaveChanges();
        }

        public Task SaveChangesAsync(CancellationToken cancellationToken = default)
        {
            return Context.SaveChangesAsync(cancellationToken);
        }

        public void BeginTransaction()
        {
            Context.Database.BeginTransaction();
            isTransaction = true;
        }

        public void Commit()
        {
            Context.Database.CommitTransaction();
            isTransaction = false;
        }

        public void Rollback()
        {
            Context.Database.RollbackTransaction();
            isTransaction = false;
        }

        public bool IsTransactionContinue()
        {
            return isTransaction;
        }        
    }

Üstteki kod bloğunda görüldüğü üzere DefaultDbContext adında db context’imiz (veritabanı kaynağı/bağlamı) UnitOfWorkManager yapısı içerisinde oluşturulmaktadır. Oluşturulan veri kaynağı Repository’ler ile paylaşılacaktır. Dolayısıyla tüm Repository’ler aynı veri kaynağını kullanacağından kaynak tekilleşecektir.

Unutmadan örnek çalışmamızda IoC container kullanıldığından çözümleme için kayıt işlemi, istek (request) başına (service lifetime scope) olarak yapılmıştır. Bu durumun nedeni istek boyunca aynı unitofwork ve repository örneğinin oluşturulmasının sağlanmasıdır.

Autofac IoC kütüphanesi kullandığınızda örnek kayıt işlemi (ConfigureContainer)

     builder.RegisterType<UnitOfWorkManager>().As<IUnitOfWorkManager>().InstancePerLifetimeScope();
     builder.RegisterGeneric(typeof(Repository<>)).As(typeof(IRepository<>)).InstancePerLifetimeScope();

Asp.Net Core IoC kütüphanesi ile kayıt işlemi (Startup.cs)

     services.AddScoped<IUnitOfWorkManager, UnitOfWorkManager>();
     services.AddScoped(typeof(IRepository<>), typeof(Repository<>));

Uygulamamızda jenerik tip depo tasarım modeli kullanılacaktır. Repository adındaki jenerik modelimiz IRepository arayüzünden türetilecektir. Türetilen jenerik model UnitOfWorkManager  yapısını içerecektir ve bu yapıyla birlikte db context kaynağına erişecektir.

IRepository arayüzü ile devam edelim.

  public interface IRepository<T> where T : class
    {
        //Get Methods
        T Get(int Id);
        IQueryable Where(Expression<Func<T, bool>> where);
        IQueryable GetAll();

        //Get Async
        Task<T> GetAsync(int Id);

        //Execute Methods
        int Insert(T obj);
        int Update(T obj);
        int Delete(T obj);

        //Execute Async
        Task<int> InsertAsync(T obj);
        Task<int> UpdateAsync(T obj);
        Task<int> DeleteAsync(T obj);
    }
  public class Repository<T> : IRepository<T> where T : class
    {
        private DbSet<T> _objectSet { get; set; }
        private DefaultDbContext _dbcontext { get; set; }
        private IUnitOfWorkManager _unitOfWorkManager { get; set; }

        public Repository(IUnitOfWorkManager unitOfWorkManager)
        {
            _unitOfWorkManager = unitOfWorkManager;
            _dbcontext = (_unitOfWorkManager as UnitOfWorkManager).Context;
            _objectSet = _dbcontext.Set<T>();
        }

        public T Get(int Id)
        {
            return _objectSet.Find(Id);
        }

        public async Task<T> GetAsync(int Id)
        {
            return await _objectSet.FindAsync(Id);
        }

        public IQueryable<T> GetAll()
        {
            return _objectSet.AsQueryable();
        }

        public IQueryable<T> Where(Expression<Func<T, bool>> where)
        {
            return _objectSet.Where(where);
        }

        public int Insert(T obj)
        {
            _objectSet.Add(obj);
            return Save();
        }

        public async Task<int> InsertAsync(T obj)
        {
            await _objectSet.AddAsync(obj);
            return await SaveAsync();
        }

        public int Update(T obj)
        {
            return Save();
        }

        public async Task<int> UpdateAsync(T obj)
        {

            return await SaveAsync();
        }

        public int Delete(T obj)
        {
            _objectSet.Remove(obj);

            return Save();
        }

        public async Task<int> DeleteAsync(T obj)
        {
            _objectSet.Remove(obj);

            return await SaveAsync();
        }

        private int Save()
        {
            return _dbcontext.SaveChanges();
        }

        private async Task<int> SaveAsync()
        {
            return await _dbcontext.SaveChangesAsync();
        }
    }

Örneğimiz ile ihtiyaca göre 2 kullanım opsiyonu sağlanmıştır. Uygulamamızda CRUD işlemleri sırasında UnitOfWork ile BeginTransaction() kullanılmışsa kayıtlar Commit() ile kaydedilecek ve hata durumunda RollBack() ile geri alınacaktır. Eğer BeginTransaction() kullanılmazsa her bir işlem sonunda kayıt işlemi gerçek zamanlı gerçekleştirilecektir.

Transaction kullanılarak veri tabanı işlemi örneği :

      //transaction başlatılır.
      _unitOfWorkManager.BeginTransaction();
      try
      {
          //kullanıcı eklenir.
          var userId = _userRepo.Insert(user);

          //kullanıcı için claim eklenir.
          if (userId > 0)
              _userClaimRepo.Insert(new UserClaims() { ClaimId = 1, UserId = userId });

          //claim eklemede bir sorun var ise kullanıcı ekleme işlemi de geri alınacaktır.
          //sorun yok ise işlem commit ile kaydedilecektir.
          _unitOfWorkManager.Commit();
      }
      catch {
          //sorun oluştuğunda geri alma işlemi
          _unitOfWorkManager.Rollback();
      }

Alttaki ekran alıntısında kullanıcı ekleme “Add” metodu örneğinde transaction kullanılarak veri tutarlılığı sağlanmıştır.

Altta göreceğiniz kullanıcı ekleme “Add” metodunda ise transaction kullanılmadan uygulama yapılmıştır.

Sağlıkla kalın..

Bir cevap yazın

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

*