نظرات مطالب
نمایش رکوردها به ترتیب اولویت به کمک jQuery UI sortable در ASP.NET MVC

«این کار احتمالا در ASP.NET WebForm  کار سختی نیست»

اتفاقا کار ساده‌ای نیست و همین مراحل باید طی شود. ضمن اینکه کار با jquery ajax در آنجا به این یک دستی نیست. نیاز است در code behind فرم، متد وب سرویس مانندی  به صورت استاتیک تعریف شود (که خودش سبب می‌شود تا دسترسی به اعتبار سنجی توکار مبتنی بر فرم‌ها محدود شود) یا اینکه از یک هندلر مجزا بجای یک اکشن متد کمک گرفته شود ... خلاصه خیلی داستان دارد.

مطالب
بررسی تغییرات Blazor 8x - قسمت چهاردهم - امکان استفاده از کامپوننت‌های Blazor در برنامه‌های ASP.NET Core 8x
ASP.NET Core 8x به همراه یک IResult جدید به‌نام RazorComponentResult است که توسط آن می‌توان در Endpoint‌های Minimal-API و همچنین اکشن متدهای MVC، از کامپوننت‌های Blazor، خروجی گرفت. این خروجی نه فقط static یا به عبارتی SSR، بلکه حتی می‌تواند تعاملی هم باشد. در این مطلب، جزئیات فعالسازی و استفاده از این IResult جدید را در یک برنامه‌ی Minimal-API بررسی می‌کنیم.


ایجاد یک برنامه‌ی Minimal-API جدید در دات نت 8

پروژه‌ای را که در اینجا پیگیری می‌کنیم، بر اساس قالب استاندارد تولید شده‌ی توسط دستور dotnet new webapi تکمیل می‌شود.


ایجاد یک صفحه‌ی Blazor 8x به همراه مسیریابی و دریافت پارامتر

در ادامه قصد داریم که یک کامپوننت جدید را به نام SsrTest.razor در پوشه‌ی جدید Components\Tests ایجاد کرده و برای آن مسیریابی از نوع page@ هم تعریف کنیم. یعنی نه‌فقط قصد داریم آن‌را توسط RazorComponentResult رندر کنیم، بلکه می‌خواهیم اگر آدرس آن‌را در مرورگر هم وارد کردیم، قابل دسترسی باشد.
به همین جهت یک پوشه‌ی جدید را به نام Components در ریشه‌ی پروژه‌ی Web API جاری ایجاد می‌کنیم، با این محتوا:
برای ایده گرفتن از محتوای مورد نیاز، به «معرفی قالب‌های جدید شروع پروژه‌های Blazor در دات نت 8» قسمت دوم این سری مراجعه کرده و برای مثال قالب ساده‌ترین حالت ممکن را توسط دستور زیر تولید می‌کنیم (در یک پروژه‌ی مجزا، خارج از پروژه‌ی جاری):
dotnet new blazor --interactivity None
پس از اینکار، محتویات پوشه‌ی Components آن‌را مستقیما داخل پوشه‌ی پروژه‌ی Minimal-API جاری کپی می‌کنیم. یعنی در نهایت در این پروژه‌ی جدید Web API، به فایل‌های زیر می‌رسیم:
- فایل Imports.razor_ ساده شده برای سهولت کار با فضاهای نام در کامپوننت‌های Blazor (فضاهای نامی را که در آن وجود ندارند و مرتبط با پروژه‌ی دوم هستند، حذف می‌کنیم).
- فایل App.razor، برای تشکیل نقطه‌ی آغازین برنامه‌ی Blazor.
- فایل Routes.razor برای معرفی مسیریابی صفحات Blazor تعریف شده.
- پوشه‌ی Layout برای معرفی فایل MainLayout.razor که در Routes.razor استفاده شده‌است.

و ... یک فایل آزمایشی جدید به نام Components\Tests\SsrTest.razor با محتوای زیر:
@page "/ssr-page/{Data:int}"

<PageTitle>An SSR component</PageTitle>

<h1>An SSR component rendered by a Minimal-API!</h1>

<div>
    Data: @Data
</div>

@code {

    [Parameter]
    public int Data { get; set; }

}
این فایل، می‌تواند پارامتر Data را از طریق فراخوانی مستقیم آدرس فرضی http://localhost:5227/ssr-page/2 دریافت کند و یا ... از طریق خروجی جدید RazorComponentResult که توسط یک Endpoint سفارشی ارائه می‌شود:




تغییرات مورد نیاز در فایل Program.cs برنامه‌ی Web-API برای فعالسازی رندر سمت سرور Blazor

در ادامه کل تغییرات مورد نیاز جهت اجرای این برنامه را مشاهده می‌کنید:
var builder = WebApplication.CreateBuilder(args);

// ...

builder.Services.AddRazorComponents();

// ...

// http://localhost:5227/ssr-component?data=2
// or it can be called directly http://localhost:5227/ssr-page/2
app.MapGet("/ssr-component",
           (int data = 1) =>
           {
               var parameters = new Dictionary<string, object?>
                                {
                                    { nameof(SsrTest.Data), data },
                                };
               return new RazorComponentResult<SsrTest>(parameters);
           });

app.UseStaticFiles();
app.UseAntiforgery();

app.MapRazorComponents<App>();
app.Run();

// ...
توضیحات:
- همین اندازه تغییر در جهت فعالسازی رندر سمت سرور کامپوننت‌های Blazor در یک برنامه‌ی ASP.NET Core کفایت می‌کند. یعنی اضافه شدن:
AddRazorComponents ،UseAntiforgery و MapRazorComponents
- در اینجا نحوه‌ی ارسال پارامترها را به یک RazorComponentResult نیز مشاهده می‌کنید.
- در حالت فراخوانی از طریق مسیر endpoint (یعنی فراخوانی مسیر http://localhost:5227/ssr-component در مثال فوق)، خود کامپوننت فراخوانی شده، بدون layout تعریف شده‌ی در فایل App.razor، رندر می‌شود. علت اینجا است که layout برنامه به همراه کامپوننت Router و RouteView آن فعال می‌شود که این دو هم مختص به صفحات دارای مسیریابی Blazor هستند و برای رندر کامپوننت‌های خالص آن بکار گرفته نمی‌شوند. خروجی RazorComponentResult تنها یک static SSR خالص است؛ مگر اینکه فایل blazor.web.js را نیز بارگذاری کند.

یک نکته: اگر در حالت رندر توسط RazorComponentResult، علاقمند به استفاده‌ی از layout هستید، می‌توان از کامپوننت LayoutView داخل یک کامپوننت فرضی به صورت زیر استفاده کرد؛ اما این مورد هم شامل اطلاعات فایل App.razor نمی‌شود:
<LayoutView Layout="@typeof(MainLayout)">
    <PageTitle>Home</PageTitle>

    <h2>Welcome to your new app.</h2>
</LayoutView>


سؤال: آیا در این حالت کامپوننت‌های تعاملی هم کار می‌کنند؟

پاسخ: بله. فقط برای ایده گرفتن، یک نمونه پروژه‌ی تعاملی Blazor 8x را در ابتدا ایجاد کنید و قسمت‌های اضافی AddRazorComponents و MapRazorComponents آن‌را در اینجا کپی کنید؛ یعنی برای مثال جهت فعالسازی کامپوننت‌های تعاملی Blazor Server، به این دو تغییر زیر نیاز است:
// ...

builder.Services.AddRazorComponents()
       .AddInteractiveServerComponents();

// ...

app.MapRazorComponents<App>().AddInteractiveServerRenderMode();

// ...
همچنین باید دقت داشت که امکانات تعاملی، به دلیل وجود و دسترسی به یک سطر ذیل که در فایل Components\App.razor واقع شده، اجرایی می‌شوند:
<script src="_framework/blazor.web.js"></script>
و همانطور که عنوان شد، اگر از روش new RazorComponentResult استفاده می‌شود، باید این سطر را به صورت دستی اضافه‌کرد؛ چون به همراه رندر layout تعریف شده‌ی در فایل App.razor نیست. برای مثال فرض کنید کامپوننت معروف Counter را به صورت زیر داریم که حالت رندر آن به InteractiveServer تنظیم شده‌است:
@rendermode InteractiveServer

<h1>Counter</h1>

<p role="status">Current count: @_currentCount</p>

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

@code {
    private int _currentCount;

    private void IncrementCount()
    {
        _currentCount++;
    }

}
در این حالت پس از تعریف endpoint زیر، خروجی آن فقط یک صفحه‌ی استاتیک SSR خواهد بود و دکمه‌ی Click me آن کار نمی‌کند:
// http://localhost:5227/server-interactive-component
app.MapGet("/server-interactive-component", () => new RazorComponentResult<Counter>());
علت اینجا است که اگر به سورس HTML رندر شده مراجعه کنیم، خبری از درج اسکریپت blazor.web.js در انتهای آن نیست. به همین جهت برای مثال فایل جدید CounterInteractive.razor را به صورت زیر اضافه می‌‌کنیم که ساختار آن شبیه به فایل App.razor است:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Interactive server component</title>
    <base href="/"/>
</head>
<body>
   <h1>Interactive server component</h1>

   <Counter/>

  <script src="_framework/blazor.web.js"></script>
</body>
</html>
و هدف اصلی از آن، تشکیل یک قالب و درج اسکریپت blazor.web.js در انتهای آن است.
سپس تعریف endpoint متناظر را به صورت زیر تغییر می‌دهیم:
// http://localhost:5227/server-interactive-component
app.MapGet("/server-interactive-component", () => new RazorComponentResult<CounterInteractive>());
اینبار به علت بارگذاری فایل blazor.web.js، امکانات تعاملی کامپوننت Counter فعال شده و قابل استفاده می‌شوند.


سؤال: آیا می‌توان این خروجی static SSR کامپوننت‌های بلیزر را در سرویس‌های یک برنامه ASP.NET Core هم دریافت کرد؟

منظور این است که آیا می‌توان از یک کامپوننت Blazor، به همراه تمام پیشرفت‌های Razor در آن که در Viewهای MVC قابل دسترسی نیستند، به‌شکل یک رشته‌ی خالص، خروجی گرفت و برای مثال از آن به‌عنوان قالب پویای محتوای ایمیل‌ها استفاده کرد؟
پاسخ: بله! زیر ساخت RazorComponentResult که از سرویس HtmlRenderer استفاده می‌کند، بدون نیاز به برپایی یک endpoint هم قابل دسترسی است:
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;

namespace WebApi8x.Services;

public class BlazorStaticRendererService
{
    private readonly HtmlRenderer _htmlRenderer;

    public BlazorStaticRendererService(HtmlRenderer htmlRenderer) => _htmlRenderer = htmlRenderer;

    public Task<string> StaticRenderComponentAsync<T>() where T : IComponent
        => RenderComponentAsync<T>(ParameterView.Empty);

    public Task<string> StaticRenderComponentAsync<T>(Dictionary<string, object?> dictionary) where T : IComponent
        => RenderComponentAsync<T>(ParameterView.FromDictionary(dictionary));

    private Task<string> RenderComponentAsync<T>(ParameterView parameters) where T : IComponent =>
        _htmlRenderer.Dispatcher.InvokeAsync(async () =>
                                             {
                                                 var output = await _htmlRenderer.RenderComponentAsync<T>(parameters);
                                                 return output.ToHtmlString();
                                             });
}
برای کار با آن، ابتدا باید سرویس فوق را به صورت زیر ثبت و معرفی کرد:
builder.Services.AddScoped<HtmlRenderer>();
builder.Services.AddScoped<BlazorStaticRendererService>();
و سپس یک نمونه مثال فرضی نحوه‌ی تزریق و فراخوانی سرویس BlazorStaticRendererService به صورت زیر است که در آن روش ارسال پارامترها هم بررسی شده‌است:
app.MapGet("/static-renderer-service-test",
           async (BlazorStaticRendererService renderer, int data = 1) =>
           {
               var parameters = new Dictionary<string, object?>
                                {
                                    { nameof(SsrTest.Data), data },
                                };
               var html = await renderer.StaticRenderComponentAsync<SsrTest>(parameters);
               return Results.Content(html, "text/html");
           });

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

برای این کار روش استاندارد و مستقیمی وجود ندارد؛ اما  با استفاده از بعضی از روشها مانند ارسال عکس مخفی، برخی تگ‌هایی که یک آدرس را فراخوانی می‌کنند (مانند bgsound)، کلیک روی لینک و یا ترکیبی از چند روش می‌توان تا حدود زیادی به اهداف فوق رسید.

روشی که بررسی خواهیم کرد، مبتنی بر ارسال عکس بوده و برای شروع، به یک عکس بسیار کوچک (1X1) با حجم کم و غیرقابل دید (Transparent) نیاز داریم که می‌توانید یکی از آنها را از این آدرس انتخاب کنید. در این روش به همراه ایمیل، این عکس را ارسال خواهیم کرد (در تگ img). عکس باید بگونه‌ای باشد که ظاهر ایمیل را تحت تأثیر قرار ندهد و آدرس عکس باید طوری تنظیم شود که به برنامه ما یک درخواست را ارسال کند و برنامه با توجه به درخواست ارسال شده، عملیات لازم را انجام دهد. 

مثالی از عکسی که باید ارسال شود: 

<img src='http://www.example.com/1x1.gif' style='width:1px;height:1px;'>

برای شروع یک برنامه‌ی ASP.NET Core را ایجاد کرده و مراحل ارسال ایمیل را طی نمایید:
private bool SendEmail(string mail_to, string mail_subject, string mail_body)
{
    bool result = false;
    try
    {
      SmtpClient client = new SmtpClient("smtp.gmail.com");
      client.UseDefaultCredentials = false;
      client.EnableSsl = true;
      client.Port = 587;
      client.Credentials = new NetworkCredential("from@mail.com", "your_gmail_password");
      MailMessage mailMessage = new MailMessage();       mailMessage.From = new MailAddress("from@mail.com");       mailMessage.To.Add(mail_to);       mailMessage.Body = mail_body;       mailMessage.Subject = mail_subject;       client.Send(mailMessage);       result = true;      }      catch (Exception ex)      {       result = false;      }      return result; }
public IActionResult SendEmailWithTransparentImage()
{
   var email_body = "your_email_body";            
   var imageUrl = Url.Action("ImageRequestFromEmail", "Home", new {user_id=12345}, protocol: Url.ActionContext.HttpContext.Request.Scheme);            
   var imageTag = $"<img src='{imageUrl}' style='width:1px;height:1px;'>";            
   email_body += imageTag;
   var result = SendEmail("to@email.com", "test image", email_body);
   return View(result);
}

سپس اکشن متد پاسخ به درخواست عکس از طرف سرویس دهنده ایمیل را ایجاد نمایید:
[Route("1x1.gif")]
public IActionResult ImageResponse()
{
   //درخواست عکس ارسال شده و اینجا باید عملیات دلخواه را انجام دهیم
   var emailOpenDate = DateTime.Now;
            
   //سپس عکس را در جواب درخواست ارسال میکنیم
   byte[] imagegBytes = Convert.FromBase64String("R0lGODlhAQABAAAAACH5BAEAAAAALAAAAAABAAEAAAI=");
   return File(imagegBytes, "image/gif");
}
اگر اکشن SendEmailWithTransparentImage فراخوانی شود، یک ایمیل که حاوی عکس مورد نظر است، ارسال می‌شود. کاربر با باز کردن ایمیل باعث می‌شود سرویس دهنده ایمیل، درخواست خواندن عکس را به برنامه شما داده و اکشنی را که برای پاسخ به درخواست ارسال عکس نوشته شده، فراخوانی کند. از این طریق متوجه خواهیم شد که ایمیل توسط کاربر باز و مشاهده شده‌است.
بدیهی است می‌توان یک سری کوئری استرینگ را برای بهتر شدن فرآیند، به آدرس عکس مورد نظر اضافه کرد:
<img src='http://www.example.com/1x1.gif?user_id=12345' style='width:1px;height:1px;'>

در این صورت باید تغییراتی را در کدها لحاظ کنیم:
در اکشن SendEmailWithTransparentImage  :
var imageUrl = Url.Action("ImageResponse, "Home", new {user_id=12345},protocol: Url.ActionContext.HttpContext.Request.Scheme);
و امضای اکشن ImageResponse را به‌صورت زیر تغییر می‌دهیم:
public IActionResult ImageResponse(int user_id)

استفاده از عکس‌های کوچک به علت سربار کم و پشتیبانی مرورگرها، بسیار مرسوم بوده و بسیاری از آنالیزورهای سایت مانند google analytics، سایت‌های ارسال انبوه ایمیل و ...  برای رهگیری رفتار کاربران از این روش استفاده می‌کنند. 
برای مثال اگر در همین سایت Developer Tools مرورگر را باز کنید و صفحه را رفرش کنید در تب Network، با کمی دقت عکسی با کوئری استرینگ پیچیده خواهید یافت که مربوط به google analytics است و تقریبا تمام اطلاعات مورد نیاز خود در رابطه با رفتار کاربران در سایت جاری را با استفاده از این روش جمع آوری می‌کند.
مطالب دوره‌ها
مدل سازی داده‌ها در RavenDB
در مطلب جاری، به صورت اختصاصی، مبحث مدل سازی اطلاعات و رسیدن به مدل ذهنی مرسوم در طراحی‌های NoSQL سندگرا را در مقایسه با دنیای Relational، بررسی خواهیم کرد.


تفاوت‌های دوره ما با زمانیکه بانک‌های اطلاعاتی رابطه‌ای پدیدار شدند

- دنیای بانک‌های اطلاعاتی رابطه‌ای برای Write بهینه سازی شده‌اند؛ از این جهت که تاریخچه پیدایش آن‌ها به دهه 70 میلادی بر می‌گردد، زمانیکه برای تهیه سخت دیسک‌ها باید هزینه‌های گزافی پرداخت می‌شد. به همین جهت الگوریتم‌ها و روش‌های بسیاری در آن دوره ابداع شدند تا ذخیره سازی اطلاعات، حجم کمتری را به خود اختصاص دهند. اینجا است که مباحثی مانند Normalization بوجود آمدند تا تضمین شود که داده‌ها تنها یکبار ذخیره شده و دوبار در جاهای مختلفی ذخیره نگردند. جهت اطلاع در سال 1980 میلادی، یک سخت دیسک 10 مگابایتی حدود 4000 دلار قیمت داشته است.
- تفاوت مهم دیگر دوره ما با دهه‌های 70 و 80 میلادی، پدیدار شدن UI و روابط کاربری بسیار پیچیده، در مقایسه با برنامه‌های خط فرمان یا حداکثر فرم‌های بسیار ساده ورود اطلاعات در آن زمان است. برای مثال در دهه 70 میلادی تصور UI ایی مانند صفحه ابتدایی سایت Stack overflow احتمالا به ذهن هم خطور نمی‌کرده است.


تهیه چنین UI ایی نه تنها از لحاظ طراحی، بلکه از لحاظ تامین داده‌ها از جداول مختلف نیز بسیار پیچیده است. برای مثال برای رندر صفحه اول سایت استک اورفلو ابتدا باید تعدادی سؤال از جدول سؤالات واکشی شوند. در اینجا در ذیل هر سؤال نام شخص مرتبط را هم مشاهده می‌کنید. بنابراین اطلاعات نام او، از جدول کاربران نیز باید دریافت گردد. یا در اینجا تعداد رای‌های هر سؤال را نیز مشاهده می‌کنید که به طور قطع اطلاعات آن در جدول دیگری نگه داری می‌شود. در گوشه‌ای از صفحه، برچسب‌های مورد علاقه و در ذیل هر سؤال، برچسب‌های اختصاصی هر مطلب نمایش داده شده‌اند. تگ‌ها نیز در جدولی جداگانه قرار دارند. تمام این قسمت‌های مختلف، نیاز به واکشی و رندر حجم بالایی از اطلاعات را دارند.
- تعداد کاربران برنامه‌ها در دهه‌های 70 و 80 میلادی نیز با دوره ما متفاوت بوده‌اند. اغلب برنامه‌های آن دوران تک کاربره طراحی می‌شدند؛ با بانک‌های اطلاعاتی که صرفا جهت کار بر روی یک سیستم طراحی شده بودند. اما برای نمونه سایت استک اور فلویی که مثال زده شده، توسط هزاران و یا شاید میلیون‌ها نفر مورد استفاده قرار می‌گیرد؛ با توزیع و تقسیم اطلاعات آن بر روی سرورها مختلف.


معرفی مفهوم Unit of change

همین پیچیدگی‌ها سبب شدند تا جهت ساده‌سازی حل اینگونه مسایل، حرکتی به سمت دنیای NoSQL شروع شود. ایده اصلی مدل سازی داده‌ها در اینجا کم کردن تعداد اعمالی است که باید جهت رسیدن به یک نتیجه واحد انجام داد. اگر قرار است یک سؤال به همراه تگ‌ها، اطلاعات کاربر، رای‌ها و غیره واکشی شوند، چرا باید تعداد اعمال قابل توجهی جهت مراجعه به جداول مختلف مرتبط صورت گیرد؟ چرا تمام این اطلاعات را یکجا نداشته باشیم تا بتوان همگی را در طی یک واکشی به دست آورد و به این ترتیب دیگر نیازی نباشد انواع و اقسام JOIN‌ها را به چند ده جدول موجود نوشت؟
اینجا است که مفهومی به نام Unit of change مطرح می‌شود. در هر واحد تغییر، کلیه اطلاعات مورد نیاز برای رندر یک شیء قرار می‌گیرند. برای مثال اگر قرار است با شیء محصول کار کنیم، تمام اطلاعات مورد نیاز آن‌‌را اعم از گروه‌ها، نوع‌ها، رنگ‌ها و غیره را در طی یک سند بانک اطلاعاتی NoSQL سندگرا، ذخیره می‌کنیم.


محدود‌ه‌های تراکنشی یا Transactional boundaries

محدوده‌های تراکنشی در Domain driven design به Aggregate root نیز معروف است. هر محدود تراکنشی حاوی یک Unit of change قرار گرفته داخل یک سند است. ابتدا بررسی می‌کنیم که در یک Read به چه نوع اطلاعاتی نیاز داریم و سپس کل اطلاعات مورد نیاز را بدون نوشتن JOIN ایی از جداول دیگر، داخل یک سند قرار می‌دهیم.
هر محدوده تراکنشی می‌تواند به محدوده تراکنشی دیگری نیز ارجاع داده باشد. برای مثال در RavenDB شماره‌های اسناد، یک سری رشته هستند؛ برخلاف بانک‌های اطلاعاتی رابطه‌ای که بیشتر از اعداد برای مشخص سازی Id استفاده می‌کنند. در این حالت برای ارجاع به یک کاربر فقط کافی است برای مثال مقدار خاصیت کاربر یک سند به "users/1" تنظیم شود. "users/1" نیز یک Id تعریف شده در RavenDB است.
مزیت این روش، سرعت واکشی بسیار بالای دریافت اطلاعات آن است؛ دیگر در اینجا نیازی به JOINهای سنگین به جداول دیگر برای تامین اطلاعات مورد نیاز نیست و همچنین در ساختار‌های پیچیده‌تری مانند ساختارهای تو در تو، دیگر نیازی به تهیه کوئری‌های بازگشتی و استفاده از روش‌های پیچیده مرتبط با آن‌ها نیز وجود ندارد و کلیه اطلاعات مورد نظر، به شکل یک شیء JSON داخل یک سند حاضر و آماده برای واکشی در طی یک Read هستند.
به این ترتیب می‌توان به سیستم‌های مقیاس پذیری رسید. سیستم‌هایی که با بالا رفتن حجم اطلاعات در حین واکشی‌های داده‌های مورد نیاز، کند نبوده و بسیار سریع پاسخ می‌دهند.


Denormalization داده‌ها

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


ملاحظات اندازه‌های داده‌ها

زمانیکه سند‌ها بسیار بزرگ می‌شوند چه رخ خواهد داد؟ از لحاظ اندازه داده‌ها سه نوع سند را می‌توان متصور بود:
الف) سندهای محدود، مانند اغلب اطلاعاتی که تعداد فیلدهای مشخصی دارند با تعداد اشیاء مشخصی.
ب) سندهای نامحدود اما با محدودیت طبیعی. برای مثال اطلاعات فرزندان یک شخص را درنظر بگیرید. هرچند این اطلاعات نامحدود هستند، اما به صورت طبیعی می‌توان فرض کرد که سقف بالایی آن عموما به 20 نمی‌رسد!
ج) سندهای نامحدود، مانند سندهایی که آرایه‌ای از اطلاعات را ذخیره می‌کنند. برای مثال در یک سایت فروشگاه، اطلاعات فروش یک گروه از اجناس خاص را درنظر بگیرید که عموما نامحدود است. اینجا است که باید به اندازه اسناد نیز دقت داشت. برای مدیریت این مساله حداقل از دو روش استفاده می‌شود:
- محدود کردن تعداد اشیاء. برای مثال در هر سند حداکثر 100 اطلاعات فروش یک محصول بیشتر ثبت نشود. زمانیکه به این حد رسیدیم، یک سند جدید ایجاد شده و Id سند قبلی مثلا "products/1" در سند دوم ذکر خواهد شد.
- محدود کردن تعداد اطلاعات ذخیره شده بر اساس زمان
RavenDB برای مدیریت این مساله، مفهوم Includes را معرفی کرده است. در اینجا با استفاده از متد الحاقی Include، کار زنجیر کردن سندهای مرتبط صورت خواهد گرفت.



یک مثال عملی: مدل سازی داده‌های یک بلاگ در RavenDB

پس از این بحث مقدماتی که جهت معرفی ذهنیت مدل سازی داده‌ها در دنیای غیر رابطه‌ای NoSQL ضروری بود، در ادامه قصد داریم مدل‌های داده‌های یک بلاگ را سازگار با ساختار بانک اطلاعاتی NoSQL سندگرای RavenDB طراحی کنیم.
در یک بلاگ، تعدادی مطلب، نظر، برچسب (گروه‌های مطالب) و امثال آن وجود دارند. اگر بخواهیم این اطلاعات را به صورت رابطه‌ای مدل کنیم، به ازای هر کدام از این موجودیت‌ها یک جدول نیاز خواهد بود و برای رندر صفحه اصلی بلاگ، چندین و چند کوئری برای نمایش اطلاعات مطالب، نویسنده(ها)، برچسب‌ها و غیره باید به بانک اطلاعاتی ارسال گردد، که تعدادی از آن‌ها مستقیما بر روی یک جدول اجرا می‌شوند و تعدادی دیگر نیاز به JOIN دارند.
مشکلاتی که روش رابطه‌ای دارد:
- تعداد اعمالی که باید برای نمایش صفحه اول سایت صورت گیرد، بسیار زیاد است و این مساله با تعداد بالای کاربران از دید مقیاس پذیری سیستم مشکل ساز است.
- داده‌های مرتبط در جداول مختلفی پراکنده‌اند.
- این سیستم برای Write بهینه سازی شده است و نه برای Read. (همان بحث گران بودن سخت دیسک‌ها در دهه‌های قبل که در ابتدای بحث به آن اشاره شد)

مدل سازی سازگار با دنیای NoSQL یک بلاگ

در اینجا چند کلاس مقدماتی را مشاهده می‌کنید که تعریف آن‌ها به همین نحو صحیح است و نیاز به جزئیات و یا روابط بیشتری ندارند.
namespace RavenDBSample01.BlogModels
{
    public class BlogConfig
    {
        public string Id { set; get; }
        public string Title { set; get; }
        public string Description { set; get; }
        // ... more items here
    }

    public class User
    {
        public string Id { set; get; }
        public string FullName { set; get; }
        public string Email { set; get; }
        // ... more items here
    }
}
اما کلاس مطالب بلاگ را به چه صورتی طراحی کنیم؟ هر مطلب، دارای تعدادی نظر خواهد بود. اینجا است که بحث unit of change مطرح می‌شود و درج اطلاعاتی که در طی یک read نیاز است از بانک اطلاعاتی جهت رندر UI واکشی شوند. به این ترتیب به این نتیجه می‌رسیم که بهتر است کلیه کامنت‌های یک مطلب را داخل همان شیء مطلب مرتبط قرار دهیم. از این جهت که یک نظر، خارج از یک مطلب بلاگ دارای مفهوم نیست.
اما این طراحی نیز یک مشکل دارد. درست است که ساختار یک صفحه مطلب، از مطالب وبلاگ به همین نحوی است که توضیح داده شد؛ اما در صفحه اول سایت، هیچگاه کامنت‌های مطالب درج نمی‌شوند. بنابراین نیازی نیست تا تمام کامنت‌ها را داخل یک مطلب ذخیره کرد. به این ترتیب برای نمایش صفحه اول سایت، حجم کمتری از اطلاعات واکشی خواهند شد.
    public class Post
    {
        public string Id { set; get; }
        public string Title { set; get; }
        public string Body { set; get; }

        public ICollection<string> Tags { set; get; }

        public string AuthorId { set; get; }

        public string PostCommentsId { set; get; }
        public int CommentsCount { set; get; }
    }

    public class Comment
    {
        public string Id { set; get; }
        public string Body { set; get; }
        public string AuthorName { set; get; }
        public DateTime CreatedAt { set; get; }
    }

    public class PostComments
    {
        public List<Comment> Comments { set; get; }
        public string LastCommentId { set; get; }
    }
در اینجا ساختار Post و Commentهای بلاگ را مشاهده می‌کنید. جایی که ذخیره سازی اصلی کامنت‌ها صورت می‌گیرد در شیء PostComments است. یعنی PostCommentsId شیء Post به یک وهله از شیء PostComments که حاوی کلیه کامنت‌های آن مطلب است، اشاره می‌کند.
به این ترتیب برای نمایش صفحه اول سایت، فقط یک کوئری صادر می‌شود. برای نمایش یک مطلب و کلیه کامنت‌های متناظر با آن دو کوئری صادر خواهند شد.

بنابراین همانطور که مشاهده می‌کنید، در دنیای NoSQL، طراحی مدل‌های داده‌ای بر اساس «سناریوهای Read» صورت می‌گیرد و نه صرفا طراحی یک مدل رابطه‌ای بهینه سازی شده برای حالت Write.

سورس کامل ASP.NET MVC این بلاگ‌را که «راکن بلاگ» نام دارد، از GitHub نویسندگان اصلی RavenDB می‌توانید دریافت کنید.
مطالب
پَرباد - آموزش پیاده‌سازی پرداخت آنلاین در دات نت - آموزش پیشرفته
در قسمت قبل، با تنظیمات پَرباد آشنا شدیم. در این مقاله قصد داریم سایر امکانات قابل استفاده را آموزش دهیم.

آنچه شما در این مقاله یاد خواهید گرفت:

  • ایجاد صورت حساب پرداخت با استفاده از InvoiceBuilder
  • درگاه مجازی
  • استفاده از پروکسی
  • توکن پرداخت
  • تزریق وابستگی
  • Logging


ایجاد صورت حساب با استفاده از InvoiceBuilder

InvoiceBuilder به شما کمک می‌کند تا یک صورت حساب را جهت پرداخت آماده کنید.
مثال زیر را در نظر بگیرید:
var result = _onlinePayment.Request(Gateways.Mellat, 123, 25000, "http://www.mywebsite.com/foo/bar/");
همانطور که مشخص است، در این مثال یک صورت حساب با شماره رهگیری ۱۲۳ به مبلغ ۲۵۰۰۰ با یک آدرس بازگشتی به درگاه بانک ملت درخواست داده می‌شود.

اما همین دستور را با کمک InvoiceBuilder نیز می‌توان ایجاد کرد.
نمونه مثال بالا با استفاده از InvoiceBuilder
var result = _onlinePayment.Request(invoice =>
{
    invoice
          .SetTrackingNumber(123)
          .SetAmount(25000)
          .SetCallbackUrl("http://www.mywebsite.com/foo/bar/")
          .UseGateway(Gateways.Mellat);
});

مسلما استفاده از روش اول ساده‌تر به نظر می‌رسد. اما InvoiceBuilder امکانات دیگری را نیز به شما جهت ایجاد یک صورت حساب می‌دهد. در واقع InvoiceBuilder ایجاد یک صورت حساب را به هر روشی که در نظر داشته باشید برای شما میسر می‌کند و همچنین قابل توسعه در اپلیکیشن شما توسط خودتان است.

در زیر نمونه‌هایی از کارایی آن را بررسی می‌کنیم.

  • تولید اتوماتیک کد رهگیری به صورت افزایشی
  • تولید اتوماتیک کد رهگیری به صورت تصادفی
  • ایجاد یک تولید کننده کد رهگیری توسط شما
  • صورت حساب سفارشی برای امکانات اختصاصی درگاه‌های بانکی

تولید اتوماتیک کد رهگیری به صورت افزایشی

در این روش، کد رهگیری (TrackingNumber) که مورد نیاز درگاه‌های بانکی است، به صورت اتوماتیک در هنگام ایجاد درخواست پرداخت، توسط پَرباد تولید می‌شود.
var result = _onlinePayment.Request(invoice =>
{
    invoice
          .UseAutoIncrementTrackingNumber()
          .SetAmount(25000)
          .SetCallbackUrl("http://www.mywebsite.com/foo/bar/")
          .UseGateway(Gateways.Mellat);
});
کد تولید شده، به صورت افزایشی است. در واقع در هر درخواست پرداخت جدید، یک کد رهگیری تولید می‌شود که یک واحد از کد تولید شده‌ی قبلی بیشتر است. 

شما همچنین می‌توانید مقدار اولیه این عدد را جهت شروع تولید، در پارامتر متد تعیین کنید. این مقدار همچنین در قسمت تنظیمات پَرباد نیز توسط متد ConfigureAutoTrackingNumber قابل تنظیم است.

تولید اتوماتیک کد رهگیری به صورت تصادفی

var result = _onlinePayment.Request(invoice =>
{
    invoice
          .UseAutoRandomTrackingNumber()
          .SetAmount(25000)
          .SetCallbackUrl("http://www.mywebsite.com/foo/bar/")
          .UseGateway(Gateways.Mellat);
});
در این روش کد رهگیری، به صورت تصادفی در محدوده Int64 توسط پَرباد تولید خواهد شد. کد‌های تولید شده در این روش تقریبا ٪۹۹.۹ غیر تکراری هستند. اما اگر به تمیز بودن کدهای تولید شده اهمیت می‌دهید، بهتر است از روش AutoIncrement که بالاتر توضیح داده شد، استفاده کنید.

ایجاد یک تولید کننده کد رهگیری توسط شما

اگر بنا به دلایلی قصد دارید خودتان نیز یک منبع تولید کد رهگیری را ایجاد کنید، می‌توانید به روش زیر عمل کنید.
public class MyTrackingNumberProvider : ITrackingNumberProvider
{
    public Task<long> ProvideAsync(CancellationToken cancellationToken = new CancellationToken())
    {
        // تولید و برگشت کد در اینجا
    }
}

نکته ۱: شما همچنین می‌توانید در منبع خود، از تزریق وابستگی‌ها نیز استفاده کنید. بدیهی است سرویسی را که تزریق می‌کنید، باید از قبل توسط سیستم تزریق وابستگی‌های اپلیکیشن شما، ثبت شده باشد.
نکته ۲؛ شما همچنین می‌توانید بدون ایجاد هیچ منبعی، به راحتی از متد SetTrackingNumber در InvoiceBuilder (که بالاتر توضیح داده شده) استفاده کنید.

سپس در هنگام ایجاد درخواست پرداخت به روش زیر از منبع خود استفاده کنید:
var result = _onlinePayment.Request(invoice =>
{
    invoice
          .UseTrackingNumberProvider<MyTrackingNumberProvider>()
          //  یا 
          .UseTrackingNumberProvider(new MyTrackingNumberProvider())
          //  یا 
          .UseTrackingNumberProvider(services => new MyTrackingNumberProvider())
});
همانطور که می‌بینید، متد‌های مختلفی جهت استفاده از منبع مورد نظر شما موجود است.

صورت حساب سفارشی برای امکانات اختصاصی درگاه‌های بانکی

درگاه‌های بانکی علاوه بر سرویس‌های پرداخت عادی، امکانات مختلفی دیگری را نیز ارائه می‌کنند. برای مثال بانک ملت دارای سرویسی به نام "باشگاه مشتریان بانک ملت" است که امکان واریز مبلغ را از حساب مشتری، به چندین حساب مختلف می‌دهد.
نکته مهم: همانطور که می‌دانید قبل از استفاده از این گونه سرویس‌ها، کلیه شماره حساب‌هایی که قصد واریز مبلغ به آنها را دارید، باید از قبل (در هنگام عقد قرارداد با بانک) به بانک مورد نظر داده شده باشد. در غیر اینصورت امکان استفاده از این گونه سرویس‌ها را ندارید.
نکته: در هنگام نوشتار این مقاله، این سرویس هنوز در پَرباد آماده نیست و در حال توسعه است.


درگاه مجازی

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

ابتدا در قسمت تنظیمات پَرباد،  آدرس مورد نظر برای درگاه مجازی را مانند کد زیر مشخص می‌کنیم:
services.AddParbad()
        .ConfigureGateways(gateways =>
        {
            gateways
               .AddParbadVirtual()
               .WithOptions(options => options.GatewayPath = "/MyVirtualGateway");
        });


در مثال بالا، درگاه مجازی توسط آدرس داده شده در دسترس خواهد بود. توجه داشته باشید که این آدرس حتما باید با یک ( / ) آغاز شده باشد.
سپس درگاه مجازی را در اپلیکیشن خود ثبت می‌کنید:
ASP.NET CORE (Startup.cs)
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseMvc(routes =>
    {
        routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
    });

    // ثبت درگاه مجازی
    app.UseParbadVirtualGateway();
}

ASP.NET WebForms, ASP.NET MVC (Startup.cs)
public void Configuration(IAppBuilder app)
{
    var parbad = ParbadBuilder.CreateDefaultBuilder()
        .ConfigureGateways(gateways =>
        {
            gateways
               .AddParbadVirtual()
               .WithOptions(options => options.GatewayPath = "/MyVirtualGateway");
        })
        .Build();

    app.UseParbadVirtualGateway(parbad.Services);
}


تنظیمات درگاه مجازی تا اینجا به پایان رسیده و فقط در هنگام ایجاد درخواست پرداخت، از میان درگاه‌ها، درگاه مجازی پَرباد را انتخاب کنید.
var result = _onlinePayment.Request(Gateways.ParbadVirtualGateway, 123, 25000, "http://www.mywebsite.com/foo/bar/");

و در نهایت به درگاه مجازی هدایت خواهید شد:

نمونه پروژه‌های کامل را در انتهای مقاله می‌توانید مشاهده کنید.

استفاده از پروکسی

با توجه به تغییرات اخیر در بانکداری کشور، احتمال آن وجود دارد که در آینده، بعضی از درگاه‌های بانکی فقط به IP‌های داخل کشور سرویس دهی کنند. اگر وب سایت شما بنا به دلایلی در سروری خارج از کشور میزبانی می‌شود، شما جهت استفاده از درگاه‌های بانکی، نیاز به یک سرور در داخل کشور دارید که نقش پروکسی را برای شما بازی کند. پَرباد این امکان را برای شما محیا کرده و کافیست اطلاعات پروکسی سرور خود را به شکل زیر برای درگاه بانکی مورد نظر ثبت کنید.
services.AddParbad()
        .ConfigureGateways(gateways =>
        {
            gateways
               .AddMellat()
               .WithOptions(options => 
               {
                    options.TerminalId = 123;
                    options.UserName = "abc";
                    options.UserPassword = "xyz;
               )
               .WithProxy(new Uri("Proxy Server URL"), "UserName", "Password");
        });
در مثال بالا، درگاه بانک ملت (صرفا جهت مثال) با یک پروکسی تنظیم شده است. برای سایر درگاه‌های بانکی، فرمت تنظیمات، کاملا مشابه مثال بالا است.

توکن پرداخت

پَرباد جهت شناسایی و تبادل اطلاعات پرداخت با خارج از سیستم خود و بانک‌ها، از یک توکن به ازاء هر پرداخت استفاده می‌کند. به عبارت دیگر، به ازاء هر درخواست پرداخت یک توکن تولید می‌شود. به این صورت، درخواست‌های پرداخت، غیر قابل دستکاری و غیر قابل حدس زدن توسط کاربران می‌شود.
نکته: پَرباد از یک تولید کننده پیش فرض توکن استفاده می‌کند و شما نیازی به انجام هیچگونه تنظیماتی ندارید. تولید کننده پیش فرض، از یک GUID در Query String استفاده می‌کند.
اگر قصد دارید روش مورد نظر خود را برای تولید توکن جهت شناسایی یک پرداخت پیاده‌سازی کنید، می‌توانید به روش زیر عمل کنید:
ابتدا تولید کننده توکن را تعریف کنید.
public class MyTokenProvider : IPaymentTokenProvider
{
    public Task<string> ProvideTokenAsync(Invoice invoice, CancellationToken cancellationToken = new CancellationToken())
    {
        // تولید و برگرداندن توکن در اینجا
    }

    public Task<string> RetrieveTokenAsync(CancellationToken cancellationToken = new CancellationToken())
    {
        // خواندن و برگرداندن توکن در اینجا
    }
}

نکته: شما همچنین می‌توانید از تزریق وابستگی‌ها نیز استفاده کنید. بدیهی است که در اینصورت باید سرویسی که تزریق می‌کنید، از قبل در سیستم تزریق وابستگی‌های اپلیکیشن شما ثبت شده باشد.
سپس تولید کننده توکن خود را در تنظیمات به پَرباد معرفی کنید:
services.AddParbad()
        .ConfigurePaymentToken(builder => builder.AddPaymentTokenProvider<MyTokenProvider>(ServiceLifetime.Transient));

ServiceLifetime، تایین کننده طول عمر سرویس شما است.
به این صورت پَرباد، شناسایی و ردیابی یک صورت حساب را با استفاده از تولید کننده توکن شما انجام خواهد داد.


تزریق وابستگی

همانطور که قبلا در مقاله آموزش تنظیمات نیز گفته شد، پَرباد به صورت توکار، از تزریق وابستگی استاندارد مایکروسافت استفاده می‌کند. بنابراین اگر اپلیکیشن شما نیز از تزریق وابستگی مشابهی استفاده می‌کند، نیازی به خواندن و یاد گرفتن این بخش ندارید و به راحتی می‌توانید از اینترفیس IOnlinePayment در هر کجا که نیاز داشتید جهت عملیات پرداخت استفاده کنید.
اما در صورتیکه در اپلیکیشن خود از تزریق وابستگی دیگری ( مانند Autofac ) استفاده می‌کنید، باید این دو سیستم را با یکدیگر هماهنگ کنید. خوشبختانه تمام کتابخانه‌های معروف تزریق وابستگی ( مانند Autofac )  از قبل این کار را برای شما محیا کرده‌اند و شما فقط نیاز به افزودن چند خط کد به اپلیکیشن فعلی خود را دارید.
جهت فهم بهتر و آموزش عملی، یک اپلیکیشن کامل ASP.NET MVC برای شما تهیه شده که از Autofac جهت تزریق وابستگی استفاده می‌کند. در این پروژه خواهید دید چگونه به راحتی پَرباد و Autofac را با یکدیگر هماهنگ کرده و هچنین اینترفیس IOnlinePayment را درون کنترلر تزریق می‌کنیم.
لینک پروژه‌ها در انتهای همین مقاله قابل مشاهده هستند.


Logging
لاگ کردن در پَرباد توسط سیستم استاندارد مایکروسافت انجام می‌شود. این بدان معنی است که شما امکان استفاده از کتابخانه‌های Logging بسیار زیادی را دارید. نحوه استفاده و تنظیم کتابخانه‌های معروف Logging در وب سایت آنها آورده شده و به راحتی می‌توانید آنها را با لاگ مایکروسافت تطابق دهید.

نمونه پروژه‌ها
مقاله‌های مرتبط
مطالب
افزونه جی کوئری RowAdder
دیروز در یک برنامه میخواستم کاربر بتواند لیست مواد مصرفی یک کارخانه را ایجاد کند که نیاز بود کاربر بتواند از هر سطر به تعداد نامحدود ایجاد کند و برای انتخاب هر یک از مواد به همراه جزئیات آن یک سطر به لیست اضافه شود. برای اینکار میتوانیم با استفاده از فناوری جی کوئری اینکار را انجام دهیم ولی بهتر بود که این مورد به یک افزونه تبدیل میشد تا در دفعات بعدی بسیار راحت‌تر باشیم. جهت آشنایی با پلاگین نویسی بهتر هست این مقالات (+) را مطالعه فرمایید.

نحوه استفاده
نحوه استفاده از آن بسیار راحت است و در دموی html همراه آن به طور ساده در سه مثال توضیح داده شده است. ابتدا از این آدرس کتابخانه آن را دریافت کنید. این کتابخانه شامل یک فایل js که شامل کدهای پلاگین است، یک فایل css جهت تغییر استایل کدهایی است که پلاگین تولید میکند که اسامی آن دقیقا مشخص می‌کند که هر کلاس متعلق به چه بخشی است.

گام اول:
فایل‌های مورد نظر را بعد از صدا زدن کتابخانه‌ی جی کوئری صدا بزنید.
<link type="text/css" href="css/RowAdder.css" rel="stylesheet" />
    <script src="js/RowAdder.js" type="text/javascript"></script>


گام دوم :
 در تکه کدهای html، کدی را که قرار است در هر سطر تکرار شود، داخل یک div قرار داده و نامی مثل row-sample را برای آن قرار دهید (فعلا حتما این نام باشد)، بعدها پلاگین، کدهای داخل این تگ div را به عنوان هر سطر خواهد شناخت:
<div id="row-sample">
    <form style="margin: 0; padding: 0;">
        Name:<input type="text"/>
        <input type="radio" name="Gender" value="male" checked="checked">Male
        <input type="radio" name="Gender" value="female">Female
    </form>
</div>


گام سوم:
 سپس یک div دیگر ایجاد کنید و نامی مثل mypanel را به آن بدهید تا سطرهایی که ایجاد می‌شوند داخل این div قرار بگیرند.
<div id="mypanel"></div>

گام چهارم:
در بخش head یک تگ اسکریپت باز کرده و کدهای زیر را به آن اضافه می‌کنیم. این کد باعث می‌شود که پلاگین فعال شود.
<script>
$(document).ready(function() {
$("#mypanel").RowAdder();
});
</script>
گام پنجم:
 یک دکمه جهت افزودن سطر به صفحه اضافه می‌کنیم
<button id="addanotherform">Add New Form</button>

و در قسمت تگ اسکریپت هم کد زیر را اضافه می‌کنیم:
$("#addanotherform").on('click', function() {
                $("#mypanel").RowAdder('add');
            });

حال از صفحه تست می‌گیریم: با هر بار کلیک بر روی دکمه‌ی Add New Form یک سطر جدید ایجاد می‌گردد.


در تصویر بالا دکمه‌های دیگر هم دیده می‌شوند که به دیگر متدهای آن اشاره دارد:

جهت مخفی سازی:
 $("#mypanel").RowAdder('hide');

چهت نمایش:
$("#mypanel").RowAdder('show');

جهت افزودن سطر با کد:
$("#mypanel").RowAdder('add');

جهت دریافت تعداد سطرهای ایجاد شده:
$("#mypanel").RowAdder('count')


جهت دریافت کدهای یک سطر در اندیس x

$("#mypanel").RowAdder('content', 3)

جهت حذف یک سطر با اندیس x
$("#mypanel").RowAdder('remove', 3);

همانطور که با صدا زدن اولین متد پلاگین متوجه شدید و نتیجه‌ی آن را در دمو دیدید، این پلاگین از پیش فرض‌هایی جهت راه اندازی اولیه استفاده می‌کند که این پیش فرض‌ها عبارتند از تگ row-sample که بدون معرفی رسمی، آن را شناسایی کرد. همچنین ممکن است بخواهید عبارت Remove را با کلمه‌ی فارسی «حذف» جایگزین نمایید. برای اینکار می‌توانید پلاگین را به شکل زیر به کار ببرید:
    $("#mypanel").RowAdder({
                sample: '#my-custom-sample',
                type: 'text',
                value:'حذف'
        });

تغییر اولین پیش فرض، تغییر نام تگ row-sample به my-custom-sample بود و در مرحله‌ی بعد هم نام فارسی حذف را جایگزین remove کردیم. عبارت type به طور پیش فرض بر روی text قرار دارد که اجباری به ذکر آن در کد بالا نبود. ولی اگر دوست دارید که به جای نمایش عبارت حذف، از یک آیکن یا تصویر استفاده کنید، کد را به شکل زیر تغییر دهید:
  $("#mypanel").RowAdder({
                type: 'image',
                value: 'images/remove.png'
            });
در خطوط بالا عبارت type با image مقدار دهی شد و به پلاگین می‌گوید که به جای متن، از تصویر استفاده کن. همچنین value را به جای متن با آدرس تصویر مقداردهی کرده‌ایم و نتیجه را می‌توانید در دموی قرار گرفته در گیت هاب ببینید.

فایل RowAdder.css
در بردارنده هر سطر
.each-section {
    margin: 20px;
    padding: 5px;
}

جهت استایل بندی لینک چه تصویر و چه متن
.remove-link {
    color:#999;
     text-decoration: none;
}

a:hover.remove-link {
   color:#802727;
}
جهت تغییر استایل بر روی خود تصویر
.remove-image {
    
}

آشنایی با کد پلاگین
(function ($) {
    
    var settings = null;
  $.fn.RowAdder = function (method) {
    
            // call methods
            if (methods[method]) {
                return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
            } else if (typeof method === 'object' || !method) {
                return methods.init.apply(this, arguments);
            } else {
                $.error('Method ' + method + ' does not exist on jQuery.RowAdder');
            }
      

    };
})(jQuery);
در قسمت دوم آموزش پلاگین نویسی برای جی کوئری، متدها به طور واضح توضیح داده شده‌اند. این کدها وظیفه دارند متدهایی را که کاربر درخواست داده است، شناسایی و به همراه آرگومان‌های آن به سمت توابعی که به هر نام متد اختصاص داده‌ایم، ارسال کنند. در صورتیکه متدی با آرگومان‌های ناهماهنگی ارسال شوند، پیام خطایی ارسال می‌گردد و در صورتیکه تعریف نشود، به طور مستقیم init را صدا می‌زند. متغیر settings هم بعدا با تنظیمات پیش فرض پر می‌شود.

متدها
//methods
    var methods = {
        init: function (options) {
            //default-settings
             settings = $.extend({
                'sample': '#row-sample',
                'type': 'text',
                'value': 'Remove'
             }, options);
             this.attr('data-sample', settings.sample);
             this.attr('data-type', settings.type);
             this.attr('data-value', settings.value);
            Do(this);
        },
        show: function () {
            this.css("display", "inline");
        },
        hide: function () {
            this.css("display", "none");
        },
        add: function () {
            Do(this);
        },
        remove: function (index) {
            console.log(index);
           this.find(".each-section")[index].remove();
        },
        content: function (index) {
            return this.find(".each-section")[index];
        },
        count: function (index) {
            return this.find(".each-section").size();
        }
    };
متد init تنظیمات پیش فرض را دریافت می‌نماید و سپس بر روی المانی که پلاگین روی آن واقع شده‌است، مقادیر را ذخیره می‌کند تا در آینده با صدا زدن متدهای دیگر آن را استفاده نماید. کلمه‌ی this در واقع به تگی اشاره می‌کند که پلاگین روی آن اعمال شده است که در مثال‌های بالا mypanel نام داشت. متد Do تابع اصلی ما را در بر دارد که کدهای اصلی پلاگین را شامل می‌شود. مابقی متدها در واقع  جست و جویی بر المان‌ها هستند.

تابع Do
    function Do(panelDiv) {

        settings.sample = panelDiv.data('sample');
        settings.type = panelDiv.data('type');
        settings.value = panelDiv.data('value');
        //find sample code
        var rowsample = $(settings.sample);
        rowsample.css("display", "none");
        var sample = rowsample.html();


        var i = panelDiv.find(".each-section").size();
        //add html details to create a correct template
        var sectionDiv = $('<div />', { "class": 'each-section', 'id': 'section'+i });
        var image = $("<img />", { "src": settings.value,"class":"remove-image" });
        var link = $("<a />", { "text": settings.value,"class":"remove-link" });
        //remove event for remove selected form

        //create new form
        sectionDiv.html(sample);

        link.on('click', function (e) {

            e.preventDefault();
            var $this = $(this);
            $this.closest(".each-section").remove();
        });

        if (i > 0) {
            if (settings.type == 'image') {
                link.text('');
                link.append(image);

            }
            sectionDiv.append(link);
        }

        //add new created form on document
        panelDiv.append(sectionDiv);
       
    }
آرگومان داده شده، در واقع همان this هست که به این تابع ارسال شده است. در اولین گام تنظیمات ذخیره شده را که قبلا ذخیره کرده‌ایم، واکشی می‌کنیم. سپس تگ row-sample یا هر نامی را که به آن اختصاص داده شده است، می‌یابیم و محتوای آن را به شکل html در قالب string بیرون می‌کشیم. این کد html در واقع نمونه‌ای است که قرار است در سطر تکرار شود. البته تگ نمونه فقط برای نمونه به کار می‌رود و نیازی نیست روی صفحه نمایش داده شود؛ پس آن را مخفی می‌کنیم. از آنجا که ممکن است این سطری که ایجاد می‌شود، سطر اول نباشد و قبلا هم سطرهایی توسط همین متد ایجاد شده‌اند، بررسی می‌کنیم چند تگ با کلاس each-section داریم. اگر بیشتر از صفر باشد یعنی قبلا سطرهایی ایجاد شده است. در غیر اینصورت این اولین سطر ماست. اولین سطر توسط init صدا زده می‌شود و مابقی توسط متد add انجام می‌گیرد.
        settings.sample = panelDiv.data('sample');
        settings.type = panelDiv.data('type');
        settings.value = panelDiv.data('value');
        //find sample code
        var rowsample = $(settings.sample);
        rowsample.css("display", "none");
        var sample = rowsample.html();


        var i = panelDiv.find(".each-section").size();
در خطوط بعدی یک سری متغیر داریم که برای هر کدام یک قالب تگ div با کلاس‌های مختلف می‌سازیم. sectionDiv یک تگ  div  با کلاس each-section است که هر سطر را به طور کامل در خود قرار می‌دهد. link، جهت ساخت لینک حذف با کلاس remove-link به کار می‌رود. image هم یک تگ image می‌سازد تا اگر کاربر درخواست 'type:'image را داد، به جای لینک متنی حذف، از تصویر استفاده شود.
        //add html details to create a correct template
        var sectionDiv = $('<div />', { "class": 'each-section', 'id': 'section'+i });
        var image = $("<img />", { "src": settings.value,"class":"remove-image" });
        var link = $("<a />", { "text": settings.value,"class":"remove-link" });

در خط بعدی محتویات نمونه را داخل تگ sectiondiv قرار می‌دهیم:
//create new form
        sectionDiv.html(sample);

بعد از آن برای رویداد کلیک لینک حذف، کد زیر را وارد می‌کنیم:
   link.on('click', function (e) {

            e.preventDefault();
            var $this = $(this);
            $this.closest(".each-section").remove();
        });
متد closest در جی کوئری این وظیفه را دارد تا به سمت تگ‌های والد تگ this حرکت کند و با برخوردن با اولین تگ والد با کلاس each-section، آن تگ والد را بازگرداند و سپس متد remove را روی آن اجرا کند تا آن تگ به همراه تمام فرزندانش حذف شوند.

اولین شرط زیر بررسی می‌کند که آیا این سطری که ایجاد شده است سطر دوم به بعد است یا خیر؟ اگر آری پس باید دکمه‌ی حذف را به همراه داشته باشد. در صورتیکه سطر دوم به بعد باشد، وارد آن می‌شود. حالا بررسی می‌کند که کاربر برای دکمه‌ی حذف، درخواست لینک تصویری یا لینک متنی داده است و لینک مناسب را ساخته و آن را به انتهای sectionDiv اضافه می‌کند.
   if (i > 0) {
            if (settings.type == 'image') {
                link.text('');
                link.append(image);

            }
            sectionDiv.append(link);
        }

در انتها کل تگ sectionDiv را به تگ داده شده اضافه می‌کنیم تا به کاربر نمایش داده شود.
//add new created form on document
        panelDiv.append(sectionDiv);
اشتراک‌ها
نحوه بهره بردن از ابزار Remote Desktop ویندوز تحت وب
همان طور که می‌دانید Remote Desktop Connection نام نرم افزاری توکار است که در ویندوزهای مایکروسافت موجود است، که از آن می‌توان برای وصل شدن به یک کامپیوتر از راه دور استفاده کرد. 
امکان این فراهم شده است که بوسیله یک ActiveX که توسط مایکروسافت فراهم شده است، از Remote Desktop تحت وب (یعنی تحت اجرای یک مرورگر) استفاده کرد. نام این اکتیو ایکس Remote Desktop ActiveX control  است. 
نحوه بهره بردن از ابزار Remote Desktop ویندوز تحت وب
نظرات مطالب
احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular - قسمت دوم - سرویس اعتبارسنجی
یک نمونه از خطای cyclic dependency در مطلب  «احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular - قسمت ششم - کار با منابع محافظت شده‌ی سمت سرور» بحث شده‌است. ابتدا باید «private http: HttpClient» را از سازنده‌ی کلاس حذف کنید، سپس:
- یک راه حل آن، تزریق «private injector: Injector» است و دریافت وهله‌ی سرویس به صورت (HttpClient)this.injector.get.
- راه حل دوم، دریافت وابستگی مورد نیاز از طریق یک پارامتر (و نه توسط تزریق وابستگی‌ها در سازنده‌ی کلاس):
loadClientConfig(httpClient: HttpClient): Promise<any> {
و سپس دریافت آن به این صورت:
    {
      provide: APP_INITIALIZER,
      useFactory: (config: AppConfigService, httpClient: HttpClient) => () => config.loadClientConfig(httpClient),
      deps: [AppConfigService, HttpClient],
      multi: true
    },
مثال‌های بیشتر در اینجا
مطالب
اعتبارسنجی مبتنی بر کوکی‌ها در 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 نیز می‌تواند مفید باشد و به همین نحو کار می‌کنند.



کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید.