Asp.Net Core Middleware ve Request Delegate - Nedir, Nasıl Kullanılır?

By Burak TUNGUT - 21.9.2016 - 1 Yorum - Kategori Asp.Net Core

Asp.Net Core'a giriş yapmış ve ardından Application Startup konularına değinmiştik. Bu yazımda ise Asp.Net Core'un temellerini oluşturan Middleware'lara deyineceğim.

Middleware Nedir?

Asp.Net Core pipeline'ını inşaa eden, bir request'in response ile sonuçlanana kadar üzerinde gezdiği her bir component'e Middleware denir. Ne kadar da teorik oldu değil mi :) ?

Aslında Asp.Net'e baktığımızda Handler'ların tam olmasada birer karşılığı diyebiliriz Middleware'lar için. Bir request'in başlaması ile register edilmiş Middleware component'ler tek tek gezilir. Ardından ya sonraki pipeline üyesi üzerinden akışın devam ettirilmesi sağlanır ya da short circuit (kısa devre) yaptırılarak response elde edilir.
Tabi pipeline'da register edilmiş son middleware üzerinde ise short circuit yapmaksızın artık gezilecek başka bir üye kalmadığı için response oluşmuş olacaktır.

Middleware'lar tıpkı Asp.Net Web API'da ki Delegating Handler'larda olduğu gibi russian doll mimarise benzer bir yapı ile çalıştırılır. Bunu Asp.Net Web API ve Mimari Özellikleri kitabımda anlatmıştım. Hatta oradaki diyagramı burada sizler ile de paylaşayım;

 

Asp.Net Core Middleware

 

Aynı mimari Asp.Net Core dökümanlarında aşağıdaki gibi anlatılmıştır; (Kaynak : https://docs.asp.net/en/latest/fundamentals/middleware.html)

 

Asp.Net Core Middleware

 

Request Delegate'ler yani middleware içerisinde kullanacağımız yapılar IApplicationBuilder interface'i içerisindeki Run, Map, Use ve MapWhen methodları kullanılarak konfigüre edilirler. Asp.Net Core içerisinde hali hazırda var olan bir çok Middleware component mevcuttur. Bunlardan bazıları aşağıdaki gibidir;

  1. Error Handler (UseExceptionHandler methodu)
  2. Static File Server (UseStaticFiles methodu)
  3. Authentication (UseIdentity methodu)
  4. MVC (UseMvc)

4. madde de yer alan MVC Middleware'ı biraz ilginç gelmiş olabilir. Çünkü alışkın olduğumuz yapı Asp.Net MVC gibi başlı başına IIS ve Asp.Net üzerine inşaa edilmiş yeni bir framework idi. Fakat MVC, Asp.Net Core ile pipeline'a eklenebilen bir özellik gibi kullanılabilir hale getirildi.

Aslında olması gerekende buydu. Çünkü MVC bir yazılım tasarım deseni diğer bir değiş ile bir yiğidin kendine has yoğurt yiğişi idi :) Burada ise bir özellik niteliği taşıyor.

Örnek Bir Middleware Dizilimi

Visual Studio'larımızı açalım ve MVC template'ini seçerek bir adet Asp.Net Core Web projesi yaratalım. Startup sınıfı içerisindeki Configure methodu aşağıdaki gibi olacaktır;

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseBrowserLink();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }

    app.UseStaticFiles();

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}

Yukarıda göreceğiniz üzere IApplicationBuilder tipine extension olarak yazılmış bir çok method çağırılmıştır. Asp.Net Core'u geliştiren ekip burada bir kültür oluşturmuş diyebiliriz. Middleware component'lerin hepsi için birer registration methodu yazmış ve bunların ismini Use ile başlatırken bunları birer extension method olarak belirlemiştir. 

Bizde yazacağımız Middleware'ler için böyle bir yol izliyor olacağız. Öncelikle yukarıda çağırılan method'lara ve register edilme sıralarının önemine bir bakalım.

Karşımıza çıkan ilk Middleware component UseDeveloperExceptionPage() methodu ile register edilen DeveloperExceptionPageMiddleware oluyor. Fakat dikkat edersek Development enviorment'da isek bu gerçekleştiriliyor. Environment konusuna sonraki makalelerde deyineceğiz.
Bu middleware'i register ederek runtime'da alınan hatalır (response oluşmadıysa) browser'a gösterilmesini sağlayabilirsiniz. Asp.Net'te CustomError'ları kapattığınız zaman karşılaştığınız şeyi gerçekleştirmek için bunu yapmanız yeterli olacaktır.

Tabi Production enviorment'da isek bu hiçte istemeyeceğimiz bir durum. Bunun için ise UseExceptionHandler methodunu kullanabilir ve ilgili Middleware'i register edebilirsiniz.

Dikkat ederseniz pipeline'a öncelikle hata oluşması durumunda ne yapılması gerektiğini belirleyecek ve short-circuit yaparak request'in sonlandırılıp, response'a dönüşmesini sağlayacak olan Middleware component'ler registir edildi. Eğer bunlardan önce pipeline'da bir üye olsaydı muhtemelen ya hataları yutuyor ya da log'lamıyor olacaktık. Bu nedenle Middleware'lerin register edilme sıraları büyük önem taşır.

Bir de UseMvc methoduna deyinelim. Biraz önce dediğimiz gibi Asp.Net Core pipeline'ına MVC framework'ünü eklemek istiyorsanız bu methodu ve overload'larını kullanabilirsiniz. Overload'ları sayesinde alışkın olduğunuz routing ayarlamalarını yapabilirsiniz.

Yazımızın bu kısmına kadar sadece yazılmış olan Middleware'lerin nasıl register edilmiş olduklarını gördük. Ancak ne bir Middleware yazdık ne de register edecek bir extension method. Makalemizin son bölümü ile biraz kolları sıvayalım ve örnek olması için küçük fonksiyonlara sahip bir kaç Middleware yazalım :)

Bu tarz konuları işlerken yaptığım örneklerde business logic içermeyecek olabildiğince basit örnekler yapmaya özen gösteriyorum. Bazı aldığım geri dönüşler örneklerimin basit kaldığı yönünde. Ancak detaylandırılmış ve logic içeren örnekler ise anlaşılma noktasında sorun yaşatabiliyor. Bu nedenle yine örneklerimi basit ve anlaşılır tutacağım :)

Run Methodu

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    app.Run(async context =>
    {
        await context.Response.WriteAsync("Response veriliyor.");
    });
}

Hemen yukarıdaki gibi bir Configure methodu tasarlayalım ve uygulamamızı debug edelim. Ekrana sadece "Response veriliyor." şeklinde bir yazı yazdığımız bundan daha küçük bir pipeline'a sahip olmayacak uygulama elde ettik. 
Middleware component'lerin ya sonraki pipeline üyelerine görevlerini aktardıklarını ya da short-circuit yaparak akışı sonlandırdıklarından bahsetmiştik. Yukarıda kullandığımız Run methodu aslında short-circuit yapmamızı sağlayan methodumuz.

Configure methodunu aşağıdaki hale getirdiğimizde ne demek istediğim daha iyi anlaşılacaktır.

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    app.Run(async context =>
    {
        await context.Response.WriteAsync("Response veriliyor.");
    });

    app.Run(async context =>
    {
        await context.Response.WriteAsync("Response veriliyor yazdık. Peki bu yazı nereye gelecek?");
    });
}

Sizce iki mesaj birden mi yazacak? Yoksa ikisinden biri mi?
Doğru cevap : "Response veriliyor." yazısı yazacak :) Neden mi? Az önce bahsetiğimiz gibi Run methodu ile short-circuit yapılmasını sağladık ve diğer pipeline üyeleri es geçilerek Response'un oluşturulmasını sağladık.

Use Methodu

Bir de Use method'unu inceleyelim. Run'dan farklı olarak bir delegate daha sunmakta ve sonraki pipeline üyesine ulaşabilmemizi sağlamaktadır. Bunu daha çok pipeline'da iki üyenin arasına girmek gibi nitelendirebiliriz. 

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    app.Use(async (context, next) =>
    {
        Debug.WriteLine("Middleware araya girdi.");

        await next.Invoke();

        Debug.WriteLine("Sonraki üyeler çalıştırıldı ve response veriliyor.");
    });

    app.Run(async context =>
    {
        Debug.WriteLine("Short-circuit yapıldı.");

        await context.Response.WriteAsync("Response veriliyor.");
    });
}

Yukarıdaki gibi tasarladığımız bir pipeline'dan beklentimiz Use methodu ile belirlediğimiz middleware'in devreye girip Output'a bir satır yazı eklemesi, sonraki pipeline üyesini çağırması ve tekrar geri dönerek (tüm pipeline üyeleri tamamlandıktan sonra) Output ekranına ikinci satırı eklemesi.

Böyle bir pipeline'da ekrana "Response veriliyor." yazarken Output ekranına ise aşağıdaki satırlar düşecektir;

Middleware araya girdi.
Short-circuit yapıldı.
Sonraki üyeler çalıştırıldı ve response veriliyor.
 

Map Methodu

Bazı request'leri, request atılan path'e göre filtrelemek isteyebiliriz. Diğer bir değiş ile Path'e göre bazı middleware'lerin çalıştırılmasını isteyebiliriz. Böyle bir durumda Use ya da Run methodlarının içerisinde birer if bloğu kullanmamızda bir sakınca yok. Ancak bu işlemin aynısını Map methodu ile de gerçekleştirebiliriz.

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    app.Use(async (context, next) =>
    {
        Debug.WriteLine("Middleware araya girdi.");

        await next.Invoke();

        Debug.WriteLine("Sonraki üyeler çalıştırıldı ve response veriliyor.");
    });

    app.Map("/burak", internalApp =>
    {
        internalApp.Run(async context =>
        {
            await context.Response.WriteAsync("/burak adresi ile gelindi.");
        });
    });

    app.Run(async context =>
    {
        Debug.WriteLine("Short-circuit yapıldı.");

        await context.Response.WriteAsync("Response veriliyor.");
    });
}
Bir önceki örneği aynen alıp iki middleware arasında bir adet Map methodu ile middleware ekledik. Map methodu ikinci parametre olarak bizlere Action<IApplicationBuilder> tipini sunar. Dikkat edersek bu tip zaten pipeline'u build etmemize olanak sağlayan tipimiz.
Verilen bu parametre ile pipeline içerisinde bir dallanma yaratarak minik bir pipeline daha tasarlayabiliyoruz.
 
Yukarıdaki gibi bir pipeline ile /burak adresine request atarsak "/burak adresi ile gelindi." şeklinde bir response alacağız. Aksi tüm durumlar için ise "Response veriliyor." almaya devam ediyor olacağız.
 

MapWhen Methodu

Map methodu sadece request path'ine göre filtreleme yapabilmemizi sağlarken MapWhen methodu ile herhangi bir şarta göre filtreleme yapılarak middleware'in pipeline'a dahil edilmesini sağlayabiliriz. 

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    app.Use(async (context, next) =>
    {
        Debug.WriteLine("Middleware araya girdi.");

        await next.Invoke();

        Debug.WriteLine("Sonraki üyeler çalıştırıldı ve response veriliyor.");
    });

    app.MapWhen(x => x.Request.Method == "GET", internalApp =>
    {
        internalApp.Run(async context =>
        {
            await context.Response.WriteAsync("HTTP GET request'te bulunuldu.");
        });
    });

    app.Run(async context =>
    {
        Debug.WriteLine("Short-circuit yapıldı.");

        await context.Response.WriteAsync("Response veriliyor.");
    });
}

Yukarıdaki gibi build edilmiş bir pipeline ile HTTP GET request'te bulunulduğu zaman "HTTP GET request'te bulunuldu." şeklinde bir response alacağız. Aksi tüm durum ise yine "Response veriliyor." şeklinde bir response almaya devam ediyor olacağız.

Asp.Net Core için Middleware Yazıyoruz!

Basit ve anlaşılır örnekler ile middleware'ların nasıl yazılabileceğini, pipeline'a nasıl şekil verilebileceğini inceledik. Şimdi genelde integration projelerinde ihtiyacımız olan bir örnek ile konumuzu sonlandıralım.

İçinde bulunduğum, gerek dışarıya verdiğimiz gerek dışarıdan alıp consume ettiğimiz çoğu integration projesinde bir request'in kaç birim zamanda sonuçlandığını göreceğimiz metriklere ihtiyaç duyduk.
Genelde dışarıya generic bir response vererek root'ta ElapsedMilisecond gibi bir field bulundurur ve request'in kaç milisaniye sürdüğünü bir field ile sunarız. Bu örnekte, tasarlayacağımız Asp.Net Core uygulamamıza gelen request'lerin kaç milisaniye'de sonuçlandığını Output ekranına yazan bir middleware ve registration'ını gerçekleştirecek bir extension method yazacağız.

Öncelikle adı RequestMetricMiddleware olan bir sınıf yaratalım ve constructor method'u ile Invoke method'unu aşağıdaki gibi tasarlayalım;

public class RequestMetricMiddleware
{
    private readonly RequestDelegate _next;

    public RequestMetricMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        DateTime startedAt = DateTime.Now;

        await _next.Invoke(context);

        DateTime finishedAt = DateTime.Now;

        Debug.WriteLine(string.Format("Started at : {0}, Finished at : {1}, Elapsed ms : {2}", startedAt, finishedAt, (finishedAt - startedAt).TotalMilliseconds));
    }
}

Invoke methodumuz tetiklendiği andaki saat tutulacak ve pipeline'ın sonraki üyeleri çalıştırılmaya devam edecektir. Bizden sonra gelen tüm pipeline üyeleri çalıştıktan sonra eğer short-circuit yapılmadıysa bir saat daha tutulacak ve Output ekranına elde edilen saatler ve ikisi arasında geçen toplam zaman milisaniye cinsinden yazılacaktır.

Şimdi bu middleware'i register edecek olan extension method'u yazalım;

public static class RequestMetricMiddlewareExtensions
{
    public static IApplicationBuilder UseRequestMetrics(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<RequestMetricMiddleware>();
    }
}

Artık middleware'imizi kullanabiliriz! Bunun için aşağıdaki gibi bir Configure methodu tasarlayalım. Thread.Sleep method'u ile pipeline'ın biraz gecikmesini sağlayalım ve 0'dan büyük metrikler elde edelim.

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    app.UseRequestMetrics();

    app.Run(async context =>
    {
        Thread.Sleep(new Random().Next(200, 300));

        await context.Response.WriteAsync("Response veriliyor.");
    });
}

Uygulamamızı çalıştıralım ve birkaç kez request atalım. Benzer bir output ekranı aşağıdaki gibi olacaktır;

Started at : 21.9.2016 22:53:50, Finished at : 21.9.2016 22:53:50, Elapsed ms : 240,1082
Started at : 21.9.2016 22:53:50, Finished at : 21.9.2016 22:53:50, Elapsed ms : 253,1921
Started at : 21.9.2016 22:53:50, Finished at : 21.9.2016 22:53:50, Elapsed ms : 220,9552
Started at : 21.9.2016 22:53:50, Finished at : 21.9.2016 22:53:50, Elapsed ms : 287,2687
Started at : 21.9.2016 22:53:50, Finished at : 21.9.2016 22:53:51, Elapsed ms : 222,0534
Bir yazımızın daha sonuna geldik.
Asp.Net Core yazılarıma Built-in Middleware konusu ile devam ediyor olacağız. Esen kalınız :)
 
Yorum Bırak

Facebook
Son Yorumlar