Asp.​Net Core Unit Test Nedir Nasıl Yazılır

Daha önceki unit test yazılarında .net framework uygulamaları için çeşitli kütüphaneler kullanarak unit test nedir ne şekilde neden yazılır gibi konulara değinmiştik. Tekrar kısaca tanımlamak gerekirse; unit test'i yazmış olduğunuz kodun her bir birimi için testler yazarak o kodu veya business'ı test etmek olarak düşünebiliriz. Diğer bir değişle; yazmış olduğumuz sınıf veya metotlara real-case'de gelebilecek olan parametreleri geçerek doğru bir şekilde çalışıp çalışmadığını kontrol etmektir diyebiliriz. Bu yazıda ise asp.net core uygulamalarında unit test yazmak için neler yapmak gerekiyor örnek bir proje üzerinden inceleyeceğiz.

Çok da uzak olmayan bir süre önce .net core 2.1 release oldu ve 2.0'dan sonra gücüne güç katarak emin adımlarla ilerleyeceğini sunduğu benchmark sonuçları ile bizlere göstermiş oldu desek yalan olmaz. Tabi geliştirmiş olduğumuz bu core uygulamalarına test yazmadan olmaz. Asp.Net Core için unit test yazmada kullanılabilecek kütüphaneler nedir diye baktığımızda .net framework'den de hatırlayacağımız Xunit, Moq ve FluentAssertions benzeri kütüphaneler karışımıza çıkmakta. Bu kütüphaneleri kullanarak geliştirdiğimiz .net core uygulamalarına ait sınıflar ve metotlar için unit test yazmak oldukça kolay ve eğlenceli.

Örneğimize başlamadan önce terminolojide bulunan bazı terimlere değinmek gerekirse;

Sut (Service Under Test)

Sut "Service Under Test"'in kısaltılmışı olarak unit test metotlarında test etmek istediğimiz sınıfın&service'in ismini belirtmen için değişken tanımlamada kullanılan kısaltmadır diyebiliriz.

Mocking

Sut içerisinde bulunan business'a ait testleri yazarken içerisinde kullanılan nesnelere ait fake sınıflardır. Bu sınıfların ürettiği process'e göre unit testini yazdığımız business'ın her bir koşuluna göre assertion'lar oluşturabiliriz. Örnek olarak; bir dış servise bağlanıp geriye object return eden bir metodunuz olsun. Siz bu metoda ait unit test yazarken bu dış service gitmek yerine tıpkı o dış service'e gidip response almış veya alamamış gibi bu dış service bağlantısını mock'lamak olarak düşünebilirsiniz.

Ecpected ve Actual Kavramları

Expected ; unit test yazdığımız fonksiyonalitenin vermesi beklenen çıktısı, result'ını belirtirken kullanılır. Diğer bir değişyle; bu metot veya sınıf bu parametrelerle bu sonucu üretmesi beklenir.

Actual ; unit test'ini yazdığımız metot yada sınıfın gerçek, o an döndüğü result'ı tanımlarken kullanılır.

Assertion

Actual ve Expected değerlerini karşılaştırırken içerisinde tanımlamalar yapabildiğimiz yapının/metodun/sınıfın ismidir.

Yazımızda örnek olarak CustomerApi adındaki web api projemiz için hem controller hemde service katmanları için unit test projeleri oluşturacağız. Hiç vakit kaybetmeden vs'da Customer.Api adında bir asp.net core web api projesi oluştralım. Benim kullandığım environment'da .net core sdk 2.1 yüklü bu yüzden projeleri 2.1 olarak oluşturacağım. Sizlerinde örneği takip edebilmek adına geliştirme ortamınızda .net core 2.0 ve üzeri bir sdk yüklü olması gerekmekte.

Solution açıldıktan sonra birde Customer.Service adında service layer için Core Class Library projesi oluşturalım ve geliştirmelerimize başlayalım.

İlk olarak Customer domain nesnesini oluşturalım. Bu nesne üzerinde Id, FullName, CityCode ve BirthDate alanlarını tutalım. Yeni bir customer yaratırken ve mevcut customer'ı update ederken ilgili validation'ları bu sınıf içerisinde aşağıdaki gibi tanımlayalım.

    public class Customer : Entity
    {
        public string FullName { get; protected set; }
        public string CityCode { get; protected set; }
        public DateTime BirthDate { get; protected set; }

        public Customer(string fullName, string cityCode, DateTime birthDate)
        {
            if (
                string.IsNullOrEmpty(fullName) ||
                string.IsNullOrEmpty(cityCode) ||
                birthDate.Date == DateTime.Today)
            {
                throw new Exception("Fields are not valid to create a new customer.");
            }

            FullName = fullName;
            CityCode = cityCode;
            BirthDate = birthDate;
        }

        protected Customer()
        {
            
        }

        public void SetFields(string fullName, string cityCode, DateTime birthDate)
        {
            if (string.IsNullOrEmpty(fullName) ||
                string.IsNullOrEmpty(cityCode) ||
                birthDate.Date == DateTime.Today)
            {
                throw new Exception("Fields are not valid to update.");
            }

            FullName = fullName;
            CityCode = cityCode;
            BirthDate = birthDate;
        }
    }

Sonrasında Controller'ın doğrudan iletişim kurabildiği ICustomerService ve onun implementasyonunu içeren sınıfları aşağıdaki gibi tanımlayalım. Bu katman CustomerDbContext üzerinden crud işlemlerinin yapılabildiği customerRepository sınıfının kullanıldığı service katmanıdır.

public interface ICustomerService
{
    void CreateNew(CustomerDto customer);
    CustomerDto Update(CustomerDto customer);
    List<CustomerDto> GetAll();
    List<CustomerDto> GetByCityCode(string cityCode);
    CustomerDto GetById(Guid id);
}
    public class CustomerService : ICustomerService
    {
        private readonly ICustomerRepository _customerRepository;
        private readonly ICustomerAssembler _customerAssembler;
        public CustomerService(ICustomerRepository customerRepository, ICustomerAssembler customerAssembler)
        {
            _customerRepository = customerRepository;
            _customerAssembler = customerAssembler;
        }

        public void CreateNew(CustomerDto customerDto)
        {
            var customer = _customerAssembler.ToCustomer(customerDto);

            _customerRepository.Save(customer);
        }

        public CustomerDto Update(CustomerDto customer)
        {
            var existing = _customerRepository.Get(customer.Id);

            existing.SetFields(customer.FullName, customer.CityCode, customer.BirthDate);

            _customerRepository.Update(existing);

            var customerDto = _customerAssembler.ToCustomerDto(existing);

            return customerDto;
        }

        public List<CustomerDto> GetAll()
        {
            var all = _customerRepository.All().ToList();
            return _customerAssembler.ToCustomerDtoList(all);
        }

        public List<CustomerDto> GetByCityCode(string cityCode)
        {
            var list = _customerRepository.Find(c => c.CityCode == cityCode).ToList();
            return _customerAssembler.ToCustomerDtoList(list);
        }

        public CustomerDto GetById(Guid id)
        {
            var customer = _customerRepository.Get(id);
            if (customer == null)
            {
                throw new Exception("Customer with this id : " + id + " not found.");
            }
            var customerDto = _customerAssembler.ToCustomerDto(customer);
            return customerDto;
        }
    }

Yukarıdaki bağımlılığı ve dbContext registration'ı .net core built-in container'a inject etmemiz gerekiyor bunun için Startup.cs içerisinde aşağıdaki gibi bağımlıkları register edelim.

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<CustomerDbContext>(options =>
                options.UseSqlServer(Configuration.GetSection("CustomerDbConnString").Value));

            services.AddScoped<ICustomerRepository, CustomerRepository>();
            services.AddScoped<ICustomerService, CustomerService>();

            services.AddTransient<ICustomerAssembler, CustomerAssembler>();
            services.AddTransient<ICustomerService, CustomerService>();

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
        }

Bu service metotlarını kullanacak olan api projemiz ile ilgili son olarak ise CustomerController.cs adında bir api controller oluşturup içerisinde service sınıfında yer alan metotları kullandığımız api end-point'lerini tanımlayalım.

    [Route("api/[controller]")]
    [ApiController]
    public class CustomerController : ControllerBase
    {
        private readonly ICustomerService _customerService;
        public CustomerController(ICustomerService customerService)
        {
            _customerService = customerService;
        }

        // GET api/customer
        [HttpGet]
        public ActionResult<List<CustomerDto>> Get()
        {
            return Ok(_customerService.GetAll());
        }

        // GET api/customer/id
        [HttpGet("{id}")]
        public ActionResult<CustomerDto> Get(Guid id)
        {
            return Ok(_customerService.GetById(id));
        }

        // POST api/customer
        [HttpPost]
        public ActionResult Post([FromBody] CustomerDto customer)
        {
            _customerService.CreateNew(customer);
            return Ok();
        }

        // PUT api/customer
        [HttpPut]
        public ActionResult<CustomerDto> Put([FromBody] CustomerDto customer)
        {
            return Ok(_customerService.Update(customer));
        }

        // GET api/customer/getbycitycode/cityCode
        [HttpGet("getbycitycode/{cityCode}")]
        public ActionResult<List<CustomerDto>> GetByCityCode(string cityCode)
        {
            return Ok(_customerService.GetByCityCode(cityCode));
        }
    }

Projemiz hazır durumda. Şimdi ufaktan unit-tes projelerini oluşturmaya başlayalım.

İlk olarak Service ve Domain sınıfları için solution'a sağ tıklayıp Add-New Project seçeneği seçip aşağıdaki gibi Customer.Service.Test adında bir xUnit Test projesi oluşturalım.

Projemiz bir xUnit test projesidir ve testleri yazarken kullanacağımız bazı kütphaneler şu şekildedir;

  • XUnit
  • Moq
  • FluentAssertions 
  • AutoFixture

dır. Sırasıyla bu kütüphaneleri projemiz referanslarına nuget üzerinden ekleyelim. Paketleri ekledikten sonra ilk olarak Customer domain'i ile test yazmaya başlayalım. Customer sınıfına ait bir instance oluştururken Customer'a ait property'leri update ederken belli bazı kontroller bulunmakta. Bu kontroller için aşağıdaki gibi CustomerTests.cs adında olan sınıf içerisinde unit testleri tanımlayalım.

    public class CustomerTests
    {
        [Theory, AutoMoqData]
        public void Create_Customer_Should_Throw_Exception_When_FullName_Is_Empty(string cityCode, DateTime birthDate)
        {
            Assert.Throws<Exception>(() => new Customer(string.Empty, cityCode, birthDate));
        }

        [Theory, AutoMoqData]
        public void Create_Customer_Should_Throw_Exception_When_CityCode_Is_Empty(string fullName, DateTime birthDate)
        {
            Assert.Throws<Exception>(() => new Domain.Customer(fullName, string.Empty, birthDate));
        }

        [Theory, AutoMoqData]
        public void Create_Customer_Should_Throw_Exception_When_BirthDate_Is_Invalid(string fullName, string cityCode)
        {
            Assert.Throws<Exception>(() => new Domain.Customer(fullName, cityCode, DateTime.Today));
        }

        [Theory, AutoMoqData]
        public void Create_Customer_Should_Success(string fullName, string cityCode, DateTime birthDate)
        {
            var sut = new Domain.Customer(fullName, cityCode, birthDate);

            sut.FullName.Should().Be(fullName);
            sut.CityCode.Should().Be(cityCode);
            sut.BirthDate.Should().Be(birthDate);
        }

        [Theory, AutoMoqData]
        public void SetFields_Should_Update_Fields(string fullName, string cityCode, DateTime birthDate, Domain.Customer sut)
        {
            sut.SetFields(fullName, cityCode, birthDate);

            sut.FullName.Should().Be(fullName);
            sut.CityCode.Should().Be(cityCode);
            sut.BirthDate.Should().Be(birthDate);
        }
    }
    
   //Method parameter olarak Automoq yapabilmek için kullanacağımız attribute
    public class AutoMoqDataAttribute : AutoDataAttribute
    {
        public AutoMoqDataAttribute()
            : base(new Fixture().Customize(new AutoMoqCustomization()))
        {
        }
    }

Yukarıda yazmış olduğumuz testleri run etmek için ise vs. üzerinde Test => Run => All Test diyerek aşağıda olduğu gibi Test Explorer'da Customer sınıfına ait testlerinizin Passed olduğunu görebilirsiniz.

Diğer bir test sınıfı ise ICustomerService interface'ine ait metotları test edebilmek için oluşturup test case'lerini aşağıdaki gibi yazalım.

    public class CustomerServiceTests
    {
        [Theory, AutoMoqData]
        public void CreateNewCustomer_Should_Success([Frozen]Mock<ICustomerAssembler> assembler, [Frozen]Mock<ICustomerRepository> repository, CustomerDto customerDto, Domain.Customer customer, CustomerService sut)
        {
            assembler.Setup(c => c.ToCustomer(customerDto)).Returns(customer);
            repository.Setup(c => c.Save(customer)).Returns(It.IsAny<Guid>());

            Action action = () =>
            {
                sut.CreateNew(customerDto);
            };
            action.Should().NotThrow<Exception>();
        }

        [Theory, AutoMoqData]
        public void UpdateCustomer_Should_Success([Frozen]Mock<ICustomerAssembler> assembler, [Frozen]Mock<ICustomerRepository> repository, CustomerDto customerDto, Domain.Customer customer, CustomerService sut)
        {
            assembler.Setup(c => c.ToCustomer(customerDto)).Returns(customer);
            repository.Setup(c => c.Update(customer));

            Action action = () =>
            {
                sut.Update(customerDto);
            };
            action.Should().NotThrow<Exception>();
        }

        [Theory, AutoMoqData]
        public void GetAll_Should_Success([Frozen]Mock<ICustomerAssembler> assembler, [Frozen]Mock<ICustomerRepository> repository, List<Domain.Customer> customers, List<CustomerDto> customersDtos, CustomerService sut)
        {
            repository.Setup(c => c.All()).Returns(customers.AsQueryable);
            assembler.Setup(c => c.ToCustomerDtoList(customers)).Returns(customersDtos);

            Action action = () =>
            {
                var result = sut.GetAll();
                result.Count.Should().Be(customersDtos.Count);
            };
            action.Should().NotThrow<Exception>();
        }


        [Theory, AutoMoqData]
        public void GetByCityCode_Should_Success([Frozen]Mock<ICustomerAssembler> assembler, [Frozen]Mock<ICustomerRepository> repository, string cityCode, List<Domain.Customer> customers, List<CustomerDto> customersDtos, CustomerService sut)
        {
            assembler.Setup(c => c.ToCustomerDtoList(customers)).Returns(customersDtos);
            repository.Setup(x => x.Find(It.IsAny<Expression<Func<Domain.Customer, bool>>>())).Returns(customers.AsQueryable);

            Action action = () =>
            {
                var result = sut.GetByCityCode(cityCode);
                result.Should().BeEquivalentTo(customersDtos);
            };
            action.Should().NotThrow<Exception>();
        }

        [Theory, AutoMoqData]
        public void GetById_Should_Return_As_Expected([Frozen]Mock<ICustomerAssembler> assembler, [Frozen]Mock<ICustomerRepository> repository, Guid id, CustomerDto customerDto, Domain.Customer customer, CustomerService sut)
        {
            assembler.Setup(c => c.ToCustomerDto(customer)).Returns(customerDto);
            repository.Setup(c => c.Get(id)).Returns(customer);

            Action action = () =>
            {
                var result = sut.GetById(id);
                result.Should().BeEquivalentTo(customerDto);
            };
            action.Should().NotThrow<Exception>();
        }
    }

Domain ve Service sınıfları için unit testlerimizi yukarıdaki gibi oluşturduk. Şimdi ise son olarak Controller için test projesi oluşturup ilgili test case'lerini yazalım. Yine yukarıda olduğu gibi solution'da bir tane Customer.Api.Test adında core xUnit test projesi oluşturalım ve XUnit, Moq, FluentAssertions, AutoFixture kütüphanelerini nuget üzerinden projemize ekleyelim.

    public class CustomerControllerTests
    {
        [Theory, AutoMoqData]
        public void GetAll_Should_Return_As_Expected(Mock<ICustomerService> customerServiceMock, List<CustomerDto> expected)
        {
            var sut = new CustomerController(customerServiceMock.Object);
            customerServiceMock.Setup(c => c.GetAll()).Returns(expected);

            var result = sut.Get();

            var apiOkResult = result.Result.Should().BeOfType<OkObjectResult>().Subject;
            var actual = apiOkResult.Value.Should().BeAssignableTo<List<CustomerDto>>().Subject;

            Assert.Equal(expected, actual);
        }

        [Theory, AutoMoqData]
        public void GetById_Should_Return_As_Expected(Mock<ICustomerService> customerServiceMock, Guid id, CustomerDto expected)
        {
            var sut = new CustomerController(customerServiceMock.Object);
            customerServiceMock.Setup(c => c.GetById(id)).Returns(expected);

            var result = sut.Get(id);

            var apiOkResult = result.Result.Should().BeOfType<OkObjectResult>().Subject;
            var actual = apiOkResult.Value.Should().BeAssignableTo<CustomerDto>().Subject;

            Assert.Equal(expected, actual);
        }

        [Theory, AutoMoqData]
        public void Post_Should_Return_As_Expected(Mock<ICustomerService> customerServiceMock, CustomerDto customer)
        {
            var sut = new CustomerController(customerServiceMock.Object);
            customerServiceMock.Setup(c => c.CreateNew(customer));

            var actual = sut.Post(customer);

            actual.GetType().Should().Be(typeof(OkResult));
        }

        [Theory, AutoMoqData]
        public void Put_Should_Return_As_Expected(Mock<ICustomerService> customerServiceMock, CustomerDto expected)
        {
            var sut = new CustomerController(customerServiceMock.Object);
            customerServiceMock.Setup(c => c.Update(expected)).Returns(expected);

            var result = sut.Put(expected);

            var apiOkResult = result.Result.Should().BeOfType<OkObjectResult>().Subject;
            var actual = apiOkResult.Value.Should().BeAssignableTo<CustomerDto>().Subject;

            Assert.Equal(expected, actual);
        }

        [Theory, AutoMoqData]
        public void GetByCityCode_Should_Return_As_Expected(Mock<ICustomerService> customerServiceMock, string cityCode, List<CustomerDto> expected)
        {
            var sut = new CustomerController(customerServiceMock.Object);
            customerServiceMock.Setup(c => c.GetByCityCode(cityCode)).Returns(expected);

            var result = sut.GetByCityCode(cityCode);

            var apiOkResult = result.Result.Should().BeOfType<OkObjectResult>().Subject;
            var actual = apiOkResult.Value.Should().BeAssignableTo<List<CustomerDto>>().Subject;

            Assert.Equal(expected, actual);
        }
    }

Controller için unit testlerimiz bitti. Şimdi solution'da bulunan bütün testleri run ettiğimizde aşağıdaki gibi hepsinin Passed olduğunu görmemiz gerekir. Toplamda 21 tane unit test metodu bulunmakta ve hepsi geçmiş durumda.

Unit test yazmak oldukça önemli ve yılmadan usanmadan keyif alarak yapılması gereken bir gerçek olarak biz developer'ların hayatında bulunmakta. Bazen unit test yazmak istediğiniz service'i geliştirirken daha az zaman harcamış olduğunuz anlar bile yaşanacaktır ancak titizlikle yazılan her bir unit-test'in bize getirisi oldukça fazla olacaktır. Production'ına çıkmadan fonksiyonaliteyi test etmek olsun, arada kaçak küçek saçma sapan bug'ların önüne geçmek açısından olsun, hemde isimlendirmeleri doğru yaptığımız taktirde projeye yeni başlayan birinin business'ı daha kolay anlayabilmesi gibi durumları göz önüne alırsak unit test yazmanın bir çok faydalı noktası bulunmakta. Üşenmeden gücenmeden yazmanız dileğiyle..

Source Code

Asp.Net Core ElasticSearch Logging ve Kibana Kurulumu & Kullanımı

Daha önceki yazılarda asp.net coreelasticsearchlogging konularına ayrı ayrı farklı örneklerle değinmiştik. Bu yazımızda ise asp.net core projelerinde default logging storage olarak elasticsearch konfigure edip bu elasticsearch'de ki index'lerde bulunan log kayıtlarını Kibana kullanarak nasıl görüntüleyebiliriz örnek proje ile anlatacağız. 

Logging bir uygulama için olmazsa olmazların başında gelmektedir. Uygulama cycle'ın da loglama işlemi doğru zamanda doğru yerde ve en önemlisi bir birine bağlı olacak şekilde yaptığınızda log sonucunda oluşan veri aslında sizin için paha biçilmez bir monitoring aracı olabilir. Analiz yapabilir, alert oluşturabilir, çeşitli raporlar sağlayabiliriz. Bütün bu saydıklarımız ve daha fazlası için çeşitli çözümler sunulabilir ancak şuan ki günümüz yazılım dünyasında ElasticSearch, Kibana with Logstach üçlüsü en performanslı ve open-source olduğundan community'si en sağlam çözüm olarak karşımıza çıkmakta.

Uygulama geçmeden önce bilgisayarınızda .Net Core SDK, ElasticSearch ve Kibana yüklü olmalı. Kurulumlarla ilgili detaylara .Net Core SDK ve ElasticSearch için aşağıdaki başlık altında bulunan yazılarımızdan ulaşabilirsiniz. Kibana kurulumunu ise bu yazımızda anlatacağız.

1) .Net Core Sdk 2.1 Kurulumu

.Net Core SDK 2.1 kurulumunu NET Core 2.0 to 2.1 Migration yazımızda anlatmıştık ve bu yazıda belirtilen instraction'ları takip ederek sdk kurulunu sağlayabilirsiniz.

2) ElasticSearch Kurulumu

ElastciSearch kullanabilmek için bu yazıda anlatıldığı gibi kurulum işlemlerini yapıp kurulumun doğru olup olmadığını test etmek adına browser üzerinden  http://localhost:9200/ adresine httpget isteği yaptığınızda kurulu olan es'ün bilgilerini görüntüleyebilirsiniz.

3) Kibana Kurulumu

Kibana kurulumu için elastic.co adresinde yer alan download sayfasından Kibana için ilgili işletim sisteminize ait dosyaları download edelim. Ben local'de windows kullandığım için Windows uyumlu versiyonu indirdim.

Download işlemi bittikten sonra rar'lı dosyaları ben C sürücüsünü seçtim extract edelim. Extract ettikten sonra C:\kibana-6.3.2\bin klasöründe bulunan kibana.bat dosyasını run edip kibanayı start etmesini bekleyelim. Dilerseniz Kibanayı işletim sistemine service olarak register'da edebilirsiniz.

Kibananın çalışıp çalışmadığından emin olmak için browser üzerinden http://localhost:5601 adresine giderek ulaşabilirsiniz. Elasticsearch'te index oluşturduktan sonra kibana'ya tekrar döneceğiz.

4) Asp.Net Core Api Proje Oluşturulması

Artık örnek projemizi geliştirmeye başlayalım. İlk olarak vs'da ProductApi adında bir Asp.net core 2.1 Web Api projesi oluşturalım.

Projeyi oluşturduktan sonra nuget'ten indirip kullanacağımız kütüphaneleri projemiz için kuralım. Uygulama loglarını atarken Serilog kütüphanesini ve onun ElasticSearch ve .net Core için olan extension dll'lerini projemize nuget üzerinden bulup ekleyelim.

Yukarıdaki paketlerin nuget üzerinden kurulum işlemleri tamamlandıktan sonra projede yer alan Startup.cs sınıfı içerisinde serilog ve elasticsearch için gerekli olan logging konfigurasyonlarını aşağıdaki gibi yapalım. İlk olarak ConfigureServices metodunda serilog ve serilog'un storage olarak elasticsearch'ü kullanacağını belirten kod bloğunu aşağıdaki gibi yazalım.

public void ConfigureServices(IServiceCollection services)
{
    Log.Logger = new LoggerConfiguration()
        .Enrich.FromLogContext()
        .WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("http://localhost:9200/"))
        {
            AutoRegisterTemplate = true,
        })
        .CreateLogger();

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

Sonrasında ise Configure metodunda yukarıda ilgili tanımlamalarını yaptığımız serilog'u LoggerFactory'e ekleyip uygulamanın serilog üzerinden logging yapacağını belirttiğimiz kısmı yazalım.

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseHsts();
    }

    loggerFactory.AddSerilog();//serilog servisini eklediğimiz yer

    app.UseMvc();
}

Kurulumlarımızı ve proje geliştirme adımlarını tamamladık artık projemizi run ederek örnek olarak bir ProductController açıp içerisinde bulunan Get metoduna ürün isimleri girmiştim browser üzerinden http://localhost:60506/api/product adresine httpGet isteği attığımızda aşağıdaki gibi ürün isimlerini listelediğini göreceğiz.

[Route("api/[controller]")]
[ApiController]
public class ProductController : ControllerBase
{
    // GET api/product
    [HttpGet]
    public ActionResult<IEnumerable<string>> Get()
    {
        return new List<string> { "Mobile Phone", "Laptop", "Books", "Shoes" };
    }
}

Uygulamada minimum log level Information olduğundan hem uygulama start event'leri hemde end-point'e yaptığımız request-response'a ait log mesajlarını şuan elasticsearch'de logstash-yyy.MM.dd (logstash-2018.07.26 gibi.) formatında bir index oluşturup bu index'e günlük olarak insert etmiş bulunuyor. Bu index'in yaratılıp yaratılmadığını anlamak içinse yine browser üzerinden elasticsearch'ün api'sine istekte bulunarak öğrenebiliriz. Browser'dan http://localhost:9200/_cat/indices?v adesine httpGet isteği yolladığınızda yukarıda bahsettiğimiz formatta index'in oluştuğunu göreceksinizdir.

Şimdi ise son adım olarak index'lemiş olduğumuz bu logları Kibana üzerinde görüntüleyelim. http://localhost:5601 adresine gittiğimizde şuan için herhangi bir index tanımlaması yapmadığımızdan hiçbir şey görüntülenmemekte. Bunun için Kibana'da sol menüde yer alan Management sayfasına giderek Index Patterns ekranında Index pattern textbox'ına "logstash-*" yazarak Next dedikten sonra çıkan ekranda Time Filter'ı log hangi property'ye göre yapacağını belirttiğimiz dropdown'dan @timestamp field'ını seçip Create Index Pattern butonuna tıkladıktan sonra logstash- formatına uygun bütün indexleri Kibana'ya tanımlamış olduk.

 

Indexlemiş olduğumuz logları görüntüleyebilmek için yine sol menüden Discover sayfasına giderek zaman filtresine göre aşağıdaki gibi uygulamamıza ait logları görebiliriz. Dilersek search box'ı kullanarak log içersiinde aramak istediğimiz bir metni kolayca arayabilir yada uygulamanın throw ettiği exception'lar için dashboard'lar tanımlayıp daha metric'ler kullanarak kolayca görüntüleyebiliriz. 

 

Geleneksel logging tekniklerinde genelde erişilmesi ve anlaşılması zor içinde kaybolunabilen yapılar söz konusuydu. Öyle ki log dosyaları arasında aradığımız bir text'i bulmak bazen saatlerimizi bile alabilmekteydi. Ancak elasticsearch ve kibana bunu tamamiyle değiştirdi desek yanlış olmaz. Bu ikili ile birlikte uygulamanızın ürettiği günlük yüzlerce megabyte'lık log text'ini elasticsearch'e index'leyip çok rahat ve hızlı bir şekilde kibana üzerinden erişebilirsiniz. X pack kullanarak çeşitli alert yapıları tasarlayabilir uygulamanızla ilgili herhangi olağan dışı bir durumda en hızlı şekilde kolayca haberdar olabilirsiniz. 

Source Code

.Net Core 2.0 to 2.1 Migration Nasıl Yapılır

Microsoft .Net Core 2.1  SDK'sını ve bu sdk ile uyumlu çalışan kütüphanelerini nuget üzerinden yayınlayalı nerdeyse 3 ay oldu gibi ve hem 2.0'da karşılaştığımız bazı sorunların (HttpClient vb) giderilmesi hemde gözle görülür bir performans artışı açısından .Net Core 2.1 oldukça önem arz etmekte. 

.Net Core 2.1 kullanarak yeni bir uygulama geliştirmek için sdk'iniz yüklü olduğu taktirde vs 2017 kullanarak uygulamalar geliştirebilirsiniz. Mevcut 2.0 uygulamaların 2.1'e migrate edilmesi durumunda yapılacak bir kaç işlem bulunmakta.

Bu yazıda mevcut .net core 2.0 web uygulamamızı nasıl 2.1'e yükseltebiliriz inceleyeceğiz. 

ASP.NET Core 2.1 SDK Kurulumu

.net core veya asp.net core 2.1 uygulaması geliştirmek için ilk olarak 2.1 SDK'yı bu adresten indirip kurmamız gerekmekte. Açılan sayfada Download .NET SDK butonuna tıklayarak indirme işlemi bittikten sonra aşağıdaki gibi şuan ki mevcut dağıtılan 2.1.302 versiyonunun kurulumunu başlatalım.

Kurulum işlemi tamamalandıktan sonra completed ekranı aşağıdaki gibi olacaktır.

Kurulumdan emin olmak için command prompt'ta dotnet --info yazarak kurulu olan sdk bilgilerine erişebiliriz. Bende 2.1.300 versiyonu yüklü olduğundan o sürüme ait bilgiler görünmekte.

Set Proejct Target Framework to 2.1 in Solution  

SDK kurulumu tamamlandıktan sonra solution'da bulunan mevcut projemizi 2.0 ile geliştirdiğimiz için .csprj dosyasında TargetFramework olarak 2.0 yazmakta. Bilgisayarımızda yüklü Sdk'lar aşağıdaki gibi listeleneceklerdir projeyi 2.1'e çekebilmek için solution'da bulunan projenin properties'lerine giderek açılan ekranda dropdown'dan TargetFramework olarak 2.1 set edip projemizi build edelim.

Yukarıda bahsettiğimiz gibi .csprj dosyasına baktığımzıda artık .Net Core 2.1 yazılı olduğunu göreceğiz.

Şimdi ise son adım olarak projede 2.0 sdk ile uyumlu kullanılan .Net Core kütüphanelerini 2.0'dan 2.1'e update etme işlemi var. Bunun için ilk olarak 2.0 ile birlikte default gelen AspNetCore.All kütüphanesi yerini AspNetCore.App'a bırakmakta. Bunun için AspNetCore.All referansını silip AspNetCore.App'i projemize kuralım.

 

Bu kütüphane dışında projenizde kullandığınız diğer Microsoft tarafından yayınlanan EntityFramework.Core gibi kütüphaneleride 2.1 versiyonuna update etmeniz gerekmekte.

Bütün kütüphaneleri güncelledikten sonra yüklü olan 2.0 SDK'yı da silip geliştirmelerinize devam edebilirsiniz.

Yazının başında da bahsettiğim gibi .net core 2.1 ile birlikte 2.0'da bulunan bazı kütüphanelerdeki gözle görülür performans sıkıntılarının çözümü ve yeni feature'lar içermesi bakımından oldukça önemlidir ve bir an önce migration'ı yapmak biz 2.0 kullananların yararına olacaktır.

Entity Framework Core Nedir, Nasıl Kullanılır ? Generic Repository Pattern Kullanarak Asp.Net Core Web Api Projesi Geliştirme

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

Proje Oluşturulması

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

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

DbContext-Entity Tanımlaması

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

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

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

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

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

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

       services.AddMvc();
   }

Generic Repository Oluşturulması

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

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

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

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

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

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

        return entity.Id;
    }

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

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

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

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

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

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

public interface IProductRepository : IGenericRepository<Product>
{ }

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

Service Layer Oluşturulması

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

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

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

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

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

        return new GetAllProductResponse
        {
            ProductList = mappedList
        };
    }

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

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

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

Api Controller Oluşturulması

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

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

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

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

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

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

Asp.Net Core Projelerine Swagger Ekleme

Swagger biz developer'lar için oldukça önemli bir tool'dur. Server side geliştirmeler için entegre olacak client'lar veya bu endpoint'leri test etmek isteyen test ekipleri yada farklı developer takımları için döküman ve request response örnekleri hazırlamak olmazsa olmazlardan dır. .Net framework'de olduğu gibi .Net Core projelerinde de swagger kütüphanesi tam da bu gibi ihtiyaçlar için bulunmaktadır.

Swagger

Swagger'n tanımına bakacak olursak;

"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 Core projeleri içinde implementasyonu oldukça basittir. 

Imp

Geliştirmekte olduğumuz .net core web api projesine nuget üzerinden Swashbuckle.AspNetCore  paketini indirip kuralım. Kurulum işlemi tamamlandıktan sonra projemizin Startup.cs sınıfı içerisinde yer alan ConfigureServices metodunda swagger'ı service olarak ekleyelim. 

public void ConfigureServices(IServiceCollection services)
{
	services.AddSwaggerGen(c =>
	{
		c.SwaggerDoc("v1", new Info { Title = "WebApplication1", Version = "v1" });
	});

        services.AddMvc();
}

Sonrasında eklediğimiz bu service'i yine Startup.cs içerisinde yer alan Configure metodunda kullanacağımızı belirtelim. 

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
	if (!env.IsProduction())
	{
		app.UseDeveloperExceptionPage();

		app.UseSwagger();

		app.UseSwaggerUI(c =>
		{
		c.SwaggerEndpoint("/swagger/v1/swagger.json", "WebApplication1");
		});
	}
}

Projenizi run ettiğinizde browser üzerinden Swagger Ui sayfasına localhost:40383/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.

 Http200 kodu ile response'u almış oluyoruz.

Ö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.

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.

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.

.Net Core Register All Dependencies at Once (Scrutor)

.Net Core projelerinde dependency injection uygularken eğer built-in container'ı kullanıyorsanız ve onlarca Service,Repository veyahut manager vb. interface/implementasyon'u varsa bunları container'a register etmek biraz uğraştırıyor. Sebebi ise castleWindsor veya Autofac gibi DI Tool'larından alışkın olduğumuz register-all-at-once gibi bir özelliği şuan için bulunmamakta. Yani aşağıdaki gibi her bir isterface ve onun implementasyonu için ayrı ayrı registration tanımlanması gerekmekte.

services.AddSingleton<IUserService, UserService>();
services.AddSingleton<ICustomerService, CustomerService>();
services.AddSingleton<IProductService, ProductService>();
services.AddSingleton<IEmployeeService, EmployeeService>();
services.AddTransient<IUserManager, UserManager>();
services.AddTransient<ICustomerManager, CustomerManager>();
services.AddTransient<IProductManager, ProductManager>();
services.AddTransient<IEmployeeManager, EmployeeManager>();
...

Ancak kısa süre önce github'da paylaşılan ve nuget üzerinden de install edilebilen bir kütüphane yayınlandı.

Scrutor

Scrutor "Assembly scanning and decoration extensions for Microsoft.Extensions.DependencyInjection" şeklinde tanımlanmakta ve bu kütüphane ile aynısı olmasada CastleWindsor ve Autofac de olduğu gibi registration işlemleri biraz olsun basitleşiyor. Scrutor'u projenize install ettikten sonra registration tanımlarken belirttiğiniz interface'in bulunduğu bütün assembly'leri scan edip bağımlılıkları inject etmekte.

Yukarıda built-in container kullanarak register ettiğimiz bağımlılıkları scrutor kullanarak aşağıdaki gibi de yapabiliriz.

services.Scan(scan => scan  
  .FromAssemblyOf<IAssemblyMarker>()
  
   // ISingletonLifetime adında bir interface tanımlayıp container'a single instance olarak register olmasını istediğimiz dependency'leri bu interface'den inherit ederek doğrudan register edebilmekteyiz.
  .AddClasses(classes => classes.AssignableTo<ISingletonLifetime>())
  .AsImplementedInterfaces()
  .WithSingletonLifetime()
  
   // ITransientLifetime adında bir interface tanımlayıp container'a transientLifeTime'ına sahip olmasını istediğimiz dependency'leri bu interface'den inherit ederek doğrudan register edebilmekteyiz.
  .AddClasses(classes => classes.AssignableTo<ITransientLifetime>())
  .AsImplementedInterfaces()
  .WithTransientLifetime());
  
  public interface ISingletonLifetime
  { }
  public interface ITransientLifetime
  { }
  
  public interface ICustomerService : ISingletonLifetime
  { 
     //todo
  }
  
  public interface ICustomerManager : ITransientLifetime
  { 
    //todo
  }

Scrutor startUp anında assembly'i scan edip ITransientLifetime ve ISingletonLifetime interface'lerini ve onların implementedInterface'lerini bularak implementasyonlarını doğrudan container'a register eder.

Scrutor doğru kullanıldığı takdirde oldukça başarılı bir kütüphanedir ve bizleri dependecny injection konusunda belli başlı bazı zorluklardan kurtarabilmektedir.

Asp.Net Core JSON Web Token Kullanımı

Bu yazımızda asp.net core uygulamalarında token based authentication nedir, nasıl sağlanır bir örnek üzerinden inceleyeceğiz. 

ProductApi adında bir service projenizin olduğunu düşünün ve bu service üzerinde product tablonuz için CRUD işlemlerini yapan belli endpoint'ler sağladığınızı varsayalım. Herhangi bir güvenlik kontrolü bulunmayan ProductApi'nize call yapmak isteyen bir kişi geliştirme yaparken doğrudan erişebiliyor. Peki ama çok basit bir şekilde düşünecek olursak service url'lerini bulan herhangi bir kişi servisinizi manipüle etmek adına CRUD metotlarınıza doğrudan call yapabilir yada bazı metotları call edebilir bazılarını edemez vs. gibi riskler barındırmaktadır.

Bu gibi durumlara çözüm olarak token-based authentication yöntemleri geliştirilmiştir.

Token based authentication'ın genel konsepti oldukça basit; kullanıcıdan bir username ve password vs. gibi bir bilgi alıp bu bilgiyi server'a göndermek ve eğer valid bir username ve password ise karşılığında bir token dönüp o kullanıcının artık token expire oluncaya dek bütün api işlemlerini o token üzerinden yapması beklenir. 

JSON Web Token Nedir Nasıl Kullanılır;

Base64 olarak oluşturulmuş 3 ayrı bölümden oluşur;

HEADER.PAYLOAD.SIGNATURE

Header bölümünde; hangi token türünün ve şifreleme algoritmasının kullanıldığı bilgisi yer alır.

Payload; uygulama bazlı bilgilerin yer aldığı(claim,userId vs.) yani uygulamaya özel bölümdür.

Signature ise adından da anlaşıldığı gibi server tarafından üretilen signature'ın bulunduğu bölümdür.

 

Şimdi ise bir asp.net core projesinde JWT nasıl entegre edilir ve kullanılır bunu inceleyelim. 

İlk olarak aşağıdaki gibi Startup.cs bulunan ConfigureServices metodu içerisinde uygulama boyunca geçerli olan JWT Authentication middleware konfigurasyonlarını yapalım.

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(jwtBearerOptions =>
    {
        jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters()
        {
            ValidateActor = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = Configuration["Issuer"],
            ValidAudience = Configuration["Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["SigningKey"]))
        };
    });
    services.AddMvc();
}

Middleware tanımlamasını yukarıdaki gibi yaptıktan sonra bunu builder'a eklememiz gerekmekte. Bunun içinde yine Startup.cs de Configure metodu içerisinde aşağıdaki tanımlamayı yapalım.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    app.UseAuthentication();
    app.UseMvc();
}

Sırada JWT generate edecek olan endpoint'i oluşturma var. Bunun için TokenController adında bir controller oluşturalım ve içerisine kullanıcıyı validate ederken kullanılacak olan bilgilerin bulunduğu request modeli alıp geriye tokenResponse dönen bir endpoint oluşturalım.

[AllowAnonymous]
[HttpPost]
[Route("token")]
public IActionResult Post([FromBody]LoginRequest request)
{
    if (ModelState.IsValid)
    {
        var user = _userService.Get(request.UserName, request.Password); 
        if (user == null)
        {
            return Unauthorized();
        }

        var claims = new[]
        {
            new Claim(JwtRegisteredClaimNames.Sub, request.Username),
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
        };

        var token = new JwtSecurityToken
        (
            issuer: _configuration["Issuer"], //appsettings.json içerisinde bulunan issuer değeri
            audience: _configuration["Audience"],//appsettings.json içerisinde bulunan audince değeri
            claims: claims,
            expires: DateTime.UtcNow.AddDays(30), // 30 gün geçerli olacak
            notBefore: DateTime.UtcNow,
            signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["SigningKey"])),//appsettings.json içerisinde bulunan signingkey değeri
                    SecurityAlgorithms.HmacSha256)
        );
        return Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token) });
    }
    return BadRequest();
}

public class LoginRequest
{
	public string UserName {get;set;}
	public string Password {get;set;}
}

JWT based authentication yapısı projemiz için hazır. Sırada bunu test etmek var. Bunun için asp.net core projesi oluşturulurken default gelen ValuesController.cs içerisindeki Get metodunu kullanarak testimizi yapalım. Controller seviyesinde [Authorize] atrtribute'ü kullanarak authentication zorunlu olduğunu belirtebiliriz.

[Authorize]
[Route("api/[controller]")] 
public class ValuesController : Controller
{
    [HttpGet]
    public IEnumerable<string> Get()
    {
        return new string[] { "value1", "value2" };
    }
}

Postman kullanarak projemizi test edelim.

İlk olarak token almadan ValuesController'a HttpGet request'inde bulunalım ancak token bilgisi set etmediğimizden bize geriye successful bir response (http200) dönmemesi gerekir. Aşağıda görüldüğü üzre response olarak 401 yani Unauthorized cevabı aldık.

Şimdi ise TokenController da bulunan metoda request atarak token response'unu aşağıdaki gibi alalım.

Almış olduğumuz tokenResponse'u göndereceğimiz request'in Authorization header'ına set ederek tekrardan ValuesController'a istekte bulunduğumuzda bu sefer http200 ile geriye value array'ini dönen cevabı almış olacağız.

JWT'nin kullanımı özetle bu şekilde. Sizlerde geliştirdiğiniz bir api projenizi dış dünyaya açarken token-based authentication yapmak istediğinizde implementasyonu oldukça basit olan basit jwt den faydalanabilirsiniz.

Chain of Responsibility Pattern Nedir

Chain of Responsibility pattern behavioral patterns gurubuna ait olan ve özünde birbirini takip eden iş dizisine ait process'leri redirect ve handle etmek yada istekte bulunan-confirm eden süreçleri için çözüm olarak ortaya çıkmış bir tasarım desendir.

Yukarıda tanım yaparken birbirini takip eden iş dizesinden kasıt birbirlerine Loosly Coupled bir şekilde zincir gibi bağlı olan süreçleri process etmek için kullanabileceğimiz bir pattern dir.

Bir örnek ile ele alacak olursak; veznede çalışan bir kişi için günlük nakit para çekim miktarı 40 bin TL olan bir banka düşünelim ve bu bankaya gelen bir müşteri veznede bulunan kişiden 240 bin TL para çekmek istediğini söyledi. Banka kuralları gereği bu işlemin sırasıyla veznedar, yönetici, müdür ve bölge sorumlusu tarafından sırasıyla onaylaması gerekmekte. Bakacak olduğumuzda zincir şeklinde birbirine bağlı olan bir onay yapısı bulunmakta. 

Akış olarak özetleyecek olursak;

  1.  Müşteri 480 bin TL lik para çekme isteğini veznedar'a iletir.
  2.  Veznedar bu isteği alır ve kontrol eder eğer onaylayabileceği bir tutar ise onaylar, onaylayabileceği bir tutar değilse yöneticisine gönderir,
  3.  Yönetici isteği alır  onaylayabileceği bir tutar değilse müdüre iletir,
  4.  Müdür kontrol eder eğer onaylayabileceği bir tutar değilse bölge sorumlusunun onayına gönderir,
  5.  Bölge sorumlusu onaylar ve para müşteriye verilir.

Yukarıda bahsettiğimiz bu örneğimizi Chain of Responsibility pattern uygulayarak geliştirelim.

İlk olarak Withdraw adında domain model tanımlayalım.

   public class Withdraw
    {
        public string CustomerId { get; }
        public decimal Amount { get; }
        public string CurrencyType { get; }
        public string SoruceAccountId { get; }

        public Withdraw(string customerId, decimal amount, string currencyType, string soruceAccountId)
        {
            CustomerId = customerId;
            Amount = amount;
            CurrencyType = currencyType;
            SoruceAccountId = soruceAccountId;
        }
    }

Sornasında abstract bir Employee sınıfı tanımlayalım ve içerisinde aşağıdaki gibi property'lerinı yazalım.

    public abstract class Employee
    {
        protected Employee NextApprover;

        public void SetNextApprover(Employee supervisor)
        {
            this.NextApprover = supervisor;
        }

        public abstract void ProcessRequest(Withdraw req);
    }

Yukarıda bulunan NextApprover isimli property o sınıfa ait kişinin yöneticisi olarak atanan kişidir ve ProcessRequest metodunda ilgili condition'ı yazıp sırasıyla NextApprover'ları call edeceğiz.

Veznedar, Yonetici, Mudur ve BolgeSorumlusu sınıfları yukarıda tanımladığımız Employee sınıfından inherit olacak şekilde aşağıdaki gibi oluşturalım. 

    public class Veznedar : Employee
    {
        public override void ProcessRequest(Withdraw req)
        {
            if (req.Amount <= 40000)
            {
                Console.WriteLine("{0} tarafından para çekme işlemi onaylandı #{1}",
                    this.GetType().Name, req.Amount);
            }
            else if (NextApprover != null)
            {
                Console.WriteLine("{0} TL işlem tutarı {1} max. limitini aştığından işlem yöneticiye gönderildi.", req.Amount, this.GetType().Name);

                NextApprover.ProcessRequest(req);
            }
        }
    }

    public class Yonetici : Employee
    {
        public override void ProcessRequest(Withdraw req)
        {
            if (req.Amount <= 70000)
            {
                Console.WriteLine("{0} tarafından para çekme işlemi onaylandı #{1} TL",
                    this.GetType().Name, req.Amount);
            }
            else if (NextApprover != null)
            {
                Console.WriteLine("{0} TL işlem tutarı {1} max. limitini aştığından işlem yöneticiye gönderildi.", req.Amount, this.GetType().Name);

                NextApprover.ProcessRequest(req);
            }
        }
    }

    public class Mudur : Employee
    {
        public override void ProcessRequest(Withdraw req)
        {
            if (req.Amount <= 150000)
            {
                Console.WriteLine("{0} tarafından para çekme işlemi onaylandı #{1} TL",
                    this.GetType().Name, req.Amount);
            }
            else if (NextApprover != null)
            {
                Console.WriteLine("{0} TL işlem tutarı {1} max. limitini aştığından işlem yöneticiye gönderildi.", req.Amount, this.GetType().Name);

                NextApprover.ProcessRequest(req);
            }
        }
    }

    public class BolgeSorumlusu : Employee
    {
        public override void ProcessRequest(Withdraw req)
        {
            if (req.Amount <= 750000)
            {
                Console.WriteLine("{0} tarafından para çekme işlemi onaylandı #{1} TL",
                    this.GetType().Name, req.Amount);
            }
            else
            {
                throw new Exception(
                    $"Limit banka günlük işlem limitini aştığından para çekme işlemi #{req.Amount} TL onaylanmadı.");
            }
        }
    }

Son olarak ise domain modeli initialize edip chain'i oluşturup process metodunu call edelim.

    static void Main(string[] args)
    {
        var withdraw = new Withdraw("a6e193dc-cdbb-4f09-af1a-dea307a9ed15", 480000, "TRY", "TR681223154132432141412");
 
        Employee veznedar = new Veznedar();
        Employee yonetici = new Yonetici();
        Employee mudur = new Mudur();
        Employee bolgeSorumlusu = new BolgeSorumlusu();
 
        veznedar.SetNextApprover(yonetici);
        yonetici.SetNextApprover(mudur);
        mudur.SetNextApprover(bolgeSorumlusu);
 
 
        withdraw.Process(veznedar);
 
        Console.ReadKey();
    }

Yukarıdaki gibi 480000 TL lik bir işlem için istekte bulunduğumuzda console çıktısı aşağıdaki gibi olacaktır.

İşlem sırasıyla veznedar, yönetici, müdür bölge sorumlusunun önünde düşecektir. Son olarak ise bölge sorumlusunun onaylayabileceği bir tutar olduğundan onay verip banka müşterisine ödeme işlemini yapacaktır.

Chain of Responsibility pattern bir chain halinde process edilmesi gereken operasyonlar için rahatlıkla kullanabileceğimiz bir pattern dir. Yazılım dünyasında kullanım olarak diğer desing pattern'lar arasında %30-%40 lık bir orana sahip olduğu iddia edilir ve sıkça kullanılmaktadır.