Merhaba arkadaşlar, bu makalemde sizlerle Generic Repository ve Unit of Work kurumsal tasarım kalıplarını uygulayarak temel bir alt yapı (infrastructure) tasarlayacağız. Tasarlama sırasında Entity Framework’den yararlanarak code first yaklaşımı ile geliştireceğiz. Tabi ki alt yapımız ORM bağımsız (independent) olacak.

Alt yapımızı geliştirirken makul bir seviyede sıkı sıkıya bağlı (tight coupled) olmadan  gevşek bağlanmış (loosely coupled) bir şekilde geliştirmeye çalışacağız ki, gerek unit test‘ler olsun gerekse de ileride geliştireceğimiz bir modül için kırılma noktasına gelmeden, esnek bir şekilde entegre edebilmek için dependency inversion (bağımlılığın ters çevirilmesi) prensibinden faydalanarak alt yapımızı esnek ve tekrar kullanılabilir bir şekilde tasarlayacağız.

Alt yapımızı tasarlamaya başlamadan önce bu iki kurumsal tasarım kalıbını bir tanımlayalım.

Repository Pattern Nedir?

Bu desenin temelinde yatan asıl amaç;

Veritabanına veri ekleme, güncelleme ve okuma gibi CRUD (Create Read Update Delete) işlemlerimiz için oluşturmuş olduğumuz kodların tekrar kullanılabilirliğini sağlamaktır.

Yazılım geliştirmede, belkide en önemli prensiplerden birisi olan DRY (Don’t repeat yourself) yani kendini tekrarlama prensibine göre kod tekrarlarından sakınmak gerekmektedir. Repository Patterni ise bize bunu sağlamaktadır. Oluşturduğumuz bir sınıf ile tüm tablolar için CRUD işlemlerimizi yapabilmemizi sağlamaktadır ve kod tekrarını önlemektedir. Aksi durumda bir düşünsenize, 30 tablolu bir yapımız mevcut ve her biri için CRUD işlemleri yapmamız gerektiğini? Vay anam vay…

RepositoryDiagram

Unit of Work Pattern Nedir?

Unit of Work ise bize;

Veritabanı ile yapılacak olan tüm işlemleri, tek bir kanal aracılığı ile gerçekleştirme ve hafızada tutma işlemlerini sunmaktadır. Bu sayede işlemlerin toplu halde gerçekleştirilmesi ve hata durumunda geri alınabilmesi sağlamaktadır.

Unit of Work deseni tek başına kullanılabileceği gibi, Repository kurumsal tasarım deseni ile (ki makalemiz konusudur aşağıda beraber kullanımını göreceğiz) veya Identity Map kurumsal tasarım deseni ile de kullanılabilir.

Identity Map: ORM geliştirirken güncelleme (update) işlemleri sırasında veritabanından çekilmiş olan nesnenin, sadece değişime uğrayan alanlarını güncelleyebilmemizi sağlayan bir desendir.

Unit of Work deseninde ise, işlemleri hafızada tutma özelliğinden bahsetmiştik. Identity Map deseni işte bu noktada implemente edilebilir.

unitofwork

Yukarıdaki şemayı da incelediğimizde göreceğimiz üzere ilk başta veritabanına direkt erişimi görüyoruz klasik programlamada. İkinci kısımda ise bir tarafta Entity Framework için hazırlanmış olan Unit of Work deseni ile ilgili Repository’ler aracılığı ile veritabanına erişim var. Bu sayede Unit Test’ler için olan Mocklanmış sınıflar aracılığı ile test bir veritabanına veya in-memory hazırlanmış veri alanına erişimini görüyoruz.

Mock Object: Unit testler yaparken uygulamamızın tümünü ayakta tutmak/tutabilmek zor olacaktır bir hayli. Bunun yerine uygulamamızın test ortamında çalışabileceği kadar fonksiyonları içeren sınıflar hazırlanır. Bu tür sınıflara Mock Object denir.

Şimdi Repository ve Unit of Work kurumsal kalıplarının ne olduğunu anladığımıza göre, örneğimize geçelim ve beraber kullanımlarını detaylıca incelemeye başlayalım.

Örneğimiz gereği basitleştirilmiş temel bir alt yapı tasarlayacağız. Bu alt yapımız Entity Framework‘den yararlanarak code first yaklaşımı ile geliştireceğiz ve Repository ve Unit of Work desenlerini ise maksimum seviyede izole ederek, gerek Entity Framework ve MSSQL ile gereksede Mongo DB ilede çalışabilir bir şekilde modüler geliştirmeye çalışacağız.

Örneğimiz basit bir Blog sistemi olacak ve User, Article ve Category sınıflarından oluşacak.

İşe RepveUOW isminde boş bir solution oluşturarak içerisine ise RepveUOW.Data.Model isminde bir sınıf kütüphanesi (class library) ekleyerek başlıyorum.

Burada isminden de anlaşılabileceği üzere Poco sınıflarımız yani modellerimiz yer alacak. Bu sayede uygulamamız içi bu entity nesnelerimiz orm bağımsız bir şekilde gelişiyor olacak.

Model katmanı içerisine ilk başta tüm entity nesnelerimiz için ortak olan CreatedDate özelliğini kalıtım yolu ile aktarabilmek için ModelBase isminde bir soyut sınıf oluşturuyoruz.

using System;

namespace RepveUOW.Data.Model
{
    public abstract class ModelBase
    {
        public DateTime CreatedDate { get; set; }
    }
}

Soyut sınıfımız hazır olduğuna göre şimdi entitylerimizi oluşturmaya başlayabiliriz.

User sınıfı ile başlıyoruz.

using System;
using System.Collections.Generic;

namespace RepveUOW.Data.Model
{
    public class User : ModelBase
    {
        public User()
        {
            this.Articles = new List<Article>();
        }

        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
        public string Password { get; set; }

        // Fluently bir şekilde ilişkileri kullanabilmemiz için tanımlıyoruz.
        public virtual ICollection<Article> Articles { get; set; }
    }
}

Category sınıfı ekliyoruz.

using System.Collections.Generic;

namespace RepveUOW.Data.Model
{
    public class Category : ModelBase
    {
        public Category()
        {
            this.Articles = new List<Article>();
        }

        public int Id { get; set; }
        public string Name { get; set; }

        // Relations
        public virtual ICollection<Article> Articles { get; set; }
    }
}

Şimdide Article sınıfını ekliyoruz ve Model katmanımızı tamamlamış oluyoruz.

using System;

namespace RepveUOW.Data.Model
{
    public class Article : ModelBase
    {
        public int Id { get; set; }
        public int CategoryId { get; set; }
        public int UserId { get; set; }

        public string Title { get; set; }
        public string Content { get; set; }

        // Relations
        public virtual Category Category { get; set; }
        public virtual User User { get; set; }
    }
}

Modellerimizi oluştururken gördüğünüz gibi ilişkileri kullanabilmek için altta // Relations skobunda tanımladık. Bu ilişkileri daha sonra ilgili Context sınıfı içerisinde context ayağa kalkarken tanımlayacağız.

Model katmanını tasarladığımıza göre şimdi Data katmanını oluşturmaya başlayabiliriz.RepveUOW.Data isminde bir sınıf kütüphanesi daha oluşturuyoruz ve References kısmında Manage Nuget Packages sekmesine girip Entity Framework‘ü install ettikten sonra tekrar References kısmından Add Reference seçeneğini seçerek Projects tabından RepveUOW.Data.Model katmanını seçerek referans olarak ekliyoruz.

Referans işlemlerini tamamladığımıza göre RepveUOW.Data katmanı içerisinde Context isminde bir klasör tanımlayıp EFBlogContext sınıfını oluşturuyoruz.

EFBlogContext sınıfı bizim entity framework için kullanacak olduğumuz context’imiz olacak.

using System.Data.Entity;
using RepveUOW.Data.Model;

namespace RepveUOW.Data
{
    public class EFBlogContext : DbContext
    {
        public EFBlogContext()
            : base("BlogContext")
        {
        }

        public DbSet<User> Users { get; set; }
        public DbSet<Category> Categories { get; set; }
        public DbSet<Article> Articles { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            // İlişkileri kuruyoruz one-to-many olarak.
            modelBuilder.Entity<Article>()
                .HasRequired<Category>(x => x.Category)
                .WithMany(x => x.Articles)
                .HasForeignKey(x => x.CategoryId);

            modelBuilder.Entity<Article>()
                .HasRequired<User>(x => x.User)
                .WithMany(x => x.Articles)
                .HasForeignKey(x => x.UserId);

            base.OnModelCreating(modelBuilder);
        }
    }
}

Code first yaklaşımı ile geliştirmek istediğimizden EFBlogContext sınıfımızı System.Data.Entity namespace’inde bulunan DbContext sınıfından türetiyoruz. EFBlogContext’in constructor’ında DbContext sınıfının constructur’ına connection string’in ismini gönderiyoruz. Daha sonra bu connection string ismini Data katmanımızı kullanacak olan sunum katmanının ilgili App.config veya Web.config içerisinde tanımlıyor olacağız.

DbSet’ler aracılığı ile ilgili poco sınıflarımızı tanımladık ve modelimiz oluşurken ilişkileri tanımlayabilmemiz için OnModelCreating metotunu ezerek ilgili ilişkileri tanımladık. Fluent bir arayüze sahip olduğu için DbModelBuilder sınıfı gayet kolay bir şekilde ilişkilerimizi tanımlayabildik.

Code first olarak devam ettiğimiz için Sql Server üzerinde henüz hiç bir veritabanı bulunmamaktadır. EFBlogContext hazır olduğuna göre şimdi poco sınıflarımızdan yola çıkarak kolay bir şekilde veritabanı nasıl oluşturulur ona bir bakalım.

Bu işleme başlamadan önce henüz bir sunum katmanı bulunmadığı için App.config configuration tag’inin içerisinde ve configSections tag’inin altında bir conenction string tanımlayalım.

  <connectionStrings>
    <add
      name="BlogContext"
      connectionString="Data Source=localhost;Initial Catalog=BlogContext;Integrated Security=True;MultipleActiveResultSets=True;"
      providerName="System.Data.SqlClient" />
  </connectionStrings>

Şimdi sırasıyla View->Other Windows-> Package Manager Console açıyoruz ve üzerindeki Default project kısmında RepveUOW.Data seçili olduğundan emin oluyoruz.

Açılan konsola önce: Enable-Migrations –EnableAutomaticMigrations yazıyoruz ve connection string’de herhangi bir hata yapmadı isek Migrations isminde bir klasör oluşturup içerisinde Configuration sınıfını oluşturduğunu göreceğiz.

Şimdi tekrar Package Manager Console üzerinde Update-Database yazarak veritabanını oluşturmasını sağlıyoruz.

Veritabanımız ve context’imiz oluştuğuna göre şimdi generic repository sınıfını tasarlamaya başlayabiliriz. Hemen data katmanımıza Repositories isminde bir klasör ekleyerek içerisinde IRepository arayüzünü tasarlamaya başlıyoruz.

IRepository

using System;
using System.Linq;
using System.Linq.Expressions;

namespace RepveUOW.Data.Repositories
{
    /// <summary>
    /// Model katmanımızda bulunan her T tipi için aşağıda tanımladığımız fonksiyonları gerçekleştirebilecek generic bir repository tanımlıyoruz.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public interface IRepository<T> where T : class
    {
        IQueryable<T> GetAll();
        IQueryable<T> GetAll(Expression<Func<T, bool>> predicate); // LINQ desteği sunabilmek içinde expression'ları kullanıyoruz.
        T GetById(int id);
        T Get(Expression<Func<T, bool>> predicate);

        void Add(T entity);
        void Update(T entity);
        void Delete(T entity);
        void Delete(int id);
    }
}

Şimdi entity framework için kullanıyor olacağımız EFRepository sınıfını ekleyerek kodlarını aşağıdaki gibi yazıyoruz.,

using System;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Linq.Expressions;

namespace RepveUOW.Data.Repositories
{
    /// <summary>
    /// EntityFramework için hazırlıyor olduğumuz bu repositoriyi daha önceden tasarladığımız generic repositorimiz olan IRepository arayüzünü implemente ederek tasarladık.
    /// Bu şekilde tasarlamamızın ana sebebi ise veritabanına independent(bağımsız) bir durumda kalabilmek. Örneğin MongoDB için ise ilgili provider'ı aracılığı ile MongoDBRepository tasarlayabiliriz.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class EFRepository<T> : IRepository<T> where T : class
    {
        private readonly DbContext _dbContext;
        private readonly DbSet<T> _dbSet;

        public EFRepository(EFBlogContext dbContext)
        {
            if (dbContext == null)
                throw new ArgumentNullException("dbContext can not be null.");

            _dbContext = dbContext;
            _dbSet = dbContext.Set<T>();
        }

        #region IRepository Members
        public IQueryable<T> GetAll()
        {
            return _dbSet;
        }

        public IQueryable<T> GetAll(Expression<Func<T, bool>> predicate)
        {
            return _dbSet.Where(predicate);
        }

        public T GetById(int id)
        {
            return _dbSet.Find(id);
        }

        public T Get(Expression<Func<T, bool>> predicate)
        {
            return _dbSet.Where(predicate).SingleOrDefault();
        }

        public void Add(T entity)
        {
            _dbSet.Add(entity);
        }

        public void Update(T entity)
        {
            _dbSet.Attach(entity);
            _dbContext.Entry(entity).State = EntityState.Modified;
        }

        public void Delete(T entity)
        {
            // Eğer sizlerde genelde bir kayıtı silmek yerine IsDelete şeklinde bool bir flag alanı tutuyorsanız,
            // Küçük bir refleciton kodu yardımı ile bunuda otomatikleştirebiliriz.
            if (entity.GetType().GetProperty("IsDelete") != null)
            {
                T _entity = entity;

                _entity.GetType().GetProperty("IsDelete").SetValue(_entity, true);

                this.Update(_entity);
            }
            else
            {
                // Önce entity'nin state'ini kontrol etmeliyiz.
                DbEntityEntry dbEntityEntry = _dbContext.Entry(entity);

                if (dbEntityEntry.State != EntityState.Deleted)
                {
                    dbEntityEntry.State = EntityState.Deleted;
                }
                else
                {
                    _dbSet.Attach(entity);
                    _dbSet.Remove(entity);
                }
            }
        }

        public void Delete(int id)
        {
            var entity = GetById(id);
            if (entity == null) return;
            else
            {
                if (entity.GetType().GetProperty("IsDelete") != null)
                {
                    T _entity = entity;
                    _entity.GetType().GetProperty("IsDelete").SetValue(_entity, true);

                    this.Update(_entity);
                }
                else
                {
                    Delete(entity);
                }
            }
        }
        #endregion
    }
}

Repositorimiz hazır olduğuna göre artık Unit of Work desenini tasarlamaya başlayabiliriz. UnitOfWork isminde bir klasör daha oluşturarak içerisinde IUnitOfWork arayüzünü tanımlıyoruz.

using System;
using RepveUOW.Data.Repositories;

namespace RepveUOW.Data.UnitOfWork
{
    public interface IUnitOfWork : IDisposable
    {
        IRepository<T> GetRepository<T>() where T : class;
        int SaveChanges();
    }
}

İçerisinde istediğimiz tipteki repository’i bize getirecek olan GetRepository generic metotunu ve kaydetme işlemlerini toplu yapabilmemizi sağlayacak olan SaveChanges metotunun imzalarını tanımlıyoruz.

Şimdi yine entity framework için kullanacağımız EFUnitOfWork sınıfımızı UnitOfWork klasörü içerisinde tanımlıyoruz.

using System;
using RepveUOW.Data.Repositories;
using System.Data.Entity;

namespace RepveUOW.Data.UnitOfWork
{
    /// <summary>
    /// EntityFramework için oluşturmuş olduğumuz UnitOfWork.
    /// EFRepository'de olduğu gibi bu şekilde tasarlamamızın ana sebebi ise veritabanına independent(bağımsız) bir durumda kalabilmek. Örneğin MongoDB için ise ilgili provider'ı aracılığı ile MongoDBOfWork tasarlayabiliriz.
    /// </summary>
    public class EFUnitOfWork : IUnitOfWork
    {
        private readonly EFBlogContext _dbContext;

        public EFUnitOfWork(EFBlogContext dbContext)
        {
            Database.SetInitializer<EFBlogContext>(null);

            if (dbContext == null)
                throw new ArgumentNullException("dbContext can not be null.");

            _dbContext = dbContext;

            // Buradan istediğiniz gibi EntityFramework'ü konfigure edebilirsiniz.
            //_dbContext.Configuration.LazyLoadingEnabled = false;
            //_dbContext.Configuration.ValidateOnSaveEnabled = false;
            //_dbContext.Configuration.ProxyCreationEnabled = false;
        }

        #region IUnitOfWork Members
        public IRepository<T> GetRepository<T>() where T : class
        {
            return new EFRepository<T>(_dbContext);
        }

        public int SaveChanges()
        {
            try
            {
                // Transaction işlemleri burada ele alınabilir veya Identity Map kurumsal tasarım kalıbı kullanılarak
                // sadece değişen alanları güncellemeyide sağlayabiliriz.
                return _dbContext.SaveChanges();
            }
            catch
            {
                // Burada DbEntityValidationException hatalarını handle edebiliriz.
                throw;
            }
        }
        #endregion

        #region IDisposable Members
        // Burada IUnitOfWork arayüzüne implemente ettiğimiz IDisposable arayüzünün Dispose Patternini implemente ediyoruz.
        private bool disposed = false;
        protected virtual void Dispose(bool disposing)
        {
            if (!this.disposed)
            {
                if (disposing)
                {
                    _dbContext.Dispose();
                }
            }

            this.disposed = true;
        }
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        #endregion
    }
}

Unit of Work desenide hazır olduğuna göre katmanların son görünümüne bir bakalım:

katmanlar

 

Temel bir alt yapı tasarımımızı tamamladığımıza göre kullanımlarına bakabilmek amaçlı RepveUOW.Presentation.UnitTest isminde bir Unit Test projesi oluşturalım.

RepveUOW.Presentation.UnitTest katmanını oluşturduktan sonra References kısmından Add Reference seçeneğini seçerek, Data katmanında yaptığımız gibi Projects sekmesinden Data ve Model katmanımızı referans olarak ekliyoruz.

EntityTest isminde bir UnitTest sınıfı oluşturarak içerisinde aşağıda açıklamaları ile de görebildiğiniz gibi ilgili test metotlarımızı tanımlıyoruz.

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using RepveUOW.Data;
using RepveUOW.Data.UnitOfWork;
using RepveUOW.Data.Model;
using RepveUOW.Data.Repositories;

namespace RepveUOW.Presentation.UnitTest
{
    /// <summary>
    /// Repository ve UOW kullanarak ilgili test metotlarımızı yazıyoruz.
    /// </summary>
    [TestClass]
    public class EntityTest
    {
        // Entity framework için geliştirmiş olduğumuz context. Farklı ORM veya Veritabanı içinde bu context'i değiştirebiliriz.
        private EFBlogContext _dbContext;

        private IUnitOfWork _uow;
        private IRepository<User> _userRepository;
        private IRepository<Category> _categoryRepository;
        private IRepository<Article> _articleRepository;

        [TestInitialize]
        public void TestInitialize()
        {
            _dbContext = new EFBlogContext();

            // EFBlogContext'i kullanıyor olduğumuz için EFUnitOfWork'den türeterek constructor'ına
            // ilgili context'i constructor injection yöntemi ile inject ediyoruz.
            _uow = new EFUnitOfWork(_dbContext);
            _userRepository = new EFRepository<User>(_dbContext);
            _categoryRepository = new EFRepository<Category>(_dbContext);
            _articleRepository = new EFRepository<Article>(_dbContext);
        }

        [TestCleanup]
        public void TestCleanup()
        {
            _dbContext = null;
            _uow.Dispose();
        }


        [TestMethod]
        public void AddUser()
        {
            User user = new User
            {
                FirstName = "Gökhan",
                LastName = "Gökalp",
                CreatedDate = DateTime.Now,
                Email = "gok.gokalp@yahoo.com",
                Password = "123456"
            };

            _userRepository.Add(user);
            int process = _uow.SaveChanges();

            Assert.AreNotEqual(-1, process);
        }

        [TestMethod]
        public void GetUser()
        {
            User user = _userRepository.GetById(1);

            Assert.IsNotNull(user);
        }

        [TestMethod]
        public void UpdateUser()
        {
            User user = _userRepository.GetById(1);

            user.FirstName = "Mehmet";

            _userRepository.Update(user);
            int process = _uow.SaveChanges();

            Assert.AreNotEqual(-1, process);
        }

        [TestMethod]
        public void DeleteUser()
        {
            User user = _userRepository.GetById(1);

            _userRepository.Delete(user);
            int process = _uow.SaveChanges();

            Assert.AreNotEqual(-1, process);
        }
    }
}

Not: Test metotlarını çalıştırmadan önce App.config içerisinde configSections tag’inin altına tekrardan ilgili connection string’i eklemeyi unutmuyoruz.

Bu makalemizde temel bir şekilde kurumsal düzeyde alt yapı tasarlarken Generic Repository ve Unit of Work desenlerinin uygulanışını görmüş olduk.

İlgili proje örneğini aşağıdan indirebilirsiniz.

RepveUOW