Caner Tosuner

Leave your code better than you found it

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

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

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

Architecture

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

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

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

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

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

 

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

Installlations

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

  • JRE 8 Installation

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

  • Zookeeper Installation

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

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

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

JAVA_HOME  => %JAVA_HOME%\bin

ZOOKEEPER_HOME = > %ZOOKEEPER_HOME%\bin

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

 

  • Kafka Installation

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

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

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

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

Application

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

1) Kafka.Message

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

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

2) Kafka.Producer

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

Install-Package Confluent.Kafka -Version 1.0-beta

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

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

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

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

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

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

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

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

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

3. Kafka.Consumer

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

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

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

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

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

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

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

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

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

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

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

    public abstract void OnMessageDelivered(IMessage message);

    public abstract void OnErrorOccured(Error error);
}

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

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

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

        //todo email send business logic
    }

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

        //todo onerror business
    }
}

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

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

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

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

Source Code

Asp.Net Core Hangfire Kullanarak Background Task İşlemleri

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

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

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

Hangfire 

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

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

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

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

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

PM> Install-Package Hangfire

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

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

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

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

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

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

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

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

 

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

1- Fire-and-Forget Jobs

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

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

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

2- Delayed Jobs

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

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

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

3- Recurring Jobs

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

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

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

4- Continuations Jobs

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

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


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

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

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

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

Source Code

Entity Framework Core Data Concurrency Optimistic Lock

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

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

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

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

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

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

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

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

Property Based Configuration (ConcurrencyCheck Attribute)

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

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

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

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

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

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

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

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

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

RowVersion Column

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Asp.Net Core Https Kullanımı

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

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

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

İlk middleware UseHsts()

app.UseHsts();

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

Bir diğer middleware ise UseHttpsRedirection() 

app.UseHttpsRedirection();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    services.AddScoped<ICustomerRepository, CustomerRepository>();

    services.AddMvc();
}

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

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

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

public interface IGenericRepository<T> where T : class, IEntity
{
    Guid Save(T entity);
    T Get(Guid id);
    void Update(T entity);
    void Delete(Guid id);
    IQueryable<T> All();
    IQueryable<T> Find(Expression<Func<T, bool>> predicate);
}
public abstract class GenericRepository<T> : IGenericRepository<T> where T : Entity
{
    private readonly CustomerDbContext _dbContext;
    private readonly DbSet<T> _dbSet;

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

    public Guid Save(T entity)
    {
        entity.Id = Guid.NewGuid();
        _dbSet.Add(entity);
        _dbContext.SaveChanges();
        return entity.Id;
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Source Code

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

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

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

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

Sut (Service Under Test)

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

Mocking

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

Expected ve Actual Kavramları

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

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

Assertion

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

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

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

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

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

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

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

        protected Customer()
        {
            
        }

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

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

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

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

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

            _customerRepository.Save(customer);
        }

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

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

            _customerRepository.Update(existing);

            var customerDto = _customerAssembler.ToCustomerDto(existing);

            return customerDto;
        }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • XUnit
  • Moq
  • FluentAssertions 
  • AutoFixture

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

            var result = sut.Get();

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

            Assert.Equal(expected, actual);
        }

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

            var result = sut.Get(id);

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

            Assert.Equal(expected, actual);
        }

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

            var actual = sut.Post(customer);

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

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

            var result = sut.Put(expected);

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

            Assert.Equal(expected, actual);
        }

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

            var result = sut.GetByCityCode(cityCode);

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

            Assert.Equal(expected, actual);
        }
    }

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

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

Source Code

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

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

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

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

1) .Net Core Sdk 2.1 Kurulumu

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

2) ElasticSearch Kurulumu

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

3) Kibana Kurulumu

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

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

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

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

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

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

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

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

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

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

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

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

    app.UseMvc();
}

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

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

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

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

 

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

 

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

Source Code

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

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

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

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

ASP.NET Core 2.1 SDK Kurulumu

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

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

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

Set Proejct Target Framework to 2.1 in Solution  

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

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

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

 

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

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

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

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

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

Proje Oluşturulması

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

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

DbContext-Entity Tanımlaması

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

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

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

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

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

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

       services.AddMvc();
   }

Generic Repository Oluşturulması

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

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

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

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

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

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

        return entity.Id;
    }

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

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

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

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

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

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

public interface IProductRepository : IGenericRepository<Product>
{ }

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

Service Layer Oluşturulması

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

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

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

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

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

        return new GetAllProductResponse
        {
            ProductList = mappedList
        };
    }

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

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

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

Api Controller Oluşturulması

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

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

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

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

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

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