Caner Tosuner

Leave your code better than you found it

Asp.Net Core Response Caching

Daha önceki asp.net core yazılarında kütüphane ile birlikte default olarak tıpkı bir feature gibi hazır gelen ve biz developer'lar için sadece bu feature enable/disable etmek gibi ufak birkaç konfigurasyonla implementasyon tamamlayabileceğimiz bir çok özelliğin olduğundan bahsetmiştik.

Response Caching de bu feature'lardan bir tanesidir ve Aspect Oriented yaklaşımına uygun olarak geliştirilmiş bir ResponseCaching Middleware'i framework ile birlikte default gelmektedir. Asp.net core projelerinde çok küçük birkaç extension-method call ederek response caching özelliğini projemize kazandırabiliriz. Default olarak memory-cache yapsada istendiğimiz herhangi bir third party cache-server da kullanabiliriz.

Örnek bir proje ile devam edelim, ilk olarak Visual Studio'da ResponseCachingSample adında bir empty api projesi oluşturalım.

Sonrasında Startup.cs içerisinde yer alan ConfigureServices metodu içerisinde projemiz serviclerine responseCaching Middleware'ini ekleyelim.

public void ConfigureServices(IServiceCollection services)
{
     //add responseCaching service
    services.AddResponseCaching();

    services.AddMvc();
}

ResponseCaching için geçerli 3 options bulunmakta. Bunlar;

  1. SizeLimit : Maximum size of the response cache. Default olarak 100 MB dır.
  2. UseCaseSensitivePaths : Cache de bulunan path'ler case sensitive path olup olmamasını belirleyen option.
  3. MaximumBodySize : Cache'lenecek response body'ler için geçerli maximum size. Default olarak 64 MB dır.

Dilersek bu özellikleri kullanarak da responseCache'i aşağıdaki gibi service'lere ekleyebiliriz.

public void ConfigureServices(IServiceCollection services)
{
    services.AddResponseCaching(options =>
     {
          options.UseCaseSensitivePaths = true;
          options.MaximumBodySize = 1024;
     });
    services.AddMvc();
}

Service olarak eklediğimiz bu özelliği uygulamamızda kullanabilmek içinde yine Startup.cs içerisinde yer alan Configure metodu içerisinde UseResponseCaching extension metodunu call etmemiz gerekmekte.

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

Gerekli konfigurasyonları yaptıktan sonra artık controller metotlarında [ResponseCache] attribute'ünü kullanarak metodun döndüğü response'u cache'e atabiliriz.

ResponseCache attribute'üne ait parametrelere bakacak olursak;

  • Duration : Saniye cinsinden response'un ne kadar süre cache'de tutulacağını belirttiğimiz property.
  • Location : Response'un nerede cache'leneceğini belirttiğimiz parametre. Any, Client, or None. Default olarak Any set edilmiştir.
  • NoStore : Cache data sı store edilip edilmeyeceği bilgisinin sete dildiği parametre.
  • CacheProfileName : Adından da anlaşıalcağı üzre cache profil ismi
  • VaryByHeader : Response header da bulunan Vary key'ine ait value değerini temsil eder.
  • VaryByQueryKeys : Query string parametresine göre hangi response'un cache'leneceği belirtmek için kullanılır. Örnek olarak ; VaryByQueryKeys = new string[] { "clientName" } query string de bulunan farklı "clientName" parametrelerine göre cache'lenecektir.

ResponseCache attribute'ünü aşağıdaki gibi ValuesController içerisinde bulunan Get metodu için kullanalım.

[Route("api/[controller]")] 
public class ValuesController : Controller
{
    [HttpGet]
    [ResponseCache(Duration = 30)]
    public IEnumerable<string> Get()
    {
        var time= "The response time is : " + DateTime.Now.ToString();

        return new string[] { "CachedItems", time};
    }
}

Yukarıda responseCache attribute'ünü kullanarak Get metodunun return ettiği response'u 30 sn exprie süresi olacak şekilde cache'e atılacağını belirttik. Uygulama çalıştıktan sonra Get metodundan başarılı dönen ilk response CacheMiddleware'ine düşecek ve 30 saniye boyunca response'u cache'de tutacak. Bu 30 sn içerisinde gelen bütün request'lere ait response'lar hiç Get metoduna düşmeden doğrudan middleware tarafından yönetilip cache'den return edilecektir.

Tabiki şunuda unutmamak gerek; Middleware sadece Http200 result'ları için response'u cache'lemekte. 

Caching doğru kullanıldığı taktirde büyük çapta projeler için oldukça hayat kurtaran özelliktir. Özellikle response'un çok sık değişmeyip request'in çok fazla geldiği endpoint'ler için kullanmak core uygulamanızı ve onun bulunduğu storage'ı sürekli meşgul etmemek adına projelerde oldukça yaygın kullanılmaktadır.

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.

 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    public interface IUserRepository : IRepository<User>
    { }

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

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

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

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

    public interface IApiService
    {   }

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

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

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

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

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

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

    public class UserController : ApiController
    {
        private readonly IUserService _userService;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        }
    }

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

    public class ApiControllerActivator : IHttpControllerActivator
    {
        private readonly IWindsorContainer _container;

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

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

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

            return controller;
        }

        private class Release : IDisposable
        {
            private readonly Action _release;

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

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

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

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

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

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

Castle Windsor ile Exception Handling Interceptor (Dynamic Proxy)

Daha önceki IoC ve AOP yazılarında  çeşitli konulara değinerek örnek projeler üzerinde anlatımlarda bulunduk. Bu yazımızda da Castle Windsor'dan yararlanarak projelerimizde sıklıkla kullanacağımız bir ExceptionAspect veya Interceptor oluşturacağız. 

Interceptor'lar veya Dynamic Proxies Aspect Oriented Programming'in bir implementasyonudur. Bu bize oluşturduğu proxy ile metodu intercept ederek kendi kodlarımızın arasına inject etmemizi sağlar.

IL kodlarına baktığımızda aşağıdaki resimde olduğu gibi geliştirmiş olduğumuz kodları try catch finally blokları arasına alınır.

 

 

 

 

 

 

 

Daha önceki Castle Windsor Kullanarak Cache Interceptor Oluşturma yazısındaki örneğimiz üzerinden ilerleyelim. O yazıda il kodu alarak geriye ilçeleri dönen bir case üzerinden caching işlemi yapan aspect geliştirmiştik. Bu yazımızda da aynı örnek üzerinden ilerleyerek projemiz için bir ExceptionHandling aspect oluşturalım. 

İlk olarak ExceptionHandlingInterceptor adında aspect'imizi oluşturalım.

public class ExceptionHandlingInterceptor : IInterceptor
{
    public void Intercept(IInvocation invocation)
    {
        try
        {
            invocation.Proceed();      
        }
        catch (Exception ex)
        {
            LogManager.Log("Exception in : " + invocation.Method.Name + " method." + ex);

            invocation.ReturnValue = new BaseResponse { IsSuccess = false };
        }
    }
}

Yukarıdaki kodu incelediğimizde özetle şunu söylüyor ;

  • Invoke edilecek metodu try-catch-finally bloğu arasına al
  • Invoke edilen metot içerisinde hata alırsan catch bloğuna gir ve önce alınan hatayı log'a yaz
  • Sonrasında BaseResponse modelini initialize ederek client'a clear bir response model dön.

Tabi ki de çok daha farklı ayrıntıları log'a yazmanız gerekir ancak sample olduğundan şimdilik bunları yazalım.

İkinci adım olarak yazmış olduğumuz Exception aspect'ini container'a register etmek var.

Castle Windsor Kullanarak Cache Interceptor Oluşturma örneğinde ServiceInstaller class'ını olduğu gibi alıp ExceptionHandlingInterceptor'ını register işlemimizi yapalım.

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

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

Yukarıda ki register işlemi şunu söylüyor; "ILocationService interface'inin implementasyonu LocationService class'ı dır. Bu implementasyona ait 2 adet Interceptor var "CacheInterceptor" ve "ExceptionHandlingInterceptor" adında ve bu interceptor'lar LifeStyleSingleton olarak container'a register edilmişlerdir."

Uygulamamız hazır diyebiliriz. LocationService içerisinde bulunan GetDistrictsByCityCode adlı metodumuzda test etmek için aşağıdaki gibi kendimiz bir exception throw edelim.

public class LocationService : ILocationService
{
    public GetDistrictsByCityCodeResponse GetDistrictsByCityCode(int cityCode)
    {
        throw new NullReferenceException();
    }
}

Projemizi run edip ilgili endpoint'e istekte bulunduğumuzda exception fırlatılıp Interceptor içerisinde bulunan catch bloğuna düşecektir ve loglama işlemini yaptıktan sonra client'a BaseResponse modelini dönecektir.

 

Peki bunları yaparak ne sağladık ? 

  1. Exception Handling işini eski usül projede yüzlerce try-catch bloğu oluşturmak yerine AOP'in faydalarından yararlanarak tek bir yerden yönetebilir duruma getirdik.
  2. Çok daha reusable olan ve başka yerlerde de kullanabileceğimiz bir yapı tasarladık.
  3. Uygulama exception fırlattığında client'a saçma sapan StackTrace mesajı yerine her response da aldığı BaseResponse modelini dönerek response'larımızı daha tutarlı bir hale getirdik.

Postsharp Kullanarak Metot Execution Sürelerini Hesaplama

Daha önceki aspect oriented yazılarımızda ne aspect oriented'ın ne olduğunu ve nasıl kullanıldığını çeşitli örneklerle anlatmıştık ve AOP yaklaşımı ile yapılabileceklerin sınırının olmadığından bahsetmiştik. Bu yazımızda da özellikle server-side geliştirme yapanların sıkça ihtiyaç duyduğu metot execution süresinin AOP yaklaşımı ile nasıl yapıldığından bahsedeceğiz. 

Yazılımda performans bildiğiniz üzre en önemli konuların başında gelir ve büyük çapta 1 den fazla client'ın consume ettiği bir projeniz var ise metot execution sürelerini bir yerlerde logluyor olmak bizler için şu gibi durumlarda hayat kurtarıcı bir rol üstlenir; client der ki "Abi sayfa 8 saniyede açılıyor bizde bir sıkıntı yok service'den geç geliyor....vs.", server-side'da geliştirme yapan arkadaşta der ki "Yok abi ben 0.2 saniyede response dönmüşüm bu bizden değil...vs.". Bu gibi durumlarda eğer siz service olarak projenizden eminseniz bu durumu kanıtlamak için en güzel çözüm metot execution sürelerini hesaplayıp delil olarak ilgili mercilere sunmak. Ya da bu case'i de geçelim bizler developerlar olarak kendimize "arkadaş bi proje yaptık ama işler nasıl gidiyor, yazılımsal olarak performansı yerinde mi.." vs. gibi soruları sorup cevaplarını da biliyor olmamız gerekir.

Bunun için AOP çözümlerinden biri olan Postsharp'dan yararlanacağız. Case şu şekilde olacak; Bir Api projemiz olsun ve MoneyTransfer adında eft-havale-virman işlemlerini yapan end-point geliştirelim ve bu end-point'e gelen request-response'ların execution sürelerini LoggingAspect kullanarak loglayalım.

*PostSharp kurulumu vs bilgileri için şu yazıyı inceleyebilirsiniz.

    [Serializable]
    public class LoggingAspect : OnMethodBoundaryAspect
    {
        private readonly Stopwatch timer = new Stopwatch();  

        public override void OnEntry(MethodExecutionArgs args)
        {
            timer.Start();
        }

        public override void OnExit(MethodExecutionArgs args)
        {
           var executionTime = timer.ElapsedMilliseconds;
            timer.Stop();
            var logMessage = "Method : " args.Method.Name +  " Execution Time : " + executionTime + " Millisecond";
            Console.Write(logMessage );
        }
    }

Sırada MoneyTransfer işlemi yapan metodu yazalım ve bu metot parametre olarak TransferRequest objesi alsın ve response olarak TransferResponse adında bir model dönsün.

        [LoggingAspect]
        public TransferResponse MoneyTransfer(TransferRequest request)
        {
            // back-end kodlarınız

            var resp = new TransferResponse
            {
                IsSuccess = true,
                Message = "Transfer İşleminiz Gerçekleşti."
            };
            return resp;
        }

        public class TransferRequest
        {
            public decimal Amount { get; set; }
            public string SenderIBAN { get; set; }
            public string ReceiverIBAN { get; set; }
        }

        public class TransferResponse
        {
            public bool IsSuccess { get; set; }
            public string Message { get; set; }
        }
    }

Hepsi bu kadar artık yazdığımız kodları test edebiliriz. TransferMoney metoduna request yolladığımızda metot scope'ları içerisindeki kodları run etmeden aspect'imiz içerisinde bulunan OnEntry metoduna girecektir ve burada timer'ı start ediyoruz. Sonrasında metot scope'ları içerisinde bulunan kodları çalıştırıp metottan çıktıktan sonra OnExit metoduna girip timer'ı stop edip devamında execution süresini millisecond cinsinden Console'a yazacaktır.

Metot execution süresi yukarıda da belirttiğim gibi önemli bir konudur ve ne nerde ne kadar süre harcayıp client'a gidiyor bunu ölçeklendirebiliyor olmak gerekir.

Not: Çok uzun süren execution sürelerinin çok büyük bir ihtimalle networksel sorunlardan dolayı kaynaklanabileceği ihtimalini de unutmamak gerekir.

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.

Castle Windsor Kullanarak Logging Interceptor Oluşturma

Daha önceki Aspect Oriented yazılarında interceptor'lardan bahsetmiştik ve biz developer'lar için bulunmaz bir nimet olduğunu söylemiştik. Server-Side bir projede olmazsa olmaz özelliklerin başında gelen Logging, Exception Handling, Caching vs gibi özellikleri çok basit küçük interceptor'lar yazarak uygulamamıza bu özellikleri kazandırabiliriz. Daha önceki yazılarımızda bu ihtiyaçları karşılayabilmek için .Net tarafında oldukça entegrasyonu basit olan Postsharp kütüphanesinden faydalanmıştık. Bugün ki yazımızda ise IoC container'lardan Castle Windsor'ı kullanarak uygulamamız için bir Logging intercepter'ı geliştireceğiz. 

Örneğimiz şu şekilde; Email göndermek için kullanılan basit bir Service projemiz olsun ve parametre olarak Address ve html olarak Content alsın. İlk olarak VS de EmailSender adında bir Api projesi oluşturalım ve projemizin referanslarına Nuget üzerinden Castle Windsor'ın paketlerini indirip kuralım.

Sonrasında SendEmailRequest adında request modelimizi tanımlayalım

    public class SendEmailRequest
    {
        public string Address { get; set; }
        public string Content { get; set; }
    }

Email gönderme işlemini yapacak olan service interface'imiz ve onun impl. class'ını aşağıdaki gibi oluşturalım

    public interface IEmailService
    {
        bool Send(SendEmailRequest reqModel);
    }
    public class EmailService : IEmailService
    {
        public bool Send(SendEmailRequest reqModel)
        {
            //todo return true dedik ancak bu kısımda email göndermek için kullandığınız kodları yazmalıyız. 
            return true;
        }
    }

Email gönderme servisimiz artık kullanıma hazır. Artık bu service'i Api ile dışarıya açma zamanı. Projemize CommunicationController adında bir controller ve bu controller içerisinde HttpPost kabul eden SendEmail adında bir endpoint tanımlayalım.

    public class CommunicationController : ApiController
    {
        private readonly IEmailService _emailService;

        public CommunicationController(IEmailService emailService)
        {
            _emailService = emailService;
        }

        [HttpPost]
        public HttpResponseMessage SendEmail(SendEmailRequest reqModel)
        {
            var result = _emailService.Send(reqModel);
            if (!result)
            {
                return Request.CreateResponse(HttpStatusCode.NotFound);
            }
            return Request.CreateResponse(result);
        }
    }

Yukarıda görüldüğü üzre kullanacağımız IEmailService'ini Castle kullanarak EmailService'ine inject edeceğiz ve controller seviyesinde bu service interface'ini kullanarak email gönderme işlemini yapacağız.

Şimdi sırada LoggingInterceptor'ımızı oluşturma var. Bu interceptor ile log kayıtlarına MethodName, request ise aldığı parametreler ve response için return edilen değer bilgilerini atacağız. 

    public class LoggingInterceptor : IInterceptor
    {
        public void Intercept(IInvocation invocation)
        {
            var serializer = new JavaScriptSerializer();
            var parametersJson = serializer.Serialize(invocation.Arguments);

            System.Diagnostics.Debug.WriteLine("Request of " + invocation.Method.Name + " is " + parametersJson);

            invocation.Proceed();

            var returnValueJson = serializer.Serialize(invocation.ReturnValue);

            System.Diagnostics.Debug.WriteLine("Response of " + invocation.Method.Name + " is: " + invocation.ReturnValue);
        }
    }

Logları şimdilik sadece Output Window'a yazdırdım ancak gerçek hayatta tabikide NLog vs gibi bir kütüphane kullanıyor olmamızda fayda var. 

Interceptor'ımızda hazır olduğuna göre artık IoC Initialize tarafına geçip gerekli register işlemlerimizi yapabiliriz. ServiceInstaller adında aşağıdaki gibi bir class oluşturalım ve içerisinde service'imizi ve interceptor'ımızı register edelim.

    public class ServiceInstaller : IWindsorInstaller
    {
        public void Install(IWindsorContainer container, IConfigurationStore store)
        {
            container.Register(Component.For(typeof(IEmailService))
                     .ImplementedBy(typeof(EmailService))
                     .Interceptors(typeof(LoggingInterceptor)));
        }
    }

EmailSender servisini ve interceptor'ı register eden installer'ı tanımladık. Şimdi ise Web Api projemizin controller'ını register eden installer'ı oluşturalım.

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

Son adım olarak ise oluşturduğumuz bu installer'ları container'a Install edeceğiz. Bunun için projemizde bulunan Global.asax içerisinde bulunan Application_Start metodu aşağıdaki gibi olacak.

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

Hepsi bu kadardı :) 

Şimdi yazdığımız kodları test edelim. Postman kullanarak controller'da bulunan endpoint'e aşağıdaki gibi bir istek gönderip Output Window dan neler yazdırdığına bir bakalım.

 

Request sonrasında Interceptor araya girerek loglama işlemini aşağıdaki görselde olduğu gibi yapmakta.

Örneğimiz şimdilik burada bitiyor, sizlerde projeleriniz için bir çile haline gelebilme potansiyeli olan Loglama konusunu interceptor kullanarak son derece basit ve reusable hale getirebilirsiniz.

Sonraki yazılarımızda Interceptor kullanarak daha başka neler yapabiliriz fırsat buldukça inceleyeceğiz.

Caching With Postsharp

Cache bir proje için olmazsa olmazlardan biridir diyebiliriz ve ihtiyaç duyulduğunda çok can kurtarır. Öyleki çok fazla değişmesini düşünmediğiniz verileri cache de saklayarak reqeust response time konusunda projenize oldukça fazla performans kazandırabilirsiniz. Daha önceki web api için cache ve aspect oriented - postsharp konularından bahsetmiştik. WCF Service projenize Postsharp ile basit bir server-side cache yapısı kurabilirsin.

İlk adım olarak cache de tutacağımız CacheModel adlı model'i tanımlayalım.

public class CacheModel
{
    public DateTime CacheTimeOutInfo { get; set; }//cache'in ne zaman timeout olacağını belirten time bilgisi
    public object CacheData { get; set; }//cache'de tutulacak object
}

Şimdi ise CacheManager'a implement edeceğimiz ICache interface'i tanımlayalım ve içerisine cache metotlarımızı yazalım.

public interface ICache
{
   bool Contains(string key);//key varmı yokmu diye control ettiğimiz metot
   void Add(string key, CacheModel cacheData);//cache key'i ile birlikte cache model'i alıp cache'e ekleyen metot
   object Get(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
   void RemoveAll();//bütün cache'i silen metot
}

Aşağıda yazacağımız class'ta ise üstte yazdığımız ICache'i implement eden cache yönetimini yapacağımız CacheManager class'ını oluşturacağızz.

    public class CacheManager : ICache
    {
        public bool Contains(string key)
        {
            var isExist = HttpRuntime.Cache[key];
            if (isExist != null)
            {
                var obj = GetCacheModel(key);
                if (obj.CacheTimeOutInfo > DateTime.Now)
                    return true;
                Remove(key);
            }
            return false;
        }

        public void Add(string key, CacheModel cacheData)
        {
            if (Contains(key))
                Remove(key);
            HttpRuntime.Cache[key] = cacheData;
        }

        public object Get(string key)
        {
            var obj = GetCacheModel(key);
            return obj.CacheData;
        }

        private CacheModel GetCacheModel(string key)
        {
            return (CacheModel)HttpRuntime.Cache[key];
        }

        public void Remove(string key)
        {
            HttpRuntime.Cache.Remove(key);
        }

        public void RemoveAll()
        {
            var enumerator = HttpRuntime.Cache.GetEnumerator();
            while (enumerator.MoveNext())
            {
                HttpContext.Current.Cache.Remove((string)enumerator.Key);
            }
        }
    }

Sırada Postsharp kullanarak oluşturacağmız CacheAspect adında interceptor'ı yazama var. Bunun için nuget'ten postsharp'ı indiriyoruz ve aşağıdaki gibi CacheAspect adındaki interceptor'ımızı oluşturuyoruz.

    [Serializable]
    public class CacheAspect : OnMethodBoundaryAspect
    {
        //Metottan alacağımız cache süresi
        public int MethodCacheDurationInMinute = 0;

        public override void OnEntry(MethodExecutionArgs args)
        {
                 //cache key oluşturma
                var cacheKey = CreateMD5(GenerateCacheKey(args));

                var cacheManager = new CacheManager();

                var isCacheKeyExist = cacheManager.Contains(cacheKey);

                if (isCacheKeyExist)
                { 
                    //eğer request geldiğinde cache key cache de varsa client'a cahe de bulunan veriyi dön
                    args.ReturnValue = cacheManager.Get(cacheKey);
                    args.FlowBehavior = FlowBehavior.Return;
                }
                else
                {
                    //cache de yoksa normal akışta ilerlemesine devam eder
                    args.MethodExecutionTag = cacheKey;
                }
        }

        public override void OnSuccess(MethodExecutionArgs args)
        {
                //başarılı bir şekilde metottan çıktıysa cache'e at

                    var cacheKey = (string)args.MethodExecutionTag;

                    var cacheManager = new CacheManager();
                    var cacheModel = new CacheModel
                    {
                        CacheData = args.ReturnValue,
                        CacheTimeOutInfo = DateTime.Now.AddMinutes(MethodCacheDurationInMinute)
                    };
                    cacheManager.Add(cacheKey, cacheModel);
        }

        private static string GenerateCacheKey(MethodExecutionArgs args)
        {
            var generatedKey = string.Empty;

            if (args.Method.DeclaringType != null)
                generatedKey = args.Method.DeclaringType.Name + "-" + args.Method.Name + (args.Arguments.Any() ? "-" + args.Arguments.Aggregate((first, second) => first + "-" + second) : "");
            generatedKey += SerializeObjectToJson(args.Arguments);

            return generatedKey;
        }

        public static string CreateMD5(string input)
        {
            using (var md5 = System.Security.Cryptography.MD5.Create())
            {
                var inputBytes = Encoding.ASCII.GetBytes(input);
                var hashBytes = md5.ComputeHash(inputBytes);

                var sb = new StringBuilder();
                foreach (var t in hashBytes)
                {
                    sb.Append(t.ToString("X2"));
                }
                return sb.ToString();
            }
        }
    }

Artık sıra cache'i projemizde kullanmaya geldi. yazdığımız interceptor'ı projede hangi metodu cache'e atmak istiyorsak metodun başına attribute olarak ekleyip kaç dakikalık cache'de tutacağını belirteceğiz.

        [CacheAspect(MethodCacheDurationInMinute = 120)] //120 dakikalığına cachelemesini söylüyoruz
        public List<FooModel> Foo(FooRequest reqModel)
        {
            var fooManager=new FooManager();
            return fooManager.GetFooList();
        }

 

Kullanım olarak oldukç basit ve projede hangi metotta istersek aynı yukarıda olduğu gibi attribute olarak projemize ekleyebiliriz. Yazının başında da belirttiğim gibi cache bir çok zaman hayat kurtarır ve yabana atılmaması gereken bir konudur.

 

PostSharp Kullanarak Metot Parametre Null Kontrolü

Daha önceki Aspect Oriented Programming'den ve Postsharp tool'undan bahsetmiştik. Postsharp ile Log, Exception Handling ve metot bazlı Authentication kontrolü gibi işlemleri yapabildiğimizden bahsetmiştik. Bu yazımızda ise Postsharp kullanarak metot bazlı parametre kontrolü (null mı değil i vs. gibi) nasıl yapabiliriz inceleyeceğiz.

Server side geliştirmesi yapmış arkadaşlar bilirlerki metotlarda gelen objeler için validation kontrolü yapmak bazen bizi çok yorabilir veya tahmin ettiğimizden daha fazla zaman aldırabilir. Postsharp ın OnEntry() metodunu kullarak gelen parametler için null mı değil mi diye kolayca kontrol edebiliriz.

Bunun için ilk olarak NullParamsControllingAspect adında OnMethodBoundaryAspect ten inherit olan class'ı oluşturalım ve içeriği aşağıdaki gibi olacaktır. Metoda yollanan null parametleri tespit edip string message olarak client'a ArgumentException fıraltarak bilgilendirme mesajını dönecektir.

[Serializable]
    public class NullParameterControllerAspect: OnMethodBoundaryAspect
    {
        public override void OnEntry(MethodExecutionArgs args)
        {
            var messages = new StringBuilder();

            for (int i = 0; i < args.Arguments.Count; i++)
            {
                if (args.Arguments.GetArgument(i) == null)
                {
                    messages.AppendFormat(args.Method.Name +" metodunda bulunan \"{0}\" parametresi null olamaz.\n", args.Method.GetParameters()[i].Name);
                }
            }

            Console.WriteLine(messages);

            if (messages.Length > 0)
            {
                throw new ArgumentException(messages.ToString());
            }
        }

Yukarıda görüldüğü gibi OnEntry() metodunda gelen parametreyi alıp null mı diye kontrol ediyoruz.

Test için aşağıdaki gibi Prgram.cs class'ını oluşturalım. TransferRequest adında bir class'ımız olsun ve bu class'ı null set ederek ve MoneyTransfer metodunu çağıralım.

 class Program
    {
        static void Main(string[] args)
        {
            TransferRequest req_null = null;
            MoneyTransfer(req_null, "Test");//null obje kullanarak metot çağırımı
        }

        [NullParameterControllerAspect]
        public static void MoneyTransfer(TransferRequest request, string param)
        {
            //transfer işlemleri
        }

        public class TransferRequest
        {
            public decimal Amount { get; set; }
            public string SenderIBAN { get; set; }
            public string ReceiverIBAN { get; set; }
        }
    }

 Projenizi çalıştırdığınızda ekran çıktısı aşağıdaki gibi olacaktır.