ASP.NET Core’da Yapılandırma Doğrulama İşlemleri (Configuration Validation)

Okan Çilingiroğlu
HAVELSAN
Published in
3 min readMar 2, 2023

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, TestOptionssı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 Ç.

--

--

Okan Çilingiroğlu
HAVELSAN

.Net Developer, Team/Tech Leader and a bit more...