در برنامههای ASP.NET Core، امکان دریافت تنظیمات برنامه از منابع مختلفی مانند فایلهای JSON وجود دارد که در نگارشهای اخیر آن، امکان اعتبارسنجی اطلاعات آنها به صورت توکار نیز اضافه شدهاست؛ مانند:
services.AddOptions<BearerTokensOptions>()
.Bind(configuration.GetSection("BearerTokens"))
.Validate(bearerTokens =>
{
return bearerTokens.AccessTokenExpirationMinutes < bearerTokens.RefreshTokenExpirationMinutes;
}, "RefreshTokenExpirationMinutes is less than AccessTokenExpirationMinutes. Obtaining new tokens using the refresh token should happen only if the access token has expired.");
اما این امکان در مقایسه با امکاناتی که FluentValidation در اختیار ما قرار میدهد، بسیار ابتدایی به نظر میرسد. به همین جهت در این قسمت قصد داریم امکانات اعتبارسنجی کتابخانهی FluentValidation را در حین آغاز برنامه، جهت تعیین اعتبار اطلاعات فایل کانفیگ آن، مورد استفاده قرار دهیم.
معرفی تنظیمات برنامه
فرض کنید فایل appsettings.json برنامه یک چنین محتوایی را دارد:
{
"ApiSettings": {
"AllowedEndpoints": [
{
"Name": "Service 1",
"Timeout": 30,
"Url": "http://service1.site.com"
},
{
"Name": "Service 2",
"Timeout": 10,
"Url": "https://service2.site.com"
}
]
}
}
ایجاد مدلهای معادل تنظیمات JSON برنامه
بر اساس تعاریف JSON فوق، میتوان به مدلهای زیر رسید:
using System;
using System.Collections.Generic;
namespace FluentValidationSample.Models
{
public class AllowedEndpoint
{
public string Name { get; set; }
public int Timeout { get; set; }
public Uri Url { get; set; }
}
public class ApiSettings
{
public IEnumerable<AllowedEndpoint> AllowedEndpoints { get; set; }
}
}
که نحوهی معرفی آن به سیستم تزریق وابستگیهای برنامه به صورت زیر است:
namespace FluentValidationSample.Web
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.Configure<ApiSettings>(Configuration.GetSection(nameof(ApiSettings)));
و پس از آن در هر قسمتی از برنامه با تزریق <IOptions<ApiSettings میتوان به اطلاعات تنظیمات برنامه دسترسی یافت.
تعریف شرطهای اعتبارسنجی مدلهای تنظیمات برنامه
پس از مدلسازی تنظیمات برنامه و همچنین اتصال آن به <IOptions<ApiSettings، اکنون میخواهیم این مدلها، شرایط زیر را برآورده کنند:
- باید مدخل ApiSettings در فایل تنظیمات برنامه وجود خارجی داشته باشد.
- میخواهیم AllowedEndpointها نامدار بوده و هر نام نیز منحصربفرد باشد.
- مقادیر timeoutها باید بین 1 و 90 تعریف شده باشند.
- تمام URLها باید منحصربفرد باشند.
- تمام URLها باید HTTPS باشند.
برای این منظور میتوان تنظیمات زیر را توسط Fluent Validation تعریف کرد:
using System;
using System.Linq;
using FluentValidation;
using FluentValidationSample.Models;
namespace FluentValidationSample.ModelsValidations
{
public class ApiSettingsValidator : AbstractValidator<ApiSettings>
{
public ApiSettingsValidator()
{
RuleFor(apiSetting => apiSetting).NotNull()
.WithMessage("مدخل ApiSettings تعریف نشدهاست.");
RuleFor(apiSetting => apiSetting.AllowedEndpoints).NotNull().NotEmpty()
.WithMessage("مدخل AllowedEndpoints تعریف نشدهاست.");
When(apiSetting => apiSetting.AllowedEndpoints != null,
() =>
{
RuleFor(apiSetting => apiSetting.AllowedEndpoints)
.Must(endpoints => endpoints.GroupBy(endpoint => endpoint.Name).Count() == endpoints.Count())
.WithMessage("نامهای سرویسها باید منحصربفرد باشند.");
RuleFor(apiSetting => apiSetting.AllowedEndpoints)
.Must(endpoints => !endpoints.Any(endpoint => endpoint.Timeout > 90 || endpoint.Timeout < 1))
.WithMessage("مقدار timeout باید بین 1 و 90 باشد");
RuleFor(apiSetting => apiSetting.AllowedEndpoints)
.Must(endpoints => endpoints.GroupBy(endpoint => endpoint.Url.ToString().ToLower()).Count() == endpoints.Count())
.WithMessage("آدرسهای سرویسها باید منحصربفرد باشند.");
RuleFor(apiSetting => apiSetting.AllowedEndpoints)
.Must(endpoints => endpoints.All(endpoint => endpoint.Url.Scheme.Equals("https", StringComparison.CurrentCultureIgnoreCase)))
.WithMessage("تمام آدرسها باید HTTPS باشند.");
});
}
}
}
که در اینجا نکات زیر قابل ملاحظه هستند:
- چگونه میتوان از تعریف و وجود یک مدخل فایل JSON، اطمینان حاصل کرد (اعمال RuleFor به کل مدل).
- چگونه میتوان اگر مدخلی تعریف شده بود، آنگاه برای آن اعتبارسنجی خاصی را تعریف کرد (متد When).
- چگونه میتوان شرایط سفارشی خاصی را مانند بررسی منحصربفرد بودنها، بررسی کرد (متد Must).
یکپارچه کردن اعتبارسنجی کتابخانهی FluentValidation با اعتبارسنجی توکار مدلهای تنظیمات برنامه توسط ASP.NET Core
در ابتدای بحث، امکان تعریف متد Validate را که از نگارش ASP.NET Core 2.2 اضافه شدهاست، مشاهده کردید:
services.AddOptions<BearerTokensOptions>()
.Bind(configuration.GetSection("BearerTokens"))
.Validate(bearerTokens =>
{
return bearerTokens.AccessTokenExpirationMinutes < bearerTokens.RefreshTokenExpirationMinutes;
}, "RefreshTokenExpirationMinutes is less than AccessTokenExpirationMinutes. Obtaining new tokens using the refresh token should happen only if the access token has expired.");
میتوان این متد را با پیاده سازی اینترفیس توکار IValidateOptions نیز به سیستم ارائه داد:
namespace Microsoft.Extensions.Options
{
public interface IValidateOptions<TOptions> where TOptions : class
{
ValidateOptionsResult Validate(string name, TOptions options);
}
}
و اگر سرویس پیاده سازی کنندهی آنرا با طول عمر Transient به سیستم اضافه کردیم، به صورت خودکار جهت اعتبارسنجی TOptions، مورد استفاده قرار خواهد گرفت. TOptions در این مثال همان ApiSettings است.
در ادامه یک نمونه پیاده سازی جنریک IValidateOptions استاندارد ASP.NET Core را مشاهده میکنید:
using System.Linq;
using FluentValidation;
using Microsoft.Extensions.Options;
namespace FluentValidationSample.ModelsValidations
{
public class AppConfigValidator<TOptions> : IValidateOptions<TOptions> where TOptions : class
{
private readonly IValidator<TOptions> _validator;
public AppConfigValidator(IValidator<TOptions> validator)
{
_validator = validator;
}
public ValidateOptionsResult Validate(string name, TOptions options)
{
if (options is null)
{
return ValidateOptionsResult.Fail("Configuration object is null.");
}
var validationResult = _validator.Validate(options);
return validationResult.IsValid
? ValidateOptionsResult.Success
: ValidateOptionsResult.Fail(validationResult.Errors.Select(error => error.ToString()));
}
}
}
همانطور که در
قسمت دوم این سری این نیز بررسی کردیم، یکی از روشهای اجرای اعتبارسنجیهای FluentValidation، کار با اینترفیس IValidator آن است که در اینجا به سازندهی این کلاس تزریق شدهاست. سپس در متد Validate این سرویس، با فراخوانی آن، کار اعتبارسنجی وهلهی دریافتی options صورت گرفته و اگر خطایی وجود داشته باشد، بازگشت داده میشود.
در آخر روش معرفی آن به سیستم به صورت زیر است:
namespace FluentValidationSample.Web
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.Configure<ApiSettings>(Configuration.GetSection(nameof(ApiSettings)));
services.AddTransient<IValidateOptions<ApiSettings>, AppConfigValidator<ApiSettings>>();
به این ترتیب هرگاه در برنامه یک چنین تعریفی را داشته باشیم که از طریق IOptions، تنظیمات برنامه را دریافت میکند:
namespace FluentValidationSample.Web.Controllers
{
public class HomeController : Controller
{
private readonly IUsersService _usersService;
private readonly ApiSettings _apiSettings;
public HomeController(IUsersService usersService, IOptions<ApiSettings> apiSettings)
{
_usersService = usersService;
_apiSettings = apiSettings.Value;
}
اگر در سیستم یک <IValidateOptions<ApiSettings متناظر با <IOptions<ApiSettings ثبت شده باشد (مانند تنظیمات متد ConfigureServices فوق)، هرگاه که فراخوانی apiSettings.Value صورت گیرد، قبل از هرکاری متد Validate سرویس پیاده سازی کنندهی IValidateOptions متناظر، فراخوانی شده و اگر خطای اعتبارسنجی وجود داشته باشد، به صورت یک استثناء بازگشت داده میشود؛ مانند:
An unhandled exception occurred while processing the request.
OptionsValidationException: تمام آدرسها باید HTTPS باشند.
کدهای کامل این سری را تا این قسمت از اینجا میتوانید دریافت کنید: FluentValidationSample-part05.zip