نظرات مطالب
طریقه بررسی صحت کدملی به کمک متدهای الحاقی
سلام.
بله دو دستور فوق معادل یکدیگرند. من قبل از انتشار نسخه نهایی با مقادیر r و div کارهای دیگری انجام داده بودم که با توجه به حذف آن موارد می‌توان باقیمانده تقسیم sum بر 11 را تنها نگه داشت.
مطالب
بررسی ساختارهای جدید DateOnly و TimeOnly در دات نت 6
به همراه دات نت 6، دو ساختار داده‌ی جدید DateOnly و TimeOnly نیز معرفی شده‌اند که امکان کار کردن ساده‌تر با قسمت‌های فقط تاریخ و یا فقط زمان DateTime را میسر می‌کنند. این دو نوع جدید نیز همانند DateTime، از نوع struct هستند و بنابراین value type محسوب می‌شوند. در فضای نام System قرار گرفته‌اند و همچنین با نوع‌های date و time مربوط به SQL Server، سازگاری کاملی دارند.


روش استفاده از نوع DateOnly در دات نت 6

نوع‌های جدید معرفی شده، بسیار واضح هستند و مقصود از بکارگیری آن‌ها را به خوبی بیان می‌کنند. برای مثال اگر نیاز بود تاریخی را بدون در نظر گرفتن قسمت زمان آن معرفی کنیم، می‌توان از نوع DateOnly استفاده کرد؛ مانند تاریخ تولد، روزهای کاری و امثال آن. تا پیش از این برای معرفی یک چنین تاریخ‌هایی، عموما قسمت زمان DateTime را با 00:00:00.000 مقدار دهی می‌کردیم؛ اما دیگر نیازی به این نوع تعاریف نیست و می‌توان مقصود خود را صریح‌تر بیان کرد.
روش معرفی نمونه‌ای از آن با معرفی سال، ماه و روز است:
 var date = new DateOnly(2020, 04, 20);
و یا اگر خواستیم یک DateTime موجود را به DateOnly تبدیل کنیم، می‌توان به صورت زیر عمل کرد:
 var currentDate = DateOnly.FromDateTime(DateTime.Now);

همچنین در اینجا نیز همانند DateTime می‌توان از متدهای Parse و یا TryParse، برای تبدیل یک رشته به معادل DateOnly آن، کمک گرفت:
if (DateOnly.TryParse("28/09/1984", new CultureInfo("en-US"), DateTimeStyles.None, out var result))
{
   Console.WriteLine(result);
}
در یک چنین حالتی ذکر CultureInfo، دقت کار را افزایش می‌دهد؛ در غیراینصورت از CultureInfo ترد جاری برنامه استفاده خواهد شد که می‌تواند در سیستم‌های مختلف، متفاوت باشد.

و یا می‌توان توسط متد ParseExact، ساختار تاریخ دریافتی را دقیقا مشخص کرد:
DateOnly d1 = DateOnly.ParseExact("31 Dec 1980", "dd MMM yyyy", CultureInfo.InvariantCulture);  // Custom format
Console.WriteLine(d1.ToString("o", CultureInfo.InvariantCulture)); // "1980-12-31"  (ISO 8601 format)

در حین نمونه سازی DateOnly، امکان ذکر تقویم‌های خاص، مانند PersianCalendar نیز وجود دارد:
var persianCalendar = new PersianCalendar();
DateOnly d2 = new DateOnly(1400, 9, 6, persianCalendar);
Console.WriteLine(d2.ToString("d MMMM yyyy", CultureInfo.InvariantCulture));

در اینجا همچنین متدهایی مانند AddDays، AddMonths و AddYears نیز بر روی date مهیا کار می‌کنند:
var newDate = date.AddDays(1).AddMonths(1).AddYears(1)

یک نکته: برخلاف DateTime، نوع DateOnly به همراه DateTimeKind مانند Utc و امثال آن نیست و همواره DateTimeKind آن Unspecified است.


روش استفاده از نوع TimeOnly در دات نت 6

نوع و ساختار TimeOnly، قسمت زمان را به نحو صریحی مشخص می‌کند؛ مانند ساعتی که باید هر روز راس آن، آلارمی به صدا درآید و یا جلسه‌ای تشکیل شود و یا وظیفه‌ای صورت گیرد. سازنده‌ی آن overload‌های قابل توجهی را داشته و می‌تواند یکی از موارد زیر باشد:
public TimeOnly(int hour, int minute)
public TimeOnly(int hour, int minute, int second)
public TimeOnly(int hour, int minute, int second, int millisecond)
برای نمونه برای نمایش 10:30 صبح، می‌توان به صورت زیر عمل کرد:
var startTime = new TimeOnly(10, 30);
در اینجا قسمت ساعت، 24 ساعتی تعریف شده‌است. بنابراین برای نمونه، ساعت 1 عصر را باید به صورت 13 قید کرد:
var endTime = new TimeOnly(13, 00, 00);

و یا برای مثال می‌توان این نمونه‌ها را از هم کم کرد:
var diff = endTime - startTime;
خروجی این تفاوت محاسبه شده، بر حسب TimeSpan است:
Console.WriteLine($"Hours: {diff.TotalHours}");
و یا با استفاده از متد الحاقی ToTimeSpan می‌توان یک TimeOnly را به TimeSpan معادلی تبدیل نمود:
TimeSpan ts = endTime.ToTimeSpan();

برای تبدیل قسمت زمان DateTime به TimeOnly، می‌توان از متد FromDateTime به صورت زیر استفاده کرد:
var currentTime = TimeOnly.FromDateTime(DateTime.Now);
و یا اگر بخواهیم یک DateOnly را به DateTime تبدیل کنیم، می‌توان از متد الحاقی ToDateTime به همراه ذکر قسمت زمان آن بر حسب TimeOnly کمک گرفت:
DateTime dt = date.ToDateTime(new TimeOnly(0, 0));
Console.WriteLine(dt);

و در این حالت اگر خواستیم بررسی کنیم که آیا زمانی بین دو زمان دیگر واقع شده‌است یا خیر، می‌توان از متد IsBetween استفاده نمود:
 var isBetween = currentTime.IsBetween(startTime, endTime);
Console.WriteLine($"Current time {(isBetween ? "is" : "is not")} between start and end");

در اینجا امکان مقایسه این نمونه‌ها، توسط عملگرهایی مانند < نیز وجود دارد:
var startTime = new TimeOnly(08, 00);
var endTime = new TimeOnly(09, 00);
 
Console.WriteLine($"{startTime < endTime}");

اگر نیاز به تبدیل رشته‌ای به TimeOnly بود، می‌توان از متد ParseExact به همراه ذکر ساختار مدنظر، استفاده کرد:
TimeOnly time = TimeOnly.ParseExact("5:00 pm", "h:mm tt", CultureInfo.InvariantCulture);  // Custom format
Console.WriteLine(time.ToString("T", CultureInfo.InvariantCulture)); // "17:00:00"  (long time format)


عدم پشتیبانی System.Text.Json از نوع‌های جدید DateOnly و TimeOnly

فرض کنید رکوردی را به صورت زیر تعریف کرده‌ایم که از نوع‌های جدید DateOnly و TimeOnly، تشکیل شده‌است:
public record DataTypeTest(DateOnly Date, TimeOnly Time);
اگر سعی کنیم نمونه‌ای از آن را به JSON تبدیل کنیم:
var date = DateOnly.FromDateTime(DateTime.Now);
var time = TimeOnly.FromDateTime(DateTime.Now);
var test = new DataTypeTest(date, time);
var json = JsonSerializer.Serialize(test);
با استثنای زیر مواجه خواهیم شد:
Serialization and deserialization of 'System.DateOnly' instances are not supported.

برای رفع این مشکل می‌توان ابتدا تبدیلگر ویژه‌ی DateOnly و
    public class DateOnlyConverter : JsonConverter<DateOnly>
    {
        private readonly string _serializationFormat;

        public DateOnlyConverter() : this(null)
        { }

        public DateOnlyConverter(string? serializationFormat)
        {
            _serializationFormat = serializationFormat ?? "yyyy-MM-dd";
        }

        public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            var value = reader.GetString();
            return DateOnly.ParseExact(value!, _serializationFormat, CultureInfo.InvariantCulture);
        }

        public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options)
            => writer.WriteStringValue(value.ToString(_serializationFormat));
    }
و سپس تبدیلگر ویژه‌ی TimeOnly را به صورت زیر تدارک دید:
    public class TimeOnlyConverter : JsonConverter<TimeOnly>
    {
        private readonly string _serializationFormat;

        public TimeOnlyConverter() : this(null)
        {
        }

        public TimeOnlyConverter(string? serializationFormat)
        {
            _serializationFormat = serializationFormat ?? "HH:mm:ss.fff";
        }

        public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            var value = reader.GetString();
            return TimeOnly.ParseExact(value!, _serializationFormat, CultureInfo.InvariantCulture);
        }

        public override void Write(Utf8JsonWriter writer, TimeOnly value, JsonSerializerOptions options)
            => writer.WriteStringValue(value.ToString(_serializationFormat));
    }
و به نحو زیر مورد استفاده قرار داد:
var jsonOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
jsonOptions.Converters.Add(new DateOnlyConverter());
jsonOptions.Converters.Add(new TimeOnlyConverter());
var json = JsonSerializer.Serialize(test, jsonOptions);
مطالب
سفارشی سازی ASP.NET Core Identity - قسمت پنجم - سیاست‌های دسترسی پویا
ASP.NET Core Identity به همراه دو قابلیت جدید است که پیاده سازی سطوح دسترسی پویا را با سهولت بیشتری میسر می‌کند:
الف) Policies
ب) Role Claims


سیاست‌های دسترسی یا Policies در ASP.NET Core Identity

ASP.NET Core Identity هنوز هم از مفهوم Roles پشتیبانی می‌کند. برای مثال می‌توان مشخص کرد که اکشن متدی و یا تمام اکشن متدهای یک کنترلر تنها توسط کاربران دارای نقش Admin قابل دسترسی باشند. اما نقش‌ها نیز در این سیستم جدید تنها نوعی از سیاست‌های دسترسی هستند.
[Authorize(Roles = ConstantRoles.Admin)]
public class RolesManagerController : Controller
برای مثال در اینجا دسترسی به امکانات مدیریت نقش‌های سیستم، به نقش ثابت و از پیش تعیین شده‌ی Admin منحصر شده‌است و تمام کاربرانی که این نقش به آن‌ها انتساب داده شود، امکان استفاده‌ی از این قابلیت‌ها را خواهند یافت.
اما نقش‌های ثابت، بسیار محدود و غیر قابل انعطاف هستند. برای رفع این مشکل مفهوم جدیدی را به نام Policy اضافه کرده‌اند.
[Authorize(Policy="RequireAdministratorRole")]
public IActionResult Get()
{
   /* .. */
}
سیاست‌های دسترسی بر اساس Requirements و یا نیازهای سیستم تعیین می‌شوند و تعیین نقش‌ها، تنها یکی از قابلیت‌های آن‌ها هستند.
برای مثال اگر بخواهیم تک نقش Admin را به صورت یک سیاست دسترسی جدید تعریف کنیم، روش کار به صورت ذیل خواهد بود:
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
services.AddAuthorization(options =>
    {
        options.AddPolicy("RequireAdministratorRole", policy => policy.RequireRole("Admin"));
    });
}
در تنظیمات متد AddAuthorization، یک سیاست دسترسی جدید تعریف شده‌است که جهت برآورده شدن نیازمندی‌های آن، کاربر سیستم باید دارای نقش Admin باشد که نمونه‌ای از نحوه‌ی استفاده‌ی از آن‌را با ذکر [Authorize(Policy="RequireAdministratorRole")] ملاحظه کردید.
و یا بجای اینکه چند نقش مجاز به دسترسی منبعی را با کاما از هم جدا کنیم:
 [Authorize(Roles = "Administrator, PowerUser, BackupAdministrator")]
می‌توان یک سیاست دسترسی جدید را به نحو ذیل تعریف کرد که شامل تمام نقش‌های مورد نیاز باشد و سپس بجای ذکر Roles، از نام این Policy جدید استفاده کرد:
options.AddPolicy("ElevatedRights", policy => policy.RequireRole("Administrator", "PowerUser", "BackupAdministrator"));
به این صورت
[Authorize(Policy = "ElevatedRights")]
public IActionResult Shutdown()
{
   return View();
}

سیاست‌های دسترسی تنها به نقش‌ها محدود نیستند:
services.AddAuthorization(options =>
{
   options.AddPolicy("EmployeeOnly", policy => policy.RequireClaim("EmployeeNumber"));
});
برای مثال می‌توان دسترسی به یک منبع را بر اساس User Claims یک کاربر به نحوی که ملاحظه می‌کنید، محدود کرد:
[Authorize(Policy = "EmployeeOnly")]
public IActionResult VacationBalance()
{
   return View();
}


سیاست‌های دسترسی پویا در ASP.NET Core Identity

مهم‌ترین مزیت کار با سیاست‌های دسترسی، امکان سفارشی سازی و تهیه‌ی نمونه‌های پویای آن‌ها هستند؛ موردی که با نقش‌های ثابت سیستم قابل پیاده سازی نبوده و در نگارش‌های قبلی، جهت پویا سازی آن، یکی از روش‌های بسیار متداول، تهیه‌ی فیلتر Authorize سفارشی سازی شده بود. اما در اینجا دیگر نیازی نیست تا فیلتر Authorize را سفارشی سازی کنیم. با پیاده سازی یک AuthorizationHandler جدید و معرفی آن به سیستم، پردازش سیاست‌های دسترسی پویای به منابع، فعال می‌شود.
پیاده سازی سیاست‌های پویای دسترسی شامل مراحل ذیل است:
1- تعریف یک نیازمندی دسترسی جدید
public class DynamicPermissionRequirement : IAuthorizationRequirement
{
}
ابتدا باید یک نیازمندی دسترسی جدید را با پیاده سازی اینترفیس IAuthorizationRequirement ارائه دهیم. این نیازمندی مانند روشی که در پروژه‌ی DNT Identity بکار گرفته شده‌است، خالی است و صرفا به عنوان نشانه‌ای جهت یافت AuthorizationHandler استفاده کننده‌ی از آن استفاده می‌شود. در اینجا در صورت نیاز می‌توان یک سری خاصیت اضافه را تعریف کرد تا آن‌ها را به صورت پارامترهایی ثابت به AuthorizationHandler ارسال کند.

2- پیاده سازی یک AuthorizationHandler استفاده کننده‌ی از نیازمندی دسترسی تعریف شده
پس از اینکه نیازمندی DynamicPermissionRequirement را تعریف کردیم، در ادامه باید یک AuthorizationHandler استفاده کننده‌ی از آن را تعریف کنیم:
    public class DynamicPermissionsAuthorizationHandler : AuthorizationHandler<DynamicPermissionRequirement>
    {
        private readonly ISecurityTrimmingService _securityTrimmingService;

        public DynamicPermissionsAuthorizationHandler(ISecurityTrimmingService securityTrimmingService)
        {
            _securityTrimmingService = securityTrimmingService;
            _securityTrimmingService.CheckArgumentIsNull(nameof(_securityTrimmingService));
        }

        protected override Task HandleRequirementAsync(
             AuthorizationHandlerContext context,
             DynamicPermissionRequirement requirement)
        {
            var mvcContext = context.Resource as AuthorizationFilterContext;
            if (mvcContext == null)
            {
                return Task.CompletedTask;
            }

            var actionDescriptor = mvcContext.ActionDescriptor;
            var area = actionDescriptor.RouteValues["area"];
            var controller = actionDescriptor.RouteValues["controller"];
            var action = actionDescriptor.RouteValues["action"];

            if(_securityTrimmingService.CanCurrentUserAccess(area, controller, action))
            {
                context.Succeed(requirement);
            }
            else
            {
                context.Fail();
            }

            return Task.CompletedTask;
        }
    }
کار با ارث بری از AuthorizationHandler شروع شده و آرگومان جنریک آن، همان نیازمندی است که پیشتر تعریف کردیم. از این آرگومان جنریک جهت یافتن خودکار AuthorizationHandler متناظر با آن، توسط ASP.NET Core Identity استفاده می‌شود. بنابراین در اینجا DynamicPermissionRequirement تهیه شده صرفا کارکرد علامتگذاری را دارد.
در کلاس تهیه شده باید متد HandleRequirementAsync آن‌را بازنویسی کرد و اگر در این بین، منطق سفارشی ما context.Succeed را فراخوانی کند، به معنای برآورده شدن سیاست دسترسی بوده و کاربر جاری می‌تواند به منبع درخواستی، بلافاصله دسترسی یابد و اگر context.Fail فراخوانی شود، در همینجا دسترسی کاربر قطع شده و HTTP status code مساوی 401 (عدم دسترسی) را دریافت می‌کند.

منطق سفارشی پیاده سازی شده نیز به این صورت است:
نام ناحیه، کنترلر و اکشن متد درخواستی کاربر از مسیریابی جاری استخراج می‌شوند. سپس توسط سرویس سفارشی ISecurityTrimmingService تهیه شده، بررسی می‌کنیم که آیا کاربر جاری به این سه مؤلفه دسترسی دارد یا خیر؟

3- معرفی سیاست دسترسی پویای تهیه شده به سیستم
معرفی سیاست کاری پویا و سفارشی تهیه شده، شامل دو مرحله‌ی زیر است:
        private static void addDynamicPermissionsPolicy(this IServiceCollection services)
        {
            services.AddScoped<IAuthorizationHandler, DynamicPermissionsAuthorizationHandler>();
            services.AddAuthorization(opts =>
            {
                opts.AddPolicy(
                    name: ConstantPolicies.DynamicPermission,
                    configurePolicy: policy =>
                    {
                        policy.RequireAuthenticatedUser();
                        policy.Requirements.Add(new DynamicPermissionRequirement());
                    });
            });
        }
ابتدا باید DynamicPermissionsAuthorizationHandler تهیه شده را به سیستم تزریق وابستگی‌ها معرفی کنیم.
سپس یک Policy جدید را با نام دلخواه DynamicPermission تعریف کرده و نیازمندی علامتگذار خود را به عنوان یک policy.Requirements جدید، اضافه می‌کنیم. همانطور که ملاحظه می‌کنید یک وهله‌ی جدید از DynamicPermissionRequirement در اینجا ثبت شده‌است. همین وهله به متد HandleRequirementAsync نیز ارسال می‌شود. بنابراین اگر نیاز به ارسال پارامترهای بیشتری به این متد وجود داشت، می‌توان خواص مرتبطی را به کلاس DynamicPermissionRequirement نیز اضافه کرد.
همانطور که مشخص است، در اینجا یک نیازمندی را می‌توان ثبت کرد و نه Handler آن‌را. این Handler از سیستم تزریق وابستگی‌ها، بر اساس آرگومان جنریک AuthorizationHandler پیاده سازی شده، به صورت خودکار یافت شده و اجرا می‌شود (بنابراین اگر Handler شما اجرا نشد، مطمئن شوید که حتما آن‌را به سیستم تزریق وابستگی‌ها معرفی کرده‌اید).

پس از آن هر کنترلر یا اکشن متدی که از این سیاست دسترسی پویای تهیه شده استفاده کند:
[Authorize(Policy = ConstantPolicies.DynamicPermission)]
[DisplayName("کنترلر نمونه با سطح دسترسی پویا")]
public class DynamicPermissionsSampleController : Controller
به صورت خودکار توسط DynamicPermissionsAuthorizationHandler مدیریت می‌شود.


سرویس ISecurityTrimmingService چگونه کار می‌کند؟

کدهای کامل ISecurityTrimmingService را در کلاس SecurityTrimmingService می‌توانید مشاهده کنید.
پیشنیاز درک عملکرد آن، آشنایی با دو قابلیت زیر هستند:
الف) «روش یافتن لیست تمام کنترلرها و اکشن متدهای یک برنامه‌ی ASP.NET Core»
دقیقا از همین سرویس توسعه داده شده‌ی در مطلب فوق، در اینجا نیز استفاده شده‌است؛ با یک تفاوت تکمیلی:
public interface IMvcActionsDiscoveryService
{
    ICollection<MvcControllerViewModel> MvcControllers { get; }
    ICollection<MvcControllerViewModel> GetAllSecuredControllerActionsWithPolicy(string policyName);
}
از متد GetAllSecuredControllerActionsWithPolicy جهت یافتن تمام اکشن متدهایی که مزین به ویژگی Authorize هستند و دارای Policy مساوی DynamicPermission می‌باشند، در کنترلر DynamicRoleClaimsManagerController برای لیست کردن آن‌ها استفاده می‌شود. اگر این اکشن متد مزین به ویژگی DisplayName نیز بود (مانند مثال فوق و یا کنترلر نمونه DynamicPermissionsSampleController)، از مقدار آن برای نمایش نام این اکشن متد استفاده خواهد شد.
بنابراین همینقدر که تعریف ذیل یافت شود، این اکشن متد نیز در صفحه‌ی مدیریت سطوح دسترسی پویا لیست خواهد شد.
 [Authorize(Policy = ConstantPolicies.DynamicPermission)]

ابتدا به مدیریت نقش‌های ثابت سیستم می‌رسیم. سپس به هر نقش می‌توان یک ‍Claim جدید را با مقدار area:controller:action انتساب داد.
به این ترتیب می‌توان به یک نقش، تعدادی اکشن متد را نسبت داد و سطوح دسترسی به آن‌ها را پویا کرد. اما ذخیره سازی آن‌ها چگونه است و چگونه می‌توان به اطلاعات نهایی ذخیره شده دسترسی پیدا کرد؟


مفهوم جدید Role Claims در ASP.NET Core Identity

تا اینجا موفق شدیم تمام اکشن متدهای دارای سیاست دسترسی سفارشی سازی شده‌ی خود را لیست کنیم، تا بتوان آن‌ها را به صورت دلخواهی انتخاب کرد و سطوح دسترسی به آن‌ها را به صورت پویا تغییر داد. اما این اکشن متدهای انتخاب شده را در کجا و به چه صورتی ذخیره کنیم؟
برای ذخیره سازی این اطلاعات نیازی نیست تا جدول جدیدی را به سیستم اضافه کنیم. جدول جدید AppRoleClaims به همین منظور تدارک دیده شده‌است.



وقتی کاربری عضو یک نقش است، به صورت خودکار Role Claims آن نقش را نیز به ارث می‌برد. هدف از نقش‌ها، گروه بندی کاربران است. توسط Role Claims می‌توان مشخص کرد این نقش‌ها چه کارهایی را می‌توانند انجام دهند. اگر از قسمت قبل بخاطر داشته باشید، سرویس توکار UserClaimsPrincipalFactory دارای مرحله‌ی 5 ذیل است:
«5) اگر یک نقش منتسب به کاربر دارای Role Claim باشد، این موارد نیز واکشی شده و به کوکی او به عنوان یک Claim جدید اضافه می‌شوند. در ASP.NET Identity Core نقش‌ها نیز می‌توانند Claim داشته باشند (امکان پیاده سازی سطوح دسترسی پویا).»
به این معنا که با لاگین شخص به سیستم، تمام اطلاعات مرتبط به او که در جدول AppRoleClaims وجود دارند، به کوکی او به صورت خودکار اضافه خواهند شد و دسترسی به آن‌ها فوق العاده سریع است.

در کنترلر DynamicRoleClaimsManagerController، یک Role Claim Type جدید به نام DynamicPermissionClaimType اضافه شده‌است و سپس ID اکشن متدهای انتخابی را به نقش جاری، تحت Claim Type عنوان شده، اضافه می‌کند (تصویر فوق). این ID به صورت area:controller:action طراحی شده‌است. به همین جهت است که در  DynamicPermissionsAuthorizationHandler همین سه جزء از سیستم مسیریابی استخراج و در سرویس SecurityTrimmingService مورد بررسی قرار می‌گیرد:
 return user.HasClaim(claim => claim.Type == ConstantPolicies.DynamicPermissionClaimType &&
claim.Value == currentClaimValue);
در اینجا user همان کاربرجاری سیستم است. HasClaim جزو متدهای استاندارد آن است و Type انتخابی، همان نوع سفارشی مدنظر ما است. currentClaimValue دقیقا همان ID اکشن متد جاری است که توسط کنار هم قرار دادن area:controller:action تشکیل شده‌است.
متد HasClaim هیچگونه رفت و برگشتی را به بانک اطلاعاتی ندارد و اطلاعات خود را از کوکی شخص دریافت می‌کند. متد user.IsInRole نیز به همین نحو عمل می‌کند.


Tag Helper جدید SecurityTrimming

اکنون که سرویس ISecurityTrimmingService را پیاده سازی کرده‌ایم، از آن می‌توان جهت توسعه‌ی SecurityTrimmingTagHelper نیز استفاده کرد:
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            context.CheckArgumentIsNull(nameof(context));
            output.CheckArgumentIsNull(nameof(output));

            // don't render the <security-trimming> tag.
            output.TagName = null;

            if(_securityTrimmingService.CanCurrentUserAccess(Area, Controller, Action))
            {
                // fine, do nothing.
                return;
            }

            // else, suppress the output and generate nothing.
            output.SuppressOutput();
        }
عملکرد آن نیز بسیار ساده است. اگر کاربر، به area:controller:action جاری دسترسی داشت، این Tag Helper کاری را انجام نمی‌دهد. اگر خیر، متد SuppressOutput را فراخوانی می‌کند. این متد سبب خواهد شد، هر آنچه که داخل تگ‌های این TagHelper قرار گرفته، در صفحه رندر نشوند و از خروجی آن حذف شوند. به این ترتیب، کاربر به اطلاعاتی که به آن دسترسی ندارد (مانند لینک به مدخلی خاص را) مشاهده نخواهد کرد. به این مفهوم security trimming می‌گویند.
نمونه‌ای از کاربرد آن‌را در ReportsMenu.cshtml_ می‌توانید مشاهده کنید:
            <security-trimming asp-area="" asp-controller="DynamicPermissionsTest" asp-action="Products">
                <li>
                    <a asp-controller="DynamicPermissionsTest" asp-action="Products" asp-area="">
                        <span class="left5 fa fa-user" aria-hidden="true"></span>
                        گزارش از لیست محصولات
                    </a>
                </li>
            </security-trimming>
در اینجا اگر کاربر جاری به کنترلر DynamicPermissionsTest و اکشن متد Products آن دسترسی پویا نداشته باشد، محتوای قرارگرفته‌ی داخل تگ security-trimming را مشاهده نخواهد کرد.

برای آزمایش آن یک کاربر جدید را به سیستم DNT Identity اضافه کنید. سپس آن‌را در گروه نقشی مشخص قرار دهید (منوی مدیریتی،‌گزینه‌ی مدیریت نقش‌های سیستم). سپس به این گروه دسترسی به تعدادی از آیتم‌های پویا را بدهید (گزینه‌ی مشاهده و تغییر لیست دسترسی‌های پویا). سپس با این اکانت جدید به سیستم وارد شده و بررسی کنید که چه تعدادی از آیتم‌های منوی «گزارشات نمونه» را می‌توانید مشاهده کنید (تامین شده‌ی توسط ReportsMenu.cshtml_).


مدیریت اندازه‌ی حجم کوکی‌های ASP.NET Core Identity

همانطور که ملاحظه کردید، جهت بالابردن سرعت دسترسی به اطلاعات User Claims و Role Claims، تمام اطلاعات مرتبط با آن‌ها، به کوکی کاربر وارد شده‌ی به سیستم، اضافه می‌شوند. همین مساله در یک سیستم بزرگ با تعداد صفحات بالا، سبب خواهد شد تا حجم کوکی کاربر از 5 کیلوبایت بیشتر شده و توسط مرورگرها مورد قبول واقع نشوند و عملا سیستم از کار خواهد افتاد.
برای مدیریت یک چنین مساله‌ای، امکان ذخیره سازی کوکی‌های شخص در داخل بانک اطلاعاتی نیز پیش بینی شده‌است. زیر ساخت آن‌را در مطلب «تنظیمات کش توزیع شده‌ی مبتنی بر SQL Server در ASP.NET Core» پیشتر در این سایت مطالعه کردید و در پروژه‌ی DNT Identity بکارگرفته شده‌است.
اگر به کلاس IdentityServicesRegistry مراجعه کنید، یک چنین تنظیمی در آن قابل مشاهده است:
 var ticketStore = provider.GetService<ITicketStore>();
identityOptionsCookies.ApplicationCookie.SessionStore = ticketStore; // To manage large identity cookies
در ASP.NET Identity Core، امکان تدارک SessionStore سفارشی برای کوکی‌ها نیز وجود دارد. این SessionStore  باید با پیاده سازی اینترفیس ITicketStore تامین شود. دو نمونه پیاده سازی ITicketStore را در لایه سرویس‌های پروژه می‌توانید مشاهده کنید:
الف) DistributedCacheTicketStore
ب) MemoryCacheTicketStore

اولی از همان زیرساخت «تنظیمات کش توزیع شده‌ی مبتنی بر SQL Server در ASP.NET Core» استفاده می‌کند و دومی از IMemoryCache توکار ASP.NET Core برای پیاده سازی مکان ذخیره سازی محتوای کوکی‌های سیستم، بهره خواهد برد.
باید دقت داشت که اگر حالت دوم را انتخاب کنید، با شروع مجدد برنامه، تمام اطلاعات کوکی‌های کاربران نیز حذف خواهند شد. بنابراین استفاده‌ی از حالت ذخیره سازی آن‌ها در بانک اطلاعاتی منطقی‌تر است.


نحوه‌ی تنظیم سرویس ITicketStore را نیز در متد setTicketStore می‌توانید مشاهده کنید و در آن، در صورت انتخاب حالت بانک اطلاعاتی، ابتدا تنظیمات کش توزیع شده، صورت گرفته و سپس کلاس DistributedCacheTicketStore به عنوان تامین کننده‌ی ITicketStore به سیستم تزریق وابستگی‌ها معرفی می‌شود.
همین اندازه برای انتقال محتوای کوکی‌های کاربران به سرور کافی است و از این پس تنها اطلاعاتی که به سمت کلاینت ارسال می‌شود، ID رمزنگاری شده‌ی این کوکی است، جهت بازیابی آن از بانک اطلاعاتی و استفاده‌ی خودکار از آن در برنامه.


کدهای کامل این سری را در مخزن کد DNT Identity می‌توانید ملاحظه کنید.
مطالب
React 16x - قسمت 19 - کار با فرم‌ها - بخش 2 - اعتبارسنجی ورودی‌های کاربران
تمام فرم‌های تعریف شده، نیاز به اعتبارسنجی اطلاعات وارد شده‌ی توسط کاربران خود را دارند. ابتدا اعتبارسنجی اطلاعات را در حین ارسال فرم و سپس آن‌را همزمان با ورود اطلاعات، بررسی می‌کنیم.


اصول کلی طراحی یک اعتبارسنج ساده

در قسمت قبل، تمام اطلاعات فرم لاگین را درون شیء account خاصیت state قرار دادیم. در اینجا نیز شبیه به چنین شیءای را برای ذخیره سازی خطاهای اعتبارسنجی فیلدهای فرم، تعریف می‌کنیم:
class LoginForm extends Component {
  state = {
    account: { username: "", password: "" },
    errors: {
      username: "Username is required"
    }
  };
خاصیت errors تعریف شده، یک شیء را باز می‌گرداند که حاوی اطلاعات و خطاهای مرتبط با اعتبارسنجی فیلدهای مشکل دار است. بنابراین نام خواص این شیء، با نام فیلدهای فرم تطابق دارند. کار کردن با یک شیء هم جهت یافتن خطاهای یک فیلد مشخص، ساده‌تر است از کار کردن با یک آرایه؛ از این جهت که نیازی به جستجوی خاصی در این شیء نبوده و با استفاده از روش دسترسی پویای به خواص یک شیء جاوا اسکریپتی مانند errors["username"]، می‌توان خطاهای مرتبط با هر فیلد را به سادگی نمایش داد.
البته در ابتدای کار، خاصیت errors را با یک شیء خالی ({}) مقدار دهی می‌کنیم و سپس در متد مدیریت ارسال فرم به سرور:
  validate = () => {
    return { username: "Username is required." };
  };

  handleSubmit = e => {
    e.preventDefault();

    const errors = this.validate();
    console.log("Validation errors", errors);
    this.setState({ errors });
    if (errors) {
      return;
    }

    // call the server
    console.log("Submitted!");
  };
- ابتدا خروجی متد validate سفارشی را بررسی می‌کنیم که خروجی آن، خطاهای ممکن است.
- اگر خطایی وجود داشت، به مرحله‌ی بعد که ارسال فرم به سمت سرور می‌باشد، نخواهیم رسید و کار را با یک return، خاتمه می‌دهیم.
- علت فراخوانی متد setState در اینجا، درخواست رندر مجدد فرم، با توجه به خطاهای اعتبارسنجی ممکنی است که به خاصیت errors، اضافه یا به روز رسانی کرده‌ایم.
- نمونه‌ای از خروجی متد validate را نیز در اینجا مشاهده می‌کنید که تشکیل شده‌است از یک شیء، که هر خاصیت آن، به نام یک فیلد موجود در فرم، اشاره می‌کند.


پیاده سازی یک اعتبارسنج ساده

در اینجا یک نمونه پیاده سازی ساده و ابتدایی منطق اعتبارسنجی فیلدهای فرم را ملاحظه می‌کنید:
  validate = () => {
    const { account } = this.state;

    const errors = {};
    if (account.username.trim() === "") {
      errors.username = "Username is required.";
    }

    if (account.password.trim() === "") {
      errors.password = "Password is required.";
    }

    return Object.keys(errors).length === 0 ? null : errors;
  };
- ابتدا توسط Object Destructuring، خاصیت account شیء منتسب به خاصیت state کامپوننت را دریافت می‌کنیم، تا مدام نیاز به نوشتن this.state.account نباشد.
- سپس یک شیء خالی error را تعریف کرده‌ایم.
- در ادامه با توجه به اینکه مقادیر المان‌های فرم در state وجود دارند، خالی بودن آن‌ها را بررسی می‌کنیم. اگر خالی بودند، یک خاصیت جدید را با همان نام المان مورد بررسی، به شیء errors اضافه کرده و پیام خطایی را درج می‌کنیم.
- در نهایت این شیء errors و یا نال را (در صورت عدم وجود خطایی) بازگشت می‌دهیم.

برای آزمایش آن، پس از اجرای برنامه، یکبار بدون وارد کردن اطلاعاتی، بر روی دکمه‌ی Login کلیک کنید؛ یکبار هم با وارد کردن اطلاعاتی در فیلدهای مختلف. در این بین کنسول توسعه دهندگان مرورگر را نیز جهت مشاهده‌ی شیء‌های error لاگ شده، بررسی نمائید.



نمایش خطاهای اعتبارسنجی فیلدهای فرم

در قسمت قبل، کامپوننت جدید src\components\common\input.jsx را جهت کاهش کدهای تکراری تعاریف المان‌های ورودی، ایجاد کردیم. در اینجا نیز می‌توان کار نمایش خطاهای اعتبارسنجی را قرار داد:
import React from "react";

const Input = ({ name, label, value, error, onChange }) => {
  return (
    <div className="form-group">
      <label htmlFor={name}>{label}</label>
      <input
        value={value}
        onChange={onChange}
        id={name}
        name={name}
        type="text"
        className="form-control"
      />
      {error && <div className="alert alert-danger">{error}</div>}
    </div>
  );
};

export default Input;
- در اینجا ابتدا خاصیت error را به لیست خواص مورد انتظار از شیء props، اضافه کرده‌ایم.
- سپس با توجه به نکته‌ی «رندر شرطی عناصر در کامپوننت‌های React» در قسمت 5، اگر error مقداری داشته باشد و به true تفسیر شود، آنگاه به صورت خودکار، div ای که دارای کلاس‌های بوت استرپی اخطار است به همراه متن خطا، رندر خواهد شد؛ در غیراینصورت هیچ div ای به صفحه اضافه نمی‌شود.
- اکنون متد رندر کامپوننت فرم لاگین را به صورت زیر تکمیل می‌کنیم:
  render() {
    const { account, errors } = this.state;
    return (
      <form onSubmit={this.handleSubmit}>
        <Input
          name="username"
          label="Username"
          value={account.username}
          onChange={this.handleChange}
          error={errors.username}
        />
        <Input
          name="password"
          label="Password"
          value={account.password}
          onChange={this.handleChange}
          error={errors.password}
        />
        <button className="btn btn-primary">Login</button>
      </form>
    );
  }
در ابتدای متد رندر، با استفاده از Object Destructuring، خاصیت errors شیء منتسب به خاصیت state کامپوننت را دریافت کرده‌ایم. سپس با استفاده از آن، ویژگی جدید error را که به تعریف کامپوننت Input اضافه کردیم، در دو فیلد username و password، مقدار دهی می‌کنیم.

تا اینجا اگر تغییرات را ذخیره کرده و برنامه را اجرا کنیم، با کلیک بر روی دکمه‌ی Login، خطاهای اعتبارسنجی به صورت زیر ظاهر می‌شوند:


در این حالت اگر هر دو فیلد را تکمیل کرده و بر روی دکمه‌ی لاگین کلیک کنیم، به خطای زیر در کنسول توسعه دهندگان مرورگر می‌رسیم:
loginForm.jsx:55 Uncaught TypeError: Cannot read property 'username' of null
at LoginForm.render (loginForm.jsx:55)
علت اینجاست که چون فرم اعتبارسنجی شده و مشکلی وجود نداشته‌است، خروجی متد validate در این حالت، null است. بنابراین دیگر نمی‌توان به خاصیت برای مثال username آن دسترسی یافت. برای رفع این مشکل در متد handleSubmit، جائیکه errors را در خاصیت state به روز رسانی می‌کنیم، اگر errors نال باشد، بجای آن یک شیء خالی را بازگشت می‌دهیم:
this.setState({ errors: errors || {} });
این قطعه کد، به این معنا است که اگر errors مقدار دهی شده بود، از آن استفاده کن، در غیراینصورت {} (یک شیء خالی جاوا اسکریپتی) را بازگشت بده.


اعتبارسنجی فیلدهای یک فرم در حین ورود اطلاعات در آن‌ها

تا اینجا نحوه‌ی اعتبارسنجی فیلدهای ورودی را در حین submit بررسی کردیم. شبیه به همین روش را به حالت onChange و متد handleChange فرم لاگین که در قسمت قبل تکمیل کردیم نیز می‌توان اعمال کرد:
  handleChange = ({ currentTarget: input }) => {
    const errors = { ...this.state.errors }; //cloning an object
    const errorMessage = this.validateProperty(input);
    if (errorMessage) {
      errors[input.name] = errorMessage;
    } else {
      delete errors[input.name];
    }

    const account = { ...this.state.account }; //cloning an object
    account[input.name] = input.value;

    this.setState({ account, errors });
  };
- ابتدا شیء errors را clone می‌کنیم؛ چون می‌خواهیم خواصی را به آن کم و زیاد کرده و سپس بر اساس آن مجددا state را به روز رسانی کنیم.
- سپس اینبار فقط نیاز داریم اعتبار اطلاعات ورودی یک فیلد را بررسی کنیم و متد validate فعلی، فیلدهای کل فرم را با هم تعیین اعتبار می‌کند. به همین جهت متد جدید validateProperty را به صورت زیر تعریف می‌کنیم. اگر این متد خروجی داشت، خاصیت متناظر با آن‌را در شیء errors به روز رسانی می‌کنیم؛ در غیراینصورت این خاصیت را از شیء errors حذف می‌کنیم تا پیام اشتباهی را نمایش ندهد. در نهایت توسط متد setState، مقدار خاصیت errors را با شیء errors جاری به روز رسانی می‌کنیم:
  validateProperty = ({ name, value }) => {
    if (name === "username") {
      if (value.trim() === "") {
        return "Username is required.";
      }
      // ...
    }

    if (name === "password") {
      if (value.trim() === "") {
        return "Password is required.";
      }
      // ...
    }
  };
در متد validateProperty، خواص name و value از شیء input ارسالی به آن استخراج شده‌اند و سپس بر اساس آن‌ها کار اعتبارسنجی صورت می‌گیرد.

پس از ذخیره سازی این تغییرات، برای آزمایش آن، یکبار حرف a را بجای username وارد کنید و سپس آن‌را حذف کنید. بلافاصله پیام خطای مرتبطی نمایش داده خواهد شد و اگر مجددا عبارتی را وارد کنیم، این پیام محو می‌شود که معادل قسمت delete در کدهای فوق است.


معرفی Joi


تا اینجا، هدف نمایش ساختار یک اعتبارسنج ساده بود. این روش مقیاس پذیر نیست و در ادامه آن‌را با یک کتابخانه‌ی اعتبارسنجی بسیار پیشرفته به نام Joi، جایگزین خواهیم کرد که نمونه مثال‌های آن‌را در اینجا می‌توانید مشاهده کنید:
const Joi = require('@hapi/joi');
const schema = Joi.object({
    username: Joi.string().alphanum().min(3).max(30).required(),
    password: Joi.string().pattern(/^[a-zA-Z0-9]{3,30}$/),
    email: Joi.string().email({ minDomainSegments: 2, tlds: { allow: ['com', 'net'] } })
});
schema.validate({ username: 'abc', birth_year: 1994 });
ایده‌ی اصلی Joi، تعریف یک اسکیما برای object جاوا اسکریپتی خود است. در این اسکیما، تمام خواص شیء مدنظر ذکر شده و سپس توسط fluent api آن، نیازمندی‌های اعتبارسنجی هرکدام ذکر می‌شوند. برای مثال username باید رشته‌ای بوده، تنها از حروف و اعداد تشکیل شود. حداقل طول آن، 3 و حداکثر طول آن، 30 باشد و همچنین ورود آن نیز اجباری است. با استفاده از pattern آن می‌توان عبارات باقاعده را ذکر کرد و یا با متدهایی مانند email، از قالب خاص مقدار یک خاصیت، اطمینان حاصل کرد.

برای نصب آن، پس از باز کردن پوشه‌ی اصلی برنامه توسط VSCode، دکمه‌های ctrl+` را فشرده (ctrl+back-tick) و دستورات زیر را در ترمینال ظاهر شده وارد کنید:
> npm install @hapi/joi --save
> npm i --save-dev @types/hapi__joi
که در نهایت سبب نصب کتابخانه‌ی node_modules\@hapi\joi\dist\joi-browser.min.js خواهند شد و همچنین TypeScript definitions آن‌را نیز نصب می‌کنند که بلافاصله سبب فعالسازی intellisense مخصوص آن در VSCode خواهد شد. بدون نصب types آن، پس از تایپ Joi.، از مزایای تکمیل خودکار fluent api آن توسط VSCode، برخوردار نخواهیم بود.

سپس به کامپوننت فرم لاگین مراجعه کرده و در ابتدای آن، Joi را import می‌کنیم:
import Joi from "@hapi/joi";
پس از آن، اسکیمای شیء account را تعریف خواهیم کرد. اسکیما نیازی نیست جزئی از state باشد؛ چون قرار نیست تغییر کند. به همین جهت آن‌را به صورت یک خاصیت جدید در سطح کلاس کامپوننت تعریف می‌کنیم:
  schema = {
    username: Joi.string()
      .required()
      .label("Username"),
    password: Joi.string()
      .required()
      .label("Password")
  };
خاصیت اسکیما را با یک شیء با ساختار از نوع Joi.object، که خواص آن، با خواص شیء account مرتبط با فیلدهای فرم لاگین، تطابق دارد، تکمیل می‌کنیم. مقدار هر خاصیت نیز با Joi. شروع شده و سپس نوع و محدودیت‌های مدنظر اعتبارسنجی را می‌توان تعریف کرد که در اینجا هر دو مورد باید رشته‌ای بوده و به صورت اجباری وارد شوند. توسط متد label، برچسب نام خاصیت درج شده‌ی در پیام خطای نهایی را می‌توان تنظیم کرد. اگر از این متد استفاده نشود، از همان نام خاصیت ذکر شده استفاده می‌کند.
سپس ابتدای متد validate قبلی را به صورت زیر بازنویسی می‌کنیم:
  validate = () => {
    const { account } = this.state;
    const validationResult = Joi.object(this.schema).validate(account, {
      abortEarly: false
    });
    console.log("validationResult", validationResult);
ابتدا مقدار خاصیت account، از شیء state استخراج شده‌است که حاوی شیءای با اطلاعات نام کاربری و کلمه‌ی عبور است. سپس این شیء را به متد validate خاصیت اسکیمایی که تعریف کردیم، ارسال می‌کنیم. Joi، اعتبارسنجی را به محض یافتن خطایی، متوقف می‌کند. به همین جهت تنظیم abortEarly آن به false صورت گرفته‌است تا تمام خطاهای اعتبارسنجی را نمایش دهد.
اکنون اگر برنامه را اجرا کرده و بدون ورود اطلاعاتی، بر روی دکمه‌ی لاگین کلیک کنیم، خروجی زیر در کنسول توسعه دهندگان مرورگر ظاهر می‌شود:


همانطور که مشاهده می‌کنید، خروجی Joi، یک شیء است که اگر دارای خاصیت error بود، یعنی خطای اعتبارسنجی رخ‌داده‌است. سپس باید خاصیت آرایه‌ای details این شیء error را جهت یافتن خواص مشکل دار بررسی کرد. هر خاصیت در اینجا با path مشخص می‌شود. بنابراین قدم بعدی، تبدیل این ساختار، به ساختار شیء errors موجود در state کامپوننت جاری است تا مابقی برنامه بتواند بدون تغییری از آن استفاده کند.


نگاشت شیء دریافتی از Joi، به شیء errors موجود در state کامپوننت لاگین

خاصیت error شیء دریافتی از متد validate کتابخانه‌ی Joi، تنها زمانی ظاهر می‌شود که خطایی وجود داشته باشد. همچنین خاصیت details آن نیز آرایه‌ا‌ی از اشیاء با خواص message و path است. این path نیز یک آرایه است که اولین المان آن، نام خاصیت در حال بررسی است. اکنون می‌خواهیم این آرایه را تبدیل به یک شیء قابل درک برای برنامه کنیم:
  validate = () => {
    const { account } = this.state;
    const validationResult = Joi.object(this.schema).validate(account, {
      abortEarly: false
    });
    console.log("validationResult", validationResult);

    if (!validationResult.error) {
      return null;
    }

    const errors = {};
    for (let item of validationResult.error.details) {
      errors[item.path[0]] = item.message;
    }
    return errors;
  };
ابتدا بررسی می‌کنیم که آیا خاصیت error، تنظیم شده‌است یا خیر؟ اگر خیر، کار این متد به پایان می‌رسد. سپس حلقه‌ای را بر روی آرایه‌ی details، تشکیل می‌دهیم تا شیء errors مدنظر ما را با خاصیت دریافتی از path و پیام دریافتی متناظری تکمیل کند. در آخر این شیء errors با ساختار مدنظر خود را بازگشت می‌دهیم.

اکنون اگر تغییرات را ذخیره کرده و برنامه را اجرا کنیم، همانند قبل می‌توان خطاهای اعتبارسنجی در حین submit را مشاهده کرد:



بازنویسی متد validateProperty توسط Joi

تا اینجا متد validate ساده و ابتدایی خود را با استفاده از امکانات کتابخانه‌ی Joi، بازنویسی کردیم. اکنون نوبت بازنویسی متد اعتبارسنجی در حین تایپ اطلاعات است:
  validateProperty = ({ name, value }) => {
    const userInputObject = { [name]: value };
    const propertySchema = Joi.object({ [name]: this.schema[name] });
    const { error } = propertySchema.validate(userInputObject, {
      abortEarly: true
    });
    return error ? error.details[0].message : null;
  };
تفاوت این متد با متد validate، در اعتبارسنجی تنها یک خاصیت از شیء account موجود در state است؛ به همین جهت نمی‌توان کل this.state.account را به متد validate کتابخانه‌ی Joi ارسال کرد. بنابراین نیاز است بر اساس name و value رسیده‌ی از کاربر، یک شیء جدید را به صورت پویا تولید کرد. در اینجا روش تعریف { [name]: value } به computed properties معرفی شده‌ی در ES6 اشاره می‌کند. اگر در حین تعریف یک شیء، برای مثال بنویسیم {"username:"value}، این username به صورت "username" ثابتی تفسیر می‌شود و پویا نیست. اما در ES6 می‌توان با استفاده از [] ها، تعریف نام یک خاصیت را پویا کرد که نمونه‌ای از آن‌را در userInputObject و همچنین propertySchema مشاهده می‌کنید.
تا اینجا بجای this.state.account که به کل فرم اشاره می‌کند، شیء اختصاصی‌تر userInputObject را ایجاد کرده‌ایم که معادل اطلاعات فیلد ورودی کاربر است. یک چنین نکته‌ای را در مورد schema نیز باید رعایت کرد. در اینجا نمی‌توان اعتبارسنجی را با this.schema شروع کرد؛ چون این شیء نیز به اطلاعات کل فرم اشاره می‌کند و نه تک فیلدی که هم اکنون در حال کار با آن هستیم. بنابراین نیاز است یک Joi.object جدید را بر اساس name رسیده، از this.schema کلی، استخراج و تولید کرد؛ مانند propertySchema که مشاهده می‌کنید.
اکنون می‌توان متد validate این شیء اسکیمای جدید را فراخوانی کرد و خاصیت error شیء حاصل از آن‌را توسط Object Destructuring، استخراج نمود. در اینجا abortEarly به true تنظیم شده‌است (البته حالت پیش‌فرض آن است و نیازی به ذکر صریح آن نیست). علت اینجا است که نمایش خطاهای بیش از اندازه به کاربر، برای او گیج کننده خواهند بود؛ به همین جهت هربار، اولین خطای حاصل را به او نمایش می‌دهیم.


غیرفعالسازی دکمه‌ی submit در صورت شکست اعتبارسنجی‌های فرم

در ادامه می‌خواهیم دکمه‌ی submit فرم لاگین، تا زمانیکه اعتبارسنجی آن با موفقیت به پایان برسد، غیرفعال باشد:
 <button disabled={this.validate()} className="btn btn-primary">
 Login
</button>
ویژگی disabled را اگر به true تنظیم کنیم، این دکمه را غیرفعال می‌کند. در اینجا متد validate تعریف شده، نال و یا یک شیء را بازگشت می‌دهد. اگر نال را بازگشت دهد، در جاوااسکریپت به false تفسیر شده و دکمه را فعال می‌کند و برعکس؛ مانند تصویر حاصل زیر:


فراخوانی setState‌های تعریف شده‌ی در متدهای رویدادگردان این فرم هستند که سبب رندر مجدد کامپوننت شده و در این بین در متد رندر، کار بررسی مجدد مقدار نهایی متد validate صورت می‌گیرد تا بر اساس آن، فعال یا غیرفعال بودن دکمه‌ی Login، مشخص شود.

کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-19.zip
نظرات مطالب
بررسی روش مشاهده خروجی SQL حاصل از کوئری‌های Entity framework Core
یک نکته‌ی تکمیلی: یافتن ساده‌تر کوئری‌ها در لاگ نهایی

پس از فعالسازی لاگ کردن عملیات در EF Core، مشکلی که در بین انبوهی از اطلاعات لاگ شده رخ می‌دهد این است که این کوئری خاص دقیقا مرتبط به کدام قسمت برنامه است؟ برای یافتن ساده‌تر کوئری‌ها و ارتباط دادن آن‌ها با کدهای قسمت‌های مختلف برنامه، در EF Core 2.2، قابلیتی تحت عنوان TagWith افزوده شده‌است:
var orders = 
      (from b in context.Orders.TagWith(@"List of top 10 orders") 
      orderby b.Price descending 
      select b).Take(10).ToList();
با این خروجی
-- List of top 10 orders
 
SELECT TOP(@__p_1) [b].[Id], [b].[Name], [b].[Price]
 FROM [Orders] AS [b] 
ORDER BY [b].[Price] DESC
که در آن عبارت TagWith در ابتدای لاگ درج شده‌است و به این ترتیب ارتباط دادن کدهای برنامه با خروجی لاگ شده و یافتن معادل‌ها، ساده‌تر می‌شود.
نظرات اشتراک‌ها
به روز رسانی فایل OPML وبلاگ‌های IT ایرانی
سلام جناب نصیری وقت شما بخیر 
این قسمت به روز نمی‌شه ؟ چند روز پیش توی خروجی فیدهای سایت دیدم ولی هرچی سرچ زدم پیدا نکردم 
پاسخ به بازخورد‌های پروژه‌ها
خطای property not found
بر اساس اطلاعات فوق، خروجی متد GetClassesSessions لیستی از نوع Session است نه از نوع WeekDayClassSessionSemiRow که در تعریف ستون یا منبع داده استفاده کردید. (قسمت تهیه لیستی از WeekDayClassSessionSemiRow را هم ذکر نکردید)