Caner Tosuner

Leave your code better than you found it

Asp.Net Core Distributed Cache Nedir ? Redis İle Kullanımı

Daha önceki yazılarımızda Asp.Net Core da in-memory cache nedir nasıl kullanılır konularına değinmiştik. Bu yazımızda ise asp.net core projelerinde distributed cache nasıl uygulanır inceleyeceğiz.

Distributed Cache

Distributed cache projelerimizde daha performanslı ve ölçeklenebilir (scalability) modüller geliştirebilmemize olanak sağlar. In-Memory cache'de o an uygulamanın çalışmakta olduğu server'ı cache storage olarak kullandığımızdan birden fazla server'da çalışan uygulamalar için bu cache'in dağıtık olarak bütün sunucularda bulunan uygulamalara paylaştırılması ve yönetilmesi gerekmektedir. Distributed cache'de veriler merkezi olarak store edilir ve böylelikle sunuculardan herhangi biri down olduğunda bile diğer sunucularda bulunan uygulamalar cache'de bulunan data'yı kullanabilmektedirler. Ayrıca cache yapısını bozmadan istediğiniz kadar yeni app-server eklediğinizde veya stop ettiğinizde cache bundan etkilenmeyecektir. 

1-) İlk adım olarak Makinamızda redis-server kurulu olması gerekmektedir. Kurulum ile ilgili şu yazıdan yararlanabilirsiniz ve localhost:6379'dan bağlanacağımız varsayalım.

Örnek proje

Redis kurduktan sonra örnek proje üzerinden ilerleyelim. İlk olarak vs'da bir asp.net core web api uygulaması oluşturalım. 

Asp.Net Core uygulamalarında distributed-cache özelliğini kullanabilmek için Microsoft.Extensions.Caching nasmespace'i altında bulunan ve nuget'ten de kurabildiğimiz IDistributedCache interface'i bulunmaktadır ve bu interface içerisinde cache CRUD işlemlerini senkron-asenkron şekilde yapabilmemizi sağlayan metotlar bulunmaktadır.

  public interface IDistributedCache
  {
    byte[] Get(string key);
    Task<byte[]> GetAsync(string key, CancellationToken token = default (CancellationToken));
    void Set(string key, byte[] value, DistributedCacheEntryOptions options);
    Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default (CancellationToken));
    void Refresh(string key);
    Task RefreshAsync(string key, CancellationToken token = default (CancellationToken));
    void Remove(string key);
    Task RemoveAsync(string key, CancellationToken token = default (CancellationToken));
  }

2-) İkinci adım olarak Startup.cs içerisinde bulunan ConfigureServices metodu içerisinde uygulamamız servislerine redis-distributed-cache'i configure edip eklememiz gerekmekte.

 public void ConfigureServices(IServiceCollection services)
 {
     services.AddDistributedRedisCache(option =>
     {
         option.Configuration = "localhost:6379";
     });
 }

3-) Üçüncü ve son adım ise IDistributedCache interface'ini kullanarak cache crud işlemlerini yapmak. Bunun için asp.net core web api projelerinde default olarak gelen ValuesController'ı kullanalım.

[Route("api/[controller]")]
public class ValuesController : Controller
{
    private readonly IDistributedCache _distributedCache;

    public ValuesController(IDistributedCache distributedCache)
    {
        _distributedCache = distributedCache;
    }

    // GET api/values
    [HttpGet]
    public async Task<string> Get()
    {
        const string cacheKey = "values";

        var cachedItem = await _distributedCache.GetStringAsync(cacheKey);
        if (!string.IsNullOrEmpty(cachedItem))
        {
            return cachedItem; 
        }
        else
        {
            const string str = "value1 value2 value3 value4 etc.";
            await _distributedCache.SetStringAsync(cacheKey, str);
            return str;
        }
    }
}

Yukarıda görüldüğü üzre Get metoduna gelen istekte ilk olarak ilgili key'e ait cache'de bir değer var mı kontrol edilir varsa cache'de bulunan değer return edilir yoksa ilgili storage'a (database etc.) gidilir return value alınır önce cache'e atılır sonrasında return edilir.

Asp.Net Core uygulamalarında distributed-cache konfigurasyonu ve kullanımı bu kadar basitti diyebiliriz. Redis gibi couchbase veya sql server kullanarak da asp.net core uygulamalarında distributed cache özelliğini kazandırabilirsiniz ve IDistributedCache interface'ini kullanarak basitçe cache crud işlemlerini yönetebilirsiniz.

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.

StructureMap Nedir ? WebApi ile Kullanımı

Daha önceki IoC container yazılarında Ninject ve Windsor 'dan bahsetmiştik. Bu yazımızda ise 2016 benchmark'larına göre en hızlı IoC container olduğu söylenen StructureMap'i WebApi üzerinde örnek proje ile inceleyeceğiz. 

StructureMap ilk release'i .Net framework 1.1 için 2004 yılında çıkmış ve 12 yıldır hayatımızda olan en eski IoC/DI tool'u dur. Uygulama genelindeki instance yönetiminden sorumlu olup bağımlılıkları enjecte edebilmemizi sağlar.

Yazımızda StructureMap kullanarak basit bir infrastructure tasarlamaya çalışacağız. 

İlk olarak Vs'da bir Web Api projesi oluşturalım.

Sonrasında projemize nuget üzerinden StructureMap.WebApi2 paketini install edelim.

Kurulum işlemi tamamlandıktan sonra solution'da DependencyResolution adında auto generate olan bir klasör ve hem bu klasör içerisinde hemde App_Start klasörü içerisinde StructureMap konfigurasyonlarını yapabilmemizi sağlayacak olan class'ları göreceğiz. Şimdilik bu klasörü es geçelim projemizi hazır hale getirdikten sonra gerekli register işlemlerini yaparız. 

Örnek case'imiz şu şekilde olsun; UserController adında bir controller oluşturalım ve bu controller içerisinde tanımlı IUserService intercase'ini contructor injection yöntemi ile inject edelim. GetUserFullNames adında end-point ile geriye List of string dönelim.

    public class UserController : ApiController
    {
        private readonly IUserService _userService;
        public UserController(IUserService userService)
        {
            _userService = userService;
        }

        [HttpGet]
        public HttpResponseMessage GetUserFullNames()
        {
            var response = _userService.GetUserFullNames();

            return Request.CreateResponse(response);
        }
    }

Controller içerisinde kullandığımız IUserService interface'i ve onun implemantasyonunu aşağıdaki gibi oluşturalım.

public interface IUserService
{
    List<string> GetUserFullNames();
}
 
public class UserService : IUserService
{
    public List<string> GetUserFullNames()
    {
        return new List<string>
                              { "Olcay Şahan",
                                "Anderson Talisca",
                                "Oğuzhan Özyakup",
                                "Ricardo Quaresma",
                                "Cenk Tosun" };
    }
}

Yukarıda da söylediğimiz gibi UserController içerisindeki GetUserFullNames metodu HttpGet isteği alarak geriye UserService içerisinde bulunan GetUserFullNames metodunun return ettiği List of string'i dönecektir.

Örneğimiz hazır. Şimdi ise geriye son 2 adım kaldı.

1-) StructureMap ile ilgili container konfigurasyonlarını yapmak. DependencyResolution klasörü içerisinde bulunan DefaultRegistry adlı class'a gidip aşağıdaki gibi UserService'i container'a register edeceğiz.

    public class DefaultRegistry : Registry {
        #region Constructors and Destructors

        public DefaultRegistry() {
            Scan(
                scan => {
                    scan.TheCallingAssembly();
                    scan.WithDefaultConventions();
                });
            For<IUserService>().Use<UserService>();
        }

        #endregion
    }

2-) Son olarak App_Start/WebApiConfig.cs class'ına gidip DI container'ı start etmemiz gerekiyor. 

    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API configuration and services

            // Web API routes
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
            
            //structureMap start
            StructuremapWebApi.Start();
        }
    }

Hem örneğimiz hemde container ile ilgili konfigurasyonlarımız hazır. Artık projemizi run edip yazdığımız kodları test edebiliriz. Bunun için projemizi run edelim ve yazmış olduğumuz http-Get end-point'ini browser da çağıralım.

Yukarıda görüldüğü gibi UserController da bulunan end-point'e call yaptık ve bize container'da bulunan IUserService'ine ait olan implementasyonu resolve edip UserService içerisinde bulunan GetUserFullNames metodunu execute edip geriye user listesini return etti

 StructureMap yazımız şimdilik bu kadar. İlerleyen günlerde daha farklı DI Container yazılarına devam edeceğiz.

Castle Windsor Kullanarak Cache Interceptor Oluşturma

Daha önceki yazılarımızda IoC nedir ve Castle Windsor Kullanarak Cache Interceptor Oluşturma konularına değinmiştik. Bu yazımızda ise yine Windsor kullanarak projelerimizde sıkça kullandığımız bir özellik olan Caching işlemini basitçe yapabilen bir interceptor oluşturacağız.

Bir Api projesi oluşturalım ve projemizde cache ile ilgili işlemlerin yönetildiği ICache isminde aşağıdaki gibi bir interface'imiz olsun ve arka planda kullandığınız herhangi bir cache yapısı olabilir (Redis, memory cache, vs.) bu interface'in implementasyonunu kullandığınız cache yapısına göre yapmış varsayalım.

public interface ICache : IDisposable
{
    object Get(string key);
 
    void Set(string key, object obj, DateTime expireDate);
 
    void Delete(string key);
 
    bool Exists(string key);
}

Şimdi ise projemize Castle'ı nuget'ten indirip kuralım ve sonrasında aşağıdaki gibi CacAttribute adında bir attribute tanımlayalım. Bu attribute'ü kullanarak cache'e atmak istediğimiz metotları bir nevi flag'lemiş olacağız.

    [AttributeUsage(AttributeTargets.Method)]
    public class CacheAttribute : Attribute
    {
        public int CacheDurationInSecond { get; private set; }

        public CacheAttribute(int cacheDurationInSecond = 300)
        {
            CacheDurationInSecond = cacheDurationInSecond;
        }
    }

Örnek olarak projemizde LocationController adında bir controller ve içerisinde GetDistrictsByCityCode adında int cityCode parametresine göre geriye o şehirde bulunan ilçelerin listesini dönen bir end-point tanımlayalım. Projemizde kullanacağımız modellerimizi aşağıdaki gibi oluşturalım.

        public class BaseResponse
        {
            public bool IsSuccess { get; set; }
        }

        public class GetDistrictsByCityCodeResponse : BaseResponse
        {
            public List<string> CityList { get; set; }
        }

Controller'ın kullanacağı ILocationService ve onun implementasyonunu aşağıdaki gibi tanımlayalım.

        public interface ILocationService
        {
            [Cache(300)]
            GetDistrictsByCityCodeResponse GetDistrictsByCityCode(int cityCode);
        }

        public class LocationService : ILocationService
        {
            public GetDistrictsByCityCodeResponse GetDistrictsByCityCode(int cityCode)
            {
                return new GetDistrictsByCityCodeResponse
                {
                    CityList = new List<string> {
                        "Alaçam","Asarcık","Atakum",
                        "Ayvacık", "Bafra", "Canik",
                        "Çarşamba","Havza", "İlkadım",
                        "Kavak","Ladik", "Ondokuzmayıs",
                        "Salıpazarı","Tekkeköy", "Terme",
                        "Vezirköprü", "Yakakent" },
                    IsSuccess = true
                };
            }
        }

Yukarıda görüldüğü üzre GetDistrictsByCityCode metodu üzerinde CacheAttribute'ünü tanımladık ve cache süresi olarak 300 saniye yani 5 dk set ettik. Şimdi ise LocationController'ı oluşturalım ve controller içerisinde tanımlayacağımız end-point ILocationService'i kullanarak GetDistrictsByCityCode metoduna ilgili isteği yapacak. ILocationService'i controller'da kullanabilmek içinde controller'ın constructor'ında seviyesinde ILocationService'i inject edeceğiz.

        public class LocationController : ApiController
        {
            private readonly ILocationService _locationService;
            public LocationController(ILocationService locationService)
            {
                _locationService = locationService;
            }
            [HttpGet]
            public HttpResponseMessage GetDistrictsByCityCode(int cityCode)
            {
                var response = _locationService.GetDistrictsByCityCode(cityCode);
                return Request.CreateResponse(response);
            }
        }

Metodumuz geriye BaseResponse'dan türemiş olan GetDistrictsByCityCodeResponse objesini dönecek ve bu objede bulunan IsSuccess parametresini kontrol ederek modeli cache'e atıp atmamaya karar vereceğiz. Bunu yapmamızın sebebi hatalı response'ları cache atmayı önlemek. LocationService içerisindeki metotta dummy olarak geriye Samsun ilinin ilçelerini return eden bir response oluşturduk ve projemizi deneme amaçlı bunu return edeceğiz.

Sırada cache işlemlerinin yapıldığı aspect'imizi tanımlama var. Aşağıda olduğu gibi CacheInterceptor adında aspect'imizi tanımlayalım.

    public class CacheInterceptor : IInterceptor
    {
        private readonly ICache _cacheProvider;
        private readonly object _lockObject = new object();
        public CacheInterceptor(ICache cacheProvider)
        {
            _cacheProvider = cacheProvider;
        }

        public void Intercept(IInvocation invocation)
        {
            //metot için tanımlı cache flag'i var mı kontrolü yapıldığı yer
            var cacheAttribute = invocation.Method.GetAttribute<CacheAttribute>();
            if (cacheAttribute == null)//eğer o metot cache işlemi uygulanmayacak bir metot ise process normal sürecinde devam ediyor
            {
                invocation.Proceed();
                return;
            }

            lock (_lockObject)
            {
               //eğer o metot cache işlemlerinin yapılması gereken bir metot ise ilk olarak dynamic olarak aşağıdaki gibi bir cacheKey oluşturuyoruz
                var cacheKey = string.Concat(invocation.TargetType.FullName, ".", invocation.Method.Name, "(", JsonConvert.SerializeObject(invocation.Arguments), ")");
                //bu key ile tanımlı bir cache objesi var mı kontrol ediyoruz
                var cachedObj = _cacheProvider.Get(cacheKey);
                if (cachedObj != null)//eğer var ise o objeyi alıp client'a return ediyoruz
                {
                    invocation.ReturnValue = cachedObj;
                }
                else
                {
                    //yok ise metottan çıktıktan sonra return edilen response'u 
                    invocation.Proceed();
                    var returnValue = invocation.ReturnValue;
                    var response = returnValue as BaseResponse;
                    if (response != null)
                    {
                        //eğer o response bizim beklediğimiz gibi BaseResponse'dan türeyen bir model ise o modeli alıyoruz ve  başarılı bir şekilde client'a dönülen bir response ise cache'e atıyoruz
                        if (response.IsSuccess)
                        {
                            var cacheExpireDate = DateTime.Now.AddSeconds(cacheAttribute.CacheDurationInSecond);
                            _cacheProvider.Set(cacheKey, invocation.ReturnValue, cacheExpireDate);
                        }
                    }
                }
            }
        }
    }

Yukarıdaki akışı anlatmak gerekirse;

  • İlk olarak ilgili metot üzerinde bir cache flag'i var mı yok mu ona bakıyoruz. Eğer metot üzerinde tanımlı CacheAttribute'ü var ise o metot bizim için cacheinterceptor tarafından cache işlemlerine tabi tutulur demek. Yoksa hiç cache'i karıştırmadan process devam eder.
  • CacheAttribute varsa sonraki adımda metot namespace + metot ismi + metot parametrelerinden oluşan bir dynamic cacheKey oluşturuluyor ve bu cacheKey ile cache provider'ınızda (redis, memory cache, vs.) daha önce tanımlanmış olup halen geçerliliği olan bir cache objesi var mı bu kontrol yapılıyor.
  • Eğer varsa cache de bulunan obje return ediliyor ve GetDistrictsByCityCode metoduna girmeden client'a cevap dönülüyor.
  • Eğer cache'de bulunan bir obje yoksa GetDistrictsByCityCode metoduna girip ilgili işlemleri yaptıktan sonra metottan çıkıyor. Sonrasında metodun return ettiği model BaseResponse'dan türeyen bir model ise ve IsSuccess'i true ise obje cache'e atılıyor ve daha sonra client'a dönülüyor.

Geriye son adım olarak da ilgili bağımlılıkları tanımlayıp CacheInterceptor'ını install etmek kalıyor.  Aşağıdaki gibi ilk olarak interceptor'ı install ediyoruz ve sonrasında kullanmak istediğimiz LocationService için register ediyoruz.

        public class ServiceInstaller : IWindsorInstaller
        {
            public void Install(IWindsorContainer container, IConfigurationStore store)
            {
                container.Register(Component.For<CacheInterceptor>().LifestyleSingleton());

                container.Register(Component.For(typeof(ILocationService))
                         .ImplementedBy(typeof(LocationService))
                         .Interceptors(typeof(CacheInterceptor)));
            }
        }

Installer'ı container'a tanımlamak için projemizde bulunan Global.asax içerisindeki Application_Start metodu içerisinde container'ı aşağıdaki gibi ayağa kaldırıyoruz.

protected void Application_Start()
{
    var container = new WindsorContainer();
    container.Install(new ServiceInstaller());
    container.Install(new WebApiControllerInstaller());
    GlobalConfiguration.Configuration.Services.Replace(
        typeof(IHttpControllerActivator),
        new ApiControllerActivator(container));
    GlobalConfiguration.Configure(WebApiConfig.Register);
}

Yukarıda bulunan WebApiControllerInstaller ve ApiControllerActivator daha önceki yazımızda  oluşturduğumuz installer ile birebir aynıdır. 

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

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();
        }
    }
}

Projemiz hazır ve artık yazmış olduğumuz end-point'i test edebiliriz. GetDistrictsByCityCode metoduna ilk gelen request'ten sonra return edilen response cache'e atılacaktır ve CacheAttribute tanımlarken verdiğiniz cacheDuration süresi içerisindeki ikinci request'i yaptığınızda GetDistrictsByCityCode scope'ları arasındaki işlemleri yapmadan ilgili response oluşturulan cacheKey ile bulup cache'den döndüğünü göreceksinizdir. 

NInject Nedir ? NInject Kullanarak Web Api İçin Dependency İnjection

Daha önceki IoC yazılarımızda Castle Windsor dan bahsedip örnekler üzerinden kütüphaneyi incelemiştik. Bu yazımızda ise çokça yaygın olarak kullanılan IoC container'lar dan biri olan NInject'i Web APi üzerinde inceleyeceğiz.

Ninject oldukça popüler IoC container'lar dan biri olup bağımlılıkları enjekte etmede kullanılan open source bir kütüphanedir. Dependency injection bizlere loosely coupled dediğimiz birbirlerine gevşek bağlı ve daha kolay test edilebilir geliştirmeler yapmamızı sağlayan bir design pattern dir. IoC ise belli özelliklere sahip ve birbirlerine bağımlı nesnelerin işlevlerini gerçekleştirmek için ihtiyacı olan instance'ları kendilerinin değilde bir IoC container tarafından yönetilmesini söyler.

Bu yazımızda ise Web Api üzerinde Ninject implementasyonu nasıl yapılır bunu inceliyor olacağız.

İlk olarak VS'da bir tane Web Api projesi oluşturalım ve proje referanslarına nuget üzerinden Ninject.Web.WebApi.WebHost'u aşağıdaki gibi bulup install edelim.

Kurulum bittikten sonra projede bulunan App_Start klasörü içerisinde NinjectWebCommon adında otomatik olarak bir class oluşturulduğunu göreceksiniz. Bu class içerisinde ilgili install ve register işlemlerini yapacağız. Class içerisinde baktığınızda oto-generated olan bir çok kod bloğu var ancak biz şimdilik sadece   RegisterServices() metodu ile haşır neşir olacağız.

        /// <summary>
        /// Load your modules or register your services here!
        /// </summary>
        /// <param name="kernel">The kernel.</param>
        private static void RegisterServices(IKernel kernel)
        {

        }  

Register etmek istediğimiz bağımlılıkları bu metot içerisine tanımlayacağız.

Öncelikle UserController adında örnek bir controller oluşturalım ve bu controller içerisinde tanımlı IUserService intercase'ini contructor injection yöntemi ile inject edelim.

    public class UserController : ApiController
    {
        private readonly IUserService _userService;
        public UserController(IUserService userService)
        {
            _userService = userService;
        }

        [HttpGet]
        public HttpResponseMessage GetUserFullNames()
        {
            var response = _userService.GetUserFullNames();

            return Request.CreateResponse(response);
        }
    }

Controller içerisinde kullandığımız IUserService interface'i ve onun implemantasyonunu aşağıdaki gibi oluşturalım.

    public interface IUserService
    {
        List<string> GetUserFullNames();
    }

    public class UserService : IUserService
    {
        public List<string> GetUserFullNames()
        {
            return new List<string>
                                  { "Olcay Şahan",
                                    "Anderson Talisca",
                                    "Oğuzhan Özyakup",
                                    "Ricardo Quaresma",
                                    "Cenk Tosun" };
        }
    }

UserController içerisindeki GetUserFullNames metodu HttpGet isteği alarak geriye UserService içerisinde bulunan GetUserFullNames metodunun return ettiği List of string'i dönecektir.

Şimdi sırada bağımlılıkları inject etmek var. Yukarıda NinjectWebCommon class'ı içerisinde bulunan RegisterServices metodunda IUserService'i register edeceğiz. 

        private static void RegisterServices(IKernel kernel)
        {
            kernel.Bind<IUserService>().To<UserService>();
        }

Yukarıda görüldüğü üzre implementasyon oldukça basit ve projemiz hazır durumda. UserController içerisindeki metoda Postman kullanarak aşağıdaki gibi request atıp sonucu görelim.

 

Görüldüğü üzre Ninject controller'ın constructor'ında IUserService'i için bize UserService'ini resolve etti ve herhangi bir yeni instance oluşturmadan kolayca controller'ı kullanabildik.

Ninject implementasyonu diğer container'lara göre daha basittir. Sonraki yazılarımızda Ninject ile ilgili örneklerimize devam edeceğiz.

Web Api Projelerine Swagger Ekleme

Server-side bir geliştirme yapıyorsanız ve yazmış olduğunuz end-point'ler farklı client'lar tarafından kullanılacaksa api da bulunan end-point'lerin kullanımını açıklayıp request-response örneklerini içeren bir döküman yazmak bizler için kaçınılmaz bir iş.

Asp.Net Web Api mimarları aslında bu durum için Help Page ile bir çözüm sunmaya çalışmışlar ancak tam anlamıyla yeterli olamamış. Yeni bir empty olmayan web api projesi oluşturduğunuzda nuget üzerinde Microsoft ASP.NET Web Api Help Page projenize yüklü olarak gelir ve browser üzerinden adres kısmına {IIS de ki uygulama ismi}/help diyerek web api için hazırlanmış olan help page dökümanına ulaşabiliriz ve ekran görüntüsü aşağıdaki gibidir.

Ancak bu döküman bize yazının başında bahsettiğimiz örnek request atıp response alabilmemizi sağlamamakta. Yani bir nevi api'ı gerçek veya fake datalarla test etmemize olanak sağlamamakta. Help page kabaca; yazılan controller'lar da bulunan end-point'ler ve bu end-point'lerin request response modellerini listelemekte.

 

Swagger

Help Page'in hem yapabildiklerini yapan hemde yapamadıklarının fazlasını yapabilen bir tool olan swagger'dan bahsedeceğiz. Swagger.io tarafından şekilde tanımlanmıştır;

"Swagger is a simple yet powerful representation of your RESTful API. With the largest ecosystem of API tooling on the planet, thousands of developers are supporting Swagger in almost every modern programming language and deployment environment. With a Swagger-enabled API, you get interactive documentation, client SDK generation and discoverability."

-swagger.io

Swagger yazılım dünyası tarafından oldukça büyük çapta kabul görmüş yaygın olarak kullanılan bir dynamic döküman oluşturma tool'u dur. .Net tarafı için entegrasyonu oldukça basittir. 

Not: Swagger'ı projenize entegre ettikten sonra hep page'i de kullanmaya devam edebiliyorsunuz yani biri diğerinin yerini almıyor.

Swagger Kurulumu

Projemize swagger eklemek için open source olarak geliştirilen Swashbuckle adındaki kütüphaneyi projemizde Nuget Package Manager Console kullanarak indirip kuracağız.

PM> Install-Package Swashbuckle

Kurulum işlemi bittikten sonra solution da bulunan App_Start klasörünü açarak içerisine swagger configuration işlemleri için SwaggerConfig.cs adında bir class eklendiğini göreceğiz.

Configuring Swagger

SwaggerConfig.cs içerisi default olarak aşağıdaki gibidir.

    public class SwaggerConfig
    {
        public static void Register()
        {
            var thisAssembly = typeof(SwaggerConfig).Assembly;

            GlobalConfiguration.Configuration
                .EnableSwagger(c =>
                    {
                        c.SingleApiVersion("v1", "WebApplication1");
                    })
                .EnableSwaggerUi();
        }
    }

Projenizi run ettiğinizde browser üzerinden Swagger Ui sayfasına {IIS de ki uygulama ismi}/swagger şeklinde ulaşabilirsiniz ve sayfa default olarak aşağıdaki gibidir.

Yukarıda da görüldüğü gibi projemizde controller'lar içerisinde tanımlı end-point'ler, Http Request türleri, aldıkları parametreler vs gibi bilgiler yer almaktadır.

Örnek olarak POST /api/Values metodunu deneyelim. Metot isminin üzerine tıkladığımızda altta bir view expand olur ve burada request olarak göndereceğimiz parametreleri yazıp response'u alabiliriz. 

Yukarıdaki ekran görüntüsünde kısaca Values metodu string bir parametre alıyor ve geriye string bir response dönüyor. Request parametresini yazdıktan sonra Try it out butonuna tıkladığımızda aşağıdaki gibi bir ekranla karşılaşıyoruz.

 

Özetle

Biz yazılımcılar için çile haline gelen request response örnek kodları açıklama döküman vs gibi konuları swagger ile gayet basit ve kullanışlı bir hale getirebiliriz. Swagger ile ilgili daha bir çok configuration bulunmakta. VS üzerinden XML dosya generate ederek kodlarınızın üzerinde bulunan yorumlardan yola çıkarak api dökümanı oluşturma gibi bir çok özelliği bulunmakta. Ayrıntılı bilgi için Swagger.io Swashbuckle ile ilgili güncel ve daha ayrıntılı bilgileri bu linkten takip edebilirsiniz.

 

Not: Yukarıda basit anlamıyla swagger'ı anlatmaya çalıştım ancak yazının başında da belirttiğim gibi swagger.config dosyasını doğru yorumlayabildikten sonra daha bir çok özelliğini keşfedebilirsiniz. 

Web Api FluentValidation Kullanımı

Özellikle server-side geliştirme yapan kişiler için validation olmazsa olmazlardan biridir. Yazmış olduğunuz api'a client'lar belli başlı parametreler göndererek request'te bulunurlar ve sürekli bir veri alış verişi söz konusudur ve server-side geliştirici olarak asla gönderilen input'a güvenmememiz gerekir. Metotlara gönderilen parametreleri belli başlı bazı güvenlik adımlarından&validasyonlardan geçirdikten sonra işlemlere devam ediyor veya etmiyor olmamız gerekir. FluentValidation kütüphanesi ile bu gibi durumlar için belli validation-rule'lar oluşturarak unexpected-input dediğimiz istenmeyen parametrelere karşı önlemler alabiliriz.

Örnek üzerinden ilerleyecek olursa; Bir Web Api projemiz olsun ve bu proje için Para Gönderme (Eft & Havale) işlemi yapan bir modül yazalım. MoneyTransferRequest adında bir model'imiz olsun ve bu model üzerinden para göndermek için gerekli olan bilgiler kullanıcıdan alalım. MoneyTransferRequest model için FluentValidation kütüphanesini kullanarak gerekli validation işlemlerini tanımlayalım validation-failed olduğunda bu durumdan client'ı hata mesajları göndererek bilgilendirelim, yani validationMessage'ı response olarak client'a return edelim. 

İlk adım olarak vs'de açtığımız Web Api projemize nuget'ten FluentValidation.WebApi kütüphanesini indirip kuralım

MoneyTransferRequest.cs class'ımız aşağıdaki gibi olacaktır.

    public class MoneyTransferRequest
    {
        public decimal Amount { get; set; }
        public string SenderIBAN { get; set; }
        public string ReceiverIBAN { get; set; }
        public DateTime TransactionDate { get; set; }
    }

Şimdi ise sırada MoneyTransferRequest için yazacağımız Validator class'ı var. Bu class AbstractValidator<T> class'ından inherit olmak zorundadır ve request modelimizin property'leri için geçerli olan rule'ları burada tanımlayacağız.

    public class MoneyTransferRequestValidator  : AbstractValidator<MoneyTransferRequest>
    {
        public MoneyTransferRequestValidator()
        {
            RuleFor(x => x.Amount).GreaterThan(0).WithMessage("Amount cannot be less than or equal to 0.");

            RuleFor(x => x.SenderIBAN).NotEmpty().WithMessage("The Sender IBAN cannot be blank.").Length(16, 26).WithMessage("The Sender IBAN must be at least 16 characters long and at most 26 characters long.");

            RuleFor(x => x.ReceiverIBAN).NotEmpty().WithMessage("The Receiver IBAN cannot be blank.").Length(16, 26).WithMessage("The Receiver IBAN must be at least 16 characters long and at most 26 characters long.");

            RuleFor(x => x.TransactionDate).GreaterThanOrEqualTo(DateTime.Today).WithMessage("Transaction Date cannot be any past date.");
        }
    }

Tanımlamış olduğumuz MoneyTransferRequestValidator class'ını MoneyTransferRequest class'ı için olduğunu belirten tanımlamayı aşağıda olduğu gibi attribute gibi class ismi üzerinde belirtiyoruz.

    [Validator(typeof(MoneyTransferRequestValidator))]
    public class MoneyTransferRequest
    {
        public decimal Amount { get; set; }
        public string SenderIBAN { get; set; }
        public string ReceiverIBAN { get; set; }
        public DateTime TransactionDate { get; set; }
    }

FluentValidationModelValidatorProvider'ı WebApiConfig class'ı içerisinde aşağıdaki enable ederek validator için config işlemlerimizi tamamlamış oluyoruz.

public static class WebApiConfig  
{
    public static void Register(HttpConfiguration config)
    {
        FluentValidationModelValidatorProvider.Configure(config);
    }
}

Yapılan request'leri tek bir yerden handle edip valid bir işlem mi değil mi kontrolü için custom ActionFilter'ımızı tanımlayalım. Bu actionFilter validation'dan success alınamaz ise yani MoneyTransferRequest modeli için tanımladığımız validasyon kuralları sağlanmaz ise client'a yeni bir response dönüp içerisine validationMessage'ları set ediyor olacağız.

public class ValidateModelStateFilter : ActionFilterAttribute  
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        if (!actionContext.ModelState.IsValid)
        {
            actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);
        }
    }
}

Bu action filter için register işlemini aşağıdaki gibi yapıyoruz.

        public static class WebApiConfig
        {
            public static void Register(HttpConfiguration config)
            {
                config.Filters.Add(new ValidateModelStateFilter());//register ettik
                FluentValidationModelValidatorProvider.Configure(config);
            }
        }

Client'a döndüğümüz response'lar için bir base response tanımlayalım.

    public class BaseResponse
    {
        public BaseResponse(object content, List<string> modelStateErrors)
        {
            this.Result = content;
            this.Errors = modelStateErrors;
        }
        public List<string> Errors { get; set; }

        public object Result { get; set; }
    }

Şimdi ise custom ResponseHandler'ımızı tanımlicaz. Bu handler her bir response'u kontrol ederek yukarıda tanımlamış olduğumuz BaseResponse'a convert edicek ve client'a bu response modeli dönecek. 

    public class ResponseWrappingHandler : DelegatingHandler
    {
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            var response = await base.SendAsync(request, cancellationToken);

            return BuildApiResponse(request, response);
        }

        private HttpResponseMessage BuildApiResponse(HttpRequestMessage request, HttpResponseMessage response)
        {
            object content;
            var modelStateErrors = new List<string>();

            if (response.TryGetContentValue(out content) && !response.IsSuccessStatusCode)
            {
                var error = content as HttpError;
                if (error != null)
                {
                    content = null; 

                    if (error.ModelState != null)
                    {
                        var httpErrorObject = response.Content.ReadAsStringAsync().Result;

                        var anonymousErrorObject = new { message = "", ModelState = new Dictionary<string, string[]>() };

                        var deserializedErrorObject = JsonConvert.DeserializeAnonymousType(httpErrorObject, anonymousErrorObject);

                        var modelStateValues = deserializedErrorObject.ModelState.Select(kvp => string.Join(". ", kvp.Value));

                        for (var i = 0; i < modelStateValues.Count(); i++)
                        {
                            modelStateErrors.Add(modelStateValues.ElementAt(i));
                        }
                    }
                }
            }

            var newResponse = request.CreateResponse(response.StatusCode, new BaseResponse(content, modelStateErrors));

            foreach (var header in response.Headers) 
            {
                newResponse.Headers.Add(header.Key, header.Value);
            }

            return newResponse;
        }
    }

ResponseHandler'ı da api için register ediyoruz ve WebApiConfig class'ının son hali aşağıdaki gibi olacaktır.

        public static void Register(HttpConfiguration config)
        {
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            config.Filters.Add(new ValidateModelStateFilter());
            config.MessageHandlers.Add(new ResponseWrappingHandler());
            FluentValidationModelValidatorProvider.Configure(config);
        }

 Artık Api'ı test edebiliriz. Ben genelde bu tarz işlemler için postman rest client'ı kullanıyorum. Aşağıdaki gibi hatalı parametreler ile bir request'te bulunduğumuzda nasıl response alıyoruz inceleyelim.

 

Görüldüğü üzre Amount, SenderIBAN ve TransactionDate alanlarını hatalı girdiğimizde yukarıdaki gibi validation message'larının döndüğü bir response alıyoruz. ReceiverIBAN alanı validasyona takılmadığından bu alan ile ilgili herhangi bir mesaj almadık.

Özetle yazımızın başında da belirttiğim gibi Validasyon oldukça önemli bir konudur ve client hatalı bir input gönderirse anlaşılması kolay bir response oluşturarak ilgili validasyon mesajını client'a return etmek işimizi oldukça kolaylaştıracaktır. Yapmış olduğumuz bu geliştirme ile birlikte otomatik bir biçimde FluentValidation tarafından fırlatılan validasyon mesajları tam response dönme anında handle edilip client'a döndürmektedir.

Web Api MemoryCache Kullanımı

Daha önce Web Api ile ilgili yazdığımız yazılarda çeşitli konulara değinmiştik ve bunlardan bir tanesi de yazısıydı. Bu yazımızda yine web api için cache'den bahsediyor olacağız anack bu sefer output cache değilde uygumala içerisinde kendimiz manuel olarak memory'e atıp daha sonra kullanmak istediğimiz de alıp modify edebilmemizi sağlayan veya direkt olarak output cache de olduğu gibi alıp client'a dönmemizi sağlayan yapı MemoryCache den bahsediyor olacağız. MemoryCache .Net 4.0 ile birlikte System.Runtime.Caching.dll içerisinde sunulmuş bir yapı olarak karşımıza çıkıyor. Projemize bu dll'i referans olarak eklemek için solution'da bulunan references'a sağ tıklayıp add reference deyip Assemblies => Framework kategorisine tıkalyıp gelen listeden System.Runtime.Caching.dll'ini seçip ekliyoruz.

Referansımızı ekeldikten sonra projemize MemoryCacheManager adında adında bir class ekleyelim. Case şöyle olsun, ProductController.cs adında bir controller ve içerisinde GetAllProducts ve GetProductById adında iki metot tanımlayalım. İlk metotda geriye döndüğümüz product listesini ICache den implement olan MemoryCacheManager class'ını kullanarak MemoryCache'e atalım ve GetProductById metoduna Id parametresi ile request'te bulunulduğunda cache'den okuyup geriye product objesini dönelim.

ICache.cs

    public interface ICache
    {
        bool Contains(string key);//key varmı yokmu diye control ettiğimiz metot
        void Add<T>(string key, T source);//cache key'i ile birlikte cache model'i alıp cache'e ekleyen metot
        T Get<T>(string key);//key parametresi alarak cache'de ki data yı return eden metot
        void Remove(string key);//key parametresine göre mevcut cache'i silen metot
    }

 

MemoryCacheManager.cs

    public class MemoryCacheManager : ICache
    {
        ObjectCache cache;

        public MemoryCacheManager()
        {
            cache = MemoryCache.Default;
        }

        public void Add<T>(string key, T source)
        {
            //60 dakika boyunca cache'de tutacak
            var policy = new CacheItemPolicy { AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(60) };
            cache.Add(key, source, policy);
        }

        public bool Contains(string key)
        {
            return cache.Contains(key);
        }

        public T Get<T>(string key)
        {
            return (T)cache.Get(key);
        }

        public void Remove(string key)
        {
            cache.Remove(key);
        }
    }

 

Product.cs

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

 

ProductController.cs

    public class ProductController : ApiController
    {
        [HttpGet]
        public Product[] GetAllProducts()
        {
            Product[] products = new Product[]
            {
                new Product { Id = 1, Name = "Tomato Soup", Category = "Groceries", Price = 1 },
                new Product { Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M },
                new Product { Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M }
            };

            //ürünleri cache'e atıyoruz
            var cm = new MemoryCacheManager();

            string cacheKey = "products";
            if (cm.Contains(cacheKey))//varmı diye kontrol ediyoruz, eğer varsa mevcutu sil yeni listeyi ekle
                cm.Remove(cacheKey);
            cm.Add(cacheKey, products);

            return products;
        }

        [HttpGet]
        public Product GetProductById(int Id)
        {
            var cm = new MemoryCacheManager();
            string cacheKey = "products";
            if (cm.Contains(cacheKey))//varmı diye kontrol ediyoruz
                return cm.Get<Product[]>(cacheKey).FirstOrDefault(p => p.Id == Id);
            return null;
        }
    }

Projemizi run edip browser aracılığıyla önce GetAllProducts metoduna request'te bulunuyoruz ve product listesini return etmeden önce MemoryCacheManager'ın Add metodunu kullanarak geriye product listesini return etmeden önce listeyi alıp "products" key'i ile cache'e atıyor ve sonrasında return ediyor. Product listesini aldıktan sonra Id=2 olan ürün için GetProductById metoduna request'te bulunuyoruz ve ilk olarak MemoryCacheManager içerisine gidip "products" key'i ile mcache atılmış olan product listesini bulup alıyor ve sonrasında Id=2 olan product'ı bulup geriye döndürüyor.

Manuel olarak MemoryCache entegrasyonu bu şekilde yapabilirsiniz veya ihtiyaca göre farklı kullanımlarda uygulayabilirsiniz. Cache önemlidir arkadaşlar, doğru yerde kullanıldığında çok can kurtarır :)

Web Api Projelerinde Versiyonlama

Yazmış olduğunuz Api projelerinde versiyonlama yapmak oldukça önemlidir. Düzenli bir versiyonlama yaparak hem mevcut hemde yeni versiyonu kullanacak olan client'lar için tutarlı response'lar sağlayabilirsiniz. Örneğin v1.0 için çalışan listesi dönen bir api metodu yazdınız ve mevcutta kullanan kullanıcılar var. production'da. Sonra v2.0 de dediler ki çalışan isimlerinin yanında departman kodu da yazsın (Caner Tosuner, IT gibi). Her ne kadar kullanıcılar v2.0 geliştirmesini yapacak olsalarda belli bir süre daha eski versiyon için destek veriyor olmak gerekir çünkü bütün herkes aynı anda v2.0'a geçemeyebilir veya v1.0 da kalmak isteyebilir. İşte bu gibi durumları en iyi şekilde yönetilmemiz için versiyonlamayı son derece iyi yapmamız gerekir.

Web Api için versiyonlama yapmanın ortak kabul görmüş bir kaç yol bulunmakta;

  • URL de versiyon bilgisini query string olarak geçebiliriz,
  • Request Header'a "appVersion" gibi bir key/value ekleyebiliriz,
  • Genelde media type için kullanılsada AcceptHeader'a versiyon bilgisini ekleyebiliriz,
  • Her request'te BaseRequest'te içerisinde bir versiyon bilgisini alma.

Yukarıda sıralanan 4 yol da versiyonlama için çözüm ancak WebApi'ın son sürümüyle birlikte tercih edilmemeye başlandı hatta ve hatta yanlış çözüm olarak söyleyenler bile mevcut.

WebApi için versiyonlamayı Api metodlarının başına attribute olarak ekleyeceğimiz Route class'ı bulunmakta. Bu class'ı kullanarak kısaca hangi url bilgisi ile o metod çağrılabilir onu belirtmiş oluyoruz.

Burda dikkat etmemiz gereken konuların başında elimizden geldiğince mevcut URL'i bozmamaız gerekiyor.

Şimdi gelelim örneğimize.

Yukarıda bahsettiğimiz örnek üzerinden ilerleyelim ve EmployeeController adında bir ApiController'ımız var ve içerisinde GetEmployeeList() adında bir metod olsun. Bu metod öncelikle v1.0 için geriye çalışan adı ve soyadı bilgilerini içeren bir string array dönsün.

        [HttpGet]
        [Route("api/v1/Employee/{GetEmployeeList}")]
        public IHttpActionResult GetEmployeeList()
        {
            var list = new string[] { "İlhan Mansız", "Tümer Metin" };

            return Ok(list);
        }

Yukarıda ki gibi v1.0 için kullanıcılar bu metodu kullanmakta. GetEmployeeList() metoduna request'te bulunmak için url "http://localhost/api/v1/Employee/GetEmployeeList".

Ancak v2.0 da ad soyad bilgisinin yanında bir de departman adı istenmekte İlhan Mansız, Pazarlama gibi.

Şimdide v2.0 metodunu yazalım.

        [HttpGet]
        [Route("api/v2/Employee/{GetEmployeeList}")]
        public IHttpActionResult GetEmployeeListV2()
        {
            var list = new string[] { "İlhan Mansız, Pazarlama", "Tümer Metin, Satış" };

            return Ok(list);
        }

v2.0 metodunuda yazdık. Bu metoda request'te bulunmak için url "http://localhost/api/v2/Employee/GetEmployeeList".

Yukarıda da bahsettiğmiz gibi en dikkat etmemiz gerekn konuların başında client'ın yapacağı request'i ve api url'ini elimizden geldiğinde aynı tutmak ve bütün versiyonlarda url için değişen tek şeyin versiyon numarası olduğunu belirtmek. Böylelikle service tarafı için ayrımı url de bulunan versiyon numarasından alarak yapabiliriz.

Route attribute'ü kullanmadan versiyonlama işlemini farklı controller'lar kullanarak ta yapabilirsiniz. Üstteki örneğimizde olduğu gibi GetEmployeeList() meetodlarını içeren 2 farklı EmployeeV1Controller ve EmployeeV2Controller adında controller'lar tanımlayabiliriz. Url route işini de WebApiConfig.cs class'ı içerisine yine yukarıdaki url'leri kullanarak gerekli controller'lar da bulunan metodlara yönlendirmeleri yapabilirsiniz. Örneğin şu şekilde;

config.Routes.MapHttpRoute(
            name: "ApiV1_Route",
            routeTemplate: "api/v1/{controller}/{id}",
            defaults: new { controller = "EmployeeV1Controller", action = "GetEmployeeList", id = RouteParameter.Optional }
            );

config.Routes.MapHttpRoute(
            name: "Apiv2_Route",
            routeTemplate: "api/v2/{controller}/{id}",
            defaults: new { controller = "EmployeeV2Controller", action = "GetEmployeeList", id = RouteParameter.Optional }
            );

Yine "http://localhost/api/v1/Employee/GetEmployeeList" metoduna istekte bulunduğumuzda yukarıda yaptığımız config'den dolayı EmployeeV1Controller'ında bulunan GetEmployeeList metoduna yönlendirecektir. v2 için ise EmployeeV2Controller'ında bulunan GetEmployeeList metoduna yönlendirecektir.

 

Versiyonlama büyük çapta olan projeler için oldukça önemlidir. Production'a uygulamanızı çıktıktan sonraki zaman içerisinde tekrardan update'ler çıkmaya devam edeceksinizdir ve versiyonlama yaparak bu işlemleri yürütüyor olmak hem kolaylıklar sağlar hem de projenizi daha yönetilebilir hale getirir.

Web API İçin 5 Altın Kural

Çoğu zaman yazılım geliştirirken ortaya çıkan ürün yazılımcılar için değilde son kullanıcılar içindir ve bu kişiler teknik olmayan ve sadece UI tarafıyla haşır neşir olan kişilerdir. Ortaya çıkan ürünün iyi bir arayüzü vardır ve son kullanıcı arkada çalışan hangi teknolojiymiş, nasıl çalışıyormuş gibi bilgilerle hiç ilgilenmezler.

Ancak WebApi için bunu söyleyemeyiz. Yazılımcı olarak geliştirmiş olduğunu WebApi projesi end-user için değil bazen kim olduğunu bilmediğimiz başka yazılımcılar içindir. Bu kişiler teknik bilgisi en azından junior developer seviyesinde olan kişilerdir ve ortaya çıkan ürünün interface'i değilde yazılım tarafında ki her bir ince detayı inceleyip kullanacak olan kişilerdir ve sizi eleştirme hakkına da sahiptirler. 

Api'ı kullanacak olan kişiler yazılımcılar olduğundan geliştirmeyi yaparken bir WebApi client'ıymış bakış açısıyla yaklaşıp öyle geliştirme yapmak gerekir ve aslında bir nevi bu bakış açısını da Api'ı kullanan kişilerle de paylaşmış oluyoruz.

Api geliştiriciler genelde geliştirme sırasında şu sorulara odaklanırken ;"Bu servisin ne yapması gerekiyor ?", "Bu servis metodu ne sağlamalı ?", "Content-type json mı, xml mi ?" etc. , Api'ı kullanan kişiler "En az efor sarf ederek bu Api'ı nasıl kullanırım ? etc".

Görüldüğü üzre Api'ı yazan ve Api'ı kullanan kişiler tamamiyle farklı şeylere odaklanıyorlar çünkü farklı bakış açılarıyla Api'ı kullanıyorlar.  Sonuç olarak Api'ı yazan kişiler olarak uygulamayı geliştirirken consume edecek kişilerin yani Api kullanıcılarının bakış açısından bakıp onlar bu APi'ı kullanırken ne gibi sorular sorabilir veya hangi sorunun cevabına en kolay şekilde ulaşabilirler bunu düşünerek hareket etmemiz gerekir.

Peki iyi güzel ama böyle bir Api geliştirmek için neler yapmak gerekir ? Bunun için birçok geliştirici tarafından standart kabul edilen 5 altın kural vardır;

  1. Documentation (Dökümantasyon)
  2. Stability and Consistency (Kararlılık ve Tutarlılık)
  3. Flexibility (Esneklik)
  4. Security (Güvenlik)
  5. User Acceptance (Kullanıcılar tarafından kabul görmesi)

Rule 1: Documentation 

Yazılım geliştiren kişilerin ortak pek sevmediği şeylerden biri sayfalarca analistler tarafından yazılmış dökümanları okumaktır. Sizde sevmiyorsunuz dimi ?.. :)  

İyi bir dökümantasyon Api için olmazsa olmazlardandır ve çoğu zaman Api'ları geliştirren kişiler olarak bu iş biz developer'lara bırakılmıştır. Kullanıcı tarafından düşünelim bir bankada çalışıyorsunuz ve banka radikal bir karar alarak bazı servislerini dışarıya açma kararı aldı ve başladınız Api metodlarını yazmaya. Api'lar prod'a alınıp kullanıma sunulduktan sonra kullanmak isteyen kişilerin ilk başlayacakları yer Api ile ilgili yazılan dökümanları okumaktır.  Yani birinin sizin Api'ınızı kullanmasını istiyorsanız ilk yapmanız geren şey dökümantasyon yazmaktır çünkü kullanıcıların ilk bakacakları şey budur.

Peki iyi bir dökümantasyonu nasıl yazarız ?

Yazılımla projeleri ile ilgili dökümanlarda genelde; hangi sırasıyla metodların çağrılacağı, metod parametreleri, değişken tipleri, request, response class'ları etc. ve dökümantasyon oluşturmak içinde bir çok tool'da bulunmaktadır aynı zamanda kendimizde bunları basit bir template belirleyerek yazabiliriz. 

Ancak harika yazılmış bir dökümanı üstte bahsettiğimiz yaygın olarak kullanılan döküman yazma tekniklerinden ne ayırır diye soracak olursa Api'ın kullanım örnekleri ve iyi bir eğitim içeriği. Bu ikisi Api'ı kullanan kişiler için en önemli iki şeydir diyebiliriz çünkü son kullanıcı için gerçek bir örnek üzerinden ve takıldığı noktaları eğitim içeriğine bakarak en hızlı ve kolay şekilde öğrenecektir. Ancak yazdığımız dökümanın şeması ve yazdığımız örnek kodların karmaşık olmamasına da dikkat etmeliyiz. Döküman yazıyoruz diye kullanıcıya 300 sayfalık bir roman veya 15 class'lık bir örnek projede yazmamalıyız :)

Dökümantasyon işiniz bittikten sonra yapabiliyorsanız yapmış olduğunuz örnek Api kullanım projelerini ve dökümanları çevrenizdeki developer'lara gönderin ve feedback'ler almaya çalışın. Gelen feedback'ler doğrultusunda da yazmış olduğunuz dökümanıdaha user-friend hale getirebilirsiniz.  

Yaptıkları dökümanları harika bulduğum birkaç firma var ki bunlar kendi sektörlerinde en iyiler bile değiller ancak gerçek bir Api dökümanı nasıl olmalı sorusuna verilecek en iyi cevabı vermiş firmalar;

 

Rule 2: Stability and Consistency

Daha önce Facebookûn Api'larını kullandıysanız ne sıklıkla Api'ları değiştirdiklerini bilirsiniz. Sürekli olarak bir güncelleme yayınlıyorlar ve çalışan birçok uygulama da patlıyor çatlıyor. Facebook'un ne kadar başarılı bir ürün olduğuna herkes hemfikirdir ancak başarılı olmalarının sebebi son derece iyi bir Api'a sahip oldukları değil milyarları geçen kullanıcı sayısı sahip olmaları ancak iş Api konusuna gelince developer'lar için bazen hayatı zindan ettikleri oluyor. Muhtemelen sizlerin geliştirdiği Api'lar milyarlarca kullanıcı tarafından kullanılmicak ancak o bakış açısıyla geliştiriyor olmakta fayda var. Yeni sürüm yayınladığınızda eski versiyonlara desteği en azından birkaç yıl daha sürdürüyor olmak gerekir en azından mevcut kullanıcıları en iyi şekilde yeni versiyondan haberdar ederek onların yeni versiyona geçmelerini sağlamak gerekir.

Örnek üzerinden gitmek gerekirse ; farz edelim ki http://www.canertosuner.com/api/getUsers/  şeklinde tanımlı JSON formatında response dönen ve yaklaşık 10 bin kişi  tarafından kullanılan bir Api'ımız var diyelim. Api'ı production'a aldıktan aylar sonra dönen responsları değiştirdiniz ve bu Api'ı kullanan kişileri de etkileyen bir değişiklik olsun. Bu şu demek; 10 bin kişinin geliştirdiği 10 bin uygulamaların patlaması demek. Bunun çözümü tabii ki de uygulamaya yeni versiyon çıkmamak değil bir ürün aldığı update'ler ile daha iyi olur. Bunun çözümü için Api'ınıza basit bir versiyon kontrolü koymak ve eski versiyonlara desteği sürdürmek.( http://www.canertosuner.com/api/getUsers?version=2 yada http://www.canertosuner.com/api/v2/getUsers ) .Böylelikle hem eski versiyonu kullananlar yeni versiyona geçene kadar uygulamalarının çalışmalarını sağlayabilirler hemde Api'ı yeni kullanmaya başlayacak olanlar direkt olarak son versiyondan başlarlar.

Api'ınızın Consistent yani tutarlı olması da son derece önemlidir. Api'ınızın geriye döndüğü response class'larının ortak bir base class içerisinde olmasına özen gösterin. Çünkü bu class sayesinde metodun döndüğü response dışında Api kullanıcısına farklı verilerde döndürebilirsiniz. Daha önce kullanmış olduğum bazı Api'larda tutarlılık neredeyse hiç yoktu. Sürekli olarak yeni gelen versiyonlarda parametre isimleri değişiyor, metod isimleri değişebiliyor, bir önceki metodda UserID'yi int gönderirken bir sonraki metodda string olarak istiyorlardı, hata mesajları sürekli olarak farklı formatta geliyordu vs. gibi örneklerle karşılaşmak mümkün. Bu gibi durumlara dikkat etmek gerekiyor. Ortak kullanılan parametreler tek bir yerden yönetilebiliyor olmalı ve major değişiklikler kolay kolay uygulanmamalı veya uygulandığında Api'ı kullanan kişiyi etkilenmesinden kaçınılmalı.

 

Son olarak yayınlamış olduğunuz yeni versiyonlarda iyi bir changelog yayınlayıp mevcut kullanıcılarınızı bilgilendirmeniz gerekir. Böylece kullanıcılar nasıl upgrade edeceklerini öğrenmiş olurlar ve Api uygulamanızda çok büyük major değişikliklere gitmemeye özen gösterin veya bunu kullanıcıları en az seviyede etkileyecek şekilde yapın ve tutarlılığı korumaya çalışın.

 

Rule 3: Flexibility

Yazmış olduğumuz Api'ların flexible yani esnek olması oldukça öçnemli bir konudur.Peki ama esnek derken neyi kastediyorum ?

Garbage in, garbage out (GIGO) programcılar tarafından iyi bilinen bir technical-term dür. Kısaca "input olarak ne verirsen output'un ona göre değişiklik gösterir" veya ne ekersen onu biçersin". Hadi biraz daha basit söyleyelim programa yanlış bir request'te bulunursanız response'unuzda yanlış olacaktır. GIGO yu örnek olarak vermemin sebebi aslında Web Api tarafında request response ilişkisine dikkat çekmek istemem. 

Günümüzde bir çok Api doğru düzgün bir JSON desteği bile bulunmamakta ancak iyi bir Api'ın sadece JSON değil bunu yanında YAML, XML vs. gibi formatlar içinde desteği bulunmalıdır ve bunlardan hangisini return edeceği bilgisini de aslında Api kullanıcılardan yani client'lardan parametre olarak almalıdır. Örnek olarak http://www.canertosuner.com/api/getUsers?Format=JSON yada bu bilgiyi header'a parametre olarak eklemek aslında en doğru kullanımdır diyebiliriz Accept: application/json .

Bu kullanım sadece kullanıcıya dönecek olan response için değil aynı zamanda kullanıcının size göndereceği Post metodlarında body de bulunan request parametreleri içinde geçerlidir. Bir kulalnıcı JSON olarak gönderirken diğer bir kullanıcı XML olarak göndermek isteyebilir ve böyle bir esnekliğe sahip olabilmek oldukça önemli bir ayrıcalıktır.

Api'ınızın OData özelliğine sahip olması da bir o kadar önemlidir. OData ile Api'nızın döneceği respons'lara kullanıcılar tarafından filtreleme yapma şansı sunarsınız ve böylece kullanıcının istemediği bir veriyi dönmemiş olursunuz. Örneğin A kullanıcısı GetProducts metodundan sadece ProductID'lerini almak isterken B kullanıcısı ProductName ve ProductID alanlarını almak isteyebilir. OData bu gibi durumlar için son derece filexible(esnek) bir kullanım sunar.

 

Rule 4: Security

Güvenlik bir WebService & WebApi projesindeki en önemli noktalardan biri ancak öyle Api'lar var ki otantike olmak nerdeyse bazen imkansız oluyor. Tamam güvenlik önemli ama öyle katı ve karmaşık kurallar koyarak Api yazan firmalar var ki bazen birbiri ile ilişkili Api' metodları için 2-3 çeşit güvenlik algoritması kullandığımız olabiliyor ve ortada dökümanda yoksa nasıl requestte bulunacaksın, nasıl response alacaksın hepsi belirsiz. Bir Api developer olarak Api'ı kullanan kişiler için nasıl authenticate ve authorize olunur bilgilerini içeren kolay ve açıklayıcı örneklerle anlatıyor olmamız gerekir. 

Günümüz Web Api'larının çoğunda ortak olarak token-based authentication kullanılmakta. Kısaca; kullanıcı yapmış olduğu ilk requestte Api'a bazı güvenlik parametreleri geçer ve Api'da ona random generate edilmiş olan token bilgisini döner ve client bütün session'ı boyunca aynı token üzerinden request atıp response almaya devam eder. 

Token dışında OAuth 2 + SSL de Api'ınızı güvenli hale getirmek için yapılabilecek diğer seçenekler arasındadır. SSL her halükarda kullanıyor olmamız gerekir bunun yanında OAuth 2'yi de server-side'da uygulamakta oldukça basittir ve  OAuth 2'nin hemen hemen bütün yaygın kullanılan programlama dilleri için kütüphaneleri de bulunmaktadır.

Bunlarla birlikte güvenlik için dikkat edilmesi gereken birkaç konu dha bulunmaktadır;

  • Api'larda genellikle update, insert, delete gibi işlemler yapılabilmektedir. Bu işlemleri yapan metodları public hale getirip bütün kullanıcıların kullanımına sunarken dikkat edilmesi gerekmektedir. Her kullanıcının şöyle bir Api çağrısı yapamıyor olması gerekir "/user/delete/<id>" . Bununla ilgili her bir kullanıcı için Whitelisting Functionality bilgisi oluşturup bu listede belirtilen kurallara göre request validation işlemleri yapılabilir. 
  • OData kullanırken de select işlemleri yapılırken kullanıcının sahip olmasını istemediğimiz bilgiler için request'te bulunduğunda 406 Not Acceptable Response şeklinde status kodlar dönebiliriz. 
  • Cross-Site Request Forgery (CSRF) isteklerine akrşı Api'ı korumalıyız. Session veya cookie bazlı bir authentication yapımız var ise Api'ımızı CSRF ataklarına karşı koruduğumuzdan emin olmalıyız. Bununla ilgili faydalı dökümanları The Open Web Application Security Project (OWASP) da bulabilirsiniz. 
  • Resource dosyalarına olan erişimi yönetiyor olmamız gerekir. Örnek olarak bir banka kredi kartlarını tutan Api yazdınız ve Api içerisindeki bir dosyada kredi kartı görselleri bulunmakta. Herhangi bir kullanıcı gidipte  /account/card/view/152423 şeklinde bir istekte bulunup erişmemesi gereken bir resource dosyasına erişmemelidir.
  • Bütün request'leri validate ediyor olmamız gerekir. Her bir request'in kendine özgü parametreleri bulunur ve kullanıcı sürekli olarak saçma sapan request'lerde bulunduğunda örnek olarak; userId miz 4608 olsun ve user bilglsini getiren metoda 4608 den başlayıp random sayılar üreterek ( /account/getUserInfo/4609) başka kullanıcıların bilgileri için request'te bulunduklarında Api'ımızın bir şekilde bir güvenlik kontrolü olup bu kullanıcıyı tespit edip engellemelidir.

 

Rule 5: User Acceptance

Beşinci ve son kural aslında bu beşli arasındaki en önemli kural diyebiliriz. Bu kural ilk 4 kural boyunca bahsettiğim bütün şeylerin aslında bir ürün olarak hazırlanıp kullanıcıya sunulması ve kabul görmesini içermekte. Örneğin iyi bir dökümantasyon, kolay entegre edilebilir güvenlik adımları, yazdığımız Api'metodlarını tutorial'larla süsleyip max 15 dakikada kullanıcılar tarafından entegre edebilmelerini sağlama gibi durumlar diyebiliriz.

Api'ın kullanıcılar tarafından daha kolay kabul görmesini sağlayacak bazı spesific tavsiyeler saymak gerekirse;  

  • Kullanıcıların dökümantasyonda belirtilenlere göre Api'ınızı ilk denemeden sonra entegre edebildiğinden emin olun.
  • KISS - Keep it Simple Stupid anlayışına göre hareket etmeye çalışın. Fantastik authentication yöntemlerinden kaçının, SOAP, JSON, REST'i yeniden yaratmaya çalışmayın veya bütün platformlar tarafından kabul görmemiş herhangi bir yöntem kullanmayın.
  • Service arayüzü için dil spesifik library'i desteği vermeye çalışın. Otomatik olarak bu işlemi yapan güzel tool'lar da mevcut (Alpaca veya Apache Thrift gibi).
  • Gereken sign-up işlemlerini en basit şekilde yapın. Api'ınız bir open-source proje değilse ve sign-up & register gibi işlemler varsa bunları olabildiğince basite indirgeyip biran önce kullanıcıları yormadan api'ınınzın bulunduğu yere yönlendirin. Eğer sizin için extra bir maliyeti yoksa sign-up with social media account (facebook, google, twitter etc) özelliği sunabilirsiniz. 
  • "Satış sonrası destek" sağlayın. Bug'sız uygulama olmaz ve Api'ı kullanan kişiler bug veya herhangi bir sorun ile karşılaştıklarında en kısa ve kolay şekilde bunları size iletmesini sağlayın. Bu bir forum sitesi veya e-mail sistemi olabilir size kolayca raporlayabilsinler. Sorunu giderip yeni versiyon çıktıktan sonrada ilgili kişileri tekrardan bilgilendirmeyi ihmal etmeyin.

 

Özetle..

Iot (Internet of Things) ile birlikte hayatımızda kullanacağımız IP alıp internete çıkabilen bütün teknolojik ürünler WebService & Api ile haberleşiyor olacaklar ve beraberinde birçok yeni Api'lar yazılmasına ihtiyaç duyulacak. Bugünün verilerine göre düşünürsek; binlerce api & web service var ancak sıkıntılı olan şey kullanımları gerçekten kolay değil. Sebep olarak SOLID prensiplerine pek uymayan yazılım anlamında zayıf tasarımlı projeler olması, dökümantasyon tarafının yeterli düzeyde veya yeteri kadar kolay anlaşılabilir olmaması, örnek kod parçacıkları içermemesi, raporlanmasına rağmen çözülmeyen bug'lar etc.. bir sürü sıralayabiliriz.

Yukarıda belirtilen 5 altın kural bir çok kişi tarafından aslında ortak dile getirilen başlıklar ve Api projesi geliştirirken uyulması gereken kurallar olarak belirtilmekte. Eğer bizlerde bundan sonra yazacağımız Api'larda birazcıkta olsa bu kurallara uygulayarak hareket edersek her şey hem Api geliştiriciler hemde Api kullanan kişiler için daha kolay olacaktır.