ASP.NET Core’da Yapılandırma Doğrulama İşlemleri (Configuration Validation)
Yapılandırma (Configuration), .NET Framework ile karşılaştırıldığında, .NET Core’da büyük ölçüde geliştirilmiştir. Bu yazıda, kötü yapılandırılmış ya da yapılandırmanın yanlış kullanılma olasılığı yüksek uygulamalar için, mümkün olan en kısa sürede ve mümkün olan en basit şekilde bir doğrulama (Validation) yapısı kurarak, yazılımcıya ve konfigurasyon yöneticisine yardımcı olabilecek bir yapı anlatıyor olacağım. Anlatacağım yöntem ile, uygulama yapılandırmasının iyi tanımlanmış bir durumda olması sağlanmış olacaktır.
Standart Bir Web Projesinde Hazır Gelen Yapı
Bildiğinizi gibi yeni bir ASP.NET Core Web projesi açtığınızda, Solution yapınız içerisinde sizi bir appsettings.json
dosyası karşılar. Bu dosya, projeniz içerisindeki yapılandırma ihtiyaçlarınızı karşılayacak tüm girdileri barındırır. Veritabanı erişim bilgilerinden, kullanacağınız ek yazılım parçacıklarının yapılandırma bilgilerine kadar.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
Şimdi, bu konfigürasyon için güçlü türlü (strong-typed) olarak yazılmış bir tanımı temsil edecek aşağıdaki gibi bir sınıf (class) ihtiyacınız olduğunu düşünün.
public class TestOptions
{
public const string SectionName = "Test";
public string LogLevel { get; init; }
public int Retries { get; init; }
}
LogLevel sınıf özelliğinin (class property) Enum tabanlı bir değişken olduğunu, Retries sınıf özelliğinin de int Min-Max değerleri arasında herhangi bir değer alabileceğini farzedelim.
Bu noktada, TestOptions
sınıfımızı, program.cs
içerisinde aşağıdaki satır ile inject ederek kullanabiliriz.
builder.Services
.AddOptions<TestOptions>()
.Bind(config.GetSection(TestOptions.SectionName));
Artık, appsettings.json dosyasınıda aşağıdaki şekilde kullanabiliriz.
{
"Test": {
"LogLevel": "HelloWorld",
"Retries": -5
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
Bu noktada, farzedelim ki LogLevel, Microsoft.Extensions.Logging.LogLevel
sınıfı ile örtüşmeli ve Retries değeri de 1 ila 9 arası bir değer almalı.
Burada iki tür ilerleme yönetmi var. İlki System.ComponentModel.DataAnnotations
kullanarak, TestOptions.cs
içerisindeki tüm değişkenleri bu DataAnnotations ile yönetmek. Fakat bu yöntemde, DataAnnotations'un çeşitli sınırları ile limitlenmiş olur ve daha kompleks kontroller yapmak istediğimizde sıkıntı yaşayabiliriz.
Eğer bu kontrolleri sağlıklı bir şekilde gerçekleştirmezsek, TestOptions
sınıfını kullanmayı düşündüğümüz pek çok noktada beklenmedik hatalar ve akış sorunları yaşamamız kaçınılmazdır. Bu sınıfı kullanacağımız her nokta da tek tek bu değerlerin kontrolünü gerçekleştirmek yerine, aşağıda anlatacağım yapı ile bu işi otomatize etmek ve ilerleyen zamanlarda yanlış değerler ile oluşturulmuş bir yapılandırma dosyasından kaynaklı sorunlar ile uğraşmak zorunda kalmayacaksınız.
Çözümün ilk parçası, FluentValidation
ve FluentValidation.DependencyInjectionExtensions
nuget kütüphanelerini projenize eklemek. Bu yazının yazıldığı anda, ben iki kütüphane için de 11.5.0 versiyonunu kullanmaktayım.
Ardından yeni bir sınıf açıp, aşağıdaki Validator
yapısını kurmak.
public class TestOptionsValidator : AbstractValidator<TestOptions>
{
public TestOptionsValidator()
{
RuleFor(x => x.LogLevel).IsEnumName(typeof(LogLevel));
RuleFor(x => x.Retries).InclusiveBetween(1, 9);
}
}
Ardından Program.cs
içerisinde, TestOptionsValidator
’a benzer şekilde tüm Validator’lerimizi ortak ve otomatik çağırabilmesi için bir Extension yazmamız gerekmektedir.
public static class OptionsBuilderFluentValidationExtensions
{
public static OptionsBuilder<TOptions> ValidateFluently<TOptions>(this OptionsBuilder<TOptions> optionsBuilder) where TOptions : class
{
optionsBuilder.Services.AddSingleton<IValidateOptions<TOptions>>(
s => new FluentValidationOptions<TOptions>(optionsBuilder.Name, s.GetRequiredService<IValidator<TOptions>>()));
return optionsBuilder;
}
}
Bu yapıya uygun bir FluentValidationOptions
yapısı da aşağıdaki gibi olmalıdır.
public class FluentValidationOptions<TOptions> : IValidateOptions<TOptions> where TOptions : class
{
private readonly IValidator<TOptions> _validator;
public string? Name { get; }
public FluentValidationOptions(string? name, IValidator<TOptions> validator)
{
Name = name;
_validator = validator;
}
public ValidateOptionsResult Validate(string? name, TOptions options)
{
if (Name != null && Name != name)
{
return ValidateOptionsResult.Skip;
}
ArgumentNullException.ThrowIfNull(options);
var validationResult = _validator.Validate(options);
if (validationResult.IsValid)
{
return ValidateOptionsResult.Success;
}
var errors = validationResult.Errors.Select(e => $"Options validation failed for '{e.PropertyName}' with error: '{e.ErrorMessage}'.");
return ValidateOptionsResult.Fail(errors);
}
}
Son olarak, Program.cs
altında aşağıdaki yapı ile tüm yapılandırma değişkenlerimizi doğrulayabileceğiz ve uygun noktada hata alarak (hata dönüş şekillerinizi ihtiyaçlarınız doğrultusunda düzenleyebilirsiniz tabiki) ileride çıkabilecek daha kritik sorunları bertaraf etmiş olacağız.
var builder = WebApplication.CreateBuilder(args);
var config = builder.Configuration;
builder.Services.AddValidatorsFromAssemblyContaining<Program>(ServiceLifetime.Singleton);
builder.Services
.AddOptions<TestOptions>()
.Bind(config.GetSection(TestOptions.SectionName))
.ValidateFluently();
var app = builder.Build();
app.Run();
İlgili kod için GitHub link: https://github.com/unit399/AppSettingsValidator
Keyifli çalışmalar dilerim.
Okan Ç.