Derinlemesine yazılım eğitimleri için kanalımı takip edebilirsiniz...

C# Repository Design Pattern(Repository Tasarım Deseni)

Merhaba,

Bu içeriğimizde yazılım projelerinde olmazsa olmaz bir tasarım kalıbı olan Repository tasarım kalıbını inceliyor olacağız. Repository Design Pattern, veritabanı sorumluluğunu üstlenen sınıfı tasarlarken bir standart üzerine oturtmayı hedefleyen ve Entity Framework gibi ORM(Object Relational Mapping) araçlarıyla kombine edilerek sorgusal anlamda az sayıda operatif metotla yüksek seviyede veri erişim imkanı sağlayan bir strateji üzerine kurulu tasarım desenidir.

C# Repository Design Pattern(Repository Tasarım Deseni)

Çalışma Mantığı

Yazılım uygulamaları genellikle yoğun bir şekilde veritabanı işlemleri gerçekleştiren ekosisteme sahip yapılar olduğundan dolayı, ilgili uygulamanın herbir noktasında gerekli veritabanı işlemlerini tekrar tekrar yazmak yerine bu işlemleri tekrar kullanılabilirlik prensibi çerçevesinde daha pratik bir şekilde tek seferde yapmamızı sağlayan bir yapılanma geliştirmemiz gerekecektir. İşte bu yapılanma Repository sınıfı olacaktır.

Repository sınıfı, içerisinde generic yapılanmalarla geliştirilen temel operasyonel veritabanı metotlarını barındıran bir sınıftır. Yukarıdaki diyagramdan da görüldüğü üzere sorgu generate etme ve genellikle ORM araçlarıyla kombine edilerek veri eşleştirme sorumluluğunu üstlenmektedir. Bu yeteneklerini ana business sorumluluğunu yüklenen sınıflara kalıtım yoluyla aktarmakta ve bu şekilde kazandırmaktadır.

Uygulama Yöntemi

Repository Design Pattern’i uygulayabilmek için öncelikle tüm veritabanı işlemlerini operatif olarak temsil edecek interface tanımlamamız gerekmektedir. Aslında bu bir zorakilik değil, gelenektir. Şöyle konuya dair kısa bir araştırma yaparsak, genellikle tasarlanan Repository sınıfının genel hatlarının belirlenebilmesi için içerisinde kullanılacak tüm metotları barındıran bir interface ile imzalandığı gözlenmektedir. Kısaca bu bizim çalışmamızı kolaylaştıracak olan bir hamledir.

    public interface IRepository<T> where T : class
    {
        List<T> Get();
        List<A> Get<A>() where A : class;
        List<T> GetWhere(Expression<Func<T, bool>> metot);
        List<A> GetWhere<A>(Expression<Func<A, bool>> metot) where A : class;
        T GetSingle(Func<T, bool> metot);
        A GetSingle<A>(Func<A, bool> metot) where A : class;
        T GetById(int id);
        A GetById<A>(int id) where A : class;
        bool Add(T model);
        bool Add<A>(A model) where A : class;
        bool Remove(T model);
        bool Remove<A>(A model) where A : class;
        bool Remove(int id);
        bool Remove<A>(int id) where A : class;
        bool Update(T model, int id);
        bool Update<A>(A model, int id) where A : class;
        int Save();
    }

Yukarıdaki kod bloğuna göz atarsanız eğer IRepository isimli bir interface tanımlanmıştır ve içerisine aklımıza gelen tüm veritabanı işlemlerini yapmamızı sağlayacak olan metot imzaları eklenmiştir. Burada dikkat edilmesi gereken iki husus vardır; birincisi, interface generic olarak tasarlanmıştır… Bunun nedeni, tasarımda kullanılacak tipi süreçte geliştirici belirleyecektir ve tüm bu yapılanma belirtilen o tipe göre geliştirilecektir. İkinci husus ise geliştiricinin belirlediği tipin dışındaki türlerde yapılacak işlemleri karşılayabilmek için T tipinden bağımsız generic olarak tasarlanan metotlar geliştirilmiştir. Burada örnek vermek gerekirse; “Araba” modelini kullandığımız bir sınıf içerisinde “Surucu” modeline ait işlem yapmamız gerekiyorsa eğer işte burada generic metotlar devreye girecektir.

Eee madem araba üzerinden örnek verdik ilgili Repository sınıfımızı oluştururken bir ArabaContext nesnesi üzerinde işlem yaptığını varsayarak devam edelim. Sırada tasarladığımız interface’i aşağıdaki gibi Repository ismini verdiğim generic bir sınıfa uygulayarak concrete halini oluşturuyorum. Bizler örneğimizi Web API uygulamasına uygun bir sınıf ile ele almaktayız.

    public class Repository<Type> : ControllerBase, IRepository<Type> where Type : class
    {
        protected ArabaContext _arabaContext;
        public Repository(ArabaContext arabaContext)
        {
            _arabaContext = arabaContext;
        }
        [NonAction]
        public DbSet<Type> Table()
        {
            return Table<Type>();
        }
        [NonAction]
        public DbSet<A> Table<A>() where A : class
        {
            return _arabaContext.Set<A>();
        }
        [NonAction]
        public bool Add(Type model)
        {
            return Add<Type>(model);
        }
        [NonAction]
        public bool Add<A>(A model) where A : class
        {
            Table<A>().Add(model);
            Save();
            return true;
        }
        [NonAction]
        public List<Type> Get()
        {
            return Get<Type>();
        }
        [NonAction]
        public List<A> Get<A>() where A : class
        {
            return Table<A>().ToList();
        }
        [NonAction]
        public Type GetById(int id)
        {
            return GetById<Type>(id);
        }
        [NonAction]
        public A GetById<A>(int id) where A : class
        {
            return GetSingle<A>(t => typeof(A).GetProperty("Id").GetValue(t).ToString() == id.ToString());
        }
        [NonAction]
        public bool Remove(Type model)
        {
            return Remove<Type>(model);
        }
        [NonAction]
        public bool Remove<A>(A model) where A : class
        {
            Table<A>().Remove(model);
            return true;
        }
        [NonAction]
        public bool Remove(int id)
        {
            return Remove<Type>(id);
        }
        [NonAction]
        public bool Remove<A>(int id) where A : class
        {
            A silinecekData = GetSingle<A>(x => (int)typeof(A).GetProperty("Id").GetValue(x) == id);
            Remove<A>(silinecekData);
            Save();
            return true;
        }
        [NonAction]
        public int Save()
        {
            return _arabaContext.SaveChanges();
        }
        [NonAction]
        public bool Update(Type model, int id)
        {
            return Update<Type>(model, id);
        }
        [NonAction]
        public bool Update<A>(A model, int id) where A : class
        {
            A guncellenecekNesne = GetById<A>(id);
            var tumPropertyler = typeof(A).GetProperties();
            foreach (var property in tumPropertyler)
                if (property.Name != "Id")
                    property.SetValue(guncellenecekNesne, property.GetValue(model));
            Save();
            return true;
        }
        [NonAction]
        public List<Type> GetWhere(Expression<Func<Type, bool>> metot)
        {
            return GetWhere<Type>(metot);
        }
        [NonAction]
        public List<A> GetWhere<A>(Expression<Func<A, bool>> metot) where A : class
        {
            return Table<A>().Where(metot).ToList();
        }
        [NonAction]
        public Type GetSingle(Func<Type, bool> metot)
        {
            return GetSingle<Type>(metot);
        }
        [NonAction]
        public A GetSingle<A>(Func<A, bool> metot) where A : class
        {
            return Table<A>().FirstOrDefault(metot);
        }
    }

Yukarıdaki kod bloğunu incelerseniz eğer Repository isminde bir sınıf oluşturulmuştur ve biraz önce oluşturduğumuz IRepository isimli interfaceden türetilmiştir. Implementasyon neticesinde oluşturulan operatif metotların içeriğine göz atarsanız eğer isimlerine uygun işlevler gerçekleştirilmekte ve gerektiği zaman reflection dahi kullanılmaktadır ve böylece T yahut A tipine belirtilen türe göre uygun tabloda işlemler gerçekleştirilmektedir.

Peki, T ve A türlerinin ne olduğunu biliyor muyuz?
Hayır bilmiyoruz.
Öyleyse türün belli olmadığı(opsiyonel) bu durumda hangi tabloda çalışılacağını nereden anlıyoruz?
Context nesnemizin Set metodu bizlere generic olarak belirtilen türe uygun tabloyu getirmektedir. Dolayısıyla yukarıda “Table” ismindeki metot/lar verilen T yahut A türüne uygun DbSet nesnesini(dolayısıyla tabloyu) getirmekte ve bizlerde ona göre işlemler gerçekleştirmekteyiz. En iyisi bunu deneyimleyip, görmenizdir… Ayrıca tüm metotların [NonAction] attributeu ile işaretlenmesi sizi şaşırtmasın. Nihayetinde örneklendirme bir API alt yapısı üzerinden olduğu için ilgili metotların endpoint olmadığını ifade etmiş oluyor ve dışarıdan gelecek isteklerin es kasa bu metotlar tarafından karşılanmasını engellemiş oluyoruz.

Evet, şimdi oluşturduğumuz Repository sınıfını kullanalım.

    [ApiController]
    [Route("api/[controller]")]
    public class ArabaController : Repository<Araba>
    {
        public ArabaController(ArabaContext arabaContext) : base(arabaContext)
        {
        }

        public bool Ekle(Araba araba)
        {
            return Add(araba);
        }
        public bool Sil(int id)
        {
            return Remove(id);
        }
        public List<Araba> TumArabalar()
        {
            return Get();
        }
    }

Görüldüğü üzere Repository sınıfımız başarılı bir şekilde ilgili controller sınıfında kullanılmaktadır. Tabi burada “ArabaController” sınıfına gelen istekler neticesinde ilgili endpointler tetiklenecek Repository metotları kullanılmış olacaktır.

Ayrıca burada şu şekilde de bir kullanım sergilenebilmektedir.
C# Repository Design Pattern(Repository Tasarım Deseni)
Böylece Repository’den türeyen herbir sınıfın instance’i üzerinden de veritabanı işlemlerini yürütebilirsiniz.

Son söz olarak nihai usule uygun kelam etmemiz gerekirse Repository Design Pattern ORM yapılanmalarıyla(özellikle Entity Framework) oldukça basit ve bir o kadar etkili veritabanı havuzu oluşturmamızı ve tüm işlemleri tek kalemde halletmemizi sağlayan güzel ve kullanışlı bir tasarım desenidir.

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

Bunlar da hoşunuza gidebilir...

7 Cevaplar

  1. Ahmet dedi ki:

    “Burada örnek vermek gerekirse; “Araba” modelini kullandığımız bir sınıf içerisinde “Surucu” modeline ait işlem yapmamız gerekiyorsa eğer işte burada generic metotlar devreye girecektir.” Hocam burada Single responsibility prensibine göre Surucu sınıfı oluşturmamız gerekmez mi ? Neden generic fonksiyon farklı bir sınıf için oluşturduk. Generic Metodlar için farklı bir örnek verebilir misiniz ? Teşekkürler.

    • Gençay dedi ki:

      Eğer Repository’i Controller seviyesinde kullanıyorsan mecbur bu şekilde çalışman gerekiyor. Lakin her ne kadar makalede bu yöntem(yani Controller seviyesinde kullanım) ele alınmış olsada pek tavsiye edilmemekte ve makalenin sonunda atıfta bulunulan Concrete Repository sınıflarının oluşturulması daha tercih edilir olmaktadır. Dolayısıyla senin söylediğin daha hem SRP hem de clean kod açısından daha doğru ve tutarlı olacaktır.

      Sevgiler.

  2. Nihat dedi ki:

    karmaşık sql için ,örneğin adı Nihat, Soyadında “e” harfi olanlar ve satın aldığı kitapların fiyatı 15tl den yüksek olanları getirmek için bu patternin bir çözümü var mıdır?

  1. 15 Kasım 2019

    […] 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 […]

  2. 06 Mart 2021

    […] vaat eden uygulamalar da oldukça risk teşkil eden bir durumdur. Hele hele bu tarz bir tasarımda Generic Repository tasarım deseni uygulandığını düşünürseniz işin içinden çıkılması oldukça zahmetli […]

  3. 25 Eylül 2021

    […] bu tanımlamalara istinaden konuyu daha da netleştirecek bir örnek yapalım. Örneğimizde bir Repository‘den gelecek olan ‘Employee’ nesnesinin Null Object Design Pattern eşliğinde […]

  4. 16 Ekim 2021

    […] rahatlıkla görebilmekteyiz. Tabi bunun dışında genellikle kullanmayı tercih ettiğimiz Repository pattern’ını tasarlarken inşa edilen generic yapıda yanlışlıkla entity olmayan bir […]

Bir cevap yazın

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