ASP.NET Core 5/6 Mıddleware Ve Request Pıpelıne

Mehmed Emre AKDİN
SDTR
Published in
16 min readNov 7, 2021

Herkese yeniden merhaba,

Umarım sizler için her şey yolundadır. Son zamanlarda yeni şeyler öğrenmek yerine önceden bildiğim fakat üzerinde çok fazla durmadığım veya iyi bildiğim fakat zihnimin raflarında tozlanmış olan bilgilerimi tazelemeye çalışıyorum. Geçmişte ASP.NET Core’da RESTful servislerle uğraştığım çalışmalarıma göz atarken yine o günlerde de karşıma çıkan middleware yapısı ile tekrar karşılaştım. Evet ASP.NET Core’da middleware yapılanması hakkında araştırmam olmuştu. Fakat bu konuyu farklı kaynaklardan okuyup okuduklarımı zihnimde karşılaştırma ve detaylandırma fırsatım olmamıştı. Aklıma konu üzerindeki bilgilerimi tazeleyip, daha fazla araştırma yaparak konu hakkında yazı yazmak geldi. Ve çok geçmeden kendimi bu satırları yazarken buldum. Hazır olduğumuzu düşünerek başlıyorum.

ASP.NET Core Middleware Nedir?

Öncelikle türkçesini “Arayazılım/Arakatman” olarak çevirebileceğimiz middleware kavramı sadece ASP.NET Core’a özgü bir kavram değildir. Genel olarak WEB mimarilerinde request(istek)’den, response(cevap)’a kadar gerçekleşen süreç middleware olarak adlandırılır. Eğer ASP.NET Core içinde bir tanımlama yapmamız gerekirse, uygulamamıza gelen HTTP request(istek) ve response(cevap) ‘ larını işlememize olanak sağlayan C# sınıflardır demek doğru olacaktır. Bunu bir nevi filtreleme işlemi olarak düşünebilirsiniz. Yani normal bir süreçte uygulamamıza gelen istekler, tanımlanan middleware ‘lerden geçerek uygulamamıza ulaşır, uygulamamızın cevabı oluşturulduktan sonra bu cevap yine tanımlanan middleware lerden geçerek kaynağa ulaşır ve süreç tamamlanır. Middleware‘ler dememden anlayacağınız üzere birden çok tanımlanabilirler ve çeşitli amaçlarla kullanılırlar. Aklımızda bazı şeylerin hafiften şekillenmeye başlaması için middleware‘lerin kullanılabileceği bazı alanlara örnekler vermek istiyorum:

— Authentıcatıon(Kimliklendirme)

— Authorızatıon(Yetkilendirme)

— Loggıng(Kayıt Altına Alma)

— Cachıng(Ön Belleğe Alma)

— Exceptıon Handlıng (Hata İşleme)

— Javascript veya CSS dosyaları gibi statik dosyaları işlemek için kullanılabilir.

Bu saydıklarım en çok akla gelen kullanım amaçları olmakla beraber kendi middleware’nizi daha farklı bir amaç doğrultusunda oluşturabilirsiniz. Bizim bu yazıda ki ana odağımız middleware yapılanması olacaktır. Ancak ben middleware yapılanmasını anlatırken , ASP.NET Core uygulamamızın ayağa kalkma sürecinden ve bir isteğin uygulamamıza nasıl ulaştığından kısaca bahsetmek istiyorum. Çünkü işin öncül tarafını görerek Request Pipeline ‘nın oluşmasına ve HTTP isteklerinin nasıl dinlenir hale geldiğine biraz da olsa değinmek, olaya daha hakim olmamızı sağlayacaktır. Yeri gelmişken söyleyeyim tüm konuyu ASP.NET Core 5 Web API uygulaması üzerinden anlatacağım.

ASP.NET Core 5 Web API Uygulamamız Ayağa Kalkıyor

Bir ASP.NET Core 5 App oluşturduğumuzda veya bir ASP.NET Core 5 Web API oluşturduğumuzda oluşan standart dosyalara bakacak olursak, gözümüze ilk çarpan dosyalar ”Program.cs” ve “Startup.cs” dosyaları olacaktır. Bu sınıflar bir ASP.NET Core uygulaması oluştuğunda otomatik olarak oluşturulurlar. Şimdi uygulamamızın ayağa kalkma sürecini inceleyerek kısaca bu sınıfların işlevlerine değinelim.

Program.cs Ve Startup.cs Sınıfları

Program.cs sınıfı kısaca ASP.NET Core uygulamamızın başlatıldığı sınıftır diyebiliriz. Bu sınıfta bir Main() metodumuz mevcuttur. Bunun sebebi ASP.NET Core uygulamamızın bir konsol uygulaması olarak başlamasıdır. Hatırlarsak eğer .NET’ te bir konsol uygulaması oluşturduğumuzda, varsayılan olarak “.NET Framework”, bize bir “Main()” metodu ile birlikte bir sınıf (Program.cs) veriyordu. Biz biliyoruz ki, “Main()” metodu konsol uygulamasının yürütülmesi için bir giriş noktası yani Entry Point ‘ tir. ASP .NET Core için de “Main()” metodu ASP.NET Core Application’ı yapılandırarak başlatma görevini üstlenir. Ve bu noktada uygulama bir ASP.NET Core Web uygulaması haline gelir. Peki bu yapılandırma nasıl yapılır ve istekler nasıl dinlenir hale gelir kısaca bir göz atalım. Aşağıda bir ASP.NET Core uygulamasındaki standart bir “Program.cs” sınıfı verilmiştir:

Eğer Main() metodunu incelersek, beşinci satırda işletim sisteminden gelen “command-line arguments” yani komut satırı değişkenlerini kullanarak CreateHostBuilder(args) ‘a çağrı yapıldığını göreceğiz. CreateHostBuilder(args) metodu IHostBuilder ınterface nesnesi döndürerek kendi içerisinde Host sınıfındaki statik CreateDefaultBuilder() metodunu çağırır. Bu metodun işlevlerinden bazıları şunlardır:

  • ILoggerFactory ’ nin yapılandırılması
  • Configuration’dan appsettings.json ve apsettings.(Environment).json ‘nın yüklenmesi
  • Host’un yapılandırılması

Devamında ise ConfigureWebHostDefaults() metodu çağrılır. Bu metot ile yapılan bazı işlemler ise şunlardır:

  • Kestrel sunucusu web sunucu olarak ayarlanması
  • IIS Entegrasyonunu etkinleştirilmesi gibi

Ve görüldüğü üzere ConfigureWebHostDefaults() metodu içinde “IWebHostBuilder” ‘ın extension metodu olan UseStartup<Startup>() “Startup” parametresi ile çağırılarak başlangıç sınıfı olarak ayarlanıyor.

Not: “Startup.cs” sınıfımızın adını değiştirip, örneğin “MyStartup.cs” yaparsak “UseStartup” çağrımı UseStartup<MyStartup>() şeklinde olacaktır.

Ardından Oluşan IHostBuilder nesnesi üzerinden Build() metodu çağırılarak IHost nesnesi oluşturulur. Bu aşamada “Startup.cs” ‘ deki “ConfıgureServıce” metodu tetiklenir. Aşağıda ASP.NET Core Web API için“Startup.cs” dosyası verilmiştir:

ConfıgureServıce() metodu’na kısaca değinmek gerekirse uygulamamız için gerekli servisleri register ettiğini bilmemiz şuan için yeterli olacaktır. Ayrınca şunu da belirtmekte fayda var ki bu aşamada uygulamamız hala istekleri dinler pozisyonda değildir.

Ardından IHost nesnesi yardımıyla Run() metodu icra edilir bu sayede Startup.cs ’deki asıl konumuzun döneceği Configure() metodu çalıştırılarak middleware ‘lerimiz register edilir ve HTTP Pipeline ’ı oluşturulur. Bu sayede uygulamamız HTTP isteklerini dinler hale gelir. Configure() metodu temel olarak bizlere uygulamamızın HTTP isteklerini nasıl işleyeceğini ve nasıl yanıt göndereceğini kontrol eden Middleware Pipeline ‘ı sağlar.

ASP.NET Core 6 İçin Program.cs Ve Startup.cs Sınıfları

Evet bildiğiniz üzere C#10 ve Visual Studio 2022 ile birlikte 9 Kasım 2021'de piyasaya sürülecek. Eli kulağında olan bu güncellemenin preview’leri zaten yayınlanmıştı. Bunları baz alarak ASP.NET Core 6 için Program.cs ve Startup.cs sınıflarının nasıl tanımlanabildiğine de değinmek istiyorum. Belki de bu makaleyi okuduğunuz sırada projenizde Startup.cs dosyası olmayacak. 😊

Bu değişiklikler C# 9'un bir parçası olan Top Level Programmıng’i tamamıyla kullanan C#10 ile birlikte karşımıza çıkmaktadır. Program.cs ve Startup.cs sınıfı, Program.cs sınıfında birleştirilerek kod yapısında daha da sadeliğe gidilmiştir:

Evet gördüğünüz üzere Program.cs sınıfında ki main metodu yok. Ve her iki sınıfı oldukça basite indirgemiş şekilde karşımıza çıkıyor.

Not: Yukarıda ki gibi tanımlama yapabilmek için .NET Core projeniz için aşağıdakileri yapmanız gerekmekte:

1- Yeni .NET SDK’yı kurun. https://dotnet.microsoft.com/download/dotnet/6.0

2- Yeni .NET Runtime’ı kurun.
https://dotnet.microsoft.com/download/dotnet/6.0

3-Ardından projenize sol tıklayıp “Edit Project File” diyin. Veya direk olarak projenize iki kere sol tıklama yapabilirsiz. Projenizin açılan .csproj dosyasındaki TargetFramework’ü aşağıdaki gibi değiştirin:

<TargetFramework>net6.0</TargetFramework>

ASP.NET Core HTTP Request Süreci

Son olarak middleware yapılanmasına adım atmadan önce büyük resme bir göz atalım. Aşağıda ASP.NET Core uygulaması için bir HTTP isteğinin hangi süreçlerden geçtiği şematize edilmiştir:

Out-Of-Process Hosting Model (Microsoft Document)

Bildiğiniz üzere IIS kullanmak zorunda değiliz. ASP.NET Core “Cross Platform” yani platform bağımsız olduğundan IIS yerine “NGINX,Apache” gibi Reverse Proxy Server ’larını da kullanabiliriz. Hatta herhangi bir Reverse Proxy Server bile kullanmayıp direk olarak Kestrel Server ‘a istekte de bulunabiliriz. Kestrel Server şekilden de görüleceği üzere Asp.NET Core uygulamaları için kullanılan varsayılan bir web sunucusudur. Asp.Net Core uygulamaları, web isteğini işlemek için Kestrel web sunucusunu “In-Process Server”(işlem içi sunucu) olarak çalıştırır. Kestrel Server, “IIS, NGINX, Apache”, vb. gibi web sunucularının tüm gelişmiş özelliklerine sahip olmayan çok hafif bir web sunucusudur. Bu yüzden daha hızlıdır. Kestrel Server sayesinde Asp.Net Core uygulamaları platform bağımsız çalışabilir. Çünkü her web sunucunun farklı bir “Startup Confıguratıon” ‘nı vardır. Asp.NET Core uygulamaları, çapraz platform desteği sunabilmek adına uygulamanın aynı ​​(Main() ve Startup.Configure()) işlemine sahip olacağı bir “In-Process Server”(işlem içi sunucu) olarak Kestrel web sunucusunu kullanır. Bunun dışında Kestrel Server uygulama performansının iyileştirilmesine yardımcı olarak verimli bir istek işleme süreci sağlar.

Kestrel Server ‘dan kısaca bahsettikten sonra IIS’e istek atarak süreci başlatalım. IIS’e gelen isteğimiz “AspNetCoreModule” tarafından Kestrel Server ‘a iletilir. Temel olarak “AspNetCoreModule” tüm trafiği ASP.NET Core Uygulamasına yönlendiren bir IIS modülüdür. İstek Kestrel Server ‘a iletildikden sonra Kestrel Server ,IIS(Reverse Proxy Server) tarafından iletilen isteği alarak uygulamanın geri kalanı tarafından kullanılacak olan HttpContext nesnesine dönüştürür. Ardından isteğimiz middleware ’lerden geçerek “AppCode” yani uygulamamıza ulaşır.

HttpContext hakkında kısa bir bilgi verelim:

ASP.NET Core web sunucusu yani Kestrel Server tarafından oluşturulan HttpContext nesnesi, isteğin özellikleri ve istek ile ilgili hizmetleri barındırır.

HttpContext” ‘in bazı üyelerini aşağıdadır:

Properties:
Connection: İstek için temel alınan ağ bağlantısı hakkındaki bilgileri alır.
Request: İstek için HttpRequest nesnesini alır.
Response: İstek için HttpResponse nesnesini alır.
Sessıon: İstek için kullanıcı oturum verilerini yönetmek için kullanılan nesneyi alır veya ayarlar.
Methods:
Abort(): İsteğin altında yatan bağlantıyı iptal eder.

Sıra geldi middleware yapılanmasının temelini oluşturan Confıgure() metodunu incelemeye:

Configure(IApplicationBuilder,IWebHostEnvironment) metodu IApplicationBuilder interface’nin bir örneğini kullanarak uygulamamız için Request Pipeline ‘nını yapılandırmamızı sağlayan metottur. IApplicationBuilder interface’i bize Request Pipeline ‘nını yapılandırmak için mekanizmalar sağlayan bir sınıf tanımlar. Başka bir şekilde söylemek gerekirse Confıgure() metodu içinde IApplicationBuilder ‘ı kullanarak uygulamamızın Request Pipeline ‘nına middleware ekleriz.

Uygulamamızı ilk oluşturduğumuzda Configure() metodu içinde gelen varsayılan olarak çağırılmış middleware‘lere kısaca bir göz atalım:

ASP.NET Core 5 Web API Confıgure() metodunun içeriği:

  • Üçüncü satırda env.IsDevelopment() ile Development modunda olup olunmadığı kontrol edilir. Eğer “Development” modun da ise UseDeveloperExceptionPage() middleware ’i çalışır. Bu middleware geliştirme sırasında runtime hatalarını yakalamak için kullanılır.
  • app.UseSwagger() middleware ’i kullanılan Swagger’ı JSON Endpoint olarak sunmak üzere çağırılır.
  • app.UseSwaggerUI() middleware ‘i ile JSON Endpoint’i belirlenir.
  • env.IsDevelopment() kontolünde eğer “Productıon” modundaysak UseExceptionHandler () middleware’i runtime hatalarını özelleştirilmiş sayfamıza yönlendirmek için kullanılır.
  • UseHttpsRedirection () middleware’i ise HTTP isteklerini otomatik olarak HTTPS adreslerine yönlendirmeye zorlar.
  • UseStaticFiles () middleware’i HTML,CSS gibi static dosyaların wwwroot’tan alınacak şekilde ayarlanmasını sağlar.
  • UseRouting() middleware’i gelen istekteki URL’in uygulamada hangi endpoint ile eşleşeceğini belirler. Uygulamada tanımlanan endpoint’lere bakarak isteğe göre en doğru endpoint’i seçer.
  • UseAuthentication() middleware’i kimliklendirmeye izin vermek için kullanılır.
  • app.UseAuthorization() middleware’i ise yetkilendirmeye izin vermek için kullanılır.
  • UseEndpoints() middleware ’i , eşleşen endpoint ile ilgili delegate’ti çağırarak execute eder. endpoints.MapControllers() ile ise routing işlemimizi controller aracılığı ile yapacağımızı belirtiriz.([Route], [HttpGet] gibi)

Not: Asp.NET Core uygulamalarında middleware ’ler ‘Use’ ifadesi ile başlamaktadır. Zorunlu değildir fakat bir programlama geleneğidir.

Umarım bu middleware ’lerin sırasının önemli olup olmadığı sorusu aklınıza takılmıştır. Çünkü middleware ’lerin tanımlanma sırası çok büyük önem arz etmektedir. Örneğin:

UseAuthentication() ve UseAuthorization() middleware’leri UseRouting() ve UseEndpoints() arasına yerleştirilmelidir. Çünkü UseAuthentication() ve UseAuthorization() middleware’leri çalıştıkdan sonra hangi endpoint’in çalışacağını bilmek zorundadır. Eğer bu middleware ’ler UseRouting() middleware’inden önce çalışırlarsa sonunda hangi endpoint’in çalışacağını bilemezler. Başka bir örnek olarak önce UseAuthentication() ile kimliklendirme yapılmalı , ardından UseAuthorization() ile yetki kontrolü yapılmalıdır.

Inlıne Middlewares

ASP.NET Core mimarisinde Configure() metodu içinde bize hazır ve ınlıne olarak kullanabileceğimiz bazı middleware ’ler sunulmuştur. Bu middleware ’lerin method signature’ları aşağıdaki şekildedir:

Method signature’larından gördüğünüz üzere hepsi IApplicationBuilder interface’ne ait bir Extension metotdur.

Örneklerimizi basit bir API oluşturup endpoint’imize istek atarak gözlemleyeceğiz. Oluşturduğumuz basit endpoint’lerimiz aşağıdaki şekildedir:

Use

Request Pipeline ’da birden çok middleware’i birbirine bağlar. Örneğin request üzerinde işlem yaptıkdan sonra bir sonraki middleware’i çağırabilir. Ve ardından response üzerinde işlem yaparak öncül middleware’e geri dönebilir.

Use Extension metodu Func<HttpContext,Func<T>,Task> parametresi alır. Func önceden tanımlanmış bir delegate’i ifade eder. Yani Use metodu ilk parametresi HttpContext olan , ikinci parametresi ise Func<T>(senkron karşılığı void) olan ve geriye Task döndüren bir metot alır. HttpContext bizim HTTP isteğimiz hakkındaki bilgileri barındırır. Func<T> ise kendisinden sonraki metodu çağırmak ve bir sonraki middleware’i tetiklemek için kullanılan bir delegate’tir.

Delegate’ler kısaca metodların adreslerini tutabilen yani onları temsil edebilen yapılardır. Daha detaylı bilgi için : Microsoft

Örneğimize Geçelim:

Not: İlk örnekte tüm Configure() metodunu gösterdim. Diğer örneklerde fazla yer kaplamaması açısından sadece tanımladığımız middleware’leri göstereceğim.

İsteğin Postman üzerinden gönderilmesi:

Request Result:

Debug Output:

Entered The First Middleware!
Entered The Second Middleware!
Exited The Second Middleware!
Exited The First Middleware!

Sürecin işleyişini şekil üzerinden gösterelim:

Şekile dikkatli bakarsak eğer her şeyin daha net olarak anlaşılacağını düşünüyorum. İstek sırasında middleware’ler bir zincir misali birbirlerini çağırmaktadırlar. Bunu recursive bir metoda benzetebilirsiniz. Burayı anlamış olmak Custom Middleware tanımlarken işimizi kolaylaştıracaktır.

Run

Uygulamanın Request Pipeline ’nına Terminal middleware’i ekler. Terminal middleware dememizin sebebi Request Pipeline ’nına kısa devre yaptırmasıdır. Yani Run middleware’inden sonra gelen middleware’ler koşulmaz. Ve Request Pipeline sonlanır.

Metod signature’larına bakacak olursak eğer Run middleware RequestDelegate parametresi alan Extension bir metotdur. RequestDelegate ise HttpContext parametresi alan ve geriye Task döndüren bir delegate ‘tir.

Şimdi Request Pipeline’ ımızda Run ve Use tanımlayarak davranışlarını birlikte inceleyelim:

Var olan middleware’lerimizin arasına Run middleware’i ekledik. Şimdi API’mıza herhangi bir istek daha gönderip sonucu inceleyelim:

Request Result:

Request terminated!

Debug Output:

Entered The Use Middleware!
Entered The Run Middleware!
Exited The Use Middleware!

Görüldüğü üzere Run middleware’inden sonra istek sonlandırılmıştır. Yani istek API’mize ulaşmadan kısa devre yapılmıştır.

Map

Request Pipeline’nını belli bir yola göre dallara ayırabilmek için kullanılır.

Map’in aldığı parametreleri inceleyecek olursak eğer ilk parametresinin string(pathMatch) gibi bir değer olduğunu göreceğiz. Bu parametreye eşleşecek olan yol verilir. İkinci parametre olarak Action<IApplicationBuilder> alır. Action’da Func gibi bir önceden tanımlanmış bir delegate’tir. Sadece delegate’imizin bir değer döndürmesini istemiyorsak Func yerine Action kullanılır. Kısaca ikinci parametremiz IApplicationBuilder interface’ini parametre alan bir metodu temsil eder. IApplicationBuilder örneği sayesinde Map middleware’i içinde başka middleware’leri çağırabiliriz. Örnek üzerinden gidelim:

Map middleware’ini incelersek eğer GetAllUsers() endpoint’i için bir filtrelemenin söz konusu olduğunu görürüz. Yani API’mizdeki GetAllUsers() metoduna istekte bulunulmak istendiğinde Map middleware’i devreye girecektir. Bu demek oluyor ki GetAllUsers() metodu için özel bir Request Pipeline’ı oluşturulacaktır. Ayrıca Map middleware’i içinde UseRouting() ve UserEndpoints() middleware’lerininde çağırımının yapıldığını fark etmişsinizdir. Bunun sebebi Map middleware’inden sonra diğer middleware’lerin koşulmamasıdır.

Request Result:

[{"id": 1,"name": "Mehmed","surname": "AKDİN"},{"id": 2,"name": "Adil Mert","surname": "Şahin"},{"id": 3,"name": "Aleyna","surname": "Şen"},{"id": 4,"name": "Merve","surname": "Yılmaz"}]

Debug Output:

Entered The First Middleware!
Entered The Second Middleware!
Exited The Second Middleware!
Exited The First Middleware!

Sonuçlara bakarsak eğer GetAllUsers() metoduna istek yapıldığında Map middleware’i ile ilgili yol eşleştirilerek ikinci Use middleware’inin icra edilmesi sağlanmıştır. Eğer Map middleware’i olmasaydı veya GetAllUsers() metoduna istek de bulunmasaydık çıktı aşağıdaki gibi olacaktı:

Entered The First Middleware!
Entered The Thırd Middleware!
Exited The Thırd Middleware!
Exited The First Middleware!

Kısaca Map middleware’i sayesinde isteğin adresine göre özel bir pipeline oluşturmuş olduk.

MapWhen

Map middleware’inde olduğu gibi Request Pipeline’nını belli bir yola göre dallara ayırabilmek için kullanılır. Fakat MapWhen middleware’i daha güçlüdür. Çünkü Map middleware’indeki string olarak verilen path yerine, koşul bazlı filtreleme yapmamıza olanak tanır.

Aldığı parametrelere bakacak olursak eğer Map middleware’indeki string olarak verilebilen isteğin yolu yerine Func<HttpContext, bool> parametresi gelmiştir. Yani artık birinci parametremiz HttpContext alıp geriye bool değer döndüren bir fonksiyondur. Örnek üzerinden gidelim:

Görüldüğün üzere MapWhen middleware’inin ilk parametresi ile gelen isteğin yöntemini kontrol ediyoruz. Eğer bir POST isteği ise pipele’nin yolunu değiştiriyoruz. Dikkat ederseniz eğer MapWhen içinde UseRouting() ve UseEndpoints() middleware’leri yeniden tanımlanmıştır. Çünkü Map middleware’inde olduğu gibi MapWhen middleware’inden sonrada süreç otomatik sonlandırılır. Şimdi AddUser endpoint’imize istekte bulunalım:

Request Result:

{"id": 9,"name": "Ahmet","surname": "Tekin"}

Debug Output:

Entered The First Middleware!
Entered The Second Middleware!
Exited The Second Middleware!
Exited The First Middleware!

Gördüğünüz üzere AddUser endpoint’imize yaptığımız istek sonucunda üçüncü middleware’imiz hiç devreye girmemiştir. Yani MapWhen sayesinde metodun yöntemine göre isteğin gideceği pipeline’ı özelleştirmiş olduk.

UseWhen

Aldığı parametreler ve amaç olarak MapWhen ile birebir aynıdır. UseWhen’nin tek farkı MapWhen veya Map gibi Request Pipeline’nın geri kalanını yürütmeyi otomatik olarak sonlandırmak yerine ardından gelen middleware’leride icra etmesidir. Örneğin:

MapWhen örneği ile kıyaslarsak eğer tek farkı MapWhen içinde kullandığımız UseRouting() ve UseEndpoints() middleware’lerini kullanmak zorunda olmayışımız. Çünkü istek MapWhen’de olduğu gibi otomatik sonlanmayacaktır. Bu sayede hem üçüncü middleware’lerimiz icra edilecek hem de isteğimiz endpoint’imize ek bir tanımlama yapmadan ulaşacaktır. İstek de bulunalım ve sonucu beraber görelim:

Request Result:

{"id": 9,"name": "Ahmet","surname": "Tekin"}

Debug Output:

Entered The First Middleware!
Entered The Second Middleware!
Entered The Thırd Middleware!
Exited The Thırd Middleware!
Exited The Second Middleware!
Exited The First Middleware!

Evet tahmin ettiğimiz gibi tüm middleware’lerimiz sırayla icra edilerek isteğimiz endpoint’imize ulaştı. Eğer AddUser değilde GetAllUser endpoint’imize istekde bulunsaydık sonuç aşağıdaki gibi olacaktı:

Entered The First Middleware!
Entered The Thırd Middleware!
Exited The Thırd Middleware!
Exited The First Middleware!

Not: Map,MapWhen ve UseWhen middleware’lerinin yaptığı işlemleri Use veya Run middleware’leri içinde if-else koşul yapılarını kullanarak da yapabiliriz. Fakat Map,MapWhen ve UseWhen middleware’leri sayesinde daha anlaşılır ve daha temiz yapılar oluşturabiliyoruz.

Son olarak Inlıne Middleware’lerin bazı dezavantajları var. Örneğin kodun tamamı ınlıne(satıriçi) olarak yazılacağından yapacağımız işlemler arttıkça kodun okunurluğu ve bakımı zorlaşacaktır.

Custom Middlewares

ASP.NET Core’da kendi middleware’lerimizi ayrı bir sınıf olarak tanımlayıp kullanabiliyoruz. Middleware’lerimizi ayrı bir sınıf olarak tanımlarken çeşitli yöntemlerimiz mevcuttur. Bunlar aşağıdaki gibidir:

1-) Convention Based Middleware

2-) Factory Based Middleware

Bu yöntemler için tek bir örnek üzerinden gideceğim. Örneğimiz, en başta tanımladığımız servisimize gelen request ve servisimizden dönen response’ları loglama üzerine olacak. Hazırsanız başlayalım:

Convention-Based Middleware

Örneği incelemeye başlarsak, constructor’ımızın bir sonraki middleware’i çağırabilmesi için RequestDelegate ve loglama işlemi yapabilmesi için ILogger parametrelerini aldığını görüyoruz. Burada dikkat edilmesi gereken kısım RequestDelegate parametresidir. Şöyle ki:

Middleware ’ler birbirlerini ardı ardına bir zincir misali çağırabilmek için RequestDelegate tipinde bir delegate alırlar. RequestDelegate, HttpContext ’ i parametre olarak kabul eden ve Task(void) döndüren bir metodu temsil eder. Her delegate bir sonraki delegate’den önce ve sonra işlemlerini gerçekleştirebilir. Aşağıdaki RequestDelegate signature verilmiştir:

Dikkat ederseniz sınıfımız içinde public async Task InvokeAsync imzalı bir metot bulunmakta. Ve ayrıca bu metodun içinde _next(context) ile bir sonraki middleware çağırılmakta. Sizce de bu metod RequestDelegate’in imzasına benzemiyor mu? Dikkatlari toplayalım… Constructor’ımıza enjekte edilen RequestDelegate parametresi bir sonraki middleware çağırmak için kullanılıyor demiştik. Şimdi işin öncül tarafını düşünün, eğer ConventionBasedMiddleware’imizden önce başka bir middleware’imiz olsaydı o middleware’in RequestDelegat parametresi ConventionBasedMiddleware sınıfındaki public async Task InvokeAsync metodunu temsil edecekti. Bu temsilin gerçekleşebilmesi içinde ConventionBasedMiddleware sınıfındaki metodumuz dönüş tipi Task ve HttpContext’i parametre olarak alan bir metot olmak zorundadır. Kısaca InvokeAsync metodu RequestDelegate tanımına uygun olmalıdır. (Aksi takdirde derleyicimiz exception fırlatacaktır.) Bu sayede önceki middleware’de _next(context) çağrısı ile ConventionBasedMiddleware içindeki InvokeAsync metodumuza HttpContext’i parametre geçirebiliyoruz. Çünkü InvokeAsync metodunun request ve response ile ilgili bilgileri tutan HttpContext parametresi aldığını (RequestDelegate’de belirtildiği gibi) zaten biliyoruz.

Not: Eğer bir sonraki middleware çağrılmak istenmiyor ve pipeline’ı sonlandırmak istiyorsak _next(context) çağrısı kaldırılır.

InvokeAsync metodu içinde ki _next(context)’den önceki kısım anlayacağınız üzere request ile icra edilen, sonrasındaki kısım ise response ile icra edilen kısımdır. Bu yüzden _next(context)’den önce request’i sonrasında ise response’u logluyoruz. İşi uzatmamak adına diğer kısımlar yani loglama işlemlerinin yapıldığı kısımın detayına girmeyerek devam ediyorum. Sıra geldi middleware’imizi Configure() metodunda çağırmaya. Bu çağırım iki şekilde yapılabilir. Temelde ikisi de aynıdır. Hatırlarsanız Configure() içindeki tüm middleware’lerimiz IApplicationBuilder’ın bir extension’ıydı. IApplicationBuilder referansı ile middleware’imizi çağırabilmek için extension bir sınıf yazabiliriz:

Ve Configure() içinde aşağıdaki şekilde çağırım yapabiliriz:

Bunun dışında Configure() içinde direk şu şekilde de çağırımını yapabiliriz:

Şimdi servisimizde ki AddUser endpointine istek de bulunalım:

Request Result:

{"id": 9,"name": "Ahmet","surname": "Tekin"}

Log Result:

WebApplication12.Middleware.ConventionBasedMiddleware: Information: Request Host : localhost:44339 Request Path : /AddUser Query String : Request Body : {“id”: 9,“name”: “Ahmet”,“surname”: “Tekin”}WebApplication12.Middleware.ConventionBasedMiddleware: Information: Response Body : {“id”:9,”name”:”Ahmet”,”surname”:”Tekin”}

Factory-Based Middleware

ConventionBasedMiddleware ile kıyaslamak gerekirse, farklı olarak sınıfımızın IMiddleware arayüzünü uyguladığını ve constructor’da yapılan RequestDelegate enjekte işleminin artık yapılmadığını görüyoruz. Burada RequestDelegate referansı doğrudan InvokeAsync metoduna verilmiştir.

Bu yöntemde yapılanlara ek olarak middleware’imizi IoC Contanier’ına kaydetmemiz gerekiyor. Bunun için yazımızda kısaca bahsettiğimiz Startup.cs sınıfında ki ConfigureService() metoduna geri dönmemiz gerekiyor:

3. satırda FactoryBasedMiddleware middleware’mizi IoC Contanier’ına kayıt ediyoruz. Burada dikkat etmemiz gereken birinci nüans IoC Contanier’ına kayıt ederken IMiddleware’i soyut türüyle değil somutlaştırdığımız FactoryBasedMiddleware sınıfı ile kayıt işlemini yapıyoruz. İkinci nüans ise IoC Container’na kayıt yaparken AddScoped ile kayıt yapmış olmamız. Bu kayıt türünü ihtiyacımız doğrultusunda bizim belirlememiz gerekir.

Yeniden servisimizde ki AddUser endpointine istek de bulunalım:

Request Result:

{"id": 9,"name": "Ahmet","surname": "Tekin"}

Log Result:

WebApplication12.Middleware.ConventionBasedMiddleware: Information: Request Host : localhost:44339 Request Path : /AddUser Query String : Request Body : {“id”: 9,“name”: “Ahmet”,“surname”: “Tekin”}WebApplication12.Middleware.ConventionBasedMiddleware: Information: Response Body : {“id”:9,”name”:”Ahmet”,”surname”:”Tekin”}

Evet beklediğimiz gibi yine aynı sonucu elde ettik.😊

Ek olarak bilgi vermem gerekirse IoC Contanier’ı içerisine kayıt edilecek değerler üç farklı davranışla kayıt edilebilmektedir. Bunları çok kısa şekilde aşağıda basit bir hiyerarşi üzerinden inceleyelim:

Yukarıdaki hiyerarşide UsersController, UserManager ve AddressManager’a bağımlıdır. Yani UsersController içinde UserManager ve AddressManager referansı bizim için gereklidir. Ve aynı şekilde AddressManager’da UserManager’a bağımlıdır. Bu gerekli referansları constructor aracılığı ile enjekte edildiğini varsayarak:

Scoped IoC:

Her yeni HTTP request için yeni bir nesne oluşturulur. Sadece bir HTTP request’in tüm kapsamı için aynı nesne sağlanır. Örneğin, yukarıda ki hiyerarşide UsersController’a bir istek gönderilirse DI UsersManager bağımlılığını iki kere çözümleyecektir. Fakat UserManager nesnesini sadece bir kere oluşturacak ve aynı nesneyi hem UserController ve hem de AddressManager için döndürecektir. Request aynı olduğu için aynı scoped olarak kabul edilecek ve dolayısıyla tek bir request için bir tane nesne örneği oluşturulacaktır.

Transient IoC:

Her request’in her talebine karşılık bir nesne üretilir. Yine, yukarıda ki hiyerarşide UsersController’a bir istek gönderilirse, UserManager nesnesi hem UserController hem de AddressManager için farklı nesneler olarak oluşturulacaktır.

Singleton IoC:

Her yeni HTTP isteğinde uygulama bazlı tekil nesne kullanılır ve IoC Contanier’dan yapılan tüm taleplerde aynı nesne döndürülür. Örneğin yukarıdaki hiyerarşide UserController’a yapılan tek bir istek neticesinde UserManger nesnesi hem UserController ve hem de AddressManager için aynı olarak döndürelecek. Ve ikinci istek de yine aynı nesne döndürülecektir.

Karşılaştırma

Umarım bu soru, yazıyı okuyan herkesin aklına takılmıştır. Çünkü bu kararı vermek tamamen middleware’in amacına yani oluşturduğunuz middleware ile ne yapmak istediğinize bağlıdır. Aralarında ki farklar şu şekilde özetlenebilir:

Inlıne Middlewares

Tekrar ve tekrar kullanmayı planlamadığımız, hızlı fakat yeniden kullanılabilirlik ve okunabilirlik açısından kötü olan middleware’lerdir.

Convention-Based Middleware

Convention-Based Middleware’de constructor’a enjekte ettiğimiz RequestDelegate parametresi Request Pipeline ilk oluşuturulduğunda IoC Container’a otomatik olarak kayıt edilir. Bu yüzden RequestDelegate referansı otomatik olarak tüm uygulama için singleton olarak oluşturulacaktır. Örneğin bunun sadece basit bir dezavantajından bahsedelim. ConventionBasedMiddleware yapısında RequestDelegate, singleton olarak oluşturulduğundan ConventionBasedMiddleware sınıfının constructor’ında scoped veya transient tanımlı başka bir bağımlılık enjekte edemeyiz. Örneğin:

Şimdi ConfigureService() metodunda IUserManager için scoped bir kayıt yapalım:

Uygulamayı başlattığımızda aşağıdaki temel hata ile karşılaşacağız:

System.InvalidOperationException: 'Cannot resolve scoped service 'WebApplication12.Manager.IUserManager' from root provider.'

Hatanın sebebi oldukça basit singleton bir sınıfa scoped bir bağımlılık ekleyemeyiz. Convention-Based Middleware yaklaşımı için bu bağımlılıkları sadece InvokeAsync metoduna enjekte edilebilir:

Eğer ek bağımlılıklarımızın da singleton olacağını garanti edebiliyorsak Convention-Based Middleware yaklaşımı kullanılabilir.

Özetlemek gerekirse:

  • Bağımlılığımız uygulama ömrü başına bir kez yani singleton olarak oluşturulur.
  • Scoped veya transiet bağımlılıklar yalnızca metod(InvokeAsync) yoluyla enjekte edilebilir.

Factory-Based Middleware

Eğer scoped veya transiet bağımlılıklarımız varsa, daha aşina olduğumuz constructor ınjection modeliyle uyumlu olduğundan kodlama basitliği açısından Factory-Based Middleware kullanılabilir. Örnek kullanım şekli aşağıda verilmiştir.

Evet… ASP.NET Core’da middleware yapılanması ile ilgili anlatacaklarım bu kadardı. Ve yeniden bir yazının daha sonuna gelmiş bulunuyorum. Eğer gözümden kaçan yazım hatası veya anlatım bozukluğu olduysa şimdiden kusuruma bakmayın. Yazım umarım faydalı olmuştur. Okuduğunuz için teşekkürler. Bir sonraki yazıda görüşmek üzere!

Yararlandığım Bazı Kaynaklar:

https://weblog.west-wind.com/posts/2019/Mar/16/ASPNET-Core-Hosting-on-IIS-with-ASPNET-Core-22

--

--