
.Net’te Option Pattern ve Validation
Modern .NET uygulamalarında, yapılandırma ayarlarını yönetmek önemli bir gerekliliktir. Geleneksel olarak, bu ayarlar genellikle appsettings.json
, ortam değişkenleri veya komut satırı argümanları gibi farklı kaynaklardan okunur ve doğrudan kullanılabilirdi. Ancak, bu yöntemler uygulama bakımını zorlaştırabilir ve yapılandırma hatalarına karşı savunmasız hale getirebilir.
Options Pattern, .NET Core ve .NET 5+ sürümlerinde, yapılandırma yönetimini daha düzenli ve güvenli hale getiren bir tasarım desenidir. Bu desen sayesinde yapılandırma ayarları, POCO (Plain Old CLR Object) sınıflarına bağlanarak güçlü bir tip güvenliği sağlanır. Böylece, uygulama başlatılırken yapılandırma hataları erken tespit edilebilir ve geliştirme süreci daha güvenilir hale gelir.
Neden Kullanılır?
- Daha Modüler ve Yönetilebilir Kod: Options Pattern, yapılandırma ayarlarını merkezi ve modüler bir yapıya kavuşturarak kodun daha okunabilir ve sürdürülebilir olmasını sağlar.
- Dependency Injection ile Entegrasyon:
IOptions<T>
,IOptionsSnapshot<T>
veIOptionsMonitor<T>
gibi arayüzler sayesinde yapılandırma ayarları Dependency Injection ile yönetilebilir. - Runtime Güncellenebilirlik:
IOptionsMonitor<T>
ile çalışma zamanında yapılandırma ayarları dinamik olarak değiştirilebilir. - Tip Güvenliği: JSON veya başka bir kaynaktan alınan yapılandırmalar, tip dönüşüm hatalarından kaçınarak doğrudan nesnelere bağlanır.
Yapılandırma ayarlarının yönetimi kadar, bunların doğruluğunu sağlamak da kritik bir konudur. Yanlış veya eksik ayarlar uygulamanın beklenmedik şekilde çalışmasına veya hata vermesine neden olabilir. Bu nedenle, Options Pattern ile birlikte doğrulama (validation) mekanizmalarının kullanılması, hataları önceden yakalamak ve güvenilir bir sistem oluşturmak için önemlidir.
Yazı boyunca kullanacağım örnek ayarlar aşağıdaki gibi olacaktır.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"AppSettings": {
"ApplicationName": "MyApp",
"MaxUsers": 500,
"Url": "https://github.com"
}
}
En basit haliyle ayarların doğruluğunu sağlamak için hızlıca kodunuza aşağıdaki gibi basit koşullar ekleyebilirsiniz.
var applicationName = builder.Configuration.GetValue<string>("AppSettings:ApplicationName");
if (string.IsNullOrEmpty(applicationName))
{
throw new InvalidOperationException("ApplicationName is required");
}
veya
var applicationName = builder.Configuration.GetValue<string>("AppSettings:ApplicationName");
ArgumentException.ThrowIfNullOrEmpty(applicationName);
Tek bir değerin doğruluğunu sağlamak için en basit yöntem elbette hızlıca bir if eklemek veya .Net sağladığı built-in kaynakları kullanmak olacaktır ancak gerçek dünya senaryolarında uygulamalarımız tek bir configuration object veya value içermez. Uygulamalarımız geliştikçe configuration dosyamız kullandığımız teknolojiler veya iletişim kurulan servislerin bilgileri ile dolup taşar. Yukarıdaki bu yöntem basittir ancak gerçek dünya senaryolarında kullanışlı değildir. Bunun yerine daha okunaklı ve yönetilebilir bir validation yöntemine ihtiyaç duyarız.
Data Annotations
Data Annotations kullanmak bunu yapmanın basit bir yoludur.
Doğrulanmasını istediğimiz özellikleri işaretlemek için öznitelik(attribute) kullanırız.
public class AppSettings
{
[Required(ErrorMessage = "Application Name is required")]
public string ApplicationName { get; set; }
[Range(1, 100, ErrorMessage = "Max Users must be between 1 and 100")]
public int MaxUsers { get; set; }
[Required(ErrorMessage = "Url is required"), Url(ErrorMessage = "The url format is incorrect")]
public string Url { get; set; }
}
Ayarlamalarınızı doğrulamak için artık tek yapmanız gereken ValidateDataAnnotation
extension metodunu kullanmaktır.
builder.Services.AddOptions<AppSettings>()
.Bind(builder.Configuration.GetSection("AppSettings"))
.ValidateDataAnnotations()
.ValidateOnStart();
ValidateOnStart
ile uygulama başladığında ilgili yapılandırma için validation uygulanır ve eğer eksik veya hatalı bir durum varsa hata verilir.
fail: Microsoft.Extensions.Hosting.Internal.Host[11]
Hosting failed to start
Microsoft.Extensions.Options.OptionsValidationException:
DataAnnotation validation failed for 'AppSettings' members: 'MaxUsers'
with the error: 'Max Users must be between 1 and 100'.
Fluent Validation
FluentValidation
.Net ekosisteminde oldukça yaygın kullanılan bir kütüphanedir. Option Pattern Validation
için FluentValidation
kullanmak isteyebilirsiniz. (Ben istedim 🥳) FluentValidation
kullanımını kolaylaştırmak için OptionValidationSharp
isimli bir kütüphane oluşturdum.
Bu kütüphane FluentValidation
ile birlikte çalışır ve yazdığınız validation kurallarını Option Validation
için kullanmanızı sağlar.
Kütüphaneyi NuGet Package Manager üzerinden indirmek için:
dotnet add package OptionValidationSharp --version 1.0.0
Örneğin;
public class AppSettingsValidator : AbstractValidator<AppSettings>
{
public AppSettingsValidator()
{
RuleFor(r => r.ApplicationName).NotEmpty();
RuleFor(r => r.MaxUsers).InclusiveBetween(1,100);
RuleFor(r => r.Url).NotEmpty();
RuleFor(r => r.Url)
.Must(url => Uri.TryCreate(url, UriKind.Absolute, out _))
.When(r => !string.IsNullOrWhiteSpace(r.Url));
}
}
Artık her şey hazır. Tek yapmak gereken ValidateOptionSharp
isimli methodu kullanmak.
builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);
builder.Services.AddOptions<AppSettings>()
.Bind(builder.Configuration.GetSection("AppSettings"))
.ValidateOptionSharp() // <- 🔨 Validation Option with Fluent Validation
.ValidateOnStart();
Option Validation Sharp
Bu kütüphaneden biraz daha bahsetmek istiyorum. Aslında oldukça küçük ancak kullanışlı bir kütüphane hazırladığımı düşünüyorum. Kütüphane FluentValidation
bağımlılığı taşır. Option Validation
için yazdığınız kuralları Start
esnasında kullanabilmeniz için küçük bir extension method sağlar.
ValidateOptionSharp
arkasında OptionValidate
isimli bir sınıfı Singleton
olarak ekler.
using FluentValidation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace OptionValidationSharp;
/// <summary>
/// Option Validate extension
/// </summary>
/// <typeparam name="TOptions">
/// The type of options being validated.
/// </typeparam>
public class OptionValidate<TOptions>(IServiceProvider serviceProvider, string? optionName) : IValidateOptions<TOptions>
where TOptions : class
{
/// <summary>
/// Validate the options.
/// </summary>
/// <param name="name">
/// The name of the options instance being validated, if any.
/// </param>
/// <param name="options">
/// The options instance to validate.
/// </param>
/// <returns>
/// The <see cref="ValidateOptionsResult" />.
/// </returns>
public ValidateOptionsResult Validate(string? name, TOptions options)
{
if (optionName is not null && optionName != name)
{
return ValidateOptionsResult.Skip;
}
ArgumentNullException.ThrowIfNull(options);
using var scope = serviceProvider.CreateScope();
var validator = scope.ServiceProvider.GetRequiredService<IValidator<TOptions>>();
var result = validator.Validate(options);
if (result.IsValid)
{
return ValidateOptionsResult.Success;
}
var type = options.GetType().Name;
var errors = string.Join(", ", result.Errors.Select(x => x.ErrorMessage));
var message = $"Options validation failed for type '{type}'. {errors}";
return ValidateOptionsResult.Fail(message);
}
}
FluentValidation
ile yazdığınız kurallar IValidator<T>
olarak çağırılıp kullanılabilir. OptionValidationSharp
tam olarak bu özelliği kullanır. Oluşturduğunuz kurallı yukarıda belirtildiği gibi çeker ve objeniz üzerinde uygular. Eğer bir hata varsa bunu geri döndürür.
fail: Microsoft.Extensions.Hosting.Internal.Host[11]
Hosting failed to start
Microsoft.Extensions.Options.OptionsValidationException:
Options validation failed for type 'AppSettings'.
'Max Users' must be between 1 and 100. You entered 500.
Modern uygulamalarda Option Pattern
oldukça önemli bir rol oynar. Bu kadar önem kazanan bir noktada validation
da oldukça önemlidir.
Umarım faydalı olur.