Detaylı Keyed DI Servisler
Selamlar,
Bu makalede .Net 8.0 ile gelen, bildiğiniz Dependency Injection mantığını değiştiren “Keyed DI Service”lerden detaylıca bahsedeceğiz.
Temel olarak, aynı servisleri farklı adlar altında birden çok kez kaydetme ve belirli bir tür/ad birleşimini enjekte etme olanağını bizlere sunuyor. Bunu mümkün kılmak için, eski dostumuz “IServiceProvider“‘ı iki yeni yöntemle genişleten yeni bir arayüz olan “IKeyedServiceProvider”’ı kullanıyor.
Gelin hemen bir örnek ile konuya dalalım: Bir kaç kullanıcıyı siteye ekleyip, filitreleyen bir servis yazalım
UserModel:
1 2 3 4 5 6 7 |
public class User { public int Id { get; set; } public string Name { get; set; } public string Username { get; set; } public string Email { get; set; } } |
IUserService:
1 2 3 4 5 |
public interface IUserService { public List GetUsers(); public User GetUserByID(int Id); } |
UserService: Aşağıda Constructorda, eğer UserListesi boş ise doldurulan bir UserServis görülmektedir. Ayrıca “GetUserByID()” ile, istenen User Filitrelenerek getirilmektedir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public class UserService : IUserService { static List UserList; public UserService() { if(UserList == null) { UserList = new List(); UserList.Add(new User() { Id = 1, Name = "Bora",Email="bora@borakasmer.com" }); UserList.Add(new User() { Id = 2, Name = "Emre", Email = "emre78@gmail.com" }); UserList.Add(new User() { Id = 3, Name = "Mehmet", Email = "mehmet@yahoo.com" }); UserList.Add(new User() { Id = 4, Name = "Ahmet", Email = "ahmet85@gmail.com" }); } } public User GetUserByID(int Id) { return UserList.Where(x => x.Id == Id).FirstOrDefault(); } public List GetUsers() { return UserList; } } |
Program.cs:
1 |
builder.Services.AddKeyedTransient<IUserService, UserService>("user"); |
“user” string keyword’ü ile Transient olarak ilgili IUserService, tanımlanmıştır.
Peki nasıl kullanıyoruz ?
UserController: Aşağıda görüldüğü gibi, User sınıfın Constructor’ında tanımlı birşey yoktur. Hatta User sınıfın constructor’ı bile yoktur :) Servisin kullanılacağı method’a “IUserService“, “[FromKeyedServices(“user”)]” keyword’ü ile parametre olarak tanımlanmıştır. Ve ilgili servise ait methodlar, ilgili WebApi içerisinde kullanılmıştır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
[ApiController] [Route("[controller]")] public class UserController : ControllerBase { [HttpGet("GetUserList")] public List GetUserList([FromKeyedServices("user")] IUserService user) { return user.GetUsers(); } [HttpGet("GetUserByID")] public User GetUserByID([FromKeyedServices("user")] IUserService user,int Id) { return user.GetUserByID(Id); } } |
Yukarıda dikkatinizi çekmek istediğim bir konu daha var. String bir keyword ile (‘User‘) ilgili servis çağrılmıştır. Yani ilgili aynı servis, başka bir keywordler ile de ya da string keyword değiştirilerek temsil ettiği farklı servis de, method’a parametre olarak verilebilirdi.
Şimdi biraz hayal gücümüzü zorlayalım ve bu “user” string keyword’ü configden alsak, method’a parametre olarak alınan servisi dinamik olarak değiştirebilir miyize hep beraber bakalım.
Şimdi gelin “WeatherService ve WeatherService2” adında iki servis oluşturalım.
Models:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class CodPlayers { public string name { get; set; } public string language { get; set; } public string id { get; set; } public string bio { get; set; } public double version { get; set; } } public class ShortCodPlayers { public string name { get; set; } public double version { get; set; } } |
IWeatherService:
1 2 3 4 5 6 |
public interface IWeatherService { public void GetMyName(); public void GetMyName(string name); public Task<List> GetAllCodPlayers(); } |
WeatherService: Aşağıda görüldüğü gibi örnek amaçlı üç method tanımlanmıştır. WeatherService2’de de aynı methodlar vardır ama console’a farklı anlamak için bu sefer “WeatherService2” yazmaktadır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
using Con = System.Console; using Env = System.Environment; using GoogleMapPoint = (double latitude, double longitude, string name); using CW = System.Diagnostics.Debug; using Net8Features.Models; public class WeatherService : IWeatherService { public void GetMyName() { Con.WriteLine($"Hello Bora Kasmer. Os Versin:{Env.OSVersion}"); GetMyName("Bill Gates"); } public void GetMyName(string name) { GoogleMapPoint mySchool = (38.8951, -77.0364, "ITU"); Con.WriteLine($"Hello {name}"); RedisCore redis = new(); var redisValue = redis.GetValue(); Con.WriteLine(redisValue); RedisCore redis2 = new(94, "Bill Gates", null, 60); var redisValue2 = redis2.GetValue(); Con.WriteLine(redisValue2); CW.WriteLine("WEATHER SERVICE"); } public async Task<List> GetAllCodPlayers() { using (HttpClient client = new()) { var players = client.GetFromJsonAsAsyncEnumerable("https://microsoftedge.github.io/Demos/json-dummy-data/64KB.json"); var shortPlayerList = new List(); await foreach (var player in players) { shortPlayerList.Add(new ShortCodPlayers() { name = player.name, version = player.version }); } return shortPlayerList.OrderBy(x => x.version).ToList(); } } public class RedisPersonKeyGenerator(ref readonly int id, string name) { int num = id; public string RedisKey => $"person:{num.ToString()}:{name}"; } public class RedisCore(int id, string name, object val, int expireMinute) : RedisPersonKeyGenerator(id, name) { public RedisCore() : this(78, "bora", null, 30) { } // default RedisCore public int Id => id; public object Value => val; public TimeSpan ExpireTime = TimeSpan.FromMinutes(expireMinute); public string GetValue() { return $"RedisClient: Key={RedisKey}, Value:{Value}, ExpireTime:{ExpireTime}"; } } } |
program.cs: Bu sefer Weather ve Weather2 Servisleri, iki farklı string keyword ile aşağıdaki gib tanımladık. Ayrıca Configuration dosyasındaki “WeatherServiceOptions” kısmını tanımladık. Sıradaki amacımız Method içinde tanımlı servise parametresini config dosyadan alıp, çalışılacak servisi dinamik olarak değiştirmek.
1 2 3 4 5 6 7 8 9 |
var configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", false, true) .Build(); builder.Services.Configure(configuration.GetSection("WeatherServiceOptions")); builder.Services.AddKeyedTransient<IWeatherService, WeatherService>("weather"); builder.Services.AddKeyedTransient<IWeatherService, WeatherService2>("weather2"); |
appsettings.json: Başta tanımlı servis “weather” ile ilk servisdir.
1 2 3 |
"WeatherServiceOptions": { "ServiceName": "weather" }, |
WeatherServiceOptions:
1 2 3 4 |
public class WeatherServiceOptions { public string ServiceName { get; set; } } |
WeatherForecastController: Controller’ın Constructor’inda IOptionsSnapshot interface’i ile ilgili config tanımlanmıştır. Amaç, config dosya değiştiği zaman anlık bu değişimden, kodun derlenmeden fark edilmesidir.
1 2 3 4 5 |
public readonly IOptionsSnapshot _config; public WeatherForecastController(IOptionsSnapshot config) { _config = config; } |
WeatherForecastController(2): Geldik esas amacımızı test edeceğimiz EndPoint’e. FromKeyedServices ile çağracağımız string’i, configden çağırmak maalesef ÇALIŞMAMAKTADIR. Bu tarz dinamik tanımlama, KeyedServislerinde Hatay neden olmaktadır. Tanimlanacak String maalesef static olarak yazilmalidir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
//HATALI public List Get([FromKeyedServices(_config.Value.ServiceName)] IWeatherService service) public List Get([FromKeyedServices("weather2")] IWeatherService service, int id) { service.GetMyName(); List list = new(); List weekDays = ["Pazartesi", "Sali", "Carsamba", "Persembe", "Cuma"]; var randomSummaries = GetRandom(new List(), 5); foreach (var item in randomSummaries) { dicktFrozen.TryGetValue(item, out int itemID); list.Add(itemID); } return list; } |
Ama çıkmadık candan ümit kesilmez :=) Farklı bir yaklaşım ile amacımıza ulaşmaya çalışalım.
Şimdiki örnekde Database DBContext’in Connection’ini değiştirebileceğimiz, bir yöntemi göstermek istiyorum.
program.cs: Aşağıda görüldüğü gibi NorthWindContext “AddKeyedScoped()” keyword’ü ile DBContext her ihtiyaç duyulduğunda tekrardan yaratılacaktır. Doğal olarak, connection string değişirse bu herseferinde fark edilecektir.
1 2 |
builder.Services.AddTransient<NorthWindContext>(); builder.Services.AddKeyedScoped<NorthWindContext>("scoped"); |
WeatherTestController.cs: Aşağıdaki controllerda NorthwindContext, scoped olarak çağrılmaktadır..
1 2 3 4 5 6 |
[HttpGet("GetUserByIDKeyedServiceScoped")] public User GetUserByIDKeyedServiceScoped([FromKeyedServices("scoped")] NorthwindContext dbContext, int id) { var result = dbContext.GetUserByID(id); return result; } |
Şimdi sıra geldi son örneğimiz olan String Değişiminde KeyedServiceProvider ile Kullanılacak Servisin Değiştirilmesine
WeatherTestController.cs: Aşağıda görüldüğü gibi “IServiceProvider” dependency Injection ile Constructor’da alınmıştır.
1 2 3 4 5 |
IServiceProvider _serviceProvider; public WeatherTestController(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } |
ServiceType(Enum): Services tipini belirlemek amacı ile kullanılan Enum. İlgili enum parametre olarak verilerek, istenen servis dinamik olarak çağrılacaktır.
1 2 3 4 5 |
public enum ServiceType { weather = 1, weather2 = 2, } |
WeatherTestController.cs: Aşağıda görüldüğü gibi “_serviceProvider.GetRequiredKeyedService” ile parametrik olarak alınan enum serviceType’a göre, istenen servis dinamik olarak çağrılmıştır.
1 2 3 4 5 6 7 |
[HttpGet("GetUserByIDKeyedServiceProvider/{serviceType}")] public IActionResult GetUserByIDKeyedServiceProvider(ServiceType serviceType) { var weatherService = _serviceProvider.GetRequiredKeyedService(serviceType.ToString()); weatherService.GetMyName(); return Ok(); } |
Yukarıda görüldüğü gibi, seçilen servis tipine göre “WeatherService” ve “WeatherService2” dinamik olarak değiştirilip çağrılabilmektedir.
Geldik bir makalenin daha sonuna, Bu makalede, Keyed DI Serviceler ile Dependency Incetion mantığına farklı bir bakış atmış olduk. Artık çağrılacak servislerimizi Constructor’da almak yerine, methodlara parametrik olarak kullanabileceğiz. Önceden yapamadığımız, aynı servisleri artık farklı keywordler ile projemize dahil edebileceğiz. Böylece parametreye göre farklı şekilde ayağa kalkacak tek bir servisi, farklı keywordler ile projemize implemente edebileceğiz. Ayrıca _serviceProvider kullanılarak, belirlenecek parametreler ile eklenecek olan servislerin dinamik olarak değişmesi sağlanabilmektedir. Son olarak Keyed DI Serviceler ile Dependency Incetion’a büyük esneklik getirilmiştir. Özellikle dağıtık yapılarda mikroservislerde, hem kod okunaklığı, alınacak servislerin methodlara parametre olarak verilmesi ile artmış hem de string parametreler ile farklı servislerin dinamik seçilmesi, ona büyük esneklik sağlamıştır.
Yeni bir makalede görüşmek üzere hepinize hoşçakalın.
Son Yorumlar