İçeriğe atla

ASP.NET Core 6/Middleware'ler ve Request Pipeline

Vikikitap, özgür kütüphane

Bu bölüm temelleri inşa etmektedir. Bu bölümde temel anlamda ASP.NET Core’un nasıl çalıştığını göreceğiz. MVC ve Razor Pages gibi daha üst framework’lar burada öğreneceğimiz temeller üzerine kuruludur.

Middleware Mantığı

[değiştir]

Bir resim bir cümleye bedeldir derler. O yüzden aşağıdaki şekli inceleyelim.

Burada MW ile gösterilenler middleware’lerdir. Her bir middleware bir program parçasıdır, kendisine gelen talebi (request) alır ve cevaba (response) bir şeyler yazar. Her bir middleware hem request’i hem response’u değiştirebilir. Her bir middleware’in bir geliş yolu vardır, bir de dönüş yolu vardır. ASP.NET Core’da bütün üst seviye işlemler request pipeline’a koyulan middleware’ler vasıtasıyla yapılır. Kendi middleware’lerimizi de yazıp request pipeline’a koyabiliriz.

ASP.NET Core’a bir talep geldiğinde ASP.NET Core bir nesne oluşturur. Bu nesnenin içinde request ve response vardır. İşte middleware’ler arasında dönen şey bu nesnedir. Her middleware response'a yazmaz. Bazı middleware’ler diğerlerine yardımcı olmak için oradadır. Eğer hiçbir middleware gelen request'e karşılı response'a bir şeyler yazmazsa sonuçta geriye HTTP 404 Not Found cevabı döndürülür.

Başka bir bakış açısına göre middleware'leri birbirini çağıran fonksiyonlar olarak düşünebiliriz. Örneğin aşağıdaki kodu inceleyin:

MW1(request,response)
{
    // MW1'in geliş yolu
    MW2(request,response);
    // MW1'in dönüş yolu
}
 MW2(request,response)
{
    // MW2'in geliş yolu
    MW3(request,response);
    // MW2'in dönüş yolu
}
 MW3(request,response)
{
    // MW3'ün geliş ve dönüş yolu
}

Servisler

[değiştir]

Servisler middleware'lere dışarıdan enjekte edilen canlı .NET nesneleridir. Bazı servisler kütüphane mantığıyla sık yapılan işleri daha kolay yapmaya odaklanmışken bazı servisler middleware'ler arası koordinasyon sağlar. Servisler middlware'lere dependency injection dediğimiz bir teknikle enjekte edilir. Dependency injection konusu kitabın ileriki bölümlerinde detaylı bir şekilde işlenecektir. Servisleri de işin içine kattığımızda request pipeline şöyle gözükür:

Burada servis1 MW1 ve MW2 tarafından, servis2 MW2 ve MW3 tarafından ortak kullanılmaktayken servis3 servisi yalnızca MW3 middleware'i tarafından kullanılmaktadır.

Program.cs Dosyası

[değiştir]

Program.cs dosyası bir ASP.NET Core uygulamasının başladığı yerdir. Konsol uygulamalarındaki Main() metodu gibi düşünebiliriz. Bu dosya sunucu çalıştığı zaman çalışacak kodları içerir. Temel yapılandırma ayarlarını, servis ayağa kaldırmalarını ve request pipeline’a middleware eklemelerini içerir. Bu dosya bir ASP.NET Core projesinde top level statement’leri içerebilecek tek dosyadır, Visual Studio tarafından "ASP.NET Core Empty" şablonuyla bir web uygulaması oluşturulduğunda oluşan Program.cs dosyasının içeriği şöyle olacaktır:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
 
app.MapGet("/", () => "Merhaba Dünya!");
 
app.Run();

Birinci satırda WebApplication sınıfının CreateBuilder metodu çağrılmaktadır ve sonuç WebApplicationBuilder tipindeki builder değişkenine atanmaktadır. Burada sunucunun ayağa kalkması için temel yapılandırmalar yapılmaktadır. Uygulamaya servis eklemeler builder değişkeni üzerinden olur.

İkinci satırda, oluşan builder nesnesi üzerinden Build metodu çağrılmaktadır ve sonuç WebApplication tipindeki app değişkenine atanmaktadır. Burada bir önceki satırda yapılan yapılandırma ayarları gerçekleştirilmektedir. Ayrıca oluşan app değişkeni aracılığıyla middleware’ler request pipeline’a eklenmektedir.

Üçüncü satır bir rota tanımını içermektedir. Bu rota, kök dizine gelen istekleri response'a “Merhaba Dünya!” yazan lambda ifadesi şeklinde tanımlanmış bir endpoint'e eşlemektedir. Endpoint'ler ve routing mekanizması kitabın ilerleyen bölümlerinde işlenecektir. MapGet() IEndPointRouteBuilder arayüzüne ait bir metottur. Bu arayüz WebApplication sınıfı tarafından da kullanılmaktadır. Dolayısıyla MapGet() metodu bir WebApplication nesnesi üzerinden de çağrılabilmektedir.

Son satır sunucuyu çalıştırmaktadır.

Kendi Middleware'imizi Oluşturma

[değiştir]

Çoğunlukla Microsoft tarafından oluşturulan hazır middleware'leri request pipeline'a ekleriz. Ancak istersek kendi middleware'lerimizi de request pipeline'a ekleme imkanımız vardır. Bu bölümde sık sık kendi middleware'lerimizi request pipeline'a ekleme ihtiyacı duyacağımızdan değil, request pipeline'ı ve çalışmasını daha iyi anlamak için kendi middleware'lerimizi oluşturup request pipeline'a ekleyeceğiz.

İlk göreceğimiz middleware oluşturma ve request pipeline'a ekleme yöntemi Use() metodunu kullanmaktır. Örnek (Program.cs dosyası):

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Use(async (context, next) => {
    if(context.Request.Method == HttpMethods.Get && context.Request.Query["custom"] == "true")
    {
        context.Response.ContentType = "text/plain";
        await context.Response.WriteAsync("Custom Middleware \n");
    }
    await next();
});
app.MapGet("/", () => "Merhaba Dünya!");
 
app.Run();

Use() metodunun içindeki labmda ifadesinin ilk parametresi (context) HttpContext tipindedir. Bu HttpContext nesnesi üzerinden gelen request'e ulaşabilir ve response'a yazabiliriz. HttpContext sınıfı Microsoft.AspNetCore.Http isim alanı içinde tanımlıdır ve aşağıdaki üyeleri içerir:

Connection: Bu özellik HTTP request'inin altyapısını sağlayan bağlantı hakkında bilgi verir. Örneğin bağlanan (istemci) ve host bilgisayarın IP adresleri, port numaraları vs. Geri dönüş tipi ConnectionInfo'dur.
Request: Biraz önce örnek kullanımını görmüştük. Gelen request hakkında bilgi verir.
RequestServices: Servislere erişim sağlar.
Response: Biraz önce örnek kullanımını görmüştük.Gelen request'e karşılık olarak verilecek Response'u oluşturmaya yarar.
Session: Mevcut oturum hakkında bilgi verir.
User: Request'i yapan kullanıcı hakkında bilgi verir.
Features: Request özelliklerine erişim sağlar. Request yönetiminin daha düşük seviye yönleriye ilgilenir.

HttpContext sınıfının Request özelliğiyle erişilen HttpRequest sınıfının önemli üyeleri şunlardır:

Body: Request'in gövdesini döndürür.
ContentLength: Request'in ContentLength başlığının değerini döndürür.
ContentType: Request'in ContentType başlığının değerini döndürür.
Cookies: Request'in içinde cookie'ler varsa onları döndürür.
Form: Request'in body'sinin form karşılığını döndürür.
Headers: Request'in başlıklarını döndürür.
IsHttps: Gelen request HTTPS bağlantısı üzerinden yapılmışsa true döndürür.
Method: Request'in verb'ünü döndürür. (GET, POST, vs.)
Path: Request URL'sinin path kısmını döndürür.
Query: Request URL'sinin query string kısmını döndürür. (anahtar-değer çiftleri şeklinde)

HttpContext sınıfının Response özelliğiyle erişilen HttpResponse sınıfının önemli üyeleri şunlardır:

ContentLength: Response'un ContentLength başlığını ayarlar.
ContentType: Response'un ContentType başlığını ayarlar.
Cookies: İstemciye cookie gönderilmesini sağlar.
HasStarted: Önemli bir özelliktir. Eğer true döndürmüşse sunucu response'un başlıklarını istemciye göndermeye başlamıştır. Bu noktadan sonra response'un durum kodu ve başlıkları değiştirilemez.
Headers: Response'un başlıklarını oluşturmaya/değiştirmeye yarar.
StatusCode: Response'un durum kodunu ayarlamaya yarar. (404, vs.)
WriteAsync(data): Response'un gövdesine string şeklinde veri yazar.
Redirect(url): Başka bir URL'ye yönlendirme yapmaya yarar.

Yukarıdaki örneğimizde HttpContext nesnesi üzerinden request'e ve response'a erişilmektedir. Eğer request bir GET request'i ise ve "custom" query string'i varsa ve değeri "true" ise response'a "Custom Middleware" stringini yazmakta ve akışı bir sonraki middleware'e devretmektedir.

Yukarıdaki koddaki lambda ifadesinin ikinci parametresi next'tir ve sıradaki middleware'i çağırmaya yarar. Dikkat ettiuseniz next() metodu çağrılırken argüman verilmemiştir. Çünkü ASP.NET Core next() metoduna argümanları arkaplanda kendisi verir.

Kendi belirtiğimiz middleware ve request pipeline'daki diğer middleware'ler asenkron çalışır. Bu yüzden kendi yazdığımız middleware'i async ile işaretledik, ayrıca sıradaki middleware'i de await ile çağırdık.

Sınıf kullanarak kendi middleware'imizi oluşturma

[değiştir]

Yukarıdaki örnekte lambda ifadesi kullanarak kendi middleware'imizi oluşturduk, ama bu yaklaşım gereksiz yere Program.cs dosyamızı kalabalıklaştıracak ve middleware'in yeniden kullanılabilirliğine zarar verecektir. Şimdi sınıf tabanlı bir middleware yazalım. Projenize içeriği aşağıdaki gibi olan yeni bir sınıf dosyası ekleyin.

public class QueryStringMiddleware
{
    private RequestDelegate next;
    public QueryStringMiddleware(RequestDelegate next)
    {
        this.next=next;
    }
    public async Task Invoke(HttpContext context)
    {
        if(context.Request.Method == HttpMethods.Get && context.Request.Query["custom"] == "true")
        {
            if(!context.Response.HasStarted)
            {
                context.Response.ContentType = "text/plain";
            }
            await context.Response.WriteAsync("Class-based Middleware \n");
       }
       await next(context);
    }
}

Sınıf tabanlı bir middleware yazdığımız zaman sıradaki middleware'i sınıfımızda RequestDelegate tipinde bir field olarak tutarız. Invoke() metodunda parametre olarak HttpContext nesnesini alır ve üzerinde işlem yaparız. Yapıcı metotta request pipeline'daki bir sonraki middleware'i temsil eden metodu parametre olarak alır ve aynı sınıftaki field'a atarız. Invoke metodu içinde akışı sıradaki middleware'e devretmek istiyorsak RequestDelegate temsilcisi türünden olan ilgili field'ı HttpContext değişkenini parametre olarak vererek çağırırız. Program.cs dosyasında bu middleware kullanıldığı zaman önce bu sınıf türünden nesne oluşturulur, nesne oluşturulurken request pipeline'daki bir sonraki metot yapıcı metot parametresi olarak verilir. Daha sonra sunucuya her talep geldiğinde bu nesne üzerinden Invoke metodu çağrılır. Her request'te bu Invoke() metodu çağrılırken geçerli context'i temsil eden ve içinde HttpRequest ve HttpResponse nesnelerini barındıran HttpContext nesnesi bu metoda parametre olarak verilir. Sınıf tabanlı middleware'ler request pipeline'a şöyle eklenir:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Use(async (context, next) => {
    if(context.Request.Method == HttpMethods.Get && context.Request.Query["custom"] == "true")
    {
        context.Response.ContentType = "text/plain";
        await context.Response.WriteAsync("Custom Middleware \n");
    }
    await next();
});
app.UseMiddleware<QueryStringMiddleware>();
app.MapGet("/", () => "Merhaba Dünya!");
 
app.Run();

Gördüğünüz gibi sınıf tabanlı middleware'ı request pipeline'a eklemek için middleware'i eklemek istediğimiz yerde UseMiddleware() metodunu çağırmalı ve tip parametresine middleware sınıfımızı koymalıyız.

Program.cs dosyasındaki kodlar sunucu ayağa kalkarken yalnızca bir kez çalıştırılır. ASP.NET Core Program.cs dosyasındaki middleware eklemelerine bakarak bir request pipeline inşa eder. Pipeline inşa edildikten sonra her gelen talep bu pipeline'dan geçer. Middleware'leri temsil eden metotları çağırmek için gerekli olabilecek HttpContext ve RequestDelegate nesnelerini ASP.NET Core otomatik sağlar.

Middleware'lerin geri dönüş kısımları

[değiştir]

Daha önce de söylediğimiz gibi middleware'lerin bir geliş kısımları, bir de geri dönüş kısımları vardır. Biz şimdiye kadar middleware'lerin hep geliş kısımlarına bir şeyler yazdık, geri dönüş kısımlarına bir şey yazmadık. Şimdi geri dönüş kısımlarına da bir şeyler yazacağız. Örnek:

app.Use(async (context, next) => {
    await next();
    await context.Response.WriteAsync($"\nStatus Code: {context.Response.StatusCode}");
});

Program.cs dosyasındaki ilgili yerine eklenen bu middleware akışı ilk önce sıradaki middleware'e yönlendirmekte ve sadece dönüş yolunda response'a yazmaktadır. Middleware'ler sıradaki middleware'e yönlendirme yapmadan önce (giriş yolunda), sıradaki middleware'e yönlendirme yaptıktan sonra (dönüş yolunda) veya her ikisinde de işlem yapabilmektedir.

Request pipeline'a kısa devre yaptırma

[değiştir]

Bir middleware akışı sıradaki middleware'e devretmezse request pipeline'a kısa devre yaptırmış olur, daha içteki middleware'ler çalışmaz. Örnek:

app.Use(async (context, next) => {
    if(context.Request.Path == "/short") {
        await.context.Response.WriteAsync("Request Short Circuited");
    }
    else {
        await next();
    }
});

Program.cs dosyasındaki ilgili yerine eklenen bu middleware eğer gelen request'in path'ı "short" ise response'a "Request Short Circuited" yazmakta ve akışı sıradaki middleware'e yönlendirmemektedir (request pipeline'a kısa devre yaptırmaktadır). Eğer request'in path'ı "short" değilse akışı normal şekilde sıradaki middleware'e yönlendirmektedir.

Request pipeline'a kısa devre yaptıran middleware'ler daha sonra tanımlanan middleware'lerin çalışmasını engellerler, daha önce tanımlanan middleware'lerin dönüş kısmının çalışmasını engellemezler.

Pipeline'da dallanma oluşturma

[değiştir]

Map() metodu request pipeline'da bir dallanma oluşturma için kullanılır. Örnek:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Map("/branch", branch => {
    branch.Use(async (HttpContext context, Func<Task> next) => {
        branch.UseMiddleware<QueryStringMiddleWare>();
        await context.Response.WriteAsync($"Branch Middleware");
    });
});
app.UseMiddleware<QueryStringMiddleWare>();
app.MapGet("/", () => "Hello World!");
app.Run();

Bu kod "/branch" path'ıyla başlayan requestler için ayrı bir dallanma oluşturmaktadır. Eğer bir request "/branch" path'ıyla başlıyorsa örneğimizde iki farklı middleware'e sokulacaktır. Eğer request bu path'la başlamıyorsa normal yoluna devam edecektir. Eğer request pipeline'da bir dallanma yapıldıysa ana dala geri dönülemez. Dolayısıyla örneğimizde görüldüğü gibi daldaki son middleware request pipeline'a kısa devre yaptırmak zorundadır.

Request pipeline'da koşullu dallanma oluşturma

[değiştir]

Yukarıdaki örnekte gelen request'in başlangıç path'ına bakarak dallanma oluşturduk. İstersek gelen request'in başka özelliklerine bakarak da dallanma oluşturabilirdik. Örnek:

app.MapWhen(context => context.Request.Query.Keys.Contains("branch"), branch => {
    // ...
});

Bu örnekte gelen request'in query string'inde belirli bir anahtar olup olmamasına bağlı olarak request'i bir dala yönlendiriyoruz.

Terminal middleware oluşturma

[değiştir]

Terminal middleware demek akışı bir sonraki middleware'e yönlendirmeyen middleware demektir. Terminal middleware'in request pipeline'a kısa devre yaptıran middleware'lerden farkı hiç bir zaman akışı bir sonraki middleware'e devretmemesidir. Kısa devre yaptıran middleware'ler çeşitli koşullara göre akışı bir sonraki middleware'e devretme veya devretmemeyi seçebilir.

Use() veya UseMiddleware() metodunu kullanıp sıradaki middleware'ı çağırmazsak terminal middleware oluşturmuş oluruz. Veya sadece terminal middleware oluşturmak için kullanılan Run() metodunu kullanabiliriz. Örnek:

app.Run(async (context) => {
    await context.Response.WriteAsync($"Terminal Middleware");
});

Terminal middleware'leri belirtmek için kullanılan fonksiyonlar next parametresi alamaz. Sınıf tabanlı middleware'ler de terminal middleware olarak kullanılabilir. Örneğin QueryStringMiddleware sınıfını şöyle değiştirelim:

public class QueryStringMiddleWare
{
    private RequestDelegate next;
    public QueryStringMiddleWare()
    {
        // bir şey yapma
    }
    public QueryStringMiddleWare(RequestDelegate next)
    {
        this.next = next;
    }
    public async Task Invoke(HttpContext context)
    {
        if (context.Request.Method == HttpMethods.Get && context.Request.Query["custom"] == "true")
        {
            if (!context.Response.HasStarted)
            {
                context.Response.ContentType = "text/plain";
            }
            await context.Response.WriteAsync("Class-based Middleware \n");
        }
        if (next != null)
        {
            await next(context);
        }
    }
}

Yaptığımız değişiklikle QueryStringMiddleware middleware'i hem non-terminal hem de terminal middleware olarak kullanılabilir. UseMiddleware<QueryStringMiddleware>() çağrısıyla kullanılırsa parametre alan yapıcı metot çalışır ve non-terminal middleware olarak kullanılır. Terminal middleware olarak kullanmak içinse aşağıdaki kodu yazarız:

app.Run(new QueryStringMiddleware().Invoke);

Run() metodunun RequestDelegate temsilcisiyle temsil edilebilen bir parametre alması yeterlidir. Bir metodun RequestDelegate tipiyle temsil edilebilmesi için geri dönüş tipinin Task olması ve HttpContext tipinde tek bir parametre alması yeterlidir.

Middleware'leri yapılandırma

[değiştir]

Bazen middleware'leri olduğu gibi kullanmak yetmeyebilir. Bazen middleware'lerin çalışmasını isteklerimize göre özelleştirme ihtiyacı duyabiliriz. Middleware'leri konfigure etmek için çoğunlukla "options pattern" dediğimiz bir pattern kullanılır. İlk yapmamız gereken middleware için ayaraları içeren bir sınıf oluşturmaktır:

public class MiddlewareAyarlari
{
    public bool HizliCalissin { get; set; }
}

Şimdi Program.cs dosyasını şöyle değiştirelim:

using Microsoft.Extensions.Options;
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<MiddlewareAyarlari>(options => {
    options.HizliCalissin = true;
});
var app = builder.Build();
app.MapGet("/location", async (HttpContext context, IOptions<MiddlewareAyarlari> msgOpts) => {
    MiddlewareAyarlari opts = msgOpts.Value;
    await context.Response.WriteAsync(opts.HizliCalissin.ToString());
});
app.MapGet("/", () => "Hello World!");
app.Run();

Bu kodda öncelikle

builder.Services.Configure<MiddlewareAyarlari>(options => {
    options.HizliCalissin = true;
});

satırlarıyla middleware için yapılandırma ayarları belirtilmektedir. Bu middleware için tek kullanılabilecek ayar hızlı çalışmasıdır. Örneğimizde middleware hızlı çalışması için ayarlanmaktadır.

app.MapGet("/location", async (HttpContext context, IOptions<MiddlewareAyarlari> msgOpts) => {
    MiddlewareAyarlari opts = msgOpts.Value;
    await context.Response.WriteAsync(opts.HizliCalissin.ToString());
});

Burada ise middleware'in kullanacağı ayarlar middleware'e dışarıdan enjekte edilmektedir. Middleware yapılandırma ayarlarına kendi kod bloğu içinde erişebilmekte ve çalışmasını verilen yapılandırma ayarlarına göre değiştirebilmektedir.

Sınıf tabanlı middleware'de options pattern'inin kullanımı

[değiştir]

Sınıf tabanlı middleware'lerde options pattern'i kullanılarak yapılandırılabilmektedir. Şimdi projenize LocationMiddleware isimli yeni bir sınıf ekleyin ve içeriğini şöyle değiştirin:

using Microsoft.Extensions.Options;
public class LocationMiddleware
{
    private RequestDelegate next;
    private MiddlewareAyarlari options;
    public LocationMiddleware(RequestDelegate nextDelegate, IOptions<MiddlewareAyarlari> opts)
    {
        next = nextDelegate;
        options = opts.Value;
    }
    public async Task Invoke(HttpContext context)
    {
        if (context.Request.Path == "/location")
        {
            await context.Response.WriteAsync(options.HizliCalissin.ToString());
        }
        else
        {
            await next(context);
        }
    }
}

Request pipeline'a bu middleware'i

app.UseMiddleware<LocationMiddleware>();

satırını kullanarak ekleyebiliriz. Gerçek hayatta her bir middleware için ayrı bir yapılandırma sınıfı olması gerekir. Biz örneğimizde basitlik açısından aynı yapılandırma sınıfını kullandık. Yine bu middleware de hızlı çalışıp çalışmayacağı hakkındaki bilgiyi Program.cs dosyasındaki

builder.Services.Configure<MiddlewareAyarlari>(options => {
    options.HizliCalissin = true;
});

şeklinde belirttiğimiz yapılandırma ayarlarından almaktadır.