مطالب
فعال‌سازی HSTS در ASP.NET Core
برای فعال‌سازی SSL در ASP.NET Core می‌توانیم از ویژگی RequireHttps برای کنترلرها و همچنین اکشن‌متدها استفاده کنیم:
[RequireHttps]
public class AccountController : Controller
{
    public IActionResult Login()
    {
        return Content("Login Page");
    }
}
اکنون اگر پروژه را اجرا کنید، خروجی به صورت زیر خواهد بود و اگر به آدرس Account/Login مراجعه کنید، چیزی در خروجی نمایش داده نخواهد شد:

علت آن نیز این است که اگر درخواست HTTPS نباشد، یک Permanent Redirect به همان URL خواهیم داشت؛ زیرا بر روی پورتی که پروژه توسط آن اجرا شده‌است، هیچ certificateی نصب نشده‌است. در ادامه می‌خواهیم یک self-signed certificate تستی را برای اجرای پروژه ایجاد کنیم. 
توسط دستورات زیر می‌توانید یک certificate را برای localhost ایجاد کنید:
$ openssl genrsa -out key.pem 2048
$ openssl req -new -sha256 -key key.pem -out csr.csr
$ openssl req -x509 -sha256 -days 365 -key key.pem -in csr.csr -out certificate.pem
openssl pkcs12 -export -out localhost.pfx -inkey key.pem -in certificate.pem
با اجرای دستورات فوق، باید یک فایل جدید با نام localhost.pfx و تعدادی فایل دیگر، درون پروژه ایجاد شده باشند:

اکنون باید Kestrel را از وجود این certificate مطلع کنیم. برای انجام اینکار ابتدا باید پکیج زیر را نصب کنیم:
$ dotnet add package Microsoft.AspNetCore.Server.Kestrel.Https
سپس فایل Program.cs را به صورت زیر تغییر دهیم:
namespace testingSSL
{
    public class Program
    {
        public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }

        public static IWebHost BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseKestrel(options =>
                {
                    options.Listen(IPAddress.Any, 8080);
                    options.Listen(IPAddress.Any, 443,
                        listenOptions => listenOptions.UseHttps("localhost.pfx", "qwe123456"));
                })
                .UseStartup<Startup>()
                .Build();
    }
}
در این حالت با مراجعه به کنترلری که با ویژگی RequireHttps مزین شده‌است، به صورت خودکار، درخواست به نسخه HTTPS هدایت خواهد شد:

البته تا اینجا، هدف بررسی ویژگی RequireHttps بود؛ طبیعتاً به SSL در حین Development نیازی نخواهید داشت. در نتیجه می‌توانیم به صورت Global تمامی کنترلرها را در زمان Production به ویژگی گفته شده مزین کنیم:

private readonly IHostingEnvironment _env;
public Startup(IConfiguration configuration,
IHostingEnvironment env)
{
    Configuration = configuration;
    _env = env;
}

public IConfiguration Configuration { get; }

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    if (!_env.IsDevelopment())
        services.Configure<MvcOptions>(o => o.Filters.Add(new RequireHttpsAttribute()));
}


(Http Strict Transport Security (HSTS

هدایت کردن خودکار درخواست از حالت HTTP به HTTPS، توسط خیلی از سایت‌ها انجام می‌شود:

البته این روش بهتر از استفاده نکردن از SSL است؛ اما در نظر داشته باشید که همیشه اولین درخواست به صورت رمزنگاری نشده ارسال خواهد شد. فرض کنید در یک محیط پابلیک از طریق WiFi به اینترنت متصل شده‌ایم. شخصی (هکر) که بر روی مودم کنترل دارد، طوری WiFi را پیکربندی کرده‌است که به جای آدرس اصلی که در تصویر مشاهده می‌کنید، یک نسخه جعلی از سایت باز شود؛ به طوریکه URL همانند URL اصلی باشد. در این‌حالت کاربر به جای اینکه نام‌کاربری و کلمه‌عبور را وارد سایت اصلی کند، آن را درون سایت جعلی وارد خواهد کرد. برای حل این مشکل می‌توانیم وب‌سایت‌مان را طوری تنظیم کنیم که هدر Strict-Transport-Security را به هدر اولین responseی که توسط مرورگر دریافت می‌شود اضافه کند:

Strict-Transport-Security: max-age=31536000

بنابراین مرورگر وب‌سایت را درون یک لیست internal به مدت یکسال (مقدار max-age) نگهداری خواهد کرد؛ در طول این زمان به هیچ درخواست ناامنی اجازه داده نخواهد شد. به این قابلیت HSTS گفته می‌شود. البته ASP.NET Core به صورت توکار روشی را جهت اضافه کردن این هدر ارائه نداده است؛ اما می‌توانیم خودمان یک Middleware سفارشی را به pipeline اضافه کنیم تا اینکار را برایمان انجام دهد:

namespace testingSSL.Middleware
{
    public class HstsMiddleware
    {
        private readonly RequestDelegate _next;
        public HstsMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public Task Invoke(HttpContext context)
        {
            if (!context.Request.IsHttps)
                return _next(context);

            if (IsLocalhost(context))
                return _next(context);

            context.Response.Headers.Add("Strict-Transport-Security", "max-age=31536000");

            return _next(context);
        }
        private bool IsLocalhost(HttpContext context)
        {
            return string.Equals(context.Request.Host.Host, "localhost", StringComparison.OrdinalIgnoreCase);
        }
    }
}


یا اینکه می‌توانیم از کتابخانه NWebSec استفاده کنیم:

$ dotnet add package NWebsec.AspNetCore.Middleware

برای استفاده از آن نیز خواهیم داشت:

app.UseHsts(h => h.MaxAge(days: 365));


اما هنوز یک مشکل وجود دارد؛ هنوز مشکل اولین درخواست را برطرف نکرده‌ایم. زیرا مرورگر برای دریافت این هدر نیاز به مراجعه به سایت دارد. برای حل این مشکل می‌توانید آدرس وب‌سایت خود را در سایت hstspreload ثبت کنید، سپس متد PreLoad را به کد فوق اضافه کنید:

app.UseHsts(h => h.MaxAge(days: 365).Preload());

در اینحالت حتی اگر کسی به وب‌سایت شما مراجعه نکند، مرورگر می‌داند که باید از HTTPS استفاده کند. زیرا این لیست به صورت توکار درون مرورگر تعبیه شده‌است. بنابراین در این‌حالت مطمئن خواهیم شد که تمامی connectionها به سایت‌مان امن می‌باشند.


دریافت کدهای مطلب جاری (+)

مطالب
مستند سازی ASP.NET Core 2x API توسط OpenAPI Swagger - قسمت ششم - تکمیل مستندات محافظت از API
ممکن است تعدادی از اکشن متدهای API طراحی شده، محافظت شده باشند. بنابراین OpenAPI Specification تولیدی نیز باید به همراه مستندات کافی در این مورد باشد تا استفاده کنندگان از آن بدانند چگونه باید با آن کار کنند. نگارش سوم OpenAPI Specification از اعتبارسنجی و احراز هویت مبتنی بر هدرها مانند basic و یا bearer، همچنین حالت کار با API Keys مانند هدرها، کوئری استرینگ‌ها و کوکی‌ها و یا حالت OAuth2 و OpenID Connect پشتیبانی می‌کند و این موارد ذیل خواص securitySchemes و security در OpenAPI Specification ظاهر می‌شوند:
"securitySchemes":
{ "basicAuth":
       {
             "type":"http",
             "description":"Input your username and password to access this API",
             "scheme":"basic"
       }
}
…
"security":[
  {"basicAuth":[]}
]
- خاصیت securitySchemes انواع حالت‌های اعتبارسنجی پشتیبانی شده را لیست می‌کند.
- خاصیت security کار اعمال Scheme تعریف شده را به کل API یا صرفا قسمت‌های خاصی از آن، انجام می‌دهد.

در ادامه مثالی را بررسی خواهیم کرد که مبتنی بر basic authentication کار می‌کند و در این حالت به ازای هر درخواست به API، نیاز است یک نام کاربری و کلمه‌ی عبور نیز ارسال شوند. البته روش توصیه شده، کار با JWT و یا OpenID Connect است؛ اما جهت تکمیل ساده‌تر این قسمت، بدون نیاز به برپایی مقدماتی پیچیده، کار با basic authentication را بررسی می‌کنیم و اصول کلی آن از دیدگاه مستندات OpenAPI Specification تفاوتی نمی‌کند.


افزودن Basic Authentication به API برنامه

برای پیاده سازی Basic Authentication نیاز به یک AuthenticationHandler سفارشی داریم:
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;

namespace OpenAPISwaggerDoc.Web.Authentication
{
    public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
    {
        public BasicAuthenticationHandler(
            IOptionsMonitor<AuthenticationSchemeOptions> options,
            ILoggerFactory logger,
            UrlEncoder encoder,
            ISystemClock clock)
            : base(options, logger, encoder, clock)
        {
        }

        protected override Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            if (!Request.Headers.ContainsKey("Authorization"))
            {
                return Task.FromResult(AuthenticateResult.Fail("Missing Authorization header"));
            }

            try
            {
                var authenticationHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]);
                var credentialBytes = Convert.FromBase64String(authenticationHeader.Parameter);
                var credentials = Encoding.UTF8.GetString(credentialBytes).Split(':');
                var username = credentials[0];
                var password = credentials[1];

                if (username == "DNT" && password == "123")
                {
                    var claims = new[] { new Claim(ClaimTypes.NameIdentifier, username) };
                    var identity = new ClaimsIdentity(claims, Scheme.Name);
                    var principal = new ClaimsPrincipal(identity);
                    var ticket = new AuthenticationTicket(principal, Scheme.Name);
                    return Task.FromResult(AuthenticateResult.Success(ticket));
                }
                return Task.FromResult(AuthenticateResult.Fail("Invalid username or password"));
            }
            catch
            {
                return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization header"));
            }
        }
    }
}
کار این AuthenticationHandler سفارشی، با بازنویسی متد HandleAuthenticateAsync شروع می‌شود. در اینجا به دنبال هدر ویژه‌ای با کلید Authorization می‌گردد. این هدر باید به همراه نام کاربری و کلمه‌ی عبوری با حالت base64 encoded باشد. اگر این هدر وجود نداشت و یا مقدار هدر Authorization، با فرمتی که مدنظر ما است قابل decode و همچنین جداسازی نبود، شکست اعتبارسنجی اعلام می‌شود.
پس از دریافت مقدار هدر Authorization، ابتدا مقدار آن‌را از base64 به حالت معمولی تبدیل کرده و سپس بر اساس حرف ":"، دو قسمت را از آن جداسازی می‌کنیم. قسمت اول را به عنوان نام کاربری و قسمت دوم را به عنوان کلمه‌ی عبور پردازش خواهیم کرد. در این مثال جهت سادگی، این دو باید مساوی DNT و 123 باشند. اگر اینچنین بود، یک AuthenticationTicket دارای Claim ای حاوی نام کاربری را ایجاد کرده و آن‌را به عنوان حاصل موفقیت آمیز بودن عملیات بازگشت می‌دهیم.

مرحله‌ی بعد، استفاده و معرفی این BasicAuthenticationHandler تهیه شده به برنامه است:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(defaultScheme: "Basic")
                    .AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("Basic", null);
در اینجا توسط متد services.AddAuthentication، این scheme جدید که نام رسمی آن Basic است، به همراه Handler آن، به برنامه معرفی می‌شود.
همچنین نیاز است میان‌افزار اعتبارسنجی را نیز با فراخوانی متد app.UseAuthentication، به برنامه اضافه کرد که باید پیش از فراخوانی app.UseMvc صورت گیرد تا به آن اعمال شود:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
   // ...
            app.UseStaticFiles();

            app.UseAuthentication();

            app.UseMvc();
        }
    }
}

همچنین برای اینکه تمام اکشن متدهای موجود را نیز محافظت کنیم، می‌توان فیلتر Authorize را به صورت سراسری اعمال کرد:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
     // ...
   
            services.AddMvc(setupAction =>
            {
                setupAction.Filters.Add(new AuthorizeFilter());
        // ...


تکمیل مستندات API جهت انعکاس تنظیمات محافظت از اکشن متدهای آن

پس از تنظیم محافظت دسترسی به اکشن متدهای برنامه، اکنون نوبت به مستند کردن آن است و همانطور که در ابتدای بحث نیز عنوان شد، برای این منظور نیاز به تعریف خواص securitySchemes و security در OpenAPI Specification است:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSwaggerGen(setupAction =>
            {
   // ...

                setupAction.AddSecurityDefinition("basicAuth", new OpenApiSecurityScheme
                {
                    Type = SecuritySchemeType.Http,
                    Scheme = "basic",
                    Description = "Input your username and password to access this API"
                });
            });
در اینجا توسط متد setupAction.AddSecurityDefinition، ابتدا یک نام تعریف می‌شود (از این نام، در قسمت بعدی تنظیمات استفاده خواهد شد). پارامتر دوم آن همان SecurityScheme است که توضیح داده شد. برای حالت basic auth، نوع آن Http است و اسکیمای آن basic. باید دقت داشت که مقدار خاصیت Scheme در اینجا، حساس به بزرگی و کوچکی حروف است.

پس از این تنظیم اگر برنامه را اجرا کنیم، یک دکمه‌ی authorize اضافه شده‌است:


با کلیک بر روی آن، صفحه‌ی ورود نام کاربری و کلمه‌ی عبور ظاهر می‌شود:


اگر آن‌را تکمیل کرده و سپس برای مثال لیست نویسندگان را درخواست کنیم (با کلیک بر روی دکمه‌ی try it out آن و سپس کلیک بر روی دکمه‌ی execute ذیل آن)، تنها خروجی 401 یا unauthorized را دریافت می‌کنیم:


- بنابراین برای تکمیل آن، مطابق نکات قسمت چهارم، ابتدا باید status code مساوی 401 را به صورت سراسری، مستند کنیم:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc(setupAction =>
            {
                setupAction.Filters.Add(new ProducesResponseTypeAttribute(StatusCodes.Status401Unauthorized));

- همچنین هرچند با کلیک بر روی دکمه‌ی Authorize در Swagger UI و ورود نام کاربری و کلمه‌ی عبور توسط آن، در همانجا پیام Authorized را دریافت کردیم، اما اطلاعات آن به ازای هر درخواست، به سمت سرور ارسال نمی‌شود. به همین جهت در حین درخواست لیست نویسندگان، پیام unauthorized را دریافت کردیم. برای رفع این مشکل نیاز است به OpenAPI Spec اعلام کنیم که تعامل با API، نیاز به Authentication دارد:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSwaggerGen(setupAction =>
            {
    // ...

                setupAction.AddSecurityRequirement(new OpenApiSecurityRequirement
                {
                    {
                        new OpenApiSecurityScheme
                        {
                            Reference = new OpenApiReference
                            {
                                Type = ReferenceType.SecurityScheme,
                                Id = "basicAuth"
                            }
                        },
                        new List<string>()
                    }
                });
            });
در اینجا OpenApiSecurityRequirement یک دیکشنری از نوع <<Dictionary<OpenApiSecurityScheme, IList<string است که کلید آن از نوع OpenApiSecurityScheme تعریف می‌شود و باید آن‌را به نمونه‌ای که توسط setupAction.AddSecurityDefinition پیشتر اضافه کردیم، متصل کنیم. این اتصال توسط خاصیت Reference آن و Id ای که به نام تعریف شده‌ی توسط آن اشاره می‌کند، صورت می‌گیرد. مقدار این دیکشنری نیز لیستی از رشته‌ها می‌تواند باشد (مانند توکن‌ها و scopes در OpenID Connect) که در اینجا با یک لیست خالی مقدار دهی شده‌است.

پس از این تنظیمات، Swagger UI با افزودن یک آیکن قفل به مداخل APIهای محافظت شده، به صورت زیر تغییر می‌کند:


در این حالت اگر بر روی آیکن قفل کلیک کنیم، همان صفحه‌ی دیالوگ ورود نام کاربری و کلمه‌ی عبوری که پیشتر با کلیک بر روی دکمه‌ی Authorize ظاهر شد، نمایش داده می‌شود. با تکمیل آن و کلیک مجدد بر روی آیکن قفل، جهت گشوده شدن پنل API و سپس کلیک بر روی try it out  آن، برای مثال می‌توان به API محافظت شده‌ی دریافت لیست نویسندگان، بدون مشکلی، دسترسی یافت:



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: OpenAPISwaggerDoc-06.zip
مطالب
نمایش رکوردها به ترتیب اولویت به کمک jQuery UI sortable در ASP.NET MVC

همان طور که می‌دانید کاربرد پذیری در خیلی از پروژه‌ها حرف اول رو می‌زند و کاربر دوست دارد کارهایی که انجام می‌دهد خیلی راحت و با استفاده از موس باشد.یکی از کار هایی که در اکثر پروژه‌ها نیاز است ، چیدمان ترتیب رکورد‌ها است. ما می‌خواهیم در این پست ترتیبی اتخاذ کنیم که کاربر بتواند رکورد‌ها را به هر ترتیبی که دوست دارد نمایش دهد.

از توضیحاتی که قبلا  دادم مشخص است که این کار احتمالا در ASP.NET WebForm  کار سختی نیست ولی این کار باید در MVC  از ابتدا طراحی شود.

طرح سوال : یک سری رکورد از یک Table داریم که می‌خواهیم به ترتیب وارد شدن رکورد‌ها نباشد و  ترتیبی که ما می‌خواهیم نمایش داده شود.

پاسخ کوتاه : خب باید ابتدا یک فیلد (برای اولویت بندی)  به Table  اضافه کنیم  بعد اون فیلد رو بنا به ترتیبی که دوست داریم رکورد‌ها نمایش داده شود پر کنیم (Sort  می کنیم ) و در آخر هم هنگام نمایش در View رکورد‌ها را بر اساس این فیلد نمایش می‌دهیم.

(این پست هم در ادامه پست قبلی در همان پروژه است و از همان Table  ها استفاده شده است)

اضافه کردن فیلد :

ابتدا یک فیلد به Table  مورد نظر اضافه می‌کنیم. من اسم این فیلد رو Priority گذاشتم. Table  من چنین وضعیتی دارد.

افزودن فایل‌های jQuery UI :

در این مرحله شما نیاز دارید فایل‌های مورد نیاز برای Sort  کردن رکورد‌ها را اضافه کنید. شما می‌توانید فقط فایل‌های مربوط به Sortable  را به صفحه خودتان اضافه کنید و یا مثل من فایل هایی که حاوی تمام قسمت‌های jQuery UI  هست را اضافه کنید.

من برای این کار از Section  استفاده کردم ، ابتدا در Head  فایل Layout  دو Section  تعریف کردم برای CSS  و JavaScript . و فایل‌های مربوط به Sort کردن را در صفحه ای که باید عمل Sort انجام بشود در  این Section ها قرار دادم.

فایل Layout

<head>
    <meta charset="utf-8" />
    @RenderSection("meta", false)
    <title>@ViewBag.Title</title>
    <link href="@Url.Content("~/Content/~Site.css")" rel="stylesheet" type="text/css" />
    <link href="@Url.Content("~/Content/redactor/css/redactor.css")" rel="stylesheet" type="text/css" />
    <link href="@Url.Content("~/Content/css/bootstrap-rtl.min.css")" rel="stylesheet" type="text/css" />
    @RenderSection("css", false)
    <script src="@Url.Content("~/Scripts/jquery-1.8.2.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/modernizr-1.7.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Content/js/bootstrap-rtl.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Content/redactor/redactor.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script>
    @RenderSection("js", false)

</head>

فایل Index.chtml  در پوشه کنترلر Type


@model IEnumerable<KhazarCo.Models.Type>
@{
    ViewBag.Title = "Index";
    Layout = "~/Areas/Administrator/Views/Shared/_Layout.cshtml";
}
@section css
{<link href="@Url.Content("~/Content/themes/base/jquery-ui.css")" rel="stylesheet" type="text/css" />
}
@section js
{
    <script src="@Url.Content("~/Scripts/jquery-ui-1.9.0.min.js")" type="text/javascript"></script>
}


در آخر فایل Index.chtml  به اینصورت شده است:

<h2>
    نوع ها</h2>
<p>
    @Html.ActionLink("ایجاد یک مورد جدید", "Create", null, new { @class = "btn btn-info" })
</p>
<table>
    <thead>
        <tr>
            <th>
                عنوان
            </th>
            <th>
                توضیحات
            </th>
            <th>
                فعال
            </th>
            <th>
            </th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.OrderBy(m => m.Priority))
        {
            <tr id="@item.Id">
                <td>
                    @Html.DisplayFor(modelItem => item.Title)
                </td>
                <td>
                    @(new HtmlString(item.Description))
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.IsActive)
                </td>
                <td>
                    @Html.ActionLink("ویرایش", "Edit", new { id = item.Id }, new { @class = "btnEdit label label-warning" })
                    |
                    @Html.ActionLink("مشاهده", "Details", new { id = item.Id }, new { @class = "btnDetails label label-info" })
                    |
                    @Html.ActionLink("حذف", "Delete", new { id = item.Id }, new { @class = "btnDelete label label-important" })
                </td>
            </tr>
        }
    </tbody>
</table>

** توجه داشته باشید که من به هر tr  یک id  اختصاص داده ام که این مقدار id  همان مقدار فیلد Id  همان رکورد هست ، ما برای مرتب کردن به این Id  نیاز داریم (خط 25).

افزودن کد‌های کلاینت:

حالا باید کدی بنویسم که دو کار را برای ما انجام دهد : اول حالت Sort  پذیری را به سطر‌های Table  بدهد و دوم اینکه هنگامی که ترتیب سطر‌های تغییر کرد ما را با خبر کند:


<script type="text/javascript">
    $(function () {
        $("table tbody").sortable({
            helper: fixHelper,
            update: function (event, ui) {
                jQuery.ajax('@Url.Action("Sort", "Type", new { area = "Administrator" })', {
                    data: { s: $(this).sortable('toArray').toString() }
                });
            }
        }).disableSelection();
    });
    var fixHelper = function (e, ui) {
        ui.children().each(function () {
            $(this).width($(this).width());
        });
        return ui;
    };
</script>


توضیح کد :

در این کد ما حالت ترتیب پذیری را به Table  می دهیم و هنگامی که عمل Update  در Table  انجام شد تابع مربوطه اجرا می‌شود. ما در این تایع، ترتیب جدید سطر‌ها را می‌گیریم ( ** به کمک مقدار Id  که به هر سطر دادیم ، این مقدار Id  برابر بود با Id خود رکورد در Database )  و به کمکjQuery.ajax  به تابع Sort  از کنترلر Type  در منطقه (area ) Administrator  ارسال می‌کنیم و در آنجا ادامه کار را انجام میدهیم.

تابع fixHelper  هم به ما کمک می‌کند که هنگامی که سطر‌ها از جای خود جدا می‌شوند ، دارای عرض یکسانی باشند و عرض آن‌ها تغییری نکند.


افزودن کد Server:

حالا باید تابع Sort  که مقادیر را به آن ارسال کردیم بنویسم. من این تابع را بر اساس مقداری که از کلاینت ارسال می‌شود اینگونه طراحی کردم.


        public EmptyResult Sort(string s)
        {
            if (s != null)
            {
                var ids = new List<int>();
                foreach (var item in s.Split(','))
                {
                    ids.Add(int.Parse(item));
                }
                int intpriority = 0;

                foreach (var item in ids)
                {
                    intpriority++;
                    db.Types.Single(m => m.Id == item).Priority = intpriority;
                }
                db.SaveChanges();
            }
            return new EmptyResult();
        }

در ایتدا مقادیر Id  که از کلاینت  به صورت String  ارسال شده است را می‌گیریم و بعد به همان ترتیب ارسال در لیستی از int قرار می‌دهیم ids.

سپس به اضای هر رکورد Type  مقدار اولویت را به فیلدی که برای همین مورد اضافه کردیم Priority اختصاص می‌دهیم. و در آخر هم تغییرات را ذخیره می‌کنیم. (خود کد کاملا واضح است و نیاری به توضیح بیشتر نیست )

حالا باید هنگامی که لیست Type  ها نمایش داده می‌شود به ترتیب (OrderBy) فیلد Priority    نمایش داده شود پس تابع Index را اینطور تغییر می‌دهیم.

        public ViewResult Index()
        {
            return View(db.Types.Where(m => m.IsDeleted == false).OrderBy(m => m.Priority));
        }

این هم خروجی کار من:

این عکس مربوط به است به قسمت مدیریت پروژه شیرآلات مرجان خزر


نظرات مطالب
صفحه بندی و مرتب سازی خودکار اطلاعات به کمک jqGrid در ASP.NET MVC
این موارد رو با اینکه قبلا مطالعه کرده بودم دوباره دقیق‌تر مطالعه کردم
ولی مشکلی که وجود داره اینه که با وجود اینکه Routing‌ها به همین شکلی که توی ASP.NET Core 3.0 گفته شده رو تنظیم کردم
  public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        { 
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            JqGridRequest.ParametersNames = new JqGridParametersNames() {  PagesCount = "npage" };
            app.UseRouting();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            { 
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=JavaScript}/{action=Basics}/{id?}");

                endpoints.MapControllerRoute(
                   name: "default2",
                   pattern: "{controller=StarWars}/{action=Characters}");
         
                endpoints.MapRazorPages(); 
            });
        }


ولی متد سمت سرور اصلا Call نمیشه که بخواد جواب رو نمایش بده
شاید لازم باشه object از کلاس JqGridRequest که ورودی متد Characters هست خصوصیات کلاسش تغییراتی بر حسب System.Text.Json داشته باشه تا بتونه متد سمت سرور رو صحیح Call کنه
یعنی تبدیل رشته‌ی JSON به شیء متناظر با آن و یا حالت عکس آن گویا به صورت صحیح انجام نمیشه احتمالا باید پروژه Lib.AspNetCore.Mvc.JqGrid  بر حسب ASP.NET Core 3.0 کامپایل بشه


در اینجاهم یه کامنت در مورد تغییرات JSON serialization/deserialization گذاشتند
مطالب
استثنای Sequence contains no elements در حین استفاده از LINQ

در ابتدا مثال‌های زیر را در نظر بگیرید:

using System;
using System.Collections.Generic;
using System.Linq;

namespace testWinForms87
{
public class Data
{
public int id { get; set; }
public string name { get; set; }
}

class CLinqTests
{
public static int TestGetListMin1()
{
var lst = new List<Data>
{
new Data{ id=1, name="id1"},
new Data{ id=2, name="id2"},
new Data{ id=3, name="name3"}
};

return (from c in lst
where c.name.Contains("id")
select c.id).Min();
}

public static int TestGetListMin2()
{
var lst = new List<Data>();

return (from c in lst
where c.name.Contains("id")
select c.id).Min();
}
}
}
در متد TestGetListMin1 قصد داریم کوچکترین آی دی رکوردهایی را که نام آن‌ها حاوی id است، از لیست تشکیل شده از کلاس Data بدست آوریم (همانطور که مشخص است سه رکورد از نوع Data در لیست lst ما قرار گرفته‌اند).
محاسبات آن کار می‌کند و مشکلی هم ندارد. اما همیشه در دنیای واقعی همه چیز قرار نیست به این خوبی پیش برود. ممکن است همانند متد TestGetListMin2 ، لیست ما خالی باشد (برای مثال از دیتابیس، رکوردی مطابق شرایط کوئری‌های قبلی بازگشت داده نشده باشد). در این حالت هنگام فراخوانی متد Min ، استثنای Sequence contains no elements رخ خواهد داد و همانطور که در مباحث defensive programming عنوان شد، وظیفه‌ی ما این نیست که خودرو را به دیوار کوبیده (یا منتظر شویم تا کوبیده شود) و سپس به فکر چاره بیفتیم که خوب، عجب! مشکلی رخ داده است!
اکنون چه باید کرد؟ حداقل یک مرحله بررسی اینکه آیا کوئری ما حاوی رکوردی می‌باشد یا خیر باید به این متد اضافه شود (به صورت زیر):

public static int TestGetListMin3()
{
var lst = new List<Data>();
var query = from c in lst
where c.name.Contains("id")
select c.id;

if (query.Any())
return query.Min();
else
return -1;
}
البته می‌شد اگر هیچ رکوردی بازگشت داده نمی‌شد، یک استثنای سفارشی را ایجاد کرد، اما به شخصه ترجیح می‌دهم عدد منهای یک را بر گردانم (چون می‌دانم رکوردهای من عدد مثبت هستند و اگر حاصل منفی شد نیازی به ادامه‌ی پروسه نیست).

شبیه به این مورد در هنگام استفاده از تابع Single مربوط به LINQ نیز ممکن است رخ دهد (تولید استثنای ذکر شده) اما در اینجا مایکروسافت تابع SingleOrDefault را نیز پیش بینی کرده است. در این حالت اگر کوئری ما رکوردی را برنگرداند، SingleOrDefault مقدار نال را برگشت داده و استثنایی رخ نخواهد داد (نمونه‌ی دیگر آن متدهای First و FirstOrDefault هستند).
در مورد متدهای Min و Max ، متدهای MinOrDefault یا MaxOrDefault در دات نت فریم ورک وجود ندارند. می‌توان این نقیصه را با استفاده از extension methods برطرف کرد.

using System;
using System.Collections.Generic;
using System.Linq;

public static class LinqExtensions
{
public static T MinOrDefault<T>(this IEnumerable<T> source, T defaultValue)
{
if (source.Any<T>())
return source.Min<T>();

return defaultValue;
}

public static T MaxOrDefault<T>(this IEnumerable<T> source, T defaultValue)
{
if (source.Any<T>())
return source.Max<T>();

return defaultValue;
}
}
اکنون با استفاده از extension methods فوق، کد ما به صورت زیر تغییر خواهد کرد:

public static int TestGetListMin4()
{
var lst = new List<Data>();
return (from c in lst
where c.name.Contains("id")
select c.id).MinOrDefault(-1);
}

مطالب
به روز رسانی تمام فیلدهای رشته‌ای تمام جداول بانک اطلاعاتی توسط Entity framework 6.x
یکی از مراحلی که پس از ارتقاء یک سایت به HTTPS باید صورت گیرد، به روز رسانی آدرس‌های قدیمی درج شده‌ی در صفحات مختلف، از HTTP به HTTPS است؛ وگرنه با خطای «قسمتی از صفحه امن نیست» توسط مرورگر مواجه خواهیم شد:


روش‌های زیادی برای مدیریت این مساله وجود دارند؛ مانند استفاده از ماژول‌های URL Rewrite برای بازنویسی آدرس‌های نهایی صفحه‌ی در حال رندر و یا ... به روز رسانی مستقیم بانک اطلاعاتی، یافتن تمام فیلدهای رشته‌ای ممکن در تمام جداول موجود و سپس اعمال تغییرات.


یافتن لیست تمام جداول قابل مدیریت توسط Entity framework

در ابتدا می‌خواهیم لیست پویای تمام جداول مدیریت شده‌ی توسط EF را پیدا کنیم. از این جهت که نمی‌خواهیم به ازای هر کدام یک کوئری جداگانه بنویسیم.
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using EFReplaceAll.Models;

namespace EFReplaceAll.Config
{
    public class DbSetInfo
    {
        public IQueryable<object> DbSet { set; get; }
        public Type DbSetType { set; get; }
    }

    public class MyContext : DbContext
    {
        public DbSet<Product> Products { set; get; }
        public DbSet<Category> Categories { set; get; }
        public DbSet<User> Users { set; get; }

        public MyContext()
            : base("Connection1")
        {
            this.Database.Log = sql => Console.Write(sql);
        }

        public IList<DbSetInfo> GetAllDbSets()
        {
            return this.GetType()
                .GetProperties()
                .Where(p => p.PropertyType.IsGenericType &&
                            p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>))
                .Select(p => new DbSetInfo
                {
                    DbSet = (IQueryable<object>)p.GetValue(this, null),
                    DbSetType = p.PropertyType.GetGenericArguments().First()
                })
                .ToList();
        }
    }
}
در اینجا متد GetAllDbSets، به صورت پویا لیست DbSetها را به همراه نوع جنریک آن‌ها، بازگشت می‌دهد. با استفاده از این لیست می‌توان رکوردهای تمام جداول را واکشی و سپس تغییر داد.


یافتن فیلدهای رشته‌ای رکوردهای تمام جداول و سپس به روز رسانی آن‌ها

می‌خواهیم متدی را طراحی کنیم که در آن لیستی از یافتن‌ها و جایگزینی‌ها قابل تعیین باشد. به همین جهت مدل زیر را تعریف می‌کنیم:
using System;

namespace EFReplaceAll.Utils
{
    public class ReplaceOp
    {
        public string ToFind { set; get; }
        public string ToReplace { set; get; }
        public StringComparison Comparison { set; get; }
    }
}
در اینجا خاصیت Comparison امکان جستجو و جایگزینی غیرحساس به حروف کوچک و بزرگ را نیز میسر می‌کند.

سپس متدی که کار یافتن تمام فیلدهای رشته‌ای و سپس جایگزین کردن آن‌ها را انجام می‌دهد به صورت زیر خواهد بود:
using System.Collections.Generic;
using System.Linq;
using EFReplaceAll.Config;

namespace EFReplaceAll.Utils
{
    public static class UpdateDbContextContents
    {
        public static void ReplaceAllStringsAcrossTables(IList<ReplaceOp> replaceOps)
        {
            int dbSetsCount;
            using (var uow = new MyContext())
            {
                dbSetsCount = uow.GetAllDbSets().Count;
            }

            for (var i = 0; i < dbSetsCount; i++)
            {
                using (var uow = new MyContext()) // using a new context each time to free resources quickly.
                {
                    var dbSetResult = uow.GetAllDbSets()[i];
                    var stringProperties = dbSetResult.DbSetType.GetProperties()
                        .Where(p => p.PropertyType == typeof(string))
                        .ToList();
                    var dbSetEntities = dbSetResult.DbSet;
                    var haveChanges = false;
                    foreach (var entity in dbSetEntities)
                    {
                        foreach (var stringProperty in stringProperties)
                        {
                            var oldPropertyValue = stringProperty.GetValue(entity, null) as string;
                            if (string.IsNullOrWhiteSpace(oldPropertyValue))
                            {
                                continue;
                            }

                            var newPropertyValue = oldPropertyValue;
                            foreach (var replaceOp in replaceOps)
                            {
                                newPropertyValue = newPropertyValue.ReplaceString(replaceOp.ToFind, replaceOp.ToReplace, replaceOp.Comparison);
                            }
                            if (oldPropertyValue != newPropertyValue)
                            {
                                stringProperty.SetValue(entity, newPropertyValue, null);
                                haveChanges = true;
                            }
                        }
                    }

                    if (haveChanges)
                    {
                        uow.SaveChanges();
                    }
                }
            }
        }

    }
}
توضیحات:
- در اینجا using (var uow = new MyContext()) را زیاد مشاهده می‌کنید. علت اینجا است که اگر تنها با یک Context کار کنیم، EF تمام تغییرات و تمام رکوردهای وارد شده‌ی به آن‌را کش می‌کند و مصرف حافظه‌ی برنامه با توجه به خواندن تمام رکوردهای بانک اطلاعاتی توسط آن، ممکن است به چند گیگابایت برسد. به همین جهت از Contextهایی با طول عمر کوتاه استفاده شده‌است تا میزان مصرف RAM این متد سبب کرش برنامه نشود.
- در ابتدای کار توسط متد GetAllDbSets که به Context اضافه کردیم، تعداد DbSetهای موجود را پیدا می‌کنیم تا بتوان بر روی آن‌ها حلقه‌ای را تشکیل داد و به ازای هر کدام یک (()using (var uow = new MyContext را تشکیل داد.
- سپس با استفاده از نوع DbSet که توسط خاصیت dbSetResult.DbSetType در دسترس است، خواص رشته‌ای ممکن این DbSet یافت می‌شوند.
- در ادامه dbSetResult.DbSet یک Data Reader را به صورت پویا بر روی DbSet جاری باز کرده و تمام رکوردهای این DbSet را یک به یک بازگشت می‌دهد.
- در اینجا با استفاده از Reflection، از رکورد جاری، مقادیر خواص رشته‌ای آن دریافت شده و سپس کار جستجو و جایگزینی انجام می‌شود.
- در آخر هم فراخوانی uow.SaveChanges کار ثبت تغییرات صورت گرفته را انجام می‌دهد.


متدی برای جایگزینی غیرحساس به حروف بزرگ و کوچک

متد استاندارد Replace رشته‌ها، حساس به حروف بزرگ و کوچک است. یک نمونه‌ی عمومی‌تر را که در آن بتوان StringComparison.OrdinalIgnoreCase را تعیین کرد، در ذیل مشاهده می‌کنید که از آن در متد ReplaceAllStringsAcrossTables فوق استفاده شده‌است:
using System;
using System.Text;

namespace EFReplaceAll.Utils
{
    public static class StringExtensions
    {
        public static string ReplaceString(this string src, string oldValue, string newValue, StringComparison comparison)
        {
            if (string.IsNullOrWhiteSpace(src))
            {
                return src;
            }

            if (string.Compare(oldValue, newValue, comparison) == 0)
            {
                return src;
            }

            var sb = new StringBuilder();

            var previousIndex = 0;
            var index = src.IndexOf(oldValue, comparison);

            while (index != -1)
            {
                sb.Append(src.Substring(previousIndex, index - previousIndex));
                sb.Append(newValue);
                index += oldValue.Length;

                previousIndex = index;
                index = src.IndexOf(oldValue, index, comparison);
            }

            sb.Append(src.Substring(previousIndex));

            return sb.ToString();
        }
    }
}
و در آخر یک مثال از استفاده‌ی این متد تهیه شده، جهت به روز رسانی لینک‌های HTTP به HTTPS در تمام جداول برنامه به صورت زیر است:
            UpdateDbContextContents.ReplaceAllStringsAcrossTables(
                new[]
                {
                    new ReplaceOp
                    {
                        ToFind = "https://www.dntips.ir",
                        ToReplace = "https://www.dntips.ir",
                        Comparison = StringComparison.OrdinalIgnoreCase
                    },
                    new ReplaceOp
                    {
                        ToFind = "https://www.dntips.ir",
                        ToReplace = "https://www.dntips.ir",
                        Comparison = StringComparison.OrdinalIgnoreCase
                    }
                });

کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: EFReplaceAll.zip
نظرات مطالب
OutputCache در ASP.NET MVC
با سلام.
متدی به روش زیر در کنترلر خود ایجاد کرده ام:
[OutputCache(Duration = (7 * 24 * 60 * 60), VaryByParam = "none")]
[AllowAnonymous]
public virtual ActionResult Notification()
{
      ....
}
و در قسمت ادمین سیستم که در یک area جداگانه قرار دارد در اکشن متد خود اینگونه نوشتم:
Response.RemoveOutputCacheItem(Url.Action("Notification", "Article")); 
Response.RemoveOutputCacheItem(Url.Action("Notification", "Article", new { area = "" }));
هیچکدام از دو روش بالا برایم جواب نمی‌دهد و کش خالی نمی‌شود. علت  چیست؟
مطالب
شروع به کار با AngularJS 2.0 و TypeScript - قسمت دهم - کار با فرم‌ها - قسمت اول
هر برنامه‌ی وبی، نیاز به کار با فرم‌های وب را دارد و به همین جهت، AngularJS 2.0 به همراه دو نوع از فرم‌ها است: فرم‌های مبتنی بر قالب‌ها و فرم‌های مبتنی بر مدل‌ها.
کار با فرم‌های مبتنی بر قالب‌ها ساده‌تر است؛ اما کنترل کمتری را بر روی مباحث اعتبارسنجی داده‌های ورودی توسط کاربر، در اختیار ما قرار می‌دهند. اما فرم‌های مبتنی بر مدل‌ها هر چند به همراه اندکی کدنویسی بیشتر هستند، اما کنترل کاملی را جهت اعتبارسنجی ورودی‌های کاربران، ارائه می‌دهند. در این قسمت فرم‌های مبتنی بر قالب‌ها (Template-driven forms) را بررسی می‌کنیم.


ساخت فرم مبتنی بر قالب‌های ثبت یک محصول جدید

در ادامه‌ی مثال این سری، می‌خواهیم به کاربران، امکان ثبت اطلاعات یک محصول جدید را نیز بدهیم. به همین جهت فایل‌های جدید product-form.component.ts و product-form.component.html را به پوشه‌ی App\products برنامه اضافه می‌کنیم (جهت تعریف کامپوننت فرم جدید به همراه قالب HTML آن).
الف) محتوای کامل product-form.component.html
<form #f="ngForm" (ngSubmit)="onSubmit(f.form)">
    <div class="panel panel-default">
        <div class="panel-heading">
            <h3 class="panel-title">
                Add Product
            </h3>
        </div>
        <div class="panel-body form-horizontal">
            <div class="form-group">
                <label for="productName" class="col col-md-2 control-label">Name</label>
                <div class="controls col col-md-10">
                    <input ngControl="productName" id="productName" required
                           #productName="ngForm"
                           (change)="log(productName)"
                           minlength="3"
                           type="text" class="form-control"
                           [(ngModel)]="productModel.productName"/>
                    <div *ngIf="productName.touched && productName.errors">
                        <label class="text-danger" *ngIf="productName.errors.required">
                            Name is required.
                        </label>
                        <label class="text-danger" *ngIf="productName.errors.minlength">
                            Name should be minimum {{ productName.errors.minlength.requiredLength }} characters.
                        </label>
                    </div>
                </div>
            </div>
            <div class="form-group">
                <label for="productCode" class="col col-md-2 control-label">Code</label>
                <div class="controls col col-md-10">
                    <input ngControl="productCode" id="productCode" required
                           #productCode="ngForm"
                           type="text" class="form-control"
                           [(ngModel)]="productModel.productCode"/>
                    <label class="text-danger" *ngIf="productCode.touched && !productCode.valid">
                        Code is required.
                    </label>
                </div>
            </div>
            <div class="form-group">
                <label for="releaseDate" class="col col-md-2 control-label">Release Date</label>
                <div class="controls col col-md-10">
                    <input ngControl="releaseDate" id="releaseDate" required
                           #releaseDate="ngForm"
                           type="text" class="form-control"
                           [(ngModel)]="productModel.releaseDate"/>
                    <label class="text-danger" *ngIf="releaseDate.touched && !releaseDate.valid">
                        Release Date is required.
                    </label>
                </div>
            </div>
            <div class="form-group">
                <label for="price" class="col col-md-2 control-label">Price</label>
                <div class="controls col col-md-10">
                    <input ngControl="price" id="price" required
                           #price="ngForm"
                           type="text" class="form-control"
                           [(ngModel)]="productModel.price"/>
                    <label class="text-danger" *ngIf="price.touched && !price.valid">
                        Price is required.
                    </label>
                </div>
            </div>
            <div class="form-group">
                <label for="description" class="col col-md-2 control-label">Description</label>
                <div class="controls col col-md-10">
                    <textarea ngControl="description" id="description" required
                              #description="ngForm"
                              rows="10" type="text" class="form-control"
                              [(ngModel)]="productModel.description"></textarea>
                    <label class="text-danger" *ngIf="description.touched && !description.valid">
                        Description is required.
                    </label>
                </div>
            </div>
            <div class="form-group">
                <label for="imageUrl" class="col col-md-2 control-label">Image</label>
                <div class="controls col col-md-10">
                    <input ngControl="imageUrl" id="imageUrl" required
                           #imageUrl="ngForm"
                           type="text" class="form-control"
                           [(ngModel)]="productModel.imageUrl"/>
                    <label class="text-danger" *ngIf="imageUrl.touched && !imageUrl.valid">
                        Image is required.
                    </label>
                </div>
            </div>
        </div>
        <footer class="panel-footer">
            <button [disabled]="!f.valid"
                    type="submit" class="btn btn-primary">
                Submit
            </button>
        </footer>
    </div>
</form>

ب) محتوای کامل product-form.component.ts
import { Component } from 'angular2/core';
import { Router } from 'angular2/router';
import { IProduct } from './product';
import { ProductService } from './product.service';
 
@Component({
    //selector: 'product-form',
    templateUrl: 'app/products/product-form.component.html'
})
export class ProductFormComponent {
 
    productModel = <IProduct>{}; // creates an empty object of an interface
 
    constructor(private _productService: ProductService, private _router: Router) { }
 
    log(productName): void {
        console.log(productName);
    }
 
    onSubmit(form): void {
        console.log(form);
        console.log(this.productModel);
 
        this._productService.addProduct(this.productModel)
            .subscribe((product: IProduct) => {
                console.log(`ID: ${product.productId}`);
                this._router.navigate(['Products']);
            });
    }
}

اکنون ریز جزئیات و تغییرات این دو فایل را قدم به قدم بررسی خواهیم کرد.

تا اینجا در فایل product-form.component.html یک فرم ساده‌ی HTML ایی مبتنی بر بوت استرپ 3 را تهیه کرده‌ایم. نکات ابتدایی آن، دقیقا مطابق است با مستندات بوت استرپ 3؛ از لحاظ تعریف form-horizontal و سپس ایجاد یک div با کلاس form-group و قرار دادن المان‌هایی با کلاس‌های form-control در آن. همچنین برچسب‌های تعریف شده‌ی با ویژگی for، در این المان‌ها، جهت بالارفتن دسترسی پذیری به عناصر فرم، اضافه شده‌اند. این مراحل در مورد تمام فرم‌های استاندارد وب صادق هستند و نکته‌ی جدیدی ندارند.

در ادامه تعاریف AngularJS 2.0 را به این فرم اضافه کرد‌ه‌ایم. در اینجا هر کدام از المان‌های ورودی، تبدیل به Controlهای AngularJS 2.0 شده‌اند. کلاس Control، خواص ویژه‌ای را در اختیار ما قرار می‌دهد. برای مثال value یا مقدار این المان چیست؟ وضعیت touched و untouched آن چیست؟ (آیا کاربر فوکوس را به آن منتقل کرده‌است یا خیر؟) آیا dirty است؟ (مقدار آن تغییر کرده‌است؟) و یا شاید هم pristine است؟ (مقدار آن تغییری نکرده‌است). علاوه بر این‌ها دارای خاصیت valid نیز می‌باشد (آیا اعتبارسنجی آن موفقیت آمیز است؟)؛ به همراه خاصیت errors که مشکلات اعتبارسنجی موجود را باز می‌گرداند.
<div class="form-group">
    <label for="description" class="col col-md-2 control-label">Description</label>
    <div class="controls col col-md-10">
        <textarea ngControl="description" id="description" required
                  #description="ngForm"
                  rows="10" type="text" class="form-control"
                  [(ngModel)]="productModel.description"></textarea>
        <label class="text-danger" *ngIf="description.touched && !description.valid">
            Description is required.
        </label>
    </div>
</div>
در اینجا کلاس مفید دیگری به نام ControlGroup نیز درنظر گرفته شده‌است. برای مثال هر فرم، یک ControlGroup است (گروهی متشکل از کنترل‌ها، در صفحه). البته می‌توان یک فرم بزرگ را به چندین ControlGroup نیز تقسیم کرد. تمام خواصی که برای کلاس Control ذکر شدند، در مورد کلاس ControlGroup نیز صادق هستند. با این تفاوت که این‌بار اگر به خاصیت valid آن مراجعه کردیم، یعنی تمام کنترل‌های قرار گرفته‌ی در آن گروه معتبر هستند و نه صرفا یک تک کنترل خاص. به همین ترتیب خاصیت errors نیز تمام خطاهای اعتبارسنجی یک گروه را باز می‌گرداند.
هر دو کلاس Control و ControlGroup از کلاس پایه‌ای به نام AbstractControl مشتق شده‌اند و این کلاس پایه است که خواص مشترک یاد شده را به همراه دارد.

بنابراین برای کار ساده‌تر با یک فرم AngularJS 2.0، کل فرم را تبدیل به یک ControlGroup کرده و سپس هر کدام از المان‌های ورودی را تبدیل به یک Control مجزا می‌کنیم. کار برقراری این ارتباط، با استفاده از دایرکتیو ویژه‌ای به نام ngControl انجام می‌شود. بنابراین دایرکتیو ngControl، با نامی دلخواه و معین، به تمام المان‌های ورودی، انتساب داده شده‌است.
هرچند در این مثال نام ngControl‌ها با مقدار id هر کنترل یکسان درنظر گرفته شده‌است، اما ارتباطی بین این دو نیست. مقدار id جهت استفاده‌ی در DOM کاربرد دارد و مقدار ngControl توسط AngularJS 2.0 استفاده می‌شود. جهت رسیدن به کدهایی یکدست، بهتر است این نام‌ها را یکسان درنظر گرفت؛ اما هیچ الزامی هم ندارد.

برای بررسی جزئیات این اشیاء کنترل، در المان productName، یک متغیر محلی را به نام productName# تعریف کرده‌ایم و آن‌را به دایرکتیو ngControl انتساب داده‌ایم. این انتساب توسط ngForm انجام شده‌است. زمانیکه AngularJS 2.0 یک متغیر محلی تنظیم شده‌ی به ngForm را مشاهده می‌کند، آن‌را به صورت خودکار به ngControl همان المان ورودی متصل می‌کند. سپس این متغیر محلی را به متد log ارسال کرده‌ایم. این متد در کلاس کامپوننت جاری تعریف شده‌است و کار آن نمایش شیء Control جاری در کنسول developer tools مرورگر است.
<input ngControl="productName" id="productName" required
       #productName="ngForm"
       (change)="log(productName)"
       minlength="3"
       type="text" class="form-control"
       [(ngModel)]="productModel.productName"/>


همانطور که در تصویر مشاهده می‌کنید، عناصر یک شیء Control، در کنسول نمایش داده شده‌اند و در اینجا بهتر می‌توان خواصی مانند valid و امثال آن‌را که به همراه این کنترل وجود دارند، مشاهده کرد. برای مثال خاصیت dirty آن true است چون مقدار آن المان ورودی، تغییر کرده‌است.

بنابراین تا اینجا با استفاده از دایرکتیو ngControl، یک المان ورودی را به یک شیء Control متصل کردیم. همچنین نحوه‌ی تعریف یک متغیر محلی را در المانی و سپس ارسال آن را به کلاس متناظر با کامپوننت فرم، نیز بررسی کردیم.


افزودن اعتبارسنجی به فرم ثبت محصولات

به کنترل‌هایی که به صورت فوق توسط ngControl ایجاد می‌شوند، اصطلاحا implicitly created controls می‌گویند؛ یا به عبارتی ایجاد آن‌ها به صورت «ضمنی» توسط AngularJS 2.0 انجام می‌شود که نمونه‌ای از آن‌را در تصویر فوق نیز مشاهده کردید. این نوع کنترل‌های ضمنی، امکانات اعتبارسنجی محدودی را در اختیار دارند؛ که تنها سه مورد هستند:
الف) required
ب) minlength
ج) maxlength

این‌ها ویژگی‌های استاندارد اعتبارسنجی HTML 5 نیز هستند. نمونه‌ای از اعمال این موارد را با افزودن ویژگی required، به المان‌های فرم ثبت محصولات فوق، مشاهده می‌کنید.
سپس نیاز داریم تا خطاهای اعتبارسنجی را در مقابل هر المان ورودی نمایش دهیم.
<textarea ngControl="description" id="description" required
          #description="ngForm"
          rows="10" type="text" class="form-control"></textarea>
<div class="alert alert-danger" *ngIf="description.touched && !description.valid">
    Description is required.
</div>
پس از افزودن ویژگی required به یک المان، افزودن و نمایش خطاهای اعتبارسنجی، شامل سه مرحله‌ی زیر است:
الف) ایجاد یک div ساده جهت نمایش پیام خطای اعتبار سنجی
ب) افزودن یک متغیر محلی با # و تنظیم شده‌ی به ngForm، جهت دسترسی به شیء کنترل ایجاد شده
ج) استفاده از این متغیر محلی در دایرکتیو ساختاری ngIf* جهت دسترسی به خاصیت valid آن کنترل. بر مبنای مقدار این خاصیت است که تصمیم گرفته می‌شود، پیام اجباری بودن پر کردن فیلد نمایش داده شود یا خیر.
در اینجا یک سری کلاس بوت استرپ 3 هم جهت نمایش بهتر این پیام خطای اعتبارسنجی، اضافه شده‌اند.

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



بهبود شیوه نامه‌ی پیش فرض المان‌های ورودی اطلاعات در AngularJS 2.0

می‌خواهیم اگر اعتبارسنجی یک المان ورودی با شکست مواجه شد، یک حاشیه‌ی قرمز، در اطراف آن نمایش داده شود. این مورد را با توجه به اینکه AngularJS 2.0، شیوه نامه‌های ویژه‌ای را به صورت خودکار به المان‌ها اضافه می‌کند، می‌توان به صورت سراسری به تمام فرم‌ها اضافه کرد. برای این منظور فایل app.component.css واقع در ریشه‌ی پوشه‌ی app را گشوده و تنظیمات ذیل را به آن اضافه کنید:
.ng-touched.ng-invalid{
    border: 1px solid red;
}

ویژگی‌های اضافه شده‌ی در حالت شکست اعتبارسنجی؛ مانند ng-invalid


ویژگی‌های اضافه شده‌ی در حالت موفقیت اعتبارسنجی؛ مانند ng-valid



مدیریت چندین ویژگی اعتبارسنجی یک المان با هم

گاهی از اوقات نیاز است برای یک المان ورودی، چندین نوع اعتبارسنجی مختلف را تعریف کرد. برای مثال فرض کنید که ویژگی‌های required و همچنین minlength، برای نام محصول تنظیم شده‌اند. در این حالت ذکر productName.valid خیلی عمومی است و هر دو حالت اجباری بودن فیلد و حداقل طول آن‌را با هم به همراه دارد:
<div class="alert alert-danger" *ngIf="productName.touched && !productName.valid">
   Name is required.
</div>
بنابراین در این حالت از روش ذیل استفاده می‌شود:
<div *ngIf="productName.touched && productName.errors">
    <div class="alert alert-danger" *ngIf="productName.errors.required">
        Name is required.
    </div>
    <div class="alert alert-danger" *ngIf="productName.errors.minlength">
        Name should be minimum 3 characters.
    </div>
</div>
خاصیت errors نیز یکی دیگر از خواص شیء کنترل است. اگر نال بود، یعنی خطایی وجود ندارد و در غیراینصورت، به ازای هر نوع اعتبارسنجی تعریف شده، خواصی به آن اضافه می‌شوند. بنابراین ذکر productName.errors.required به این معنا است که آیا خاصیت errors، دارای کلیدی به نام required است؟ اگر بله، یعنی این فیلد هنوز پر نشده‌است.
همچنین چون در این حالت productName.touched نیاز است چندین بار تکرار شود، می‌توان آن‌را در یک div محصور کننده‌ی دو div مورد نیاز جهت نمایش خطاهای اعتبارسنجی قرار داد. به علاوه بررسی نال نبودن productName.errors نیز در div محصور کننده صورت گرفته‌است و دیگر نیازی نیست این بررسی را به ngIfهای داخلی اضافه کرد.

نکته 1
اگر علاقمند بودید تا جزئیات خاصیت errors را مشاهده کنید، آن‌را می‌توان توسط pipe توکاری به نام json به صورت موقت نمایش داد و بعد آن‌را حذف کرد:
 <div *ngIf="productName.touched && productName.errors">
  {{ productName.errors | json }}

نکته 2
بجای ذکر مستقیم عدد سه در «minimum 3 characters»، می‌توان این عدد را مستقیما از تعریف ویژگی minlength نیز استخراج کرد:
 Name should be minimum {{ productName.errors.minlength.requiredLength }} characters.


بررسی ngForm

شبیه به ngControl که یک المان ورودی را به یک کنترل AngularJS 2.0 متصل می‌کند، دایرکتیو دیگری نیز به نام ngForm وجود دارد که کل فرم را به شیء ControlGroup بایند می‌کند و برخلاف ngControl، نیازی به ذکر صریح آن وجود ندارد. هر زمانیکه AngularJS 2.0، المان استاندارد فرمی را در صفحه مشاهده می‌کند، این اتصالات را به صورت خودکار برقرار خواهد کرد.
ngForm دارای خاصیتی است به نام ngSumbit که از نوع EventEmitter است (نمونه‌ای از آن را در مبحث کامپوننت‌های تو در تو پیشتر ملاحظه کرده‌اید). بنابراین از آن می‌توان جهت اتصال رخداد submit فرم، به متدی در کلاس کامپوننت خود، استفاده کرد. متد متصل به این رخداد، زمانی فراخوانی می‌شود که کاربر بر روی دکمه‌ی submit کلیک کند:
 <form #f="ngForm" (ngSubmit)="onSubmit(f.form)">
همچنین در اینجا متغیر محلی f جهت دسترسی به شیء ControlGroup و ارسال آن به متد onSubmit تعریف شده‌است (شبیه به متغیرهای محلی دسترسی به ngControl که پیشتر جهت نمایش خطاهای اعتبارسنجی، اضافه کردیم).

پس از تعریف این رخداد و اتصال آن در قالب کامپوننت، اکنون می‌توان متد onSubmit را در کلاس آن نیز اضافه کرد.
onSubmit(form): void {
   console.log(form);
}
فعلا هدف از این متد، نمایش جزئیات شیء form دریافتی، در کنسول developer tools است.



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

در قسمت بررسی ngForm، یک متغیر محلی را به نام f ایجاد کردیم که به شیء ControlGroup فرم جاری اشاره می‌کند. از این متغیر و خاصیت valid آن می‌توان با کمک property binding به خاصیت disabled یک دکمه، آن‌را به صورت خودکار فعال یا غیرفعال کرد:
<button [disabled]="!f.valid"
        type="submit" class="btn btn-primary">
    Submit
</button>
هر زمانیکه کل فرم از لحاظ اعتبارسنجی مشکلی نداشته باشد، دکمه‌ی submit فعال می‌شود و برعکس.



نمایش فرم افزودن محصولات توسط سیستم Routing

با نحوه‌ی تعریف مسیریابی‌ها در قسمت قبل آشنا شدیم. برای نمایش فرم افزودن محصولات، می‌توان تغییرات ذیل را به فایل app.component.ts اعمال کرد:
//same as before...
import { ProductFormComponent }  from './products/product-form.component';
 
@Component({
    //same as before…
    template: `
                //same as before…
                    <li><a [routerLink]="['AddProduct']">Add Product</a></li>
               //same as before…
    `,
    //same as before…
})
@RouteConfig([
    //same as before…
    { path: '/addproduct', name: 'AddProduct', component: ProductFormComponent }
])
//same as before...
ابتدا به RouteConfig، مسیریابی کامپوننت فرم افزودن محصولات اضافه شده‌است. سپس ماژول این کلاس در ابتدای فایل import شده و در آخر routerLink آن به قالب سایت و منوی بالای سایت اضافه شده‌است.



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

برای اتصال المان‌های فرم به یک مدل، این مدل را به صورت یک خاصیت عمومی، در سطح کلاس کامپوننت فرم، تعریف می‌کنیم:
 productModel = <IProduct>{}; // creates an empty object of an interface
اگر از اینترفیسی مانند IProduct که در قسمت‌های قبل این سری تعریف شد، نیاز است شیء جدیدی ساخته شود، الزاما نیازی نیست تا یک کلاس جدید را از آن مشتق کرد و بعد متغیر new ClassName را تهیه کرد. در TypeScript می‌توان به صورت خلاصه از syntax فوق نیز استفاده کرد.
پس از تعریف خاصیت productModel، اکنون کافی است با استفاده از two-way data binding، آن‌را به المان‌های فرم نسبت دهیم. برای مثال:
<textarea ngControl="description" id="description" required
          #description="ngForm"
          rows="10" type="text" class="form-control"
          [(ngModel)]="productModel.description"></textarea>
در اینجا با استفاده از ngModel و انقیاد دو طرفه، کار اتصال به خاصیت توضیحات شیء محصول انجام شده‌است. اکنون بلافاصله تغییرات اعمالی به فرم، به مدل متناظر منعکس می‌شود و برعکس. این ngModel را به تمام المان‌های ورودی فرم متصل خواهیم کرد.
پس از تعریف یک چنین اتصالی، دیگر نیازی به مقدار دهی پارامتر onSubmit(f.form) نیست. زیرا شیء productModel، در متد onSumbit در دسترس است و این شیء همواره حاوی آخرین تغییرات اعمالی به المان‌های فرم است.

پس از اینکه فرم را به مدل آن متصل کردیم، فایل product.service.ts را گشوده و متد جدید addProduct را به آن اضافه کنید:
addProduct(product: IProduct): Observable<IProduct> {
    let headers = new Headers({ 'Content-Type': 'application/json' }); // for ASP.NET MVC
        let options = new RequestOptions({ headers: headers });
 
    return this._http.post(this._addProductUrl, JSON.stringify(product), options)
        .map((response: Response) => <IProduct>response.json())
        .do(data => console.log("Product: " + JSON.stringify(data)))
        .catch(this.handleError);
}
کار این متد، ارسال شیء محصول به یک اکشن متد برنامه‌ی ASP.NET MVC جاری است. با جزئیات کار با obsevables درمطلب «دریافت اطلاعات از سرور» پیشتر آشنا شده‌ایم.
نکته‌ی مهم اینجا است که content type پیش فرض ارسالی متد post آن، plain text است و در این حالت ASP.NET MVC شیء JSON دریافتی از کلاینت را پردازش نخواهد کرد. بنابراین نیاز است تا هدر content type را به صورت صریحی در اینجا ذکر نمود؛ در غیراینصورت در سمت سرور، شاهد نال بودن مقادیر دریافتی از کاربران خواهیم بود.
امضای سمت سرور متد دریافت اطلاعات از کاربر، چنین شکلی را دارد (تعریف شده در فایل Controllers\HomeController.cs):
 [HttpPost]
public ActionResult AddProduct(Product product)
{

اشیاء هدرها و تنظیمات درخواست، در متد addProduct سرویس ProductService، در ماژول‌های ذیل تعریف شده‌اند که باید به ابتدای فایل product.service.ts اضافه شوند:
 import { Headers, RequestOptions } from 'angular2/http';

پس از تعریف متد addProduct در سرویس ProductService، اکنون با استفاده از ترزیق این سرویس به سازنده‌ی کلاس فرم ثبت یک محصول جدید، می‌توان متد this._productService.addProduct را جهت ارسال productModel به سمت سرور، در متد onSubmit فراخوانی کرد:
//Same as before…
import { IProduct } from './product';
import { ProductService } from './product.service';
 
@Component({
//Same as before…
})
export class ProductFormComponent {
 
    productModel = <IProduct>{}; // creates an empty object of an interface
 
    constructor(private _productService: ProductService, private _router: Router) { }
 
    //Same as before… 

    onSubmit(form): void {
        console.log(form);
        console.log(this.productModel);
 
        this._productService.addProduct(this.productModel)
            .subscribe((product: IProduct) => {
                console.log(`ID: ${product.productId}`);
                this._router.navigate(['Products']);
            });
    }
}
همانطور که ذکر شد، از آنجائیکه شیء productModel حاوی آخرین تغییرات اعمالی توسط کاربر است، اکنون می‌توان پارامتر form متد onSubmit را حذف کرد.
در اینجا پس از فراخوانی متد addProduct، متد subscribe، در انتهای زنجیره، فراخوانی شده‌است. کار آن هدایت کاربر به صفحه‌ی نمایش لیست محصولات است. در اینجا this._router از طریق تزریق وابستگی‌های سرویس مسیریاب به سازنده‌ی کلاس، تامین شده‌است. نمونه‌ی آن‌را در قسمت «افزودن دکمه‌ی back با کدنویسی» مربوطه به مطلب آشنایی با مسیریابی، پیشتر مطالعه کرده‌اید.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: MVC5Angular2.part10.zip


خلاصه‌ی بحث

فرم‌های template driven در AngularJS 2.0 به این نحو طراحی می‌شوند:
 1) ابتدا فرم HTML را به حالت معمولی آن طراحی می‌کنیم؛ با تمام المان‌های آن.
 2) به تمام المان‌های فرم، دیراکتیو ngControl را متصل می‌کنیم، تا AngularJS 2.0 آن‌را تبدیل به یک کنترل خاص خودش کند. کنترلی که دارای خواصی مانند valid و touched است.
 3) سپس برای دسترسی به این کنترل ایجاد شده‌ی به صورت ضمنی، یک متغیر محلی آغاز شده‌ی با # را به تمام المان‌ها اضافه می‌کنیم.
 4) اعتبارسنجی‌هایی را مانند required  به المان‌های فرم اضافه می‌کنیم.
 5) از متغیر محلی تعریف شده و ngIf* برای بررسی خواصی مانند valid و touched برای نمایش خطاهای اعتبارسنجی کمک گرفته می‌شود.
 6) پس از تعریف فرم، تعریف ngControlها، تعریف متغیر محلی شروع شده‌ی با # و افزودن خطاهای اعتبارسنجی، اکنون نوبت به ارسال این اطلاعات به سرور است. بنابراین رخداد ngSubmit را باید به متدی در کلاس کامپوننت جاری متصل کرد.
 7) اکنون که با کلیک بر روی دکمه‌ی submit فرم، متد onSubmit متصل به ngSubmit فراخوانی می‌شود، نیاز است بین المان‌های فرم HTML و کلاس کامپوننت، ارتباط برقرار کرد. این‌کار را توسط two-way data binding و تعریف ngModel بر روی تمام المان‌های فرم، انجام می‌دهیم. این ngModelها، به یک خاصیت عمومی که متناظر است با وهله‌ای از شیء مدل فرم، متصل هستند. بنابراین این مدل، در هر لحظه، بیانگر آخرین تغییرات کاربر است و از آن می‌توان برای ارسال اطلاعات به سرور استفاده کرد.
 8) پس از اتصال فرم به کلاس متناظر با آن، اکنون سرویس محصولات را تکمیل کرده و به آن متد HTTP Post را جهت ارسال اطلاعات سمت کاربر، به سرور، اضافه می‌کنیم. در اینجا نکته‌ی مهم، تنظیم content type ارسالی به سمت سرور است. در غیراینصورت فریم ورک سمت سرور قادر به تشخیص JSON بودن این اطلاعات نخواهد شد.
مطالب
روشی برای محدود کردن API ها که هر درخواست با یک Key جدید و منحصر به فرد قابل فراخوانی باشد ( Time-based One-time Password )
TOTPیک الگوریتمی است که از ساعت برای تولید رمزهای یکبارمصرف استفاده میکند. به این صورت که در هر لحظه یک کد منحصر به فرد تولید خواهد شد. اگر با برنامه Google Authenticator کار کرده باشید این مفهوم برایتان اشناست. 
در این مطلب میخواهیم سناریویی را پیاده سازی کنیم که برای فراخوانی API‌ها باید یک رمز منحصر به فرد همراه توکن ارسال کنند. برای انجام این کار هر کاربر و یا کلاینتی که بخواهد از API استفاده کند در ابتدا باید لاگین کند و بعد از لاگین یک ClientSecret به طول 16 کاراکتر به همراه توکن به او ارسال میکنیم. از ClientSecret برای رمزنگاری کد ارسال شده ( TOTP ) استفاده میکنیم. مانند Public/Private key یک Key ثابت در سمت سرور و یک Key ثابت در برنامه موبایل وجود دارد و هنگام رمزنگاری TOTP علاوه بر Key موجود در موبایل و سرور از ClientSecret خود کاربر هم استفاده میکنیم تا TOTP بوجود آمده کاملا منحصر به فرد باشد. ( مقدار TOTP را با استفاده از دو کلید Key و ClientSecret رمزنگاری میکنیم ).
در این مثال تاریخ فعلی را ( UTC )  قبل از فراخوانی API در موبایل میگیریم و Ticks آن را در یک مدل ذخیره میکنیم. سپس مدل که شامل تایم فعلی میباشد را سریالایز میکنیم و به string تبدیل میکنیم سپس رشته بدست آمده را با استفاده از الگوریتم AES رمزنگاری میکنیم با استفاده از Key ثابت و ClientSecret. سپس این مقدار بدست آمده را در هدر Request-Key قرار میدهیم. توجه داشته باشید باید ClientSecret هم در یک هدر دیگر به سمت سرور ارسال شود زیرا با استفاده از این ClientSecret عملیات رمزگشایی مهیا میشود. به عنوان مثال ساعت فعلی به صورت Ticks به این صورت میباشد "637345971787256752 ". این عدد از نوع long است و با گذر زمان مقدار آن بیشتر میشود. در نهایت مدل نهایی سریالایز شده به این صورت است :
{"DateTimeUtcTicks":637345812872371593}
و مقدار رمزنگاری شده برابر است با :
g/ibfD2M3uE1RhEGxt8/jKcmpW2zhU1kKjVRC7CyrHiCHkdaAmLOwziBATFnHyJ3
مدل ارسالی شامل یک پراپرتی به نام DateTimeUtcTicks است که از نوع long میباشد و این مدل را قبل از فراخوانی API ایجاد میکند و مقدار رمزنگاری شده بدست آمده را در هدر Request-Key قرار میدهد به همراه ClientSecret در هدر مربوط به ClientSecret. این عمل در سمت موبایل باید انجام شود و در سمت سرور باید با استفاده از ClientSecret ارسال شده و Key ثابت در سرور این هدر را رمزگشایی کنند. چون این کار زیاد وقت گیر نیست و نهایتا یک دقیقه اختلاف زمان بین درخواست ارسال شده و زمان دریافت درخواست در سرور وجود دارد, در سمت سرور مقدار بدست آمده ( مقدار ارسال شده DateTimeUtcTicks که رمزگشایی شده است ) را اینگونه حساب میکنیم که مقدار ارسال شده از یک دقیقه قبل زمان فعلی باید بیشتر یا مساوی باشد و از تاریخ فعلی باید کوچکتر مساوی باشد. به این صورت 
var dateTimeNow = DateTime.UtcNow;
var expireTimeFrom = dateTimeNow.AddMinutes(-1).Ticks;
var expireTimeTo = dateTimeNow.Ticks;

string clientSecret = httpContext.Request.Headers["ClientSecret"].ToString();
string decryptedRequestHeader = AesProvider.Decrypt(requestKeyHeader, clientSecret);
var requestKeyData = System.Text.Json.JsonSerializer.Deserialize<ApiLimiterDto>(decryptedRequestHeader);

if (requestKeyData.DateTimeUtcTicks >= expireTimeFrom && requestKeyData.DateTimeUtcTicks <= expireTimeTo)
و در نهایت اگر مقدار ارسال شده در بین این بازه باشد به معنی معتبر بودن درخواست میباشد در غیر این صورت باید خطای مربوطه را به کاربر نمایش دهیم. در ادامه برای پیاده  پیاده سازی این سناریو از یک Middleware استفاده کرده‌ایم که ابتدا بررسی میکند آیا درخواست ارسال شده حاوی هدر Request-Key و ClientSecret میباشد یا خیر؟ اگر هدر خالی باشد یا مقدار هدر نال باشد، خطای 403 را به کاربر نمایش میدهیم. برای جلوگیری از استفاده‌ی مجدد از هدر رمزنگاری شده، هنگامیکه اولین درخواست به سمت سرور ارسال میشود، رشته‌ی رمزنگاری شده را در کش ذخیره میکنیم و اگر مجددا همان رشته را ارسال کند، اجازه‌ی دسترسی به API را به او نخواهیم داد. کش را به مدت 2 دقیقه نگه میداریم؛ چون برای هر درخواست نهایتا یک دقیقه اختلاف زمانی را در نظر گرفته‌ایم. 
 در ادامه اگر رشته‌ی رمزنگاری شده در کش موجود باشد، مجددا پیغام "Forbidden: You don't have permission to call this api" را به کاربر نمایش میدهیم؛ زیرا به این معناست که رشته‌ی رمزنگاری شده قبلا ارسال شده است. سپس رشته رمزنگاری شده را رمزگشایی میکنیم و به مدل ApiLimiterDto دیسریالایز میکنیم و بررسی میکنیم که مقدار Ticks ارسال شده از طرف موبایل، از یک دقیقه قبل بیشتر بوده و از زمان حال کمتر باشد. اگر در بین این دو بازه باشد، یعنی درخواست معتبر هست و اجازه فراخوانی API را دارد؛ در غیر این صورت پیغام 403 را به کاربر نمایش میدهیم.
مدل ApiLimiterDto   :
public class ApiLimiterDto
{
    public long DateTimeUtcTicks { get; set; }
}
میان افزار :
public class ApiLimiterMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IDistributedCache _cache;

    public ApiLimiterMiddleware(RequestDelegate next, IDistributedCache cache)
    {
        _next = next;
        _cache = cache;
    }
    private const string requestKey = "Request-Key";
    private const string clientSecretHeader = "ClientSecret";
    public async Task InvokeAsync(HttpContext httpContext)
    {
        if (!httpContext.Request.Headers.ContainsKey(requestKey) || !httpContext.Request.Headers.ContainsKey(clientSecretHeader))
        {
            await WriteToReponseAsync();
            return;
        }

        var requestKeyHeader = httpContext.Request.Headers[requestKey].ToString();
        string clientSecret = httpContext.Request.Headers[clientSecretHeader].ToString();
        if (string.IsNullOrEmpty(requestKeyHeader) || string.IsNullOrEmpty(clientSecret))
        {
            await WriteToReponseAsync();
            return;
        }
        //اگر کلید در کش موجود بود یعنی کاربر از کلید تکراری استفاده کرده است
        if (_cache.GetString(requestKeyHeader) != null)
        {
            await WriteToReponseAsync();
            return;
        }
        var dateTimeNow = DateTime.UtcNow;
        var expireTimeFrom = dateTimeNow.AddMinutes(-1).Ticks;
        var expireTimeTo = dateTimeNow.Ticks;

        string decryptedRequestHeader = AesProvider.Decrypt(requestKeyHeader, clientSecret);
        var requestKeyData = System.Text.Json.JsonSerializer.Deserialize<ApiLimiterDto>(decryptedRequestHeader);

        if (requestKeyData.DateTimeUtcTicks >= expireTimeFrom && requestKeyData.DateTimeUtcTicks <= expireTimeTo)
        {
            //ذخیره کلید درخواست در کش برای جلوگیری از استفاده مجدد از کلید
            await _cache.SetAsync(requestKeyHeader, Encoding.UTF8.GetBytes("KeyExist"), new DistributedCacheEntryOptions
            {
                AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(2)
            });
            await _next(httpContext);
        }
        else
        {
            await WriteToReponseAsync();
            return;
        }

        async Task WriteToReponseAsync()
        {
            httpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
            await httpContext.Response.WriteAsync("Forbidden: You don't have permission to call this api");
        }
    }
}
برای رمزنگاری و رمزگشایی، یک کلاس را به نام AesProvider ایجاد کرده‌ایم که عملیات رمزنگاری و رمزگشایی را فراهم میکند.
public static class AesProvider
{
    private static byte[] GetIV()
    {
        //این کد ثابتی است که باید در سمت سرور و موبایل موجود باشد
        return encoding.GetBytes("ThisIsASecretKey");
    }
    public static string Encrypt(string plainText, string key)
    {
        try
        {
            var aes = GetRijndael(key);
            ICryptoTransform AESEncrypt = aes.CreateEncryptor(aes.Key, aes.IV);
            byte[] buffer = encoding.GetBytes(plainText);
            string encryptedText = Convert.ToBase64String(AESEncrypt.TransformFinalBlock(buffer, 0, buffer.Length));
            return encryptedText;
        }
        catch (Exception)
        {
            throw new Exception("an error occurred when encrypting");
        }
    }
    private static RijndaelManaged GetRijndael(string key)
    {
        return new RijndaelManaged
        {
            KeySize = 128,
            BlockSize = 128,
            Padding = PaddingMode.PKCS7,
            Mode = CipherMode.CBC,
            Key = encoding.GetBytes(key),
            IV = GetIV()
        };
    }
    private static readonly Encoding encoding = Encoding.UTF8;
    public static string Decrypt(string plainText, string key)
    {
        try
        {
            var aes = GetRijndael(key);
            ICryptoTransform AESDecrypt = aes.CreateDecryptor(aes.Key, aes.IV);
            byte[] buffer = Convert.FromBase64String(plainText);

            return encoding.GetString(AESDecrypt.TransformFinalBlock(buffer, 0, buffer.Length));
        }
        catch (Exception)
        {
            throw new Exception("an error occurred when decrypting");
        }
    }
}
متد Decrypt و Encrypt یک ورودی به نام key دریافت میکنند که از هدر ClientSecret دریافت میشود. در سمت سرور عملا عمل Decrypt انجام میشود و Encrypt برای این مثال در سمت سرور کاربردی ندارد. 
برای رمزنگاری با استفاده از روش AES، چون از 128 بیت استفاده کرده‌ایم، باید طول متغییر key برابر 16 کاراکتر باشد و IV هم باید کمتر یا برابر 16 کاراکتر باشد.
در نهایت برای استفاده از این میان افزار میتوانیم از MiddlewareFilter استفاده کنیم که برای برخی از api‌های مورد نظر از آن استفاده کنیم.
کلاس ApiLimiterPipeline :
public class ApiLimiterPipeline
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseMiddleware<ApiLimiterMiddleware>();
    }
}
نحوه استفاده از میان افزار برای یک اکشن خاص :
[Route("api/[controller]/[action]")]
[ApiController]
public class ValuesController : ControllerBase
{
    [MiddlewareFilter(typeof(ApiLimiterPipeline))]
    public async Task<IActionResult> Get()
    {
        return Ok("Hi");
    }
}

مطالب
Tag Helper Components در ASP.NET Core 2.0
Tag Helper Components یکی از ویژگی‌های جدید ASP.NET Core 2.0 است و هدف آن میسر ساختن ایجاد و یا ویرایش المان‌های HTML ایی در حال رندر در صفحه هستند. برای مثال یکی از کاربردهای آن‌ها می‌تواند افزودن اسکریپتی به صورت پویا به تمام صفحات سایت باشد؛ مانند روش مایکروسافت برای افزودن Application Insights به برنامه‌های ASP.NET Core. در این حالت متد UserApplicationInsights یک tag helper component را به سیستم تزریق وابستگی‌ها اضافه می‌کند که کار آن افزودن اسکریپت‌های Application Insights به برنامه است؛ بدون اینکه نیازی باشد تا صفحات برنامه را جهت درج این اسکریپت‌ها ویرایش کرد یا تغییر داد.

یک مثال: تهیه‌ی یک TagHelperComponent جهت ویرایش تگ‌های article

using System;
using System.ComponentModel;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.Logging;

namespace TestTagHelperComponent2.Utils
{
    public class ArticleTagHelperComponent : TagHelperComponent
    {
        public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
        {
            if (string.Equals(context.TagName, "article", StringComparison.OrdinalIgnoreCase))
            {
                output.PostContent.AppendHtml("<script>console.log('Running ArticleTagHelperComponent');</script>");
            }
            return Task.CompletedTask;
        }
    }
}
در اینجا کار با ارث بری از کلاس پایه TagHelperComponent شروع می‌شود. عملکرد آن این است که اگر موتور Razor به پردازش تگ article رسید:
<article>
    For Testing the TagHelperComponent.
</article>
آنگاه اسکریپتی را که ملاحظه می‌کنید در این بین درج کند.

در ادامه برای اینکه سیستم را از وجود این TagHelperComponent مطلع کنیم، باید آن‌را به صورت یک سرویس جدید، به فایل آغازین برنامه معرفی کنیم:
public void ConfigureServices(IServiceCollection services)
{
   services.AddTransient<ITagHelperComponent, ArticleTagHelperComponent>();
   services.AddMvc();
}
این نوع کامپوننت‌ها تمام تگ‌های مشخص article موجود در صفحه را هدف قرار می‌دهند. اما ... اگر آن‌را اجرا کنید اتفاقی خاصی رخ نخواهد داد!
نکته‌ی مهم TagHelperComponentها این است که در قسمت بررسی تگ در حال پردازش:
 if (string.Equals(context.TagName, "article", StringComparison.OrdinalIgnoreCase))
اگر این تگ ویژه که در اینجا برای مثال article نام دارد، پیشتر تحت عنوان یک TagHelperComponentTagHelper ثبت شده باشد، آنگاه قابلیت اجرا و تحت تاثیر قراردادن این تگ را خواهد یافت. به همین جهت باید این تگ را به عنوان HtmlTargetElement به صورت ذیل تعریف کرد:
using System;
using System.ComponentModel;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.Logging;

namespace TestTagHelperComponent2.Utils
{
    [HtmlTargetElement("article")]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public class ArticleTagHelperComponentTagHelper : TagHelperComponentTagHelper
    {
        public ArticleTagHelperComponentTagHelper(
            ITagHelperComponentManager componentManager,
            ILoggerFactory loggerFactory)
        : base(componentManager, loggerFactory)
        {
        }
    }
}
سپس آن‌را به فایل Views\_ViewImports.cshtml به نحو زیر اضافه نمود:
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, TestTagHelperComponent2
در اینجا TestTagHelperComponent2 نام اسمبلی جاری است که حاوی ArticleTagHelperComponentTagHelper می‌باشد.

پس از این تنظیمات است که اگر برنامه را اجرا کنید، این تغییر را (درج اسکریپت در بین تگ article) ملاحظه خواهید کرد:



Tag Helper Components توکار ASP.NET Core 2.0

در حال حاضر دو TagHelperComponent به نام‌های HeadTagHelper و BodyTagHelper به صورت پیش فرض به سیستم اضافه شده‌اند. یعنی تگ‌های head و body در ASP.NET Core 2.0 را می‌توان توسط TagHelperComponent تحت تاثیر قرار داد و نیازی به تنظیمات TagHelperComponentTagHelper اضافه‌ی فوق برای آن‌ها وجود ندارد.
یک مثال:
    public class MyHeadTagHelperComponent : TagHelperComponent
    {
        public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
        {
            if (string.Equals(context.TagName, "head", StringComparison.OrdinalIgnoreCase))
            {
                output.PostContent.AppendHtml("<script>console.log('head tag');</script>");
            }
            return Task.CompletedTask;
        }
    }
در اینجا چون تگ ویژه‌ی head پیشتر در سیستم ثبت شده‌است، مقایسه‌ی انجام شده معتبر بوده و برای فعالسازی آن تنها کاری را که باید انجام داد، ثبت سرویس آن است (البته به شرطی که Microsoft.AspNetCore.Mvc.TagHelpers در فایل Views\_ViewImports.cshtml پیشتر تعریف شده باشد):
public void ConfigureServices(IServiceCollection services)
{
   services.AddTransient<ITagHelperComponent, MyHeadTagHelperComponent>();
   services.AddTransient<ITagHelperComponent, ArticleTagHelperComponent>();
   services.AddMvc();
}
اینکار سبب درج اسکریپتی پیش از بسته شدن تگ head صفحه می‌شود: