Entity Framework Core Nedir ? Generic Repository Pattern Kullanarak Asp.Net Core Web Api Projesi Geliştirme

.Net Core'un duyurulmasıyla birlikte microsoft .Net Framework çatısı altında geliştirmekte olduğu bütün ürünlerin -core versiyonlarını geliştirmeye devam ediyor ve Entity Framework Core da bunlardan bir tanesi. En son 2.1 versiyonu ile birlikte benchmark testlerinde en hızlı orm olarak karşımıza çıktı. Bizde bu yazımızda entity framework 2.1 kullanarak Generic Repository Pattern ile birlikte bir Asp.Net Core 2.1 WebApi uygulaması geliştireceğiz.

Proje Oluşturulması

İlk olarak vs'da EfCoreWithWebApiSample adında versiyon olarak Asp.Net Core 2.1 seçerek bir Web Api Application oluşturalım.

Not: Geliştirmeye başlamadan önce makinanızda .Net Core sdk 2.1.3 rc1 ve host edebilmemizi sağlayan .Net hosting 2.1.0 rc1 kurulumlarının olması gerekmekte.

DbContext-Entity Tanımlaması

Api projemizde bir ProductDbContext'i ile product database'inde bulunan ürünler için CRUD işlemlerini içeren api end-point'leri yer alacaktır. Bunun için ilk olarak projemizde ProductDbContext'ini ve Product entity sınıfını oluşturalım.

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

        public DbSet<Product> Product { get; set; }
        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
        }
    }

    public class Product
    {
        [Key]
        public Guid Id { get; set; }
        public string Name { get; set; }
    }

Yukarıda görüldüğü üzre context ve entity tanımlamalarını yaptık şimdi ise ProductDbContext'i Startup.cs içerisinde service olarak ekleme işlemini yapalım. Bunun için projemizde yer alan appsettings.json dosyasına connstring'i aşağıdaki gibi tanımlayalım ve sonrasında Startup.cs'de yer alan ConfigureServices metodu içerisinde context'i servislere ekleyelim.

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ProductDbConnString": "Server=.;Initial Catalog=productdb;Persist Security Info=False;User ID=productuser;Password=qwerty135-;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"
}
   public void ConfigureServices(IServiceCollection services)
   {
       services.AddDbContext<ProductDbContext>(options =>
           options.UseSqlServer(Configuration.GetSection("ProductDbConnString")));

       services.AddMvc();
   }

Generic Repository Oluşturulması

Repository katmanı doğrudan context'i constructor injection yöntemi ile alarak database CRUD işlemlerini yapmamızı sağlayacak olan katman. Bunun için ilk olarak IGenericRepository adında bir interface tanımlayalım.

   public interface IGenericRepository<T> where T : class, IEntity
    {
      Guid Save(T entity);
      T Get(Guid id);
      void Update(T entity);
      void Delete(Guid id);
      IQueryable<T> All();
      IQueryable<T> Find(Expression<Func<T, bool>> predicate);
   }

Bu interface'e ait abstract GenericRepository sınıfını aşağıdaki gibi IGenericRepository interface'inden implement ederek metotlarını oluşturalım.

public abstract class GenericRepository<T> : IGenericRepository<T> where T : class, IEntity
{
    private readonly ProductDbContext _dbContext;
    private readonly DbSet<T> _dbSet;

    protected GenericRepository(ProductDbContext dbContext)
    {
        this._dbContext = dbContext;
        this._dbSet = _dbContext.Set<T>();
    }

    public Guid Save(T entity)
    {
        entity.Id = Guid.NewGuid();
        _dbSet.Add(entity);

        return entity.Id;
    }

    public T Get(Guid id)
    {
        return _dbSet.Find(id);
    }

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

    public void Delete(Guid id)
    {
        var entity = Get(id);
        _dbSet.Remove(entity);
    }

    public IQueryable<T> All()
    {
        return _dbSet.AsNoTracking();
    }

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

Generic Repository için gerekli olan base sınıf ve interface'i yukarıdaki gibi tanımladık. Şimdi sırada Product entity'si için kullanacağımız ProductRepository ve onun interface'i var.

public interface IProductRepository : IGenericRepository<Product>
{ }

public class ProductRepository : GenericRepository<Product>, IProductRepository
{
    public ProductRepository(ProductDbContext dbContext) : base(dbContext)
    {
    }
}

Service Layer Oluşturulması

Service layer controller ile repository arasında kullanacağımız katman olacak ve uygula için business'ların bulunduğu katmanda diyebiliriz. Bunun için aşağıdaki gibi IProductService ve onun implementasyonu ile birlikte request-response dto sınıflarını oluşturalım.

public interface IProductService
{
    GetAllProductResponse GetAllProducts();
    void AddProduct(AddProductRequest reqModel);
}

public class ProductService : IProductService
{
    private readonly IProductRepository _productRepository;
    public ProductService(IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }

    public GetAllProductResponse GetAllProducts()
    {
        var result = _productRepository.All();
        var mappedList = new List<ProductDto>();

        foreach (var item in result)
        {
            mappedList.Add(new ProductDto { Id = item.Id, Name = item.Name });
        }

        return new GetAllProductResponse
        {
            ProductList = mappedList
        };
    }

    public void AddProduct(AddProductRequest reqModel)
    {
        _productRepository.Save(new Product { Name = reqModel.Name });
    }
}

Controller'a geçmeden şu ana kadar oluşturduğumuz dependency'leri inject edelim. Bunun için Startup.cs içerisinde yer alan ConfigureServices metodu içerisinde aşağıdaki tanımlamaları yapalım.

   services.AddScoped<IProductRepository, ProductRepository>();
   services.AddScoped<IProductService, ProductService>();

Api Controller Oluşturulması

Son adım olarak ise service'de yer alan bu iki metot için end-ponit'leri oluşturmak var. Bunun için projede yer alan Controller klasörü içerisine ProductController adında bir Controller ekleyelim ve aşağıdaki 2 end-point'i tanımlayalım.

[Route("api/[controller]")]
[ApiController]
public class ProductController : ControllerBase
{
    private readonly IProductService _productService;
    public ProductController(IProductService productService)
    {
        _productService = productService;
    }

    [HttpGet]
    public ActionResult<GetAllProductResponse> GetAll()
    {
        var response = _productService.GetAllProducts();
        return Ok(response);
    }

    [HttpPost]
    public ActionResult Post([FromBody] AddProductRequest reqModel)
    {
        _productService.AddProduct(reqModel);
        return Ok();
    }
}

Geliştirmelerimiz bu kadardı. Entity Framework Core ve Asp.Net Core Web Api kullanarak uçtan uca bir ProductApi oluşturduk ve data access layer için Generic Repository Pattern'den faydalandık. 

Yukarıda da bahsettiğim gibi Entity Framework Core benchmark testlerinde en performanslı orm olarak karşımıza çıkmakta ve microsoft'un core çatısı altında en çok önem verdiği ürünlerin başında gelmekte. Sizlerde bu yazımızda olduğu gibi hızlı bir şekilde uçtan uca bir api projesi geliştirebilirsiniz.

ElasticSearch Client Using Nest, ElasticSearch Net, GenericRepository, Nancy

Daha önceki elasticsearch ile ilgili yazımızda ElasticSearch Nedir ? Windows Üzerinde Kurulumu konularına değinmiştik. Bu yazımızda ise bir elastic search client uygulaması geliştireceğiz ve bu uygulamayı geliştirirken ElasticSearch.Net-Nest ve Nancy kütüphanelerinden faydalanacağız.

Elasticsearch java dilinde open-source olarak geliştirilen, dağıtık mimariye uygun, kolay ölçeklenebilir, enterprise düzeyde bir big-data arama motorudur bizlere sahip olduğu. Sahip olduğu API ile rest-call yaparak birçok crud ve çeşitli query-filter işlemlerini yapabilmemizi sağlamaktadır. .Net uygulamalarında bu api yi doğrudan kullanabilmemizi sağlayan nuget üzerinden indirilip kullanılabilen ElasticSearch.Net ve Nest adında 2 kütüphane mevcut. Client uygulamamızı geliştirirken bu 2 kütüphaneyi kullanacağız.

Geliştirmiş olduğumuz bu uygulamayı da host ederken daha önceki Nancy Nedir (NancyFx) yazısında detaylıca bahsettiğimiz Nancy framework kullanacağız.

Projemiz şu şekilde olsun yukarıda bahsettiğimiz gibi bir elasticsearch client uygulaması oluşturalım ve bu uygulama üzerinden product_index adında bir index oluşturarak bu index'e Product modellerimizi document olarak insert edelim sonrasında bu index üzerinde select update delete gibi işlemler yapalım.

İlk olarak vs da ElasticSearchClient adında bir console app. oluşturalım. Sonrasında proje referanslarına sağ tıklayarak nuget üzerinden sırasıyla aşağıdaki kütüphaneleri indirip kuralım.

1-Nest 

Install-Package NEST

2-ElasticSearch.Net

Install-Package Elasticsearch.Net

3-Nancy

Install-Package Nancy 

4-Nancy.Hosting.Self

Install-Package Nancy.Hosting.Self

Yukarıdaki bu 4 paketi kurduktan sonra projede yüklü olan nuget paketleri listesi aşağıdaki gibi listelenecektir. Ek olarak Nest paketi Newtonsoft paketine depended olduğundan listede Newtonsoft.Json paketide bulunacaktır.

İlgili paketleri projemize kurduktan sonra Product entity'sini ve projemizde yer alacak bütün entity'ler de bulunması gereken Id bilgisini içeren BaseEntity ve Product sınıflarını aşağıdaki gibi oluşturalım.

    public abstract class BaseEntity
    {
        public Guid Id { get; set; }
    }

    public class Product : BaseEntity
    {
        public string Name { get; set; }
        public int Quantity { get; set; }
        public decimal Price { get; set; }
    }

Projede kullanılanılacak bazı appSettings değerleri ise aşağıdaki gibidir. Bunları config dosyamıza ekleyelim yada kod içinde doğrudan da yazabilirsiniz. Geliştirmeyi local de yaptığımızdan es'e ait url localhost şeklinde tanımlı ancak bambaşka bir server'da da elasticsearch'ü host edebilirsiniz.

  <appSettings>
    <add key="NancyAddress" value="http://localhost:7880" />
    <add key="ElasticSearchApiAddress" value="http://localhost:9200" />
    <add key="ProductIndexName" value="product_index" />
  </appSettings>

Örneğin data katmanı için tıpkı bir db ile veri alış verişi yapıyormuş gibi bir GenericRepository katmanı oluşturarak tasarlayalım ve elasticsearch'e yapacağımız CRUD metotlarını bu class içerisine tanımlayalım. 

    public interface IGenericRepository<T> where T : class
    {
        Guid Save(T entity);
        T Get(Guid id);
        void Update(T entity);
        bool Delete(Guid id);
        IEnumerable<T> All();
        IEnumerable<T> Search(BaseSearchModel search);
    }

    public abstract class GenericRepository<T> : IGenericRepository<T> where T : BaseEntity
    {
        private readonly ElasticClient _elasticClient;
        private readonly string _indexName;

        protected GenericRepository(string indexName)
        {
            _elasticClient = ElasticSearchClientHelper.CreateElasticClient();
            _indexName = indexName;
        }
    }

Yukarıda tanımladığımız GenericRepository contructor inj. parameter olarak ilgili tipe ait index adı bilgisini almaktadır ve nuget'ten kurduğumuz ElasticSearchClient nesnesini initialize etme işini ElasticSearchClientHelper adındaki class'ta bulunan metoda verdik.

    public static class ElasticSearchClientHelper
    {
        public static ElasticClient CreateElasticClient()
        {
            var node = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));
            var settings = new ConnectionSettings(node);
            return new ElasticClient(settings);
        }

        public static void CheckIndex<T>(ElasticClient client, string indexName) where T : class
        {
            var response = client.IndexExists(indexName);
            if (!response.Exists)
            {
                client.CreateIndex(indexName, index =>
                   index.Mappings(ms =>
                       ms.Map<T>(x => x.AutoMap())));
            }
        }
    }

Şimdi sırasıyla interface içerisinde bulunan metotlara ait elasticsearch geliştirmelerini baserepository'miz içerisinde yapalım.

İlk olarak All() metodu ile başlayalım. Bu metot isminden de anlaşılacağı üzre elasticsearch belirtmiş olduğumuz indexde ilgili tipimize ait bulunan bütün kayıtları return edecektir.

        public IEnumerable<T> All()
        {
            return _elasticClient.Search<T>(search =>
                search.MatchAll().Index(_indexName)).Documents;
        }

Get(Guid id) metodu ise index'de bulunan kaydı aldığı id parametresi ile bulup return eder bulamadığı durumda exception throw eder.

        public T Get(Guid id)
        {
            var result = _elasticClient.Get<T>(id.ToString(), idx => idx.Index(_indexName));
            if (!result.IsValid)
            {
                throw new Exception(result.OriginalException.Message);
            }
            return result.Source;
        }

Üçüncü olarak Delete(Guid id) metodunu yazalım.

        public bool Delete(Guid id)
        {
            var result = _elasticClient.Delete<T>(id.ToString(), idx => idx.Index(_indexName));
            if (!result.IsValid)
            {
                throw new Exception(result.OriginalException.Message);
            }
            return result.Found;
        }

Generic Save(T entity) metodu aldığı nesne için ilk olarak o index'de o tip için bir tanımlama yapılmışmı kontrol eder sonrasında ise Save işlemini yapar.

        public Guid Save(T entity)
        {
            ElasticSearchClientHelper.CheckIndex<T>(_elasticClient, _indexName);

            entity.Id = Guid.NewGuid();
            var result = _elasticClient.Index(entity, idx => idx.Index(_indexName));
            if (!result.IsValid)
            {
                throw new Exception(result.OriginalException.Message);
            }
            return entity.Id;
        }

Update(T entity) metodunu da aşağıdaki gibi tanımlayalım.

        public void Update(T entity)
        {
            var result = _elasticClient.Update(
                    new DocumentPath<T>(entity), u =>
                        u.Doc(entity).Index(_indexName));
            if (!result.IsValid)
            {
                throw new Exception(result.OriginalException.Message);
            }
        }

Son olarak ise Search metodunu oluşturalım. Elasticsearch'ün bize sunduğu en büyük avantajlardan biride çok farklı şekillerde search/query işlemleri yapabilmemizdir. Es. üzerinde Query DSL adı verilen bir search language ile json formatında farklı farklı filtrelere sahip query'ler yazabiliriz. Bu konu başlı başına ayrı bir yazı konusu olduğundan daha sonraki yazılarda değineceğiz. Şimdilik buradan bilgi alabilirsiniz. Biz projemzide search işlemi yaparken Match Query'den faydalanacağız. Search metodu BaseSearchModel adında içerisinde aşağıdaki property'leri içeren bir parametre almaktadır.

    public class BaseSearchModel
    {
        public int Size { get; set; }
        public int From { get; set; }
        public Dictionary<string, string> Fields { get; set; }
    }

Bu class içerisinde bulunan "Fields" bizim o ürün ile ilgili hangi alanları hangi değerlerle search edeceğimiz bilgisini tutacak olan bir çeşit dynamic search property'si. "Size" request başına kaç document return edecek bilgisi için. "From" property'si ise pageIndex olarak kullanacağız.

        public IEnumerable<T> Search(BaseSearchModel request)
        {
            var dynamicQuery = new List<QueryContainer>();
            foreach (var item in request.Fields)
            {
                dynamicQuery.Add(Query<T>.Match(m => m.Field(new Field(item.Key.ToLower())).Query(item.Value)));
            }

            var result = _elasticClient.Search<T>(s => s
                                       .From(request.From)
                                       .Size(request.Size)
                                       .Index(_indexName)
                                        .Query(q => q.Bool(b => b.Must(dynamicQuery.ToArray()))));

            if (!result.IsValid)
            {
                throw new Exception(result.OriginalException.Message);
            }

            return result.Documents;
        }

Yukarıda yazmış olduğumuz sorgu gönderilen request filed'larında bulunan key'i index de bulunan mapping'e de bulunan tipin field'ı value'su ise bu değere karşılık aranan değer.

Repository katmanı için base'de bulunan işlemleri içeren geliştirmeyi tamamladık. Şimdi ise sırada ProductRepository sınıfını oluşturmak var.

İlk olarak IBaseRepository<Product> interface'ini implement eden IProductRepository adındaki interface'i ve sonrasında BaseRepository<Product> abstract class'ını inherit alan ve IProductRepository interface'ini implement eden ProductRepository adındaki implementation sınıfını oluşturalım.

   public interface IProductRepository: IBaseRepository<Product>
    { }
    public class ProductRepository : BaseRepository<Product>, IProductRepository
    {
        public ProductRepository() : base("product_index")
        { }
    }

ProductRepository sınıfı BaseRepository sınıfının constructor'ını product tipine ait index ismini parametre geçerek çağırmaktadır.

Repository katmanı ile ilgili geliştirmelerimiz bitti. Şimdi ise araya bir Service layer yazalım. ProductService adındaki sınıf tanımlayacağımız end-point'ler ile doğrudan iletişim kurarak repository için gerekli crud işlemlerini call etmeden sorumlu olacak kısaca repository'i doğrudan dışarıya açmak yerine araya bir service layer geliştiriyoruz. 

    public interface IProductService
    {
        List<Product> Search(SearchProductRequest reqModel);
        SaveProductResponse Save(SaveProductRequest reqModel);
        UpdateProductResponse Update(UpdateProductRequest reqModel);
        List<Product> GetAll();
        bool Delete(Guid productId);
    }

Yukarıda metotlara ait olan request ve response sınıflarını aşağıdaki gibi oluşturabilirsiniz yada ihtiyacınıza göre farklı şekilde de tanımlayabilirsiniz.

    public class SearchProductRequest : BaseSearchModel
    {   }
    public class SaveProductRequest
    {
        public Product Product{ get; set; }
    }
    public class UpdateProductRequest
    {
        public Product Product{ get; set; }
    }
    public class SaveProductResponse
    {
        public Product Product { get; set; }
    }
    public class UpdateProductResponse
    {
        public Product Product { get; set; }
    }

IProductService interface'ine ait implementasyonu da aşağıdaki gibi oluşturalım.

    public class ProductService : IProductService
    {
        private readonly IProductRepository _productRepository;

        public ProductService()
        {
            _productRepository = new ProductRepository();
        }

        public List<Product> Search(SearchProductRequest reqModel)
        {
            return _productRepository.Search(reqModel).ToList();
        }

        public SaveProductResponse Save(SaveProductRequest reqModel)
        {
            var entityId = _productRepository.Save(reqModel.Product);
            return new SaveProductResponse { Product = _productRepository.Get(entityId) };
        }

        public UpdateProductResponse Update(UpdateProductRequest reqModel)
        {
            _productRepository.Update(reqModel.Product);
            return new UpdateProductResponse { Product = _productRepository.Get(reqModel.Product.Id) };
        }

        public List<Product> GetAll()
        {
            return _productRepository.All().ToList();
        }

        public bool Delete(Guid productId)
        {
            return _productRepository.Delete(productId);
        }
    }

Projemiz hazır gibi tek eksik kalan şey end-point'leri tanımlayarak NancyHost'u ayağa kaldırmak. Bunun için ilk olarak ProductModule.cs sınıfını oluşturalım. Nancyfx projelerinde end-point tanımlanırken ilgili sınıf NancyModule sınıfından inherit olur ve tanımlanan bu sınıflara yaygın olarak sonuna _Module eki getirilir. Bizde Product nesnesi için gerekli olan service adreslerini tanımlayacağımız sınıfa ProductModule adını verelim.

    public class ProductModule : NancyModule
    {
        public ProductModule()
        {
            IProductService productService = new ProductService();
            
            Post["/product/save"] = parameters =>
            {
                var request = this.Bind<SaveProductRequest>();

                return productService.Save(request);
            };

            Put["/product/update"] = parameters =>
            {
                var request = this.Bind<UpdateProductRequest>();

                return productService.Update(request);
            };

            Delete["/product/delete/{productId}"] = parameters =>
            {
                var productId = parameters.productId;
                return productService.Delete(productId);
            };

            Post["/product/search"] = parameters =>
            {
                var request = this.Bind<SearchProductRequest>();

                return productService.Search(request);
            };

            Get["/product/all"] = parameters =>
            {
                return productService.GetAll();
            };
        }
    }

Yukarıda görüldüğü üzre save-update-delete-search ve all metotlarını tanımladık. Post metodunda gönderilen request'i almak için this.Bind<RequestModel>() yazarak gönderilen json request'i model'imize bind edebiliriz. Delete metodunda da query-string kuıllanım örneği bulunmakta. Query string de gönderilen productId değerini almak için Delete["/product/delete/{productId}"] şeklinde kullanabiliriz.

İkinci olarak NancyHost'u ayağa kaldırmak var. Program.cs içerisinde aşağıdaki gibi gerekli konfigurasyonları yapalım ve uygulamamızı http://localhost:7880 adresinde network'e açalım.

    class Program
    {
        private readonly NancyHost _nancy;

        public Program()
        {
            var uri = new Uri("http://localhost:7880");
            var hostConfigs = new HostConfiguration { UrlReservations = { CreateAutomatically = true } };
            _nancy = new NancyHost(uri, new DefaultNancyBootstrapper(), hostConfigs);
        }

        private void Start()
        {
            _nancy.Start();
            Console.WriteLine($"Started listennig address {"http://localhost:7880"}");
            Console.ReadKey();
            _nancy.Stop();
        }

        static void Main(string[] args)
        {
            var p = new Program();
            p.Start();
        }
    }

İlk olarak Program const. metodu içerisinde configde yazılı olan nancyHostUrl'ini alıp Nancy'den bize bu  url'i reserve etmesini istiyoruz. Sonrasında ise NancyHost sınıfını initialize ediyoruz. Sonrasında Main function içerisinde start metodunu çağırarak projemizi run ediyoruz ve uygulamamız http://localhost:7880 adresinde host ediliyor olacak.

Şimdi postman veya herhangi bir rest-call app. kullanarak yazmış olduğumuz end-point'lere request atalım.

İlk olarak Save işlemiyle başlayalım; http://localhost:7880/product/save adresine aşağıdaki gibi bir Post işlemi yapalım.

Request;

{
	"Product":{
		"Name":"Tomato Soup",
		"Quantity":13,
		"Price":5.29
	}
}

Response olarak ise bize save işlemi yapılan nesneyi return edecektir.

{
    "product": {
        "name": "Tomato Soup",
        "quantity": 13,
        "price": 5.29,
        "id": "a55841a4-3817-475c-bc68-aafcbd452bf8"
    }
}

İkinci olarak kaydettiğimiz bu nesneyi http://localhost:7880/product/update adresine httpPut request'i göndererek Quntity bilgisi 18 olarak Update edelim.

{
	"Product":{
		"Name":"Tomato Soup",
		"Quantity":18,
		"Price":5.29,
		"Id":"a55841a4-3817-475c-bc68-aafcbd452bf8"
	}
}

Response olarak ise yine save işleminde olduğu gibi update edilen nesneyi bize return edecektir.

İki ürün daha Save edelim sonrasında ilk kaydettiğimiz ürünü silelim.

{
	"Product":{
		"Name":"Cheese",
		"Quantity":20,
		"Price":11.49
	}
}
{
	"Product":{
		"Name":"Tomato",
		"Quantity":30,
		"Price":0.49
	}
}

Silme işlemi için ise delete metodunun ProductModule içerisinde Http Delete request'i kabul ettiğini belirtmiştik. http://localhost:7880/product/delete/a55841a4-3817-475c-bc68-aafcbd452bf8 adresine sonuna Id bilgisini ekleyerek httpDelete request'i atıyoruz ve cevap olarak bize true string değerini dönüyor.

Son olarak ise Search işlemi yapalım. Name değeri Cheese olan ürünü search edelim. Bunun için aşağıdaki gibi bir request'i mizi hazırlayalım. 

 

Search işlemi için tam olmasa da bir çeşit dynamic query yazabileceğimiz bir key-value dictionary parameteresi istedik request te. Bu işimizi görüyor olsada aslında pekte güvenilir sayılmaz veya tamda ihtiyacımızı karşılamayabilir. ElasticSearch bize multi-field query yazabileceğimiz bir api sağlıyor ancak repsoitory katmanınızı dışarı açtığınızı düşündüğünüzde işin içine güvenlik girdiğinde tercih etmememiz gereken bir yapı haline geliyor yada kullandığınız taktirde bu dictionary içerisinde bulunan field'lara bir çeşit filtering uygulayarak daha güvenli hale getirmemiz gerekmekte.

Diğer bir seçenek ise Search kısmını generic değilde her repository'e ait model için yazılmasını sağlamak. Bunun için ISearchableRepository adında bir interface tanımlayarak içerisine IEnumerable<T> Search(T search) gibi bir metot eleyerek kullanmak isteyen repository bu interface'i implement eder ve kendi query'sini yazabilir.

    public interface ISearchableRepository<T>
    {
        IEnumerable<T> Search(T search);
    }

IProductRepository interface'i ve ProductRepository class'ının ise son olarak aşağıdaki gibi olacaktır. 

    public interface IProductRepository: IBaseRepository<Product>, ISearchableRepository<Product>
    { }
    public class ProductRepository : BaseRepository<Product>, IProductRepository
    {
        public ProductRepository() : base("product_index")
        { }

        public IEnumerable<Product> Search(Product search)
        {
            throw new System.NotImplementedException();
        }
    }

İhityaç duyduğunuz gibi yukarıdaki Search metodunun içerisini doldurarak daha secure bir şekilde data-layer'ı dışarıya açabiliriz.

Elasticsearch client uygulamamız ile ilgili geliştirme şimdilik bu kadar. Yazının başında da belirttiğim gibi E.S son derece esnek query'ler yazabileceğimiz şahane bir api diline sahip. İhtiyaca göre çok daha farklı türlerde filtering ve searching işlemleri yapabiliriz. Daha detaylı bilgi için elastic.co adresine göz atabilirsiniz.

Unit of Work Interceptor, Castle Windsor, NHibernate ve Generic Repository

Unit of Work Pattern Martin Fowler'ın 2002 yılında yazdığı Patterns of Enterprise Application Architecture kısaca PoEAA olarak da adlandırılan kitabında bahsetmesiyle hayatımıza girmiş bir pattern dır.

M.Fowler kitabında UoW'ü şu şekilde tanımlar,

Maintains a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems.

Unit of Work; database'de execute etmemiz gereken bir dizi işlemin yani birden fazla transaction'a ihtiyaç duyarak yapacağımız işlemler (Create, Update,  Insert, Delete, Read) dizinini success veya fail olması durumunda tek bir unit yani tek bir birim olarak ele alıp yönetilmesini sağlayan pattern dir.

Diğer bir değişle; ardı ardına çalışması gereken 2 sql transaction var ve bunlardan biri insert diğeride update yapsın. İlk olarak insert yaptınız ve hemen sonrasında update sorgusunu çalıştırdınız fakat update işlemi bir sorun oluştu ve fail oldu. Unit of work tam da bu sırada araya girerek bu iki işlemi bir birimlik bir işlem olarak ele alır ve normal şartlarda ikisininde success olması durumunda commit edeceği sessino'ı update işlemi fail verdiğinden ilk işlem olan insert'ü rollback yapar ve db de yanlış veya eksik kayıt oluşmasını engeller. Yada ikiside success olduğunda session'ı commit ederek consistency'i sağlar.

Örnek üzerinden ilerleyecek olursak; bir data-access katmanımız olsun ve ORM olarak da NHibernate'i kullanıyor olalım. Projemizde IoC container olarak da Castle Windsor'ı entegre edelim. İlk olarak Vs'da "UoW_Sample" adında bir Empty Asp.Net Web Api projesi oluşturalım ve sonrasında nugetten Sırasıyla Fluent-NHibernate ve Castle Windsor'ı yükleyelim.

Case'imiz şu şekilde olsun; User ve Address adında tablolarımız var ve AddNewUser adında bir endpoint'ten hem kullanıcı hemde address bilgileri içeren bir model alarak sırasıyla User'ı ve Address'i insert etmeye çalışalım. User'ı insert ettikten sonra Address insert sırasında bir sorun oluşsun ve UoW araya girerek kaydedilecek olan user'ı da rollback yapsın.

Öncelikle User ve Address modellerimizi aşağıdaki gibi oluşturalım.

public class User
   {
       public virtual int Id { get; set; }
       public virtual string Name { get; set; }
       public virtual string SurName { get; set; }
   }
public class Address
   {
       public virtual int Id { get; set; }
       public virtual string CityCode { get; set; }
       public virtual string DistrictCode { get; set; }
       public virtual string Description { get; set; }
       public virtual int UserId { get; set; }
   }

Bu modellere ait Nhibernate Mapping'lerini de aşağıdaki gibi oluşturalım.

public class UserMap : ClassMap<User>
{
    public UserMap()
    {
        Id(x => x.Id);
        Map(x => x.Name);
        Map(x => x.SurName);
        Table("Users");
    }
}
public class AddressMap : ClassMap<Address>
{
    public AddressMap()
    {
        Id(x => x.Id);
        Map(x => x.CityCode);
        Map(x => x.DistrictCode);
        Map(x => x.Description);
        Map(x => x.UserId);
        Table("Address");
    }
}

Repository kullanımı için aşağıdaki gibi generic repo class'larını oluşturalım. Bu arayüz üzerinden db de bulunan tablolarımız için CRUD işlemlerini yapacağız.

    public interface IRepository<T> where T : class
    {
        T Get(int id);
        IQueryable<T> SelectAll();
        T GetBy(Expression<Func<T, bool>> expression);
        IQueryable<T> SelectBy(Expression<Func<T, bool>> expression);
        int Insert(T entity);
        void Update(T entity);
    }
  public abstract class BaseRepository<T> : IRepository<T> where T : class
    {
        public ISessionFactory SessionFactory { get; private set; }

        public ISession _session
        {
            get { return this.SessionFactory.GetCurrentSession(); }
        }

        public BaseRepository(ISessionFactory sessionFactory)
        {
            SessionFactory = sessionFactory;
        }

        public T Get(int id)
        {
            return _session.Get<T>(id);
        }

        public IQueryable<T> SelectAll()
        {
            return _session.Query<T>();
        }

        public T GetBy(Expression<Func<T, bool>> expression)
        {
            return SelectAll().Where(expression).SingleOrDefault();
        }
        public IQueryable<T> SelectBy(Expression<Func<T, bool>> expression)
        {
            return SelectAll().Where(expression).AsQueryable();
        }

        public int Insert(T entity)
        {
            var savedId = (int)_session.Save(entity);
            _session.Flush();
            return savedId;
        }

        public void Update(T entity)
        {
            _session.Update(entity);
            _session.Flush();
        }
    }

Tablolarımıza karşılık UserRepository ve AddressRepository class'larını arayüzleri ile birlikte aşağıdaki gibi tanımlayalım.

    public interface IUserRepository : IRepository<User>
    { }

    public class UserRepository : BaseRepository<User>, IUserRepository
    {
        public UserRepository(ISessionFactory sessionFactory) : base(sessionFactory)
        {
        }
    }
    public interface IAddressRepository : IRepository<Address>
    { }

    public class AddressRepository : BaseRepository<Address>, IAddressRepository
    {
        public AddressRepository(ISessionFactory sessionFactory) : base(sessionFactory)
        {
        }
    }

Repository'lerimiz direkt olarak api'ın controller'ları ile haberleşmesini istemediğimizden bir service katmanımızın olduğunu düşünerek UserService adında doğrudan Repository'ler ile iletişim kurabilen class'ımızı oluşturalım ve Unit Of Work interceptor'ı da bu service class'ları seviyesinde container'a inject edeceğiz.

Projede yer alan service'leri bir çeşit flag'lemek adına IApiService adında bir base interface tanımlayalım.Bu interface'i daha sonrasında container'a bütün service'leri register etmede de kullanacağız.

    public interface IApiService
    {   }

    public interface IUserService : IApiService
    {
        void AddNewUser(AddNewUserRequest reqModel);
    }
    public class UserService : IUserService
    {
        private readonly IUserRepository _userRepository;
        private readonly IAddressRepository _addressRepository;

        public UserService(IUserRepository userRepository, IAddressRepository addressRepository)
        {
            _userRepository = userRepository;
            _addressRepository = addressRepository;
        }

        public void AddNewUser(AddNewUserRequest reqModel)
        {
            var user = new User { Name = reqModel.User.Name, SurName = reqModel.User.SurName };
            var userId = _userRepository.Insert(user);

            var address = new Address { UserId = userId, CityCode = reqModel.Address.CityCode, Description = reqModel.Address.Description, DistrictCode = reqModel.Address.DistrictCode };
            _addressRepository.Insert(address);
        }
    }

    public class AddNewUserRequest
    {
        public UserDto User { get; set; }
        public AddressDto Address { get; set; }
    }
    public class UserDto
    {
        public string Name { get; set; }
        public string SurName { get; set; }
    }
    public class AddressDto
    {
        public string CityCode { get; set; }
        public string DistrictCode { get; set; }
        public string Description { get; set; }
    }

Yukarıda end-point'imizin alacağı request model ve onun dto class'larını da oluşturduk. Şimdi ise api end-point'imizi tanılayalım.  UserController adında client'ların call yapacağı controller'ımız aşağıdaki gibi olacaktır.

    public class UserController : ApiController
    {
        private readonly IUserService _userService;

        public UserController(IUserService userService)
        {
            _userService = userService;
        }

        [HttpPost]
        public virtual HttpResponseMessage AddNewUser(AddNewUserRequest reqModel)
        {
            _userService.AddNewUser(reqModel);
            return Request.CreateResponse();
        }
    }

Geliştirmemiz gereken 2 yer kaldı Castle Windsor implementasyonu ve UnitOfWork Interceptor oluşturulması. Projemizde her şeyi interface'ler üzerinden yaptık ve constructor injection'dan faydalandık. Şimdi ise Repository, Service ve Controller'lar için bağımlılıkları enjekte edelim ve UnitOfWork Interceptor'ı oluşturalım. 

İlk olarak NHibernateInstaller.cs'i tanımlayalım. Burda web.config/app.config dosyamızda "ConnString" key'i ile kayıtlı database conenction string'imiz olduğunu varsayalım ve aşağıdaki gibi tanımlamalarımızı yapalım.

    public class NHibernateInstaller : IWindsorInstaller
    {
        public void Install(IWindsorContainer container, IConfigurationStore store)
        {
            var sessionFactory = Fluently.Configure()
               .Database(MsSqlConfiguration.MsSql2012.ConnectionString(c => c.FromConnectionStringWithKey("ConnString")).ShowSql())
               .Mappings(m => m.FluentMappings.AddFromAssemblyOf<UserMap>())
               .ExposeConfiguration(cfg => new SchemaUpdate(cfg).Execute(false, true))
                        .ExposeConfiguration(cfg =>
                        {
                            cfg.CurrentSessionContext<CallSessionContext>();
                        })
               .BuildSessionFactory();

            container.Register(
                Component.For<ISessionFactory>().UsingFactoryMethod(sessionFactory).LifestyleSingleton());
        }
    }

İkinci olarak RepositoryInstaller.cs'i oluşturalım. Bu installer ile projemizde bulunan bütün repository interfacelerini ve onların implementasyonlarını container'a register etmiş olucaz. Her bir repository'i ayrı ayrı register etmek yerine bütün repository'lerimiz IRepository interface'in den türediğinden container'a IRepository'i implement eden bütün class'ları register etmesini belirteceğiz.

    public class RepositoryInstaller : IWindsorInstaller
    {
        public void Install(IWindsorContainer container, IConfigurationStore store)
        {
            container.Register(
                Classes.FromThisAssembly()
                    .Pick()
                    .WithServiceAllInterfaces()
                    .LifestylePerWebRequest()
                    .Configure(x => x.Named(x.Implementation.Name))
                          .ConfigureIf(x => typeof(IRepository<>).IsAssignableFrom(x.Implementation), null));
        }
    }

Üçüncü olarak ServiceInstaller.cs class'ını tanımlayalım ancak öncesinde yukarıda da belirttiğimiz gibi UnitOfWork'ü service seviyesinde container'a register edeceğiz. Sebebi ise repository'e erişimimiz service class'ları üzerinden olması. UnitOfWork'ü de interceptor olarak yaratacağız ve böylelikle service metoduna girerken session'ı bind edip metot içerisinde herhangi bir exception aldığında rollback yapacağız yada herhangi bir sorun yoksada session'ı commit edip query'leri execute etmesini sağlayacağız. Aşağıda ilk olarak unitofwork manager ve interceptor class'larını oluşturalım.

    public interface IUnitOfWorkManager
    {
        void BeginTransaction();
        
        void Commit();
        
        void Rollback();
    }
    public class UnitOfWorkManager : IUnitOfWorkManager
    {
        public static UnitOfWorkManager Current
        {
            get { return _current; }
            set { _current = value; }
        }
        [ThreadStatic]
        private static UnitOfWorkManager _current;
        
        public ISession Session { get; private set; }
        
        private readonly ISessionFactory _sessionFactory;
        
        private ITransaction _transaction;
        
        public UnitOfWorkManager(ISessionFactory sessionFactory)
        {
            _sessionFactory = sessionFactory;
        }
        
        public void BeginTransaction()
        {
            Session = _sessionFactory.OpenSession();
            CurrentSessionContext.Bind(Session);
            _transaction = Session.BeginTransaction();
        }

        public void Commit()
        {
            try
            {
                _transaction.Commit();
            }
            finally
            {
                Session.Close();
            }
        }

        public void Rollback()
        {
            try
            {
                _transaction.Rollback();
            }
            finally
            {
                Session.Close();
            }
        }
    }

 Yukarıda oluşturduğumuz manager'ı kullanarak UnitOfWorkInterceptor'ı da aşağıdaki gibi tanımlayalım.

    public class UnitOfWorkInterceptor : Castle.DynamicProxy.IInterceptor
    {
        private readonly ISessionFactory _sessionFactory;

        public UnitOfWorkInterceptor(ISessionFactory sessionFactory)
        {
            _sessionFactory = sessionFactory;
        }

        public void Intercept(IInvocation invocation)
        {
            try
            {
                UnitOfWorkManager.Current = new UnitOfWorkManager(_sessionFactory);
                UnitOfWorkManager.Current.BeginTransaction();

                try
                {
                    invocation.Proceed();
                    UnitOfWorkManager.Current.Commit();
                }
                catch
                {
                    UnitOfWorkManager.Current.Rollback();
                    throw new Exception("Db operation failed.");
                }
            }
            finally
            {
                UnitOfWorkManager.Current = null;
            }
        }
    }

Yukarıda tanımladığımız interceptor'ı aşağıdaki gibi service'leri register ederken bu service class'larına ait metotlar için UnitOfWorkInterceptor'ı configure etmesini belirteceğiz.

    public class ServiceInstaller : IWindsorInstaller
    {
        public void Install(IWindsorContainer container, IConfigurationStore store)
        {
            container.AddFacility<TypedFactoryFacility>();

            container.Register(
                Classes.FromAssemblyContaining<UserService>()
                    .Pick()
                    .WithServiceAllInterfaces()
                    .LifestylePerWebRequest()
                    .Configure(x => x.Named(x.Implementation.Name))
                          .ConfigureIf(x => typeof(IApiService).IsAssignableFrom(x.Implementation),
                            y => y.Interceptors<UnitOfWorkInterceptor>()));

        }
    }

Projemiz bir Web Api projesi olduğundan controller'lar ile ilgili container registration işlemleri için gerekli olan WebApiControllerInstaller.cs class'ı ve ControllerActivator.cs class'ı tanımlamaları da aşağıdaki gibidir.

    public class ApiControllerActivator : IHttpControllerActivator
    {
        private readonly IWindsorContainer _container;

        public ApiControllerActivator(IWindsorContainer container)
        {
            _container = container;
        }

        public IHttpController Create(
            HttpRequestMessage request,
            HttpControllerDescriptor controllerDescriptor,
            Type controllerType)
        {
            var controller =
                (IHttpController)this._container.Resolve(controllerType);

            request.RegisterForDispose(
                new Release(
                    () => this._container.Release(controller)));

            return controller;
        }

        private class Release : IDisposable
        {
            private readonly Action _release;

            public Release(Action release)
            {
                _release = release;
            }

            public void Dispose()
            {
                _release();
            }
        }
    }
    public class WebApiControllerInstaller : IWindsorInstaller
    {
        public void Install(IWindsorContainer container, IConfigurationStore store)
        {
            container.Register(Classes.FromThisAssembly()
                .BasedOn<ApiController>()
                .LifestylePerWebRequest());
        }
    }

Geldik son adıma. Yukarıda tanımladığımız bütün installer class'larını container'a install etmeye. Bunun için projede yer alan Global.asax.cs içerinde yer alan Application_Start metodu içerisine aşağıdaki gibi installation işlemlerini yapalım.

        protected void Application_Start()
        {
            var container = new WindsorContainer();
            container.Register(Component.For<UnitOfWorkInterceptor>().LifestyleSingleton());
            container.Install(new ServiceInstaller());
            container.Install(new RepositoryInstaller());
            container.Install(new NHibernateInstaller());
            container.Install(new WebApiControllerInstaller());
            GlobalConfiguration.Configuration.Services.Replace(
                typeof(IHttpControllerActivator),
                new ApiControllerActivator(container));
            GlobalConfiguration.Configure(WebApiConfig.Register);
        }

Postman üzerinden aşağıdaki gibi end-point'imize call yapalım ve hem iki insert işlemininde başarılı olduğu case'i hemde user insert başarılı olduktan sonra address insert sırasında bir hata verdirip ilk işleminde rollback olduğu case'i oluşturup gözlemleyebiliriz.

Unit of Work pattern gözlemlediğim kadarıyla genellikle projede her query execution sırasında o satırları try-catch e alarak değişik logic'ler uygulanarak yapılıyor ancak. Aspect oriented'ın bize sağladıklarından faydalanarak bir interceptor ile projede her yerde kullanabileceğimiz basit bir infrastructure geliştirebiliriz. Bu pattern ile aynı işleve hizmet eden birden fazla küçük küçük db transaction'ını tek bir unit olarak yönetip dirty data'nın da önüne geçmiş oluyoruz.