Caner Tosuner

Leave your code better than you found it

Curator Kullanarak Elasticsearch Index'lerini Periyodik Olarak Temizleme

Verilerinizi elasticsearch'de index'lemeye başladıktan belli bir süre sonra disk alanınızın hızla dolduğunu fark edeceksiniz ve akabinde kibana'da yavaşlıklar başlayacak veya grafana elk-disc alert'iniz slack üzerinden size alarmlar gönderme başlayacaktır. Eğer elasticsearch'de index'lediğimiz veriler silmekten yana bir sıkıntı olmayacak türde ise; sonrasında muhtemelen büyük bir çoğunluğumuz ilk yöntem olarak elasticsearch-api kullanarak eski indexleri silmek aklımıza gelecektir. Oldukça ilkel işleyen bir süreç gibi; disk doldu gir makinaya yada postman üzerinden api-call ile log sil. Herhalde hiçbirimizin yapmak istemeyeceği bir yöntem.

Curator bu gibi işlemleri belli rule'lara göre otomatik olarak yapmaya yarayan bir tool'dur. Curator ile, hangi indeksin temizleneceğini ve kaç günlük verinin saklanacağını bir config-file ile tanımlayabilirsiniz ve belli periyotlar ile background-job run ederek rule'lara göre silme işlemini gerçekleştirir.

Örneğin payment-logs-* index'i hariç elasticsearch'de bulunan ve 90 günden daha eski bütün index'leri silmesini Curator'a tanımlayabilir ve Curator'ı haftada bir cron-job olarak çalışacak şekilde programlayarak index'leri sildirebiliriz.

Kurulum

Curator için makinanızda python-pip kurulu olması gerekmekte.

1) Install Python pip

$ sudo apt-get install python-pip

2) Install Curator

$ sudo pip install elasticsearch-curator

3.) Create new directory for config-files

mkdir curator
cd curator
touch config.yml

vim komutu ile config.yml dosyasının içine girip içerisinde bulunacak komutları aşağıdaki gibi yazalım. Bu dosyada elasticsearch adresi ve connection kurarken gerekli olan tanımlar yer almakta.

client:
	hosts:
		- 127.0.0.1
	port: 9200
	url_prefix:
	use_ssl: False
	certificate:
	client_cert:
	client_key:
	ssl_no_validate: False
	http_auth:
	timeout: 30
	master_only: False

logging:
	loglevel: INFO
	logfile:
	logformat: default
	blacklist: ['elasticsearch', 'watches', '.watches','.kibana']

Şimdi ise rule ve aciton'ların yer aldığı actions.yml dosyasını oluşturup içerisini ihtiyacımıza uygun olacak işekilde aşağıdaki komutlarda olduğu gibi yazalım.

actions:
  1:
    action: delete_indices
    description: >-
      Delete indices older than 90 days except payment-logs indices !
    options:
      ignore_empty_list: True
      disable_action: False
    filters:
    - filtertype: pattern
      kind: prefix
      value: payment-logs-
      exclude: True
    - filtertype: age
      source: name
      direction: older
      timestring: '%Y.%m.%d'
      unit: days
      unit_count: 90

Özetle; filtertype: age'i yani creationdate'i 90 günden fazla olan index'leri filtertype: pattern payment-logs index'i hariç silmesini belirttik.

Curator dry-run komutu ile bir nevi test yaparmışcasına komutumuzu çalıştırıp hangi indexleri delete edeceğini silmeden görebiliriz. Bunun için kullanacağımız komut ;

curator --config /root/curator/config.yml --dry-run /root/curator/actions.yml

Artık service olarak çalıştırmak istediğimiz curator'u run etmek için kullanacağımız komut ise aşağıdaki gibi olacaktır.

Curator job'ının 2 günde bir gece saat 02:00 da çalışmasını istiyoruz komutumuz;

0 2 */2 * * curator --config /root/curator/config.yml /root/curator/actions.yml

Özetleyecek olursak; curator kullanarak silmek istediğimiz elasticsearch index'lerini belli rule'lar tanımlayarak otomatik bir şekilde silmemizi sağlayan bir tool'dur. Bu tool'u kullanarak kolay bir şekilde manuel bir müdahale gerektirmeden eski index'lerden kurtulup elasticsearch makinanızda free disc-space yaratabilirsiniz. Daha fazla bilgi için elasticsearch-curator sayfasına göz atabilirsiniz.

Source

GraphQL Nedir, .Net Core GraphQL Kullanımı

GraphQL, Api'larınız için server-side runtime execute edebileceğiniz bir query language'dir. Dil ve teknoloji bağımsız olarak bir çok framework çin desteği bulunmaktadır (asp.net, java, nodejs etc.) ve herhangi bir database yada farklı data-storage engine'lara bağımlı olmaksızın entegre edilebilir bir yapıdır. Klasik api'lardan farklı olarak client retriew etmek istediği alanları ve uygulanacak filtreleri kolayca belirtebilir.

Örnek verecek olursak; userList dönen bir api tasarladınız ve geriye db'de bulunan user'ları belirli filtreler uygulayarak return ediyor. Api v1.0'da request olarak userId ve response olarakda userFullName ve birthDate dönen bir dto olarak tasarlandı. v2.0'da dedilerki request'e birthdate ve response'a da zipcode alanları eklenecek. Yani tekrardan bir geliştirme yapmanız gerekmekte. Bir kaç hafta sonra dedilerki başka bir ekip daha bu endpoint'i kullanacak ancak onlar şu-şu parametrelere göre kullanmak istiyor vs. GraphQl sağladığı fluent yapı ile birlikte isteyene istediğini gibi filtreleyebildiği ve entity'nize göre response oluşturabildiği bir yapı tasarlamayı sağlamakta. 

Örnek bir proje üzerinden anlatmaya devam edelim.

Creating an Asp.Net Core Web Application 

İlk olarak vs'da GraphQL_Sample adında bir Asp.net Core WebApp projesi oluşturalım. En son güncel olan .net core versiyonu kurulu olduğundan versiyon 3.1 ile uygulamayı oluşturdum.

Sonrasında nuget üzerinden GraphQL'in son versiyonu olan 2.4.0 paketini projemize install edelim. Dilerseniz console üzerindende install komutunu çalıştırabilirsiniz.

Install-Package GraphQL -Version 2.4.0

 

Öncelikle projemizde kullanacağımız data-source entity tanımlamalarını yapalım. Projemiz herhangi bir data-soruce'da bulunan Customer tablosundaki kayıtları dönen bir api olsun ve bu api'a ait Customer entity'sini aşağıdaki gibi oluşturalım.

Define Entity and Repository

public class Customer
{
    public Guid Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime BirthDate { get; set; }
    public string ZipCode { get; set; }
    public bool IsActive { get; set; }
}

Bu entity'ye ait CustomerRepository ve ICustomerRepository class'larını oluşturup bağımlılığı asp.net core'un default DI tool'una Startup.cs içerisinde register edelim

public interface ICustomerRepository
{
    List<Customer> All();
}
public class CustomerRepository : ICustomerRepository
{
    public List<Customer> All()
    {
        return new List<Customer>
        {
            new Customer
            {
                Id = new Guid(),
                FirstName = "Brain",
                LastName = "Adams",
                BirthDate = new DateTime(1985,11,20),
                IsActive = true,
                ZipCode = "11572"
            },
            new Customer
            {
                Id = new Guid(),
                FirstName = "Joe",
                LastName = "Colmun",
                BirthDate = new DateTime(1991,1,14),
                IsActive = true,
                ZipCode = "22687"
            },
            new Customer
            {
                Id = new Guid(),
                FirstName = "Lorena",
                LastName = "McCarty",
                BirthDate = new DateTime(1972,7,4),
                IsActive = true,
                ZipCode = "11572"
            },
            new Customer
            {
                Id = new Guid(),
                FirstName = "Ivan",
                LastName = "Lopez",
                BirthDate = new DateTime(1990,2,9),
                IsActive = true,
                ZipCode = "56874"
            },
            new Customer
            {
                Id = new Guid(),
                FirstName = "Jason",
                LastName = "Smith",
                BirthDate = new DateTime(200,8,17),
                IsActive = true,
                ZipCode = "96314"
            },
            new Customer
            {
                Id = new Guid(),
                FirstName = "Clain",
                LastName = "Adams",
                BirthDate = new DateTime(1986,5,5),
                IsActive = true,
                ZipCode = "11572"
            }
        };
    }
}

All() metodu hayali db-storage'da bulunan Customer tablosundaki bütün kayıtları select eden metod olarak düşünebiliriz ve yukarıdaki gibi dummy olarak bir customerList return etmekte.
ICustomerRepository ve CustomerRepository'ye ait dependency injection işlemini aşağıdaki gibi .net core default DI tool'unda tanımlayalım. Bunun için Startup.cs içerisinde bulunan ConfigureServices emtodunu kullanacağız.

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<ICustomerRepository, CustomerRepository>();
}

Schema;
Schema, GraphQL API'sinin ana yapı taşlarından biridir.
Schema çoğunlukla Query, Mutation, Types bilgilerini içerir. Şemadaki sorgu bir ObjectGraphType'tır.

Şimdi sırada GraphQL type'larını oluşturma var. GraphQL doğrudan bizim poco sınıflarımızı alıp mapping işlemi yapamıyor. Bunun için CustomerType adında ObjectGraphType sınıfını inherit alan bir mapping sınıfı oluşturmamız gerekmekte.

public class CustomerType : ObjectGraphType<Customer>
{
    public CustomerType()
    {
        Field(x => x.Id, type: typeof(IdGraphType)).Description("Id of the Customer");
        Field(x => x.FirstName);
        Field(x => x.LastName).Description("Customer's lastName");
        Field(x => x.BirthDate);
        Field(x => x.IsActive);
        Field(x => x.ZipCode);
    }
}

Yukarıda görüldüğü üzre Customer sınıfı için graphQl da mapping'i bulunan bir sınıf yarattık ve constructor içerisinde bu sınıfa ait field'ların mapping'ini tanımladık ve yine Startup.cs içerisinde container'a register edelim.

services.AddSingleton<CustomerType>();

Sonraki adım ise AppQuery sınıfını oluşturmak. Bu sınıf query'ler oluşturmamıza yardımcı olacak. GraphQL de "customers" key'ine karşılık bütün müşterileri return eden bir tanım yaptık

public class AppQuery : ObjectGraphType
{
    public AppQuery (ICustomerRepository customerRepository)
    {
        Field<ListGraphType<CustomerType>>("customers", resolve: context => customerRepository.All());
    }
}

RotQuery sınıfını yine Startup.cs içerisinde DI tool'una register edelim.

services.AddScoped<AppQuery>();

AppSchema sınıfı ile GraphQL örneğimiz için kullanacağımız schema anımlamasını yapalım.

public class AppSchema : Schema, ISchema
{
    public AppSchema(IDependencyResolver resolver) : base(resolver)
    {
        Query = resolver.Resolve<AppQuery>();
    }
}

GraphQL NuGet kütüphanesi, build-in DI tool'u ile birlikte gelir. 'Schema' sınıfı 'GraphQL.IDependencyResolver' property'sine sahiptir ve böylece gereken tüm ObjectGraphType'ı ihtiyaç durumunda koalyca resolve edebilir.

Yine bu yukarıda tanımladığımız Schema için DI registration'ınını Startup.cs içinde tanımlayalım.

services.AddScoped<IDependencyResolver>(_ => new FuncDependencyResolver(_.GetRequiredService));
services.AddScoped<ISchema, AppSchema>();

Son olarak ise Api'a ait olan CustomerController içerisinde ilgili endpoint'i aşağıdaki gibi oluşturalım.

[ApiController]
[Route("[controller]")]
public class CustomerController : ControllerBase
{
    private readonly ISchema _schema;
    private readonly IDocumentExecuter _executer;

    public CustomerController(ISchema schema, IDocumentExecuter executer)
    {
        _schema = schema;
        _executer = executer;

    }

    [HttpPost]
    public async Task<IActionResult> Post([FromBody] CustomerQueryDto query)
    {
        var result = await _executer.ExecuteAsync(_ =>
        {
            _.Schema = _schema;
            _.Query = query.Query;
        }).ConfigureAwait(false);
		
		if (result.Errors?.Count > 0)
        {
            return BadRequest(result);
        }
 
        return Ok(result.Data);
    }
}

Artık api'ımızı test edebiliriz. Postman üzerinden aşağıdaki gibi ilk olarak RoorQuery de belirttiğimiz All() metodu için tanımladığımız query için spesific field'lar için bir request atalım ve response'da istekte bulunduğumuz alanların geldiğini görelim.

{ "query": "query { customers { id firstName } }" }

Farklı query'ler yazacak olursak; birkaç parametreye göre filtre uygulayıp sonuçları dönen bir query için aşağıdaki gibi appQuery sınıfının içerisinde field tanımını yapalım.

Field<ListGraphType<CustomerType>>("filterCustomers",
    arguments: new QueryArguments
    {
        new  QueryArgument<StringGraphType> { Name = "firstName"},
        new  QueryArgument<StringGraphType> { Name = "lastName"},
        new  QueryArgument<BooleanGraphType> { Name = "isActive"},
        new  QueryArgument<DateGraphType> { Name = "birthDate"},

    },
    resolve: context =>
      {
          var firstName = context.GetArgument<string>("firstName");
          var lastName = context.GetArgument<string>("lastName");
          var isActive = context.GetArgument<bool>("isActive");

          return customerRepository.All().Where(x => x.FirstName.ToLower() == firstName.ToLower() && 
                                                     x.LastName.ToLower() == lastName.ToLower() && 
                                                     x.IsActive == isActive).ToList();
      });

Tekrardan postman üzerinden isteği attığımızda response'un döndüğünü görebiliriz.

{ "query": "query { filterCustomers(firstName:\"Lorena\",lastName:\"McCarty\", isActive:true) { id firstName lastName } }"}

 

Aliases
Api tarafında filteredCustomers şeklinde bir tanımlama yaptığımızdan requestte bulunurkende aynı fieldName'de dönmekte. Ancak aliases kullanarak response field'ı modify edebiliriz. Kullanım şekli olarak aşağıdaki gibidir ve graphql api'nize bu isteği attığınızda artık response'da bulunan array'in adı customers olacaktır.

query {
 customers: filteredCustomers(firstName: "Lorena") {
  id
  firstName
 }
}

Fragments

GraphQL API'sindeki iki kayıt arasında karşılaştırma yapmak için kullanılır. Aşağıdaki sorgu örneğindeki gibi bir fragment query'si oluşturabiliriz.

query {
 customerOne: filterCustomers(firstName: "Lorena") {
  ...props
 }
 customerTwo: filterCustomers(firstName: "Clain") {
  ...props
 }
}
fragment props on CustomerType {
 id
 firstName
 lastName
}

 

Özetleyecek olursak; qraphQl'e başlangıç seviyesinde değinmiş olmakla beraber .net core ile oldukça basit bir şekilde GraphQl tabanlı api'lar tasarlayabilir ve ihtiyacınıza göre complex query tanımlamaları yapabilirsiniz.

Source

Yeni Başlayanlar İçin Docker, Docker File - Docker Commands

Daha önceki docker yazısında Asp.Net Core Uygulaması Docker'da Nasıl Deploy Edilir konusuna değinmiştik örnek bir proje üzerinde anlatmıştık. Bu yazımızda ise basit bir şekidle dockerFile ve docker komutları nelerdir bunlara değineceğiz.

  • docker file; Docker image'inin nasıl build edileceğini tarif eden dosyadır,
  • docker build komutu; docker file'da tarif edilen image'i build eden komuttur,
  • docker run komutu, oluşturduğumuz bu image'i başlatan(start) komuttur,
  • docker compose file; bir veya birden fazla docker container'ını tek seferde komutlar vererek yönetmemizi sağlayan yaml dosyasıdır.

Docker file özetle; içerisinde uygulamayı run edebilmek için nelerin gerekli olduğunu belirten bir dizi komutların yer aldığı YAML formatında olan dosyadır.

Bir önceki docker yazımızdada kullandığımız örnek bir docker file'a bakacak olursak 4 ana bölümde inceleyebiliriz.

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"]

 

1-) Runtime'ın Belirlenmesi

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

İlk satır olan FROM syntax'ı uygulamamızı oluştururken kullanılacak base docker image'ini tanımlar. Bu örnekte microsoft tarafından oluşturulan dotnet 2.2-sdk docker image'ini kullanacağımızı belirttik.

Bir sonraki satır olan WORKDIR komutu ise working directory'i neresi olacak belirttiğimiz yer.

2-) Build

COPY *.csproj ./
RUN dotnet restore

Bu iki satırda Docker’a .csproj ile biten herhangi bir dosyayı yukarıda belirttiğimiz çalışma dizinine kopyalamasını söylüyoruz. Sonra dotnet CLI'den restore komutunu çalıştırıyoruz.

3-) Publish

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

Proje dosyaları ve restore sonrası oluşan dosyalarla birlikte kalan dosyalarının kopyalarını çıkarıyoruz ve ardından Release konfigurasyonlarını kullanarak her şeyi oluşturuyoruz. Bu, container'ı start ettiğimizde çalıştırdığımız uygulama olacaktır. 

4-) Serve

Şimdi farklı bir base image'e geçiyoruz. Bu sefer Microsoft asp.net core runtime image'ini kullanıyoruz. Bu image ile daha öcneki adımlarda compile olmuş uygulamayı run edebilmeyi sağlayacağız.

Working directory olarak /app set edilir ve birinci adımda compile ettiğimiz uygulama mevcut çalışma dizinine kopyalanır.

Son olarak ise entrypoint satırıyla .net core runtime kullanarak uygulamanın run edileceği yani çalıştırmak için kullanılacak dll bilgisi belirtilir.

Build The Image

Artık bir docker file'ımız ve içerisinde image'i oluşturmak için gerekli stepleri içeren kod satırları mevcut. Tek yapmamız gereken docker build komutu ile uygulamamızın image'ini oluşturmasını sağlamak.

docker build -t hellodocker .

Komutu çalıştırdıktan sonra aşağıdakine benzer bir ekran görüyor olmanız gerekir. 

 

Start The Container

Artık uygulamamıza ait image var ve bu image'i bir container içerisinde host edebiliriz.

 docker run -p 3010:80 hellodocker

Docker'a az önce oluşturduğumuz image'i kullanarak yeni bir container oluşturmasını belirttik ve bu konteynerdeki 3010 nolu harici portu 80 nolu iç porta bind ettik. 

Artık tarayıcınızı açıp http://localhost:3010/ adresine gidip uygulamanızı görüntüleyebilirsiniz.

Özetle yazımızda bir docker file neye benzer içerisindeki komutlar en basit haliyle ne işe yarar ve docker file dosyamızı nasıl run edebiliriz konularına açıklık getirmeye çalıştık. Umarım yeni başlayanlar için hızlıca öğrenip uygulayabilmek adına güzel bir yazı olmuştur.

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