Caner Tosuner

Leave your code better than you found it

Asp.Net Core Uygulamalarında Farklı DI/IoC Containerlar Nasıl Kullanılır, Autofac Kullanımı

Asp.net core default olarak oldukça lightweight bir sürüm olan built-in dependency injection tool'ı ile birlikte gelmekte ve bu di tool'ını kullanarak basit bir şekilde uygulama genelindeki instance yönetimini sağlayabilmekteyiz. Şu yazımızda built-in DI container nedir nasıl kullanılır değinmiştik. Bununla birlikte asp.net core third-party dependency injection tool'larını da desteklemekte.

Bu yazımızda bir asp.net core uygulamasına Autofac kütüphanesini default DI container'ı olarak nasıl implement ederiz inceleyeceğiz.

Yazımızın başındada bahsettiğimiz gibi asp.net core default built-in DI Container desteği sunmakta ve kullanım olarakta aşağıdaki gibi bağımlılıkları register edebilmekteyiz.

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<ICustomerRepository, CustomerRepository);

    services.AddMvc();
}

Ihtiyaç duyduğumuz yerde de bu bağımlılığı constructor injection yöntemiyle inject edip kullanabilmekteyiz.

public class CustomerController : Controller
{
    private readonly ICustomerRepository _customerRepository;

    public HomeController(ICustomerRepository customerRepository)
    {
        _customerRepository = customerRepository;
    }
}

Aynı bu bağımlılıkları autofac, NInject, Unity StructureMap gibi birçok DI Container kütüphanesini kullanarakta tanımlayabiliriz. Bütün bu DI Container'ların instance yönetimi dışında developer'lar tarafından sevilen ve beğenilen bir çok farklı özellikleri bulunmakta. Bunu sağlayan şey ise IServiceProvider interface'i. Asp.Net Core yumlu DI kütüphaneleri bu interface'i implement edip bağımlılıkları bu ortak interface üzerine inşa ettiklerinden asp.net core uygulamalarında third-party DI kütüphanelerini kullanabilmekteyiz. Uygulamaya third-party DI kütüphanesini belirttiğimiz yer ise Startup.cs içerisinde yer alan void ConfigureServices metodunu aşağıdaki gibi IServiceProvider return edecek şekilde değiştirmek.

public IServiceProvider ConfigureServices(IServiceCollection services)
{
   // add services
   
   // return third-party tool's class which is implemented IServiceProvider
}

Bizde örnek olarak Autofac DI Container kütüphanesini projemize implement edip bağımlılıkları bunun üzerinden register edicez. Projemize nuget üzerinden Autofac kütüphanesini install ettiğimizi varsayalım ve autofac builder işlemlerini ConfigureServices metodu içerisinde aşağıki gibi tanımlayalım.

public IServiceProvider ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    var builder = new ContainerBuilder();
	
    builder.RegisterType<CustomerRepository>().As<ICustomerRepository>();
	
    builder.Populate(services);
 
    var appContainer = builder.Build();
 
    return new AutofacServiceProvider(appContainer);
}

Kullanım basit haliyle yukarıdaki gibi Builder oluşturup bağımlılıkları register edip sonrasında IServiceProvider interface'ini implement eden AutofacServiceProbider sınıfını return ettik ve artık uygulamamız için DI Container built-in container yerine Autofac olarak implement ettik. 

Autofac dışında diğer DI Container tool'larını kullanmak isterseniz aynen yukarıdaki gibi Startup.cs içerisinde ConfigureService metodunu kullandığınız DI tool'un da yer alan IServiceProvider interface'ini implement eden sınıfı  bağımlılıkları register ettikten sonra return etmeniz yeterli olacaktır.

Asp.Net Core Uygulaması Docker'da Nasıl Deploy Edilir - Dockerize an Asp.Net Core Application

Docker container'lar virtual machine'lere göre daha lightweight ve portable alternatifler sağlamak için kullanılan yapılardır. Sanal makinelerin aksine container'lar uygulama bazlı olup istenilen teknolojiye göre gerekli bağımlılıkların bulunduğu o uygulamaya özel environment'lar ayağa kaldırabilirler.

Docker container, uygulamanızı ihtiyaca göre farklı makinelerde horizontal bir şekilde çoğaltmanızı oldukça kolaylaştırır. Örneğin, Linux’ta geliştirmeyi tamamladık ve mac makinama gittim ve uygulamanın ordada çalışmasını istiyoruz repoyu klonladım ve “docker-compose-up” yapmam o makinada uygulamayı ve bağımlılıklarını kurup tekrar browserdan kolayca erişebilmeme olanak sağladı, bu oldukça ciddi bir kolaylık.

Container'lar aynı zamanda izolasyonu sağlarlar. Container'larda kullanılan bağımlılıklar ve ayarlar, makinenizde çalışan diğer uygulamaları etkilemez. Bu, dependecny-conflict'lerle karşılaşmamıza engel olur.

Deployment süreçleri docker ile birlikte oldukça hızlı bir hale gelir. Oluşturulan image'ler bir merkezi docker-registry'e atılır ve ihtiyaç duyulduğunda tekrar tekrar rebuild işlemi olmadan registry'den pull edilerek kullanılabilir.

Docker, uygulamaları bir container içerisinde build, deploy ve manage etmeye yarayan open-source olarak geliştirilen bir toolkit dir. Container; hem uygulama kodunu hemde gerekli bağımlılıkları içeren bir yazılım birimi olarak tanımlanabilir ve her bir container birbirinden izole bir şekilde aynı operating system'ı paylaşarak host edilirler. Tek şart bu host operating system Windows yada Linux olsun Docker runtime kurulu olması gerekmektedir.

Bu yazıda en basit haliyle bir asp.net core uygulaması docker kullanarak nasıl host edilir inceleyeceğiz.

1- Installing Docker

İlgili işletim sisteminize göre makinamıza docker kurmamız gerekmekte. Bunun içi aşağıdaki adreslerden faydalanabilirsiniz.

İlgili instruction'ları takip edip kurulum işlemini tamamladıktan sonra docker işletim sisteminiz üzerinde çalışmaya başlayacaktır. Ben windows üzerinde çalıştığımdan docker for windows sürümünü kurdum. Version kontrolü için cmd'de docker --version komutunu çalıştırdığınızda aşağıdaki gibi kurulu olan versiyon bilgisini görüntüleyebilirsiniz. 

2- Creating Asp.Net Core App.

.Net Core uygulaması oluşturmanın 2 farklı yolu bulunmakta;

İlk olarak; vs'da HelloDocker adında bir Asp.Net Core web api uygulaması oluşturalım ve enable-docker support özelliğini aktif edelim.

Projeyi oluşturduğumuzda enable-docker support özelliğini aktif ettiğimizden solution'da farklı olarak, dockerfile ve docker-compose dosyaları karşımıza çıkmakta.

Dockerfile kısaca, docker uygulamayı build ederken neler yapılacağı ne gibi bağımlılıklar install edileceği gibi her bir image için oluşturulan configuration dosyalarıdır. Image oluşturulduktan sonra bu image'in bulunduğu container başlatılabilir.

Docker-compose.yml ise; multi-container çalışan uygulamalar için gerekli tanımlamaların yapıldığı developing ve testing sırasında kullanılan bir command-line dosyasıdır.

Uygulamalarınıza docker support eklemek görüldüğü üzre VS-2017 ile birlikte oldukça basit bir hal almaktadır.

Uygulama oluşturmanın ikinci yolu ise komut satırlarını kullanmak. Dilersek vs üzerinden değilde aşağıdaki gibi komut satırlarını kullanarak da projemizi oluşturabiliriz. İlk olarak netcore-docker adında bir folder yaratalım ve aşağıdaki komut satırlarını bu folder içerisinde açtığımız command-prompt'ta çalıştıralım.

mkdir HelloDocker
cd HelloDocker

dotnet new webapi
dotnet restore

Komutları çalıştırdıktan sonra yukarıda belirttiğimiz klasör içerisinde HelloDocker projemiz yaratılmış olacaktır. HelloDocker projesini run ettiğinizde default olarak localhost:5000 de çalışan bir uygulama olarak host edildiğini göreceksiniz.

 dotnet run

Default uygulama ayarlarında https redirection olduğundan browser'dan https://localhost:5001/api/Values adresine gittiğinizde uygulamanın çalıştığını görebilirsiniz.

3- Creating an Image

Sırada Dockerfile oluşturmak var. Proje dizinine Dockerfile adında bir dosya açıp image'i oluştururken kullanılacak komutları aşağıdaki gibi yazalım ve bu komutların ne işe yaradıklarına bir göz atalım.

FROM microsoft/dotnet:2.2-sdk AS build
WORKDIR /app

COPY *.csproj ./
RUN dotnet restore

COPY . ./
RUN dotnet publish -c Release -o out

FROM microsoft/dotnet:2.2-aspnetcore-runtime AS runtime

WORKDIR /app

COPY --from=build /app/out .

ENTRYPOINT ["dotnet","HelloDocker.dll"]

Dockerfile özetle 2 ana bölümden oluşur. Birincisi uygulamayı build etme ikincisi ise uygulamayı run etme bölümleridir. Dosya içerisindeki komutların ne olduğuna bakacak olursak;

  1. FROM hangi docker image'i kullanılacağını belirttiğimiz komut. Bu projemiz build etmek için dotnetcore 2.2 SDK image'ini kullanacağız.
  2. WORKDIR image içerisinde working directory olarak kullanacağımız yeri belirttiğimiz komut. Bu projemizde working directory olarak /app kullanacağız.
  3. COPY proje dosyalarını local file system'dan image'e kopyalamak için kullanılan komuttur. Bizde projemizde ilk olarak csproj dosyasını kopyalayıp restore edeceğiz, sonra bütün bu oluşan dosyaları yeniden kopyalayıp dotnet publish komutu çalıştırarak uygulamamızı oluşturacağız.
  4. ENTRYPOINT container ayağa kaldırılırken ilk olarak çalışacak olan komut ve parametreleri belirttiğimiz komuttur. Container run edilirken dotnet komutuyla HelloDocker.dll'i execute edilecektir.

Image'i oluşturmak için ise aşağıdaki kod satırını run etmemiz yeterli.

docker build -t hellodocker .

Bu işlem internet hızınıza bağlı olarak biraz zaman alacaktır. İşlem sonunda image'i oluşturabilmesi için gerekli olan dependency'leri download edip image'i yaratacaktır.

Download işlemi bittikten sonra docker image ls komutunu çalıştırdığınızda image'in listelendiğini görebilirsiniz.

docker image ls 

4- Deploying a Container

Image'imizde hazır olduğuna göre son adım olarak bu image'i bir container içerisinde host edip kullanmaya başlayalım. Bunun için kullanmamız gereken en basit şekli aşağıdaki kod satırını çalıştırmak.

docker run hellodocker:latest
 

Uygulamayı localhost:80 portunda host ettiğini ekranı yazdırdı. İlk olarak gidip Container oluştumu, oluştuysa çalışıyormu diye kontrol etmek için,

docker container ls


Görüldüğü üzre container up ve çalışır durumda ancak container'a bir isim vermedik ve bu durumda docker kendisi random bir isim atadı. Bu aslında özellikle birden fazla image ile uğraşılan projelerde pekte tavsiye edilen bir durum değil ve browser'dan http://localhost/api/Values adresine gittiğinizde ekranda json response yerine hata yazdığını göreceksiniz.  Sorunları çözmek için container'ı stop edip silelim.

docker container stop amazing_banach
docker container rm amazing_banach

Daha önce çalıştırdığımız docker run komutunu aşağıdaki gibi revize edip tekrardan çalıştıralım.

docker run --name hellodocker --env ASPNETCORE_ENVIRONMENT=Development -p 80:80 hellodocker:latest

3 parametre ekledik. Container adı, aspnetcore environment değeri ve hangi port üzerinden hizmet vereceği. Şimdi tekrardan browser üzerinden http://localhost/api/Values adresine gittiğimizde response alabildiğimizi göreceksinizdir.

Bazı Faydalı Docker Komutları

Yüklü olan docker image'lerini listelemeyi sağlayan komut

docker image ls

Image'i silmek için kullanılan komut

docker rmi image_id

Container'ları listelemek için kullanılan komut

docker container ls

Çalışmakta olan bir container'ı stop etmek için kullanılan komut

docker stop container_name

Durmakta olan bir container'ı silmek için kullanılan komut

docker rm container_name

Source

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ırabilmek 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.