نظرات مطالب
بررسی فرمت کوکی‌های ASP.NET Identity
با سلام
آقای نصیری در آخرین سوال گفتید که باید از UpdateSecurityStampAsync استفاده بشه. من تو برنامم ، بر اساس هر Application_AuthenticateRequest میام چک میکنم که اگر کاربر حذف یا قفل یا ... بود به صورت خودکار Signout بشه. با توجه به نکته امنیتی که که شما گفتید باید اینجا هم از  UpdateSecurityStampAsync  استفاده بشه ، من به این صورت استفاده کردم :


   public class MvcApplication : HttpApplication
    {
        public MvcApplication()
        {
            var wrapper = new EventHandlerTaskAsyncHelper(AuthenticateRequest);
            AddOnAuthenticateRequestAsync(wrapper.BeginEventHandler, wrapper.EndEventHandler);
        }
        private async Task AuthenticateRequest(object sender, EventArgs e)
        {
            if (Context.User == null)
                return;

            //bla bla

            if (هرچیزی که مجاز نباشد)
            {
                var authenticationManager = ProjectObjectFactory.Container.GetInstance<IAuthenticationManager>();
                authenticationManager.SignOut
                (
                    DefaultAuthenticationTypes.ExternalCookie,
                    DefaultAuthenticationTypes.ApplicationCookie
                );
                FormsAuthentication.SignOut();
                await userService.UpdateSecurityStampAsync(userId);
            }
        }
    }

در عمل چک کردم جواب داد ، البته مجبور بودم از یک EventHandlerTaskAsyncHelper استفاده کنم چون نمیشد تو خود متد Application_AuthenticateRequest متدهای Async تعریف کرد.

سوال من اینه ، آیا اصلا نیاز هست در اینجا هم security stamp رو آپدیت کنیم ؟ یا اصلا این روشی که من رفتم از نظر امنیتی درست هست یا خیر.

با تشکر
مطالب
Blazor 5x - قسمت یازدهم - مبانی Blazor - بخش 8 - کار با جاوا اسکریپت
در حین کار با برنامه‌های وب، چشم‌پوشی از جاوا اسکریپت عملا ممکن نیست؛ هرچند با Blazor، امکان انجام کارهایی را یافته‌ایم که پیشتر با MVC و یا Razor pages میسر نبودند، اما هیچگاه به تنهایی نمی‌تواند جایگزین کامل جاوا اسکریپت، در تولید برنامه‌های وب باشد. بنابراین ضروری است که نحوه‌ی یکپارچگی جاوا اسکریپت را با برنامه‌های مبتنی بر Blazor، بررسی کنیم.


ایجاد کامپوننت جدید BlazorJS

برای بررسی نحوه‌ی تعامل جاوا اسکریپت و Blazor، در ابتدا کامپوننت جدید Pages\LearnBlazor\BlazorJS.razor را ایجاد کرده:
@page "/BlazorJS"

<h3>BlazorJS</h3>

@code
{
}
و همچنین مدخل منوی آن‌را نیز بر اساس مسیریابی ابتدای فایل این کامپوننت، به فایل Shared\NavMenu.razor اضافه می‌کنیم:
<li class="nav-item px-3">
    <NavLink class="nav-link" href="BlazorJS">
        <span class="oi oi-list-rich" aria-hidden="true"></span> BlazorJS
    </NavLink>
</li>


روش فراخوانی کدهای جاوا اسکریپتی از طریق کدهای سی‌شارپ Blazor

فرض کنید می‌خواهیم در حین کلیک بر روی دکمه‌ای مانند دکمه‌ی حذف، ابتدا تائیدیه‌ای را توسط تابع confirm جاوا اسکریپتی، از کاربر اخذ کنیم. روش انجام چنین کاری در برنامه‌های مبتنی بر Blazor به صورت زیر است:
@page "/BlazorJS"

@inject IJSRuntime JsRuntime

<h3>BlazorJS</h3>

<div class="row">
    <button class="btn btn-secondary" @onclick="TestConfirmBox">Test Confirm Button</button>
</div>
<div class="row">
    @if (ConfirmResult)
    {
        <p>Confirmation has been made!</p>
    }
    else
    {
        <p>Confirmation Pending!</p>
    }
</div>

@code {
    string ConfirmMessage = "Are you sure you want to click?";
    bool ConfirmResult;

    async Task TestConfirmBox()
    {
        ConfirmResult = await JsRuntime.InvokeAsync<bool>("confirm", ConfirmMessage);
    }
}
توضیحات:
- در اینجا می‌خواهیم تابع استاندارد confirm جاوا اسکریپتی را از طریق کدهای سی‌شارپ، با کلیک بر روی دکمه‌ی Test Confirm Button، فراخوانی کنیم. به همین جهت onclick@ این دکمه، به متد TestConfirmBox کدهای UI سی‌شارپ این کامپوننت، متصل شده‌است.
- برای دسترسی به توابع جاوا اسکریپتی، نیاز است سرویس توکار IJSRuntime را به کدهای کامپوننت تزریق کنیم که روش انجام آن‌را توسط دایرکتیو inject@ مشاهده می‌کنید. برای دسترسی به این سرویس توکار، نیاز به تنظیمات ابتدایی خاصی نیست و اینکار پیشتر انجام شده‌است.
- سرویس JsRuntime تزریق شده، دو متد مهم InvokeVoidAsync و InvokeAsync را جهت فراخوانی توابع جاوا اسکریپتی به همراه دارد. اگر تابعی، خروجی غیر void داشته باشد، باید از متد InvokeAsync استفاده کرد. برای مثال خروجی تابع استاندارد confirm، از نوع boolean است. بنابراین نوع این خروجی را به صورت یک آرگومان جنریک متد InvokeAsync مشخص کرده‌ایم.
- اولین پارامتر متد InvokeAsync، نام رشته‌ای تابع جاوا اسکریپتی است که قرار است صدا زده شود. پارامترهای اختیاری بعدی که به صورت params object?[]? args تعریف شده‌اند، لیست نامحدود آرگومان‌های ورودی این متد هستند.
- فیلد ConfirmMessage، پیامی را جهت اخذ تائید، تعریف می‌کند که به عنوان پارامتر متد confirm، توسط JsRuntime.InvokeAsync فراخوانی خواهد شد.
- فیلد ConfirmResult، نتیجه‌ی فراخوانی متد confirm جاوا اسکریپتی را به همراه دارد.
- در اینجا روش عکس العمل نشان دادن به خروجی دریافتی از متد جاوااسکریپتی را نیز مشاهده می‌کنید. پس از پایان متد TestConfirmBox که یک متد رویدادگران است، همانطور که در مطلب بررسی «چرخه‌ی حیات کامپوننت‌ها» نیز بررسی کردیم، متد StateHasChanged، در پشت صحنه فراخوانی می‌شود که سبب رندر مجدد UI خواهد شد. بنابراین در حین رندر مجدد UI، بر اساس مقدار جدید ConfirmResult دریافت شده‌ی از کاربر، با تشکیل یک if/else@، می‌توان به نتیجه‌ی تائید یا عدم تائید کاربر، واکنش نشان داد. با این توضیحات در اولین بار نمایش کامپوننت جاری چون مقدار ConfirmResult مساوی false است، پیام زیر را مشاهده می‌کنیم:


اما در ادامه با کلیک بر روی دکمه و تائید پیام ظاهر شده، عبارت زیر ظاهر می‌شود:



روش افزودن یک کتابخانه‌ی خارجی جاوا اسکریپتی به پروژه‌های Blazor

فرض کنید می‌خواهیم پیام‌های برنامه را توسط کتابخانه‌ی معروف جاوا اسکریپتی Toastr نمایش دهیم؛ با این دمو.
مرحله‌ی اول کار با این کتابخانه، دریافت فایل‌های CSS و JS آن است. برای این منظور قصد داریم از برنامه‌ی مدیریت بسته‌های LibMan استفاده کنیم:
dotnet tool install -g Microsoft.Web.LibraryManager.Cli
libman init
libman install bootstrap --provider unpkg --destination wwwroot/lib/bootstrap
libman install jquery --provider unpkg --destination wwwroot/lib/jquery
libman install toastr --provider unpkg --destination wwwroot/lib/toastr
بنابراین خط فرمان را در ریشه‌ی پروژه گشوده و پنج دستور فوق را اجرا می‌کنیم. دستور اول، ابزار خط فرمان LibMan را نصب می‌کند. دستور دوم، یک فایل libman.json خالی را در این پوشه ایجاد می‌کند و سه دستور بعدی، جی‌کوئری، بوت استرپ و toastr را دریافت و در پوشه‌ی wwwroot/lib قرار می‌دهند. Toastr برای اجرا، نیاز به jQuery نیز دارد.
البته تعاریف مداخل آن‌ها به فایل libman.json نیز اضافه می‌شوند. مزیت آن، اجرای دستور libman restore برای بازیابی و نصب مجدد تمام بسته‌های ذکر شده‌ی در فایل libman.json است.

پس از دریافت بسته‌های سمت کلاینت آن، مداخل مرتبط را به فایل Pages\_Host.cshtml برنامه‌ی Blazor Server اضافه خواهیم کرد (و یا در فایل wwwroot/index.html برنامه‌های Blazor WASM).
<head>
    <base href="~/" />
    <link rel="stylesheet" href="lib/toastr/build/toastr.min.css" />

</head>
<body>
 
    <script src="lib/jquery/dist/jquery.min.js"></script>
    <script src="lib/toastr/build/toastr.min.js"></script>
    <script src="_framework/blazor.server.js"></script>
</body>
مدخل فایل css آن‌را در قسمت head و فایل js آن‌را پیش از بسته شدن تگ body تعریف می‌کنیم. در اینجا نیازی به ذکر پوشه‌ی آغازین wwwroot نیست؛ چون base href تعریف شده، به این پوشه اشاره می‌کند.

یک نکته: می‌توان فایل csproj برنامه را به صورت زیر تغییر داد تا کار اجرای دستور libman restore را قبل از build، به صورت خودکار انجام دهد:
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <Target Name="DebugEnsureLibManEnv" BeforeTargets="BeforeBuild" Condition=" '$(Configuration)' == 'Debug' ">
    <!-- Ensure libman is installed -->
    <Exec Command="libman --version" ContinueOnError="true">
      <Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
    </Exec>
    <Error Condition="'$(ErrorCode)' != '0'" Text="libman is required to build and run this project. To continue, please run `dotnet tool install -g Microsoft.Web.LibraryManager.Cli`, and then restart your command prompt or IDE." />
    <Message Importance="high" Text="Restoring dependencies using 'libman'. This may take several minutes..." />
    <Exec WorkingDirectory="$(MSBuildProjectDirectory)" Command="libman restore" />
  </Target>
</Project>


روش فراخوانی یک کتابخانه‌ی خارجی جاوا اسکریپتی در پروژه‌های Blazor

پس از افزودن فایل‌های سمت کلاینت toastr و تعریف مداخل آن در فایل Pages\_Host.cshtml برنامه‌ی Blazor Server جاری، اکنون می‌خواهیم از این کتابخانه استفاده کنیم. یک روش کار با این نوع کتابخانه‌های عمومی و سراسری به صورت زیر است:
- ابتدا فایل خالی جدید wwwroot\js\common.js را ایجاد می‌کنیم.
- سپس تابع عمومی و سراسری ShowToastr را بر اساس امکانات کتابخانه‌ی toastr و مستندات آن، به صورت زیر ایجاد می‌کنیم:
window.ShowToastr = (type, message) => {
  // Toastr don't work with Bootstrap 4.2
  toastr.options.toastClass = "toastr"; // https://github.com/CodeSeven/toastr/issues/599

  if (type === "success") {
    toastr.success(message, "Operation Successful", { timeOut: 20000 });
  }
  if (type === "error") {
    toastr.error(message, "Operation Failed", { timeOut: 20000 });
  }
};
چون تابع ShowToastr به شیء window انتساب داده شده‌است، در سراسر برنامه‌ی جاری قابل دسترسی است.
سطر اول آن هم برای رفع عدم تداخل با بوت استرپ 4x اضافه شده‌است. بوت استرپ 4x به همراه کلاس‌های CSS مشابهی است که نیاز است با تنظیم toastClass به مقداری دیگر، این تداخل را برطرف کرد.

- در ادامه مدخل تعریف فایل wwwroot\js\common.js را به انتهای تگ body فایل Pages\_Host.cshtml اضافه می‌کنیم:
    <script src="lib/jquery/dist/jquery.min.js"></script>
    <script src="lib/toastr/build/toastr.min.js"></script>
    <script src="js/common.js"></script>
    <script src="_framework/blazor.server.js"></script>
</body>

در آخر برای آزمایش آن به کامپوننت Pages\LearnBlazor\BlazorJS.razor مراجعه کرده و تابع سراسری ShowToastr را دقیقا مانند روشی که در مورد تابع confirm بکار بردیم، توسط سرویس JsRuntime، فراخوانی می‌کنیم:
@page "/BlazorJS"

@inject IJSRuntime JsRuntime


<div class="row">
    <button class="btn btn-success" @onclick="@(()=>TestSuccess("Success Message"))">Test Toastr Success</button>
    <button class="btn btn-danger" @onclick="@(()=>TestFailure("Error Message"))">Test Toastr Failure</button>
</div>

@code {
    async Task TestSuccess(string message)
    {
        await JsRuntime.InvokeVoidAsync("ShowToastr", "success", message);
    }

    async Task TestFailure(string message)
    {
        await JsRuntime.InvokeVoidAsync("ShowToastr", "error", message);
    }
}
در اینجا دو دکمه، جهت فراخوانی متد ShowToastr با پارامترهای مختلفی تعریف شده‌اند. چون تابع ShowToastr خروجی ندارد، به همین جهت اینبار از متد InvokeVoidAsync استفاده کرده‌ایم. پارامتر اول آن، نام متد ShowToastr است. پارامتر‌های دوم و سوم آن با آرگومان‌های (type, message) تعریف شده‌ی تابع ShowToastr تطابق دارند. به علاوه در این مثال، روش ارسال پارامترها را نیز در onlick@ توسط arrow functions مشاهده می‌کنید.



کاهش کدهای تکراری فراخوانی متدهای جاوا اسکریپتی با تعریف متدهای الحاقی

می‌توان جهت کاهش تکرار کدهای استفاده از تابع ShowToastr، متدهای الحاقی زیر را برای سرویس IJSRuntime تهیه کرد:
using System.Threading.Tasks;
using Microsoft.JSInterop;

namespace BlazorServerSample.Utils
{
    public static class JSRuntimeExtensions
    {
        public static ValueTask ToastrSuccess(this IJSRuntime JSRuntime, string message)
        {
            return JSRuntime.InvokeVoidAsync("ShowToastr", "success", message);
        }

        public static ValueTask ToastrError(this IJSRuntime JSRuntime, string message)
        {
            return JSRuntime.InvokeVoidAsync("ShowToastr", "error", message);
        }
    }
}
و سپس فضای نام آن‌را به فایل Imports.razor_ معرفی نمود تا در تمام کامپوننت‌های برنامه قابل استفاده شوند.
@using BlazorServerSample.Utils
به این ترتیب به فراخوانی‌های ساده شده‌ی زیر خواهیم رسید:
async Task TestSuccess(string message)
{
   //await JsRuntime.InvokeVoidAsync("ShowToastr", "success", message);
   await JsRuntime.ToastrSuccess(message);
}


فراخوانی یک متد عمومی واقع در کامپوننت فرزند از طریق کامپوننت والد

فرض کنید در کامپوننت فرزند Pages\LearnBlazor\LearnBlazor‍Components\ChildComponent.razor که در قسمت‌های قبل آن‌را تکمیل کردیم، متد عمومی زیر تعریف شده‌است:
@inject IJSRuntime JsRuntime


@code {
    // ...

    public async Task TestSuccess(string message)
    {
        await JsRuntime.ToastrSuccess(message);
    }
}
اکنون اگر بخواهیم این متد عمومی را از طریق کامپوننت والد یا دربرگیرنده‌ی آن فراخوانی کنیم، نیاز است از مفهوم جدیدی به نام ref استفاده کرد. برای این منظور به کامپوننت Pages\LearnBlazor\ParentComponent.razor مراجعه کرده و تغییرات زیر را اعمال می‌کنیم:
@page "/ParentComponent"

<ChildComponent
    OnClickBtnMethod="ShowMessage"
    @ref="ChildComp"
    Title="This title is passed as a parameter from the Parent Component">
    <ChildContent>
        A `Render Fragment` from the parent!
    </ChildContent>
    <DangerChildContent>
        A danger content from the parent!
    </DangerChildContent>
</ChildComponent>

<div class="row">
    <button class="btn btn-success" @onclick="@(()=>ChildComp.TestSuccess("Done!"))">Show Alert</button>
</div>


@code {
    ChildComponent ChildComp;
    // ...
}
با استفاده از ref@ که به فیلد ChildComp انتساب داده شده‌است، می‌توان ارجاعی از کامپوننت فرزند را (وهله‌ای از کلاس مرتبط با آن‌را) در کامپوننت جاری بدست آورد و سپس از آن جهت فراخوانی متدهای عمومی کامپوننت فرزند استفاده کرد.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-11.zip
مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت پنجم - پیاده سازی ورود و خروج از سیستم
پس از راه اندازی IdentityServer، نوبت به امن سازی برنامه‌ی Mvc Client توسط آن می‌رسد و اولین قسمت آن، ورود به سیستم و خروج از آن می‌باشد.


بررسی اجزای Hybrid Flow

در قسمت سوم در حین «انتخاب OpenID Connect Flow مناسب برای یک برنامه‌ی کلاینت از نوع ASP.NET Core» به این نتیجه رسیدیم که Flow مناسب یک برنامه‌ی Mvc Client از نوع Hybrid است. در اینجا هر Flow، شروع به ارسال درخواستی به سمت Authorization Endpoint می‌کند؛ با یک چنین قالبی:
https://idpHostAddress/connect/authorize? 
client_id=imagegalleryclient 
&redirect_uri=https://clientapphostaddress/signin-oidcoidc 
&scope=openid profile 
&response_type=code id_token 
&response_mode=form_post
&nonce=63626...n2eNMxA0
- در سطر اول، Authorization Endpoint مشخص شده‌است. این آدرس از discovery endpoint که یک نمونه تصویر محتوای آن‌را در قسمت قبل مشاهده کردید، استخراج می‌شود.
- سپس client_id جهت تعیین برنامه‌ای که درخواست را ارسال می‌کند، ذکر شده‌است؛ از این جهت که یک IDP جهت کار با چندین نوع کلاینت مختلف طراحی شده‌است.
- redirect_uri همان Redirect Endpoint است که در سطح برنامه‌ی کلاینت تنظیم می‌شود.
- در مورد scope در قسمت قبل در حین راه اندازی IdentityServer توضیح دادیم. در اینجا برنامه‌ی کلاینت، درخواست scopeهای openid و profile را داده‌است. به این معنا که نیاز دارد تا Id کاربر وارد شده‌ی به سیستم و همچنین Claims منتسب به او را در اختیار داشته باشد.
- response_type نیز به code id_token تنظیم شده‌است. توسط response_type، نوع Flow مورد استفاده مشخص می‌شود. ذکر code به معنای بکارگیری Authorization code flow است. ذکر id_token و یا id_token token هر دو به معنای استفاده‌ی از implicit flow است. اما برای مشخص سازی Hybrid flow یکی از سه مقدار code id_token و یا code token و یا code id_token token با هم ذکر می‌شوند:


- در اینجا response_mode مشخص می‌کند که اطلاعات بازگشتی از سمت IDP که توسط response_type مشخص شده‌اند، با چه قالبی به سمت کلاینت بازگشت داده شوند که می‌تواند از طریق Form POST و یا URI باشد.


در Hybrid flow با response_type از نوع code id_token، ابتدا کلاینت یک درخواست Authentication را به Authorization Endpoint ارسال می‌کند (با همان قالب URL فوق). سپس در سطح IDP، کاربر برای مثال با ارائه‌ی کلمه‌ی عبور و نام کاربری، تعیین اعتبار می‌شود. همچنین در اینجا IDP ممکن است رضایت کاربر را از دسترسی به اطلاعات پروفایل او نیز سؤال بپرسد (تحت عنوان مفهوم Consent). سپس IDP توسط یک Redirection و یا Form POST، اطلاعات authorization code و identity token را به سمت برنامه‌ی کلاینت ارسال می‌کند. این همان اطلاعات مرتبط با response_type ای است که درخواست کرد‌ه‌ایم. سپس برنامه‌ی کلاینت این اطلاعات را تعیین اعتبار کرده و در صورت موفقیت آمیز بودن این عملیات، اکنون درخواست تولید توکن هویت را به token endpoint ارسال می‌کند. برای این منظور کلاینت سه مشخصه‌ی authorization code ،client-id و client-secret را به سمت token endpoint ارسال می‌کند. در پاسخ یک identity token را دریافت می‌کنیم. در اینجا مجددا این توکن تعیین اعتبار شده و سپس Id کاربر را از آن استخراج می‌کند که در برنامه‌ی کلاینت قابل استفاده خواهد بود. این مراحل را در تصویر زیر می‌توانید ملاحظه کنید.
البته اگر دقت کرده باشید، یک identity token در همان ابتدای کار از Authorization Endpoint دریافت می‌شود. اما چرا از آن استفاده نمی‌کنیم؟ علت اینجا است که token endpoint نیاز به اعتبارسنجی client را نیز دارد. به این ترتیب یک لایه‌ی امنیتی دیگر نیز در اینجا بکار گرفته می‌شود. همچنین access token و refresh token نیز از همین token endpoint قابل دریافت هستند.




تنظیم IdentityServer جهت انجام عملیات ورود به سیستم بر اساس جزئیات Hybrid Flow

برای افزودن قسمت لاگین به برنامه‌ی MVC قسمت دوم، نیاز است تغییراتی را در برنامه‌ی کلاینت و همچنین IDP اعمال کنیم. برای این منظور کلاس Config پروژه‌ی IDP را که در قسمت قبل ایجاد کردیم، به صورت زیر تکمیل می‌کنیم:
namespace DNT.IDP
{
    public static class Config
    {
        public static IEnumerable<Client> GetClients()
        {
            return new List<Client>
            {
                new Client
                {
                    ClientName = "Image Gallery",
                    ClientId = "imagegalleryclient",
                    AllowedGrantTypes = GrantTypes.Hybrid,
                    RedirectUris = new List<string>
                    {
                        "https://localhost:5001/signin-oidc"
                    },
                    PostLogoutRedirectUris = new List<string>
                    {
                        "https://localhost:5001/signout-callback-oidc"
                    },
                    AllowedScopes =
                    {
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile
                    },
                    ClientSecrets =
                    {
                        new Secret("secret".Sha256())
                    }
                }
             };
        }
    }
}
در اینجا بجای بازگشت لیست خالی کلاینت‌ها، یک کلاینت جدید را تعریف و تکمیل کرده‌ایم.
- ابتدا نام کلاینت را مشخص می‌کنیم. این نام و عنوان، در صفحه‌ی لاگین و Consent (رضایت دسترسی به اطلاعات پروفایل کاربر)، ظاهر می‌شود.
- همچنین نیاز است یک Id دلخواه را نیز برای آن مشخص کنیم؛ مانند imagegalleryclient در اینجا.
- AllowedGrantTypes را نیز به Hybrid Flow تنظیم کرده‌ایم. علت آن‌را در قسمت سوم این سری بررسی کردیم.
- با توجه به اینکه Hybrid Flow از Redirectها استفاده می‌کند و اطلاعات نهایی را به کلاینت از طریق Redirection ارسال می‌کند، به همین جهت آدرس RedirectUris را به آدرس برنامه‌ی Mvc Client تنظیم کرده‌ایم (که در اینجا بر روی پورت 5001 کار می‌کند). قسمت signin-oidc آن‌را در ادامه تکمیل خواهیم کرد.
- در قسمت AllowedScopes، لیست scopeهای مجاز قابل دسترسی توسط این کلاینت مشخص شده‌اند که شامل دسترسی به ID کاربر و Claims آن است.
- به ClientSecrets نیز جهت client authenticating نیاز داریم.


تنظیم برنامه‌ی MVC Client جهت انجام عملیات ورود به سیستم بر اساس جزئیات Hybrid Flow

برای افزودن قسمت لاگین به سیستم، کلاس آغازین پروژه‌ی MVC Client را به نحو زیر تکمیل می‌کنیم:
namespace ImageGallery.MvcClient.WebApp
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(options =>
            {
                options.DefaultScheme = "Cookies";
                options.DefaultChallengeScheme = "oidc";
            }).AddCookie("Cookies")
              .AddOpenIdConnect("oidc", options =>
              {
                  options.SignInScheme = "Cookies";
                  options.Authority = "https://localhost:6001";
                  options.ClientId = "imagegalleryclient";
                  options.ResponseType = "code id_token";
                  //options.CallbackPath = new PathString("...")
                  //options.SignedOutCallbackPath = new PathString("...")
                  options.Scope.Add("openid");
                  options.Scope.Add("profile");
                  options.SaveTokens = true;
                  options.ClientSecret = "secret";
                  options.GetClaimsFromUserInfoEndpoint = true;
              });
این قسمت تنظیمات، سمت کلاینت OpenID Connect Flow را مدیریت می‌کند.

- ابتدا با فراخوانی AddAuthentication، کار تنظیمات میان‌افزار استاندارد Authentication برنامه‌های ASP.NET Core انجام می‌شود. در اینجا DefaultScheme آن به Cookies تنظیم شده‌است تا عملیات Sign-in و Sign-out سمت کلاینت را میسر کند. سپس DefaultChallengeScheme به oidc تنظیم شده‌است. این مقدار با Scheme ای که در ادامه آن‌را تنظیم خواهیم کرد، تطابق دارد.

- سپس متد AddCookie فراخوانی شده‌است که authentication-Scheme را به عنوان پارامتر قبول می‌کند. به این ترتیب cookie based authentication در برنامه میسر می‌شود. پس از اعتبارسنجی توکن هویت دریافتی و تبدیل آن به Claims Identity، در یک کوکی رمزنگاری شده برای استفاده‌های بعدی ذخیره می‌شود.

- در آخر تنظیمات پروتکل OpenID Connect را ملاحظه می‌کنید. به این ترتیب مراحل اعتبارسنجی توسط این پروتکل در اینجا که Hybrid flow است، پشتیبانی خواهد شد.  اینجا است که کار درخواست Authorization، دریافت و اعتبارسنجی توکن هویت صورت می‌گیرد. اولین پارامتر آن authentication-Scheme است که به oidc تنظیم شده‌است. به این ترتیب اگر قسمتی از برنامه نیاز به Authentication داشته باشد، OpenID Connect به صورت پیش‌فرض مورد استفاده قرار می‌گیرد. به همین جهت DefaultChallengeScheme را نیز به oidc تنظیم کردیم. در اینجا SignInScheme به Cookies تنظیم شده‌است که با DefaultScheme اعتبارسنجی تطابق دارد. به این ترتیب نتیجه‌ی موفقیت آمیز عملیات اعتبارسنجی در یک کوکی رمزنگاری شده ذخیره خواهد شد. مقدار خاصیت Authority به آدرس IDP تنظیم می‌شود که بر روی پورت 6001 قرار دارد. تنظیم این مسیر سبب خواهد شد تا این میان‌افزار سمت کلاینت، به discovery endpoint دسترسی یافته و بتواند مقادیر سایر endpoints برنامه‌ی IDP را به صورت خودکار دریافت و استفاده کند. سپس ClientId تنظیم شده‌است که باید با مقدار تنظیم شده‌ی آن در سمت IDP یکی باشد و همچنین مقدار ClientSecret در اینجا نیز باید با ClientSecrets سمت IDP یکی باشد. ResponseType تنظیم شده‌ی در اینجا با AllowedGrantTypes سمت IDP تطابق دارد که از نوع Hybrid است. سپس دو scope درخواستی توسط این برنامه‌ی کلاینت که openid و profile هستند در اینجا اضافه شده‌اند. به این ترتیب می‌توان به مقادیر Id کاربر و claims او دسترسی داشت. مقدار CallbackPath در اینجا به RedirectUris سمت IDP اشاره می‌کند که مقدار پیش‌فرض آن همان signin-oidc است. با تنظیم SaveTokens به true امکان استفاده‌ی مجدد از آن‌ها را میسر می‌کند.

پس از تکمیل قسمت ConfigureServices و انجام تنظیمات میان‌افزار اعتبارسنجی، نیاز است این میان‌افزار را نیز به برنامه افزود که توسط متد UseAuthentication انجام می‌شود:
namespace ImageGallery.MvcClient.WebApp
{
    public class Startup
    {
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseAuthentication();

پس از این تنظیمات، با اعمال ویژگی Authorize، دسترسی به کنترلر گالری برنامه‌ی MVC Client را صرفا محدود به کاربران وارد شده‌ی به سیستم می‌کنیم:
namespace ImageGallery.MvcClient.WebApp.Controllers
{
    [Authorize]
    public class GalleryController : Controller
    {
    // .... 
   
        public async Task WriteOutIdentityInformation()
        {
            var identityToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.IdToken);
            Debug.WriteLine($"Identity token: {identityToken}");

            foreach (var claim in User.Claims)
            {
                Debug.WriteLine($"Claim type: {claim.Type} - Claim value: {claim.Value}");
            }
        }
در اینجا علاوه بر اعمال فیلتر Authorize به کل اکشن متدهای این کنترلر، یک اکشن متد جدید دیگر را نیز به انتهای آن اضافه کرده‌ایم تا صرفا جهت دیباگ برنامه، اطلاعات دریافتی از IDP را در Debug Window، برای بررسی بیشتر درج کند. البته این روش با Debug Window مخصوص Visual Studio کار می‌کند. اگر می‌خواهید آن‌را در صفحه‌ی کنسول dotnet run مشاهده کنید، بجای Debug باید از ILogger استفاده کرد.

فراخوانی متد GetTokenAsync با پارامتر IdToken، همان Identity token دریافتی از IDP را بازگشت می‌دهد. این توکن با تنظیم SaveTokens به true در تنظیمات AddOpenIdConnect که پیشتر انجام دادیم، قابل استخراج از کوکی اعتبارسنجی برنامه شده‌است.
این متد را در ابتدای اکشن متد Index فراخوانی می‌کنیم:
        public async Task<IActionResult> Index()
        {
            await WriteOutIdentityInformation();
            // ....


اجرای برنامه جهت آزمایش تنظیمات انجام شده

برای اجرای برنامه:
- ابتدا به پوشه‌ی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشه‌ی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آن‌را اجرا کنید تا برنامه‌ی IDP راه اندازی شود.
- در آخر به پوشه‌ی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا MVC Client راه اندازی شود.

اکنون که هر سه برنامه با هم در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید:


در این حالت چون فیلتر Authorize به کل اکشن متدهای کنترلر گالری اعمال شده، میان‌افزار Authentication که در فایل آغازین برنامه‌ی کلاینت MVC تنظیم شده‌است، وارد عمل شده و کاربر را به صفحه‌ی لاگین سمت IDP هدایت می‌کند (شماره پورت آن 6001 است). لاگ این اعمال را هم در برگه‌ی network مرورگر می‌تواند مشاهده کنید.

در اینجا نام کاربری و کلمه‌ی عبور اولین کاربر تعریف شده‌ی در فایل Config.cs برنامه‌ی IDP را که User 1 و password است، وارد می‌کنیم. پس از آن صفحه‌ی Consent ظاهر می‌شود:


در اینجا از کاربر سؤال می‌پرسد که آیا به برنامه‌ی کلاینت اجازه می‌دهید تا به Id و اطلاعات پروفایل و یا همان Claims شما دسترسی پیدا کند؟
فعلا گزینه‌ی remember my design را انتخاب نکنید تا همواره بتوان این صفحه را در دفعات بعدی نیز مشاهده کرد. سپس بر روی گزینه‌ی Yes, Allow کلیک کنید.
اکنون به صورت خودکار به سمت برنامه‌ی MVC Client هدایت شده و می‌توانیم اطلاعات صفحه‌ی اول سایت را کاملا مشاهده کنیم (چون کاربر اعتبارسنجی شده‌است، از فیلتر Authorize رد خواهد شد).


همچنین در اینجا اطلاعات زیادی نیز جهت دیباگ برنامه لاگ می‌شوند که در آینده جهت عیب یابی آن می‌توانند بسیار مفید باشند:


با دنبال کردن این لاگ می‌توانید مراحل Hybrid Flow را مرحله به مرحله با مشاهده‌ی ریز جزئیات آن بررسی کنید. این مراحل به صورت خودکار توسط میان‌افزار Authentication انجام می‌شوند و در نهایت اطلاعات توکن‌های دریافتی به صورت خودکار در اختیار برنامه برای استفاده قرار می‌گیرند. یعنی هم اکنون کوکی رمزنگاری شده‌ی اطلاعات اعتبارسنجی کاربر در دسترس است و به اطلاعات آن می‌توان توسط شیء this.User، در اکشن متدهای برنامه‌ی MVC، دسترسی داشت.


تنظیم برنامه‌ی MVC Client جهت انجام عملیات خروج از سیستم

ابتدا نیاز است یک لینک خروج از سیستم را به برنامه‌ی کلاینت اضافه کنیم. برای این منظور به فایل Views\Shared\_Layout.cshtml مراجعه کرده و لینک logout را در صورت IsAuthenticated بودن کاربر جاری وارد شده‌ی به سیستم، نمایش می‌دهیم:
<div class="navbar-collapse collapse">
    <ul class="nav navbar-nav">
        <li><a asp-area="" asp-controller="Gallery" asp-action="Index">Home</a></li>
        <li><a asp-area="" asp-controller="Gallery" asp-action="AddImage">Add an image</a></li>
        @if (User.Identity.IsAuthenticated)
        {
            <li><a asp-area="" asp-controller="Gallery" asp-action="Logout">Logout</a></li>
        }
    </ul>
</div>


شیء this.User، هم در اکشن متدها و هم در Viewهای برنامه، جهت دسترسی به اطلاعات کاربر اعتبارسنجی شده، در دسترس است.
این لینک به اکشن متد Logout، در کنترلر گالری اشاره می‌کند که آن‌را به صورت زیر تکمیل خواهیم کرد:
namespace ImageGallery.MvcClient.WebApp.Controllers
{
    [Authorize]
    public class GalleryController : Controller
    {
        public async Task Logout()
        {
            // Clears the  local cookie ("Cookies" must match the name of the scheme)
            await HttpContext.SignOutAsync("Cookies");
            await HttpContext.SignOutAsync("oidc");
        }
در اینجا ابتدا کوکی Authentication حذف می‌شود. نامی که در اینجا انتخاب می‌شود باید با نام scheme انتخابی مرتبط در فایل آغازین برنامه یکی باشد.
سپس نیاز است از برنامه‌ی IDP نیز logout شویم. به همین جهت سطر دوم SignOutAsync با پارامتر oidc را مشاهده می‌کنید. بدون وجود این سطر، کاربر فقط از برنامه‌ی کلاینت logout می‌شود؛ اما اگر به IDP مجددا هدایت شود، مشاهده خواهد کرد که در آن سمت، هنوز نام کاربری او توسط IDP شناسایی می‌شود.


بهبود تجربه‌ی کاربری Logout

پس از logout، بدون انجام یکسری از تنظیمات، کاربر مجددا به برنامه‌ی کلاینت به صورت خودکار هدایت نخواهد شد و در همان سمت IDP متوقف می‌شد. برای بهبود این وضعیت و بازگشت مجدد به برنامه‌ی کلاینت، اینکار را یا توسط مقدار دهی خاصیت SignedOutCallbackPath مربوط به متد AddOpenIdConnect می‌توان انجام داد و یا بهتر است مقدار پیش‌فرض آن‌را به تنظیمات IDP نسبت داد که پیشتر در تنظیمات متد GetClients آن‌را ذکر کرده بودیم:
PostLogoutRedirectUris = new List<string>
{
     "https://localhost:5001/signout-callback-oidc"
},
با وجود این تنظیم، اکنون IDP می‌داند که پس از logout، چه آدرسی را باید به کاربر جهت بازگشت به سیستم قبلی ارائه دهد:


البته هنوز یک مرحله‌ی انتخاب و کلیک بر روی لینک بازگشت وجود دارد. برای حذف آن و خودکار کردن Redirect نهایی آن، می‌توان کدهای IdentityServer4.Quickstart.UI را که در قسمت قبل به برنامه‌ی IDP اضافه کردیم، اندکی تغییر دهیم. برای این منظور فایل src\IDP\DNT.IDP\Quickstart\Account\AccountOptions.cs را گشوده و سپس فیلد AutomaticRedirectAfterSignOut را که false است، به true تغییر دهید.

 
تنظیمات بازگشت Claims کاربر به برنامه‌ی کلاینت

به صورت پیش‌فرض، Identity Server اطلاعات Claims کاربر را ارسال نمی‌کند و Identity token صرفا به همراه اطلاعات Id کاربر است. برای تنظیم آن می‌توان در سمت تنظیمات IDP، در متد GetClients، زمانیکه new Client صورت می‌گیرد، خاصیت AlwaysIncludeUserClaimsInIdToken هر کلاینت را به true تنظیم کرد؛ اما ایده خوبی نیست. Identity token از طریق Authorization endpoint دریافت می‌شود. در اینجا اگر این اطلاعات از طریق URI دریافت شود و Claims به Identity token افزوده شوند، به مشکل بیش از حد طولانی شدن URL نهایی خواهیم رسید و ممکن است از طرف وب سرور یک چنین درخواستی برگشت بخورد. به همین جهت به صورت پیش‌فرض اطلاعات Claims به Identity token اضافه نمی‌شوند.
در اینجا برای دریافت Claims، یک endpoint دیگر در IDP به نام UserInfo endpoint درنظر گرفته شده‌است. در این حالت برنامه‌ی کلاینت، مقدار Access token دریافتی را که به همراه اطلاعات scopes متناظر با Claims است، به سمت UserInfo endpoint ارسال می‌کند. باید دقت داشت زمانیکه Identity token دوم از Token endpoint دریافت می‌شود (تصویر ابتدای بحث)، به همراه آن یک Access token نیز صادر و ارسال می‌گردد. اینجا است که میان‌افزار oidc، این توکن دسترسی را به سمت UserInfo endpoint ارسال می‌کند تا user claims را دریافت کند:


در تنظیمات سمت کلاینت AddOpenIdConnect، درخواست openid و profile، یعنی درخواست Id کاربر و Claims آن وجود دارند:
options.Scope.Add("openid");
options.Scope.Add("profile");
برای بازگشت آن‌ها به سمت کلاینت، درخواست دریافت claims از UserInfo Endpoint را در سمت کلاینت تنظیم می‌کنیم:
options.GetClaimsFromUserInfoEndpoint = true;
همین اندازه تنظیم میان‌افزار oidc، برای انجام خودکار کل گردش کاری یاد شده کافی است.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشه‌ی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشه‌ی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آن‌را اجرا کنید تا برنامه‌ی IDP راه اندازی شود.
- در آخر به پوشه‌ی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه با هم در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحه‌ی login نام کاربری را User 1 و کلمه‌ی عبور آن‌را password وارد کنید.
مطالب
چگونه کد قابل تست بنویسیم - قسمت دوم و پایانی
گام 3 – از بین بردن ارتباط لایه‌ها (Loose Coupling)
بجای استفاده از اشیاء واقعی، براساس interface‌ها برنامه نویسی کنید. اگر شما کد خود را با استفاده از IShoppingCartService  به عنوان یک interface بجای استفاده از شیء واقعی ShoppingCartService نوشته باشید، زمانیکه تست را مینویسید، میتوانید یک سرویس کارت خرید جعلی (mocking) که IShoppingCartService را پیاده سازی کرده جایگزین شیء اصلی نمایید. در کد زیر، توجه کنید تنها تغییر این است که متغیر عضو اکنون از نوع IShoppingCartService  است بجای ShoppingCartService .
public interface IShoppingCartService
{
    ShoppingCart GetContents();
    ShoppingCart AddItemToCart(int itemId, int quantity);
}
public class ShoppingCartService : IShoppingCartService
{
    public ShoppingCart GetContents()
    {
        throw new NotImplementedException("Get cart from Persistence Layer");
    }
    public ShoppingCart AddItemToCart(int itemId, int quantity)
    {
        throw new NotImplementedException("Add Item to cart then return updated cart");
    }
}
public class ShoppingCart
{
    public List<product> Items { get; set; }
}
public class Product
{
    public int ItemId { get; set; }
    public string ItemName { get; set; }
}
public class ShoppingCartController : Controller
{
    //Concrete object below points to actual service
    //private ShoppingCartService _shoppingCartService;
    //loosely coupled code below uses the interface rather than the 
    //concrete object
    private IShoppingCartService _shoppingCartService;
    public ShoppingCartController()
    {
        _shoppingCartService = new ShoppingCartService();
    }
    public ActionResult GetCart()
    {
        //now using the shared instance of the shoppingCartService dependency
        ShoppingCart cart = _shoppingCartService.GetContents();
        return View("Cart", cart);
    }
    public ActionResult AddItemToCart(int itemId, int quantity)
    {
        //now using the shared instance of the shoppingCartService dependency
        ShoppingCart cart = _shoppingCartService.AddItemToCart(itemId, quantity);
        return View("Cart", cart);
    }
}

گام 4 – وابستگی‌ها را تزریق کنید
اکنون ما تمام وابستگی‌ها را در یک جا مرکزیت داده‌ایم و کد ما رابطه کمی با آن وابستگی‌ها دارد. همانند گذشته، چندین راه برای پیاده سازی این گام وجود دارد. بدون استفاده از ابزارهای کمکی برای این مفهوم، ساده‌ترین راه دوباره نویسی (Overload) متد ایجاد کننده است:
//loosely coupled code below uses the interface rather 
//than the concrete object
private IShoppingCartService _shoppingCartService;
//MVC uses this constructor 
public ShoppingCartController()
{
    _shoppingCartService = new ShoppingCartService();
}
//You can use this constructor when testing to inject the    
//ShoppingCartService dependency
public ShoppingCartController(IShoppingCartService shoppingCartService)
{
    _shoppingCartService = shoppingCartService;
}

گام 5 – تست را با استفاده از یک شیء جعلی (Mocking) انجام دهید
مثالی از یک سناریوی تست ممکن در زیر آمده است. توجه کنید که یک شیء جعلی از نوع کلاس ShoppingCartService ساخته‌ایم. این شیء جعلی فرستاده می‌شود به شیء Controller و متد GetContents پیاده سازی میشود تا بجای آنکه کد اصلی را که به منبع داده مراجعه می‌کند اجرا نماید، داده‌های جعلی و شبیه سازی شده را برگرداند. بدلیل آنکه تمام کد را نوشته ایم، بسیار سریعتر از اجرای کوئری بر روی دیتابیس اجرا خواهد شد و دیگر نگرانی بابت تهیه داده تستی و یا تصحیح داده بعد از اتمام تست (بازگرداندن داده به حالت قبل از تست) نخواهم داشت. توجه داشته باشید که بدلیل مرکزیت دادن به وابستگی‌ها در گام 2 ، تنها باید یکبار آنرا تزریق نماییم و بخاطر کاری که در گام 3 انجام شد، وابستگی ما به حدی پایین آمده که میتوانیم هر شیء‌ایی  را  (جعلی و یا حقیقی) ارسال کنیم و فقط کافیست شیء مورد نظر IShoppingCartService را پیاده سازی کرده باشد.
[TestClass]
public class ShoppingCartControllerTests
{
    [TestMethod]
    public void GetCartSmokeTest()
    {
        //arrange
        ShoppingCartController controller = 
           new ShoppingCartController(new ShoppingCartServiceStub());
        // Act
        ActionResult result = controller.GetCart();
        // Assert
        Assert.IsInstanceOfType(result, typeof(ViewResult));
    }
}
/// <summary>
/// This is is a stub of the ShoppingCartService
/// </summary>
public class ShoppingCartServiceStub : IShoppingCartService
{
    public ShoppingCart GetContents()
    {
        return new ShoppingCart
        {
            Items = new List<product> {
                new Product {ItemId = 1, ItemName = "Widget"}
            }
        };
    }
    public ShoppingCart AddItemToCart(int itemId, int quantity)
    {
        throw new NotImplementedException();
    }
}

مطالب تکمیلی
از یک ابزار کنترل وابستگی (IoC/DI) استفاده کنید:
از رایج‌ترین و عمومی‌ترین ابزارهای کنترل وابستگی برای .Net می‌توان به StructureMap و CastleWindsor اشاره کرد. در کد نویسی واقعی، شما وابستگی‌های بسیاری خواهید داشت، که این وابستگی‌ها هم وابستگی هایی دارند که به سرعت از مدیریت شما خارج خواهند شد. راه حل این مشکل استفاده از یک ابزار کنترل وابستگی خواهد بود.
از یک چارچوب تجزیه (Isolation Framework) استفاده نمایید:
برای ایجاد اشیاء جعلی ممکن است کار زیادی لازم باشد و  استفاده از یک Isolation Framework میتواند زمان و میزان کد نویسی شمارا کم کند. از رایج‌ترین این ابزارها میتوان Rhino Mocks  و Moq را نام برد.
مطالب
ارسال خطاهای رخ‌داده‌ی در برنامه‌های سمت کلاینت Blazor WASM، به تلگرام
هر زمانیکه در سمت کلاینت، استثناء یا خطایی رخ می‌دهد، کاربر با نوار زرد رنگی در پایین صفحه، از آن مطلع می‌شود؛ اما برنامه نویس چطور؟! به همین جهت در این مطلب قصد داریم تمام خطاهای رخ داده‌ی در برنامه‌ی سمت کلاینت را لاگ کرده و به سرور تلگرام ارسال کنیم. مزیت کار کردن با تلگرام، دسترسی به سروری است که تقریبا همواره در دسترس است و برخلاف بانک اطلاعاتی برنامه که ممکن است در لحظه‌ی بروز خطا، خودش سبب ساز اصلی باشد و قادر به ثبت اطلاعات خطاهای رسیده‌ی از سمت کلاینت نباشد، چنین مشکلی را با تلگرام نداریم (مانند همان جمله‌ی معروف: «بک‌آپ سروری که روی همان سرور گرفته می‌شود، بک آپ نام ندارد!»). همچنین بررسی و حذف گزارش‌های رسیده‌ی به آن نیز بسیار ساده‌است و می‌توان این گزارش‌ها را مستقل از سرور برنامه و از طریق وسایل مختلفی مانند گوشی‌های همراه، تبلت‌ها و غیره نیز بررسی کرد.




نحوه‌ی نمایش خطاها در برنامه‌های Blazor

در حین توسعه‌ی برنامه‌های Blazor، اگر استثنائی رخ دهد، نوار زرد رنگی در پایین صفحه، ظاهر می‌شود که امکان هدایت توسعه دهنده را به کنسول مرورگر، برای مشاهده‌ی جزئیات بیشتر آن خطا را دارد. در حالت توزیع برنامه، این نوار زرد رنگ تنها به ذکر خطایی رخ داده‌است اکتفا کرده و گزینه‌ی راه اندازی مجدد برنامه را با ریفرش کردن مرورگر، پیشنهاد می‌دهد. سفارشی سازی آن هم در فایل wwwroot/index.html در قسمت زیر صورت می‌گیرد:
<div id="blazor-error-ui">
    An unhandled error has occurred.
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>
که شیوه نامه‌های پیش‌فرض آن در فایل wwwroot/css/app.css قرار دارند. در حالت عادی المان blazor-error-ui به همراه یک display: none است که از نمایش آن جلوگیری می‌کند. اما در زمان بروز خطایی، فریم‌ورک آن‌را به صورت display: block نمایش می‌دهد.


نحوه‌ی مدیریت استثناءها در برنامه‌های Blazor

توصیه شده‌است که کار مدیریت استثناءها باید توسط توسعه دهنده صورت گیرد و بهتر است جزئیات آن‌ها و یا stack-trace آن‌ها را به کاربر نمایش نداد؛ تا مبادا اطلاعات حساسی فاش شوند و یا کاربر مهاجم بتواند توسط آن‌ها اطلاعات ارزشمندی را از نحوه‌ی عملکرد برنامه بدست آورد.
برخلاف برنامه‌های ASP.NET Core که دارای یک middleware pipeline هستند و برای مثال توسط آن‌ها می‌توان مدیریت سراسری خطاهای رخ‌داده را انجام داد، چنین ویژگی در برنامه‌های Blazor وجود ندارد؛ چون در اینجا مرورگر است که هاست برنامه بوده و processing pipeline آن‌را تشکیل می‌دهد.
اما ... اگر استثنائی مدیریت نشده در یک برنامه‌ی Blazor رخ‌دهد، این استثناء در ابتدا توسط یک ILogger، لاگ شده و سپس در کنسول مرورگر نمایش داده می‌شود. در اینجا Console Logging Provider، تامین کننده‌ی پیش‌فرض سیستم ثبت وقایع برنامه‌های Blazor است. به همین جهت استثناءهای مدیریت نشده‌ی برنامه را می‌توان در کنسول توسعه دهندگان مرورگر نیز مشاهده کرد. برای مثال اگر سطح لاگ ارائه شده LogLevel.Error باشد، به صورت خودکار به معادل console.error ترجمه می‌شود.
بنابراین اگر در برنامه‌ی Blazor جاری یک ILoggerProvider سفارشی را تهیه و آن‌را به سیستم تزریق وابستگی‌های برنامه معرفی کنیم، می‌توان از تمام وقایع سیستم (هر قسمتی از آن که از ILogger استفاده می‌کند)، منجمله تمام خطاهای رخ‌داده (و مدیریت نشده) مطلع شد و برای مثال آن‌ها را به سمت Web API برنامه، جهت ثبت در بانک اطلاعاتی و یا نمایش در برنامه‌ی تلگرام، ارسال کرد و این دقیقا همان کاری است که قصد داریم در ادامه انجام دهیم.


نوشتن یک ILoggerProvider سفارشی جهت ارسال رخ‌دادها برنامه‌ی سمت کلاینت، به یک Web API

برای ارسال تمام وقایع برنامه‌ی کلاینت به سمت سرور، نیاز است یک ILoggerProvider سفارشی را تهیه کنیم که شروع آن به صورت زیر است:
using System;
using System.Net.Http;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace BlazorWasmTelegramLogger.Client.Logging
{
    public class ClientLoggerProvider : ILoggerProvider
    {
        private readonly HttpClient _httpClient;
        private readonly WebApiLoggerOptions _options;
        private readonly NavigationManager _navigationManager;

        public ClientLoggerProvider(
                IServiceProvider serviceProvider,
                IOptions<WebApiLoggerOptions> options,
                NavigationManager navigationManager)
        {
            if (serviceProvider is null)
            {
                throw new ArgumentNullException(nameof(serviceProvider));
            }

            if (options is null)
            {
                throw new ArgumentNullException(nameof(options));
            }

            _httpClient = serviceProvider.CreateScope().ServiceProvider.GetRequiredService<HttpClient>();
            _options = options.Value;
            _navigationManager = navigationManager ?? throw new ArgumentNullException(nameof(navigationManager));
        }

        public ILogger CreateLogger(string categoryName)
        {
            return new WebApiLogger(_httpClient, _options, _navigationManager);
        }

        public void Dispose()
        {
        }
    }
}
توضیحات:
زمانیکه قرار است یک لاگر سفارشی را به سیستم تزریق وابستگی‌های برنامه معرفی کنیم، روش آن به صورت زیر است:
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace BlazorWasmTelegramLogger.Client.Logging
{
    public static class ClientLoggerProviderExtensions
    {
        public static ILoggingBuilder AddWebApiLogger(this ILoggingBuilder builder)
        {
            if (builder == null)
            {
                throw new ArgumentNullException(nameof(builder));
            }

            builder.Services.AddSingleton<ILoggerProvider, ClientLoggerProvider>();
            return builder;
        }
    }
}
باید کلاسی را داشته باشیم مانند ClientLoggerProvider که یک ILoggerProvider را پیاده سازی می‌کند و نحوه‌ی ثبت آن نیز باید حتما Singleton باشد. مزیت معرفی ILoggerProvider به این نحو، امکان دسترسی به سرویس‌های برنامه در سازنده‌ی کلاس ClientLoggerProvider است و در این حالت دیگر نیاز به نوشتن new ClientLoggerProvider نبوده و خود سیستم تزریق وابستگی‌ها، سازنده‌های ClientLoggerProvider را تامین می‌کند.
در کلاس ClientLoggerProvider فوق، سه وابستگی تزریق شده را مشاهده می‌کنید:
public ClientLoggerProvider(
                IServiceProvider serviceProvider,
                IOptions<WebApiLoggerOptions> options,
                NavigationManager navigationManager)
با استفاده از IServiceProvider می‌توان به HttpClient برنامه دسترسی یافت. از این جهت که چون HttpClient به صورت پیش‌فرض با طول عمر Scoped به سیستم معرفی شده، امکان تزریق مستقیم آن به سازنده‌ی یک ILoggerProvider از نوع Singleton وجود ندارد. به همین جهت از IServiceProvider برای تامین آن استفاده خواهیم کرد. مابقی موارد مانند IOptions که تنظیمات این لاگر را فراهم می‌کند و یا NavigationManager استاندارد برنامه که امکان دسترسی به Url جاری را میسر می‌کند، به صورت پیش‌فرض دارای طول عمر Singleton هستند و می‌توان آن‌ها را بدون مشکل، به سازنده‌ی لاگر سفارشی، تزریق کرد.
مهم‌ترین قسمت ILoggerProvider سفارشی، متد CreateLogger آن است که یک ILogger را بازگشت می‌دهد:
public ILogger CreateLogger(string categoryName)
{
   return new WebApiLogger(_httpClient, _options, _navigationManager);
}
بنابراین در ادامه نیاز است، یک ILogger سفارشی را نیز پیاده سازی کنیم:
using System;
using System.Net.Http;
using System.Net.Http.Json;
using BlazorWasmTelegramLogger.Shared;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;

namespace BlazorWasmTelegramLogger.Client.Logging
{
    public class WebApiLogger : ILogger
    {
        private readonly WebApiLoggerOptions _options;
        private readonly HttpClient _httpClient;
        private readonly NavigationManager _navigationManager;

        public WebApiLogger(HttpClient httpClient, WebApiLoggerOptions options, NavigationManager navigationManager)
        {
            _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
            _options = options ?? throw new ArgumentNullException(nameof(options));
            _navigationManager = navigationManager ?? throw new ArgumentNullException(nameof(navigationManager));
        }

        public IDisposable BeginScope<TState>(TState state) => default;

        public bool IsEnabled(LogLevel logLevel) => logLevel >= _options.LogLevel;

        public void Log<TState>(
            LogLevel logLevel,
            EventId eventId,
            TState state,
            Exception exception,
            Func<TState, Exception, string> formatter)
        {
            if (!IsEnabled(logLevel))
            {
                return;
            }

            if (formatter is null)
            {
                throw new ArgumentNullException(nameof(formatter));
            }

            try
            {
                ClientLog log = new()
                {
                    LogLevel = logLevel,
                    EventId = eventId,
                    Message = formatter(state, exception),
                    Exception = exception?.Message,
                    StackTrace = exception?.StackTrace,
                    Url = _navigationManager.Uri
                };
                _httpClient.PostAsJsonAsync(_options.LoggerEndpointUrl, log);
            }
            catch
            {
                // don't throw exceptions from the logger
            }
        }
    }
}
نحوه‌ی عملکرد این ILogger سفارشی بسیار ساده‌است:
- متد IsEnabled آن مشخص می‌کند که چه سطحی از رخ‌دادهای سیستم را باید لاگ کند. این سطح را نیز از تنظیمات برنامه دریافت می‌کند:
using Microsoft.Extensions.Logging;

namespace BlazorWasmTelegramLogger.Client.Logging
{
    public class WebApiLoggerOptions
    {
        public string LoggerEndpointUrl { set; get; }

        public LogLevel LogLevel { get; set; } = LogLevel.Information;
    }
}
در این تنظیمات مشخص می‌کنیم که Url مربوط به اکشن متد Web API ما که قرار است اطلاعات به سمت آن ارسال شوند، چیست؟ همچنین حداقل سطح لاگ مدنظر را نیز باید مشخص کنیم. اطلاعات آن توسط فایل Client\wwwroot\appsettings.json با این محتوای فرضی قابل تنظیم است:
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "WebApiLogger": {
    "LogLevel": "Warning",
    "LoggerEndpointUrl": "/api/logs"
  }
}
و همچنین باید کلاس WebApiLoggerOptions را به نحو زیر در کلاس Program برنامه به سیستم تزریق وابستگی‌ها، معرفی کرد تا <IOptions<WebApiLoggerOptions قابلیت تزریق به سازنده‌ی تامین کننده‌ی لاگر را پیدا کند:
namespace BlazorWasmTelegramLogger.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("#app");

            builder.Services.Configure<WebApiLoggerOptions>(options => builder.Configuration.GetSection("WebApiLogger").Bind(options));
            // …
        }
    }
}
- متد لاگ این لاگر سفارشی، پیام نهایی قابل ارسال به سمت Web API را تشکیل داده و توسط متد httpClient.PostAsJsonAsync آن‌را ارسال می‌کند. به همین جهت ساختار لاگ مدنظر را در فایل Shared\ClientLog.cs به صورت زیر تعریف کرده‌ایم که بین برنامه‌ی کلاینت و سرور، مشترک است:
using Microsoft.Extensions.Logging;

namespace BlazorWasmTelegramLogger.Shared
{
    public class ClientLog
    {
        public LogLevel LogLevel { get; set; }

        public EventId EventId { get; set; }

        public string Message { get; set; }

        public string Exception { get; set; }

        public string StackTrace { get; set; }

        public string Url { get; set; }
    }
}
این اطلاعاتی است که کلاینت به ازای رخ‌دادی خاص، جمع آوری کرده و به سمت سرور ارسال می‌کند.

در آخر هم کار ثبت متد ()AddWebApiLogger که معرفی ILoggerProvider سفارشی ما را انجام می‌دهد، به صورت زیر خواهد بود:
namespace BlazorWasmTelegramLogger.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("#app");

            builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

            builder.Services.Configure<WebApiLoggerOptions>(options => builder.Configuration.GetSection("WebApiLogger").Bind(options));
            builder.Services.AddLogging(configure =>
            {
                configure.AddWebApiLogger();
            });

            await builder.Build().RunAsync();
        }
    }
}
تا اینجا اگر هر نوع استثنای مدیریت نشده‌ای در برنامه‌ی Blazor WASM رخ دهد، چون سطح لاگ آن بالاتر از Warning تنظیم شده‌ی در فایل Client\wwwroot\appsettings.json است:
public bool IsEnabled(LogLevel logLevel) => logLevel >= _options.LogLevel;
به صورت خودکار به سمت کنترلر api/logs ارسال خواهد شد. بنابراین مرحله‌ی بعدی، تکمیل کنترلر یاد شده‌است.


ایجاد سرویسی برای ارسال لاگ‌های برنامه به سمت تلگرام

پیش از اینکه کار تکمیل کنترلر api/logs را در برنامه‌ی Web API انجام دهیم، ابتدا در همان برنامه‌ی Web API، سرویسی را برای ارسال لاگ‌های رسیده به سمت تلگرام، تهیه می‌کنیم. علت اینکه این قسمت را به برنامه‌ی سمت سرور محول کرده‌ایم، شامل موارد زیر است:
- درست است که می‌توان کتابخانه‌های مرتبط با تلگرام را به برنامه‌ی سی‌شارپی Blazor خود اضافه کرد، اما هر وابستگی سمت کلاینتی، سبب حجیم‌تر شدن توزیع نهایی برنامه خواهد شد که مطلوب نیست.
- برای کار با تلگرام نیاز است توکن اتصال به آن‌را در یک محل امن، نگهداری کرد. قرار دادن این نوع اطلاعات حساس، در برنامه‌ی سمت کلاینتی که تمام اجزای آن از مرورگر قابل استخراج و بررسی است، کار اشتباهی است.
- ارسال اطلاعات لاگ برنامه‌ی سمت کلاینت به Web API، مزیت لاگ سمت سرور آن‌را مانند ثبت در یک فایل محلی، ثبت در بانک اطلاعاتی و غیره را نیز میسر می‌کند و صرفا محدود به تلگرام نیست.

برای ارسال اطلاعات به تلگرام، سرویس سمت سرور زیر را تهیه می‌کنیم:
using System;
using System.Text;
using System.Threading.Tasks;
using BlazorWasmTelegramLogger.Shared;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Telegram.Bot;
using Telegram.Bot.Types.Enums;

namespace BlazorWasmTelegramLogger.Server.Services
{
    public class TelegramLoggingBotOptions
    {
        public string AccessToken { get; set; }
        public string ChatId { get; set; }
    }

    public interface ITelegramBotService
    {
        Task SendLogAsync(ClientLog log);
    }

    public class TelegramBotService : ITelegramBotService
    {
        private readonly string _chatId;
        private readonly TelegramBotClient _client;

        public TelegramBotService(IOptions<TelegramLoggingBotOptions> options)
        {
            _chatId = options.Value.ChatId;
            _client = new TelegramBotClient(options.Value.AccessToken);
        }

        public async Task SendLogAsync(ClientLog log)
        {
            var text = formatMessage(log);
            if (string.IsNullOrWhiteSpace(text))
            {
                return;
            }

            await _client.SendTextMessageAsync(_chatId, text, ParseMode.Markdown);
        }

        private static string formatMessage(ClientLog log)
        {
            if (string.IsNullOrWhiteSpace(log.Message))
            {
                return string.Empty;
            }

            var sb = new StringBuilder();
            sb.Append(toEmoji(log.LogLevel))
                .Append(" *")
                .AppendFormat("{0:hh:mm:ss}", DateTime.Now)
                .Append("* ")
                .AppendLine(log.Message);

            if (!string.IsNullOrWhiteSpace(log.Exception))
            {
                sb.AppendLine()
                    .Append('`')
                    .AppendLine(log.Exception)
                    .AppendLine(log.StackTrace)
                    .AppendLine("`")
                    .AppendLine();
            }

            sb.Append("*Url:* ").AppendLine(log.Url);
            return sb.ToString();
        }

        private static string toEmoji(LogLevel level) =>
            level switch
            {
                LogLevel.Trace => "⬜️",
                LogLevel.Debug => "🟦",
                LogLevel.Information => "⬛️️️",
                LogLevel.Warning => "🟧",
                LogLevel.Error => "🟥",
                LogLevel.Critical => "❌",
                LogLevel.None => "🔳",
                _ => throw new ArgumentOutOfRangeException(nameof(level), level, null)
            };
    }
}
توضیحات:
- برای کار با API تلگرام، از کتابخانه‌ی معروف Telegram.Bot استفاده کرده‌ایم که به صورت زیر، وابستگی آن به برنامه‌ی Web API اضافه می‌شود:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <ItemGroup>
    <PackageReference Include="Telegram.Bot" Version="15.7.1" />
  </ItemGroup>
</Project>
- این سرویس برای کار کردن، نیاز به تنظیمات زیر را دارد:
    public class TelegramLoggingBotOptions
    {
        public string AccessToken { get; set; }
        public string ChatId { get; set; }
    }
- برای دریافت AccessToken، در برنامه‌ی تلگرام خود، بات مخصوصی را به نام https://t.me/botfather یافته و سپس آن‌را استارت کنید:


پس از شروع این بات، ابتدا دستور newbot/ را صادر کنید. سپس یک نام را از شما می‌پرسد. نام دلخواهی را وارد کنید. در ادامه یک نام منحصربفرد را جهت شناسایی این بات خواهد پرسید. پس از دریافت آن، توکن خود را همانند تصویر فوق، مشاهده می‌کنید.

- مرحله‌ی بعد تنظیم ChatId است. نحوه‌ی کار برنامه به این صورت است که پیام‌ها را به این بات سفارشی خود ارسال کرده و این بات، آن‌ها را به کانال اختصاصی ما هدایت می‌کند. بنابراین یک کانال جدید را ایجاد کنید. ترجیحا بهتر است این کانال خصوصی باشد. سپس کاربر test_2021_logs_bot@ (همان نام منحصربفرد بات که حتما باید با @ شروع شود) را به عنوان عضو جدید کانال خود اضافه کنید. در اینجا عنوان می‌کند که این کاربر چون بات است، باید دسترسی ادمین را داشته باشد که دقیقا این دسترسی را نیز باید برقرار کنید تا بتوان توسط این بات، پیامی را به کانال اختصاصی خود ارسال کرد.
بنابراین تا اینجا یک کانال خصوصی را ایجاد کرده‌ایم که بات جدید test_2021_logs_bot@ عضو با دسترسی ادمین آن است. اکنون باید Id این کانال را بیابیم. برای اینکار بات دیگری را به نام JsonDumpBot@ یافته و استارت کنید. سپس در کانال خود یک پیام آزمایشی جدید را ارسال کنید و در ادامه این پیام را به بات JsonDumpBot@ ارسال کنید (forward کنید). همان لحظه‌ای که کار ارسال پیام به این بات صورت گرفت، Id کانال خود را در پاسخ آن می‌توانید مشاهده کنید:


در این تصویر مقدار forward_from_chat:id همان ChatId تنظیمات برنامه‌ی شما است.

در آخر این اطلاعات را در فایل Server\appsettings.json قرار می‌دهیم:
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "TelegramLoggingBot": {
    "AccessToken": "1826…",
    "ChatId": "-1001…" 
  }
}
که نحوه‌ی ثبت و معرفی آن‌ها به سیستم تزریق وابستگی‌های برنامه‌ی Web API، به صورت زیر است:
namespace BlazorWasmTelegramLogger.Server
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<TelegramLoggingBotOptions>(options =>
                            Configuration.GetSection("TelegramLoggingBot").Bind(options));
            services.AddSingleton<ITelegramBotService, TelegramBotService>();

            // ...
        }

        // ...
    }
}
سرویس ITelegramBotService را با طول عمر Singleton معرفی کرده‌ایم. چون new TelegramBotClient ای که در سازنده‌ی آن صورت می‌گیرد:
    public class TelegramBotService : ITelegramBotService
    {
        private readonly string _chatId;
        private readonly TelegramBotClient _client;

        public TelegramBotService(IOptions<TelegramLoggingBotOptions> options)
        {
            _chatId = options.Value.ChatId;
            _client = new TelegramBotClient(options.Value.AccessToken);
        }
باید فقط یکبار در طول عمر برنامه انجام شود و از این پس، هر بار که متد client.SendTextMessageAsync_ آن فراخوانی می‌گردد، پیامی به سمت بات و سپس کانال اختصاصی ما ارسال می‌شود.


ایجاد کنترلر Logs، جهت دریافت لاگ‌های رسیده‌ی از سمت کلاینت

مرحله‌ی آخر کار بسیار ساده‌است. سرویس تکمیل شده‌ی ITelegramBotService را به سازنده‌ی کنترلر Logs تزریق کرده و سپس متد SendLogAsync آن‌را فراخوانی می‌کنیم تا لاگی را که از کلاینت دریافت کرده، به سمت تلگرام هدایت کند:
using System;
using System.Threading.Tasks;
using BlazorWasmTelegramLogger.Server.Services;
using BlazorWasmTelegramLogger.Shared;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace BlazorWasmTelegramLogger.Server.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class LogsController : ControllerBase
    {
        private readonly ILogger<LogsController> _logger;
        private readonly ITelegramBotService _telegramBotService;

        public LogsController(ILogger<LogsController> logger, ITelegramBotService telegramBotService)
        {
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
            _telegramBotService = telegramBotService;
        }

        [HttpPost]
        public async Task<IActionResult> PostLog(ClientLog log)
        {
            // TODO: Save the client's `log` in the database

            _logger.Log(log.LogLevel, log.EventId, log.Url + Environment.NewLine + log.Message);

            await _telegramBotService.SendLogAsync(log);

            return Ok();
        }
    }
}


آزمایش برنامه

برای آزمایش برنامه، برای مثال در فایل Client\Pages\Counter.razor یک استثنای عمدی مدیریت نشده را قرار داده‌ایم:
@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;

        throw new InvalidOperationException("This is an exception message from the client!");
    }
}
اکنون اگر برنامه را اجرا کرده و سپس بر روی دکمه‌ی شمارشگر کلیک کنیم، همان تصویر ابتدای مطلب را که حاصل از ارسال جزئیات این استثنای مدیریت نشده به سمت تلگرام است، مشاهده خواهیم کرد.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: BlazorWasmTelegramLogger.zip
مطالب
استفاده از Kendo UI templates
در مطلب «صفحه بندی، مرتب سازی و جستجوی پویای اطلاعات به کمک Kendo UI Grid» در انتهای بحث، ستون IsAvailable به صورت زیر تعریف شد:
columns: [
               {
                   field: "IsAvailable", title: "موجود است",
                   template: '<input type="checkbox" #= IsAvailable ? checked="checked" : "" # disabled="disabled" ></input>'
                }
]
Templates، جزو یکی از پایه‌های Kendo UI Framework هستند و توسط آن‌ها می‌توان قطعات با استفاده‌ی مجدد HTML ایی را طراحی کرد که قابلیت یکی شدن با اطلاعات جاوا اسکریپتی را دارند.
همانطور که در این مثال نیز مشاهده می‌کنید، قالب‌های Kendo UI از Hash (#) syntax استفاده می‌کنند. در اینجا قسمت‌هایی از قالب که با علامت # محصور می‌شوند، در حین اجرا، با اطلاعات فراهم شده جایگزین خواهند شد.
برای رندر مقادیر ساده می‌توان از # =# استفاده کرد. از # :# برای رندر اطلاعات HTML-encoded کمک گرفته می‌شود و #  # برای رندر کدهای جاوا اسکریپتی کاربرد دارد. از حالت HTML-encoded برای نمایش امن اطلاعات دریافتی از کاربران و جلوگیری از حملات XSS استفاده می‌شود.
اگر در این بین نیاز است # به صورت معمولی رندر شود، در حالت کدهای جاوا اسکریپتی به صورت #\\ و در HTML ساده به صورت #\ باید مشخص گردد.


مثالی از نحوه‌ی تعریف یک قالب Kendo UI

    <!--دریافت اطلاعات از منبع محلی-->
    <script id="javascriptTemplate" type="text/x-kendo-template">
        <ul>
            # for (var i = 0; i < data.length; i++) { #
            <li>#= data[i] #</li>
            # } #
        </ul>
    </script>

    <div id="container1"></div>
    <script type="text/javascript">
        $(function () {
            var data = ['User 1', 'User 2', 'User 3'];
            var template = kendo.template($("#javascriptTemplate").html());
            var result = template(data); //Execute the template
            $("#container1").html(result); //Append the result
        });
    </script>
این قالب ابتدا در تگ script محصور می‌شود و سپس نوع آن مساوی text/x-kendo-template قرار می‌گیرد. در ادامه توسط یک حلقه‌ی جاوا اسکریپتی، عناصر آرایه‌ی فرضی data خوانده شده و با کمک Hash syntax در محل‌های مشخص شده قرار می‌گیرند.
در ادامه باید این قالب را رندر کرد. برای این منظور یک div با id مساوی container1 را جهت تعیین محل رندر نهایی اطلاعات مشخص می‌کنیم. سپس متد kendo.template بر اساس id قالب اسکریپتی تعریف شده، یک شیء قالب را تهیه کرده و سپس با ارسال آرایه‌ای به آن، سبب اجرای آن می‌شود. خروجی نهایی، یک قطعه کد HTML است که در محل container1 درج خواهد شد.
همانطور که ملاحظه می‌کنید، متد kendo.template، نهایتا یک رشته را دریافت می‌کند. بنابراین همینجا و به صورت inline نیز می‌توان یک قالب را تعریف کرد.


کار با منابع داده راه دور

فرض کنید مدل برنامه به صورت ذیل تعریف شده‌است:
namespace KendoUI04.Models
{
    public class Product
    {
        public int Id { set; get; }
        public string Name { set; get; }
        public decimal Price { set; get; }
        public bool IsAvailable { set; get; }
    }
}
و لیستی از آن توسط یک ASP.NET Web API کنترلر، به سمت کاربر ارسال می‌شود:
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using KendoUI04.Models;

namespace KendoUI04.Controllers
{
    public class ProductsController : ApiController
    {
        public IEnumerable<Product> Get()
        {
            return ProductDataSource.LatestProducts.Take(10);
        }
    }
}
در سمت کاربر و در View برنامه خواهیم داشت:
    <!--دریافت اطلاعات از سرور-->
    <div>
        <div id="container2"><ul></ul></div>
    </div>

    <script id="template1" type="text/x-kendo-template">
        <li> #=Id# - #:Name# - #=kendo.toString(Price, "c")#</li>
    </script>

    <script type="text/javascript">
        $(function () {
            var producatsTemplate1 = kendo.template($("#template1").html());

            var productsDataSource = new kendo.data.DataSource({
                transport: {
                    read: {
                        url: "api/products",
                        dataType: "json",
                        contentType: 'application/json; charset=utf-8',
                        type: 'GET'
                    }
                },
                error: function (e) {
                    alert(e.errorThrown);
                },
                change: function () {
                    $("#container2 > ul").html(kendo.render(producatsTemplate1, this.view()));
                }
            });
            productsDataSource.read();
        });
    </script>
ابتدا یک div با id مساوی container2 جهت تعیین محل نهایی رندر قالب template1 در صفحه تعریف می‌شود.
هرچند خروجی دریافتی از سرور نهایتا یک آرایه از اشیاء Product است، اما در template1 اثری از حلقه‌ی جاوا اسکریپتی مشاهده نمی‌شود. در اینجا چون از متد kendo.render استفاده می‌شود، نیازی به ذکر حلقه نیست و به صورت خودکار، به تعداد عناصر آرایه دریافتی از سرور، قطعه HTML قالب را تکرار می‌کند.
در ادامه برای کار با سرور از یک Kendo UI DataSource استفاده شده‌است. قسمت transport/read آن، کار تعریف محل دریافت اطلاعات را از سرور مشخص می‌کند. رویدادگران change آن اطلاعات نهایی دریافتی را توسط متد view در اختیار متد kendo.render قرار می‌دهد. در نهایت، قطعه‌ی HTML رندر شده‌ی نهایی حاصل از اجرای قالب، در بین تگ‌های ul مربوط به container2 درج خواهد شد.
رویدادگران change زمانیکه data source، از اطلاعات راه دور و یا یک آرایه‌ی جاوا اسکریپتی پر می‌شود، فراخوانی خواهد شد. همچنین مباحث مرتب سازی اطلاعات، صفحه بندی و تغییر صفحه، افزودن، ویرایش و یا حذف اطلاعات نیز سبب فراخوانی آن می‌گردند. متد view ایی که در این مثال فراخوانی شد، صرفا در روال رویدادگردان change دارای اعتبار است و آخرین تغییرات اطلاعات و آیتم‌های موجود در data source را باز می‌گرداند.


یک نکته‌ی تکمیلی: فعال سازی intellisense کدهای جاوا اسکریپتی Kendo UI

اگر به پوشه‌ی اصلی مجموعه‌ی Kendo UI مراجعه کنید، یکی از آن‌ها vsdoc نام دارد که داخل آن فایل‌های min.intellisense.js و vsdoc.js مشهود هستند.
اگر از ویژوال استودیوهای قبل از 2012 استفاده می‌کنید، نیاز است فایل‌های vsdoc.js متناظری را به پروژه اضافه نمائید؛ دقیقا در کنار فایل‌های اصلی js موجود. اگر از ویژوال استودیوی 2012 و یا بالاتر استفاده می‌کنید باید از فایل‌های intellisense.js متناظر استفاده کنید. برای مثال اگر از kendo.all.min.js کمک می‌گیرید، فایل متناظر با آن kendo.all.min.intellisense.js خواهد بود.
بعد از اینکار نیاز است فایلی به نام references.js_ را به پوشه‌ی اسکریپت‌های خود با این محتوا اضافه کنید (برای VS 2012 به بعد):
/// <reference path="jquery.min.js" />
/// <reference path="kendo.all.min.js" />
نکته‌ی مهم اینجا است که این فایل به صورت پیش فرض از مسیر Scripts/_references.js/~ خوانده می‌شود. برای اضافه کردن مسیر دیگری مانند js/_references.js/~ باید آن‌را به تنظیمات ذیل اضافه کنید:
 Tools menu –> Options -> Text Editor –> JavaScript –> Intellisense –> References
گزینه‌ی Reference Group را به (Implicit (Web تغییر داده و سپس مسیر جدیدی را اضافه نمائید.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:
KendoUI04.zip
نظرات مطالب
پیاده سازی JSON Web Token با ASP.NET Web API 2.x
با توجه به این موضوع که اپلیکیشنهای کنونی زمانی که کاربر ثبت نام میکنه باید متد لاگین به صورت خودکار انجام شده و access_token  در اختیار آپ قرار بگیره بنده این متد را نوشتم
       public async Task<IHttpActionResult> LoginUserAsync(LoginUserBindingModel model)
        {
          var requestParams = new List<KeyValuePair<string, string>>
          {
          new KeyValuePair<string, string>("grant_type", "password"),
          new KeyValuePair<string, string>("username", model.Username),
          new KeyValuePair<string, string>("password", model.Password)
          };
            var requestParamsFormUrlEncoded = new FormUrlEncodedContent(requestParams);
            using (var client = new HttpClient())
            {
                client.BaseAddress = new Uri("http://localhost:9577/");
                client.DefaultRequestHeaders.Accept.Clear();
              //  client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                var response = await client.PostAsync("/login", requestParamsFormUrlEncoded);
                if (response.StatusCode == HttpStatusCode.OK)
                {
                    var responseString = await response.Content.ReadAsStringAsync();
                    var jsSerializer = new JavaScriptSerializer();
                    var responseData = jsSerializer.Deserialize<Dictionary<string, string>>(responseString);
                    return Content(HttpStatusCode.OK, responseData);
                }
                else
                {
                    return  BadRequest(response.ToString());
                }
            }
        }
ولی response برابر با 400 میشه ! چگونه باید به  access_token تولیدی دسترسی داشت ؟
مطالب
اعتبارسنجی مبتنی بر کوکی‌ها در ASP.NET Core 2.0 بدون استفاده از سیستم Identity
ASP.NET Core 2.0 به همراه یک AuthenticationMiddleware است که یکی از قابلیت‌های این میان‌افزار، افزودن اطلاعات کاربر و یا همان HttpContext.User به یک کوکی رمزنگاری شده و سپس اعتبارسنجی این کوکی در درخواست‌های بعدی کاربر است. این مورد سبب خواهد شد تا بتوان بدون نیاز به پیاده سازی سیستم کامل ASP.NET Core Identity، یک سیستم اعتبارسنجی سبک، ساده و سفارشی را تدارک دید.



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

در اینجا کنترل کامل سیستم در اختیار ما است و در این حالت می‌توان طراحی تمام قسمت‌ها را از ابتدا و مطابق میل خود انجام داد. برای مثال سیستم اعتبارسنجی ساده‌ی ما، شامل جدول کاربران و نقش‌های آن‌ها خواهد بود و این دو با هم رابطه‌ی many-to-many دارند. به همین جهت جدول UserRole نیز در اینجا پیش بینی شده‌است.

جدول کاربران

    public class User
    {
        public User()
        {
            UserRoles = new HashSet<UserRole>();
        }

        public int Id { get; set; }

        public string Username { get; set; }

        public string Password { get; set; }

        public string DisplayName { get; set; }

        public bool IsActive { get; set; }

        public DateTimeOffset? LastLoggedIn { get; set; }

        /// <summary>
        /// every time the user changes his Password,
        /// or an admin changes his Roles or stat/IsActive,
        /// create a new `SerialNumber` GUID and store it in the DB.
        /// </summary>
        public string SerialNumber { get; set; }

        public virtual ICollection<UserRole> UserRoles { get; set; }
    }
در اینجا SerialNumber فیلدی است که با هر بار ویرایش اطلاعات کاربران باید از طرف برنامه به روز رسانی شود. از آن جهت غیرمعتبر سازی کوکی کاربر استفاده خواهیم کرد. برای مثال اگر خاصیت فعال بودن او تغییر کرد و یا نقش‌های او را تغییر دادیم، کاربر در همان لحظه باید logout شود. به همین جهت چنین فیلدی در اینجا در نظر گرفته شده‌است تا با بررسی آن بتوان وضعیت معتبر بودن کوکی او را تشخیص داد.

جدول نقش‌های کاربران

    public class Role
    {
        public Role()
        {
            UserRoles = new HashSet<UserRole>();
        }

        public int Id { get; set; }
        public string Name { get; set; }

        public virtual ICollection<UserRole> UserRoles { get; set; }
    }
البته این سیستم ساده دارای یک سری نقش ثابت و مشخص است.
    public static class CustomRoles
    {
        public const string Admin = nameof(Admin);
        public const string User = nameof(User);
    }
که این نقش‌ها را در ابتدای کار برنامه به بانک اطلاعات اضافه خواهیم کرد.


جدول ارتباط نقش‌ها با کاربران و برعکس

    public class UserRole
    {
        public int UserId { get; set; }
        public int RoleId { get; set; }

        public virtual User User { get; set; }
        public virtual Role Role { get; set; }
    }
وجود این جدول در EF Core جهت تعریف یک رابطه‌ی many-to-many ضروری است.


تعریف Context برنامه و فعالسازی Migrations در EF Core 2.0

DbContext برنامه را به صورت ذیل در یک اسمبلی دیگر اضافه خواهیم کرد:
    public interface IUnitOfWork : IDisposable
    {
        DbSet<TEntity> Set<TEntity>() where TEntity : class;

        int SaveChanges(bool acceptAllChangesOnSuccess);
        int SaveChanges();
        Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = new CancellationToken());
        Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken());
    }

    public class ApplicationDbContext : DbContext, IUnitOfWork
    {
        public ApplicationDbContext(DbContextOptions options) : base(options)
        { }

        public virtual DbSet<User> Users { set; get; }
        public virtual DbSet<Role> Roles { set; get; }
        public virtual DbSet<UserRole> UserRoles { get; set; }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            // it should be placed here, otherwise it will rewrite the following settings!
            base.OnModelCreating(builder);

            // Custom application mappings
            builder.Entity<User>(entity =>
            {
                entity.Property(e => e.Username).HasMaxLength(450).IsRequired();
                entity.HasIndex(e => e.Username).IsUnique();
                entity.Property(e => e.Password).IsRequired();
                entity.Property(e => e.SerialNumber).HasMaxLength(450);
            });

            builder.Entity<Role>(entity =>
            {
                entity.Property(e => e.Name).HasMaxLength(450).IsRequired();
                entity.HasIndex(e => e.Name).IsUnique();
            });

            builder.Entity<UserRole>(entity =>
            {
                entity.HasKey(e => new { e.UserId, e.RoleId });
                entity.HasIndex(e => e.UserId);
                entity.HasIndex(e => e.RoleId);
                entity.Property(e => e.UserId);
                entity.Property(e => e.RoleId);
                entity.HasOne(d => d.Role).WithMany(p => p.UserRoles).HasForeignKey(d => d.RoleId);
                entity.HasOne(d => d.User).WithMany(p => p.UserRoles).HasForeignKey(d => d.UserId);
            });
        }
    }
در اینجا موجودیت‌های برنامه به صورت DbSet در معرض دید EF Core 2.0 قرار گرفته‌اند. همچنین رابطه‌ی Many-to-Many بین نقش‌ها و کاربران نیز تنظیم شده‌است.
سازنده‌ی کلاس به همراه پارامتر DbContextOptions است تا بتوان آن‌را در آغاز برنامه تغییر داد.


فعالسازی مهاجرت‌ها در EF Core 2.0

EF Core 2.0 برخلاف نگارش‌های قبلی آن به دنبال کلاسی مشتق شده‌ی از IDesignTimeDbContextFactory می‌گردد تا بتواند نحوه‌ی وهله سازی ApplicationDbContext را دریافت کند. در اینجا چون DbContext تعریف شده دارای یک سازنده‌ی با پارامتر است، EF Core 2.0 نمی‌داند که چگونه باید آن‌را در حین ساخت مهاجرت‌ها و اعمال آن‌ها، وهله سازی کند. کار کلاس ApplicationDbContextFactory ذیل دقیقا مشخص سازی همین مساله است:
    /// <summary>
    /// Only used by EF Tooling
    /// </summary>
    public class ApplicationDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
    {
        public ApplicationDbContext CreateDbContext(string[] args)
        {
            var basePath = Directory.GetCurrentDirectory();
            Console.WriteLine($"Using `{basePath}` as the BasePath");
            var configuration = new ConfigurationBuilder()
                                    .SetBasePath(basePath)
                                    .AddJsonFile("appsettings.json")
                                    .Build();
            var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
            var connectionString = configuration.GetConnectionString("DefaultConnection");
            builder.UseSqlServer(connectionString);
            return new ApplicationDbContext(builder.Options);
        }
    }
کاری که در اینجا انجام شده، خواندن DefaultConnection از قسمت ConnectionStrings فایل appsettings.json است:
{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=(LocalDB)\\MSSQLLocalDB;Initial Catalog=ASPNETCore2CookieAuthenticationDB;Integrated Security=True;MultipleActiveResultSets=True;"
  },
  "LoginCookieExpirationDays": 30
}
و سپس استفاده‌ی از آن جهت تنظیم رشته‌ی اتصالی متد UseSqlServer و در آخر وهله سازی ApplicationDbContext.
کار یافتن این کلاس در حین تدارک و اعمال مهاجرت‌ها توسط EF Core 2.0 خودکار بوده و باید محل قرارگیری آن دقیقا در اسمبلی باشد که DbContext برنامه در آن تعریف شده‌است.


تدارک لایه سرویس‌های برنامه

پس از مشخص شدن ساختار موجودیت‌ها و همچنین Context برنامه، اکنون می‌توان لایه سرویس برنامه را به صورت ذیل تکمیل کرد:

سرویس کاربران
    public interface IUsersService
    {
        Task<string> GetSerialNumberAsync(int userId);
        Task<User> FindUserAsync(string username, string password);
        Task<User> FindUserAsync(int userId);
        Task UpdateUserLastActivityDateAsync(int userId);
    }
کار این سرویس ابتدایی کاربران، یافتن یک یک کاربر بر اساس Id او و یا کلمه‌ی عبور و نام کاربری او است. از این امکانات در حین لاگین و یا اعتبارسنجی کوکی کاربر استفاده خواهیم کرد.
پیاده سازی کامل این سرویس را در اینجا می‌توانید مشاهده کنید.

سرویس نقش‌های کاربران
    public interface IRolesService
    {
        Task<List<Role>> FindUserRolesAsync(int userId);
        Task<bool> IsUserInRole(int userId, string roleName);
        Task<List<User>> FindUsersInRoleAsync(string roleName);
    }
از این سرویس برای یافتن نقش‌های کاربر لاگین شده‌ی به سیستم و افزودن آن‌ها به کوکی رمزنگاری شده‌ی اعتبارسنجی او استفاده خواهیم کرد.
پیاده سازی کامل این سرویس را در اینجا می‌توانید مشاهده کنید.

سرویس آغاز بانک اطلاعاتی

    public interface IDbInitializerService
    {
        void Initialize();
        void SeedData();
    }
از این سرویس در آغاز کار برنامه برای اعمال خودکار مهاجرت‌های تولیدی و همچنین ثبت نقش‌های آغازین سیستم به همراه افزودن کاربر Admin استفاده خواهیم کرد.
پیاده سازی کامل این سرویس را در اینجا می‌توانید مشاهده کنید.

سرویس اعتبارسنجی کوکی‌های کاربران

یکی از قابلیت‌های میان‌افزار اعتبارسنجی ASP.NET Core 2.0، رخ‌دادی است که در آن اطلاعات کوکی دریافتی از کاربر، رمزگشایی شده و در اختیار برنامه جهت تعیین اعتبار قرار می‌گیرد:
    public interface ICookieValidatorService
    {
        Task ValidateAsync(CookieValidatePrincipalContext context);
    }

    public class CookieValidatorService : ICookieValidatorService
    {
        private readonly IUsersService _usersService;
        public CookieValidatorService(IUsersService usersService)
        {
            _usersService = usersService;
            _usersService.CheckArgumentIsNull(nameof(usersService));
        }

        public async Task ValidateAsync(CookieValidatePrincipalContext context)
        {
            var userPrincipal = context.Principal;

            var claimsIdentity = context.Principal.Identity as ClaimsIdentity;
            if (claimsIdentity?.Claims == null || !claimsIdentity.Claims.Any())
            {
                // this is not our issued cookie
                await handleUnauthorizedRequest(context);
                return;
            }

            var serialNumberClaim = claimsIdentity.FindFirst(ClaimTypes.SerialNumber);
            if (serialNumberClaim == null)
            {
                // this is not our issued cookie
                await handleUnauthorizedRequest(context);
                return;
            }

            var userIdString = claimsIdentity.FindFirst(ClaimTypes.UserData).Value;
            if (!int.TryParse(userIdString, out int userId))
            {
                // this is not our issued cookie
                await handleUnauthorizedRequest(context);
                return;
            }

            var user = await _usersService.FindUserAsync(userId).ConfigureAwait(false);
            if (user == null || user.SerialNumber != serialNumberClaim.Value || !user.IsActive)
            {
                // user has changed his/her password/roles/stat/IsActive
                await handleUnauthorizedRequest(context);
            }

            await _usersService.UpdateUserLastActivityDateAsync(userId).ConfigureAwait(false);
        }

        private Task handleUnauthorizedRequest(CookieValidatePrincipalContext context)
        {
            context.RejectPrincipal();
            return context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        }
    }
کار این سرویس، تعیین اعتبار موارد ذیل است:
- آیا کوکی دریافت شده دارای اطلاعات HttpContext.User است؟
- آیا این کوکی به همراه اطلاعات فیلد SerialNumber است؟
- آیا این کوکی به همراه Id کاربر است؟
- آیا کاربری که بر اساس این Id یافت می‌شود غیرفعال شده‌است؟
- آیا کاربری که بر اساس این Id یافت می‌شود دارای SerialNumber یکسانی با نمونه‌ی موجود در بانک اطلاعاتی است؟

اگر خیر، این اعتبارسنجی رد شده و بلافاصله کوکی کاربر نیز معدوم خواهد شد.


تنظیمات ابتدایی میان‌افزار اعتبارسنجی کاربران در ASP.NET Core 2.0

تنظیمات کامل ابتدایی میان‌افزار اعتبارسنجی کاربران در ASP.NET Core 2.0 را در فایل Startup.cs می‌توانید مشاهده کنید.

ابتدا سرویس‌های برنامه معرفی شده‌اند:
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IUnitOfWork, ApplicationDbContext>();
            services.AddScoped<IUsersService, UsersService>();
            services.AddScoped<IRolesService, RolesService>();
            services.AddScoped<ISecurityService, SecurityService>();
            services.AddScoped<ICookieValidatorService, CookieValidatorService>();
            services.AddScoped<IDbInitializerService, DbInitializerService>();

سپس تنظیمات مرتبط با ترزیق وابستگی‌های ApplicationDbContext برنامه انجام شده‌است. در اینجا رشته‌ی اتصالی، از فایل appsettings.json خوانده شده و سپس در اختیار متد UseSqlServer قرار می‌گیرد:
            services.AddDbContext<ApplicationDbContext>(options =>
            {
                options.UseSqlServer(
                    Configuration.GetConnectionString("DefaultConnection"),
                    serverDbContextOptionsBuilder =>
                        {
                            var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds;
                            serverDbContextOptionsBuilder.CommandTimeout(minutes);
                            serverDbContextOptionsBuilder.EnableRetryOnFailure();
                        });
            });

در ادامه تعدادی Policy مبتنی بر نقش‌های ثابت سیستم را تعریف کرده‌ایم. این کار اختیاری است اما روش توصیه شده‌ی در ASP.NET Core، کار با Policyها است تا کار مستقیم با نقش‌ها. Policy‌ها انعطاف پذیری بیشتری را نسبت به نقش‌ها ارائه می‌دهند و در اینجا به سادگی می‌توان چندین نقش و یا حتی Claim را با هم ترکیب کرد و به صورت یک Policy ارائه داد:
            // Only needed for custom roles.
            services.AddAuthorization(options =>
                    {
                        options.AddPolicy(CustomRoles.Admin, policy => policy.RequireRole(CustomRoles.Admin));
                        options.AddPolicy(CustomRoles.User, policy => policy.RequireRole(CustomRoles.User));
                    });

قسمت اصلی تنظیمات میان افزار اعتبارسنجی مبتنی بر کوکی‌ها در اینجا قید شده‌است:
            // Needed for cookie auth.
            services
                .AddAuthentication(options =>
                {
                    options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                    options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                    options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                })
                .AddCookie(options =>
                {
                    options.SlidingExpiration = false;
                    options.LoginPath = "/api/account/login";
                    options.LogoutPath = "/api/account/logout";
                    //options.AccessDeniedPath = new PathString("/Home/Forbidden/");
                    options.Cookie.Name = ".my.app1.cookie";
                    options.Cookie.HttpOnly = true;
                    options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
                    options.Cookie.SameSite = SameSiteMode.Lax;
                    options.Events = new CookieAuthenticationEvents
                    {
                        OnValidatePrincipal = context =>
                        {
                            var cookieValidatorService = context.HttpContext.RequestServices.GetRequiredService<ICookieValidatorService>();
                            return cookieValidatorService.ValidateAsync(context);
                        }
                    };
                });
ابتدا مشخص شده‌است که روش مدنظر ما، اعتبارسنجی مبتنی بر کوکی‌ها است و سپس تنظیمات مرتبط با کوکی رمزنگاری شده‌ی برنامه مشخص شده‌اند. تنها قسمت مهم آن CookieAuthenticationEvents است که نحوه‌ی پیاده سازی آن‌را با معرفی سرویس ICookieValidatorService پیشتر بررسی کردیم. این قسمت جائی است که پس از هر درخواست به سرور اجرا شده و کوکی رمزگشایی شده، در اختیار برنامه جهت اعتبارسنجی قرار می‌گیرد.

کار نهایی تنظیمات میان افزار اعتبارسنجی در متد Configure با فراخوانی UseAuthentication صورت می‌گیرد. اینجا است که میان افزار، به برنامه معرفی خواهد شد:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
   app.UseAuthentication();

همچنین پس از آن، کار اجرای سرویس آغاز بانک اطلاعاتی نیز انجام شده‌است تا نقش‌ها و کاربر Admin را به سیستم اضافه کند:
            var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
            using (var scope = scopeFactory.CreateScope())
            {
                var dbInitializer = scope.ServiceProvider.GetService<IDbInitializerService>();
                dbInitializer.Initialize();
                dbInitializer.SeedData();
            }


پیاده سازی ورود و خروج به سیستم

پس از این مقدمات به مرحله‌ی آخر پیاده سازی این سیستم اعتبارسنجی می‌رسیم.

پیاده سازی Login
در اینجا از سرویس کاربران استفاده شده و بر اساس نام کاربری و کلمه‌ی عبور ارسالی به سمت سرور، این کاربر یافت خواهد شد.
در صورت وجود این کاربر، مرحله‌ی نهایی کار، فراخوانی متد الحاقی HttpContext.SignInAsync است:
        [AllowAnonymous]
        [HttpPost("[action]")]
        public async Task<IActionResult> Login([FromBody]  User loginUser)
        {
            if (loginUser == null)
            {
                return BadRequest("user is not set.");
            }

            var user = await _usersService.FindUserAsync(loginUser.Username, loginUser.Password).ConfigureAwait(false);
            if (user == null || !user.IsActive)
            {
                await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
                return Unauthorized();
            }

            var loginCookieExpirationDays = _configuration.GetValue<int>("LoginCookieExpirationDays", defaultValue: 30);
            var cookieClaims = await createCookieClaimsAsync(user).ConfigureAwait(false);
            await HttpContext.SignInAsync(
                CookieAuthenticationDefaults.AuthenticationScheme,
                cookieClaims,
                new AuthenticationProperties
                {
                    IsPersistent = true, // "Remember Me"
                    IssuedUtc = DateTimeOffset.UtcNow,
                    ExpiresUtc = DateTimeOffset.UtcNow.AddDays(loginCookieExpirationDays)
                });

            await _usersService.UpdateUserLastActivityDateAsync(user.Id).ConfigureAwait(false);

            return Ok();
        }
مهم‌ترین کار متد HttpContext.SignInAsync علاوه بر تنظیم طول عمر کوکی کاربر، قسمت createCookieClaimsAsync است که به صورت ذیل پیاده سازی شده‌است:
        private async Task<ClaimsPrincipal> createCookieClaimsAsync(User user)
        {
            var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
            identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()));
            identity.AddClaim(new Claim(ClaimTypes.Name, user.Username));
            identity.AddClaim(new Claim("DisplayName", user.DisplayName));

            // to invalidate the cookie
            identity.AddClaim(new Claim(ClaimTypes.SerialNumber, user.SerialNumber));

            // custom data
            identity.AddClaim(new Claim(ClaimTypes.UserData, user.Id.ToString()));

            // add roles
            var roles = await _rolesService.FindUserRolesAsync(user.Id).ConfigureAwait(false);
            foreach (var role in roles)
            {
                identity.AddClaim(new Claim(ClaimTypes.Role, role.Name));
            }

            return new ClaimsPrincipal(identity);
        }
در اینجا است که اطلاعات اضافی کاربر مانند Id او یا نقش‌های او به کوکی رمزنگاری شده‌ی تولیدی توسط HttpContext.SignInAsync اضافه شده و در دفعات بعدی و درخواست‌های بعدی او، به صورت خودکار توسط مرورگر به سمت سرور ارسال خواهند شد. این کوکی است که امکان کار با MyProtectedApiController و یا MyProtectedAdminApiController را فراهم می‌کند. اگر شخص لاگین کرده باشد، بلافاصله قابلیت دسترسی به امکانات محدود شده‌ی توسط فیلتر Authorize را خواهد یافت. همچنین در این مثال چون کاربر Admin لاگین می‌شود، امکان دسترسی به Policy مرتبطی را نیز خواهد یافت:
[Route("api/[controller]")]
[Authorize(Policy = CustomRoles.Admin)]
public class MyProtectedAdminApiController : Controller

پیاده سازی Logout

متد الحاقی HttpContext.SignOutAsync کار Logout کاربر را تکمیل می‌کند.
        [AllowAnonymous]
        [HttpGet("[action]"), HttpPost("[action]")]
        public async Task<bool> Logout()
        {
            await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
            return true;
        }


آزمایش نهایی برنامه

در فایل index.html ، نمونه‌ای از متدهای لاگین، خروج و فراخوانی اکشن متدهای محافظت شده را مشاهده می‌کنید. این روش برای برنامه‌های تک صفحه‌ای وب یا SPA نیز می‌تواند مفید باشد و به همین نحو کار می‌کنند.



کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید.
نظرات مطالب
ایجاد یک Repository در پروژه برای دستورات EF
با سلام من یک  معماری طراحی کردم به شکل زیر
ابتدا یک اینترفیس به شکل زیر دارم
using System;
using System.Collections;
using System.Linq;

namespace Framework.Model
{
    public interface IContext
    {
        T Get<T>(Func<T, bool> prediction) where T : class;
        IEnumerable List<T>(Func<T, bool> prediction) where T : class;
        void Insert<T>(T entity) where T : class;
        int Save();
    }
}
بعد یک کلاس ازش مشتق شده
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Text;

namespace Framework.Model
{
    public class Context : IContext
    {
        private readonly DbContext _dbContext;

        public Context(DbContext context)
        {
            _dbContext = context;
        }

        public T Get<T>(Func<T,bool> prediction) where T : class
        {
            var dbSet = _dbContext.Set<T>();
            if (dbSet!= null)
                return dbSet.Single(prediction);

            throw new Exception();
        }

        public void Insert<T>(T entity) where T : class
        {
            var dbSet = _dbContext.Set<T>();
            if (dbSet != null)
            {
                _dbContext.Entry(entity).State = EntityState.Added;
            }
        }

        public int Save()
        {
            return _dbContext.SaveChanges();
        }


        IEnumerable IContext.List<T>(Func<T, bool> prediction)
        {
            var dbSet = _dbContext.Set<T>();
            if (dbSet != null)
                return dbSet.Where(prediction).ToList();

            throw new Exception();
        }
    }
}
سپس یک کلاش context دارم که مستقیما از dbcontext مشتق شده
using System.Data.Entity;
using DataModel;

namespace Model
{
    public class EFContext : DbContext
    {
        public EFContext(string db): base(db)
        {

        }

        public DbSet<Product> Products { get; set; }
    }
}
و سپس کلاس دارم که اومده پیاده سازی کرده context که خودم ساختمو 
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Text;

namespace Model
{
    public class Context : Framework.Model.Context
    {
        public Context(string db): base(new EFContext(db))
        {
            
        }
    }
}
در پروژه دیگری اومدم یک کلاس context جدید ساختم 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Biz
{
    public class Context : Model.Context
    {
        public Context(string db) : base(db)
        {

        }
    }
}
و در کنترلر هم به این شکل ازش استفاده کردم
using System.Web.Mvc;
using Framework.Model;

namespace ProductionRepository.Controllers
{
    public class BaseController : Controller
    {
        public IContext DataContext { get; set; }

        public BaseController()
        {
            DataContext = new Biz.Context(System.Configuration.ConfigurationManager.ConnectionStrings["Database"].ConnectionString);
        }
    }
}
using System.Web.Mvc;
using DataModel;
using System.Collections.Generic;

namespace ProductionRepository.Controllers
{
    public class ProductController : BaseController
    {
        public ActionResult Index()
        {
            var x = DataContext.List<Product>(s => s.Name != null);
            return View(x);
        }

    }
}
و این هم تست 
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web.Mvc;

namespace TestUnit
{
    [TestFixture]
    public class Test
    {
        [Test]
        public void IndexShouldListProduct()
        {
            var repo = new Moq.Mock<Framework.Model.IContext>();
            var products = new List<DataModel.Product>();
            products.Add(new DataModel.Product { Id = 1, Name = "asdasdasd" });
            products.Add(new DataModel.Product { Id = 2, Name = "adaqwe" });
            products.Add(new DataModel.Product { Id = 4, Name = "qewqw" });
            products.Add(new DataModel.Product { Id = 5, Name = "qwe" });
            repo.Setup(x => x.List<DataModel.Product>(p => p.Name != null)).Returns(products.AsEnumerable());
            var controller = new ProductionRepository.Controllers.ProductController();
            controller.DataContext = repo.Object;
            var result = controller.Index() as ViewResult;
            var model = result.Model as List<DataModel.Product>;
            Assert.AreEqual(4, model.Count);
            Assert.AreEqual("", result.ViewName);

        }
    }
}
نظرتون چیه آقای نصیری
مطالب
ASP.NET Web API - قسمت پنجم
مدیریت کدهای وضعیت در Web API
تمامی پاسخ‌های دریافتی از Web API توسط Client، باید در قالب کدهای وضعیت HTTP باشند. دو کلاس جدید با نام‌های HttpResponseMessage و HttpResponseException همراه با ASP.NET MVC 4 معرفی شده اند که ارسال کدهای وضعیت پردازش درخواست به Client را آسان می‌سازند. به عنوان مثال، ارسال وضعیت برای چهار عمل اصلی بازیابی، ایجاد، آپدیت و حذف رکورد را بررسی می‌کنیم.

بازیابی رکورد
بر اساس مستندات پروتوکل HTTP، در صورتی که منبع درخواستی Client پیدا نشد، باید کد وضعیت 404 برگشت داده شود. این حالت را در متد ذیل پیاده سازی کرده ایم.
public Product GetProduct(int id)
{
    Product item = repository.Get(id);
    if (item == null)
    {
        throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotFound)); 
    }
    return item;
}
در صورتی که رکوردی با مشخصه‌ی درخواستی پیدا نشد، با استفاده از کلاس HttpResponseException، خطایی به Client ارسال خواهد شد. پارامتر سازنده‌ی این کلاس، شی ای از نوع کلاس HttpResponseMessage است. سازنده‌ی کلاس HttpResponseMessage، مقداری از یک enum با نام HttpStatusCode را می‌پذیرد. مقدار NotFound، نشان از خطای 404 است و زمانی به کار می‌رود که منبع درخواستی وجود نداشته باشد. اگر محصول درخواست شده یافت شد، در قالب JSON برگشت داده می‌شود. در شکل ذیل، پاسخ دریافتی در زمان درخواست محصولی که وجود ندارد را ملاحظه می‌کنید.

ایجاد رکورد
برای ایجاد رکورد، Client درخواستی از نوع POST را همراه با داده‌های رکورد در بدنه‌ی درخواست به Server ارسال می‌کند. در ذیل، پیاده سازی ساده ای از این حالت را مشاهده می‌کنید.
public Product PostProduct(Product item)
{
    item = repository.Add(item);
    return item;
}
 این پیاده سازی کار می‌کند اما کمبودهایی دارد:
  • کد وضعیت پردازش درخواست: به طور پیش فرض، Web API، کد 200 را در پاسخ ارسال می‌کند، اما بر اساس مستندات پروتوکل HTTP، زمانی که یک درخواست از نوع POST منجر به تولید منبعی می-شود، Server باید کد وضعیت 201 را به Client برگشت بدهد.
  • آدرس منبع جدید ایجاد شده: بر اساس مستندات پروتوکل HTTP، زمانی که منبعی بر روی Server ایجاد می‌شود، باید آدرس منبع جدید ایجاد شده از طریق هدر Location به Client ارسال شود.
با توجه به این توضیحات، متد قبل به صورت ذیل در خواهد آمد.
public HttpResponseMessage PostProduct(Product item)
{
    item = repository.Add(item);
    var response = Request.CreateResponse(HttpStatusCode.Created, item);

    string uri = Url.Link("DefaultApi", new { id = item.Id });
    response.Headers.Location = new Uri(uri);
    return response;
}
همان طور که ملاحظه می‌کنید، خروجی متد از نوع کلاس HttpResponseMessage است، چون با استفاده از این نوع می‌توانیم جزئیات مورد نیاز را در مورد نتیجه‌ی پردازش درخواست به مرورگر ارسال کنیم. همچنین، داده‌های رکورد جدید نیز در بدنه‌ی پاسخ، با یک فرمت مناسب مانند XML یا JSON برگشت داده می‌شوند. با استفاده از متد CreateResponse کلاس Request و پاس دادن کد وضعیت و شی ای که قصد داریم به Client ارسال شود به این متد، شی ای از نوع کلاس HttpResponseMessage ایجاد می‌کنیم. آدرس منبع جدید نیز با استفاده از response.Headers.Location مشخص شده است. نمونه ای از پاسخ دریافت شده در سمت Client به صورت ذیل است.


آپدیت رکورد
آپدیت با استفاده از درخواست‌های از نوع PUT انجام می‌گیرد. یک مثال ساده در این مورد.

public void PutProduct(int id, Product product)
{
    product.Id = id;
    if (!repository.Update(product))
    {
        throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotFound));
    }
}
نام متد با عبارت Put آغاز شده است. بنابراین توسط Web API برای پردازش درخواست‌های از نوع PUT در نظر گرفته می‌شود. متد قبل، دو پارامتر ورودی دارد. id برای مشخصه‌ی محصول، و محصول آپدیت شده که در پارامتر دوم قرار می‌گیرد. مقدار پارامتر id از آدرس دریافت می‌شود و مقدار پارامتر product از بدنه‌ی درخواست. به طور پیش فرض، Web API، مقدار داده هایی با نوع ساده مانند int، string و bool را از طریق route، و مقدار نوع‌های پیچیده‌تر مانند داده‌های یک کلاس را از بدنه‌ی درخواست می‌خواند.

حذف یک رکورد
حذف یک رکورد، با استفاده از درخواست‌های از نوع DELETE انجام می‌گیرد. یک مثال ساده در این مورد.
public HttpResponseMessage DeleteProduct(int id)
{
    repository.Remove(id);
    return new HttpResponseMessage(HttpStatusCode.NoContent);
}
بر اساس مستندات پروتکل HTTP، اگر منبعی که Client قصد حذف آن را دارد از پیش حذف شده است، نباید خطایی به وی گزارش شود. معمولاً در متدهایی که وظیفه‌ی حذف منبع را بر عهده دارند، کد 204 مبنی بر پردازش کامل درخواست و پاسخ خالی برگشت داده می‌شود. این کد با استفاد از مقدار NoContent برای HttpStatusCode مشخص می‌شود.

فراخوانی متدها و مدیریت کدهای وضعیت HTTP در سمت Client
حال ببینیم چگونه می‌توان از متدهای قبل در سمت Client استفاده و خطاهای احتمالی آنها را مدیریت کرد.
بهتر است مثال را برای حالتی که در آن رکوردی آپدیت می‌شود بررسی کنیم. کدهای مورد نیاز برای فراخوانی متد PutProduct در سمت Client به صورت ذیل است.
var id = $("#myTextBox").val();

$.ajax({
  url: "/api/Test/" + id,
   type: 'PUT',
   data: { Id: "1", Name: "Tomato Soup", Category: "Groceries", Price: "1.39M" },
   cache: false,
   statusCode: {
       200: function (data) {
           alert("آپدیت انجام شد");
        },
       404:
            function () {
                alert("خطا در آپدیت");
            }
      }
});
از متدهای get، getJson یا post در jQuery نمی‌توان برای عمل آپدیت استفاده نمود، چون Web API انتظار دارد تا نام فعل درخواستی، PUT باشد. اما با استفاده از متد ajax و ذکر نام فعل در پارامتر type آن می‌توان نوع درخواست را PUT تعریف کرد. خط 5 بدین منظور است. از طریق خصیصه‌ی statusCode نیز می‌توان کدهای وضعیت مختلف HTTP را بررسی کرد. دو کد 200 و 404 که به ترتیب نشان از موفقیت و عدم موفقیت در آپدیت رکورد هستند تعریف شده و پیغام مناسب به کاربر نمایش داده می‌شود.
در حالتی که آپدیت با موفقیت همراه باشد، بدنه‌ی پاسخ به شکل ذیل است.


  و در صورتی که خطایی رخ دهد، بدنه‌ی پاسخ دریافتی به صورت ذیل خواهد بود.