Unit of Work & Repository Patterns Implementation

unit-of-work-repository

Unit of Work ve Repository tasarım desenleri veri odaklı mimarilerle çalışırken kurulan yapıların daha esnek, test edilebilir olmasını sağlarken transaction yönetimini ve kodların tekrar kullanılmasını da kolaylaştırmaktadır.

Repository Pattern

Repository Pattern veri kaynağından veri çeken mehodların bir kere yazılarak istenildiği kadar kullanılmasını sağlayarak kod tekrarının önüne geçmeyi hedeflemektedir. Günümüzde uygulamalar çoğunlukla sunum, iş ve veri erişim olmak üzere üç geleneksel katmandan meydana gelmektedir. Veri erişim katmanı veritabanlarıyla persistence bir ORM framework kullanarak iletişim sağlamaktadır. Repository tasarım deseni veri erişim katmanıyla ORM arasını izole ederek tek yönlü bir bağımlılık sağlamayı da hedefler.

İki farklı şekilde uygulanmaktadır. İlk uygulamada her bir domain varlığı için yazılacak Repository ile en çok satan ürünler, sık ziyaret edilen kategoriler gibi kendilerine özel methodları tanımlamalarına imkan sağlanır. İkinci uygulanmadaysa domain varlıklarının CRUD gibi ortak işlemleri gerçekleştirebilecekleri ortak bir Generic Repository tanımlanmasıyla bu işlemlerin tekrar yazılması engellenir.

Domain varlıklarının çok olduğu uygulamalarda kullanımı tercih edilmektedir.

Unit of Work

Unit of Work Pattern iş katmanında yapılacak değişikliklerin anlık değil toplu şekilde biriktirilerek açılacak tek bir veritabanı bağlantısıyla yapılmasını hedeflemektedir. Bu sayede güvenli bir transaction yapısı kurgulanmış olmaktadır. İşlemler toplu şekilde bir transaction içerisinde yansıtıldığından olası bir hata durumunda değişikliklerin geri alınması da sağlanmış olmaktadır.

Unit of Work tasarım deseni tek başına da kullanılabileceği gibi Repository tasarım deseniyle birlikte kullanımında çok daha efektif olmaktadır. Yoğun işlem gerektiren uygulamalarda veritabanını yormadığından performans odaklıdır.

ef-unit-of-work-repository

Bu yazıda hali hazırda Unit of Work ve Repository tasarım desenlerini uygulayan Entity Framework Core kullanılacaktır. Burada DbSet bir Repository gibi davranıp ekleme, güncelleme, silme gibi işlemleri yerine getirirken; DbContext ise Unit of Work gibi davranarak değişiklikleri izleme ve yazma işlemlerinden sorumludur. Entity Framework Core kendi içerisinde transaction yönetimini SaveChanges methoduyla sağlamaktadır. Bu method çağırıldığında bir transaction başlatılır ve gönderilen toplu değişiklikler işletilir.

Uygulama içerisinde bu tasarım desenlerinin tekrar neden uygulanması gerektiği sorgulanabilir. Repository Pattern uygulanmadığı durumda yalnızca DbSet methodlarına sahip olunacağından ihtiyaç duyulan sorguların, ihtiyaç duyulduğu yerlerde tekrar yazılması gerekecektir. Uygulama ORM aracına tightly-coupled olacağından framework agnostic olmayacak ve değişiklik tüm uygulamayı etkileyecektir.

repository-pattern

Şekilde görüldüğü üzere GenericRepository sınıfı ortak işlemleri içeren IGenericRepository arayüzünü implemente etmektedir. ProductRepository sınıfı hem ortak işlemleri gerçekleştiren GenericRepository sınıfını hem de kendi varlığına özel işlemleri gerçekleştirmek için IProductRepository arayüzünü implemente etmektedir. Tasarım desenlerini uygulamak üzere uygulamanın veri erişim katmanını oluşturalım.

dotnet new classlib -n DataAccess

Uygulama içerisinde kullanılacak varlıkları tanımlayarak DbContext sınıfımızı code-first yaklaşımıyla oluşturup gerekli Microsoft.EntityFrameworkCore.SqlServer NuGet paketini kuralım.

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

    public virtual ICollection<Product> Products { get; set; } = new HashSet<Product>();
}

public class Product
{
    public int Id { get; set; }
    public int CategoryId { get; set; }
    public string Name { get; set; }
    public int UnitsInStock { get; set; }
    public decimal UnitPrice { get; set; }

    public virtual Category Category { get; set; }
}

public class DataContext : DbContext
{
    public DataContext(DbContextOptions<DataContext> options) : base(options) { }

    public DbSet<Category> Categories => Set<Category>();
    public DbSet<Product> Products => Set<Product>();
}

Uygulamamız içerisinde olabildiğinde loosely-coupled bir yapı için arayüzlerden mümkün mertebe faydalanmamız gerekmektedir. Uygulama içerisinde domain varlıkları tarafından ortaklaşa kullanılacak Generic Repository ile ilgili tanımlamaları yapalım.

public interface IGenericRepository<TEntity> where TEntity : class
{
    TEntity? Get(int id);
    IEnumerable<TEntity> GetWhere(Expression<Func<TEntity, bool>> predicate);
    void Add(TEntity entity);
    void Update(TEntity entity);
    void Delete(TEntity entity);
}

Yukarıdaki kod incelendiğinde ilgili methodların IQueryable yerine IEnumerable döndüğü görülecektir. IQueryable üzerinde tekrar sorgu yazılabildiği için bu yaklaşım tercih edilmemelidir. Ayrıca bu arayüzü implemente eden sınıflar DbContext sınıf erişimini dışarıya kapatmalıdırlar. Artık Entity Framework Core implementasyonuna sahip olacak IGenericRepository arayüzünü implemente edebiliriz.

public abstract class GenericRepository<TEntity> : IGenericRepository<TEntity> where TEntity : class
{
    protected readonly DbContext _context;

    public GenericRepository(DbContext context)
    {
        _context = context ?? throw new ArgumentNullException(nameof(DbContext));
    }

    public virtual TEntity? Get(TKey id)
    {
        return _context.Set<TEntity>().Find(id);
    }
    public virtual TEntity? Get(Expression<Func<TEntity, bool>> predicate)
    {
        return _context.Set<TEntity>().AsNoTracking().FirstOrDefault(predicate);
    }
    public virtual List<TEntity> GetWhere(Expression<Func<TEntity, bool>> predicate)
    {
        return _context.Set<TEntity>().AsNoTracking().Where(predicate).ToList();
    }
    public virtual void Add(TEntity entity)
    {
        _context.Set<TEntity>().Add(entity);
    }
    public virtual void Update(TEntity entity)
    {
        _context.Set<TEntity>().Update(entity);
    }
    public virtual void Delete(TEntity entity)
    {
        _context.Set<TEntity>().Remove(entity);
    }
}

Yukarıdaki implementasyon incelendiğinde sınıf kendi başına var olamayacağı için abstract olarak tanımlandığı ve methodlarının virtual olarak işaretlendiği görülecektir. Böylece GenericRepository sınıfını inherit edecek sınıflar özel bir ihtiyaç doğrultusunda bu methodları override edebilirler.

Sonrasında domain varlıklarına denk düşen Repository tanımlamalarını gerçekleştirelim.

public interface ICategoryRepository : IGenericRepository<Category> { }

public interface IProductRepository : IGenericRepository<Product>
{
    IEnumerable<Product> GetMostExpensiveProducts(int count);
    IEnumerable<Product> GetProducts(int page, int pageSize);
}

Görüldüğü üzere oluşturulan arayüzler IGenericRepository arayüzünden türerken IProductRepository bünyesinde iki özel method tanımlamaktadır. Oluşturduğumuz arayüzleri implemente edebiliriz.

public class CategoryRepository : GenericRepository<Category>, ICategoryRepository
{
    public CategoryRepository(DataContext context) : base(context) { }
}

public class ProductRepository : GenericRepository<Product>, IProductRepository
{
    public ProductRepository(DataContext context) : base(context) { }

    public List<Product> GetMostExpensiveProducts(int count)
    {
        return Context.Products.OrderByDescending(o => o.UnitPrice).Take(count).ToList();
    }
    public List<Product> GetProducts(int page, int pageSize)
    {
        return Context.Products.Skip((page - 1) * pageSize).Take(pageSize).ToList();
    }

    private DataContext Context { get { return _context as DataContext; } }
}

Repository arayüz ve sınıfları tanımlandığına göre Unit of Work implementasyonu gerçekleştirilebilir.

public interface IUnitOfWork : IDisposable
{
    ICategoryRepository Categories { get; }
    IProductRepository Products { get; }

    int SaveChanges();
}

public class UnitOfWork : IUnitOfWork
{
    private readonly DataContext _context;

    public UnitOfWork(DataContext context)
    {
        _context = context ?? throw new ArgumentNullException(nameof(DbContext));
        Categories = new CategoryRepository(_context);
        Products = new ProductRepository(_context);
    }

    public ICategoryRepository Categories { get; private set; }
    public IProductRepository Products { get; private set; }

    public int SaveChanges() => _context.SaveChanges();

    public void Dispose() => _context.Dispose();
}

Yukarıdaki kod incelendiğinde ihtiyaç duyulan DbContext sınıfının constructor üzerinden alındığına ve tanımlanmış Repository sınıflarının aynı instance’ı kullandığına dikkat edilmelidir.

İşlemler tamamlandığına göre iş katmanı oluşturulabilir. Yazıyı uzatmamak adına sonraki bölümlerde yalnızca Product domain varlığı üzerinden devam edilecektir.

dotnet new classlib -n Business
cd Business && dotnet add reference ../DataAccess

Ürünlere ilişkin servisimizi oluşturup implemente ederek Repository sınıflarıyla Unit of Work tasarım deseni üzerinden konuşmasını sağlayalım.

public interface IProductService
{
    Product? Get(int id);
    void Add(Product product);
    List<Product> Get(int page, int pageSize);
    List<Product> GetMostExpensives();
}

public class ProductManager : IProductService
{
    private readonly IUnitOfWork _unitOfWork;

    public ProductManager(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }

    public Product? Get(int id)
    {
        return _unitOfWork.Products.Get(id);
    }
    public Add(Product product)
    {
        _unitOfWork.Products.Add(product);
        _unitOfWork.SaveChanges();
    }
    public List<Product> Get(int page, int pageSize)
    {
        return _unitOfWork.Products.GetProducts(page, pageSize);
    }
    public List<Product> GetMostExpensives()
    {
        return _unitOfWork.Products.GetMostExpensiveProducts(10);
    }
}

Uygulamanın son projesi sunum katmanını oluşturarak servisimizi dış dünyaya açalım.

dotnet new webapi -n Api
cd Api && dotnet add reference ../Business

Öncelikle Program.cs dosyasında built-in IoC Container içerisine gerekli servisleri Dependency Injection ile elde edebilmek için ekleyelim.

builder.Services.AddDbContext<DataContext>(options => 
{
    options.UseSqlServer(builder.Configuration.GetConnectionString("SqlServer"));
});

builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
builder.Services.AddScoped<IProductService, ProductManager>();

Oluşturulan IProductService arayüzünü kullanacak controller sınıfını oluşturalım.

public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;

    public ProductsController(IProductService productService)
    {
        _productService = productService;
    }

    [HttpGet("{id}")]
    public IActionResult Get(int id)
    {
        return Ok(_productService.Get(id));
    }

    [HttpGet("{page}/{pageCount}")]
    public IActionResult Get(int page, int pageCount)
    {
        return Ok(await _productService.Get(page, pageCount));
    }

    [HttpPost]
    public IActionResult Post([FromBody] Product product)
    {
        _productService.Add(product);
        return Ok();
    }
}

Böylece temel düzeyde Unit of Work ve Repository tasarım desenleri kurumsal şekilde implemente edilmiştir. Yazı içerisinde kullanılan projeye GitHub üzerinden erişebilirsiniz.

Bir cevap yazın

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