ASP.NET Core 6/Servisler ve Dependency Injection

Web uygulamamızın birçok yerinde sık kullanılan birtakım işlevlere ve verilere ihtiyaç duyabiliriz. Bunlar veritabanına erişme ve veri yazma, loglama yapma, konfigürasyon verilerine erişme gibi işleri içerir. Bunlar ASP.NET Core tarafından sağlanan hazır servislerle sağlanır. Ancak istersek kendi servislerimizi yazıp daha sonra uygulamamızın istediğimiz yerinde kullanabiliriz. İstediğimiz her sınıfı servis yapabiliriz. Kendi yazdığımız servislerin sağlayacağı işlevsellik konusunda herhangi bir kısıtlama yoktur.

Servisler dependency injection dediğimiz bir teknikle yaratılır ve tüketilir. Dependency injection sayesinde servisi kullanan sınıf (veya metot) servise bir arayüz üzerinden erişir, gerçek implementasyon sınıfını bilmez. Bunun avantajı programımız içindeki tek bir satırı değiştirerek program içindeki tüm servis implementasyonlarını da değiştirebilmemizdir. Örneğin çoğunlukla geliştiriciler geliştirme aşamasında gerçek veritabanına okuyup yazmazlar, bunun yerine dahili .NET nesneleriyle çalışırlar. Programın kend içinde tam bir tutarlılıkla çalıştığından emin olduktan sonra, deploy sürecine yakın bir aşamada veritabanı bağlantısı yapılır. Veya yine oturum da veritabanı gibi fiziksel bir kaynaktır. Geliştirme ve test sürecinde direkt oturuma okuyup yazmamamız gerekir. Bunun yerine oturum değişkenlerini taklit edecek dahili .NET nesnelerini kullanmamız gerekir. Eğer dependency injection ve servis kavramı olmasaydı programımızın veri kaynağını dahili .NET nesnelerinden gerçek veritabanına çevirmek için programımız içindeki dahili veri kaynağına olan erişimleri tek tek bulup bunları veritabanına işaret edecek şekilde değiştirmemiz gerekecekti.

Yukarıda verdiğimiz örnekte programımız veritabanı (veya oturum) bileşenine gevşek bağlıdır denir. Dependency injection gevşek bağlı bileşenler oluşturmaya yarar. Bu sadece veritabanı ve oturum gibi fiziksel kaynaklarla kısıtlı değildir. Kendi programımızı da bileşenlere bölebiliriz. Bu sayede bir bileşenin çok fazla değişiklik yüzünden bakımının zorlaştığı durumlarda o bileşeni tamamen çöoe atıp aynı işlevselliği çok daha bakımı kolay, temiz ve sade kodlarla sağlamaya çalışabiliriz. Eğer programımız tam bir bütün olsaydı benzer durumda bütün programı çöpe etmamız gerekecekti.

Servisleri sınıf kütüphaneleri gibi düşünebiliriz. Ancak servisler sınıf kütüphanelerinin aksine canlı .NET nesneleridir. Bir servise ihtiyaç duyduğumuz yerde ASP.NET Core ilgili sınıf nesnesini oluşturup bir arayüz üzerinden bize verir. Biz nesne oluşturma süreciyle ilgilenmeyiz ve oluşturulan nesnenin hangi implementasyon kullanılarak oluşturulduğunu da bilmeyiz.

Bu bölümde neden servislere ve dependency injection'a ihtiyaç duyduğumuz, servisler ve dependency injection olmadan önce benzer işlevselliği hangi yapılarla sağlamaya çalıştığımız anlatılacaktır. Daha sonra ASP.NET Core'da servis tanımlamarının ve tüketiminin nasıl yapılacağı gösterilecektir. Ancak öncelikle üzerinde çalışacağımız örnek projenin oluşturulması gerekli.

Örnek projenin oluşturulması

değiştir

ASP.NET Core Empty şablonunu kullanarak ismi "Servisler" olan yeni bir ASP.NET Core projesi oluşturun. Bu projeye IResponseFormatter.cs isminde ve içeriği aşağıdaki gibi olan yeni bir arayüz ekleyin:

namespace Servisler
{
    public interface IResponseFormatter
    {
        Task Format(HttpContext context, string content);
    }
}

Bu arayüze bir implementasyon oluşturmak için projeye TextResponseFormatter.cs isminde ve içeriği aşağıdaki gibi olan yeni bir sınıf ekleyin:

namespace Servisler
{
    public class TextResponseFormatter : IResponseFormatter
    {
        private int responseCounter = 0;
        public async Task Format(HttpContext context, string content)
        {
            await context.Response.WriteAsync($"Response {++responseCounter}:\n{content}");
        }
    }
}

Bu sınıfın Format() metodu HttpContext ve string türünde iki parametre almaktadır. Yaptığı iş basitçe kendisine verilen stringi kendisine verilen HttpContext nesnesi üzerinde response'a yazmaktır. Ayrıca response'a yazmak için bir önceki TextResponseFormatter nesnesiyle aynı nesnenin kullanılıp kullanılmadığını belirlemek için bir responseCounter değişkeni tutmaktadır.

Şimdi projeye WeatherMiddleware.cs isminde ve içeriği aşağıdaki gibi olan yeni bir sınıf ekleyin:

namespace Servisler
{
    public class WeatherMiddleware
    {
        private RequestDelegate next;
        public WeatherMiddleware(RequestDelegate nextDelegate)
        {
            next = nextDelegate;
        }
        public async Task Invoke(HttpContext context)
        {
            if (context.Request.Path == "/middleware/class")
            {
                await context.Response.WriteAsync("Middleware Class: It is raining in London");
            }
            else 
            {
                await next(context);
            }
        }
    }
}

Bu middleware basitçe eğer gelen request'in path'ı "/middleware/class" ise response'a yazma yapmakta, değilse hiçbir şey yapmadan akışı sıradaki middleware'e yönlendirmektedir. Şimdi projeye ismi WeatherEndpoint.cs olan ve içeriği aşağıdaki gibi olan bir sınıf ekleyelim:

namespace Servisler
{
    public class WeatherEndpoint
    {
        public static async Task Endpoint(HttpContext context)
        {
            await context.Response.WriteAsync("Endpoint Class: It is cloudy in Milan");
        }
    }
}

Şimdi Program.cs dosyasının içeriğini aşağıdaki kodlarla değiştirelim:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseMiddleware<WeatherMiddleware>();
IResponseFormatter formatter = new TextResponseFormatter();
app.MapGet("endpoint/1", async context => {
    await formatter.Format(context, "Endpoint-1: It is snowing in Chicago");
});
app.MapGet("endpoint/class", WeatherEndpoint.Endpoint);
app.MapGet("endpoint/2", async context => {
    await context.Response.WriteAsync("Endpoint-2: It is sunny in LA");
});
app.Run();

Bu Program.cs dosyasında biraz önce tanımladığımız WeatherMiddleware middleware'i request pipeline'a eklenmektedir. Ayrıca çeşitli path'lara karşılık üç tane endpoint eşlemesi yapılmaktadır. Bunlardan ikisi lambda ifadesi şeklinde, biri sınıf şeklindedir. Burada dikkat etmemiz gereken nokta IResponseFormatter formatter = new TextResponseFormatter(); şeklindeki değişken tanımlamasının en üst blokta yapılmış olmasıdır. Program.cs dosyasındaki kodlar sunucu ayağa kalkarken yalnızca bir kez çalışır. ASP.NET Core, Program.cs dosyasındaki kodlara bakarak bir request pipeline ve endpoint eşleme tablosu inşa eder. En üst blokta tanımlanan değişkenler middleware ve endpoint blokları tarafından kullanılabilir. Daha da önemlisi bu değişkene değişiklik yapıldığı zaman sunucunun hafızesında tutulan aynı değişkene değişiklik yapılmış olur. Bu da değişkenlerin birden fazla request-response zincirinin ardından da canlı kalacağı anlamına gelir.

Bu şekilde projeyi çalıştırdığınızda ve "endpoint/1" path'ına talepte bulunduğunuzda şöyle bir çıktı alırsınız:

Response 1:
Endpoint-1: It is snowing in Chicago

Eğer tarayıcıyı yenilerseniz Response-2, Response-3,... şeklinde response sayacının arttığını görürsünüz. Çünkü aynı TextResponseFormatter nesnesi üzerinden Format() metodu çağrılmakta ve Format() metodu da bağlı olduğu nesnein sayacını bir artırmakta ve ekrana basmaktadır. Aynı TextResponseFormatter nesnesi sunucunun belleğinde tutulmaktadır.

Servisler ve Dependency Injection Yaklaşımının Çözdüğü Sorunlar

değiştir

Servisler ve dependency injection yaklaşımı iki temel sorunu çözmektedir. Bu sorunlar servislere ulaşım için standart bir yol oluşturulması ve zayıf bağlılıktır.

Servislere Ulaşım için Standart Bir Yol Oluşturulması

değiştir

Servis benzeri yapılara dependency injection kavramı hayatımıza girmeden önce de aşinaydık. Ancak işlemler daha farklı şekilde yürütülüyordu. Burada projemizdeki TextResponseFormatter sınıfını bir servis olarak ele alalım. Programımızın birçok yerinde bu sınıfın nesnesine ihtiyaç duyduğumuzu ve bu nesneye kolayca erişmek istediğimizi düşünelim. Bu servis nesnesine programımızın istediğimiz bir yerinden erişmek için nesnenin kaynağı olan noktadan kullanmak istediğimiz noktaya kadar metotlar ve yapıcı metotlar vasıtasıyla elden ele metot ve yapıcı metot parametreleri aracılığıyla taşımaktır. Ancak bu yaklaşım çok kötü bir yaklaşımdır, programımızın karmaşıklığının gereksiz yere artırır. Başka bir yaklaşım (ve burada örneğini vereceğimiz yaklaşım) singleton pattern'idir. Singleton pattern'inin TextResponseFormatter sınıfı için kullanımını görmek için TextResponseFormatter.cs dosyasını şöyle değiştirin:

namespace Servisler
{
    public class TextResponseFormatter : IResponseFormatter
    {
        private int responseCounter = 0;
        private static TextResponseFormatter shared;
        public async Task Format(HttpContext context, string content)
        {
            await context.Response.WriteAsync($"Response {++responseCounter}:\n{content}");
        }
        public static TextResponseFormatter Singleton
        {
            get
            {
                if (shared == null) shared = new TextResponseFormatter();
                return shared;
            }
        }
    }
}

Singleton pattern'i basitçe eğer nesne oluşturulmuşsa o nesneyi verir, eğer nesne oluşturulmamışsa oluşturup verir. Her halükarda programın her yerinde tek bir nesne dolaşır. Singleton pattern'iyle oluşturulan bu servise nasıl erişildiğini görmek için Program.cs dosyasını şöyle değiştirin:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseMiddleware<WeatherMiddleware>();
//IResponseFormatter formatter = new TextResponseFormatter();
app.MapGet("endpoint/1", async context => {
    await TextResponseFormatter.Singleton.Format(context, "Endpoint-1: It is snowing in Chicago");
});
app.MapGet("endpoint/class", WeatherEndpoint.Endpoint);
app.MapGet("endpoint/2", async context => {
    await TextResponseFormatter.Singleton.Format(context, "Endpoint-2: It is sunny in LA");
});
app.Run();

Bu örnekte eğer ilk önce tarayıcıya "endpoint/1" path'ına daha sonra "endpoint/2" path'ına talepte bulunursa sayacın arttığını görürüz.

Singleton patter'i basit ve kullanışlıdır. Zaten bu yüzden dependency injection kavramı hayatımıza girmeden önce yaygın şekilde kullanılmaktaydı. Ancak singleton pattern'i servislere standart bir yolla erişimi sağlamaz. Her sınıf singleton pattern'ini farklı bir yolla uygulayabilir. Bu örneğimizde nesneye ulaşmayı sağlayan metodun ismi Singleton'dır. Farklı servisler bu metoda farklı bir isim koyabilir. Servisi kullanacak bütün sınıflar bu sınıfta Singleton isminde bir metot olduğunu ve bu metodun daima tek olacak olan TextResponseFormatter nesnesini döndürdüğünü bilmek zorundadır. En önemlisi TextResponseFormatter sınıfı hem servisin kendisinden hem de servisin dağıtımından sorumludur. Eğer dağıtım sorumluluğunu servis sınıfına yüklersek her servis dağıtımı farklı şekilde yapabilir.

Zayıf Bağlılık

değiştir

Biraz önce TextResponseFormatter servisine ulaşmak için TextResponseFormatter sınıfının içinde tanımlanan Singleton() metoduna ulaştık. Bu durumda IResponseFormatter arayüzünü hiç kullanmadık. Ama biz istiyoruz ki servise bir arayüz üzerinden ulaşalım. Bu sayede tüketici kod servise zayıf bağlı olsun, asıl işi yapacak implementasyon kodu kolayca değişebilsin. Bu sorunu çözmek için de çeşitli pattern'ler vardır. Bu pattern'lerden biri de "Type Broker"dır. Şimdi projemize ismi TypeBroker.cs olan ve içeriği aşağıdaki gibi olan yeni bir sınıf ekleyelim:

namespace Servisler
{
    public static class TypeBroker
    {
        private static IResponseFormatter formatter = new TextResponseFormatter();
        public static IResponseFormatter Formatter => formatter;
    }
}

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

using Servisler;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseMiddleware<WeatherMiddleware>();
app.MapGet("endpoint/1", async context => {
    await TypeBroker.Formatter.Format(context, "Endpoint-1: It is snowing in Chicago");
});
app.MapGet("endpoint/class", WeatherEndpoint.Endpoint);
app.MapGet("endpoint/2", async context => {
    await TypeBroker.Formatter.Format(context, "Endpoint Function: It is sunny in LA");
});
app.Run();

Bu örneğimizde TextResponseFormatter servisine TypeBroker sınıfının statik Formatter özelliği ile arayüz üzerinden erişmekteyiz. Bu sayede implementasyonu TypeBroker sınıfı üzerinden yapacağımız tek satırlık bir kod değişikliği ile değiştirebiliriz. Bu durum servise zayıf bağlılık sağlar. TypeBroker pattern'i servislere standart bir yolla erişme sorunu çözmeden zayıf bağlılık sorununu çözmektedir. Çünkü servis kullanıcıları servisi kullanabilmek için TypeBroker isimli bir ayrı bir sınıf olduğunu ve bu sınıfın içindeki Formatter özelliği ile servise erişildiğini bilmek zorundadır. Her servis bu pattern'i uygulamayabilir.

Dependency Injection'ın Kullanımı

değiştir

Dependency injection'a neden ihtiyacımız olduğunu gördükten sonra şimdi dependency injection'ın ASP.NET Core'da nasıl kullanıldığını görelim. Şimdi IResponseFormatter arayüzü için yeni bir implementasyon yazalım. İmplementasyonun ismi HtmlResponseFormatter.cs ve içeriği şöyle:

namespace Servisler
{
    public class HtmlResponseFormatter : IResponseFormatter
    {
        public async Task Format(HttpContext context, string content)
        {
            context.Response.ContentType = "text/html";
            await context.Response.WriteAsync($@"
                <!DOCTYPE html>
                <html lang=""en"">
                <head><title>Response</title></head>
                <body>
                <h2>Formatted Response</h2>
                <span>{content}</span>
                </body>
                </html>");
        }
    }
}

Bir önceki implementasyon içeriği response'a text olarak yazmaktaydı. Bu implementasyon HTML olarak yazmaktadır. Bunun için ContentType'ı "text/html" olarak değiştirmekte ve kendisine gelen string'i bir HTML şablonu içine yerleştirmektedir. IResponseFormatter servisi için programımız içinde her yerde HtmlResponseFormatter implementasyonunu kullanmak için Program.cs dosyasını şöyle değiştirmeliyiz:

using Servisler;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IResponseFormatter, HtmlResponseFormatter>();
var app = builder.Build();
app.UseMiddleware<WeatherMiddleware>();
app.MapGet("endpoint/1", async (HttpContext context, IResponseFormatter formatter) => {
    await formatter.Format(context,"Endpoint-1: It is snowing in Chicago");
});
app.MapGet("endpoint/class", WeatherEndpoint.Endpoint);
app.MapGet("endpoint/2", async (HttpContext context, IResponseFormatter formatter) => {
    await formatter.Format(context, "Endpoint-2: It is sunny in LA");
});
app.Run();

Burada servis eklemesi builder.Services.AddSingleton<IResponseFormatter, HtmlResponseFormatter>(); satırıyla yapılır. Eklenen bu servis ise endpoint fonksiyonlarına eklenen parametre ile kullanılır. ASP.NET Core endpoint eşlemelerini oluştururkrn endpoint fonksiyonlarındaki IResponseFormatter türünden olan formatter parametrelerini görünce IResponseFormatter türünü nasıl çözümleyeceğini görmek için servis yapılandırmasına bakar ve az önce yazdığımız builder.Services.AddSingleton<IResponseFormatter, HtmlResponseFormatter>(); kodunu görür. Burada "IResponseFormatter gördüğün yere TextResponseFormatter yaz" demektedir. TextResponseFormatter türünden nesne oluşturur ve endpoint eşlemesini böyle oluşturur, daha sonraki her IResponseFormatter talebinde aynı nesneyi kullanır. Örneğin ikinci endpoint de endpoint eşlemesinin oluşturulması için IResponseFormatter nesnesi istemektedir. Bu endpoint eşlemesi için de aynı nesne kullanılır.

NOT: "endpoint/1" ve "endpoint/2" route yapılandırmalarında lambda fonksiyonunu belirtirken parametre tiplerini belirtmek zorundayız. Çünkü bu durumda MapGet() metodunun ikinci parametresi Delegate tipindedir. Bu sayede ilgili yere her tipte fonksiyon yazabiliriz, fonksiyonun parametreleri istediğimiz sayıda ve tipte olabilir, bu sayede istediğimiz sayıda ve tipte servis ekleyebiliriz. Bu yüzden parametre tiplerini belirtmek zorundayız.

Middleware sınıfından servise erişmek

değiştir

Projemizdeki WeatherMiddleare.cs sınıfının içeriğini şöyle değiştirelim:

namespace Servisler
{
    public class WeatherMiddleware
    {
        private RequestDelegate next;
        private IResponseFormatter formatter;
        public WeatherMiddleware(RequestDelegate nextDelegate, IResponseFormatter respFormatter)
        {
            next = nextDelegate;
            formatter = respFormatter;
        }
        public async Task Invoke(HttpContext context)
        {
            if (context.Request.Path == "/middleware/class")
            {
                await formatter.Format(context, "Middleware Class: It is raining in London");
            } else await next(context);
        }
    }
}

Bu örneğimizde servis middleware sınıfına enjekte edilmektedir. ASP.NET Core Program.cs dosyasındaki app.UseMiddleware<WeatherMiddleware>(); satırını görür. Request pipeline'ı inşa edebilmek için WeatherMiddleware tipinden nesne oluşturması gerekmektedir. Bu nesneyi oluşturmak üzere WeatherMiddleware sınıfına gittiğinde yapıcı metotta IResponseFormatter türünden nesne gerektiğini görür. Servis yapılandırmasına bakar ve HtmlResponseFormatter nesnesi oluşturur (daha önce oluşturulduysa aynı nesneyi kullanır), bu nesneyi kullanarak WeatherMiddleware nesnesi oluşturur. Artık Request pipeline bu şekilde oluşturulmuştur. Artık sunucuya her talep geldiğinde bu WeatherMiddleware nesnesi üzerinden Invoke() metodu çalışacaktır.

Endpoint'ten servise erişmek

değiştir

Endpoint sınıfı statik bir metot üzerinden iş görmektedir. Endpoint sınıfında parametreleri belirtebileceğimiz bir yapıcı metot yoktur. Servise statik metodun içinden erişmek zorundayız. Bunun için çeşitli yaklaşımlar vardır. Devam eden kısımlarda bu yaklaşımlar anlatılmaktadır.

HttpContext nesnesi üzerinden servise erişme

değiştir

Örnek:

namespace Servisler
{
    public class WeatherEndpoint
    {
        public static async Task Endpoint(HttpContext context)
        {
            IResponseFormatter formatter = context.RequestServices.GetRequiredService<IResponseFormatter>();
            await formatter.Format(context, "Endpoint Class: It is cloudy in Milan");
        }
    }
}

Örneğimizde HttpContext nesnesi üzerinden erişilen RequestServices özelliği üzerinden GetRequiredService() metodu kullanılarak servise erişilmektedir. GetRequiredService() metoduyla benzer işlevi gören metotlar da vardır. Bunlar:

GetService<T>(): Servis varsa tip parametresindeki servisi döndürür. Servis yoksa null döndürür.
GetService(type): Servis varsa parametresindeki servisi döndürür. Servis yoksa null döndürür.
GetRequiredService<T>(): Servis varsa tip parametresindeki servisi döndürür. Servis yoksa çalışma zamanı hatası oluşur.
GetRequiredService(type): Servis varsa parametresindeki servisi döndürür. Servis yoksa çalışma zamanı hatası oluşur.

Adaptör fonksiyonu kullanmak

değiştir

Bir önceki örneğimizde akış her endpoint'e yönlendiğinden servis çözümlemesi yapılarak servise erişilmektedir. Halbuki buna gerek yoktur. Çünkü servise singleton pattern'i kullanılarak erişilmektedir. Her seferinde servis çözümleme gibi maliyetli bir işlemin yapılmasına gerek yoktur. Singleton servisler için bir kez servis çözümlemesi yapılıp diğer durumlarda direkt servis nesnesinin kullanılması daha mantıklı olacaktır. Şimdi Program.cs dosyasını şöyle değiştirelim:

using Servisler;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IResponseFormatter, HtmlResponseFormatter>();
var app = builder.Build();
app.UseMiddleware<WeatherMiddleware>();
app.MapGet("endpoint/1", async (HttpContext context, IResponseFormatter formatter) => {
    await formatter.Format(context,"Endpoint-1: It is snowing in Chicago");
});
IResponseFormatter formatter = app.ServiceProvider.GetRequiredService<IResponseFormatter>();
app.MapGet("endpoint/class", context => WeatherEndpoint.Endpoint(context, formatter));
//app.MapGet("endpoint/class", WeatherEndpoint.Endpoint);
app.MapGet("endpoint/2", async (HttpContext context, IResponseFormatter formatter) => {
    await formatter.Format(context, "Endpoint-2: It is sunny in LA");
});
app.Run();

Bu örneğimizde Program.cs dosyasında IResponseFormatter nesnesine bir kez erişilmekte ve daha sonraki bütün "endpoint/class" path'ı için yapılan endpoint eşlemelerinde aynı IResponseFormatter nesnesi kullanılmaktadır.

ActivatorUtilities sınıfının kullanımı

değiştir

Varsayalım yapıcı metot yoluyla bir servise bağımlılığı olan bir sınıf türünden nesne oluşturmak istiyorsanız. Bu durumda ne yapmanız gerekir? İşte bu bölümde bu sorunun cevabını arayacağız. Şimdi WeatherEndpoint.cs dosyasının içeriğini şöyle değiştirin:

namespace Servisler
{
    public class WeatherEndpoint
    {
        private IResponseFormatter formatter;
        public WeatherEndpoint(IResponseFormatter responseFormatter)
        {
            formatter = responseFormatter;
        }
        public async Task Endpoint(HttpContext context)
        {
            await formatter.Format(context, "Endpoint Class: It is cloudy in Milan");
        }
    }
}

Burada WeatherEndpoint sınıfını middleware sınıfına benzettik. Servis, yapıcı metot üzerinden enjekte ediliyor. ASP.NET Core varsayılan olarak bu tür bir endpoint sınıfındaki bağımlılıkları çözemez. Bu bağımlılıkları çözebilmek için ptojemize EndpointExtensions.cs isimli ve içeriği aşağıdaki gibi olan bir dosya ekleyelim:

using System.Reflection;
namespace Servisler
{
    public static class EndpointExtensions
    {
        public static void MapEndpoint<T>(this IEndpointRouteBuilder app, string path, string methodName = "Endpoint")
        {
            MethodInfo methodInfo = typeof(T).GetMethod(methodName);
            if(methodInfo == null || methodInfo.ReturnType != typeof(Task))
            {
                throw new System.Exception("Method cannot be used");
            }
            T endpointInstance = ActivatorUtilities.CreateInstance<T>(app.ServiceProvider);
            app.MapGet(path, (RequestDelegate)methodInfo.CreateDelegate(typeof(RequestDelegate), endpointInstance));
        }
    }
}

Bu eklenti metodunda önce tip parametresinde belirtilen tipte parametrede ismi belirtilen metot aranmaktadır. Metot varsa ilgili MethodInfo değişkeninde tutulmaktadır. Metot yoksa veya geri dönüş tipi Task değilse çalışma zamanı hatası oluşmaktadır. Daha sonrasında tip parametresinde verilen tipten nesne oluşturulmaktadır. Eğer nesne oluştururuken yapıcı metotta bağımlılıklar varsa bunlar da çözümlenmektedir. Daha sonra Program.cs dosyasından alınacak olan app değişkeni ile endpoint eşleme yapılmaktadır. Belirtilen path belirtilen sınıf nesnesi üzerinden belirtilen metoda eşlenmektedir. Belirtilen metodun RequestDelegate temsilcisine dönüştürülebiliyor olması gerekir. Eğer belirtilen metot RequestDelegate temsilcisine dönüştürülemezse çalışma zamanı hatası oluşur.

ActivatorUtilities sınıfı Microsoft.Extensions.DependencyInjection isim alanı altındadır. ActivatorUtilities sınıfının benzer metotları şunlardır:

CreateInstance<T>(services,args): Tip parametresinde belirtilen tipte nesne oluşturur, bağımlılıklar otomatik çözülür, istenirse servis olmayan argümanlar da (args) verilebilir.
CreateInstance(services, type, args): Parametrede belirtilen tipte nesne oluşturur, bağımlılıklar otomatik çözülür, istenirse servis olmayan argümanlar da (args) verilebilir.
GetServiceOrCreateInstance<T>(services, args): Tip parametresinde belirtilen nesne varsa bu nesneyi döndürür, yoksa oluşturup döndürür, bağımlılıklar otomatik çözülür, istenirse servis olmayan argümanlar da (args) verilebilir.
GetServiceOrCreateInstance(services, type, args): Parametrede belirtilen nesne varsa bu nesneyi döndürür, yoksa oluşturup döndürür, bağımlılıklar otomatik çözülür, istenirse servis olmayan argümanlar da (args) verilebilir.

Program.cs dosyasına, bu eklenti metodunu kullanarak yukarıda yazdığımız endpoint sınıfına route eşlemesi yapacak kodu şöyle ekleyebiliriz:

using Servisler;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IResponseFormatter, HtmlResponseFormatter>();
var app = builder.Build();
app.UseMiddleware<WeatherMiddleware>();
app.MapGet("endpoint/1", async (HttpContext context, IResponseFormatter formatter) => {
    await formatter.Format(context, "Endpoint-1: It is snowing in Chicago");
});
app.MapEndpoint<WeatherEndpoint>("endpoint/class");
app.MapGet("endpoint/2", async (HttpContext context, IResponseFormatter formatter) => {
    await formatter.Format(context, "Endpoint-2: It is sunny in LA");
});
app.Run();

Elde ettiğimiz şey MapEndpoint() eklenti metodu aracılığıyla bağımlılığı olan endpoint sınıflarına tıpkı middleware sınıfları gibi kolayca route eşlemesi yapabilmektedir.

Farklı yaşam döngüsü olan servislerin kullanımı

değiştir

Şimdiye kadar servisleri hep singleton pattern'ine göre kullandık. Bunun anlamı bir servis nesnesini her talep ettiğimizde aynı nesnenin verilecek olmasıdır. Ancak bu durum her senaryoya uygun olmayabilir. Bazen servisleri her talepte yeni nesne oluşturulacak şekilde veya her request-reponse ikilisi için yeni nesne olacak şekilde kullanmak isteyebiliriz. Bu yapılandırmalar Program.cs dosyasında servisleri eklediğimiz kısımda aşağıdaki metotları kullanarak yapılır:

AddSingleton<T,U>(): Şimdiye kadar servis eklemek için bu metodu kullandık. Her servis talebinde aynı nesne kullanılır.
AddTransient<T,U>(): Her servis talebinde yeni nesne oluşturulur.
AddScoped<T,U>(): Her bir scope için yeni nesne oluşturulur. Çoğunlukla bu scope bir request-response ikilisidir.

Bu metotların tek bir tip parametresi olan versiyonları da vardır. Bu durumda servise arayüz üzerinden değil, direkt sınıf üzerinden erişilir. Bu durumda servise zayıf bağlılık olmaz, ancak servise ulaşımda kolaylık yine korunur.

Transient servisler

değiştir

Farklı yaşam döngülerine sahip servisleri daha rahat işleyebilmek için projemize GuidService.cs isimli bir dosya ekleyelim ve içeriği şöyle yapalım:

namespace Servisler
{
    public class GuidService : IResponseFormatter
    {
        private Guid guid = Guid.NewGuid();
        public async Task Format(HttpContext context, string content)
        {
            await context.Response.WriteAsync($"Guid: {guid}\n{content}");
        }
    }
}

Bu sınıf IResponseFormatter servisine yeni bir implementasyon eklemektedir. Guid yapısının NewGuid() metodu her seferinde farklı bir guid değeri döndürmektedir. Bu sayede aynı nesne üzerinde çalışıp çalışmadığımızı daha rahat anlayabileceğiz. Şimdi Program.cs dosyasını şöyle değiştirelim:

using Servisler;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddTransient<IResponseFormatter, GuidService>();
var app = builder.Build();
app.UseMiddleware<WeatherMiddleware>();
app.MapGet("endpoint/1", async (HttpContext context, IResponseFormatter formatter) => {
    await formatter.Format(context, "Endpoint-1: It is snowing in Chicago");
});
app.MapEndpoint<WeatherEndpoint>("endpoint/class");
app.MapGet("endpoint/2", async (HttpContext context, IResponseFormatter formatter) => {
    await formatter.Format(context, "Endpoint-2: It is sunny in LA");
});
app.Run();

Gördüğünüz gibi artık programımız IResponseFormatter servisi için GuidService implementasyonunu kullanmaktadır. Ayrıca her servis çözümlemesi yapıldığında yeni bir GuidService nesnesi oluşturulacaktır. Bunu görmek için için önce "endpoint/1", sonra "endpoint/2" path'ına talepte bulunun. Sayfaya verilen guid çıktılarının değiştiğini göreceksiniz.

Transient servis nesnelerinin yanlışlıkla yeniden kullanılması

değiştir

Nesnelerin yaşam döngüsü kuralları servis çözümlemesi yapıldığında geçerlidir. Eğer servis çözümlemesi yapmadan daha önce oluşturduğunuz servis nesnesini kullanıyorsanız istediğiniz sonucu elde edemeyebilirsiniz. Örneğin programımızda önce "endpoint/1", sonra "endpoint/2" path'ına talepte bulunduğumuzda sayfaya verilen guid çıktılarının değiştiğini göreceğiz. Ama örneğin "endpoint/1" path'ındayken sayfayı yenilersek guid çıktısının değişmediğini görürüz. Çünkü daha önce de söylediğimiz gibi Program.cs dosyasındaki kodlar sunucu ayağa kalkarken bir kez çalıştırılır. Program.cs dosyasındaki kodlara bağlı olarak bir endpoint eşleme tablosu oluşturulur. Endpoint eşleme tablosu oluştururken ASP.NET Core "endpoint/1" ve "endpoint/2" path'larına karşılık gelen fonksiyonları inceler. Fonksiyonlardaki bağımlılıkları çözer. Daha sonra gelen her request'te fonksiyona verilecek nesneler bellidir. Bunlar HttpContext nesnesi ve çözümlenmiş olan IResponseFormatter nesneleridir. WeatherMiddleware sınıfı için de aynı sorun geçerlidir. WeatherMiddleware sınıfına bağımlılık yapıcı metot üzerinden enjekte edilmektedir. Request pipeline oluşturulurken ilgili middleware'ın yapıcı metodu yalnızca bir kez çalıştırılmaktadır. Daha sonraki her request'te aynı nesne üzerinden Invoke() metodu çağrılmaktadır. Middleware sınıfı için olan sorunu şöyle çözebiliriz:

namespace Servisler
{
    public class WeatherMiddleware
    {
        private RequestDelegate next;
        //private IResponseFormatter formatter;
        public WeatherMiddleware(RequestDelegate nextDelegate)
        {
            next = nextDelegate;
            //formatter = respFormatter;
        }
        public async Task Invoke(HttpContext context, IResponseFormatter formatter)
        {
            if (context.Request.Path == "/middleware/class")
            {
                await formatter.Format(context, "Middleware Class: It is raining in London");
            } else await next(context);
        }
    }
}

Gördüğünüz gibi bağımlılık yapıcı metottan Invoke() metoduna taşınmaktadır. Bu sayede her request'te yeniden çözümleme yapılacak ve her seferinde yeni bir nesnenin oluşturulması sağlanacaktır. Şimdi endpoint'ler için eşleme yapan EndpointExtensions.cs dosyasını şöyle değiştirelim:

using System.Reflection;
namespace Servisler
{
    public static class EndpointExtensions
    {
        public static void MapEndpoint<T>(this IEndpointRouteBuilder app, string path, string methodName = "Endpoint")
        {
            MethodInfo methodInfo = typeof(T).GetMethod(methodName);
            if (methodInfo == null || methodInfo.ReturnType != typeof(Task))
            {
                throw new System.Exception("Method cannot be used");
            }
            T endpointInstance = ActivatorUtilities.CreateInstance<T>(app.ServiceProvider);
            ParameterInfo[] methodParams = methodInfo.GetParameters();
            app.MapGet(path, context => (Task)(methodInfo.Invoke(endpointInstance, methodParams.Select(p =>
                p.ParameterType == typeof(HttpContext)
                ? context
                : app.ServiceProvider.GetService(p.ParameterType)).ToArray())));
        }
    }
}

Bu karışık kod MapEndpoint() isimli endpoint eşleme metodunu geliştirir. Daha önce sadece yapıcı metottaki bağımlılıkları çözmeye yarıyordu. Şimdi metotta da bağımlılıklar varsa onları da çözebilecek. Metodun bağımlılıklarını çözebilmek için önce metodun parametrelerini alıyor. Eğer parametre tipi HttpContext ise buna dokunmaz, eğer değilse kendisine verilen app değişkeni üzerinden servisleri çözer ve metodun servisleri çözülmüş haline eşleme yapar. Bu metot HttpContext tipi dışındaki bütün parametreleri servis gibi ele alır. Normal, servis olmayan parametrelerin de hesaba katıldığı daha gelişmiş (ve karışık) bir MapEndpoint() metodu da yazılabilir. Sonuç olarak transient olarak çözümlenmesini istediğimiz servisleri metot parametresi yaparız ve her request geldiğinde yeni çözümleme yapılır. Şimdi endpoint sınıfındaki bağımlılığı metoda taşıyabiliriz:

namespace Servisler
{
    public class WeatherEndpoint
    {
        //private IResponseFormatter formatter;
        //public WeatherEndpoint(IResponseFormatter responseFormatter) {
        // formatter = responseFormatter;
        //}
        public async Task Endpoint(HttpContext context, IResponseFormatter formatter)
        {
            await formatter.Format(context, "Endpoint Class: It is cloudy in Milan");
        }
    }
}

Scoped servisler

değiştir

Scoped servisler singleton ve transient servislerin arasında yer alır. Her request-response için bir nesne oluşturulur. Scoped servis örneklerini görmek için WeatherMiddleware sınıfını şöyle değiştirelim:

namespace Servisler
{
    public class WeatherMiddleware
    {
        private RequestDelegate next;
        public WeatherMiddleware(RequestDelegate nextDelegate)
        {
            next = nextDelegate;
        }
        public async Task Invoke(HttpContext context, IResponseFormatter f1, IResponseFormatter f2, IResponseFormatter f3)
        {
            if (context.Request.Path == "/middleware/class")
            {
                await f1.Format(context, string.Empty);
                await f2.Format(context, string.Empty);
                await f3.Format(context, string.Empty);
            } else await next(context);
        }
    }
}

Şimdi programı çalıştırıp "/middleware/class" path'ına talepte bulunursanız sayfaya üç tane farklı guid çıktısı verildiğini görürsünüz. Çünkü Program.cs dosyasında IResponseFormatter servisi transient olarak tanımlanmıştır. Eğer Program.cs dosyasındaki

builder.Services.AddTransient<IResponseFormatter, GuidService>();

satırını

builder.Services.AddScoped<IResponseFormatter, GuidService>();

satırı ile değiştirirsek her üç guid kodunun da aynı olduğunu görürüz. Ancak yine sayfayı yenilediğinizde bu sefer farklı bir guid kodunun üç kez ekrana yazıldığını görürsünüz.

Scoped servise scope dışında erişmeye çalışmak

değiştir

Scoped servisler sadece belirli bir scope'ta kullanılabilirler. Her request geldiğinde yeni bir scope oluşur. Bir scoped servise scoope dışında erişmeye çalışmak çalışma zamanı hatasına neden olur. Örneğin "endpoint/class" path'ına talep göndermemiz durumunda çalışma zamanı hatası oluşur. Çünkü EndpointExtensions sınıfının kodunu incelersek servisin Program.cs dosyasından alınan app değişkeni üzerinden servise erişmeye çalıştığını görürsünüz. Sınıfın hataya neden olan kısmı şöyledir:

app.MapGet(path, context => (Task)(methodInfo.Invoke(endpointInstance, methodParams.Select(p => 
    p.ParameterType == typeof(HttpContext)
    ? context 
    : app.ServiceProvider.GetService(p.ParameterType)).ToArray())));

Buradaki app değişkeni Program.cs dosyasında endpoint eşlemeleri yapmak için kullandığımız değişkendir ve request'ten bağımsızdır. Scoped servislere hatasız bir şekilde erişebilmek için endpoint'e gelen HttpContext nesnesi üzerinden servislere erişmemiz gerekir. Sorunu çözmek için EndpointExtensions.cs dosyasındaki yukarıdaki kısmı şöyle değiştirmeliyiz:

app.MapGet(path, context => (Task)(methodInfo.Invoke(endpointInstance, methodParams.Select(p => 
    p.ParameterType == typeof(HttpContext)
    ? context 
    : context.RequestServices.GetService(p.ParameterType)).ToArray())));

Evet, her şey güzel gidiyor. Ancak bir sorun var. Bir endpoint sınıfında bir scoped servise veya transient servise bağımlılık oluşturmak için bağımlılığın metot üzerinden tanımlanması gerekiyor. Eğer bir singleton servise bağımlılık oluşturmak istiyorsak yapıcı metot üzerinden bağımlılık oluşturmalıyız. Bu, istenmeyen bir durumdur. Servisi kullanacak olan sınıf servisin nasıl sağlandığı bilgisine sahip değildir, zaten bu bilgiye de sahip olmamalıdır. Sadece servis nesnesine erişmeli ve işini görmelidir. Gerisine karışmamalıdır. O yüzden EndpointExtensions.cs dosyasının içeriğini yeniden değiştirmemiz gerekiyor:

using System.Reflection;
namespace Servisler
{
    public static class EndpointExtensions
    {
        public static void MapEndpoint<T>(this IEndpointRouteBuilder app, string path, string methodName = "Endpoint")
        {
            MethodInfo methodInfo = typeof(T).GetMethod(methodName);
            if (methodInfo == null || methodInfo.ReturnType != typeof(Task))
            {
                throw new System.Exception("Method cannot be used");
            }
            T endpointInstance = ActivatorUtilities.CreateInstance<T>(app.ServiceProvider);
            ParameterInfo[] methodParams = methodInfo.GetParameters();
            app.MapGet(path, context => {
                T endpointInstance = ActivatorUtilities.CreateInstance<T>(context.RequestServices);
                return (Task)methodInfo.Invoke(endpointInstance, methodParams.Select(p => 
                    p.ParameterType == typeof(HttpContext)
                    ? context
                    : context.RequestServices.GetService(p.ParameterType)).ToArray())!;
            });
        }
    } 
}

Bu kodda gelen her request için yeni bir endpoint sınıfı nesnesi oluşturulmakta, nesne oluşturulurken bağımlılık varsa çözümlenmekte, daha sonra bu nesne üzerinden ilgili metot çağrılmakta, eğer metodun da bağımlılıkları varsa bunlarda çözümlenmekte, en son söz konusu metot söz konusu nesne üzerinden çağrılmaktadır. Servis edinimleri HttpContext nesnesi üzerinden yapıldığı için scoped servislerde sorun çıkartmaz. Yine transient servisler için de sorunsuzdur. Çünkü her request için yeni servis çözümlemesi yapılır. Zaten singleton servislerde bir sorun yoktu. Bu sayede endpoint sınıfı artık kullandığı servisin yaşam döngüsünü umursamadan istediği servisi istediği gibi yapıcı metot yoluyla ve metot parametresi yoluyla özgürce kullanabilecek.

Scoped servislerin lambda ifadelerinde kullanılması

değiştir

Program.cs dosyasındaki fonksiyon şeklinde tanımlanan endpoint'ler de HttpContext nesnesi üzerinden servislere erişebilir. Program.cs dosyasını şöyle değiştirelim:

using Servisler;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IResponseFormatter, GuidService>();
var app = builder.Build();
app.UseMiddleware<WeatherMiddleware>();
app.MapGet("endpoint/1", async (HttpContext context, IResponseFormatter formatter) =>
{
    await formatter.Format(context, "Endpoint-1: It is snowing in Chicago");
});
app.MapEndpoint<WeatherEndpoint>("endpoint/class");
app.MapGet("endpoint/2", async (HttpContext context) => {
    IResponseFormatter formatter = context.RequestServices.GetRequiredService<IResponseFormatter>();
    await formatter.Format(context, "Endpoint-2: It is sunny in LA");
});
app.Run();

Örnekte 2. endpoint IResponseFormatter servisine HttpContext nesnesi üzerinden erişmektedir.

app değişkeni üzerinden scoped servislere erişme

değiştir

Şimdiye kadar app değişkeni üzerinden scoped servislere erişemeyeceğimizi söylemiştik. Ancak bazen nadir durumlarda app değişkeni üzerinden bir scoped servise erişmemiz gerekebilir. Middleware ve endpoint'lerde her halükarda elimizde HttpContext nesnesi olacaktır. Ancak Program.cs dosyasında bir endpoint ve middleware içinde olmayan, ama bir scoped servise erişmesi gereken bir kod yazmamız gerektiği durumlarda sorunu şöyle çözebiliriz:

app.Services.CreateScope().ServiceProvider.GetRequiredService<ScopedServis>();

Burada scoped servise ulaşabilmek için önce CreateScope() metoduyla bir scope oluşturmaktayız, daha sonra bu scope üzerinden scoped servise erişmekteyiz.

Bağımlılık zincirleri oluşturma

değiştir

Bir sınıf yapıcı metot veya metot parametresi yoluyla bir servise bağımlı olabilir. Bu servis de yine yapıcı metot veya metot parametresi yoluyla başka bir servise bağımlı olabilir. Bu şekilde bağımlılık zincirleri oluşturulabilir. Örnek olarak projemize içeriği aşağıdaki gibi olan ITimeStamper.cs isminde bir arayüz ekleyelim:

namespace Servisler
{
    public interface ITimeStamper
    {
        string TimeStamp { get; }
    }
}

Daha sonra bu arayüzü uygulayan DefaultTimeStamper isminde bir sınıf ekleyelim. İçeriği şöyle olsun:

namespace Servisler
{
    public class DefaultTimeStamper : ITimeStamper
    {
        public string TimeStamp
        {
            get => DateTime.Now.ToShortTimeString();
        }
    }
}

Şimdi IResponseFormatter servisine yeni bir implementasyon ekleyelim. İsmi TimeResponseFormatter.cs olsun ve içeriği şöyle olsun:

namespace Servisler
{
    public class TimeResponseFormatter : IResponseFormatter
    {
        private ITimeStamper stamper;
        public TimeResponseFormatter(ITimeStamper timeStamper)
        {
            stamper = timeStamper;
        }
        public async Task Format(HttpContext context, string content)
        {
            await context.Response.WriteAsync($"{stamper.TimeStamp}: {content}");
        }
    }
}

Şimdi Program.cs dosyasına aşağıdaki servis üyelik kodlarını ekleyelim. Program.cs dosyasında sadece bu servis üyelik kodları olsun. Yine servis eklemeleri mutlaka servislerin eklendiği kısımda yapın (builder değişkeninin oluşturulması ile app değişkeninin oluşturulması arası):

builder.Services.AddScoped<IResponseFormatter, TimeResponseFormatter>();
builder.Services.AddScoped<ITimeStamper, DefaultTimeStamper>();

Programımızda IResponseFormatter servisine başvuru olduğunda çözümlenme için IResponseFormatter sınıfının yapıcı metoduna bakılır. Yapıcı metota başka bir servise başvuru vardır. Sonra bu servis çözümlenir. Bu servis çözümlendikten sonra çözümlenen ITimeStamper nesnesi kullanılarak TimeResponseFormatter nesnesi oluşturulur.

Birbirine bağımlı servisler için farklı yaşam döngüleri kullanılması hata oluşturmaz, ancak bu durum mantıksız ve tuhaf sonuçlara neden olabilir. Servis yaşam döngülerinin yalnızca servis çözümlemesi yapıldığında uygulanacağı unutulmamalıdır. Örneğin asıl servis singleton yaşam döngüsüne sahip olsun, bunun bağımlı olduğu servis transient yaşam döngüsüne sahip olsun. Eğer bağlantılı servis sadece asıl servis tarafından kullanılıyorsa bağlantılı servis de otomatik olarak singleton'mış gibi davranacaktır. Çünkü ilk sefer dışında hiçbir zaman bağlantılı servis çözümlemesi yapılmayacaktır. Daima daha önce oluşturulan asıl servis nesnesi kullanılacaktır.

Programın yapılandırmasıne göre farklı servislere abone olma

değiştir

Örnek (Program.cs dosyası):

using Servisler;
var builder = WebApplication.CreateBuilder(args);
IWebHostEnvironment env = builder.Environment;
if (env.IsDevelopment())
{
    builder.Services.AddScoped<IResponseFormatter, TimeResponseFormatter>();
    builder.Services.AddScoped<ITimeStamper, DefaultTimeStamper>();
}
else
{
    builder.Services.AddScoped<IResponseFormatter, HtmlResponseFormatter>();
}
var app = builder.Build();
app.UseMiddleware<WeatherMiddleware>();
app.MapGet("endpoint/1", async (HttpContext context, IResponseFormatter formatter) => {
    await formatter.Format(context, "Endpoint-1: It is snowing in Chicago");
});
app.MapEndpoint<WeatherEndpoint>("endpoint/class");
app.MapGet("endpoint/2", async (HttpContext context) => {
    IResponseFormatter formatter = context.RequestServices.GetRequiredService<IResponseFormatter>();
    await formatter.Format(context, "Endpoint-2: It is sunny in LA");
});
app.Run();

Bu örnekte programın geliştirme sürecinde olup olmadığına göre farklı servislere abone olunmaktadır. Aynı zamanda programın yapılandırma ayarlarına env değişkeni üzerinden erişeceğimiz Configuration özelliği aracılığıyla da erişebilir ve farklı yapılandırmalara göre farklı servislere üye olmayı seçebiliriz.

Servis nesnelerini elle oluşturma

değiştir

Şimdiye kadar servis nesneleri oluşturma süreci otomatikti. Biz bir arayüz, bir de implementasyon sınıfı belirtiyorduk. ASP.NET Core bizim yerimize arayüz gerektiği durumlarda ilgili sınıf nesnesini oluşturup servisi kullanacak sınıfa veriyordu. İstersek bu sürece el atabiliriz. Örnek (Program.cs dosyası):

using Servisler;
var builder = WebApplication.CreateBuilder(args);
IConfiguration config = builder.Configuration;
builder.Services.AddScoped<IResponseFormatter>(serviceProvider => {
    string typeName = config["services:IResponseFormatter"];
    return (IResponseFormatter)ActivatorUtilities.CreateInstance(serviceProvider, typeName == null
        ? typeof(GuidService) : Type.GetType(typeName, true));
});
builder.Services.AddScoped<ITimeStamper, DefaultTimeStamper>();
var app = builder.Build();
app.UseMiddleware<WeatherMiddleware>();
app.MapGet("middleware/1", async (HttpContext context, IResponseFormatter formatter) => {
    await formatter.Format(context, "Middleware-1: It is snowing in Chicago");
});
app.MapEndpoint<WeatherEndpoint>("endpoint/class");
app.MapGet("endpoint/2", async (HttpContext context) => {
    IResponseFormatter formatter = context.RequestServices.GetRequiredService<IResponseFormatter>();
    await formatter.Format(context, "Endpoint-2: It is sunny in LA");
});
app.Run();

Bu kodda ITimeStamper servisi için nesne oluşturma süreci otomatiktir. Ancak IResponseFormatter servisi için nasıl nesne oluşturulacağını kendimiz belirliyoruz. Bunun için yapılandırma ayarlarından "services:IResponseFormatter" kısmını buluyoruz. Eğer yapılandırma ayarlarında IResponseFormatter servisi için bir implementasyon belirtilememişse implementasyon için GuidService sınıfını seçiyoruz. Eğer yapılandırma ayarlarında IResponseFormatter servisi için bir implementasyon belirtilmişse bu implementasyonu kullanıyoruz. Bu kodun düzgün çalışması için appsettings.Development.json dosyasının içeriğini şöyle değiştirmeliyiz:

{
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft.AspNetCore": "Warning"
        }
    },
    "services": {
        "IResponseFormatter": "Servisler.HtmlResponseFormatter"
    }
}

Basitçe bu yapılandırma dosyasına "services" isimli bir girdi, bunun da altına "IResponseFormatter" isimli bir girdi, ve değer olarak da kullanmak istediğimiz implementasyon sınıfını ekliyoruz.

AddScoped() metoduna benzer şekilde AddTransient() ve AddSingleton() metotları kullanarak da elle implementasyon nesnesi oluşturabiliriz. Elbetteki nesne oluşturacak kodlar sadece nesne oluşturma gerektiğinde çalışacaktır. Örneğin AddSingleton() metodunu kullanmışsak programın ömrü boyunca sadece bir kez çalışacaktır.

Bir servis için birden fazla implementasyonu hazır bulundurma

değiştir

Şimdiye kadar servisi kullanacak sınıfın hangi servis implementasyonunu kullanacağını seçme imkanı yoktu. Ancak istersek servisi kullanan sınıflara implementasyon sınıfını seçme hakkını tanıyabiliriz. Şimdi IResponseFormatter arayüzünü şöyle değiştirelim:

namespace Servisler
{
    public interface IResponseFormatter
    {
        Task Format(HttpContext context, string content);
        public bool RichOutput => false;
    }
}

Şimdi HtmlResponseFormatter sınıfının içeriğini şöyle değiştirelim:

namespace Servisler
{
    public class HtmlResponseFormatter : IResponseFormatter
    {
        public async Task Format(HttpContext context, string content)
        {
            context.Response.ContentType = "text/html";
            await context.Response.WriteAsync($@"
                <!DOCTYPE html>
                <html lang=""en"">
                <head><title>Response</title></head>
                <body>
                <h2>Formatted Response</h2>
                <span>{content}</span>
                </body>
                </html>");
        }
        public bool RichOutput => true;
    }
}

Bu implementasyon sınıfı RichOutput özelliğinin üzerine yazmakta çıktının zengin metin formatında olduğunu belirtmektedir. Şimdi Program.cs dosyasını şöyle değiştirelim:

using Servisler;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IResponseFormatter, TextResponseFormatter>();
builder.Services.AddScoped<IResponseFormatter, HtmlResponseFormatter>();
builder.Services.AddScoped<IResponseFormatter, GuidService>();
var app = builder.Build();
app.MapGet("single", async context => {
    IResponseFormatter formatter = context.RequestServices.GetRequiredService<IResponseFormatter>();
    await formatter.Format(context, "Single service");
});
app.MapGet("/", async context => {
    IResponseFormatter formatter = context.RequestServices.GetServices<IResponseFormatter>().First(f => f.RichOutput);
    await formatter.Format(context, "Multiple services");
});
app.Run();

Gördüğünüz gibi Program.cs dosyasına aynı servis için üç implementasyon ekledik. İmplementasyonu seçmek istemeyen sınıflar direkt servisi kendilerine enjekte edebilir. Bu durumda son tanımlanan implementasyon (GuidService) kullanılacaktır. Örneğin "single" path'ına karşılık gelen endpoint bu yolu izlemektedir. / path'ına karşılık gelen endpoint ise belirli bir özelliği true olan ilk implemetasyonu seçmektedir. Bu örneğimizde RichOutput özelliği true olan ilk implementasyon olan HtmlResponseFormatter sınıfı implementasyon olarak seçilmektedir. Bu özelliklerin sayısı daha fazla artırılarak daha karmaşık seçimler de yapılabilir. Ancak servisi kullanacak sınıf dikkatli olmalıdır, sonuçta kriterleri sağlayan bir servis olmayabilir. Böyle bir durumda servis kullanılmaya çalışıldığında çalışma zamanı hatası oluşacaktır. Bu çalışma hatasını önleme sorumluluğu servisi kullanacak sınıftadır.

Jenerik tipli servisler kullanma

değiştir

Bir servis arayüzü jenerik içerebilir. Bu durum o servisi kullanmamıza engel değildir. Örnek (Program.cs dosyası):

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton(typeof(ICollection<>), typeof(List<>));
var app = builder.Build();
app.MapGet("string", async context => {
    ICollection<string> collection = context.RequestServices.GetRequiredService<ICollection<string>>();
    collection.Add($"Request: { DateTime.Now.ToLongTimeString() }");
    foreach (string str in collection)
    {
        await context.Response.WriteAsync($"String: {str}\n");
    }
});
app.MapGet("int", async context => {
    ICollection<int> collection = context.RequestServices.GetRequiredService<ICollection<int>>();
    collection.Add(collection.Count() + 1);
    foreach (int val in collection)
    {
        await context.Response.WriteAsync($"Int: {val}\n");
    }
});
app.Run();

Bu örneğimizde tip parametresi ne olursa olsun ICollection<> tipindeki servis çağrıları için List<> kullanılmaktadır. Burada önemli nokta ICollection<string> ile ICollection<int> tiplerinin farklı tipler olarak ele alınmasıdır. Bu sayede servis singleton pattern'inde olsa da ICollection<string> ve ICollection<int> servis çağrıları için iki farklı nesne üretilir.