نظرات مطالب
Blazor 5x - قسمت 14 - کار با فرم‌ها - بخش 2 - تعریف فرم‌ها و اعتبارسنجی آن‌ها
یک نکته‌ی تکمیلی: روش تهیه‌ی ویژگی‌های سفارشی اعتبارسنجی، در برنامه‌های Blazor

اگر ویژگی‌های پیش‌فرض مهیا، پاسخگوی اعتبارسنجی مدنظر نبودند، می‌توان یک attribute سفارشی را تهیه کرد:
using System.ComponentModel.DataAnnotations;

namespace CustomValidators
{
    [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)]
    public class EmailDomainValidator : ValidationAttribute
    {
        public string AllowedDomain { get; set; }

        protected override ValidationResult IsValid(object value, 
            ValidationContext validationContext)
        {
            string[] strings = value.ToString().Split('@');
            if (strings[1].ToUpper() == AllowedDomain.ToUpper())
            {
                return null;
            }

            return new ValidationResult($"Domain must be {AllowedDomain}",
            new[] { validationContext.MemberName });
        }
    }
}
توضیحات:
- کار با ارث بری از کلاس پایه‌ی ValidationAttribute شروع می‌شود و باید متد IsValid آن‌را بازنویسی کرد.
- اگر متد IsValid، نال برگرداند، یعنی مشکلی نیست؛ در غیراینصورت خروجی آن باید از نوع ValidationResult باشد.
- پارامتر validationContext اطلاعاتی مانند نام خاصیت در حال بررسی را ارائه می‌دهد.
- در اینجا متد ()ValidationContext.GetService نال را بر می‌گرداند؛ یعنی فعلا از تزریق وابستگی‌ها در آن پشتیبانی نمی‌شود.

و در آخر روش استفاده‌ی از آن، همانند سایر ویژگی‌های اعتبارسنجی است:
public class Employee
{
    [EmailDomainValidator(AllowedDomain = "site.com")]
    public string Email { get; set; }
}
نظرات مطالب
Blazor 5x - قسمت 34 - توزیع برنامه‌های Blazor بر روی IIS
نمونه‌ای از راه اندازی یک برنامه‌ی Blazor WASM در IIS

الف) به قسمت application pools در IIS Manager مراجعه کرده و گزینه‌ی add application pool را انتخاب کنید. سپس یک نمونه‌ی جدید را برای مثال به نام no-managed ایجاد کنید که net clr version. آن به گزینه‌ی no managed code اشاره می‌کند.
ب) پس از publish برنامه مطابق مطلب فوق (برای مثال با اجرای دستور dotnet publish -c Release) و معرفی مسیر پوشه‌ی publish به IIS (برای مثال به عنوان یک Application جدید ذیل default web site با کلیک راست بر روی default web site و انتخاب گزینه‌ی Add application)، این application pool جدید را به برنامه‌ی خود در IIS نسبت دهید. برای اینکار basic settings سایت را باز کرده و بر روی دکمه‌ی select که در کنار نام application pool هست، کلیک کرده و گزینه‌ی no-managed قسمت الف را انتخاب کنید.

نکته 1: برنامه‌های blazor wasm، یا standalone هستند و یا hosted. مورد standalone یعنی کاری به Web API ندارد و به خودی خود، به صورت یک سایت استاتیک قابل مشاهده‌است. حالت hosted یعنی به همراه web api هم هست و توسط دستور برای مثال «dotnet new blazorwasm -o BlazorIIS --hosted --no-https» ایجاد می‌شود که به همراه سه پوشه‌ی کلاینت، سرور و shared است. برای توزیع حالت متکی به خود، فقط محتویات پوشه‌ی publish، به عنوان مسیر برنامه، در IIS معرفی خواهند شد. در حالت hosted، مسیر اصلی، پوشه‌ی publish مربوط به پروژه‌ی سرور است؛ یعنی: Server\bin\Release\net5.0\publish. در این حالت پوشه‌ی Client\bin\Release\net5.0\publish باید به داخل همین پوشه‌ی publish سرور کپی شود. یعنی پوشه‌ی publish پروژه‌ی client باید به درون پوشه‌ی publish پروژه‌ی server کپی شود تا با هم یک برنامه‌ی قابل توزیع توسط IIS را تشکیل دهند.
در اینجا تنها فایل‌هایی که تداخل پیدا می‌کنند، فایل‌های web.config هستند که باید یکی شوند. فایل web.config برنامه‌ی web api با برنامه‌ی client یکی نیست، اما می‌توان محتویات این دو را با هم یکی کرد.
نکته 2: محل اجرای دستور dotnet publish -c Release مهم است. اگر این دستور را در کنار فایل sln پروژه‌ی hosted اجرا کنید، سه خروجی نهایی publish را تولید می‌کند و پس از آن باید فایل‌های کلاینت را به سرور، به صورت دستی کپی کرد. اما اگر دستور publish را درون پوشه‌ی سرور اجرا کنید، کار کپی فایل‌های کلاینت را به درون پوشه‌ی نهایی publish تشکیل شده، به صورت خودکار انجام می‌دهد؛ علت اینجا است که اگر به فایل csproj. پروژه‌ی سرور دقت کنید، ارجاعی را به پروژه‌ی کلاینت نیز دارد (هر چند ما از کدهای آن در برنامه‌ی web api استفاده نمی‌کنیم). این ارجاع تنها کمک حال دستور dotnet publish -c Release است و کاربرد دیگری را ندارد.

ج) اکنون اگر برنامه را برای مثال با مسیر فرضی جدید http://localhost/blazortest اجرا کنید، خطای 500.19 را دریافت می‌کنید. علت آن‌را در مطلب «بررسی خطاهای ممکن در حین راه اندازی اولیه برنامه‌های ASP.NET Core در IIS» بررسی کرده‌ایم. باید IIS URL Rewrite ماژول را نصب کرد؛ تا این مشکل برطرف شود. همچنین دلیل دیگر مشاهده‌ی این خطا، عدم نصب بسته‌ی هاستینگ متناظر با شماره نگارش NET. مورد استفاده‌است (اگر برنامه‌ی شما از نوع hosted است و web api هم دارد).
د) پس از آن باز هم برنامه اجرا نمی‌شود! اگر در خطاها دقت کنید، به دنبال اسکریپت‌هایی شروع شده از مسیر localhost است و نه از پوشه‌ی جدید blazortest. برای رفع این مشکل باید فایل publish\wwwroot\index.html را مطابق نکته‌ی base-URL که کمی بالاتر ذکر شد، ویرایش کرد تا blazor بداند که فایل‌ها، در چه مسیری قرار دارند (و در ریشه‌ی سایت واقع نشده‌اند):
<head>
<base href="/blazortest/" />
اکنون برنامه بدون مشکل بارگذاری و اجرا می‌شود.
نظرات مطالب
Blazor 5x - قسمت 33 - احراز هویت و اعتبارسنجی کاربران Blazor WASM - بخش 3- بهبود تجربه‌ی کاربری عدم دسترسی‌ها
یک نکته‌ی تکمیلی: کار با Policy‌ها در برنامه‌های Blazor WASM

در این مطلب، روشی را برای برقراری دسترسی نقش Admin، به تمام قسمت‌های محافظت شده‌ی برنامه، با معرفی نقش آن به یک ویژگی Authorize سفارشی شده، مشاهده کردید. هرچند این روش کار می‌کند، اما روش جدیدتر برقراری یک چنین دسترسی‌های ترکیبی در برنامه‌های ASP.NET Core و سایر فناوری‌های مشتق شده‌ی از آن، کار با Policyها است که برای نمونه در مثال فوق، به صورت زیر قابل پیاده سازی است:

الف) تعریف Policyهای مشترک بین برنامه‌های Web API و WASM
Policyهای تعریف شده، باید قابلیت اعمال به اکشن متدهای کنترلرها و همچنین کامپوننت‌های WASM را داشته باشند. به همین جهت آن‌ها را در پروژه‌ی اشتراکی BlazorServer.Common که در هر دو پروژه استفاده می‌شود، قرار می‌دهیم:
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization; // dotnet add package Microsoft.AspNetCore.Authorization

namespace BlazorServer.Common
{
    public static class PolicyTypes
    {
        public const string RequireAdmin = nameof(RequireAdmin);
        public const string RequireCustomer = nameof(RequireCustomer);
        public const string RequireEmployee = nameof(RequireEmployee);
        public const string RequireEmployeeOrCustomer = nameof(RequireEmployeeOrCustomer);

        public static AuthorizationOptions AddAppPolicies(this AuthorizationOptions options)
        {
            options.AddPolicy(RequireAdmin, policy => policy.RequireRole(ConstantRoles.Admin));
            options.AddPolicy(RequireCustomer, policy =>
                    policy.RequireAssertion(context =>
                        context.User.HasClaim(claim => claim.Type == ClaimTypes.Role
                            && (claim.Value == ConstantRoles.Admin || claim.Value == ConstantRoles.Customer))
                    ));
            options.AddPolicy(RequireEmployee, policy =>
                    policy.RequireAssertion(context =>
                        context.User.HasClaim(claim => claim.Type == ClaimTypes.Role
                            && (claim.Value == ConstantRoles.Admin || claim.Value == ConstantRoles.Employee))
                    ));

            options.AddPolicy(RequireEmployeeOrCustomer, policy =>
                                policy.RequireAssertion(context =>
                                    context.User.HasClaim(claim => claim.Type == ClaimTypes.Role
                                        && (claim.Value == ConstantRoles.Admin ||
                                            claim.Value == ConstantRoles.Employee ||
                                            claim.Value == ConstantRoles.Customer))
                                ));
            return options;
        }
    }
}
در اینجا یکسری Policy جدید را مشاهده می‌کنید که در آن‌ها همواره نقش Admin حضور دارد و همچین روش or آن‌ها را توسط policy.RequireAssertion مشاهده می‌کنید. این تعاریف، نیاز به نصب بسته‌ی Microsoft.AspNetCore.Authorization را نیز دارند. با کمک Policyها می‌توان ترکیب‌های پیچیده‌ای از دسترسی‌های موردنیاز را ساخت؛ بدون اینکه نیاز باشد مدام AuthorizeAttribute سفارشی را طراحی کرد.

ب) افزودن Policyهای تعریف شده به پروژه‌های Web API و WASM
پس از تعریف Policyهای مورد نیاز، اکنون نوبت به افزودن آن‌ها به برنامه‌های Web API:
namespace BlazorWasm.WebApi
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            // ...

            services.AddAuthorization(options => options.AddAppPolicies());

            // ...
و همچنین WASM است:
namespace BlazorWasm.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            // ...

            builder.Services.AddAuthorizationCore(options => options.AddAppPolicies());

            // ...
        }
    }
}
به این ترتیب Policyهای یک‌دستی را بین برنامه‌های کلاینت و سرور، به اشتراک گذاشته‌ایم.

ج) استفاده از Policyهای تعریف شده در برنامه‌ی WASM
اکنون که برنامه قابلیت کار با Policyها را پیدا کرده، می‌توان فیلتر Roles سفارشی را حذف و با فیلتر Authorize پالیسی دار جایگزین کرد:
@page "/hotel-room-details/{Id:int}"

// ...

@*
@attribute [Roles(ConstantRoles.Customer, ConstantRoles.Employee)]
*@

@attribute [Authorize(Policy = PolicyTypes.RequireEmployeeOrCustomer)]

حتی می‌توان از پالیسی‌ها در حین تعریف AuthorizeViewها نیز استفاده کرد:
<AuthorizeView  Policy="@PolicyTypes.RequireEmployeeOrCustomer">
    <p>You can only see this if you're an admin or an employee or a customer.</p>
</AuthorizeView>
اشتراک‌ها
انتشار Stimulsoft Reports 2018.1 با پشتیبانی از NET Core.

Native .NET Core Support
In the release 2018.1, we present a full-featured report generator, created using the cross-platform technology — .NET Core. A full set of Web components such as the report designer as well as additional tools for quick export and report printing is available. The .NET Core components are included in the product Stimulsoft Reports.Web and Stimulsoft Reports.Ultimate. 

انتشار Stimulsoft Reports 2018.1 با پشتیبانی از NET Core.
نظرات مطالب
اعتبارسنجی مبتنی بر کوکی‌ها در ASP.NET Core 2.0 بدون استفاده از سیستم Identity
یک نکته‌ی تکمیلی: RedirectUri در SignInAsync نیاز به encoding دارد.

روشی که در مطلب جاری مطرح شده، در برنامه‌های Blazor SSR هم قابل استفاده‌است و جهت سهولت بازگشت از صفحه‌ی لاگین به صفحه‌ی قبلی، می‌توان از خاصیت RedirectUri آن بدون نیاز به هیچ متد دیگری استفاده کرد:
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, cookieClaims,
            new AuthenticationProperties
            {
                // ...
                RedirectUri = GetSafeRedirectUri(ReturnUrl)
            });
پیاده سازی متد GetSafeRedirectUri را در اینجا می‌توانید مشاهده کنید که هدف از آن، مقاومت در برابر حملات Open redirect است. فقط ... نکته‌ی مهمی که در اینجا لحاظ شده، encoding آن است که بدون آن، به خطای زیر خواهیم رسید:
Unhandled exception rendering component: Invalid non-ASCII or control character in header: 0x062A 
System.InvalidOperationException: Invalid non-ASCII or control character in header: 0x062A at 
void Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.ThrowInvalidHeaderCharacter(char ch)
فرض کنید که RedirectUri نهایی، به همراه حروف فارسی است. چون متد HttpContext.SignInAsync از هدرهای HTTP برای انجام هدایت به صفحه‌ای دیگر استفاده می‌کند،‌ مقادیر این هدرها نمی‌توانند یونیکد باشند و حتما باید encode شوند و اگر اینکار انجام نشود، هر از چندگاهی با خطای فوق مواجه خواهیم شد. متد ویژه‌ی UriHelper.Encode، دقیقا جهت encoding آدرس‌های وب، مخصوص قرارگیری در هدرهای HTTP طراحی شده‌است.
مطالب
بررسی تغییرات Blazor 8x - قسمت دوازدهم - قالب جدید پیاده سازی اعتبارسنجی و احراز هویت - بخش دوم
در قسمت قبل، با نام‌هایی مانند IdentityRevalidatingAuthenticationStateProvider و PersistingRevalidatingAuthenticationStateProvider آشنا شدیم. در این قسمت جزئیات بیشتری از این کلاس‌ها را بررسی می‌کنیم.


نحوه‌ی پیاده سازی AuthenticationStateProvider در پروژه‌های Blazor Server 8x

در کدهای زیر، ساختار کلی کلاس AuthenticationStateProvider ارائه شده‌ی توسط قالب رسمی پروژه‌های Blazor Server به همراه مباحث اعتبارسنجی مبتنی بر ASP.NET Core Identity را مشاهده می‌کنید:
public class IdentityRevalidatingAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider
{

    protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);

    protected override async Task<bool> ValidateAuthenticationStateAsync(
        AuthenticationState authenticationState, CancellationToken cancellationToken)
    {
     // ...
    }
}
کار این کلاس، پیاده سازی کلاس پایه‌ی RevalidatingServerAuthenticationStateProvider است. این کلاس پایه، چیزی نیست بجز یک کلاس پیاده سازی کننده‌ی AuthenticationStateProvider که در آن توسط حلقه‌ای، کار یک تایمر را پیاده سازی کرده‌اند که برای مثال در اینجا هر نیم ساعت یکبار، متد ValidateAuthenticationStateAsync را صدا می‌زند.
برای مثال در اینجا (یعنی کلاس بازنویسی کننده‌ی متد ValidateAuthenticationStateAsync که توسط تایمر کلاس پایه فراخوانی می‌شود) اعتبار security stamp کاربر جاری، هر نیم ساعت یکبار بررسی می‌شود. اگر فاقد اعتبار بود، کلاس پایه‌ی استفاده شده، سبب LogOut خودکار این کاربر می‌شود.


نحوه‌ی پیاده سازی AuthenticationStateProvider در پروژه‌های Blazor WASM 8x

دو نوع پروژه‌ی مبتنی بر وب‌اسمبلی را می‌توان در دات نت 8 ایجاد کرد: پروژه‌های حالت رندر Auto و پروژه‌های حالت رندر WASM
در هر دوی این‌ها، از کامپوننت ویژه‌ای به نام PersistentComponentState استفاده شده‌است که معرفی آن‌را در قسمت هشتم این سری مشاهده کردید. کار این کامپوننت در سمت سرور به صورت زیر است:
public class PersistingRevalidatingAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider
{
    public PersistingRevalidatingAuthenticationStateProvider(
        ILoggerFactory loggerFactory,
        IServiceScopeFactory scopeFactory,
        PersistentComponentState state,
        IOptions<IdentityOptions> options)
        : base(loggerFactory)
    {
     // ...
    }

    protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);

    protected override async Task<bool> ValidateAuthenticationStateAsync(
        AuthenticationState authenticationState, CancellationToken cancellationToken)
    {
     // ...
    }

    private async Task OnPersistingAsync()
    {
     // ...
                _state.PersistAsJson(nameof(UserInfo), new UserInfo
                {
                    UserId = userId,
                    Email = email,
                });
     // ...
    }
}
همانطور که مشاهده می‌کنید، مهم‌ترین تفاوت آن با پروژه‌های Blazor Server، ذخیره سازی state به صورت JSON است که اینکار توسط کامپوننت PersistentComponentState انجام می‌شود و این Component-Stateهای مخفی حاصل از فراخوانی PersistAsJson، فقط یکبار توسط قسمت کلاینت، به صورت زیر خوانده می‌شوند:
public class PersistentAuthenticationStateProvider(PersistentComponentState persistentState) : AuthenticationStateProvider
{
    private static readonly Task<AuthenticationState> _unauthenticatedTask =
        Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())));

    public override Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        if (!persistentState.TryTakeFromJson<UserInfo>(nameof(UserInfo), out var userInfo) || userInfo is null)
        {
            return _unauthenticatedTask;
        }

        Claim[] claims = [
            new Claim(ClaimTypes.NameIdentifier, userInfo.UserId),
            new Claim(ClaimTypes.Name, userInfo.Email),
            new Claim(ClaimTypes.Email, userInfo.Email) ];

        return Task.FromResult(
            new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(claims,
                authenticationType: nameof(PersistentAuthenticationStateProvider)))));
    }
}
در این کلاس سمت کلاینت و قرار گرفته‌ی در پروژه‌های WASM، نحوه‌ی پیاده سازی AuthenticationStateProvider را مشاهده می‌کنید که توسط آن و به کمک PersistentComponentState، کار خواندن اطلاعات UserInfo ای که پیشتر توسط state.PersistAsJson_ در سمت سرور در انتهای HTML صفحه ذخیره شده بود، انجام می‌‌شود.

بنابراین PersistentComponentState کار پرکردن اطلاعات یک کش مانند را در سمت سرور انجام داده و آن‌را به صورت سریالایز شده با قالب JSON، به انتهای HTML صفحه اضافه می‌کند. سپس زمانیکه کلاینت بارگذاری می‌شود، این اطلاعات را خوانده و استفاده می‌کند. یکبار از متد PersistAsJson آن در سمت سرور استفاده می‌شود، برای ذخیره سازی اطلاعات و یکبار از متد TryTakeFromJson آن در سمت کلاینت، برای خواندن اطلاعات.

یک نکته: پیاده سازی anti-forgery-token هم با استفاده از PersistentComponentState صورت گرفته‌است.