Caner Tosuner

Leave your code better than you found it

Asp.Net Core HttpClientFactory Kullanımı

Bu yazıda Asp.Net Core 2.1 ile birlikte gelen HttpClientFactory sınıfını inceleyeceğiz. Asp.Net Core 2.0 sürümünde HttpClient sınıfı ile ilgili ciddi sorunlar vardı. Bunlardan en bariz olanı yük altında çalışan uygulamalarda socket connection mangement tarafında bir takım sorunlar olmasıydı. Herhangi bir .net core 2.0 uygulaması üzerinde HttpClient sınıfını kullanarak bir loop içerisinde htpp call yapıp netstat çektiğinizde açılan socket'lerde TimeWait'ler oluşturduğunu bununda uygulamalarda ciddi sorunlara sebebiyet verdiği gözlemlendi. Github'da açılan issue'lar vs derken Microsoft tarafında geliştirme yapan team sorunları kabul ederek 2.1 versiyonu ile HttpClient'ı yeniden ele alacakalrını belirttiler ve 2.1 release'den sonra hayatımıza HttpClientFactory sınıfı girdi.

HttpClientFactory, doğru memory management'ı yaparak http istekleri yapmamızı sağlan HttpClient(.net 4.5 ile geldi) sınıfının instance'ını oluşturmak için kullanılan sınıftır. HttpClient sınıfının çok fazla instance'ını oluşturmak uygulamalar için maliyetli bir işlemdir. Her yeni bir instance remote server için yeni bir connection demektir. Çok fazla trafiğin olduğu bir uygulamada ise gerektiğinden fazla httpClient instance'ı oluşturmak uygulama için kullanılabilecek socket'lerin tüketilmesi demektir ki bunu istemeyiz.

Bu sınıf HttpClient instance'larının doğru yönetilmesini sağlar ve böylelikle yukarıda bahsettiğimiz sorunları çözdüğünden oldukça önemli bir feature dır.

3 farklı kullanım şekli sunulmuştur;

  • HttpClientFactory doğrudan kullanma
  • Named Client Oluşturma
  • Typed Client Oluşturma

Asp.Net Core 2.1 versiyonu ile birlikte HttpClient kullanımı uygulama servislerinde ayrı bir feature olarak sunulmuştur ve bizimde bu sınıfı kullanabilmek için yapmamız gereken feature'ı uygulamada kullancağımızı belirttiğimiz aşağıdaki kod satırını Startup.cs içerisinde ConfigureServices metoduna yazmak.

services.AddHttpClient();

1) HttpClientFactory Sınıfını Doğrudan Kullanarak

ApiController seviyesinden doğrudan HttpClientFactory sınıfını inject ederek ihtiyaç duyulan yerde httpClient instance'ı yaratabiliriz.

public class FooController : ControllerBase
{
    private readonly IHttpClientFactory _httpClientFactory;
  
    public FooController(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }
  
    [HttpGet]
    public async Task<ActionResult> GetHomePage()
    {
        var client = _httpClientFactory.CreateClient();
        client.BaseAddress = new Uri("https://www.microsoft.com");
        string result = await client.GetStringAsync("/");
        return Ok(result);
    }
}

2) Named Clients

Bir diğer kullanım şekli ise ilgili domain'e özel named client'lar oluşturmak. Örneğin uygulamada www.microsoft.com  domaininde bulunan adreslere birden fazla request attığınız bir case için bu domaine özel custom client oluşturmak daha performanslı olmakta. Yukarıda kullanıdğımız AddHttpClient HttpClient sınıfını doğrudan kullanmamızı sağladığı için yazmıştık. NamedClient içinse yine bu metodu bu sefer "microsoft" ismine özel bir client tanımlaması olduğundan aşağıdaki gibi yazalım.

services.AddHttpClient("microsoft", c =>
{
    c.BaseAddress = new Uri("https://www.microsoft.com");
    c.DefaultRequestHeaders.Add("CustomHeaderKey", "It-is-a-HttpClientFactory-Sample");
});

Kullanım olarak ise yine apiController içerisinde aşağıdaki gibi "microsoft" ismindeki client'ı factory'den isteyebiliriz.

public class FooController : ControllerBase
{
    private readonly IHttpClientFactory _httpClientFactory;
  
    public FooController(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }
  
    [HttpGet]
    public async Task<ActionResult> GetHomePage()
    {
        var client = _httpClientFactory.CreateClient("microsoft");
        string result = await client.GetStringAsync("/");
        return Ok(result);
    }
}

3) Typed Clients

 

Üçüncü ve son kullanım şekli ise yine ilgili domain'e özel custom typed client sınıfı oluşturabiliriz.

public class MicrosoftHttpClient
{
    public HttpClient Client { get; private set; }
    
    public MicrosoftHttpClient(HttpClient httpClient)
    {
        httpClient.BaseAddress = new Uri("https://www.microsoft.com/");
        httpClient.DefaultRequestHeaders.Add("CustomHeaderKey", "It-is-a-HttpClientFactory-Sample");
        Client = httpClient;
    }
}

Yukarıda oluşturduğumuz bu custom client sınıfını ConfigureServices metodu içerisinde uygulama servislerine aşağıdaki gibi register ederek ihtiyaç duyduğumuz yerde kullanabiliriz.

services.AddHttpClient<MicrosoftHttpClient>();

Controller içerisinde ise constructor'a MicrosoftHttpClient sınıfını inject etmemiz yeterli.

public class FooController : ControllerBase
{
    private readonly MicrosoftHttpClient _microsoftHttpClient;
  
    public FooController(MicrosoftHttpClient microsoftHttpClient)
    {
        _microsoftHttpClient = microsoftHttpClient;
    }
  
    [HttpGet]
    public async Task<ActionResult> GetHomePage()
    {
        string result = await _microsoftHttpClient.client.GetStringAsync("/");
        return Ok(result);
    }
}

Dilerseniz MicrosoftHttpClient sınıfını IMicrosoftHttClient adında bir interface'den türetip kullanmak istediğiniz yerde constructor sevisyesinde bu interface'i de inject ederek deneyebilirsiniz.

Özellikle trafiğin çok yoğun olduğu uygulamalarda başka bir remote server'a http call yaparak birşeyler consume etmek istediğinizde external connection management'ı oldukça önemli bir hal almakta. Her ne kadar microsoftun geliştirdiği ürünlerde bir çok şeyi biz developer'lara bırakmadan arka planda kendisi halletsede asp.net core 2.0 da acı bir şekilde deneyimlediğimiz gibi yukarıdaki gibi benzer sorunlar olabilmekte. HttpClientFactory sınıfının 3 farklı kullanım şeklini ele aldık ve bunlardan herhangi birini ihtiyacınız doğrultusunda kullanarak remote server call işlemlerinizi kolayca güvenli bir şekilde yapabilirsiniz.

Asp.Net Core Web Api Integration Test Nedir Nasıl Yazılır

Unit test veya integration test projenizde bulunan business logic'lerin belirtilen input'lar çerçevesinde ne şekilde çalışması gerektiğini garanti ettiğimiz yapılar olup projelerimiz için oldukça önemli bir Must layer olmaktadır. Daha önceki yazılarımızda unit test konusuna değinip örnek proje üzerinde işlemiştik. Bu yazımızın konusu ise asp.net core uygulamarı için integration test nasıl yazılır.

Integration test; projenizde yer alan birden fazla modülün belirtilen input'lar ile beklendiği şekilde çalışmasını test eden yapıdır. Diğer bir deyişle, bir biri ile bağlı şekilde çalışan modüler yapıları ve onların sahip oldukları business logic'leri toplu bir şekilde ele alıp doğruluğunu veya yanlışlığını test etmemizi sağlayan yapıdır.

Örnek olarak verecek olursak; bir CustomerApi projemiz var end-point'ler üzerinden database'de crud işlemleri yapmakta. Bu end-point'lerin testini uçtan uca request handle'dan başlayıp business-layer'da yer alan logic'lerin bulunduğu metodları ve repository-layer üzerinden database'de ki ilgili column'ların doğruluğuna kadar olan bu birbirine bağlı iki sistemin (database & api) uçtan uca kontrollerini integration-test yazarak yapabiliriz.

Daha önceki unit test yazımızda yaptığımız örnek üzerinden ilerleyelim. O yazımızdaki örnekte asp.net core 2.1 kullanarak CustomerApi adında bir proje oluşturup unit testlerini yazmıştık. Yine aynı örnek üzerinden integration test projeleri oluşurup test metotlarımızı yazalım.

Solution açıldıktan sonra test klasörü içerisine Customer.Api.IntegrationTest adında bir xUnit test projesi oluşturalım

 

CustomerController.cs içerisinde CRUD işlemlerini yapan metotlarımız mevcut ve sırasıyla bu metotlar için integration test metotlarını yazacağımız CustomerControllerTests.cs sınıfını oluşturalım.

Asp..net core ile birlikte TestServer.cs diye bir sınıf hayatımıza girdi ve bu sınıf ile birlikte tıpkı gerçekten iis veya kestrel'de bir web-api uygulaması host edermiş gibi CustomerApi uygulamamızı integration testler için bir stup projesi ayağa kaldırmamıza olanak sunan bir sınıftır.

Bu sınıfı kullanabilmek için nuget üzerinden Microsoft.AspNetCore.TestHost kütüphanesini projemiz referanslarına ekleyelim. Kurulum tamamladıktan sonra constructor içerisinde aşağıdaki gibi testServer ve rest-call'ları yapmamızı sağlayacak olan httpClient imp. yapalım.

public class CustomerControllerTests
{
    private readonly HttpClient _client;

    public CustomerControllerTests()
    {
       var testServer = new TestServer(new WebHostBuilder()
        .UseStartup<TestStartup>()
        .UseEnvironment("Development"));
       _client = testServer.CreateClient();
    }
}

TestServer sınıfının initialize olması için IWebHostBuilder'a ihtiyacı var ve CustomerApi projemizde bulunan Startup.cs sınıfı uygulamayı ayağa kaldırırken bize bir IWebHostBuilder return etmekte. Bizimde amacımız bi CustomerApi projesi run etmek olduğundan IntegrationTest projemizin referanslarına CustomerApi projesini ekleyip Startup.cs'i kullanması gerektiğini belirtmek. Ancak mevcut test/qa database'ini kullanmak istemiyoruz, o database'i dirty data ile doldurmak işimize gelmez. Bunun için TestStartup adında Startup.cs den türeyen bir sınıf oluşturarak test için Entity Framework InMemoryDatabase özelliğini kullanabilmesini sağlayalım.

public class TestStartup : Startup
{
    public TestStartup(IConfiguration configuration) : base(configuration)
    {  }

    public override void ConfigureDatabase(IServiceCollection services)
    {
        services.AddDbContext<CustomerDbContext>(options =>
            options.UseInMemoryDatabase("customerDb_test"));
    }
}

İlk olarak httpPost isteği alarak customerInsert işlemi yapan end-point için test metodumuzu yazalım.

[Fact]
public async Task Post_Should_Return_OK_With_Empty_Response_When_Insert_Success()
{
    var expectedResult = string.Empty;
    var expectedStatusCode = HttpStatusCode.OK;

    // Arrange
    var request = new CustomerDto
    {
        FullName = "Caner Tosuner",
        CityCode = "Ist",
        BirthDate = new DateTime(1990, 1, 1)
    };
    var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");

    // Act
    var response = await _client.PostAsync("/api/customer", content);

    var actualStatusCode = response.StatusCode;
    var actualResult = await response.Content.ReadAsStringAsync();

    // Assert
    Assert.Equal(expectedResult, actualResult);
    Assert.Equal(expectedStatusCode, actualStatusCode);
}

FullName alanı boş gönderildiğinde throw edilen exception için integrationtest metodunu aşağıdaki gibi yazalım.

[Fact]
public async Task Post_Should_Return_FAIL_With_Error_Response_When_Insert_FullName_Is_Empty()
{
    var expected = "Fields are not valid to create a new customer.";

    // Arrange
    var request = new CustomerDto
    {
        FullName = string.Empty,
        CityCode = "Ist",
        BirthDate = new DateTime(1990, 1, 1)
    };
    var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");

    // Act
    var exception = await Assert.ThrowsAsync<Exception>(async () => await _client.PostAsync("/api/customer", content));
    
    // Assert
    Assert.Equal(expected, exception.Message);
}

Şimdi ise biraz daha karmaşık bir test metodu yazalım. Insert işlemi success olduktan sonra getAll metoduna call yaparak dönen listede bir adım önce insert ettiğimiz customer'ın olduğunu test eden metodu aşağıdaki gibi yazalım.

[Theory]
[InlineData("/api/customer", "/api/customer")]
public async Task Get_Should_Return_OK_With_Inserted_Customer_When_Insert_Success(string postUrl,string getUrl)
{
    var expectedResult = string.Empty;
    var expectedStatusCode = HttpStatusCode.OK;

    // Arrange-1
    var request = new CustomerDto
    {
        FullName = "Caner Tosuner",
        CityCode = "Ist",
        BirthDate = new DateTime(1990, 1, 1)
    };
    var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");

    // Act-1
    var response = await _client.PostAsync(postUrl, content);

    var actualStatusCode = response.StatusCode;
    var actualResult = await response.Content.ReadAsStringAsync();

    // Assert-1
    Assert.Equal(expectedResult, actualResult);
    Assert.Equal(expectedStatusCode, actualStatusCode);


            
    // Act-2
    var responseGet = await _client.GetAsync(getUrl);

    var actualGetResult = await responseGet.Content.ReadAsStringAsync();
    var getResultList = JsonConvert.DeserializeObject<List<CustomerDto>>(actualGetResult);

    var insertedCustomer = getResultList.Any(c => c.FullName == request.FullName);

    // Assert-2
    Assert.NotEmpty(getResultList);
    Assert.True(insertedCustomer);
}

Bütün api end-point'leri için ise aşağıdaki gibi bir test metodu yazarak sırasıyla insert, getAll, update, getbycityCode metotlarını uçtan uca olan integration testimizi yazabiliriz.

[Theory]
[InlineData("/api/customer", "/api/customer")]
public async Task Insert_GetAll_Update_GetByCityCode_Should_Return_Expected_Result(string postUrl, string getUrl)
{
    #region Insert
    var expectedResult = string.Empty;
    var expectedStatusCode = HttpStatusCode.OK;

    // Arrange-1
    var request = new CustomerDto
    {
        FullName = "Caner Tosuner",
        CityCode = "Ist",
        BirthDate = new DateTime(1990, 1, 1)
    };
    var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");

    // Act-1
    var response = await _client.PostAsync(postUrl, content);

    var actualStatusCode = response.StatusCode;
    var actualResult = await response.Content.ReadAsStringAsync();

    // Assert-1
    Assert.Equal(expectedResult, actualResult);
    Assert.Equal(expectedStatusCode, actualStatusCode);
    #endregion



    #region GetAll
    // Act-2
    var responseGet = await _client.GetAsync(getUrl);
    responseGet.EnsureSuccessStatusCode();

    var actualGetResult = await responseGet.Content.ReadAsStringAsync();
    var getResultList = JsonConvert.DeserializeObject<List<CustomerDto>>(actualGetResult);

    var insertedCustomerExist = getResultList.Any(c => c.CityCode == request.CityCode);

    // Assert-2
    Assert.NotEmpty(getResultList);
    Assert.True(insertedCustomerExist);
    #endregion



    #region Update
    // Arrange-3
    var insertedCustomer = getResultList.Single(c => c.CityCode == request.CityCode);
    var requestUpdate = new CustomerDto
    {
        FullName = "Ali Tosuner",
        CityCode = "Ist",
        BirthDate = new DateTime(1994, 1, 1),
        Id = insertedCustomer.Id
    };
    var contentUpdate = new StringContent(JsonConvert.SerializeObject(requestUpdate), Encoding.UTF8, "application/json");

    // Act-3
    var responseUpdate = await _client.PutAsync(postUrl, contentUpdate);
    responseUpdate.EnsureSuccessStatusCode();
    var updateActualResult = await responseUpdate.Content.ReadAsAsync<CustomerDto>();

    // Assert-3
    Assert.Equal(updateActualResult.FullName, requestUpdate.FullName);
    #endregion



    #region GetByCityCode
    // Act-2
    var responseGetByCityCode = await _client.GetAsync("/api/customer/getbycitycode/"+requestUpdate.CityCode);
    responseGetByCityCode.EnsureSuccessStatusCode();

    var actualGetByCityCodeResult = await responseGetByCityCode.Content.ReadAsStringAsync();
    var getByCityCodeResultList = JsonConvert.DeserializeObject<List<CustomerDto>>(actualGetByCityCodeResult);

    var updatedCustomerExist = getByCityCodeResultList.Any(c => c.CityCode == request.CityCode);

    // Assert-2
    Assert.NotEmpty(getByCityCodeResultList);
    Assert.True(updatedCustomerExist);
    #endregion

}

Yazdığımız bütün testleri run ettiğimizde ise aşağıdaki gibi hepsinin pass statüsünde olduğunu görebiliriz.

Yazının başında da belirtiğimiz gibi test yazmak projelerimiz için oldukça önemlidir. Birbirine bağlı yapılar için integration test yazarak uçtan uca olan bütün process'i test edip less bug, more refactoring için çok önemli bir adım atmış oluruz. Tabi bunu sadece integration test yazarak değil beraberinde unit-test metotlarınıda yazarak yapmamız çok daha keyifli olur.

Source Code

.Net Core Windows Service Çalışabilen Uygulama Geliştirme Topshelf

Windows servisler herhangi bir arayüzü olmadan işletim sisteminde background'da uzun süreli hiç durmaksızın çalışması gereken uygulamalar yaratmak için faydalanabileceğimiz yöntemlerden biridir. Windows servisler herhangi bir manuel start stop işlemi gerektirmeksizin üzerinde çalıştıkları işletim sistemi ile birlikte start-stop olabilecek şekilde konfigüre edilirler.

Windows service şeklinde çalışan projeler oluşturmak için birden fazla yöntem bulunmaktadır ancak open-source geliştirilen Topshelf kütüphanesi kalitesi ve .Net Core uyumluluğuyla oldukça ön plana çıkmakta. 

Bu yazıda bir .net core console uygulamasını topshelf kullanarak Windows Service olarak nasıl host edebiliriz bunu inceleyeceğiz. 

Topshelf .net framework/.net core kullanılarak geliştirilen console uygulamalarını windows service olarak install ve host etmeye yarayan bir open-source kütüphanedir. Kütüphane  service class mimarisi karmaşasından uzak sadece bir kaç class implementasyonu ile console uygulamalarını windows service olarak host etmemize olanak sağlar.

Örnek bir uygulama üzerinden anlatacak olursak;

İlk olarak vs'da NetCoreTopshelf.Sample adında bir .net core console app. oluşturalım.

Sonrasında nuget üzerinden hali hazırda develope branch'i de olsa Topshelf'i proje referanslarına ekleyelim.

Sırada Windows Service Logic bilgisini içeren HelloWorldServiceHost.cs sınıfını aşağıdaki gibi oluşturalım.

public class HelloWorldServiceHost
{
    public void Start()
    {
        Console.WriteLine("Hello World Service Started !!");
    }

    public void Stop()
    {
        Console.WriteLine("Hello World Service Stopped !!");
    }
}

Uygulama kod kısmı ile ilgili son kısım ise Windows Service configure etmek kaldı. Program.cs içerisindeki Main metodunda aşağıdaki gibi uygulamamızı windows service registration yaparken gerekli olan konfigurasyonları belirtelim.

static void Main(string[] args)
{
    HostFactory.Run(hostConfig =>
    {
        hostConfig.Service<HelloWorldServiceHost>(serviceConfig =>
        {
            serviceConfig.ConstructUsing(() => new HelloWorldServiceHost());
            serviceConfig.WhenStarted(s => s.Start());
            serviceConfig.WhenStopped(s => s.Stop());
        });
        hostConfig.RunAsLocalSystem();
        hostConfig.SetServiceName("Hello World Service");
        hostConfig.SetDisplayName("Hello World Service Host");
        hostConfig.SetDescription("Hello World Service Host using .Net Core and Topshelf.");
    

Windows service olarak exe install etmemiz gerekmekte ancak .net core uygulamaları default'da executable bir output üretmemekte. Bunu yapabilmemiz için extradan proje csproj dosyasına gidip aşağıdaki gibi .exe çıktısı üretmesini sağlatacak olan RuntimeIdentifier kod satırını ekleyelim.

<RuntimeIdentifier>win7-x64</RuntimeIdentifier>

 

Uygulama geliştirmemiz bitti. Tek yapmamız gereken exe'yi service olarak install etmek. Bunun için NetCoreTopshelf.Sample.exe dosyasının bulunduğu path'e gidip aşağıdaki gibi administrator olarak çalıştırdığımız Command Prompt'ta ilgili install komutunu çalıştıralım. 

 NetCoreTopshelf.Sample.exe install

Uygulamamız windows service olarak çalışmaya başladı. Emin olmak için bilgisayarınızdan Task Manager'a gidip Services tab'ının altında aşağıdaki gibi HelloWorld ismini göreceksinizdir.

 

Service'i uninstall etmek içinse NetCoreTopshelf.Sample.exe uninstall komutunu çalıştırmanız yeterli.

NetCoreTopshelf.Sample.exe uninstall

Yazının başında da belirtiğimiz gibi windows-service çalışan uygulamalar geliştirmenin çeşitli yolları vardır ancak Topshelf kütüphanesi kullanarak bu uygulamaları geliştirmek oldukça hızlı ve basit bir seçenek olarak karşımıza çıkmakta. Özellikle .Net Core için konuşmak gerekirse bugün itibariyle windows-service olarak çalışabilen self-hosted uygulamalar oluşturmak pek kolay gibi görünmesede Topshelf ile bunu yapabilmek mümkün mümkün.

Source Code

.Net Core Kafka Kurulum ve Producer Consumer Kullanımı

Daha önceki fire-and-forget yapılarını incelerken rabbitmq üzerinde masstransit kullanarak anlatıp örnek projeler ile incelemiştik. Bu yazımızda ise .Net Core uygulamarında apache kafka kullanımına değineceğiz. 

Messaging queue yapıları ana uygulamanızın yükünü azaltmak ve microservice mimarisinin fire-and-forget yapılarının en yaygın çözümlerinden biri olarak yazılım geliştirme hayatımızda yer edinmekte. Apache Kafka ise bu yapılardan biri olarak open source geliştirilen distributed, scalable ve high-performance sunabilen bir publish-and-subscribe message broker dır. High volumes of data yani oldukça yüksek hacimli verileri işleyebilmek adına kullanabileceğimiz teknolojilerin başında gelmektedir.

Architecture

Apache Kafka'nın mimarisine ve terminolojide geçen terimlere bakacak olursak;

Kafka bir veya birden fazla sunucu üzerinde bir cluster oluşturarak çalışır ve kafka üzerindeki her bir record key-value ve timestamp bilgileri kullanılarak topic olarak adlandırılan kategoriler içerisinde store edilir.

Kafka basic olarak aşağıdaki 4 ana başlıktan oluşur;

  • Cluster : broker olarak adlandırılan bir veya birden fazla server'ların yer aldığı collection.
  • Producer : message'ları publish eden yani kafka'ya message üreten yapının/uygulamanın adı.
  • Consumer : publish edilmiş message'ları retrieve/consume eden uygulama.
  • Zookeper : distributed olarak multiple instance çalışan uygulamaları koordine etmede kullanılan bir uygulamadır.

Yukarıda da bahsettiğimiz gibi kafka'da her bir data => message olarak adlandırılır. Kafka her bir mesajı byte array'ler şeklinde key-value olarak timestamp bilgisi ile saklar. Her bir kafka server'ı broker olarak adlandırılır.  Producer-consumer ve cluster'lar arası iletişim TCP protokolü ile kurulur ve cluester'a yeni broker'lar ekleyerek kafka'yı horizontal olarak scale edebiliriz.

 

Producer ilgili message'ları kafkaya push eder ve kafka mesajları partition dediğimiz sıralı mesaj dizinleri olarak dinamik bir şekilde daha önceden kendisine subscribe olmuş consumer'lar tarafından alınmak üzere hazırlar ve sırası geldiğinde consume edilir.

Installlations

Surce code bölümünde docker-compose dosyasını run ederekte kurulumları yapabilirsiniz ancak biz local makinada teker teker manuel olarak kurulumları yapalım. Kafka'nın java ve zookeper dependency'leri bulunmakta ve bunun için ilk olarak makinamıza JRE8 ve Zookeper yüklememiz gerekmekte. 

  • JRE 8 Installation

İşletim sistemi versiyonunuza göre bu adresten JRE8'i indirip kuralım.

  • Zookeeper Installation

JRE'den sonra bu adresten zookeeper'ın son stable versiyonunu indirip kuralım.

Zookeeper indirdikten sonra C dizinine dosyaları çıkartalım "C:\zookeeper-3.4.13”. Daha sonra config klasörü içerisinde bulunan zoo_sample.cfg dosyasının ismini zoo.cfg olarak değiştirelim. Sonrasında bu dosyanın içine gidip dataDir'e settings'ini dataDir=/data olarak güncelleyelim.

Son olarak ise işletim sistemi system variable'larına hem JAVA_HOME'u hemde ZOOKEEPER_HOME'u tanımlamak var. Bunun için makinanızda system variable'larına aşağıdaki gibi JAVA_HOME ve ZOOKEEPER_HOME variable'larını tanımlayıp Path bölümüne de bunların path bilgilerini geçelim.

JAVA_HOME  => %JAVA_HOME%\bin

ZOOKEEPER_HOME = > %ZOOKEEPER_HOME%\bin

 Zookeper server'ı run etmek için command prompt'tan olarak zkserver yazmak yeterli.

 

  • Kafka Installation

Kafka için bu adresten kafkanın binary dosyalarını inderelim ve ilgili dosyaları C:/kafka dizinie exract edelim. Powershell yada cmd kullanarak kafka dizinine gidip şu komutu çalıştıralım;

 .\bin\windows\kafka-server-start.bat ./config/server.properties

bu komutla birlikte kafka çalışmaya başlayacaktır.

Bütün kurumlarımızı tamamladık şimdi sırada Producer ve Consumer uygulamalarını oluşturmak var.

Application

Yazının başında da söylediğimiz gibi bir .Net Core uyglaması üzerinde kafka kullanacağız ve örnek proje olarak, email göndermede kullanılan bir producer-consumer uygulaması geliştirelim. İlk olarak aşağıdaki gibi Kafka message'ını tanımlayalım. Bu message sınıfı hem consumer hemde producer tarafından kullanılacağından solution'da Kafka.Message adında bir .Net Core class-library projesi içerisinde tanımlı olsun.

1) Kafka.Message

public class EmailMessage:IMessageBase
{
    public string To { get; set; }
    public string Subject { get; set; }
    public string Content { get; set; }
}

Bu message sınıfına ait verileri producer tarafından kafka'da bulunan emailmessage-topic adındaki topic collection'ına bırakılacak. Yine aynı solution'da Kafka.Producer adında bir console application oluşturalım.

2) Kafka.Producer

Nuget üzerinde kafka client olarak kullanılabilecek belli başlı bazı kütüphaneler bulunmakta. .Net Core uyumluluğu açısından biz örnek projede Confluent.Kafka client'ını kullanacağız. Her ne kadar beta versiyonu olsada github-rating'leri bakımından oldukça beğenilen bir kütüphanedir.

Install-Package Confluent.Kafka -Version 1.0-beta

Producer'da belirtilen topic için kafka ya message push etmede kullanacağımız IMessageProducer interface ve implementasyonunu aşağıdaki gibi tanımlayalım ve kullanım olarakda Program.cs içerisinde Main func'da Produce metodunu call ederek EmailMessage'ını push edelim.

public interface IMessageProducer
{
    void Produce(string topic, IMessageBase message);
}

public class MessageProducer : IMessageProducer
{
    public void Produce(string topic, IMessageBase message)
    {
        var config = new ProducerConfig { BootstrapServers = "localhost:9092" };

        using (var producer = new Producer<Null, string>(config))
        {
            var textMessage = JsonConvert.SerializeObject(message);
           
            producer.BeginProduce(topic, new Message<Null, string> { Value = textMessage }, OnDelivery);

            // wait for up to 10 seconds for any inflight messages to be delivered.
            producer.Flush(TimeSpan.FromSeconds(10));
        }
    }

    private void OnDelivery(DeliveryReportResult<Null, string> r)
    {
        Console.WriteLine(!r.Error.IsError ? $"Delivered message to {r.TopicPartitionOffset}" : $"Delivery Error:{r.Error.Reason}");
    }
}

static void Main(string[] args)
{
    IMessageProducer messageProducer = new MessageProducer();

    //produce email message
    var emailMessage = new EmailMessage
    {
        Content = "Contoso Retail Daily News Email Content",
        Subject = "Contoso Retail Daily News",
        To = "all@contosoretail.com.tr"
    };
    messageProducer.Produce("emailmessage-topic", emailMessage);
    
    Console.ReadLine();
}

Dilerseniz topic oluşturma ve message produce işlemlerini command-prompt üzerinden de yapabilirsiniz, biz örnek projede için kafka client kullanarak topic oluşturduk.

3. Kafka.Consumer

Consumer projeside kafka da emailmessage-topic'ine push edilen message'ları consume edip ilgili business'ları process eden uygulamamız olacaktır. Bunun için solution'da Kafka.Consumer adında bir Console Application oluşturalım ve yine nuget üzerinden Confluent.Kafka kütüphanesini projemiz referanslarına ekleyelim.

Kurulumu tamamladıktan sonra consume işleminde kullanacağımız abstract MessageConsumerBase sınıfını aşağıdaki gibi tanımlayalım.

public abstract class MessageConsumerBase<IMessage>
{
    private readonly string _topic;

    protected MessageConsumerBase(string topic)
    {
        this._topic = topic;
    }

    public void StartConsuming()
    {
        var conf = new ConsumerConfig
        {
            GroupId = "emailmessage-consumer-group",
            BootstrapServers = "localhost:9092",
            AutoOffsetReset = AutoOffsetResetType.Earliest
        };

        using (var consumer = new Consumer<Ignore, string>(conf))
        {
            consumer.Subscribe(_topic);

            var keepConsuming = true;
            consumer.OnError += (_, e) => keepConsuming = !e.IsFatal;

            while (keepConsuming)
            {
                try
                {
                    var consumedTextMessage = consumer.Consume();
                    Console.WriteLine($"Consumed message '{consumedTextMessage.Value}' Topic: {consumedTextMessage.Topic}'.");

                    var message = JsonConvert.DeserializeObject<IMessage>(consumedTextMessage.Value);

                    OnMessageDelivered(message);
                }
                catch (ConsumeException e)
                {
                    OnErrorOccured(e.Error);
                }
            }

            // Ensure the consumer leaves the group cleanly and final offsets are committed.
            consumer.Close();
        }
    }

    public abstract void OnMessageDelivered(IMessage message);

    public abstract void OnErrorOccured(Error error);
}

Bu base sınıfı inherit almış EmailMessageConsumer sınıfı StartConsuming() metodunu call ederek consume etmeye başlamasını sağlayan kod bloğunu Program.cs içerisinde aşağıdaki gibi tanımlayalım.

public class EmailMessageConsumer : MessageConsumerBase<EmailMessage>
{
    public EmailMessageConsumer() : base("emailmessage-topic") { }

    public override void OnMessageDelivered(EmailMessage message)
    {
        Console.WriteLine($"To: {message.To} \nContent: {message.Content} \nSubject: {message.Subject}");

        //todo email send business logic
    }

    public override void OnErrorOccured(Error error)
    {
        Console.WriteLine($"Error: {error}");

        //todo onerror business
    }
}

static void Main(string[] args)
{
    Console.WriteLine("Consumer Started !");

    var emailMessageConsumer = new EmailMessageConsumer();
    emailMessageConsumer.StartConsuming();
    
    Console.ReadLine();
}

Örnek uygulama geliştirmemiz bitti. Önce producer ardında consumer projelerini sırasıyla run edip producer tarafından üretilen mesajın kafka üzerinden consumer tarafından consume edilip data-transfer'in sağlandığını görebilirsiniz.

Kafka günümüz itibariyle rakiplerine gore data-transmission'ı daha hızlı ve performanslı olması açısından özellikle real-time streaming uygulamalar için en iyi çözüm olarak kabul edilmekte. RabbitMQ, MSMQ, IBM MQ ve Kafka gibi messaging yapılarının arasından neden kafka diye sorduğumuzda; kafka özellikle huge-amount-of-data transfer söz konusu olduğunda (örnek olarak: IOT ve Chat yapıları ) sektör tarafından en iyi seçenek olarak kabul edilmekte. Eğer uygulamanız hızlı ve scalable bir message-broker'a ihtiyaç duyarsa kafka müthiş bir seçenek olacaktır.

Source Code

Asp.Net Core Hangfire Kullanarak Background Task İşlemleri

Projelerimizde olağan akışında ilerlerken veya bir business rule çalışırken mevcut akışı durdurmadan asenkron bir şekilde uygulamadan bağımsız çalışmasını istediğimiz process'ler olmuştur. Bu gibi ihtiyaçları karşılaması için Azure olsun Google-Cloud yada Amazon olsun kendi cloud çözümlerini üreterek kullanmamıza olanak sağlamaktadırlar.

Peki ya on-premise dediğimiz kurum içi yada domain içi çözüm olarak neler yapabiliriz ? Fire-and-forget (messaging queue) yapılarından birini kullanabilir veya mevcut uygulamada background job'lar üretebiliriz.

Bu yazımızda Asp.net Core uygulamalarında Hangfire kullanarak background-task'lar nasıl oluşturulur inceleyeceğiz.

Hangfire 

Özetle; open-source olarak geliştirilmiş schedule edilebilen process'lerin kolay bir şekilde yönetimini sağlayan bir kütüphanedir. Sahip olduğu dashboard ile job'larınızı historical olarak görüntüleyebilir, start-stop/restart gibi işlemler yapabilirsiniz.

An easy way to perform background processing in .NET and .NET Core applications. No Windows Service or separate process required.

Hangfire job'ları yönetirken storage alanı olarak hemen hemen bütün database türleri için destek sağlamaktadır. SQL Server, Redis, PostgreSQL, MongoDB etc.

İlk olarak uygulamamızda kullanmak üzere local Sql Server üzerinde Hangfire adında bir databse oluşturalım.

Sonrasında Vs.'da BackgroundTaskWithHangfire adında bir Api projesi oluşturalım ve nuget üzerinden bugün için en güncel olan Hangfire v1.6.20 paketini projemize ekleyelim. 

PM> Install-Package Hangfire

İlk başta oluşturduğumuz database'in conn string bilgilerini appSettings.json dosyasına ekleyelim.

  "HangfireDbConn": "Server=.;Initial Catalog=Hangfire;Persist Security Info=False;User ID=HangfireUser;Password=qwerty135*;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=True;Connection Timeout=30;"

Sırada uygulama üzerinde hangfire configuration'ı var. Bunun için Startup.cs de bulunan ConfigureServices metodu içerisinde Hangfire'ı service olarak ekleyelim ve sonrasında Configure metodu içerisinde bu service'i kullanacağımızı belirten kod bloklarını yazalım.

public void ConfigureServices(IServiceCollection services)
{
    services.AddHangfire(_ => _.UseSqlServerStorage(Configuration.GetValue<string>("HangfireDbConn")));
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseHangfireDashboard();
    app.UseHangfireServer();
}

Yukarıdaki kod bloğunda hangfire storage bilgisini vererek uygulamamız service'lerine register ettik ve devamındada hangfire'ın server ve dashboard service'lerini kullanacağımızı belirttik.

Hangfire dashboard default olarak uygulamanın çalıştığı portta' http://<application>/hangfire adresinde host edilir. Bizde localhostta çalıştığımızdan uygulamayı run edip browser üzerinden http://localhost/hangfire adresine gittiğimizde aşağıdaki gibi hangfire dashboard'u görüntüleyebiliriz.

Dashboard'la birlikte uygulama her start olduğunda db'de ilgili tablolar oluşmuşmu diye check ederek oluşmadıysa tabloları oluşturur. Hangfire tablolarını sql server management studio üzerinden görüntüleyebilirsiniz.

 

Hangfire konfigurasyonunu tamamladık şimdi sırasıyla Hangfire kütüphanesinde bulunan BackgroundJob sınıfını kullanarak oluşturabileceğimiz job türlerine bakacak olursak;

1- Fire-and-Forget Jobs

            Job create edildikten sonra çalışır ve process olur.

public class FireAndForgetJob
{
    public FireAndForgetJob()
    {
        //Fire and forget
        var jobId = BackgroundJob.Enqueue(() => ProcessFireAndForgetJob());
    }

    public void ProcessFireAndForgetJob()
    {
        Console.WriteLine("I am a Fire and Forget Job !!");
    }
}

2- Delayed Jobs

            Belli bir zaman bilgisi set edilerek sadece bir kez çalışmasını istediğimiz task'lar için kullanabileceğimiz job türü. Aşağıdaki gibi Job register olduktan 4 dkka sonra çalışacaktır.

public class DelayedJob
{
    public DelayedJob()
    {
        //Delayed job
        var jobId = BackgroundJob.Schedule(() => ProcessDelayedJob(), TimeSpan.FromMinutes(4));
    }

    public void ProcessDelayedJob()
    {
        Console.WriteLine("I am a Delayed Job !!");
    }
}

3- Recurring Jobs

            Recurring yani tekrar eden task'lar için kullanılan job türü. Örneğin; her saat başı çalışmasını istediğiniz bir job'a ihtiyacınız olduğunda aşağıdaki gibi tanımlayabiliriz.

public class Recurring_Job
{
    public Recurring_Job()
    {
        //Recurring job
        RecurringJob.AddOrUpdate(() => ProcessRecurringJob(), Cron.Hourly);
    }

    public void ProcessRecurringJob()
    {
        Console.WriteLine("I am a Recurring Job !!");
    }
}

4- Continuations Jobs

            Parent-child ilişkisinin olduğu yani bir job'ın çalışması için başka bir job'ın tamamlanmasını bekleyip o Cmplete olduktan sonra çalışmasını istediğimiz işler için kullanabileceğimiz job türü.

public class ContinuationsJob
{
    public ContinuationsJob()
    {
        //Delayed job
        var parentJobId = BackgroundJob.Schedule(() => Console.WriteLine("I am a Delayed Job !!"), TimeSpan.FromMinutes(4));


        //Continuations job
        BackgroundJob.ContinueWith(parentJobId, () => ProcessContinuationsJob());
    }

    public void ProcessContinuationsJob()
    {
        Console.WriteLine("I am a Recurring Job !!");
    }
}

Projeyi run edip tekrardan  http://localhost/hangfire adresine gittiğimizde ilgili job türlerine ait bilgileri dashboard'da görüntüleyebiliriz.

Özetlemek gerekirse; uygulamanızda çalıştırmanız gereken background-task'ları için Hangfire implementasyonunu hızlı bir şekilde yapıp dashboard'u ile birlikte kolayca kullanabilirsiniz. Eğer .net core'un kendi background task sınıfını kullanarak ilerlemek isterseniz hangfire'a göre daha zorlu bir süreç sizi bekliyor olacaktır. Hem yönetilebilirlik açısından hemde visualization olarak hangfire kesinlikle sizin için daha sorunsuz ve kullanışlı bir çözüm olacaktır. Hangfire'ın muadili olan Quartz.net veya bir queue çözümü de kullanarak işlemlerinizi yapabilirsiniz.

Source Code

Entity Framework Core Data Concurrency Optimistic Lock

Data Concurrency yönetimi server-side bir projede oldukça büyük önem arzetmektedir. Kullanıcıya hem doğru hemde fresh dediğimiz bayat olmayan veriyi Concurrency conflicts'lerine yol açmadan ulaştırıyobilmek gerekir. Concurrency conflict'leri dediğimiz durum Optimistic Lock Nedir ? Pessimistic Lock Nedir ?

Data concurrency yazımızdada bahsettiğimiz üzre;

Bir internet sitesinde kayıtlı bulunan adres bilginizi güncellemek istiyorsunuz. Aynı anda 2 farklı bilgisayardan bilgileri güncelle sayfasını açtınız ve adresiniz o an "Samsun" olarak kayıtlı yani 2 ekranda da "Samsun" yazıyor. İlk bilgisayarda bulunan kişi adres bilgisini "Ankara" olarak değiştirdi ve güncelle butonuna basıp bilgiyi güncelledi.

İkinci ekranda bulunan kişi ise ekranda halen "Samsun" yazılı iken adres bilgisini "İstanbul" olarak değiştirdi ve güncelle butonuna basıp bilgiyi güncelledi. Ekranda yazan "Samsun" kaydı artık bizim için bayat bir kayıttır ve birinci kullanıcı değişikliği "Samsun" => "Ankara" yaptığını düşünürken ikinci kişi bu değişikliği "Samsun" => "İstanbul" yaptığını düşünüyor. Halbuki gerçekte olan ikinci kişi adres bilgisi ekranda "Ankara" iken => "İstanbul" olarak değiştirmiş oldu.

Kısaca Last In Wins yani son gelen kazanır. Bu gibi durumlara yol açmamak adına kullanmış olduğunuz ORM çeşidine göre farklı çözümler sunulmakta. Data Concurrency'yi sağlayabilmek adına genellikle üzerinde işlem yapılan data'ya lock işlemi uygulanır. Locking işlemi için 2 farklı yaklaşım vardır. Pessimistik Lock ve Optimistic Lock.

Bu yazımızda ise Entity Framework Core kullandığımız bir projede Concurrency sağlamak adına neler yapabiliriz değineceğiz.

Entity Framework Core Concurrency Conflict'lerini engel olmak adına 2 seçenek sunmakta.

  1. Mevcut Property'i bir Concurrency Token attribute'ü ile konfigüre etmek,
  2. RowVersion oluşturarak tıpkı bir concurency token gibi davranmasını sağlamak. 

Property Based Configuration (ConcurrencyCheck Attribute)

Property'ler ConcurrencyCheck attribute'ü kullanılarak o property için bir concurrency token oluşturmasını sağlar ve conflict'lere engel olmamıza olanak sağlar.

public class Customer
{
    [ConcurrencyCheck]
    public string FullName { get; set; }
    public string CityCode { get; set; }
    public DateTime BirthDate { get; set; }
}

Concurrency Token tanımlamanın bir diğer yoluda mapping oluştururken property için bu bilgiyi IsConcurrencyToken() metodunu kullanarak set etmek.

public class CustomerDbContext : DbContext
{
    public DbSet<Customer> Customers { get; set; }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
         modelBuilder.Entity<Customer>()
            .Property(a => a.FullName).IsConcurrencyToken();
    } 
}

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

Yukarıda yaptığımız konfigurasyonlar sonucunda model'i retrieve ederken almış olduğumuz concurrency token bilgisi her bir Update ve Delete query'si için where koşuluna eklenir. Execution sırasında Entity Framework where koşuluna eklediği bu token bilgisi konfigüre edilmiş kolonlardan birisi data'nın retrieve edildiği ve update işlemi gönderildiği zaman diliminde değiştirilmişse DbUpdateConcurrencyException throw eder.

Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded.

Test ettiğimizde update işlemi gönderdiğimiz sırada data retrieve anında db de ilgli alanı manuel olarak update ettikten sonra devam ettiğimizde aşağıdaki gibi hata alırız. 

Concurrency token kullanarak modelinizde bulunan property'ler için tanımlamalar yaparak data-concurrency'i sağlayabilirsiniz ancak unutmamalıyız ki aynı modelde bulunan çok fazla property için(more than 20) kullanmak where koşulunda belirtilen condition'ları arttırmak demektir ve buda execution süresinin artmasına sebep olacaktır. 

RowVersion Column

Data Concurrency sağlamak adına kullanılabilecek ikinci yöntem ise Rowversion kullanmak. İlgili tabloya RowVersion adında yeni bir column ekleyerek o row için geçerli bir concurrency token yada version bilgisi store ederek. Concerrency Conflict oluşmasına engel olabiliriz. RowVersion incremental olması gerekmekte bu sebeple numeric bir değeri olmalı. Örnek olarak ; userA ve userB aynı model'i rowVersion 1 olarak retrieve ettiler ancak userA bir update işlemi yaparak rowVersion'ı 2 yaptı. userB update işlemi gönderdiğinde Entity Framework Core DbUpdateConcurrencyException throw ederek conflict hatası verecektir ve update gerçekleşmeyecektir. 

RowVersion property'sinin tipi byte array olması gerekmekte ve TimeStamp data annotations attribute'ü kullanarak konfigüre edilebilmekte.

public class Customer
{
    public string FullName { get; set; }
    public string CityCode { get; set; }
    public DateTime BirthDate { get; set; }
    [TimeStamp]
    public byte[] RowVersion { get; set; }
}

Fluent Api kullanarak yapmak istersekte aşağıdaki gibi mapping sırasında bu bilgiyi set edebiliriz.

public class CustomerDbContext : DbContext
{
    public DbSet<Customer> Customers { get; set; }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
         modelBuilder.Entity<Customer>()
            .Property(a => a.RowVersion).IsRowVersion();
    } 
}

public class Customer
{
    public string FullName { get; set; }
    public string CityCode { get; set; }
    public DateTime BirthDate { get; set; }
    public byte[] RowVersion { get; set; }
}

Yine yukarıdaki gibi projeyi çalıştırıp 2 farklı session'da data get işleminden ilgili row'Da bulunan column'lar dan herhangi birini başarılı bir şekilde update edip sonra diğerinde update işlemi gönderdiğimizde entity franework tarafından data-conflict olduğunu söyleyen DbUpdateConcurrencyException throw edildiğini göreceksinizdir.

Bu exception'ı bir try-catch bloğu kullanarak handle edebilir ve ikinci kullanıcıya anlamlı bir error-message göstererek tekrardan en güncel olan kaydı gösterip ardından yeniden update işlemi yapmasını sağlayabilirsiniz.

Asp.Net Core HTTP.sys Web Server Kullanımı

Http.sys, IIS bağımsız olmasını istediğiniz asp.net core uygulamarı için kullanılan windows-only bir web server dır. Asp.Net core 1.x versiyonlarında WebListener olarak karşımızdayken 2.x ile birlikte HTTP.sys olarak değiştirildi.

Kestrel'e alternatif olmakla birlikte kestrel'de bulunmayan bazı feature'lara da sahip. Bunlardan bazıları;

  • Windows Authentication
  • Port sharing
  • HTTPS with SNI
  • HTTP/2 over TLS (Windows 10 or later)
  • Direct file transmission
  • Response caching
  • WebSockets (Windows 8 or later)
  • Supported Windows versions:
  • Windows 7 or later => Windows Server 2008 R2 or later

Asp.Net Core uygulamaları için Kestrel best choise olarak önerilsede yukarıda da belirttiğimiz gibi sahip olduğu bazı özellikler bakımından kestrel'in önüne geçebilmektedir. Uygulama doğrudan HTTP.sys üzerinde built olduğundan kestrel'de olduğu gibi bazı attack'alrdan korunmak adına bir reverse proxy server'a ihtiyaç duyulmamaktadır ve sunucunun güvenliğini ve ölçeklenebilirliğini yönetebildiğinden bir çok saldırı türüne karşı uygulamayı koruyabilmektedir.

Kullanım olarak bakacak olursak;
Program.cs içerisinde WebHost konfigurasyonlarını tanımlarken uygulamamızın HttpSys üzerinde run edileceğini aşağıdaki gibi belirtmemiz gerekmekte. Bunun için UseHttpSys metodunu kullanacağız. Bu metoda erişemezseniz nuget üzerinden Microsoft.AspNetCore.Server.HttpSys paketini install etmeniz gerekmekte.

public class Program
{
    public static void Main(string[] args)
    {
        CreateWebHostBuilder(args).Build().Run();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>()
            .UseHttpSys(options =>
            {
                options.UrlPrefixes.Add("http://localhost:4440");
                options.Authentication.Schemes = AuthenticationSchemes.NTLM;
                options.Authentication.AllowAnonymous = false;
                options.MaxRequestBodySize = 30000000;
                options.MaxConnections = 100;
            });
}

Uygulama HTTPsys server üzerinde 4440 portunda çalışacaktır. Uygulamayı run edebilmek için vs'da default IISExpress olan run profile'ını launchsettings.json dosyasında da tanımlı olan profile'ı aşağıdaki resimde olduğu gibi HTTPsysServer yapıp run diyelim.

Run dedikten sonra uygulama ilgili bir kaç bilginin bulunduğu bir console ekranı açılacaktır. Browser üzerinden belirtmiş olduğumuz adrese giderek uygulamaya kolayca erişebiliriz.

Yazının başında da belirttiğimiz gibi, HTTPsys, Asp.Net Core 2.0 ile birlikte IIS bağımsız uygulamalar geliştirmek istediğimizde Kestrel'e alternatif olarak karşımıza çıkmakta ve sahip olduğu bazı özellikler bakımından Kestrel'in yerini de alabilmekte. Tek can sıkıcı noktası Windows-only olsada performans ve security açısından oldukça faydalı bir option olarak seçenekler arasında bulunmakta.

HTTPsys ile ilgili daha detaylı bilgilere buradan ulaşabilirsiniz.

Asp.Net Core Https Kullanımı

Klasik Asp.Net'den [RequireHttps] attribute'ü kullanarak uygulayabildigimiz Https Asp.Net Core 1.1 ile gelmiş olsada konfigüre edilebilmesi oldukça zahmetli bir haldeydi. 2.0 ile ufak bir dokunuş daha yaptılar ancak asıl olması gereken yere 2.1 sürümü ile geldi desek çok yanlış olmaz. Asp.Net Core 2.1 ile Https configure ve redirect etme işlemleri oldukça basit bir şekilde yapılabilmekte.

.Net Core 2.1 kullanarak vs'da bir Asp.Net Core Web Api uygulaması oluşturduğumuzda Kestrel'in dinlediği 2 url default olarak uygulamada gelmekte.  https://localhost:5001 ve http://localhost:5000 .

Startup.cs içerisinde set edebileceğimiz Https zorunlu hale getiren ve redirect işlemini yapabilmemizi sağlayan bir kaç middleware bulunmakta.

İlk middleware UseHsts()

app.UseHsts();

Bu middleware; man-in-the-middle ataklarına karşı HSTS (HTTP Strinct Transport Protocol)'i aktif eder. Browser'a header'da belli zaman aralıklarında sertifikayı cache'lemesini söyleyerek belirtilen time-range'in dışında sertifika değişip değişmediğini kontrol etmekte.

Bir diğer middleware ise UseHttpsRedirection() 

app.UseHttpsRedirection();

Bu middleware ise http://localhost:5000'e gelen istekleri https://localhost:5001 adresine redirect eder.

Uygulamamızla ilgili http konfigurasyonları yaptık ancak sertifika eksik. Bunun için development mode'da sertifika satın almadan V.S. 2017 kullanılarak dummy bir sertifika oluşturulabilir. Production için ise ilgili sertifikayı satın aldıktan sonra Windows Certificate Store'a install edebilir yada proje deploy dosyaları arasında saklayabiliriz.

Asp.Net Core uygulamanızın Https connection sağlamak için diskte bulunan file-certificate'i kullanmasını sağlayabiliriz.

Bunun için Program.cs içerisinde bulunan CreateWebHostBuilder metodunda proje oluşturulurken gelen default konfigurasyon buunmakta. 

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
    .UseStartup<Startup>();

CreateWebHostBuilder metodu oldukça customise edilebilen bir metot ve dilersek bunu aşağıdaki gibi konfigüre ederek Kestrel'e hangi portları dinleyeceğini söyleyip hangisinde Https sertifika tanımlaması yapacağını söyleyebiliriz.

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseKestrel(options =>
        {
            options.Listen(IPAddress.Loopback, 5000);
            options.Listen(IPAddress.Loopback, 5001, listenOptions =>
            {
                listenOptions.UseHttps("certificate.pfx", "myAppCertificate");
            });
        })
        .UseStartup<Startup>();

Kestrel üzerinde çalışan uygulamamız için 5000 ve 5001 portunu dinle, 5001 portu için ilgili Https tanımlamasını baz al. Bu tanımlamaları Asp.Net Core 2.1 ile birlikte projede yer alan launchSettings.json dosyasında da yapabildiğimizi unutmayalım.

Basitçe Asp.Net core uygulamalarında Https kullanımı nasıl olur çok fazla derine inmeden anlatmaya çalıştık. Daha fazla detay için bu adresten faydalanabilirsiniz.

Asp.Net Core Unit Testing Database and Repository, In Memory Database Kullanımı

Asp.Net Core uygulamalarında unit test nedir nasl yazılır gibi konulara daha önceki yazımızda değinmiştik. O yazıdaki örnekte controller ve service layer'lar için nasıl unit test metotları yazabiliriz öğrenmiştik. Bu yazımızda ise bir diğer layer olan repository-layer için entity framework kullanılan bir projede nasıl unit testler yaratabiliriz inceleyeceğiz.

Entity framework core'un klasik entity framework'e kıyasla oldukça performanslı olmasıyla birlikte bazı artılarının olduğundan bahsetmiştik. Bu artılardan birisi de in-memory database option sunması (EF 6.1 ve sonrası içinde mevcut). Bu feature'dan önce repository'ler için unit test metotları yazmak istediğimizde Entity'lerin bulunduğu fake DbSet oluşturarak fake database ve tablolarını yaratmamız gerekiyordu. Yukarıda da belirttiğimiz üzre entity framework core ile birlikte in-memory database oluşturarak kolayca unit test sınıfları oluşturabiliriz.

Örnek projemiz üzerinden ilerleyecek olursak; bir tane asp.net core web application'ımız var ve sahip olduğu CustomerDbContext adında ki dbcontex'i kullanarak dışarıya end-point'ler açmakta. Hızlıca CustomerDbContext sınıfına bakacak olursak;

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

    public DbSet<Customer> Customer{ get; set; }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);
    }
}

Startup.cs sınıfı içerisinde bulunan ConfigureServices metodunda ise CustomerDbContext'i constructor inejction uygulayarak base repository sınıfına taşıyacağımızdan context service olarak built-in container'a register edelim.

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

    services.AddScoped<ICustomerRepository, CustomerRepository>();

    services.AddMvc();
}

Yukarıdaki kod bloğunu basit bir şekilde anlatmak gerekirse,appsettings.json dosyasında yer alan connString adresini kullanarak CustomerDbContext'i bir sqlServer instance'ı ile ilişkilendirerek ayağa kaldırır. 

{
  "CustomerDbConnString": "Server=.;Initial Catalog=Customerdb;Persist Security Info=False;User ID=Customeruser;Password=qwerty135-;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=True;Connection Timeout=30;"
}

Amacımız CustomerRepository sınıfı için unit testler yazmak. GenericRepository pattern tercih etmiş olalım ve ilgili repository layer sınıflarımız aşağıdaki gibidir.

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);
}
public abstract class GenericRepository<T> : IGenericRepository<T> where T : Entity
{
    private readonly CustomerDbContext _dbContext;
    private readonly DbSet<T> _dbSet;

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

    public Guid Save(T entity)
    {
        entity.Id = Guid.NewGuid();
        _dbSet.Add(entity);
        _dbContext.SaveChanges();
        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;
        _dbContext.SaveChanges();
    }

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

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

    public IQueryable<T> Find(Expression<Func<T, bool>> predicate)
    {
        return _dbSet.Where(predicate);
    }
}
public interface ICustomerRepository : IGenericRepository<Customer>
{ }
public class CustomerRepository : GenericRepository<Customer>, ICustomerRepository
{
    public CustomerRepository(CustomerDbContext dbContext) : base(dbContext)
    {
    }
}

CustomerDbContext'i constructor inejction uygulayarak base repository sınıfına taşıdık. Görüldüğü üzre CRUD işlemleri için metotları bulunan repository'nin unit testlerini yazacağız. Bunun için eski usul bir unit test db'si oluşturmak gibi çözümlere gitmeyeceğiz. Bunun yerine nuget üzerinden indirip kullanabileceğimiz Microsoft.EntityFrameworkCore.InMemory kütüphanesini kullanarak projemizde bir in-memory database ayağa kaldırabiliriz. İlgili kütüphaneyi nuget üzerinden projemiz referanslarına ekleyelim.

Kurulum tamamlandıktan sonra solution'da yeni bir xUnit test projesi oluşturalım ve ilk olarak repository'de ki Save metodu için aşağıdaki gibi unit-test metodunu yazalım.

[Fact]
public void Save_Should_Save_The_Customer_And_Should_Return_All_Count_As_Two()
{
    var customer1 = new Domain.Customer("Caner Tosuner", "IST", DateTime.Today.AddYears(28));
    var customer2 = new Domain.Customer("Caner Tosuner", "IST", DateTime.Today.AddYears(28));

    var options = new DbContextOptionsBuilder<CustomerDbContext>()
        .UseInMemoryDatabase("customer_db")
        .Options;

    using (var context = new CustomerDbContext(options))
    {
        var repository = new CustomerRepository(context);
        repository.Save(customer1);
        repository.Save(customer2);
        context.SaveChanges();
    }

    using (var context = new CustomerDbContext(options))
    {
        var repository = new CustomerRepository(context);
        repository.All().Count().Should().Be(2);
    }
}

Yukarıda görüldüğü üzre nuget'ten eklediğimiz kütüphane ile birlikte DbContextOptionsBuilder sınfınının instance'ını alarak extension metot olarak kullanabileceğimiz UseInMemoryDatabase() metodu yer almakta. Bu metot unit test run edilirken bizim dbContext nesnemizle birebir aynı yeni bir in-memory CustomerDbContext sınıfı oluşturmamıza olanak sağlar. CustomerRepositoryTests sınıfının bütün test metotları ile birlikte son hali aşağıdaki gibidir.

    public class CustomerRepositoryTests
    {
        [Fact]
        public void Save_Should_Save_The_Customer_And_Should_Return_All_Count_As_Two()
        {
            var customer1 = new Domain.Customer("Caner Tosuner", "IST", DateTime.Today.AddYears(28));
            var customer2 = new Domain.Customer("Caner Tosuner", "IST", DateTime.Today.AddYears(28));

            var options = new DbContextOptionsBuilder<CustomerDbContext>()
                .UseInMemoryDatabase("customer_db")
                .Options;

            using (var context = new CustomerDbContext(options))
            {
                var repository = new CustomerRepository(context);
                repository.Save(customer1);
                repository.Save(customer2);
                context.SaveChanges();
            }

            using (var context = new CustomerDbContext(options))
            {
                var repository = new CustomerRepository(context);
                repository.All().Count().Should().Be(2);
            }
        }

        [Fact]
        public void Delete_Should_Delete_The_Customer_And_Should_Return_All_Count_As_One()
        {
            var customer1 = new Domain.Customer("Caner Tosuner", "IST", DateTime.Today.AddYears(28));
            var customer2 = new Domain.Customer("Caner Tosuner", "IST", DateTime.Today.AddYears(28));

            var options = new DbContextOptionsBuilder<CustomerDbContext>()
                .UseInMemoryDatabase("customer_db")
                .Options;

            using (var context = new CustomerDbContext(options))
            {
                var repository = new CustomerRepository(context);
                repository.Save(customer1);
                repository.Save(customer2);
                context.SaveChanges();
            }

            using (var context = new CustomerDbContext(options))
            {
                var repository = new CustomerRepository(context);
                repository.Delete(customer1.Id);
                context.SaveChanges();
            }

            using (var context = new CustomerDbContext(options))
            {
                var repository = new CustomerRepository(context);
                repository.All().Count().Should().Be(1);
            }
        }

        [Fact]
        public void Update_Should_Update_The_Customer()
        {
            var customer = new Domain.Customer("Caner Tosuner", "IST", DateTime.Today.AddYears(28));

            var options = new DbContextOptionsBuilder<CustomerDbContext>()
                .UseInMemoryDatabase("customer_db")
                .Options;

            using (var context = new CustomerDbContext(options))
            {
                var repository = new CustomerRepository(context);
                repository.Save(customer);
                context.SaveChanges();
            }

            customer.SetFields("Caner T", "IZM", customer.BirthDate);

            using (var context = new CustomerDbContext(options))
            {
                var repository = new CustomerRepository(context);
                repository.Update(customer);
                context.SaveChanges();
            }

            using (var context = new CustomerDbContext(options))
            {
                var repository = new CustomerRepository(context);
                var result = repository.Get(customer.Id);

                result.Should().NotBe(null);
                result.FullName.Should().Be(customer.FullName);
                result.CityCode.Should().Be(customer.CityCode);
                result.BirthDate.Should().Be(customer.BirthDate);
            }
        }

        [Fact]
        public void Find_Should_Find_The_Customer_And_Should_Return_All_Count_As_One()
        {
            var customer1 = new Domain.Customer("Caner Tosuner", "IST", DateTime.Today.AddYears(28));
            var customer2 = new Domain.Customer("Caner Tosuner", "IZM", DateTime.Today.AddYears(28));

            var options = new DbContextOptionsBuilder<CustomerDbContext>()
                .UseInMemoryDatabase("customer_db")
                .Options;

            using (var context = new CustomerDbContext(options))
            {
                var repository = new CustomerRepository(context);
                repository.Save(customer1);
                repository.Save(customer2);
                context.SaveChanges();
            }

            using (var context = new CustomerDbContext(options))
            {
                var repository = new CustomerRepository(context);
                var result = repository.Find(c => c.CityCode == customer1.CityCode);
                result.Should().NotBeNull();
                result.Count().Should().Be(1);
            }
        }
    }

Testlerimizi run ettiğimizde aşağıdaki gibi bütün repsoitroy metotlarına ait testlerin success olduğunu görebiliriz.

Source Code

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

Expected 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