İçeriğe atla

ASP.NET Core 6/Endpoint'ler ve Routing Mekanizması

Vikikitap, özgür kütüphane

Bu bölümde geçen bölümde gördüğümüz middleware'lere benzer yapıda olan, ancak bazı konularda middleware'lerden ayrışan endpoint dediğimiz yapıları ve endpoint'lerin beraber çalıştığı ASP.NET Core tarafından sağlanan routing mekanizmasını göreceğiz.

Örnek projenin hazırlanması

[değiştir]

Bu bölümde kullanılacak örnek projenin ismi Routing. "ASP.NET Core Empty" şablonunu kullanarak projeyi oluşturun. Projeye ismi Population.cs olan ve içeriği aşağıdaki gibi olan dosyayı ekleyin:

namespace Routing
{
    public class Population
    {
        private RequestDelegate next;
        public Population() { }
        public Population(RequestDelegate nextDelegate)
        {
            next = nextDelegate;
        }
        public async Task Invoke(HttpContext context)
        {
            string[] parts = context.Request.Path.ToString().Split("/", StringSplitOptions.RemoveEmptyEntries);
            if (parts.Length == 2 && parts[0] == "population")
            {
                string city = parts[1];
                int? pop = null;
                switch (city.ToLower())
                {
                    case "london":
                        pop = 8_136_000;
                        break;
                    case "paris":
                        pop = 2_141_000;
                        break;
                    case "monaco":
                        pop = 39_000;
                        break;
                }
                if (pop.HasValue)
                {
                    await context.Response.WriteAsync($"City: {city}, Population: {pop}");
                    return;
                }
            }
            if (next != null)
            {
                await next(context);
            }
        }
    }
}

Bu middleare "population/<city>" şablonundaki path'lara karşılık vermektedir. Buradaki <city> değeri london, paris veya monaco olabilir. Eğer <city> bu üç şehirden birisi değilse veya path'ın ilk segmenti "population" değilse akış -varsa- sıradaki middleware'e yönlendirilir. Eğer path'ın ilk segmenti "population" ise ve ikinci segmenti biraz önce bahsettiğimiz üç şehirden biriyse bu şehirlerin nüfuslarını response'a yazar ve request pipeline'a kısa devre yaptırır. Middleware, gelen request'in kendisi için olup olmadığını anlamak için request path'ını "/" karakterine göre parçalara böler ve segmentleri teker teker inceler. Şimdi buna benzer şekilde projemize Capital.cs dosyasını ekleyelim. İçeriği şöyle olsun:

namespace Routing
{
    public class Capital
    {
        private RequestDelegate next;
        public Capital() { }
        public Capital(RequestDelegate nextDelegate)
        {
            next = nextDelegate;
        }
        public async Task Invoke(HttpContext context)
        {
            string[] parts = context.Request.Path.ToString().Split("/", StringSplitOptions.RemoveEmptyEntries);
            if (parts.Length == 2 && parts[0] == "capital")
            {
                string capital = null;
                string country = parts[1];
                switch (country.ToLower())
                {
                    case "uk":
                        capital = "London";
                        break;
                    case "france":
                        capital = "Paris";
                        break;
                    case "monaco":
                        context.Response.Redirect($"/population/{country}");
                        return;
                }
                if (capital != null)
                {
                    await context.Response.WriteAsync($"{capital} is the capital of {country}");
                    return;
                }
            }
            if (next != null)
            {
                await next(context);
            }
        }
    }
}

Bu middleware bir önceki middleware ile benzer mantıkta çalışır. Tek fark şehirlerin nüfusları yerine ülkelerin başkentini vermesidir. Eğer gelen talepteki ülke "monaco" ise başkentini yazmak yerine /population/monaco" path'ına tekrar talepte bulunulmasını sağlar. Şimdi bu iki middleware'i request pipeline'a ekleyelim (Program.cs dosyası):

using Routing;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseMiddleware<Population>();
app.UseMiddleware<Capital>();
app.Run(async (context) => {
    await context.Response.WriteAsync("Terminal Middleware Reached");
});
app.Run();

Programı bu haliyle çalıştırdığımız zaman beklediğimiz gibi çalıştığını görürüz. Örneğin "population/london" path'ına talepte bulunduğumuzda londra'nın nüfusunu ekrana yazdığını görürüz.

URL routing'i anlama

[değiştir]

Projemizde iki middleware var. Birisi "population/<city>" path'ına karşılık verirken, diğeri "capital/<country>" path'ına karşılık veriyor. İkisi de request'in kendisi için olup olmadığını anlamak için aynı işlemleri yapıyor, path string'ini parçalara ayırıp segmentleri inceliyor. Bu ideal durumdan uzaktır. Bunun için daha otomatik bir yola ihtiyacımız var. Ayrıca her middleware'in hangi path'a karşılık geldiği bariz değil. Her bir middleware'in hangi path'a karşılık geldiğini anlamak middleware'in içindeki kodları okumak zorundayız. Ayrıca ikinci middleware ilk middleware'i işaret eden URL'yi elle oluşturmuştur. Bu hataya açık bir durumdur. Çünkü ilk middleware ileride kaynak kodunu değiştirebilir, farklı bir URL şemasına geçebilir. Örneğin ileride ilk middleware "population/<city>" yerine "size/<city>" path'ına karşılık vermeye başlayabilir.

URL routing mekanizaması bütün bu sorunları çözmektedir. Sistem tarafından tanımlanan routing middleware'i devreye girmekte, gelen path'a göre akışı belirli bir endpoint'e yönlendirmektedir. Endpoint'ler bazı açılardan middleware'lere benzerler. Örneğin ikisi de HttpContext nesnesi üzerinde çalışır, request'i inceler ve response'a yazar. Ancak endpoint'lerde akışın yönlendirilebileceği sıradaki bir endpoint yoktur. Programda birden fazla endpoint olabilir ve her request'te request'in path'ına göre tek bir endpoint çalışır. Geçen derste request pipeline'ı görmüştük. Endpoint'leri de hesaba kattığımızda request pipeline şöyle gözükecektir:

Endpoint adı üstünde uç noktada bulunur. Esasında request'i inceleyerek response'a yazan asıl birim endpoint'tir. Middleware'ler çoğunlukla endpoint'e çeşitli konularda yardım ederler. Middleware'ler, response'a yazdıkları nadir zamanlarda da request pipeline'a kısa devre yaptırıp sıradaki middleware'lerin süreci devralmasını engellerler. Birden fazla middleware'ın ortaklaşa bir şekilde response'a yazması çok çok nadir bir durumdur.

Her bir path'a (veya path şablonuna) karşılık gelen endpoint'i belirtme Program.cs dosyasında yapılır. Program.cs dosyasında her bir path ve ona karşılık gelen endpoint ikilisine bir rota (route) denir. Routing middleware'inin Program.cs dosyasındaki rotalara bakarak gelen request'i belirli bir endpoint'e yönlendirmesine rotalama (routing) denir.

Routing middleware'inin kullanımı

[değiştir]

Routing middleware, request pipeline'a iki sistem middleware'i aracılığıyla eklenir. Bunlar UseRouting() ve UseEndpoints()'tir. UseRouting, rotalama yapacak middleware'i request pipeline'a ekler. UseEndpoints ise rotaları belirtmeye yarar. Şimdi Program.cs dosyasını şöyle değiştirelim:

using Routing;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseMiddleware<Population>();
app.UseMiddleware<Capital>();
app.UseRouting();
app.UseEndpoints(endpoints => {
    endpoints.MapGet("routing", async context => {
        await context.Response.WriteAsync("Request Was Routed");
    });
});
app.Run(async (context) => {
    await context.Response.WriteAsync("Terminal Middleware Reached");
});
app.Run();

IApplicationBuilder arayüzüne ait olan UseEndpoints() metodu parametre olarak IEndpointRouteBuilder alan ve geriye değer döndürmeyen bir fonksiyon alır. Fonksiyon parametresi (endpoints) kullanılarak rotalar oluşturulur. MapGet() metodu IEndpointRouteBuilder arayüzünde tanımlı bir eklenti metodudur ve belirli bir path'a gelen GET request'lerini lambda fonksiyonu şeklinde tanımlanmış endpoint'e yönlendirir. Aynı arayüzde tanımlı MapGet metodunun farklı versiyonları şunlardır:

MapGet(pattern, endpoint): Pattern'e gelen GET request'lerini belirtilen endpoint'e yönlendirir.
MapPost(pattern,endpoint): Pattern'e gelen POST request'lerini belirtilen endpoint'e yönlendirir.
MapPut(pattern,endpoint): Pattern'e gelen PUT request'lerini belirtilen endpoint'e yönlendirir.
MapDelete(pattern,endpoint): Pattern'e gelen DELETE request'lerini belirtilen endpoint'e yönlendirir.
MapMethods(pattern, methods, endpoint): Belirtilen metotlarla pattern'e gelen request'lerini belirtilen endpoint'e yönlendirir.
Map(pattern,endpoint): Pattern'e gelen tüm request'leri belirtilen endpoint'e yönlendirir.

Programı çalıştıdığımız zaman http://localhost:5000/routing adresine gelen talepler lambda ifadesi şeklinde belirtilen endpoint'e yönlendirilecek ve ekrana "Request was Routed" yazacaktır. Routing middleware'i bu şekilde rota belirtimi yapıldığı zaman request pipeline'a kısa devre yaptırır ve sıradaki middleware'ın çalışmasını engeller. Yukarıdaki örneğimizde ekrana "Terminal Middleware Reached" çıktısı eklenmeyecektir. Eğer rota eşleşmeseydi terminal middleware çalışacaktı. Şimdi Program.cs dosyasını şöyle değiştirelim:

using Routing;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
//app.UseMiddleware<Population>();
//app.UseMiddleware<Capital>();
app.UseRouting();
app.UseEndpoints(endpoints => {
    endpoints.MapGet("routing", async context => {
        await context.Response.WriteAsync("Request Was Routed");
    });
    endpoints.MapGet("capital/uk", new Capital().Invoke);
    endpoints.MapGet("population/paris", new Population().Invoke);
});
app.Run(async (context) => {
    await context.Response.WriteAsync("Terminal Middleware Reached");
});
app.Run(); ();

Gördüğünüz gibi middleware olarak kullanılan iki bileşen endpoint'e dönüştürülmüştür. Bir middleware sınıfının Invoke() metodu RequestDelegate temsilcisi tipinde olmalıdır, bu aynı zamanda MapGet() metodunda rota belirtirken kullanılan temsilcidir. Benzerliklerinden dolayı endpoint'ler ve middleware'ler birbirine çok kolayca dönüştürülebilirler.

Pipeline konfigürasyonunun basitleştirilmesi

[değiştir]

Daha eski ASP.NET Core sürümlerinde Program.cs dosyasında rotalama tam olarak böyle yapılıyordu. Ancak ASP.NET Core 6 sürümünden itibaren Microsoft otomatik olarak UseEouting ve UseEndpoints middleware'lerini otomatik olarak request pipeline'a eklemektedir. Şimdi Program.cs dosyamızı şöyle değiştirelim:

using Routing;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("routing", async context => {
    await context.Response.WriteAsync("Request Was Routed");
});
app.MapGet("capital/uk", new Capital().Invoke);
app.MapGet("population/paris", new Population().Invoke);
//app.Run(async (context) => {
// await context.Response.WriteAsync("Terminal Middleware Reached");
//});
app.Run();

Gördüğünüz gibi Program.cs dosyasında UseRouting() ve UseEndpoints() middleware çağrıları yok. Rotalar direkt IEndpointRouteBuilder arayüzünü uygulamış olan WebApplication nesnesi (app) üzerinden tanımlanıyor. Zaten de MapGet() ve MapPost() gibi rota oluşturmaya yarayan metotlar bu arayüzdeydi. Ayrıca bu şekilde rota tanımlaması yapıldığı zaman rota eşleşsin veya eşleşmesin akış sıradaki middleware'lere devredilir. Bu yüzden terminal middleware tanımı pasifize edilmiştir.

URL desenlerini anlama

[değiştir]

Şimdilik güzel gidiyoruz, ama bazı sorunlarımız var. Birincisi routing middleware endpoint'lerin desteklediği her URL için yönlendirme yapmıyor, sadece "capital/uk" ve "population/paris" path'ları için yönlendirme yapıyor. İkinci olarak routing middleware görevini yapmasına ve sadece URL'si uygun request'leri nedpoint'lere yönlendirmesine rağmen endpoint'lerin içinde yeniden URL işleme yapılıyor. Şimdilik elde ettiğimiz tek fayda endpoint'lerin kodunu incelemek zorunda kalmadan endpoint'lerin hangi path'a karşılık verdiğini görebilmek.

Şimdlik endpoint'ler request'in path'ındaki segmentleri incelemek zorundalar. Çünkü gelen request'in kendisine geldiğinden emin olsa bile path'daki ülke veya şehir bilgisini bir şekilde almak zorunda. Sorun aslında aşırı katı bir rotalama yapmamız. Sadece "capital/uk" ve "population/paris" path'ları için rotalama yapıyoruz. Segment sayısının daha az veya daha fazla olması veya birinci veya ikinci segmentlerin farklı olması routing middleware'in yönlendirme yapmamasına neden oluyor. İşte bu noktada segment değişkenleri devreye giriyor.

Segment değişkenleri

[değiştir]

Biraz önceki örneğimizdeki URl pattern'lerindeki literal segment'ler kullanmaktadır. Bunun anlamı sadece tam olarak o path'a gelen talepler için yönlendirme yapılmasıdır. Şimdi Program.cs dosyasını şöyle değiştirelim:

using Routing;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("{first}/{second}/{third}", async context => {
    await context.Response.WriteAsync("Request Was Routed\n");
    foreach (var kvp in context.Request.RouteValues)
    {
        await context.Response.WriteAsync($"{kvp.Key}: {kvp.Value}\n");
    }
});
app.MapGet("capital/uk", new Capital().Invoke);
app.MapGet("population/paris", new Population().Invoke);
app.Run(); ;

Gördüğünüz gibi segment değişkenleri rota tanımlaması yaparken { ve } karakterleri arasında belirtiliyor. Endpoint'in içinde ilgili segment değişkenine de context nesnesi üzerinden erişebiliyoruz. Bu örneğimizde segment'lerin içeriği ne olursa olsun path'ında üç tane segmenti olan her request ilgili endpoint'e yönlendirilmektedir. action, area, controller, handler ve page isimleri segment değişkenleri için kullanılamaz. HttpContext sınıfının Request özelliği ile erişilen HttpRequest sınfına ait RouteValue özelliği geriye RouteValuesDictionary tipinden nesne döndürür. RouteValuesDictionary tipinin önemli üyeleri şunlardır:

[key]: İndeksleyici vasıtasıyla herhangi bir segmentin değerine erişmeyi sağlar. (object)
Keys: Segment değişken isimlerini döndürür. (ICollection<string>)
Values:: Segment değerlerini döndürür. (ICollection<object>)
Count:: Segment sayısını döndürür. (int)
ContainsKey: URL'de belirrli bir isimli segmentin olup olmadığını döndürür. (bool)

RouteValuesDictionarı bir IEnumerable'dır ve üzerinde foreach döngüsü ile dönülebilir. Her bir dönüşte KeyValuePair<string,object> döndürür.

Middleware sınıflarını endpoint'e dönüştürme

[değiştir]

Artık routing middleware sayesinde yalnızca endpoint'lerin ilgilendiği talepleri endpoint'e yeteneğine sahibiz. Endpoint'e dönüştüreceğimiz middleware sınıfları artık gelen talebin kendisine geldiğinden emin ve segment değişkenlerini kullanabildiği direkt URL işlemesine gerek yok. Şimdi Capital.cs dosyamızı şöyle değiştirebiliriz:

namespace Routing
{
    public class Capital
    {
        public static async Task Endpoint(HttpContext context)
        {
            string capital = null;
            string country = context.Request.RouteValues["country"] as string;
            switch (country.ToLower())
            {
                case "uk":
                    capital = "London";
                    break;
                case "france":
                    capital = "Paris";
                    break;
                case "monaco":
                    context.Response.Redirect($"/population/{country}");
                    return;
            }
            if (capital != null)
            {
                await context.Response.WriteAsync($"{capital} is the capital of {country}");
            }
            else
            {
                context.Response.StatusCode = StatusCodes.Status404NotFound;
            }
        }
    }
}

Bu kod URL'nin path'ının ilk segmentinin "capital" olduğuna güvenerek "country" isimli ikinc, segmenti almaktadır. Diğer işlemler bildiğiniz gibidir. Artık bu bir endpoint olduğu için akışı bir sonraki middleware'e yönlendirecek kod bulunmamaktadır. Eğer gelen request'in country isimli segment değişkeni desteklenen üç ülke isminden biri değilse gerşye 404 Not Found hata kodu gönderilmektedir. Ayrıca fark ettiğiniz üzere yazdığınız bu endpoint sınıfı nesnesi oluşrurulması mantıklı bir sınıf değildir. Bütün iş statik Endpoint() metodu ile yapılmaktadır. Bu yaklaşım rota oluştururken daha sade ve şık bir görünüm sağlayacaktır. Şimdi benzer değişiklikleri Population.cs dosyasına yapalım:

namespace Routing
{
    public class Population
    {
        public static async Task Endpoint(HttpContext context)
        {
            string city = context.Request.RouteValues["city"] as string;
            int? pop = null;
            switch (city.ToLower())
            {
                case "london":
                    pop = 8_136_000;
                    break;
                case "paris":
                    pop = 2_141_000;
                    break;
                case "monaco":
                    pop = 39_000;
                    break;
            }
            if (pop.HasValue)
            {
                await context.Response.WriteAsync($"City: {city}, Population: {pop}");
            }
            else
            {
                context.Response.StatusCode = StatusCodes.Status404NotFound;
            }
        }
    }
}

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

using Routing;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("{first}/{second}/{third}", async context => {
    await context.Response.WriteAsync("Request Was Routed\n");
    foreach (var kvp in context.Request.RouteValues)
    {
        await context.Response.WriteAsync($"{kvp.Key}: {kvp.Value}\n");
    }
});
app.MapGet("capital/{country}", Capital.Endpoint);
app.MapGet("population/{city}", Population.Endpoint);
app.Run();

Şimdi bazı sorunlar çözülmüş gibi gözüküyor. Program.cs dosyasındaki rota tanımlamalarına bakarak hangi endpoint'in hangi rotaya karşılık geldiğini rahatlıkla görebiliyoruz, endpoint'lerde tekrar tekrar URL işleme yapılmıyor ve her bir endpoint desteklediği bütün URL'leri alabiliyor. Şimdilik tek bir sorun var. Capital endpoint'inden Population endpoint'ine elle yazılmış bir bağlantı var. Şimdi bu sorunu çözeceğiz.

Rotalardan URL oluşturma

[değiştir]

Elle URL oluşturma yerine rotalardan oluşturma daha sağlıklıdır. Çünkü endpoint'ler rotalama mekanizmasını değiştirebilir. Örneğin Population endpoint'i "population/<city>" path'ı yerine "size/<city>" path'ına karşılık vermeye başlayabilir. Bu tür değişikliklerle başa çıkabilmemiz lazım. Şimdi Program.cs dosyasını şöyle değiştirelim:

using Routing;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("{first}/{second}/{third}", async context => {
    await context.Response.WriteAsync("Request Was Routed\n");
    foreach (var kvp in context.Request.RouteValues)
    {
        await context.Response.WriteAsync($"{kvp.Key}: {kvp.Value}\n");
    }
});
app.MapGet("capital/{country}", Capital.Endpoint);
app.MapGet("population/{city}", Population.Endpoint).WithMetadata(new RouteNameMetadata("population"));
app.Run();

Burada Population endpoint'ine oluşturulan rotaya "population" ismini verdik. Şimdi Capital.cs dosyasını şöyle değiştirelim:

namespace Routing
{
    public class Capital
    {
        public static async Task Endpoint(HttpContext context)
        {
            string capital = null;
            string country = context.Request.RouteValues["country"] as string;
            switch (country.ToLower())
            {
                case "uk":
                    capital = "London";
                    break;
                case "france":
                    capital = "Paris";
                    break;
                case "monaco":
                    LinkGenerator generator = context.RequestServices.GetService<LinkGenerator>();
                    string url = generator.GetPathByRouteValues(context, "population", new { city = country });
                    if (url != null)
                    {
                        context.Response.Redirect(url);
                    }
                    return;
            }
            if (capital != null)
            {
                await context.Response.WriteAsync($"{capital} is the capital of {country}");
            }
            else {
                context.Response.StatusCode = StatusCodes.Status404NotFound;
            }
        }
    }
}

Bu kod biraz karışık gözükebilir. Bizim için önemli olan şu iki satırdır:

LinkGenerator generator = context.RequestServices.GetService<LinkGenerator>();
string url = generator.GetPathByRouteValues(context, "population", new { city = country });

Burada öncelikle link oluşturma servisine erişiyoruz. Sonra ASP.NET Core'a diyoruz ki: "'population' isimli rotayı bul, bu rotadaki 'city' parametresine belrttiğim değeri yaz, linki böyle oluştur. Program.cs dosyasındaki population rotasına bakarsak cidden city isminde bir parametre aldığını görürüz. Mevcut durumda oluşturulacak linkin path'ı "population/monaco" olacaktır. İleride rota tanımlaması "population/{city}" yerine "size/{city}" path'ına karşılık vermeye başlarsa üretilen link de ona göre değişecektir. Ancak rota ve parametre isimlerinin değişmemesi gerekir. Böylelikle son sorunu da çözmüş olduk.

Rota şablonlarını kullanma

[değiştir]

ASP.NET Core'daki routing mekanizmasının asıl gücü rota şablonlarına uygulanabilecek kısıt ve genişletmeler yardımıyla hangi URL path'larının hangi endpoint'lere karşılık geldiği bilgisini daha detaylı bir şekilde verebilmekten gelmektedir. Örneğin biraz önceki örneğimizde Population endpoint'i sadece üç şehir ismine karşılık verebilmesine rağmen iki segmenti olup ilk segmenti "population" olan bütün taleplere cevap vermektedir. İkinci segmentteki değişkenlerle kendisi uğraşmaktadır. Nüfusunu bildiği bir şehir gelmezse kendisi 404 kodunu döndürmektedir. Buna benzer birçok kriter olabilir. Şimdi bu kriterleri göreceğiz.

Tek bir segmentten birden fazla değer çıkarma

[değiştir]

URL path'ındaki / karakteri ile ayrılmış her bir birime segment denir. Şimdilik sadece segmentlere ayrı ayrı erişebiliyorduk. Şimdi bir segmenti de böleceğiz.

NOT: Bu bölümde Program.cs dosyasındaki tam kodu göstermek yerine pratiklik olması açısından Program.cs dosyasındaki sadece rota tanımlaması yapılan kodu göstereceğiz.

Örnek:

app.MapGet("files/{filename}.{ext}", async context => {
    await context.Response.WriteAsync("Request Was Routed\n");
    foreach (var kvp in context.Request.RouteValues)
    {
        await context.Response.WriteAsync($"{kvp.Key}: {kvp.Value}\n");
    }
});

Bu örneğimizde ikinci segment . karakteriyle iki kısma bölünmüş, ilk kısım filename isimli bir değişkene, ikinci kısım ext isimli bir değişkene atanmıştır. Burada yaptığımız iş aslında segmentleri ayıran stringi belirtmektir. Buradaki nokta karakteri yerine segment ayırıcı olarak herhangi bir stringi seçebiliriz. Bir pattern'de birden fazla farklı segment ayrıcı kullanabiliriz. Segment ayırıcılar tek karakter olmak zorunda değildir.

Bir segment string'i ile URL path'ı parçalara ayrıldığı zaman ASP.NET Core parçalamaya sondan başlar. Örneğin yukarıdaki örnekte dosya ismini "dosya.txt.dll" verirsek ASP.NET Core bunu "dosya.txt" ve "dll" olarak ayıracaktır. Bu bilgiyi bildiğimiz müddetçe bu davranış çoğunlukla ciddi bir soruna neden olmaz. Ancak bazen segment literali ile segment değişkenine girilen değerin aynı olması durumunda sorun oluşabilir. Örnek:

app.MapGet("example/red{color}", async context => //...

Burada renk kısmına "blue", "pink", vb. bir renk verirsek sorun oluşmaz. Ancak "example/redredgreen" path'ında sorun çıkar. Çünkü ASP.NET Core path'ı sondan işlemeye başlar, önce "green"i görür, sonra "green"den sonraki "red"i görür. Ancak bu "red"in literal olan "red" mi yoksa segment değişkeninin içinde olması gereken "red" mi olduğunu anlayamaz ve daha sonra göreceğimiz "Pattern Mismatch" hatasını döndürür. Çözüm ASP.NET Core'un kafasını karıştıracak karmaşık linkler üretmemek ve rotaları olabildiğince basit ve karışıklığa mahal vermeyecek şekilde tutmaktır. Ayrıca ileride Pattern Mismatch hatalarından nasıl sakınabileceğimizi de göreceğiz.

Segment değişkenleri için varsayılan değerler kullanma

[değiştir]

Şimdi istenen segment sayısından daha az bir segment verilse de eşleme yapılabilmesini sağlayan bir yöntem göreceğiz. Örnek:

app.MapGet("capital/{country=France}", Capital.Endpoint);

Burada country parametresinin varsayılanını France yaptık. Bu rota ile ilk segment'i "capital" olması şartıyla tek segmenti olan URL'ler de ilgili endpoint'e eşlenecektir, bu durumda ikinci segmentin değeri France olacaktır. Segment sayısı ikiden fazlaysa eşleme yapılmaz.

Opsiyonel segmentler

[değiştir]

Opsiyonel segmentler varsayılan segment değeri kullanmaya benzer sonuç verir. Ancak varsayılan kullanımında endpoint'in ilgili segmentin URL'de olup olmadığından haberi olmaz. Eğer endpoint'in bir segmentin olup olmadığından haberi olmasını istiyorsak opsiyonel segmentler kullanırız. Örnek:

app.MapGet("size/{city?}", Population.Endpoint);

Opsiyonel segment kullanımında segment kullanılmaması durumunda segment değişkeninin değeri endpoint'in içinde null olur. Endpoint belirtilen durumla kendisi uğraşması gerekir. Bu örneğimizde yine ilk segmenti "size" olan tek ve iki segmenti olan URL'ler eşlenir.

catchall

[değiştir]

catchall segment değişkeni bir veya birden fazla segmenti yakalamaya yarar. Örnek:

app.MapGet("{first}/{second}/{*catchall}", async context => {
    await context.Response.WriteAsync("Request Was Routed\n");
    foreach (var kvp in context.Request.RouteValues)
    {
        await context.Response.WriteAsync($"{kvp.Key}: {kvp.Value}\n");
    }
});

Burada ilk iki segment first ve second değişkenlerine atanır. Üçüncü ve daha sonrası olası tüm segmenttler catchall isimli bir segment değişkenine atanır. Bu segment değişkeni geri kalan tüm segmentleri "segment1/segment2/segment3/..." şeklinde tek bir string'de tutar. Bu örneğimizde iki veya daha fazla segment değişkeni olan tüm URL'ler eşlenir.

Segment eşlemeyi kısıtlama

[değiştir]

Şimdiye kadar hep path değerlerini daha geniş bir kümeye genişletmek için rota şablonlarını kullandık. Şimdi tam tersini yapacağız. Rotanın belirtiğinden daha küçük bir kümeyi alacağız, geri kalanlar eşlenmeyecek. Örnek:

app.MapGet("{first:int}/{second:bool}", async context => {
    await context.Response.WriteAsync("Request Was Routed\n");
    foreach (var kvp in context.Request.RouteValues)
    {
        await context.Response.WriteAsync($"{kvp.Key}: {kvp.Value}\n");
    }
});

Bu örneğimizde ilk segment değişkenimiz tam sayı, ikinci segment değişkenimiz true veya false olmalı. Aksi durumda eşleme yapılmaz. Bunlara benzer şekilde başka kısıtlar da vardır. Bunları tek bir tabloda vermek doğru olacaktır.

Kısıt Tanım
alpha İngiliz alfabesinin a-z aralığındaki değerler (küçük-büyük harf duyarlılığı yok)
bool true veya false (küçük-büyük harf duyarlılığı yok)
datetime tarih-zaman (lokalize edilmemiş invariant kültürde, birazdan açıklamasını yapacağız)
decimal decimal tipine dönüştürülebiliyorsa (lokalize edilmemiş invariant kültürde)
double double tipine dönüştürülebiliyorsa (lokalize edilmemiş invariant kültürde)
file noktayla ayrılmış dosya ismi ve uzantısı şeklindeyse (örneğin dosya.txt)
float float tipine dönüştürülebiliyorsa (lokalize edilmemiş, invariant kültürde)
guid bir guid değeriyse
int tam sayı
length(karakter) ilgili segment belirtilen sayıda karakterden oluşuyorsa
length(min,max) ilgili segment belirtilen aralıkta karekter sayısına sahipse (sınırlar dahil)
long long tipine dönüştürülebiliyorsa
max(deger) int tipine dönüştürülebiliyorsa ve maksimum değeri belirtilen değer ise
maxlength(karakter) maksimum belirtilen sayıda karaktere sahipse
min(deger) int tipine dönüştürülebiliyorsa ve minimum değeri belirtilen değer ise
minlength(karakter) minimum belirtilen sayıda karaktere sahipse
nonfile dosya değilse (file kısıtını sağlamıyorsa)
range(min,max) int tipine dönüştürülebiliyorsa ve belirtilen aralıktaysa (sınırlar dahil)
regex(ifade) belirtilen düzenli ifadeye uyuyorsa

Kuşkusuz kültürler farklıdır. Farklı kültürlerde tarih ve zaman farklı şekillerde gösterilir. Lokalize edilmemiş invariant kültürden kastım Amerikan kültürüdür. ASP.NET Core'un 9 Kasım 2024 saat 23:15'i bir tarih-zaman olarak algılayabilmesi için 11.9.2024-11:15pm olarak verilmesi gerekir. Benzer şekilde 9 Kasım 2024'ün geçerli bir tarih olarak algılanabilmesi için 11.9.2024 şeklinde verilmesi gerekir. Türk kültürüne göre verilen tarih-zamanlar ASP.NET Core'un datetime kısıtına uymayabilir. Benzer şekilde double gibi ondalıklı tiplerde ondalık kısmı ayırmak için nokta, üçlü grupları ayırmak için virgül kullanılmalıdır.

Kısıtlar birleştirilebilir. Örnek:

app.MapGet("{first:alpha:length(3)}/{second:bool}", async context => {
    await context.Response.WriteAsync("Request Was Routed\n");
    foreach (var kvp in context.Request.RouteValues)
    {
        await context.Response.WriteAsync($"{kvp.Key}: {kvp.Value}\n");
    }
});

Bu örneğimizde ilk parametre İngiliz alfabesinin a-z aralığındaki karakterlerini içerebilir ve yalnızca 3 karakterden oluşabilir.

Kuşkusuz yukarıda saydığımız kısıtların en güçlüsü düzenli ifade kısıtıdır. Düzenli ifade kısıtı ile diğer bütün kısıtların sağladığı kısıtlar sağlanabilir. Ancak bu kitap düzenli ifadeler üzerine yazılmış bir kitap değildir. O yüzden düzenli ifadeleri öğrenmek istiyorsanız İnternette düzenli ifadeler üzerine yazılmış kaynakları araştırabilirsiniz. Burada düzenli ifadelerin çok küçük bir kısmı kullanılacaktır. Örnek:

app.MapGet("capital/{country:regex(^uk|france|monaco$)}", Capital.Endpoint);

Bu örneğimizde "capital/uk", "capital/france" ve "capital/monaco" path'ları için eşleme yapılmaktadır. Diğer path'lar için eşleme yapılmamaktadır. Düzenli ifadelerde büyük-küçük harf duyarlılığı yoktur.

Fallback rotaları oluşturma

[değiştir]

Diğer rotaların karşılamadığı bir talebi karşılamak için fallback rotalar kullanılır. Örnek:

app.MapFallback(async context => {
    await context.Response.WriteAsync("Routed to fallback endpoint");
});

Fallback rotanın işlevini düzgün bir şekilde yerine getirebilmesi için Program.cs dosyasındaki en son yapılan rota tanımı olması gerekmektedir. İki farklı metot fallback rota tanımlaması yapmak için kullanılabilir.

MapFallback(endpoint): Bir endpoint'e rotalama yapar. (yukarıda kullandığımız).
MapFallbackToFile(path): Belirli bir path'a rotalama yapar.

Kendi kısıtlarımızı oluşturma

[değiştir]

ASP.NET Core'un sağladığı kısıtlar işimizi görmezse kendi kısıtımızı da oluşturabiliriz. Kendi kısıtımızı oluşturmak için Microsoft.AspNetCore.Routing isim alanındaki IRouteConstarint arayüzünü uygulayan bir sınıf oluşturmalıyız. Örnek:

namespace Routing
{
    public class CountryRouteConstraint : IRouteConstraint
    {
        private static string[] countries = { "uk", "france", "monaco" };
        public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
        {
            string segmentValue = values[routeKey] as string;
            return Array.IndexOf(countries, segmentValue.ToLower()) > -1;
        }
    }
}

Şimdi bu kısıtı bir segment değişkenine uygulayalım (Program.cs dosyası):

using Routing;
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<RouteOptions>(opts => {
    opts.ConstraintMap.Add("countryName", typeof(CountryRouteConstraint));
});
var app = builder.Build();
app.MapGet("capital/{country:countryName}", Capital.Endpoint);
app.Run();

Burada öncelikle oluşturduğumuz kısıt sınıfını kaydetmekteyiz. Kısıta "countryName" ismini vermekteyiz. Bu ismi kısıtı kullanırken kullanıyoruz. Kısıt basitçe eğer segment değeri "uk", "france" veya "monaco" ise olumlu cevap vermekte, aksi halde olumsuz cevap vermektedir. Gördüğünüz gibi kısıt sınıfına test edilen segment değeri yanında geçerli bağlamın HttpContext nesnesi, bütün segment anahtarları ve değerleri, vs. de gitmektedir. Bütün bunları kullanarak çok karmaşık kısıtlar oluşturabiliriz.

Belirsiz rota oluşturmaktan kaçınma

[değiştir]

ASP.NET Core belirli bir pattern'e karşılık gelebilecek birden fazla rota bulabilir. Bu durumda daha spesifik olan daha öncelikli hale gelir. Örneğin "{segment1:int}/segment2" pattern'i ve {segment1}/segment2 pattern'i "15/pattern2" path'ını karşılamaktadır. Ancak "{segment1:int}/segment2" pattern'i daha spesifik olduğu için bu pattern'e karşılık gelen rota seçilir.

Ancak bazen ASP.NET Core iki pattern'e eşit öncelik verir ve seçilecek rotayı belirleyemez. Bu tür bir durumda çalışma zamanı hatası oluşur. Böyle durumda yapılması gereken öncelikli şey seçilmesi istenen rotayı spesifikleştirmektir. Ancak bazen bu çözüm olmayabilir. Yapılacak spesifikleştirme başka şeyleri bozacak olabilir. Böyle bir durumda yapılacak işlem ASP.NET Core'un rota seçim mekanizmasına müdahale etmektir. Şimdi Program.cs dosyasına aşağıdaki iki rotayı ekleyelim:

app.Map("{number:int}", async context => {
    await context.Response.WriteAsync("Routed to the int endpoint");
});
app.Map("{number:double}", async context => {
    await context.Response.WriteAsync("Routed to the double endpoint");
});

Örneğin 23.5 değeri için ikinci rota seçilir. Ancak 23 değeri için, 23 hem int'e hem double'a dönüştürülebildiği için, rotalama yapılamaz ve çalışma zamanı hatası oluşur. Bu sorunu çözmek için öncelikleri elle belirtebiliriz:

app.Map("{number:int}", async context => {
    await context.Response.WriteAsync("Routed to the int endpoint");
}).Add(b => ((RouteEndpointBuilder)b).Order = 1);
app.Map("{number:double}", async context => {
    await context.Response.WriteAsync("Routed to the double endpoint");
}).Add(b => ((RouteEndpointBuilder)b).Order = 2);

Daha düşük sayı verilen rota daha önceliklidir. Bu örneğimizde bir sayı hem int'e hem de double'a dönüştürülebiliyorsa int rotasına öncelik verilecektir.

Endpoint çalışmadan önce hangi endpoint'in seçildiğini görme

[değiştir]

Rotalama mekanizmasının yaptığı gelen request'e göre bir endpoint seçmektir. İstersek araya girerek akış endpoint'e yönlendirilmeden önce hangi endpoint'in seçildiğini görebiliriz. Örnek (Program.cs dosyası):

using Routing;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Use(async (context, next) => {
    Endpoint end = context.GetEndpoint();
    if (end != null)
    {
        await context.Response.WriteAsync($"{end.DisplayName} Selected \n");
    }
    else
    {
        await context.Response.WriteAsync("No Endpoint Selected \n");
    }
    await next();
});
app.Map("{number:int}", async context => {
    await context.Response.WriteAsync("Routed to the int endpoint");
}).WithDisplayName("Int Endpoint").Add(b => ((RouteEndpointBuilder)b).Order = 1);
app.Map("{number:double}", async context => {
    await context.Response.WriteAsync("Routed to the double endpoint");
}).WithDisplayName("Double Endpoint").Add(b => ((RouteEndpointBuilder)b).Order = 2);
app.MapFallback(async context => {
    await context.Response.WriteAsync("Routed to fallback endpoint");
});
app.Run();

Bu kodda öncelikle endpoint'lere isim verilmektedir. Daha sonra request pipeline'a eklediğimiz bir middleware'le hangi endpoint'in seçildiği response'a yazılmaktadır. HttpContext sınıfına eklenen GetEndpoint() metodu Endpoint sınıfı türünden nesne döndürür. Bu sınıfın önemli üyeleri şunlardır:

DisplayName: Endpoint'in ismi
Metadata: Endpoint'le ilgili metadatayı döndürür.
RequestDelegate: Her bir endpoint'in RequestDelegate temsilci tipine uyan bir metot olduğunu söylemiştik. İşte bu metodu RequestDelegate tipine döndürüp tutar.

HttpContext sınıfına ait başka bir eklenti metodu SetEndpoint()'tir ve request'e karşılık seçilen endpoint'ten başka bir endpoint'in seçilmesini sağlar. Bu yolla ASP.NET Core'un sunduğu routing mekanizmasına müdahale etmiş oluruz.