Caner Tosuner

Leave your code better than you found it

NULL Object Pattern Nedir ?

NULL Object Pattern Gang of Four’s Design Patterns kitabında anlatılmış olup behavioral design pattern'ler den biridir. Bu pattern'in amacı uygulama içeresinde null objeler return etmek yerine ilgili tipin yerine geçen ve expected value'nun null objesi olarak kabul edilen tipi geriye dönmektir diğer bir değişle null yerine daha tutarlı nesneler dönmektir. Bu nesne asıl return edilmesi gereken nesnenin null değeri olarak kabul edilirken onunla aynı özelliklere sahip değildir, çok daha az bilgi içermektedir. NULL Object Pattern , süreli olarak null kontrolü yaparak hem server-side hemde client-side için boilerplate code yazmaya engel olmak amacıyla ortaya çıkmış bir pattern dir.

Platform yada dil farketmeksizin geliştirme yaparken sürekli olarak nullreferenceexception aldığımız durumlar olmuştur bu durumdan kurtulmak adına obj null mı değil mi diye bir sürü if/else kontrolleri yaparız. Bu pattern'i kullanarak biraz sonraki örnekte yapacağımız gibi boilerplate code'lar yazmaktan nasıl kurtulabiliriz bunu inceleyeceğiz.

Örneğimizi 2 şekilde ele alalım. İlk olarak geriye null değer return ederek çoğunlukla nasıl geliştirme yapıyoruz o case'i ele alalım, sonrasında ise NULL Object Pattern kullanarak nasıl geliştirebiliriz onu inceleyelim.

Öncelikle Customer adında bir nesnemiz var ve repository kullanarak geriye bu nesneyi return edelim. 

    public class Customer
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastNae { get; set; }
        public int NumberOfChildren { get; set; }
        public string GetFullName()
        {
            return FirstName + " " + LastName;
        }
    }

Service katmanında generic bir repository yapımız varmış gibi varsayalım ve repository üzerinden GetCustomerByFirstName adında bir metot tanımlayalım.

public class CustomerService
    {
        public Customer GetCustomerByFirstName(string firstName)
        {
            return _customerRepository.List(c => c.FirstName == firstName).FirstOrDefault();
        }
    }

Sonrasında yukarıda tanımladığımız metodu call yaparak geriye customer objesini dönelim ve bazı değerleri ekrana yazdıralım.

   var customerService = new CustomerService();
   var customer = customerService.GetCustomerByFirstName("tosuner");
   Console.WriteLine("FullName : " + customer.GetFullName() + "\nNumber Of Childreen:" + customer.NumberOfChildren);

Yukarıdaki gibi customer'ın null geldiği durumda exception thrown 'system.nullreferenceexception' hatasını çoktan aldık gibi yani memory'de değeri assing edilmemiş bir yere erişmeye çalışıyoruz. Peki çözüm olarak ne yapabilirdik, ilk akla gelen aşağıdaki gibi bir kontrol olacaktır.

    var customerService = new CustomerService();
    var customer = customerService.GetCustomerByFirstName("tosuner");
    if (customer != null)
    {
        Console.WriteLine("FullName : " + customer.GetFullName() + "\nNumber Of Childreen:" + customer.NumberOfChildren);
    }
    else
    {
        Console.WriteLine("Name : Customer Not Found !" + "\nNumber Of Childreen: 0");
    }

Yukarıdaki gibi bir çözüme gittiğimizde customer objesini get ettiğimiz bir sürü yer olduğunu düşünün ve her yerde sürekli olarak null kontrolü yapıp sonrasında console'a değerleri yazıyor oluruz. Aslında bu şu deme değil;"null kontrolü yapma arkadaş !" kesinlikle bu değil tabikide ihtiyaç duyulan yerlerde bu kontrol yapılmalı hatta birçok case'de null ise throw new CustomBusinessException() vs şeklinde exception'da throw edeceğimiz durumlar olabilir. Demek istediğim yukarıdaki gibi client'a bu kontrolü olabildiğince bırakmamak.

NULL Object Pattern uygulayarak nasıl bir çözüm getirirdik ona bakalım. İlk olarak AbstractCustomer adında base sınıfımızı oluşturalım.

    public abstract class AbstractCustomer
    {
        public abstract int Id { get; set; }
        public abstract string FirstName { get; set; }
        public abstract string LastName { get; set; }
        public abstract int NumberOfChildren { get; set; }
        public abstract string GetFullName();
    }

Sonrasında Customer objesini bu sınıftan türetelim.

    public class Customer : AbstractCustomer
    {
        public override string FirstName { get; set; }
        public override string LastName { get; set; }
        public override int NumberOfChildren { get; set; }
        public override int Id { get; set; }

        public override string GetFullName()
        {
            return FirstName + " " + LastName;
        }
    }

Şimdi ise bu pattern'in getirdiği çözüm olarak geriye null value dönmeyip asıl return edilmek istenen sınıf yerine onun null olduğunu belirten bir sınıf geriye dönelim. Bu sınıfa da NullCustomer adını verelim.

    public class NullCustomer : AbstractCustomer
    {
        public override string FirstName { get; set; }
        public override string LastName { get; set; }
        public override int NumberOfChildren { get; set; }
        public override int Id { get; set; }

        public override string GetFullName()
        {
            return "Customer Not Found !";
        }
    }

Sonrasında service katmanını aşağıdaki gibi düzenleyelim.

    public class CustomerService
    {
        public AbstractCustomer GetCustomerByFirstName(string firstName)
        {
            return _customerRepository.Where(c => c.FirstName == firstName).FirstOrDefault().GetValue();
        }
    }
    public static class CustomerExtensions
    {
        public static AbstractCustomer GetValue(this AbstractCustomer customer)
        {
            return customer == null ? new NullCustomer() : customer;
        }
    }

Yukarıdaki kod bloğunda görüldüğü üzre repository null değer dönmek yerine yeni bir NullCustomer sınıfı return edecektir.

Son adım olarak da cient tarafında yazılacak kod ise yazımızın ilk başında yazdığımız kod bloğu ile aynı.

   var customerService = new CustomerService();
   var customer = customerService.GetCustomerByFirstName("tosuner");
   Console.WriteLine("FullName : " + customer.GetFullName() + "\nNumber Of Childreen:" + customer.NumberOfChildren);

Bu pattern ile;

  • null reference kontrollerinden kurtulduk,
  • duplicate kod oranını azalttık,
  • memory de değeri olmayan bir alana erişmek yerine null value görevi gören bir nesneye eriştik,
  • dahası client tarafı için daha temiz ve kolay anlaşılır bir kod bıraktık,

Daha öncede belirtiğim gibi bu pattern'i her zaman uygulama gibi bir durum söz konusu değil, daha doğrusu sürekli null check yapmak yerine bu pattern'i uygulayalım gibi bir düşünce doğru değil. Client-side geliştirme yapan developer'a bu kontrolleri yaptırmak istemediğimizde yada "ben server-side'dan hiçbir zaman null dönmicem.." şeklinde bir garanti vermek istediğinizde kullanabileceğimiz bir pattern dir.

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.

Dapper ORM Nedir

Object relational mappers (ORMs); ilişkisel veritabanları ile programlama dillerinin nesne modelleri arasındaki uyumsuzluğu ortadan kaldırmak, kod ile database arasındaki uyumu %100 sağlamak için yazılım dünyasında uzun süredir kullanılmaktadırlar. Daha önceki ORM yazımızda Fluent NHibernate Nedir ve Kullanımı konusuna değinmiştik. Bu yazımızda ise birçok kişi tarafından en hızlı ORM olarak kabul edilen Dapper'ı inceleyeceğiz.

Dapper, Stack Overflow ekibi tarafından open-source geliştirilen lightweight bir ORM'dir. Lightweight olması sebebiyle diğer ORM'lere kıyasla çok daha hızlıdır.

Dapper, performans ve kullanım kolaylığı sağlaması düşünülerek geliştirilmiş bir ORM dir. Transaction, stored procedure yada bulk insery kullanarak static ve dynamic object binding yapabilmemizi sağlar.

Dapper Kullanımı

Dapper kullanımı için örnek basit bir app. geliştirelim. İlk olarak Vs'da Dapper_Sample adında bir ConsoleApp oluşturalım. Sonrasında nuget üzerinden Dapper'ı projemize indirip kuralım.

 Install-Package Dapper -Version 

Kurulum işlemi tamamlandıktan sonra örneğimiz şu şekilde olsun; ContosoCorp adında bir db ve bu db de Customer adında bir tablomuz olsun ve dapper kullanarak repository class ile bu tablo üzerinde CRUD işlemleri yapalım.

Öncelikle ContosoCorp db si için table create script'ini aşağıdaki gibi yazıp execute edelim ve Customer tablomuz oluşturalım.

create table Customer
(
 Id uniqueidentifier,
 FirstName nvarchar(50) not null,
 LastName nvarchar(50) not null,
 Email nvarchar(50) not null
)

İlk olarak tablomuza karşılık gelen Customer entity'sini aşağıdaki gibi oluşturalım.

    public class Customer
    {
        public Guid Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
    }

Şimdi ise CRUD işlemleri için CustomerRepository adında bir class oluşturalım ve içerisini CRUD metotlarını tanımlayalım.

    public class CustomerRepository
    {
        protected SqlConnection GetOpenConnection()
        {
            var connection = new SqlConnection(ConfigurationManager.ConnectionStrings["ContosoCorpDbConn"].ConnectionString);
            connection.Open();
            return connection;
        }
    }

Config dosyamızda kullanacağımız db için gerekli olan conn. string'inin ContosoCorpDbConn adıyla yazılı olduğunu varsayalım ve GetOpenConnection() metodu ile repository metotlarında kullanacağımız SqlConnection yaratacağız.

Dapper'da bulunan Query() metodu veritabanından retrieve data işlemini yani veri almamızı sağlayan ve model mapping'i yaparak modellerimizi dolduran metoddur. Aşağıda yazacağımız All() metodu ile Customer tablosunda bulunan bütün kayıtları select edeceğiz. 

        public IEnumerable<Customer> All()
        {
            using (var conn = GetOpenConnection())
            {
                return conn.Query<Customer>("Select * From Customer").ToList();
            }
        }

İkinci metot olarak Id parametresi alarak geriye tek bir Customer objesi return eden Get() metodunu yazalım.

        public Customer Get(Guid id)
        {
            using (var conn = GetOpenConnection())
            {
                return conn.Query<Customer>("Select * From Customer WHERE Id = @Id", new { id }).SingleOrDefault();
            }
        }

Dapper'da bulunan Execute() metodu ise db de insert, update veya delete işlemlerinde kullanılır. Aşağıda yazacağımız metot ile parametre olarak verilen customer nesnesini insert edeceğiz.

        public void Insert(Customer customer)
        {
            using (var conn = GetOpenConnection())
            {
                string sqlQuery = "INSERT Customer(FirstName,LastName,Email) Values(@FirstName,@LastName,@Email)";
                conn.Execute(sqlQuery, customer);
            }
        }

Yukarıda görüldüğü üzre ado.net'e aşinalığı olan kişiler aslında bir nevi ado.net yazdığımızı göreceklerdir çünkü dapper'ın temelinde AdoçNet vardır. Son metot olarak dapper ile yukarıda yazdığımız All() metodunu birde stored-procedure kullanarak yazalım. İlk olarak db üzerinden aşağıdaki gibi GetAllCustomersSP adında stored-procedure'ü tanımlayalım.

CREATE PROCEDURE GetAllCustomersSP
AS
BEGIN
    SELECT * FROM Customer
END

Sonrasında bu procedure'ü execute eden repository metodunu yazalım.

        public IEnumerable<Customer> AllUsingSp()
        {
            using (var conn = GetOpenConnection())
            {
                return conn.Query<Customer>("GetAllCustomersSP", commandType: CommandType.StoredProcedure).ToList();
            }
        }

 Repository metotlarını tanımladık sırada bu metotları kullanmak var. Program.cs içerisinde bu metotları call ederek yazdığımız kodları test edelim.

        static void Main(string[] args)
        {
            var custRepo = new CustomerRepository();

            var customer1 = new Customer { Id = Guid.NewGuid(), FirstName = "Caner", LastName = "Tosuner", Email = "info@canertosuner.com" };
            var customer2 = new Customer { Id = Guid.NewGuid(), FirstName = "Berker", LastName = "Sönmez", Email = "info@berkersonmez.com" };

            custRepo.Insert(customer1);
            custRepo.Insert(customer2);

            //returns 2 records
            var allCustomersUsingQuery = custRepo.All();

            //returns 2 records
            var allCustomersUsingSp = custRepo.AllUsingSp();

            Console.ReadLine();
        }

Projemizi run ettiğimizde Customer tablosuna sırasıyla 2 kayıt atacaktır ve sonrasında All() ve AllUsingSp() metotlarını kullanarak tabloya select sorgusu yaptığımızda bize yukarıda kaydettiğimiz 2 kaydı return edecektir.

Dapper aynı zamanda transactional işlemleri de destekler. UnitOfWork pattern'i BeginTransaction() ve EndTransaction() metotlarını kullanarak uygulanabilir.

 

Not : Nuget de bulunan DapperExtensions kütüphanesi kullanarak gerekli mapping işlemlerini yaptıktan sonra repository metotları içerisinde sql komutları yerine aşağıdaki gibi de kullanabiliriz. Not: Bu mapping üzerinden table/column auto generate işlemi olmamakta sadece mapping işlemini sağlamakta.

Install-Package DapperExtensions 

    public class CustomerMapper : ClassMapper<Customer>
    {
        public CustomerMapper()
        {
            base.Table("Customer");
            Map(f => f.Id).Key(KeyType.Guid);
            Map(f => f.FirstName);
            Map(f => f.LastName);
            Map(f => f.Email);
        }
    }
        public IEnumerable<Customer> All()
        {
            using (var conn = GetOpenConnection())
            {
                return conn.GetList<Customer>().ToList();
            }
        }

        public Customer Get(Guid id)
        {
            using (var conn = GetOpenConnection())
            {
                return conn.Get<Customer>(id);
            }
        }

DapperExtensions kütüphanesi ile kolayca bir Generic Repository Pattern infrastructure'ı geliştirebilirsiniz. Yazının sadeliği açısından bu ben değinmedim ancak sonraki yazılarımızda bir IoC kütüphanesi kullanarak Dapper ile basitçe bir api projesi geliştireceğiz.

Dapper son derece geliştirmesi kolay ve kullanışlı bir ORM dir. NHibernate ve EntityFramework'de olduğu gibi model mapping'inizden table/column generate etmez ancak sql sorgularını oldukça hızlı bir şekilde execute eder ve result'larını POCO class'larınıza map'ler. Bu özelliğiyle developer'lar arasında oldukça popülerdir.

Nancy Nedir (NancyFx)

Nancy .Net ve Mono için HTTP protokolü üzerinde çalışan uygulamalar geliştirmemizi sağlayan bir lightweight framework dür. Ruby de kullanılan Sinatra framework'ün den esinlenerek geliştirilmiştir ve az kaynak tüketmesinden dolayı performansıyla ön plana çıkmıştır.

Nancy developer'ları MVC(Model-View-Controller) pattern'nini veya başka herhangi bir pattern kullanmaya zorlamadan basit bir şekilde geliştirme yapmamıza olanak sağlar. Sebebi ise yukarıda bahsettiğimiz gibi sadece HTTP isteklerine cevap veren küçük ve orta ölçekli bir uygulama görevi görüyor olması.

MVC pattern'nini implement etmeye zorlamıyor derken edemeyeceğimiz anlamına da gelmemekte. Tıpkı ASP.Net MVC yada WebApi projelerinde olduğun gibi solution'da View klasörü yaratarak projeniz için olan .cshtml'leri bu dosya altında oluşturabilir veya Model klasörü yaratarak projede kullandığınız request response yada viewModel sınıflarınızı bu klasör altına oluşturabilirsiniz. Özetle Nancy ASP.Net MVC ve Web Api'nin bir alternatifi diyebiliriz. 

En büyük özelliği ise IIS e bağımlı olmadan Windows'da çalışmakta kalmayıp OSX, Linux hatta Raspberry Pi üzerinde bile çalışabilmektedir. Raspberry Pi üzerinden ASP.Net MVC çalıştırmak nasıl olurdu acaba..

Nancy ile örnek bir api projesi yapalım. İlk olarak  vs. da Nancy_Sample adında bir console app. oluşturalım ve sonrasında aşağıdaki gibi nuget üzerinden ihtiyacımız olan dll leri kuralım.

 

Nancy kütüphanesini kullanabilmek için Nancy ve host edebilmemizi sağlayan Nancy.Hosting.Self ve cshtml view'lerini kullanabilmemizi sağlayan Nancy.Viewengines.Razor paketlerini projemize ekleyelim.

Sonrasında Program.cs içerisine aşağıdaki gibi nancy konfigurasyonlarımızı yapalı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 listenig address {"http://localhost:7880"}");
        Console.ReadKey();
        _nancy.Stop();
    }

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

Yukarıdaki kod bloğunda Nancy bizim için host edilen makinada http://localhost:7880 portunu reserv ederek dinlemeye başlayacaktır. Bu adrese gelen http isteklerini ilgili route'a yönlendirecektir.

Browser üzerinden bu adrese gittiğimizde aşağıdaki gibi bir ekran ile karşılaşırız.

404 Not Found sayfasını almamızın sebebi projemizde henüz endpoint'leri tanımlayacağımız NancyModule class'ından türeyen bir Module olmaması.

Hemen projemize SampleModule adında NancyModule class'ından inherit alan bir class oluşturalım ve içerisine httpGet isteği alan bir end-point tanımlayalım.

public class SampleModule : NancyModule
{
    public SampleModule()
    {
        Get["/"] = parameters => "Que pasa primo !";
    }
}

Projeyi tekrar run edip browser'dan kontrol ettiğimizde aşağıdaki gibi Get metodunun return ettiği response'u göreceğiz.

Şimdi birde HttpPost örneği yapalım. Request olarak 2 sayı alan ve geriye bu 2 sayının toplamını return eden bir end-point yazalım. Request ve Response modellerimiz aşağıdaki gibi olacak şekilde oluşturalım.

public class SumRequestModel
{
    public int X { get; set; }
    public int Y { get; set; }
}

public class SumResponseModel
{
    public int Result { get; set; }
}

SampleModule içerisine yazacağımız end-point ise aşağıdaki gibi gönderilen request parametrelerine göre geriye toplamlarını dönecektir.

public class SampleModule : NancyModule
{
    public SampleModule()
    {
        Post["/sum"] = parameters =>
        {
            var request = this.Bind<SumRequestModel>();

            return new SumResponseModel { Result = request.X + request.Y };
        };
    }
}

Postman üzerinden aşağıdaki gibi bir httpPost request'inde bulunduğumuzda request olarak gönderilen parametrelere göre response da toplamlarını dönmektedir.

 

Yukarıda yaptığımız örnek ile Nancy kullanarak basit bir api nasıl geliştirebiliriz inceledik. Yazının başında da söylediğimiz üzre Nancy ile geriye View de yani html sayfaları da return edebiliriz.

Örnek olarak solution'da View adında bir klasör ve içerisine Home adında .cshml uzantılı bir htmlFile oluşturalım. İçerisine de aşağıdaki gibi body tagleri arasına basit bir form input'ları ekleyelim.

<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title></title>
</head>
<body>
    <form method="post" name="myForm">
        First name: <input type="text" name="fname"><br>
        Last name: <input type="text" name="lname"><br>
        <input type="button" value="Send">
    </form>
</body>
</html>

Oluşturduğumuz bu sayfaya solution'da sağ tıklayıp Properties'den Copy to Output Directory özelliğini Copy always olarak değiştirmemiz gerekmekte aksi taktirde proje run edildiğinde Home.cshtml dosyasına erişemiyor.

Yukarıdakileri yaptıktan sonra browser'dan http://localhost:7880/home adresine bir istek geldiğinde Home.cshtml sayfamıza yönlendirecek kodu yazalım.

public class SampleModule : NancyModule
{
    public SampleModule()
    {
        Get["/home"] = parameters =>
        {
            return View["View/Home.cshtml"];
        };
    }
}

Projeyi run edip browser'dan http://localhost:7880/home adresine gittiğimizde bizi Home.cshtml sayfasına yönlendirip ekrana formu yazdıracaktır.

 

Özetle; Nancy Microsoft tarafından ASP.Net'in core dll'i olan System.Web'e bağımlı olmadan özgürce geliştirdiği şahane bir framework dür. Genelde çok büyük ölçekli projelerde tercih edilmese de ihtiyaç olduğunda bizleri çok fazla iş yükünden kurtararak ve de bence en önemlisi IIS'e bağlı kalmadan belli bir göreve hizmet eden küçük ölçekli lightweight uygulamalar geliştirmemize olanak sağlar. Daha fazla bilgi için nancyfx.org sayfasına göz atabilirsiniz.

RabbitMQ MassTransit Kullanarak Producer Consumer Yapısı

Daha önceki RabbitMQ Nedir yazımızda genel hatlarıyla rabbitmq dan bahsedip windows makina üzerinde kurulumunu anlatıp kısaca; erişilebilirliği, güvenilirliği yüksek ve ölçeklenebilen loosly-coupled asenkron iletişim sağlayan uygulamalar geliştirmektir diye tanımlamıştık.

Bu yazımızda ise Masstransit kullanarak bir Producer Consumer projesi oluşturacağız ancak makinamızda rabbitmq kurulu çalışır vaziyette olduğundan emin olalım.

Enterprise Service Bus (ESB) Nedir ?

Enterprise service bus diğer ismiyle message broker; rabbitmq gibi messaging katmanlarının üzerinde bulunan ve distributed synchronous yada asynchronous communication sağlayabilen bir middleware' dir. Bize birbirinden tamamen farklı olabilen uygulamalar arasında message-based communication yapabilmemizi sağlayan bir transport gateway'i dir. Diğer bir deyişle; message'ları provider ve consumer arasında transport eden bir çeşit middleware tool'u dur. 

 

 

Şimdi ise masstransit kullanarak bir örnek üzerinden ilerleyelim. Örneğimiz şu şekilde olsun; günlük şirket bültenini tüm çalışanlara email olarak gönderen bir producer/consumer yapısı tasarlayalım.

VS da sırasıyla 3 proje oluşturacağız.

  1. RMQMessage(class lib.)
  2. RMQProducer(console app.)
  3. RMQConsumer(console app.)

1-) RMQMessage

İlk projemiz olan RMQMessage ile başlayalım. Vs da RMQMessage adından bir class-library oluşturalım. Masstransit'in message-based communication sağladığını söylemiştik ve hem producer hemde consumer tarafından kullanılacak bu message yada contract diyede adlandırdığımız model ve onun sub-model'lerini oluşturacağız. 

İlk olarak abstract olan BaseMessage.cs'i oluşturalım ve bu model içerisinde ortak olmasını istediğimiz property'ler yer alsın.

    public abstract class BaseMessage
    {
        public DateTime PublishedTime { get; set; }
        public DateTime ConsumedTime { get; set; }
        public Guid QueueId { get; set; }
    }

Sonrasında yukarıda bahsettiğimiz producer ve consumer'ın kullanacağı NewsletterMailMessage adında contract'ımızı aşağıdaki oluşturalım.

    public class NewsletterMailMessage: BaseMessage
    {
        public List<string> AddressList { get; set; }
        public NewsletterModel NewsLetter { get; set; }
    }
    public class NewsletterModel
    {
        public string MailSubject{ get; set; }
        public string HtmlContent { get; set; }
    }

Yukarıda modellerimizi oluşturduk ve RMQMessage projesindeki işimiz bitti. Şimdi sırada 2. projeyi oluşturmak var.

2-) RMQProducer

Bunun için aynı solution'da RMQProducer adında bir console app oluşturalım. Bu proje queue'ya ilgili message'ı publish etmeden sorumlu olacak. Projeyi oluşturduktan sonra nuget üzerinden package manager console kullanarak aşağıdaki masstransit.rabbitmq paketini yükleyelim.

Masstransit.RabbitMQ

 Install-Package MassTransit.RabbitMQ

Bu paket ile birlikte rabbitmq için kullanılan masstransit kütüphanesi onun dependency'leri projemize kurulmuş olacak.

Kurulum tamamlandıktan sonra RMQMessage projesini RMQProducer projesine reference olarak ekleyelim. Ekleme nedenimiz NewsletterMailMessage.cs modelini kullanabilmek.

Program.cs içerisine aşağıdaki gibi InitializeBus adında bir metot tanımlayalım ve bu metot IBusControl arayüzünü initialize etmekten sorumlu olsun.

    static IBusControl InitializeBus()
    {
        return Bus.Factory.CreateUsingRabbitMq(cfg =>
         {
             cfg.Host(new Uri("rabbitmq://localhost/"), h =>
             {
                 h.Username("guest");
                 h.Password("guest");
             });
         });
    }

local makinada kurulu olan rabbitmq url'i rabbitmq://localhost/ dir ve default username password ise guest olarak tanımlanmıştır.

RabbitMQ ya bağlantı kısmını halletik şimdi ise message objemizi initialize edelim.

    static NewsletterMailMessage InitializeMessage()
    {
        var message = new NewsletterMailMessage
        {
            AddressList = _customerService.GetAllMailAddresses(), //assume more than 2000
            NewsLetter = new NewsletterModel
            {
                MailSubject = "Daily Newsletter of Contoso Corp.",
                HtmlContent = "Lorem ipsum dolor sit amet, et nam mucius docendi hendrerit, an usu decore mandamus. Ei qui quod decore, cum nulla nostrud erroribus ut, est eu aperiri interesset. Legere mentitum per an. Hinc legimus nostrum cu vix."
            },
            PublishedTime = DateTime.Now,
            QueueId = Guid.NewGuid()
        };

        Console.WriteLine("Message published !\nSubject : " + message.NewsLetter.MailSubject + "\nPublished at : " + message.PublishedTime + "\nQueueId : " + message.QueueId);
        return message;
    }

Son adım olarak main fonksiyonu içerisinde bu iki metodu call ederek message'ı queue'ya push edip Producer projemizdeki işlemleri bitirelim.

   static void Main(string[] args)
   {
       var busControl = InitializeBus();
       busControl.Start();
       Console.WriteLine("Started publishing.");

       var message = InitializeMessage();

       busControl.Publish(message);
       Console.ReadLine();
   }

 

3-) RMQConsumer

Sırada son adım olan consumer projesini oluşturma var. Consumer Producer'ın gönderdiği message'ları dinleyerek ilgili queue için consume etmeden sorumlu. Projeyi oluşturduktan sonra nuget üzerinden package manager console kullanarak aşağıdaki masstransit.rabbitmq paketini yükleyelim.

Masstransit.RabbitMQ

 Install-Package MassTransit.RabbitMQ

Bu paket ile birlikte rabbitmq için kullanılan masstransit kütüphanesi onun dependency'leri projemize kurulmuş olacak.

Kurulum tamamlandıktan sonra RMQMessage projesini RMQConsumer projesine reference olarak ekleyelim. Ekleme nedenimiz NewsletterMailMessage.cs modelini kullanabilmek.

İlk olarak rabbitmq ile connection sağlama ve hangi queue kullanılacak gibi konfigurasyonları Pragram.cs içerisine tanımlayalım.

    static void Main(string[] args)
    {
        var busControl = Bus.Factory.CreateUsingRabbitMq(cfg =>
        {
            var host = cfg.Host(new Uri("rabbitmq://localhost/"), h =>
            {
                h.Username("guest");
                h.Password("guest");
            });

            cfg.ReceiveEndpoint(host, "DailyNewsletterMail", e =>
            e.Consumer<NewsletterMailMessageConsumer>());
        });

        busControl.Start();
        Console.WriteLine("Started consuming.");
        Console.ReadLine();
    }

Yukarıdaki kod bloğu şunu söylüyor; eğer rabbitmq local makinamızda kurulu ise adresi rabbitmq://localhost/ şeklindedir ve default userName-password guest olarak gelmektedir. Queue ismi olarak DailyNewsletterMail verdik ve bu queue için consume edilecek olan message da NewsletterMailMessageConsumer şeklinde belirttik.

Şimdi ise NewsletterMailMessageConsumer.cs adında message objemizi consume edecek olan kısmı geliştirelim.

public class NewsletterMailMessageConsumer : IConsumer<NewsletterMailMessage>
{
    public Task Consume(ConsumeContext<NewsletterMailMessage> context)
    {
        var message = context.Message;
        message.ConsumedTime = DateTime.Now;

        Console.WriteLine("Message consumed !\nSubject : " + message.NewsLetter.MailSubject + "\nConsumed at: " + message.ConsumedTime + "\nQueueId : " + message.QueueId);

        //todo send mail impr.

        return context.CompleteTask;
    }
}

Yukarıdaki kod blou şunu anlatıyor; NewsletterMailMessageConsumer adında IConsumer interface'inin implement eden ve bu implementasyon sonucunda Consume metoduna sahip olan bir consumer'ı mız var. Bu consumer DailyNewsletterMail queue'suna gelen NewsletterMailMessage modelini consume eder.

Sırasıyla Consumer'ı ve Producer'ı run ettiğimizde ilk olarak rmq management-console da DailyNewsletterMail queue'su aşağıdaki gibi listelenecektir.

 Producer ve Consumer projelerinin ayrı ayrı console çıktıları ise aşağıdaki gibi olacaktır.

Aynı QueueId ye ait message'ımız Producer dan çıkıp consumer'a ulaştığı bilgisini yukarıdaki gibi görüyoruz.

Queue yapıları özellikle async mesajlaşma gerektiren yerlerde oldukça önemlidir ve hayat kurtarır. Bu yazıda service bus'lar dan Masstransit kullanarak basit bir consume/producer uygulaması geliştirdik ve daha sonraki yazılarımızda diğer queue yapıları ile ilgilide konuları ele almaya devam edeceğiz.

RabbitMQ Nedir ? Windows Üzerinde Kurulumu

Messaging Queue (MQ), fire-and-forget communication dediğimiz asynchronous çalışma yapısı üzerine kurulmuş yapılar için günümüz yazılım dünyasının en popüler yapısıdır. Bu yapılara örnek olarak; JMS, MSMQ, RabbitMQ, Kafka etc. verebiliriz ve  genel çerçeveden baktığımızda messaging queue'ler bir sender-receiver şeklinde çalışırlar.

RabbitMQ Nedir

RabbitMQ, Erlang dili ile open-source olarak geliştirilen ve Open Telecom Platform kütüphanesi üzerinde build edilebilen günümüz server-to-server/app-to-app communication ihtiyaçları konusunda giderek popüler olan hızlı bir messaging queue yapısıdır. Advanced Message Queuing Protocol (AMQP) implement ederek uygulamalar ve server'lar arası veri alışverişini sağlar.

Rabbitmq Publisher ve Consumer mantığıyla çalışır. Örneğin data-exchange yapmanız gereken bir iş var bunu rabbitmq üzerinden publisher'ile ilgili queue'ya publish edip sonrasında bu queue'yu consume edecek bir consumer projesi oluşturup yapmak istediğimiz bu işlemi consumer'a yaptırabiliriz. Bu işlemler genelde ana uygulama üzerinde yapmak istemeyeceğimiz yükü fazla olan işlemler olabilir.

Daha basit anlatacak olursak; sunucu RabbitMQ sunucusuna bir message gönderir ve sunucu bu mesajı ilgili queue'ya yönlendirir. Sonrasında başka bir uygulama bu queue'yu dinler ve FIFO mantığıyla çalışan kuyruktaki bu mesajları consume ederek süreci sonlandırır. Sahip olduğu Web Management Interface ile de bulunan queue'ları görüntüleyebilme, requeue etme, delete, purge gibi daha bir çok işlemi yapabilmemizi sağlar.

Queue yapıları aslında projelerimiz için birer middleware görevi görmektedir. Ana uygulamanızdan queue ya push ettiğiniz message Consumer down olsa dahi o message'ı queue'da bekletir ve consumer tekrardan start olduğunda o message'ı tekrar tekrar push etmeye çalışır ve böylelikle veri kaybının da önüne geçmemizi sağlar. 

Bazı term'lere bakacak olursak;

Producer/Publisher : Queue'ya message'ı gönderen yapıya verilen isimdir.
Consumer : Queue'yu dinleyerek ilgili message'ları receive eden yapıdır.                                                                                                                                               Queue : First-in-first-out mantığıyla çalışan kuyruk yapımız.
Exchange : Routing yani kuyruğa iletilen message'ı route eden yapıdır ve routing işlemini yapan çeşitli yapılar bulunmaktadır.

 

Kurulum

İlk olarak rabbitmq erlang dili ile geliştirildiğinden makinamızda sahip olduğumuz işletim sistemine göre uygun versiyona ait erlang dosyalarını erlang.org sitesinden indirip kuralım.

 

Sonrasında ise rabbitmq.com sitesinden Windows için güncel rabbitmq server versiyonunu indirip kuralım.

Kurulumlar sorunsuz bir şekilde tamamlandıktan sonra yukarıda da bahsettiğimiz web arayüzünü aktifleştirelim. Bunun için rabbimq sbin dosyası içerisinde olan RabbitMQ Command Prompt'ı çalıştıralım ve aşağıdaki komut satırını yazalım.

> rabbitmq-plugins enable rabbitmq_management

RabbitMQ windows makinada Windows Service olarak çalışır ve yukarıda yazdığımız web arayüzünü enable etme komutunun hemen çalışabilmesi için rabbitmq'yu aşağıdaki gibi stop/start edelim.

> net service stop RabbitMQ
.....
> net service start RabbitMQ

Şimdi ise browser üzerinden http://localhost:15672/ adresine giderek login için default credential'lar username/password guest/guest olarak girdikten sonra aşağıdaki gibi arayüzü göreceğiz.

Şuan mevcutta herhangi bir queue oluşturmadığımızdan üstte bulunan exchange tab'ına tıkladığımızda aşağıdaki gibi default exchange listesini görüntüleyebiliriz.

RabbitMQ Windows Üzerinden kurulumu bu kadardı.

Sonraki yazılarda Exchange tool'larındanbirini kullanarak basit bir publisher consumer örneği ile devam edeceğiz.                               

                       

 

OptimisticLock using Fluent NHibernate

OptimisticLock ve PessimisticLock konuları hakkında Optimistic Lock Nedir ? Pessimistic Lock Nedir ? Data concurrency yazımızda bahsetmiştik. Kısaca hatırlatmak gerekirse;farklı thread'ler de aynı row üzerinde işlem yapılırken herhangi bir lock işlemi olmadan update edilmek istenen verinin bayat olup olmadığını o verinin kayıtlı olduğu tabloda yer alan versiyon numarası olarak da adlandırılan bir column'da bulunan değeri kontrol eder ve eğer versiyon eşleşmiyorsa yani veri bayat ise işlem geri çekilir.

Bu yazıda ise Nhibernate kullanarak optimistic lock nasıl yapılır bunu inceleyeceğiz. Daha önceki Unit of Work Interceptor, Castle Windsor, NHibernate ve Generic Repository yazısında geliştirdiğimiz proje üzerinden ilerleyelim. Bir web api projesi oluşturmuştuk ve nuget üzerinden Fluent Nhibernate'i yüklemiştik. İçerisinde User ve Address adında iki tane tablomuz bulunuyordu. Nhibernate için optimistic lock konfigurasyonu mapping işlemi yapılırken belirtiliyor. Bizde öncelikle versiyonlamak veya optimistic lock uygulamak istediğimiz entity'ler için bir base model oluşturalım.

    public abstract class VersionedEntity
    {
        public virtual int EntityVersion { get; set; }
    }

User modelimiz ise yukarıda tanımladığımız modelden inherit olsun ve aşağıdaki gibi UserMapping.cs içerisinde konfigurasyonlarımızı yapalım.

 

    public class User : VersionedEntity
    {
        public virtual int Id { get; set; }
        public virtual string Name { get; set; }
        public virtual string SurName { get; set; }
    }

    public class UserMap : ClassMap<User>
    {
        public UserMap()
        {
            Table("Users");
            Id(x => x.Id);
            Map(x => x.Name);
            Map(x => x.SurName);

            // versiyon işlemi için kullanılacak column
            Version(X => X.EntityVersion);
            
            // optimistic lock'ı versiyonlama üzerinden aktif hale getiriyoruz
            OptimisticLock.Version();
        }
    }

Database de Users tablomuzda EntityVersion adında bir column yaratılacak ve bu column o row için yapılan her bir update işleminde 1 artacaktır.

Konfigurasyon işlemi bu kadar şimdi test yapalım. Aşağıdaki gibi AddnewUser metoduna postman üzerinden sırayla 1 insert 2 get 2 put(update) request'i atalım.

İlk insert işlemi sonrasında db deki kayıt aşağıdaki gibi EntityVersion= 1 şeklinde olacaktır.

Sonrasında ardı ardına 2 get işlemi yapıp db deki kaydı alalım ve sonrasındaki ilk update işlemi sonrasında kaydımız aşağıdaki gibi EntityVersion = 2 şeklinde güncellenecektir.

İkinci get işlemini yapan transaction için yani üstte update yapılmışken eline stale/bayat veriye sahipken update işlemi yapmaya çalıştığında diğer bir değişle db de ki EntityVersion = 2 iken ikinci işlemin elinde EntityVersion = 1 olan kayıt varken update yapmaya çalıştığında aşağıdaki gibi bir exception throw edilir.

Hata mesajı bize o row'un bize başkabir transaction tarafından update veya delete edildiğini belirtmekte. Bu durumu yaşamamak için ikinci işlem için tekrardan db de bulunan kayıt get edilip üzerinden bir update işlemi yapıldığında db deki son görüntüsü aşağıdaki gibi EntityVersion = 3 şeklinde olacaktır.

 

Optimistic Lock için yazımız buraya kadar. Yukarıda da belirttiğim gibi örnek kodlar Unit of Work Interceptor, Castle Windsor, NHibernate ve Generic Repository yazısında bulunmakta. Eksik kalan yerler için ordan devam edebilirsiniz.

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.

ElasticSearch Nedir ? Windows Üzerinde Kurulumu

Elasticsearch, java dilinde open-source olarak geliştirilen, dağıtık mimariye uygun, kolay ölçeklenebilir, enterprise düzeyde bir big-data arama motorudur. Sahip olduğu Http protokolü üzerinde çalışan Restful Api ile CRUD işlemlerini oldukça hızlı bir şekilde yapabilmemize olanak sağlar. 

ElasticSearch veya diğer search engine'lerin geliştirilmesine asıl sebep olan şey big-data dır. Her an her saniye milyonlarca satır veri üretiminden bahsediyoruz ve toplanmış olan bu verileri analiz etmek istediğimizde bu işlemi database seviyesinde yapıyorsak yani SQL'e bağımlıysak hız konusunda geride kalıyoruz. ElasticSearch core kısmında yer alan çeşitli algoritmalarıyla text-search işlemini oldukça kısa sürede hızlı bir şekilde yapabilmektedir.

Kurulum

Yukarıda bahsettiğimiz üzre elasticsearch Java tabanlı bir kütüphane olduğundan windows üzerinde kurulum yapmadan önce pc'nizde en az Java 8 versiyonu kurulu olmak şartıyla JRE ve JDK yüklü olmak zorundadır.

Elasticsearch elasticsearch.org adresinde ZIP and TAR.GZ gibi değişik formatlarda kurulum paketi sunmaktadır. Ancak ben temiz bir kurulum yapmanız adına MSI formatında olan paketi indirmeyi tercih edicem. İndirme işlemi bittiğinde exe'yi çalıştıralım ve bu adreste belirtildiği şekilde veya aşağıdaki görselde de olduğu gibi gerekli konfigurasyonları yaparak kurulumu tamamlayalım.

Kurulum sırasında path, memory-size gibi çeşitli konfigurasyonlar yapabilirsiniz. Eğer kurulumu Install as a service seçeneği ile yaptıysanız elasticsearch service olarak arka planda pc niz açık olduğu sürece çalışacaktır. Service'i görüntülemek için Windows Search kısmına "Services" yazdığınızda çıkan icon'a tıklayalım ve aşağıdaki gibi service listesinde elasticsearch'ü görelim.

Son olarak ES'ün çalışıp çalışmadığını browser üzerinden de kontrol edebiliriz. Browser'ın adres kısmına http://localhost:9200/ yazarak aşağıdaki gibi mevcut pc'niz de kurulu olan ES ile ilgili bilgilere ulaşabilirsiniz.

{
  "name" : "DESKTOP-GRKHT7E",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "saiTqKiQRr6m_GQ03BCH0Q",
  "version" : {
    "number" : "5.5.0",
    "build_hash" : "260387d",
    "build_date" : "2017-06-30T23:16:05.735Z",
    "build_snapshot" : false,
    "lucene_version" : "6.6.0"
  },
  "tagline" : "You Know, for Search"
}

Mimarisi

Elasticsearch'ü database üzerinden anlamaya çalışacak olursak;

SQL de bulunan Database ES'de Index'e denk gelmektedir. Tablo ise Tip yani ES'e Index yaparken vereceğimiz modellerimize denk gelmektedir. Tabloya kaydettiğimiz her bir row ise ES de Document olarak adlandırılır. Tabloda bulunan Column'lar Field yani Tip olarak verdiğimiz model de bulunan property veya field'lar dır. Schema ise Mapping olarak adlandırılır.

Aslında Code-First yaklaşımına aşina olan arkadaşlar yukarıdaki görsele baktıklarında ES'ünde bir nevi code-first mantığıyla çalıştığını görebilirler.

ES ile ilgili bu yazımızda sona geldik ancak bir sonraki yazımızda yapmış olduğumuz bu kurulum üzerinden bir .Net projesi geliştirerek ES Client'ların dan biri olan NEST'i kullanarak örnekler vermeye devam edeceğiz.

Nhibernate IPreInsertEventListener ve IPreUpdateEventListener Kullanımı

Server-side bir projede geliştirme yapıyorsanız ve db de bolca CRUD işlemleri için query'ler çalıştırmanız gerekiyorsa sizden db de kaydedilen o row için sizden insert veya update anında bazı bilgileri otomatik bir şekilde o row için kaydetmeniz istenebilir. Örnek olarak; CreatedDate veya update edilen değer için ModifiedDate gibi alanlar tutmanız muhakkak istenir istenmese dahi bu bilgileri ilgili colum'lar da tutmak muhakkak bir gün işinize yarayacaktır.

Eğer CRUD işlemlerini Ado.Net kullanarak yapıyorsanız query'nin sonuna bu değerleri ekleyebilir yada stored-procedure kullanıyorsanız da bu işlemleri sp içerisinde de yapabiliriz.

Bu yazımızda bu ve benzeri işlemleri Fluent-Nhibernate kullanarak nasıl yapabiliriz konusuna değineceğiz. 

Her defasında yeni kayıt geldi modeli initialize ederken CreatedDate alanına DateTime.Now set et, yada her update işlemi geldiğinde ModifiedDate alanına DateTime.Now alanını set et. Pek de hoş durmuyor sanki. 50'ye yakın db de tablonuz olduğunu düşünün her bir entity için gidip bu işlemleri heralde yapmak istemeyiz .

Eğer proejenizde NHibernate'i kullanıyorsanız Nhibernate bu işlemler için bizlere aşağıdaki interface'leri sunmakta.

  • IPreInsertEventListener
  • IPreUpdateEventListener

IPreInsertEventListener; adında da anlaşılacağı üzre entity'niz insert edilirken bir interceptor gibi araya girmemizi sağlayan ve insert query execution'dan OnPreInsert adındaki metoduna invoke edilerek entity'niz üzerinde işlemler yapmanızı sağlar.

IPreUpdateEventListener; ise bir update listener'ı dır ve içerisinde implement edebildiğimiz OnPreUpdate  metodu çağrılır update query'sinin execution'dan önce call edilerek yine entity üzerinde değişiklikler yapabilmemizi sağlar.

Örnek olarak bir BaseModel'miz olsun ve projemizde bulunan her bir entity için tablolarda ortak bulunan alanları bu class içerisinde tanımlayabiliriz.

    public abstract class BaseModel
    {
        public virtual Guid Id { get; set; }
        public virtual DateTime? CreatedDate { get; set; }
        public virtual DateTime? ModifiedDate { get; set; }
    }

Db de bulunan tablolarımıza karşılık gelen entity'lerimiz ise yukarıda tanımladığımız BaseModel class'ından inherit olacaklar. Örnek olarak Customer adında aşağıdaki gibi bir entity tanımlayalım.

    public class Customer : BaseModel
    {
        public virtual string FirstName { get; set; }
        public virtual string LastName { get; set; }
        public virtual string Email { get; set; }
    }

Şimdi ise CustomerMap class'ını oluşturacaz ancak öncesinde BaseModel içerisinde bulunan proeprty'ler için BaseMapping adında bir class tanımlayalım. Customer ve diğer db modellerimizde bu BaseMapping'i kullanarak map işlemlerini yapacağız. Bunu yapmamızdaki amaç her bir entity için ayrı ayrı gidip BaseModel içerisinde bulunan alanların map'ini yapmamak. 

    public class BaseMapping<T> : ClassMap<T> where T : BaseModel
    {
        public BaseMapping()
        {
            Id(x => x.Id);
            Map(x => x.CreatedDate);
            Map(x => x.ModifiedDate);
        }
    }

 

Artık BaseMapping 'i kullanarak CustomerMap class'ını oluşturabiliriz. 

    public class CustomerMap : BaseMapping<Customer>
    {
        public CustomerMap()
        {
            Map(x => x.FirstName);
            Map(x => x.LastName);
            Map(x => x.Email);
        }
    }

Sırada Listener class'ını oluşturmak var. Aşağıda NHInsertUpdateListener adında bir class tanımlayalım. Yazının başında bahsettiğimiz her tablomuzda bulunan CreatedDate ve ModifiedDate tarih alanlarını NHInsertUpdateListener içerisinde set edeceğiz.

    public class NHInsertUpdateListener : IPreInsertEventListener, IPreUpdateEventListener
    {
        public bool OnPreUpdate(PreUpdateEvent @event)
        {
            var audit = @event.Entity as BaseModel;
            if (audit == null)
                return false;

            var time = DateTime.Now;

            Set(@event.Persister, @event.State, "CreatedDate", time);

            audit.CreatedDate = time;

            return false;
        }

        public bool OnPreInsert(PreInsertEvent @event)
        {
            var audit = @event.Entity as BaseModel;
            if (audit == null)
                return false;


            var time = DateTime.Now;

            Set(@event.Persister, @event.State, "ModifiedDate", time);

            audit.ModifiedDate = time;

            return false;
        }

        private void Set(IEntityPersister persister, object[] state, string propertyName, object value)
        {
            var index = Array.IndexOf(persister.PropertyNames, propertyName);
            if (index == -1)
                return;
            state[index] = value;
        }
    }

Artık son adım olarak FluentNHibernate'i ayağa kaldırmak var. Nh configuration'ı aşağıdaki gibi tanımlayabiliriz.

Fluently.Configure()
               .Database(MsSqlConfiguration.MsSql2012.ConnectionString(c => c.FromAppSetting("dbConnectionString")).ShowSql())
               .Mappings(m => m.FluentMappings.AddFromAssemblyOf<CustomerMap>())
               .ExposeConfiguration(cfg => new SchemaUpdate(cfg).Execute(false, true))
               .ExposeConfiguration(cfg =>
               {
                   cfg.SetProperty(
                      NHibernate.Cfg.Environment.CurrentSessionContextClass,
                       "web");
                   cfg.AppendListeners(ListenerType.PreUpdate, new IPreUpdateEventListener[] { new NHInsertUpdateListener() });
               })
               .BuildSessionFactory();

 

Sırasıyla yazmak gerekirse neler yaptık;

  1. Vs da bir tane proje oluşturduk. (Console veya Api),
  2. FluentNHibernate paketini nuget'ten indirip kurduk,
  3. Bir db miz olduğunu ve connection string bilgisinin web config'de tanımlı olduğunu varsaydık,
  4. Tablolarda ortak kullanılan propert'leri BaseModel adında ki class da topladık,
  5. Daha sonra BaseMapping adında bir mapping tanımlaması yaparak entity içerisindeki property'leri map ettik,
  6. CustomerMap class'ını oluşturarak mapping işlemini tanımladık,
  7. NHInsertUpdateListener'ı oluşturduk ve CreatedDate - ModifiedDate alanları için değerleri set ettik.
  8. Fluent Nhibernate konfigurasyonunu oluşturduk.

 Listener'lar biraz az bilinen bir özellik gibi görünse de oldukça faydalıdırlar. Örnekte olduğu gibi benzer case'lerde kullanarak bizleri satırlarca tekrar eden kodlardan uzaklaştırır.