Asp.Net Core Middleware Nedir Nasıl Kullanılır

.Net Core Microsoft tarafından open-source olarak geliştirilmiş modüler bir .net framework'dür. Asp.Net Core ise klasik bildiğimiz Asp.Net kütüphanesinin open-source olarak microsoft tarafından release edilmiş halidir.

Daha önceki yazılarda da bahsettiğimiz üzre asp.net core'da bir çok feature yada özellik ayrı bir modül olarak kolayca entegre edilebilecek şekilde geliştirilmiştir. Middleware'de yine bunlardan biri. 

Middleware nedir diye soracak olursak; Middleware asp.net core içerisinde request-response pipeline'ını handle etmemizi sağlayan bir çeşit interceptor görevi gören sınıflardır. Bu sınıfları kullanarak controller metodunuza gelen request'leri veya response'ları modify edebilir, header check yapabilir yada authorization kontrollerini kolayca entegre edebiliriz.

Middleware asp.net core cycle'ın da ki konumunu anlamak için aşağıdaki resime bamak yeterli.

Uygulama içerisinde tanımlı olan middleware'ler register edilme sırasıyla birlikte yukarıdaki resimde olduğu gibi birbirlerini call ederek pipeline'ı tamamlarlar. 

Middleware Imp.

Örneğimiz şu şekilde olsun; bir api projemiz var ve bu projede middleware kullanarak client'dan request header da beklediğimiz Client-Key, Device-Id key-value parametrelerini gönderip göndermediğini kontrol edelim. Göndermediği durumda http400 ile geriye hata dönelim.

Middleware tanımlamanın birkaç farklı yolu var ancak en basit olanını inceleyeceğiz. İlk olarak vs.'da MiddlewareSample adında bir asp.net core projesi oluşturalım. Sonrasında projemize HeaderCheckMiddleware adında bir sınıf ekleyelim.

    public class HeaderCheckMiddleware
    {
        private readonly RequestDelegate _next;
        public HeaderCheckMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task Invoke(HttpContext httpContext)
        {
            var key1 = httpContext.Request.Headers.Keys.Contains("Client-Key");
            var key2 = httpContext.Request.Headers.Keys.Contains("Device-Id");

            if (!key1 || !key2)
            {
                httpContext.Response.StatusCode = 400;
                await httpContext.Response.WriteAsync("Missing requeired keys !");
                return;
            }
            else
            {
                //todo
            }
            await _next.Invoke(httpContext);
        }
    }

Yukarıda görüldüğü üzre, Invoke metodu middleware call edildiğinde execute edilecek olan metottur. Bu metot; end-poit'e gelen request'i ve end-point'in return ettiği response'a müdahale edebilmemizi sağlar. Bizde yapılan her httpRequest'inde header de beklediğimiz Client-Key, Device-Id vs. gibi bilgileri kontrol etme işini yukarıda olduğu gibi bu metodun içerisinde yaptık. Eğer bu 3 header key'in den birisi dahi header'da yok ise htpp400 olarak geriye hata mesajı return ettik.

Middleware'imizi tanımladıktan sonra geriye bunu asp.net core projemize register etmek kalıyor. Bunun içinde aşağıdaki gibi bir extension metot yazalım ve register etme işlemini asp.net core ile birlikte gelen Startup.cs de bulunan Configure metodu içerisinde tıpkı projede ayağa kaldırabileceğimiz bir servismiş gibi enable edelim.

    public static class MiddlewareExtension
    {
        public static IApplicationBuilder UseHeaderCheckMiddleware(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<HeaderCheckMiddleware>();
        }
    }

Son adım olarak ise Configure metodu içerisinde middleware'i aktifleştirelim .

 public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
 {
     app.UseHeaderCheckMiddleware();
 }

Aşağıdaki gibi postman veya herhangi bir rest-call tool'u kullanarak projenizde bulunan herhangi bir end-point'e call yaptığınızda header da beklenen parametreleri göndermezsek hata almış olacağız.

Parametreleri doğru bir şekilde gönderdiğimiz durumda ise sorunsuz şekilde endpoint'e ulaşıp success response alabiliriz.

Middleware asp.net core projelerinde aop özelliklerini uygulayabilmemizi sağlar ve bununla birlikte bizlere projemiz için modüler özellikleri olan küçük feature'lar ekleterek kod tekrarından ve spaghetti code bloklarından bizleri kurtarabilir. Örnekte sadece küçük bir header check işlemi yaptık ancak middleware kullanarak bunu gibi daha bir çok geliştirmede yapabiliriz.

 

Asp.Net Core Logging

Asp.net core yazılarında daha önce asp.net core'a giriş yapmıştık ve devamında build-in container'dan bahsetmiştik. Kısaca tanımlayacak olursak; asp.net core microsoft tarafından open-source olarak geliştirilmiş asp.net'e göre daha modüler bir cross platform web kütüphanesidir. 

Bu yazıda ise asp.net core'da logging nedir nasıl yapılır inceleyeceğiz. 

Log bir uygulama için olmazsa olmazların başında gelir ve projeler için oldukça önemli bir feature'dır. Yukarıda da yazdığımız gibi asp.net core modüler bir framework dür ve logging de asp.net core uygulamanızda kolayca ayağa kaldırabileceğiniz bir service olarak yer almaktadır.

Öncelikle Vs'da AspCoreLogging adında bir web-api projesi oluşturalım. Eğer oluşturduğunuz proje asp.net core 1.x versiyonu ise projemize Microsoft.Extensions.Logging dll'ini referans olarak eklememiz gerekmekte ama eğer asp.net core 2.x versiyonlarından birine ait ise default olarak gelmekte.

Microsoft.Extensions.Logging namespace'i bizim asp.net core içerisinde logging için gerekli olan sınıf&arayüz ve metotları vs. içermekte.

Bunlara bakacak olursak;

  • ILogger
  • ILoggingFactory
  • LoggingFactory
  • ILoggingProvider

built-in class ve interface'leri bu namespace altında bulunmakta.

ILogger interface'i kullanacağımız log-storage'a log kaydetmemizi sağlayan gerekli metotları içerir.

public interface ILogger
{ 
   void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter);
   bool IsEnabled(LogLevel logLevel);
   IDisposable BeginScope<TState>(TState state);
}

Geliştireceğimiz custom-logger'ı oluşturmak için ILogger interface'ine ait metotları extend edip metotlarını kullanacağız.

ILoggerFactory interface'i ise yukarıda bahsettiğimiz ILogger arayüzünü extend eden custom-logger'ın instance'ını oluşturmada kullanacağımız interface'dir.

public interface ILoggerFactory : IDisposable
{
    ILogger CreateLogger(string categoryName);
    void AddProvider(ILoggerProvider provider);
}

Asp.net Core içerisinde yukarıda bahsettiğimiz ILoggerFactor interface'ini implement eden LoggerFactory sınıfı bulunmakta. Runtime'da asp.net core framework bu sınıfa ait instance yaratarak default gelen kendi built-in IoC container'ına register eder.

ILoggingProvider interface'i istenilen logging kategorisindeki gerekli logger sınıflarını yaratır ve yönetir. Framework içerisinde default olarak gelen provider'lar şu şekildedir;

  • Console
  • Debug
  • EventSource
  • EventLog
  • TraceSource
  • Azure App Service
public interface ILoggerProvider : IDisposable
{
   ILogger CreateLogger(string categoryName);
}

Bu interface'i bize projede kullanacağımız customLogger'ın instance'ını oluşturmamızı sağlayacak sınıfı tanımlarken kullanacağız.

File Logging Impl.

Şimdi ise yukarıda bahsettiğimiz adımları fileLogging için geliştirmeye başlayalım. İlk olarak projemize ILogger interface'ini implement eden FileLogger sınıfını aşağıdaki gibi tanımlayalım.

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
    {
        var message = string.Format("{0}: {1} - {2}", logLevel.ToString(), eventId.Id, formatter(state, exception));
        WriteMessageToFile(message);
    }
    private static void WriteMessageToFile(string message)
    {
        const string filePath = "C:\\AspCoreFileLog.txt";
        using (var streamWriter = new StreamWriter(filePath, true))
        {
            streamWriter.WriteLine(message);
            streamWriter.Close();
        }
    }
    public IDisposable BeginScope<TState>(TState state)
    {
        return null;
    }
    public bool IsEnabled(LogLevel logLevel)
    {
        return true;
    }

Yukarıda da görüldüğü üzre projede üretilen loglar server'da bulunan C sürücüsünde AspCoreFileLog.txt adındaki dosyaya yazılacak. FileLogger sınıfını oluşturduktan sonra bu sınıfı projemize inject etmemizi sağlayacak olan ILoggerProvider interface'ini implement edecek olan FileLogProvider sınıfını tanımlayalım.

public class FileLogProvider : ILoggerProvider
{
    public ILogger CreateLogger(string category)
    {
        return new FileLogger();
    }
    public void Dispose()
    {

    }
}

FileLogProvider sınıfı proje içerisinde tanımlanan logger'ın instance'ının create edilmesini sağlar. 

Son adım olarak yukarıda tanımladığımız FileLogProvider'ı Startup.cs sınıfında bulunan Configure metodunda loggerFactory'nin provider'larına eklememiz kalıyor. Bu işlemi de aşağıdaki gibi startup.cs içerisinde yapalım.

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    //provider'ı ekledik
    loggerFactory.AddProvider(new FileLogProvider());

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseMvc();
}

 

Geliştirmemiz bitti artık yazdığımız kodları test edebiliriz. Bunun için ILogger interface'inin projede yer alan aşağıdaki controller'a constructor seviyesinde inject ettikten sonra end-point'lere sırasıyla browser üzerinden get işlemi yapalım.

[Route("api/[controller]")]
public class ValuesController : Controller
{
    private readonly ILogger<ValuesController> _logger;
    public ValuesController(ILogger<ValuesController> logger)
    {
        this._logger = logger;
    }

    // GET api/values
    [HttpGet]
    public IEnumerable<string> Get()
    {
        _logger.LogInformation("Hi from logger !");
        return new string[] { "value1", "value2" };
    }

    // GET api/values/5
    [HttpGet("{id}")]
    public string Get(int id)
    {
        throw new NullReferenceException("Null exp. from myApp !");
        return "value";
    }
}

Http-Get request'i yolladıktan sonra C:\\AspCoreFileLog.txt adresine gittiğimizde içerisinde hem framework'ün ürettiği hemde bizim controller metodunda yazdırdığımız log row'larını görebilirsiniz.

 

 

Yazının başında da bahsettiğimiz üzre Asp.Net Core'da logging moduler bir şekilde ayrı bir service olarak gelmekte ve fileLog dışında database, flat file yada diğer log target türlerini kullanarak logging'i genişletebilirsiniz. Bunun dışında NLog, Serilog yada .net core desteği olan third-party logging provider'ları da kullanabilirsiniz.

Asp.Net Core In-Memory Cache

Daha önceki Asp.Net Core yazılarında as.net core'a giriş yapıp sonrasında asp.net core framework ile birlikte gelen built-in container'ını incelemiştik.

Asp.Net Core Windows, Linux, MacOS üzerinde çalışan moden web uygulamaları geliştirmemizi sağlayan modüler bir framework'dür. Modüler olmasının dezavantajı olarak da klasik Asp.net kütüphanesine kıyasla içerisinde default olarak gelen bir çok özellik bulunmamaktadır. Bunlardan biride default olarak içerisinde bir Cache object bulunmuyor ancak bir kaç küçük geliştirmeyle uygulamanıza hem in-memory hemde distributed caching özelliklerini kazandırabiliyoruz.. 

Bu yazımızda da asp.net core uygulamamıza in-memory cache özelliğini nasıl kazandırabiliriz basit bir örnek ile  inceleyeceğiz. 

Enable In-Memory Cache

In-memory cache özelliği asp.net core içerisinde bir service olarak bulunmaktadır. Bu servis default kapalı gelir yapmamız gereken startup.cs içerisinde bulunan ConfigureServices metodunda aşağıdaki gibi cache servisini açmak.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddMemoryCache();
}

Core projelerinde in-memory cache kullanmamızı sağlayan arayüzün adı IMemoryCache. Bu interface'e ait metotları vs. kullanarak cache set,get,remove gibi işlemleri yapabiliriz.

public interface IMemoryCache : IDisposable
{
    bool TryGetValue(object key, out object value);
    ICacheEntry CreateEntry(object key);
    void Remove(object key);
}

Using IMemoryCache to Cache

ConfigureServices metodu içerisinde servisi aktifleştirdikten sonra IMemoryCache interface'ini kullanmak istediğimiz katmana ait constructor'da inject etmemiz gerekmekte.
Bizde geriye product list return ettiğimiz bir controller tanımlayarak IMemoryCache interface'ini aşağıdaki gibi const. inj. parameter olarak verelim.

[Route("api/[controller]")]
public class ProductController : Controller
{
    private readonly IMemoryCache _memoryCache;
    public ProductController(IMemoryCache memoryCache)
    {
        _memoryCache = memoryCache;
    }

    // GET api/values
    [HttpGet]
    public IEnumerable<Product> Get()
    {

    }
}


public class Product
{
    public int Quantity { get; set; }
    public string Name { get; set; }
}

Şimdi ise get metodunun içerisini dolduralım. Set metodu parametre olarak 1:key, 2:value, 3:cacheOptions . Cache options olarak AbsoluteExpiration;cache expire süresi ve Priority; memory şiştiğinde cache objelerini hangi priority'de silecek bunun bilgisinin bulunduğu ayarları set edeceğiz. 

[HttpGet]
public IEnumerable<Product> Get()
{
    const string cacheKey = "productListKey";

    if (!_memoryCache.TryGetValue(cacheKey, out List<Product> response))
    {
        response = new List<Product> { new Product { Name = "test 1 ", Quantity = 20 }, new Product { Name = "test 2", Quantity = 45 } };

        var cacheExpirationOptions =
            new MemoryCacheEntryOptions
            {
                AbsoluteExpiration = DateTime.Now.AddMinutes(30),
                Priority = CacheItemPriority.Normal
            };
        _memoryCache.Set(cacheKey, response, cacheExpirationOptions);
    }
    return response;
}

Gelen ilk request için cache'de o key'e ait bir obje olmadığından ilk response source'a gidip(bir repository yada service layer olabilir) dönen değer alınıp 30 dkka expire süresi set edilerek oluşturacaktır. Artık ondan sonraki bütün request'ler 30 dkka süresince source'a gitmeden response'u cache'de bulup Get işlemi yapıp return edecektir. Expire süresi dolduğunda ise ilgili key ve obje cache'den silinecektir.

Set, Get yapabildiğimiz gibi Remove işlemide yapabiliriz. Bunun için cacheKey değerini parametre olarak Remove metoduna verip call yapmak yeterli.

 _memoryCache.Remove(cacheKey);

CacheItemPriority enum'ı içerisinde Low, Normal, High, NeverRemove değerleri mevcut. CachedObject Priority değerine göre memory de yer açmak için sırayla silinir. Memory'den otomatik silme işlemi yapıldığında bunun bilgisini bize iletmesini sağlayan bir callback handler metodunu aşağıdaki gibi options'a register edebiliriz ve silme işlemi yapılırken bu metot tetiklenerek bize haber verir.

 cacheExpirationOptions.RegisterPostEvictionCallback
     (ProductGetALLCacheItemChangedHandler, this);
 _memoryCache.Set(cacheKey, response, cacheExpirationOptions);

Cache nerede ve nasıl uygulanması gerektiğine karar verildiğinde server-side bir uygulama için olmazsa özelliklerden biri haline gelmiştir. Asp.net core'da da yazının başında bahsettiğimiz gibi memory ve distributed cache işlemleri yapmamızı sağlayan service'ler bulunmaktadır. Bu yazımızda basitçe memory cache özelliğini projemize nasıl kazandırabiliriz konusuna değindik. Sonraki yazılarda redis kullanarak distributed cache yapısını uygulamamıza nasıl entegre edebiliriz inceleyeceğiz.

Asp.Net Core Dependency Injection

Asp.Net Core basitçe nedir ne değildir bahsetmiştik daha önceki yazılarımızda. Bugünkü yazıda ise Asp.Net Core için Dependency Injection konusuna değinip framework içerisinde default gelen built-in DI Container'ını inceleyeceğiz.

.Net core için 3rd party DI container'lar kullanılabildiği gibi küçük basit uygulamalar yada microservice'ler için kullanılabilen microsoft'un geliştirmiş olduğu basit kullanıma sahip bir built-in default DI tool'u da framework ile sunulmuştur. Castle, Autofac yada Unity framework'leri kadar çeşitli özelliklere sahip olmasa da oldukça kullanması basit ve performanslı bir framework olarak karşımıza çıkmakta. Örnek bir proje üzerinden nedir, ne değildir, nasıl kullanılır anlatmaya başlayalım.

Creating a Sample Project

Aşağıdaki gibi Asp.Net Core'da Web Api olarak yazılmış bir projemiz olsun ve proje içerisinde Repository ve Service katmanları için kullandığımız interface'leri container'a register edelim ve kullanalım. İlk olarak domain objemizi aşağıdaki gibi yaratalım.

namespace AspCoreDIContainerSample.Domain
{
    public class City
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
    }
}

Sonrasında yukarıda oluşturduğumuz domain model için repsoitory inteface ve sınıfını oluşturalım. 

namespace AspCoreDIContainerSample.Repository
{
    public interface ICityRepository
    {
        IEnumerable<City> GetAll();
    }
}
namespace AspCoreDIContainerSample.Repository
{
    public class CityRepository : ICityRepository
    {
        public IEnumerable<City> GetAll()
        {
            // send back some hard-coded data
            return new List<City>
            {
                new City { Id = "3938ca15-cba2-44a5-bd53-b5d6a5e306f5", Name = "Ankara" },
                new City { Id = "cfd61cbf-a161-440f-9d1d-fdef32379b71", Name = "Samsun" }
            };
        }
    }
}

Yukarıda oluşturduğumuz ICityRepository interface'i aşağıda tanımlayacağımız CityService sınıfına constructer-injected parameter olarak vereceğiz. 

namespace AspCoreDIContainerSample.Service
{
    public interface ICityService
    {
        List<City> GetAllCities();
    }
}
namespace AspCoreDIContainerSample.Application
{
    public class CityService : ICityService
    {
        private readonly ICityRepository cityRepository;
 
        public CityService(ICityRepository cityRepository)
        {
            this.cityRepository = cityRepository;
        }
 
        public List<City> GetAllCities()
        {
            return this.cityRepository.GetAll().ToList();
        }
    }
}

Register All Used Components

Yukarıda kullandığımız repository ve service objelerini container'a implementasyonlarını belirterek register etmemiz gerekiyor. Bunu için aşağıdaki gibi proje solution'ında bulunan Startup.cs sınıfından faydalanacağız. Startup.cs proje ilk run edildiğinde çalışan kod satırlarının bulunduğu sınıftır. Bizde proje ilk run edildiğinde ilgili kullanılan bağımlılıkları register edeceğiz. Startup.cs de bulunan ConfigureServices adlı metot register işlemlerini yapmak için default gelen bir metottur. Aşağıdaki gibi tanımlamalarımızı yapalım.

    public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<ICityRepository, CityRepository>();
            services.AddScoped<ICityService, CityService>();
        }

 Yukarıda görüldüğü üzre ICityRepository ve ICityService interface'lerini implementasyonlarını belirterek AddScoped metodunu kullanarak register ettik. Peki ne bu AddScoped ?

 

Dependency Injection Lifetimes - Scoped, Singleton, Transient

Dependency injection lifetime container'dan istenen objenin ne zaman instance create edileceğini yada ne zaman yeniden create edilmesi gerektiğini sağlar. Container'da lifetime tanımı yapabilmemizi sağlayan 3 state vardır. Sırasıyla bu üçlüye bakacak olursak;

Scoped : Her scope için tek bir instance yaratılmasını sağlayan lifetime adı dır. Örneğin web projesi için projenize gelen her bir HttpRequest için ilgili instance'ı yaratıp container'da tutar ve o http lifecycle'ı boyunca hep aynı instance'ı kullanır.

Singleton : İsminden de anlaşılacağı üzre singleton yani uygulama ilk çalıştığında tek bir instance yaratır ve sonrasından uygulama stop olana kadar bu instance'ı kullanır.

Transient : Bu lifetime ise ilgili obje her istendiğinde yeni bir instance yaratır ve özellikle stateless servisler için best-practice olarak kullanılır.

Biz yukarıda kullandığımız interface'lerin lifetime'larını Scoped olarak tanımladık ancak ihtiyaca göre Singleton yada Transient olarak da tanımlayabilirdik.

  services.AddSingleton<ICityRepository, CityRepository>();
  services.AddSingleton<ICityService, CityService>();

 

Using Registered Components

Son adım olarak register ettiğimiz bu service'leri kullanmak kaldı. Aşağıdaki gibi CityController içerisinde container'da register edilen instance'ları kullanarak geriye cityList return edelim.

namespace AspCoreDIContainerSample.Api
{
    public class CityController : Controller
    {
        private readonly ICityService _cityService;
 
        public CityController(ICityService cityService)
        {
            this._cityService= cityService;
        }

        [HttpGet]
        public List<City> GetCityList()
        {
            return this.cityService.GetAllCities();
        }
    }
}

Asp.Net Core için Dependency Injection kullanım örneğimiz bu kadardı. Yukarıdaki end-point'e call yaparak geriye Samsun ve Ankara illerinin bulunduğu array'i return ettiğini göreceğiz.

Built-in dependency injection castle, ninject veya autofac kadar zenginliğe sahip bir IoC container değildir ancak oldukça basit kullanıma sahip hızlı ve implementasyonu kolay bir container dır. Eğer isterseniz bu container'ın asp.net core için bir kütüphanesi geliştirildiyse default built-in container ile replace de edebilirsiniz. Sonraki Asp.Net Core DI yazımızda 3rd party asp.net core DI container'larından birini nasıl built-in container yerine kullanabiliriz inceleyeceğiz.

Asp.Net Core'a Giriş

.Net Core Microsoft tarafından open-source olarak geliştirilmiş  modüler bir framework'dür. Asp.Net Core ise klasik bildiğimiz Asp.Net kütüphanesinin open-source olarak microsoft tarafından release edilmiş halidir.

Asp.Net yaklaşık 15 yıl önce Microsoft tarafından geliştirilmiş olup günümüz web teknolojileri arasından oldukça popüler olan bir kütüphanedir. Microsoft open-source dünyanın önlenemez yükselişi ile birlikte daha fazla dayanamadı ve en base de bildiğimiz .Net çatısı altında olan bütün teknolojilerini yeniden yapılandırıp open-souce olarak bütün dünya ile paylaşacağını bizlere 3 yıl önce duyurmuştu. Akabinde sırayla .Net çatısı altındaki framework'ler git-hub üzerinde yayınlanmaya başlandı.

Ms'in web için geliştirdiği kütüphanelerin kronolojik sırasına bakacak olursak

  • 2002 - ASP.Net 
  • 2008 - ASP.Net MVC 
  • 2012 - ASP.Net Web API and SignalR

son olarak ise 2016 yilinda .Net Core 1.0 release oldu ve bunula birlikte Asp.Net Core hayatımıza girdi.

Asp.Net Core Asp.Net'in daha sade ve modern bir yüzü olarak karşımıza çıkıyor. Microsoft'un Zamarin'i satın alması sonrası sahip olduğu cross-platform deneyiminide işin içine katarak Asp.Net Core'u bir Cross Platform kütüphane olarak bizlere sundu. Framework'ü kullanarak IoT uygulamaları, back-end service uygulamaları ve web projeleri geliştirebiliriz.


.Net Standart 2.0

Ms ilk olarak .Net Core 1.0 versiyonunu release etti ancak bu sürüm biz .net developer'lar için biraz alışkın olduğumuzdan farklı bir mimariye sahipti ve geliştirme yaparkende bazı zorluklarıda bulunuyordu. Microsoft 1.0 dan sonra yayınladığı 2.0 versiyonu ile birlikte .Net Standart 2.0 adında bir yenilik daha getirdi. .Net standart microsoft için şu demekti ben bütün .net teknolojilerini, dillerini vs hepsini bir standart altında topluyorum ve bunun adınıda .Net standart 2.0 koyuyorum dedi. Bu standart ile birlikte bütün .net platformlarının base'i .Net Standart olmuş oldu ve CoreCLR adında yeni bir ortak iletişim dili geliştirildi.

Asp.Net Core'un getirdiği yenilikleri ve faydaları sıralayacak olursak

  • Cross platform
  • High performance
  • Flexible deployment
  • Open source
  • Support for built-in dependency injection
  • Light-weight and modular
  • Side by side support
  • Faster development


Mac ve Linux işletim sistemlerinde çalışır hale getirildi ve cross platfrom coverage'ını yükseltmiş oldu.


Geliştirme Yaparken

İlk olarak bilgisayarınızda Vs 2015 update 3 yada vs2017 .Net Core SDK ile birlikte yüklü olması gerekmekte.
Proje oluşturuken ise klasik herzaman yaptığımız gibi new aşağıdaki gibi new project diyerek geliştirmek istediğimiz proje türünü seçerek solution'ımızıo yaratabiliriz.


Bu yazıda Asp.Net Core'a küçük bir giriş yaptık. Diğer yazılarımızda framework'ü örnek uygulamalar ile incelemeye devam edeceğiz.

Parallel LINQ Nedir

Linq(Language Integrated Query), heralde .Net framework'e ait en önemli 4-5 özellikten birisidir desek kimsenin itirazı olmaz. .NET 3.5 ile hayatımıza giren linq temelde; senkron bir biçimde veri üzerinde çeşitli query'ler yazıp filtreler oluşturabildiğimiz çok zengin içeriği olan bir .Net teknolojisidir. 

Parallel Linq (PLINQ), IEnumerable yada Enumerable<T> türündeki data-source'lar üzerinde linq işlemlerini paralel bir şekilde async olarak yapıalbilmesini sağlayan yapıdır. Bu işlemler PLinq'e ait bazı extension metotlar üzerinden yapılabilmektedir ve bu metotlar sayesinde eşzamanlı olarak işlem process edilebilmektedir. 

Örnek üzerinden ilerleyecek olursak, console'a 0-100 arası 4'e bölünebilen sayıları önce linq sonrada plinq kullanarak yazdıralım. İlk olarak aşağıdaki gibi kullanacağımız veriyi hazırlayalım.

    var source = new List<int>();
    for (int i = 0; i <= 100; i++)
    {
        source.Add(i);
    }

Linq kullanarak aşağıdaki kod bloğunu yazıp projeyi run edelim.

    Console.WriteLine("Non Parallel Result\n");
    var nonParallelResults =
                     from item in source
                     where item % 4 == 0
                     select item;

    foreach (int item in nonParallelResults)
    {
        Console.Write(item + " ");
    }
    Console.WriteLine("\n");
    Console.WriteLine("\n");

Projemizi run ettiğimizde aşağıdaki gibi sırasıyla sayıları ekrana display edecektir.

Şimdi ise aynı işlemi plinq kullanarak paralel olarak yaptıralım.

    Console.WriteLine("Parallel Result\n");
    var parallelResults =
                     from item in source.AsParallel()
                     where item % 4 == 0
                     select item;
    foreach (int item in parallelResults)
    {
        Console.Write(item + " ");
    }

Tekrardan projeyi run ettiğimizde ekran çıktısı aşağıdaki gibi olacaktır.

 Yukarıdaki işlemi paralel biçimde yapmamızı sağlayan şey .AsParallel() extension metodu dur. Linq kullanarak baktığımızda 4'e bölüne bilen sayıları küçükten büyüğe sıralayarak ekrana print ettiğini görürken PLinq kullanarak sayıların herhangi bir sıraya uymadan paralel bir şekilde print edildiğini gördük.

Source üzerinde Linq mu yoksa PLinq mu kullanılacağı tamamiyle yapacağınız iş ile ilgili bir durum yani "it depends on the business". Eğer sırasıyla devam etmesini istediğiniz bir process değilse ve process edilen işlemler arası herhangi bir bağımlılık vs. bulunmuyorsa PLinq çok rahat bir şekilde kullanabiliriz. 

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.