.NET Core'da Custom Feature Flag Provider Geliştirme

Merhabalar 👋

Bu yazımda .NET Core'da feature'ları yönetebildiğimiz kütüphane olan FeatureManagement kütüphanesinden ve feature ayarlarımızı saklayabileceğimiz farklı alanlardan bahsedeceğim.

Bazen geliştirdiğimiz özellikleri canlı ortama almışken o özelliklerin kapalı gelmesini ve zamanı geldiği zaman açmak isteyebiliriz. Bu tip operasyonları yönetmek için .NET Core'da FeatureManagement kütüphanesinden yararlanıyoruz.

FeatureManagement'ı Kullanarak Feature'ların Yönetimi

Bu yazıda örneğimizi bir console application üzerinde gerçekleştireceğiz. Örneğimize başlamak için aşağıdaki komutu kullanarak bir console application oluşturalım.

dotnet new console -n FeatureManagement.Console

Sonrasında ise projemize Microsoft.FeatureManagement pakedini referans olarak eklemek için aşağıdaki komutu yazalım.

dotnet add package Microsoft.FeatureManagement

Projeyi oluşturduktan sonra kullanmış olduğumuz IDE veya metin editörü yardımıyla projemizi açalım. Program.cs dosyamızın içeriğini aşağıdaki şekilde değiştirelim.

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.FeatureManagement;

namespace FeatureManagement.Console
{
	class Program
	{
		static IConfiguration SetupConfiguration()
		{
			var builder = new ConfigurationBuilder()
					.SetBasePath(Directory.GetCurrentDirectory())
					.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);

			return builder.Build();
		}

		static IServiceCollection ConfigureServices()
		{
			var services = new ServiceCollection();
			var config = SetupConfiguration();

			services.AddSingleton(config);
			services.AddFeatureManagement(config.GetSection("Features"));

			return services;
		}

		static async Task Main(string[] args)
		{
			var services = ConfigureServices();
			var serviceProvider = services.BuildServiceProvider();
			var featureManager = serviceProvider.GetRequiredService<IFeatureManager>();

			if (await featureManager.IsEnabledAsync("SendEmail"))
			{
				SendMail();
			}
		}

		static void SendMail()
		{
			System.Console.WriteLine("E-mail sent!");
		}
	}
}

Bu değişikliği yaptıktan sonra ise projemizin ana dizinine appsettings.json dosyasını ekleyelim ve içeriğini aşağıdaki şekilde düzenleyelim.

{
  "Features": {
    "SendEmail": true
  }
}

Şimdi ise neler yaptığımızı inceleyelim. SetupConfiguration metodu ile appsettings.json dosyasının uygulamamız tarafından okunmasını sağladık. Ayar dosyasını okumamızın sebebi FeatureManagement'ın varsayılan olarak aktif/pasif ayarlarını appsettings.json üzerinde yönettiğinden dolayıdır.

ConfigureServices metodu ile dependency injection ayarlarını yaptık. Bu bölümde services.AddFeatureManagement extension method'u ile FeatureManagement'ı uygulamamız genelinde kullanılabilir hale getirdik. Parametre olarak verdiğimiz config.GetSection("Features") değeri ile de feature'ların appsettings.json dosyasında hangi bölümde bulunduğunu belirttik. Ayrıca bu extension method IFeatureManager interface'ini uygulama genelinde kullanılabilir hale getirmektedir.

services.AddFeatureManagement metoduna herhangi bir parametre vermezseniz feature ayarlarını appsettings.json dosyasında FeatureManagement isminde bir alanda arayıp yönetecektir.

Artık uygulamamız üzerinde feature'ların aktif olup olmadığını IFeatureManager.IsEnabledAsync metodu ile kontrol edebiliriz. Örnek kodumunuzun Main metodunda featureManager.IsEnabledAsync("SendMail") çağrımı ile SendMail feature'ının aktif olup olmadığını anlayabiliriz. SendMail'in aktif olup olmadığının bilgisi appsettings.json dosyasında Features.SendMail bölümününde bulunmaktadır.

Şimdi dotnet run komutu ile uygulamamızı çalıştıralım ve çıktıyı kontrol edelim.

appsettings.json dosyasında SendEmail feature'ının değeri true olduğundan dolayı ekranda Email sent! yazmaktadır. Bu alanın değerini false olarak değiştirirsek ekranda hiçbir şey yazmayacaktır.

Feature Filter Tanımlamak

Eğer featurelarımızın temel yönetimini true/false olarak değil de daha kapsamlı değerlerle yönetmek istersek Feature Filter'lardan yararlanabiliriz.

Örneğin kullanıcının girmiş olduğu mobil işletim sistemi versiyonuna göre push notification gönderim seçeneklerini düzenleyelim. Öncelikle projemize appsettings.json dosyamızı aşağıdaki şekilde düzenleyelim.

{
  "Features": {
    "SendPush": {
      "EnabledFor": [
        {
          "Name": "OperatingSystem",
          "Parameters": {
            "Value": "14.5"
          }
        }
      ]
    }
  }
}

Bu dosyadaki tanımlamaya göre feature'ımızın ismi SendPush ve filter'ımızın ismi ise OperatingSystem'dır. Parameteres'ın altında ise filter'ın çalışması gereken koşulu belirtiyoruz.

Şimdi ise feature filter'ı tanımlayabiliriz. Bunun için projemize PushNotificationFilter.cs isminde bir dosua ekleyelim ve içeriğini aşağıdaki şekilde düzenleyelim.

using System.Threading.Tasks;
using Microsoft.FeatureManagement;

namespace FeatureManagement.Console
{
	public class PushNotificationContext
	{
		public string OsVersion { get; set; }
	}

	[FilterAlias("OperatingSystem")]
	public class PushNotificationFilter : IContextualFeatureFilter<PushNotificationContext>
	{
		public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext featureFilterContext, PushNotificationContext appContext)
		{
			return Task.FromResult(appContext.OsVersion == "14.5");
		}
	}
}

PushNotificationFilter sınıfımızın appsettings.json dosyasında hangi filter ile eşleşeceğini FilterAlias attribute'ü ile belirtiyoruz. Feature filter dışarıdan veri alacağından dolayı sınıfı IContextualFeatureFilter interface'inden türetiyoruz. Eğer dışarıdan alınacak bir context'e ihtiyaç yoksa IFeatureFilter interface'i kullanılabilir. EvaluateAsync metodu ile context üzerinden gelen verinin 14.5 değerine eşit olup olmadığına bakıyoruz. Eğer eşitse feature'ımız aktif olarak değerlendirilecektir.

Son aşamada ise yazdıklarımızı test edelim. Program.cs dosyasının içeriğini aşağıdaki şekilde düzenleyelim.

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.FeatureManagement;

namespace FeatureManagement.Console
{
	class Program
	{
		static IConfiguration SetupConfiguration()
		{
			var builder = new ConfigurationBuilder()
					.SetBasePath(Directory.GetCurrentDirectory())
					.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);

			return builder.Build();
		}

		static IServiceCollection ConfigureServices()
		{
			var services = new ServiceCollection();
			var config = SetupConfiguration();

			services.AddSingleton(config);
			services.AddFeatureManagement(config.GetSection("Features"))
					.AddFeatureFilter<PushNotificationFilter>();

			return services;
		}

		static async Task Main(string[] args)
		{
			var services = ConfigureServices();
			var serviceProvider = services.BuildServiceProvider();
			var featureManager = serviceProvider.GetRequiredService<IFeatureManager>();

			var supportedContext = new PushNotificationContext { OsVersion = "14.5" };
			var unsupportedContext = new PushNotificationContext { OsVersion = "11.4" };

			if (await featureManager.IsEnabledAsync("SendPush", supportedContext))
			{
				SendPush();
			}

			if (!await featureManager.IsEnabledAsync("SendPush", unsupportedContext))
			{
				System.Console.WriteLine("Unsupported operating system.");
			}
		}

		static void SendPush()
		{
			System.Console.WriteLine("Push sent!");
		}
	}
}

ConfigureServices'ın altında AddFeatureFilter<PushNotificationFilter>() extension method'unu kullanarak FeatureManagement'a PushNotificationFilter sınıfını kullanmasını ilettik.

Birden fazla feature kullanılabilir. Bunun için zincir şeklinde AddFeatureFilter<TFilter>() metodunu çağırmanız yeterlidir.

Artık dışarıdan girdi olarak gelen değerleri PushNotificationContext sınıfının bir instance'ına atadık. featureManager.IsEnabledAsync metoduna ikinci parametre olarak context'i geçerek feature filter'ın dışarıdan gelen değeri almasını sağladık.

Uygulamamızı çalıştırdığımız zaman aşağıdaki gibi bir sonuç alacağız.

Görüldüğü üzere ilk context'te 14.5 değeri tanımlı olduğundan dolayı ekranda Push sent! ifadesi yazmaktadır. İkinci context'te ise 11.4 değeri yazdığı için feature koşulu gerçekleşmedi ve bu sebepten dolayı ekranda Unsupported operating system. ifadesi yazılmıştır.

Custom Feature Flag Provider Geliştirme

FeatureManagement kütüphanesi feature'ları varsayılan olarak appsettings.json dosyasından okuyup yönetmektedir. Ancak projelerimizin igtiyacı dahilinde feature'ların farklı ortamlarda yönetilmesi gerekebilir. Örneğin bu bilgiler Redis, Couchbase, MongoDb, SQL Server vb. ortamlarda saklanıp yönetilebilir. FeatureManagement kütüphanesi bu esnekliği bize IFeatureDefinitionProvider interface'i üzerinden bize sağlamaktadır.

Bu örneğimizde feature'ları Redis üzerinde yöneteceğiz. Docker üzerinde bir Redis container'ı ayağa kaldırmak için aşağıdaki komutu kullanabiliriz.

docker run -p 6379:6379 --name redis redis

Redis üzerinde feature bilgilerini Features key'inin altında aşağıdakine benzer bir formatta tutacağız.

{
 "SendEmail": true,
 "SendPush": false
}

Şimdi ise projemize custom feature flag provider'ını ekleyebiliriz. Bunun için projemize RedisFeatureDefinitionProvider.cs isminde bir dosya ekleyelim ve içeriğini aşağıdaki şekilde düzenleyelim.

using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.FeatureManagement;
using StackExchange.Redis;

namespace FeatureManagement.Console.CustomFeatureProvider
{
	public class RedisFeatureDefinitionProvider : IFeatureDefinitionProvider
	{
		public async IAsyncEnumerable<FeatureDefinition> GetAllFeatureDefinitionsAsync()
		{
			var features = await GetAllFeatures();

			foreach (var feature in features)
			{
				yield return GetFeatureDefinition(feature.Key, feature.Value);
			}
		}

		public async Task<FeatureDefinition> GetFeatureDefinitionAsync(string featureName)
		{
			var features = await GetAllFeatures();

			if (features.TryGetValue(featureName, out var isFeatureEnabled))
			{
				return GetFeatureDefinition(featureName, isFeatureEnabled);
			}

			return null;
		}

		private async Task<Dictionary<string, bool>> GetAllFeatures()
		{
			var redis = ConnectionMultiplexer.Connect("localhost");
			var db = redis.GetDatabase();
			var featuresString = await db.StringGetAsync("Features");
			var features = JsonSerializer.Deserialize<Dictionary<string, bool>>(featuresString);

			return features;
		}

		private FeatureDefinition GetFeatureDefinition(string featureName, bool isEnabled)
		{
			var featureDefinition = new FeatureDefinition
			{
				Name = featureName
			};

			if (isEnabled)
				featureDefinition.EnabledFor = new[]
				{
					new FeatureFilterConfiguration
					{
						Name = "AlwaysOn"
					}
				};

			return featureDefinition;
		}
	}
}

FeatureManagement, IsEnabledAsync metodunu çağırdığında eklemiş olduğunuz provider'daki GetFeatureDefinitionAsync metodu tetiklenecektir. Bu metot tetiklendiğinde Redis'ten feature bilgilerini alıyoruz ve bir Dictionary'nin içerisini dolduğuruyoruz. GetFeatureDefinitionAsync metoduna parametre olarak gelen feature ismiyle eşleşen bir kayıt varsa bu kaydın bilgilerini GetFeatureDefinition metoduna iletiyoruz. Eğer feature aktif ise FeatureDefinition'ın EnabledFor property'sinde AlwaysOn değerini dönerek FeatureManagement'a ilgili feature'ın aktif olduğunu bildirmiş oluyoruz.

Son olarak yaptığımız geliştirmeyi test edebiliriz. Bunun için Program.cs dosyasının içeriğini aşağıdaki şekilde düzenleyelim.

using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.FeatureManagement;

namespace FeatureManagement.Console.CustomFeatureProvider
{
	class Program
	{
		static IServiceCollection ConfigureServices()
		{
			var services = new ServiceCollection();

			services
				.AddScoped<IFeatureDefinitionProvider, RedisFeatureDefinitionProvider>()
				.AddFeatureManagement();

			return services;
		}

		static async Task Main(string[] args)
		{
			var services = ConfigureServices();
			var serviceProvider = services.BuildServiceProvider();
			var featureManager = serviceProvider.GetRequiredService<IFeatureManager>();

			if (await featureManager.IsEnabledAsync("SendEmail"))
			{
				System.Console.WriteLine("Email service active!");
			}
			else
			{
				System.Console.WriteLine("Email service passive!");
			}

			if (await featureManager.IsEnabledAsync("SendPush"))
			{
				System.Console.WriteLine("Push service active!");
			}
			else
			{
				System.Console.WriteLine("Push service passive!");
			}
		}
	}
}

Eklemiş olduğumuz provider'ı scoped olarak AddScoped<IFeatureDefinitionProvider, RedisFeatureDefinitionProvider>() kayıt ederek FeatureManagement'ın ilgili bilgilere erişirken artık bizim yeni eklediğimiz provider'ı kullanmasını sağladık.

Uygulamamızı çalıştırdığımız zaman aşağıdaki gibi bir sonuçla karşılaşacağız.

Yazıda kullanılan örnek projeye https://github.com/mennan/feature-management-sample adresinden erişebilirsiniz.

Kaynaklar