اشتراک‌ها
بررسی بهبودهای کارآیی در NET 7.

TL;DR: .NET 7 is fast. Really fast. A thousand performance-impacting PRs went into runtime and core libraries this release, never mind all the improvements in ASP.NET Core and Windows Forms and Entity Framework and beyond. It’s the fastest .NET ever. If your manager asks you why your project should upgrade to .NET 7, you can say “in addition to all the new functionality in the release, .NET 7 is super fast.” 

بررسی بهبودهای کارآیی در NET 7.
نظرات مطالب
کار با SignalR Core از طریق یک کلاینت Angular
به روز رسانی برای NET Core 2.2.104. و Angular 7 و signalr 1.1.2 
بسته‌ی سمت کلاینت signalr به صورت زیر تامین می‌شود:
npm install @aspnet/signalr --save
نکات Angular ای موجود در مطلب جاری هنوز برقرار هستند؛ منهای تغییر روش وهله سازی hubConnection که به صورت زیر در آمده‌است:
let hubConnection = new HubConnectionBuilder()
      .withUrl(this.hubPath)
      .build();
مابقی آن مانند قبل است.

در سمت سرور هم تنظیمات Cors به صورت زیر تغییر می‌کند به همراه «تغییرات مورد نیاز جهت ارتقاء به ASP.NET Core 2.1»:
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSignalR();
            services.AddCors(options =>
            {
                options.AddPolicy("CorsPolicy",
                    builder => builder
                        .WithOrigins("http://localhost:4200") //Note:  The URL must be specified without a trailing slash (/).
                        .AllowAnyMethod()
                        .AllowAnyHeader()
                        //.SetIsOriginAllowed((host) => true)
                        .AllowCredentials());
            });

پس از اعمال این تغییرات، کدهای به روز شده‌ی انتهای مطلب را از اینجا می‌توانید دریافت کنید: SignalRCore2Sample-SDK-2.2.104.zip
اشتراک‌ها
مخزن آموزشی Net. Core با مثالهای پیاده سازی شده

در این مخزن روش‌های بهینه و توصیه شده جهت ساخت برنامه‌های تحت وب با استفاده از Net Core. در قالب 12 پروژه پیاده سازی شده است که منبع خوبی جهت الگو برداری است.

Boilerplate for ASP.NET Core reference application with Entity Framework Core, demonstrating a layered application architecture with DDD best practices. Implements NLayer Hexagonal architecture (Core, Application, Infrastructure and Presentation Layers) and Domain Driven Design (Entities, Repositories, Domain/Application Services, DTO's...) and aimed to be a Clean Architecture, with applying SOLID principles in order to use for a project template. Also implements best practices like loosely-coupled, dependency-inverted architecture and using design patterns such as Dependency Injection, logging, validation, exception handling, localization and so on. 

مخزن آموزشی Net. Core با مثالهای پیاده سازی شده
نظرات مطالب
اعمال تزریق وابستگی‌ها به مثال رسمی ASP.NET Identity
سلام،

اگر بخوایم مقاله فعلی رو با درنظر گرفتن موارد زیر به یک برنامه ASP.NET Core اعمال کنیم به چه نحوی هستش؟

ممنون
مطالب
تغییرات رمزنگاری اطلاعات در NET Core.
در NET Core. به ظاهر دیگر خبری از کلاس‌هایی مانند RNGCryptoServiceProvider برای تولید اعداد تصادفی و یا SHA256Managed (و تمام کلاس‌های Managed_) برای هش کردن اطلاعات نیست. در ادامه این موارد را بررسی کرده و با معادل‌های آن‌ها در NET Core. آشنا خواهیم شد.


تغییرات الگوریتم‌های هش کردن اطلاعات

با حذف و تغییرنام کلاس‌هایی مانند SHA256Managed (و تمام کلاس‌های Managed_) در NET Core.، معادل کدهایی مانند:
using (var sha256 = new SHA256Managed()) 
{ 
   // Crypto code here... 
}
به صورت ذیل درآمده‌است:
public static string GetHash(string text)
{
  using (var sha256 = SHA256.Create())
  {
   var hashedBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(text));
   return BitConverter.ToString(hashedBytes).Replace("-", "").ToLower();
  }
}
البته اگر از یک برنامه‌ی ASP.NET Core استفاده می‌کنید، اسمبلی‌های مرتبط آن به صورت پیش فرض به پروژه الحاق شده‌اند. اما اگر می‌خواهید یک کتابخانه‌ی جدید را ایجاد کنید، نیاز است وابستگی ذیل را نیز به فایل project.json آن اضافه نمائید:
 "dependencies": {
  "System.Security.Cryptography.Algorithms": "4.2.0"
},

به علاوه اگر نیاز به محاسبه‌ی هش حاصل از جمع چندین byte array را دارید، در اینجا می‌توان از الگوریتم‌های IncrementalHash به صورت ذیل استفاده کرد:
using (var md5 = IncrementalHash.CreateHash(HashAlgorithmName.MD5))
{
  md5.AppendData(byteArray1, 0, byteArray1.Length);
  md5.AppendData(byteArray2, 0, byteArray2.Length); 
  var hash = md5.GetHashAndReset();
}
این الگوریتم‌ها شامل MD5، SHA1، SHA256، SHA384 و SHA512 می‌شوند.


تولید اعداد تصادفی Thread safe در NET Core.

روش‌های زیادی برای تولید اعداد تصادفی در برنامه‌های دات نت وجود دارند؛ اما مشکل اکثر آن‌ها این است که thread safe نیستند و نباید از آن‌ها در برنامه‌های چند ریسمانی (مانند برنامه‌های وب)، به نحو متداولی استفاده کرد. در این بین تنها کلاسی که thread safe است، کلاس RNGCryptoServiceProvider می‌باشد؛ آن هم با یک شرط:
   private static readonly RNGCryptoServiceProvider Rand = new RNGCryptoServiceProvider();
از کلاس آن باید تنها یک وهله‌ی static readonly در کل برنامه وجود داشته باشد (مطابق مستندات MSDN).
بنابراین اگر در کدهای خود چنین تعریفی را دارید:
 var rand = new RNGCryptoServiceProvider();
اشتباه است و باید اصلاح شود.
در NET Core. این کلاس به طور کامل حذف شده‌است و معادل جدید آن کلاس RandomNumberGenerator است که به صورت ذیل قابل استفاده است (و در عمل تفاوتی بین کدهای آن با کدهای RNGCryptoServiceProvider نیست):
    public interface IRandomNumberProvider
    {
        int Next();
        int Next(int max);
        int Next(int min, int max);
    }

    public class RandomNumberProvider : IRandomNumberProvider
    {
        private readonly RandomNumberGenerator _rand = RandomNumberGenerator.Create();

        public int Next()
        {
            var randb = new byte[4];
            _rand.GetBytes(randb);
            var value = BitConverter.ToInt32(randb, 0);
            if (value < 0) value = -value;
            return value;
        }

        public int Next(int max)
        {
            var randb = new byte[4];
            _rand.GetBytes(randb);
            var value = BitConverter.ToInt32(randb, 0);
            value = value % (max + 1); // % calculates remainder
            if (value < 0) value = -value;
            return value;
        }

        public int Next(int min, int max)
        {
            var value = Next(max - min) + min;
            return value;
        }
    }
در اینجا نیز یک وهله از کلاس RandomNumberGenerator را ایجاد کرده‌ایم، اما استاتیک نیست. علت اینجا است که چون برنامه‌های ASP.NET Core به همراه یک IoC Container توکار هستند، می‌توان این کلاس را با طول عمر singleton معرفی کرد که همان کار را انجام می‌دهد:
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.TryAddSingleton<IRandomNumberProvider, RandomNumberProvider>();
و پس از این تنظیم، می‌توان سرویس IRandomNumberProvider را در تمام قسمت‌های برنامه، با کمک تزریق وابستگی‌های آن در سازنده‌ی کلاس‌ها، استفاده کرد و نکته‌ی مهم آن thread safe بودن آن جهت کاربردهای چند ریسمانی است. بنابراین دیگر در برنامه‌های وب خود از new Random استفاده نکنید.


نیاز به الگوریتم‌های رمزنگاری متقارن قوی و معادل بهتر آن‌ها در ASP.NET Core

ASP.NET Core به همراه یکسری API جدید است به نام data protection APIs که روش‌هایی را برای پیاده سازی بهتر الگوریتم‌های هش کردن اطلاعات و رمزنگاری اطلاعات، ارائه می‌دهند و برای مثال ASP.NET Core Identity و یا حتی Anti forgery token آن، در پشت صحنه دقیقا از همین API برای انجام کارهای رمزنگاری اطلاعات استفاده می‌کنند.
برای مثال اگر بخواهید کتابخانه‌ای را طراحی کرده و در آن از الگوریتم AES استفاده نمائید، نیاز است تنظیم اضافه‌تری را جهت دریافت کلید عملیات نیز اضافه کنید. اما با استفاده از data protection APIs نیازی به اینکار نیست و مدیریت ایجاد، نگهداری و انقضای این کلید به صورت خودکار توسط سیستم data protection انجام می‌شود. کلیدهای این سیستم موقتی هستند و طول عمری محدود دارند. بنابراین باتوجه به این موضوع، روش مناسبی هستند برای تولید توکن‌های Anti forgery و یا تولید محتوای رمزنگاری شده‌ی کوکی‌ها. بنابراین نباید از آن جهت ذخیره سازی اطلاعات ماندگار در بانک‌های اطلاعاتی استفاده کرد.
فعال سازی این سیستم نیازی به تنظیمات اضافه‌تری در ASP.NET Core ندارد و جزو پیش فرض‌های آن است. در کدهای ذیل، نمونه‌ای از استفاده‌ی از این سیستم را ملاحظه می‌کنید:
    public interface IProtectionProvider
    {
        string Decrypt(string inputText);
        string Encrypt(string inputText);
    }

namespace Providers
{
    public class ProtectionProvider : IProtectionProvider
    {
        private readonly IDataProtector _dataProtector;

        public ProtectionProvider(IDataProtectionProvider dataProtectionProvider)
        {
            _dataProtector = dataProtectionProvider.CreateProtector(typeof(ProtectionProvider).FullName);
        }

        public string Decrypt(string inputText)
        {
                var inputBytes = Convert.FromBase64String(inputText);
                var bytes = _dataProtector.Unprotect(inputBytes);
                return Encoding.UTF8.GetString(bytes);
        }

        public string Encrypt(string inputText)
        {
            var inputBytes = Encoding.UTF8.GetBytes(inputText);
            var bytes = _dataProtector.Protect(inputBytes);
            return Convert.ToBase64String(bytes);
        }
    }
}
کار با تزریق IDataProtectionProvider در سازنده‌ی کلاس شروع می‌شود. سرویس آن به صورت پیش فرض توسط ASP.NET Core در اختیار برنامه قرار می‌گیرد و نیازی به تنظیمات اضافه‌تری ندارد. پس از آن باید یک محافظت کننده‌ی جدید را با فراخوانی متد CreateProtector آن ایجاد کرد و در آخر کار با آن به سادگی فراخوانی متدهای Unprotect و Protect است که ملاحظه می‌کنید.
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.TryAddSingleton<IProtectionProvider, ProtectionProvider>();
پس از طراحی این سرویس جدید نیز می‌توان وابستگی‌های آن‌را به نحو فوق به سیستم معرفی کرد و از سرویس IProtectionProvider آن در تمام قسمت‌های برنامه (جهت کارهای کوتاه مدت رمزنگاری اطلاعات) استفاده نمود.

مستندات مفصل این API را در اینجا می‌توانید مطالعه کنید.


معادل الگوریتم Rijndael در NET Core.

همانطور که عنوان شد، طول عمر کلیدهای data protection API محدود است و به همین جهت برای کارهایی چون تولید توکن‌ها، رمزنگاری کوئری استرینگ‌ها و یا کوکی‌های کوتاه مدت، بسیار مناسب است. اما اگر نیاز به ذخیره سازی طولانی مدت اطلاعات رمزنگاری شده وجود داشته باشد، یکی از الگوریتم‌های مناسب اینکار، الگوریتم AES است.
الگوریتم Rijndael نگارش کامل دات نت، اینبار نام اصلی آن یا AES را در NET Core. پیدا کرده‌است و نمونه‌ای از نحوه‌ی استفاده‌ی از آن، جهت رمزنگاری و رمزگشایی اطلاعات، به صورت ذیل است:
public string Decrypt(string inputText, string key, string salt)
{
    var inputBytes = Convert.FromBase64String(inputText);
    var pdb = new Rfc2898DeriveBytes(key, Encoding.UTF8.GetBytes(salt));
 
    using (var ms = new MemoryStream())
    {
        var alg = Aes.Create();
 
        alg.Key = pdb.GetBytes(32);
        alg.IV = pdb.GetBytes(16);
 
        using (var cs = new CryptoStream(ms, alg.CreateDecryptor(), CryptoStreamMode.Write))
        {
            cs.Write(inputBytes, 0, inputBytes.Length);
        }
        return Encoding.UTF8.GetString(ms.ToArray());
    }
}
 
public string Encrypt(string inputText, string key, string salt)
{
 
    var inputBytes = Encoding.UTF8.GetBytes(inputText);
    var pdb = new Rfc2898DeriveBytes(key, Encoding.UTF8.GetBytes(salt));
    using (var ms = new MemoryStream())
    {
        var alg = Aes.Create();
 
        alg.Key = pdb.GetBytes(32);
        alg.IV = pdb.GetBytes(16);
 
        using (var cs = new CryptoStream(ms, alg.CreateEncryptor(), CryptoStreamMode.Write))
        {
            cs.Write(inputBytes, 0, inputBytes.Length);
        }
        return Convert.ToBase64String(ms.ToArray());
    }
}
همانطور که در کدهای فوق نیز مشخص است، این روش نیاز به قید صریح key و salt را دارد. اما روش استفاده‌ی از data protection APIs مدیریت key و salt را خودکار کرده‌است؛ آن هم با طول عمر کوتاه. در این حالت دیگر نیازی نیست تا جفت کلیدی را که احتمالا هیچگاه در طول عمر برنامه تغییری نمی‌کنند، از مصرف کننده‌ی کتابخانه‌ی خود دریافت کنید و یا حتی نام خودتان را به عنوان کلید در کدها به صورت hard coded قرار دهید!
اشتراک‌ها
تغییرات ASP.NET Core در NET 8 Preview 5.

Here’s a summary of what’s new in this preview release:

  • Improved ASP.NET Core debugging experience
  • Servers & middleware
    • IHttpSysRequestTimingFeature
    • SNI hostname in ITlsHandshakeFeature
    • IExceptionHandler
  • Blazor
    • New Blazor Web App project template
    • Blazor router integration with endpoint routing
    • Enable interactivity for individual components with Blazor Server
    • Improved packaging of Webcil files
    • Blazor Content Security Policy (CSP) compatibility
  • API authoring
    • Support for generic attributes
  • SignalR
    • SignalR seamless reconnect
  • Native AOT
    • Support for AsParameters and automatic metadata generation in compile-timed generated minimal APIs
  • Authentication and authorization
    • Authentication updates in ASP.NET Core SPA templates
    • New analyzer for recommended AuthorizationBuilder usage
     
تغییرات ASP.NET Core در NET 8 Preview 5.
مطالب
Blazor 5x - قسمت 21 - احراز هویت و اعتبارسنجی کاربران Blazor Server - بخش 1 - افزودن قالب ابتدایی Identity
در ادامه‌ی مثال این سری، می‌خواهیم امکان ثبت و ویرایش اتاق‌ها را (و یا امکانات رفاهی یک هتل را که به صورت تمرینی دقیقا مشابه افزودن مشخصات اتاق‌ها، اضافه شده و کدهای آن از فایل پیوستی انتهای بحث قابل دریافت است) به کاربران اعتبارسنجی شده‌ی دارای نقش مدیریتی، محدودیت کنیم و نمی‌خواهیم عموم کاربران برنامه بتوانند در این قسمت‌ها، تغییری را ایجاد کنند. برای این منظور از امکانات توکار و استاندارد ASP.NET Core Identity استفاده خواهیم کرد و این کتابخانه را از صفر و بدون تغییری، به پروژه‌ی جاری از نوع Blazor Server، به همان نحوی که طراحی شده، اضافه می‌کنیم و در مراحل بعدی، بر اساس نیازهای برنامه، قسمت‌های مختلف آن‌را سفارشی سازی خواهیم کرد.


تغییر نوع DbContext برنامه

پیش از شروع به یکپارچه کردن ASP.NET Core Identity با برنامه‌ی جاری، نیاز است نوع DbContext آن‌را به صورت زیر تغییر داد:
using BlazorServer.Entities;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace BlazorServer.DataAccess
{
    public class ApplicationDbContext : IdentityDbContext
    {
      // ...
- بنابراین به فایل BlazorServer\BlazorServer.DataAccess\ApplicationDbContext.cs مراجعه کرده و برای شروع، بجای DbContext، از IdentityDbContext استفاده می‌کنیم.
- این تغییر، نیاز به نصب بسته‌ی نیوگت Microsoft.AspNetCore.Identity.EntityFrameworkCore را نیز در پروژه‌ی جاری دارد تا IdentityDbContext آن شناسایی شده و قابل استفاده شود.


نصب ابزار تولید کدهای ASP.NET Core Identity

اگر از ویژوال استودیوی کامل استفاده می‌کنید، گزینه‌ی افزودن کدهای ASP.NET Core Identity به صورت زیر قابل دسترسی است:
project -> right-click > Add > New Scaffolded Item -> select Identity > Add
اما از آنجائیکه قصد داریم این مطلب، برای کاربران VSCode و همچنین سایر سیستم عامل‌ها نیز قابل استفاده باشد، از NET Core CLI. استفاده خواهیم کرد. برای این منظور، ابتدا ابزار سراسری dotnet-aspnet-codegenerator را نصب می‌کنیم:
dotnet tool install -g dotnet-aspnet-codegenerator
سپس به پروژه‌ی اصلی Blazor Server مراجعه کرده (BlazorServer.App.csproj در این مثال) و در پوشه‌ی آن، دستورات زیر را اجرا می‌کنیم تا بسته‌های نیوگت مورد نیاز ASP.NET Core Identity و UI آن، نصب شوند:
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.AspNetCore.Identity.UI
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
پس از نصب این وابستگی‌ها، اکنون در همین ریشه‌ی پروژه‌ی اصلی، دستور زیر را اجرا می‌کنیم تا فایل‌های ASP.NET Core Identity اضافه شوند:
dotnet aspnet-codegenerator identity --dbContext BlazorServer.DataAccess.ApplicationDbContext --force
در اینجا ذکر فضای نام کامل کلاس ApplicationDbContext ضروری است.
حال اگر به پروژه دقت کنیم، پوشه‌ی جدید Areas که به همراه فایل‌های مدیریتی ASP.NET Core Identity است، اضافه شده و حاوی کدهای صفحات لاگین، ثبت نام کاربر و غیره است.


اعمال تغییرات ابتدایی مورد نیاز جهت استفاده از ASP.NET Core Identity

تا اینجا کدهای پیش‌فرض مدیریتی ASP.NET Core Identity را به پروژه اضافه کردیم. در ادامه نیاز است تغییرات ذیل را به پروژه‌ی اصلی Blazor Server اعمال کنیم تا بتوان از این فایل‌ها استفاده کرد:
- به فایل BlazorServer.App\Startup.cs مراجعه کرده و UseAuthentication و UseAuthorization را دقیقا در محلی که مشاهده می‌کنید، اضافه می‌کنیم. همچنین در اینجا نیاز است مسیریابی‌های razor pages را نیز فعال کرد.
namespace BlazorServer.App
{
    public class Startup
    {
        // ...
 
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {

            // ...

            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
                // ...
            });
        }
    }
}
- سپس به پوشه‌ی BlazorServer.DataAccess برنامه وارد شده و دستورات Migrations را یکبار دیگر اجرا می‌کنیم، تا جداول پیش‌فرض Identity، بر اساس Context جدید آن ایجاد شوند:
dotnet tool update --global dotnet-ef --version 5.0.4
dotnet build
dotnet ef migrations --startup-project ../BlazorServer.App/ add AddIdentity --context ApplicationDbContext
dotnet ef --startup-project ../BlazorServer.App/ database update --context ApplicationDbContext
پس از اجرای این دستورات، جداول جدید زیر به بانک اطلاعاتی برنامه اضافه می‌شوند:



افزودن گزینه‌ی منوی لاگین به برنامه‌ی Blazor Server


پس از این تغییرات، به برنامه‌ای رسیده‌ایم که مدیریت قسمت Identity آن، توسط قالب استاندارد مایکروسافت که در پوشه‌ی Areas\Identity\Pages\Account نصب شده و بر اساس فناوری ASP.NET Core Razor Pages کار می‌کند، انجام می‌شود.
اکنون می‌خواهیم در منوی برنامه‌ی Blazor Server خود که با صفحات Identity یکی شده‌است، لینکی را به صفحه‌ی لاگین این Area اضافه کنیم. اگر به فایل Shared\MainLayout.razor آن مراجعه کنیم، به صورت پیش‌فرض، لینکی به صفحه‌ی About، قرار دارد. به همین جهت این مورد را به صورت زیر اصلاح می‌کنیم:
ابتدا کامپوننت جدید BlazorServer.App\Shared\LoginDisplay.razor را با محتوای زیر ایجاد می‌کنیم:
<a href="Identity/Account/Register">Register</a>
<a href="Identity/Account/Login">Login</a>

@code {

}
که لینک‌هایی را به صفحات لاگین و ثبت نام یک کاربر جدید، تعریف می‌کند.
سپس از این کامپوننت در فایل BlazorServer.App\Shared\MainLayout.razor استفاده می‌کنیم:
<div class="top-row px-4">
    <LoginDisplay></LoginDisplay>
    <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
</div>


ثبت و فعالسازی سرویس‌های کار با ASP.NET Core Identity

البته اگر در این حال برنامه را اجرا کنیم، با کلیک بر روی لینک‌های فوق، استثنائی را مانند یافت نشدن سرویس UserManager، مشاهده خواهیم کرد. برای رفع این مشکل، به فایل BlazorServer.App\Startup.cs مراجعه کرده و سرویس‌های Identity را ثبت می‌کنیم:
namespace BlazorServer.App
{
    public class Startup
    {
        // ...

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

            services.AddIdentity<IdentityUser, IdentityRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders()
                .AddDefaultUI();

           // ...
اکنون اگر برنامه را اجرا کنیم، برای مثال صفحه‌ی ثبت یک کاربر جدید، بدون مشکل و خطایی نمایش داده می‌شود:


همانطور که مشاهده می‌کنید، قالب این قسمت Identity، با قالب قسمت Blazor Server یکی نیست؛ چون توسط Razor Pages و Area آن تامین می‌شود که master page خاص خودش را دارد. زمانیکه قالب Identity را اضافه می‌کنیم، علاوه بر Area خاص خودش، پوشه‌ی جدید Pages\Shared را نیز ایجاد می‌کند که قالب صفحات Identity را به کمک فایل Pages\Shared\_Layout.cshtml تامین می‌کند:


بنابراین سفارشی سازی قالب این قسمت، شبیه به قالبی که برای کامپوننت‌های Blazor مورد استفاده قرار می‌گیرد، باید در اینجا انجام شود و سفارشی سازی قالب کامپوننت‌های Blazor، در پوشه‌ی Shared ای که در ریشه‌ی پروژه‌است (BlazorServer.App\Shared\MainLayout.razor) انجام می‌شود.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-21.zip
مطالب
کار با کوکی‌ها در ASP.NET Core
API کار با کوکی‌ها نیز در ASP.NET Core نسبت به نگارش‌های دیگر تغییریافته‌است که در ادامه این موارد را بررسی خواهیم کرد. همچنین با کمک مطلب «تغییرات رمزنگاری اطلاعات در NET Core.» یک تامین کنند‌ه‌ی سفارشی کوکی‌های رمزنگاری شده را نیز ایجاد می‌کنیم.


خلاصه‌ای از روش‌های کار با کوکی‌ها در ASP.NET Core

ایجاد یک کوکی جدید
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
 
namespace Core1RtmEmptyTest.Controllers
{
    public class TestCookiesController : Controller
    {
        public IActionResult Index()
        {
            this.Response.Cookies.Append("key", "value", new CookieOptions
            {
                HttpOnly = true,
                Path = this.Request.PathBase.HasValue ? this.Request.PathBase.ToString() : "/",
                Secure = this.Request.IsHttps
            });
 
            return Content("OK!");
        }
    }
}
کوکی جدید را می‌توان توسط متد Append مجموعه‌ی کوکی‌ها، به Response اضافه کرد:


همانطور که در تصویر نیز مشخص است، طول عمر این کوکی، به سشن تنظیم شده‌است و با پایان سشن جاری مرورگر (بسته شدن کل مرورگر)، این کوکی نیز غیرمعتبر شده و به صورت خودکار حذف خواهد شد. برای تعیین عمر دقیق یک کوکی می‌توان از خاصیت Expires شیء CookieOptions که در مثال فوق مقدار دهی نشده‌است، استفاده کرد؛ مانند:
 Expires = DateTimeOffset.UtcNow.AddDays(2)

خواندن محتویات کوکی ذخیره شده

پس از ثبت کوکی در Response، خواندن آن در Request بعدی به شکل زیر است:
 var value = this.Request.Cookies["key"];
در این حالت اگر کلید درخواستی در مجموعه‌ی کوکی‌ها یافت نشد، نال بازگشت داده می‌شود.
شیء this.Request.Cookies از نوع IRequestCookieCollection است:
    public interface IRequestCookieCollection : IEnumerable<KeyValuePair<string, string>>, IEnumerable
    {
        string this[string key] { get; }
        ICollection<string> Keys { get; }
        bool ContainsKey(string key);
        bool TryGetValue(string key, out string value);
    }
و همانطور که ملاحظه می‌کنید، برای دریافت مقدار یک کوکی یا می‌توان از indexer آن مانند مثال فوق و یا از متد TryGetValue استفاده کرد.
در مستندات آن عنوان شده‌است که در حالت استفاده‌ی از indexer، درصورت یافت نشدن کلید، string.Empty بازگشت داده می‌شود (که آزمایشات null را نمایش می‌دهند). اما در حالت استفاده‌ی از TryGetValue بر اساس خروجی bool آن دقیقا می‌توان مشخص کرد که آیا این کوکی وجود داشته‌است یا خیر.
در اینجا همچنین متد ContainsKey نیز جهت بررسی وجود یک کلید، در مجموعه‌ی کلیدها نیز پیش بینی شد‌ه‌است.
بنابراین بهتر است جهت یافتن مقادیر کوکی‌ها از روش ذیل استفاده کرد:
string cookieValue;
if (this.Request.Cookies.TryGetValue(key, out cookieValue))
{
   // TODO: use the cookieValue
}
else
{
  // this cookie doesn't exist.
}

حذف کوکی‌های موجود

در اینجا متد Delete نیز پیش بینی شده‌است که باید بر روی کوکی‌های Response فراخوانی شود:
 this.Response.Cookies.Delete("key");
کار آن افزودن یک کوکی دیگر با همین کلید، اما منقضی شده‌است؛ تا مرورگر را مجبور به حذف آن کند. در اینجا در صورت نیاز می‌توان به عنوان پارامتر دوم، CookieOptions این کوکی جدید را نیز تنظیم کرد.


همانطور که در تصویر نیز مشخص است، در صورت عدم تنظیم CookieOptions، این کوکی جدید اضافه شده، دارای تاریخ انقضای 1970 است که سبب خواهد شد تا توسط مرورگر، غیرمعتبر درنظر گرفته شده و حذف شود.


طراحی یک تامین کننده‌ی کوکی‌های امن

پس از آشنایی با مقدمات کوکی‌ها و همچنین «بررسی تغییرات رمزنگاری اطلاعات در NET Core.»، اکنون می‌توان یک تامین کننده‌ی کوکی‌های رمزنگاری شده را برای ASP.NET Core به نحو ذیل طراحی کرد:
public interface ISecureCookiesProvider
{
    void Add(HttpContext context, string token, string value);
    bool Contains(HttpContext context, string token);
    string GetValue(HttpContext context, string token);
    void Remove(HttpContext context, string token);
}
 
public class SecureCookiesProvider : ISecureCookiesProvider
{
    private readonly IProtectionProvider _protectionProvider;
 
    public SecureCookiesProvider(IProtectionProvider protectionProvider)
    {
 
        _protectionProvider = protectionProvider;
    }
 
    public void Add(HttpContext context, string token, string value)
    {
        value = _protectionProvider.Encrypt(value);
        context.Response.Cookies.Append(token, value, getCookieOptions(context));
    }
 
    public bool Contains(HttpContext context, string token)
    {
        return context.Request.Cookies.ContainsKey(token);
    }
 
    public string GetValue(HttpContext context, string token)
    {
        string cookieValue;
        if (!context.Request.Cookies.TryGetValue(token, out cookieValue))
        {
            return null;
        }
        return _protectionProvider.Decrypt(cookieValue);
    }
 
    public void Remove(HttpContext context, string token)
    {
        if (context.Request.Cookies.ContainsKey(token))
        {
            context.Response.Cookies.Delete(token);
        }
    }
 
    /// <summary>
    /// Expires at the end of the browser's session.
    /// </summary>
    private CookieOptions getCookieOptions(HttpContext context)
    {
        return new CookieOptions
        {
            HttpOnly = true,
            Path = context.Request.PathBase.HasValue ? context.Request.PathBase.ToString() : "/",
            Secure = context.Request.IsHttps
        };
    }
}
- نکاتی را که در ابتدای مطلب در مورد ثبت و خواندن و حذف کوکی‌ها مطالعه کردید، به این کلاس اضافه شده‌اند. با این تغییر که پیش از ذخیر‌ه‌ی مقدار کوکی، این مقدار رمزنگاری می‌شود و همچنین پس از خواندن آن، رمزگشایی خواهد شد.
- در این تامین کننده‌ی کوکی‌های امن، IProtectionProvider تزریقی به سازنده‌ی کلاس را در مطلب «تغییرات رمزنگاری اطلاعات در NET Core.» پیشتر ملاحظه کرده‌اید.
- در اینجا برای ثبت سرویس جدید، تنظیمات ابتدایی برنامه چنین شکلی را پیدا می‌کنند و پس از آن می‌توان سرویس ISecureCookiesProvider را به کنترلرهای برنامه تزریق و استفاده کرد:
public class Startup
{ 
    public void ConfigureServices(IServiceCollection services)
    {
        services.TryAddSingleton<IProtectionProvider, ProtectionProvider>();
        services.TryAddSingleton<ISecureCookiesProvider, SecureCookiesProvider>();
- چون در کلاس SecureCookiesProvider، خاصیت Expires تنظیم نشده‌است، طول عمر این کوکی‌ها محدود است به مدت زمان باز بودن مرورگر. بنابراین در صورت نیاز این مورد را تغییر دهید.
مطالب
صفحه بندی اطلاعات در ASP.NET MVC به روش HashChange
یکی از مواردی که درپروژه‌‌ها زیاد مورد استفاده قرار میگیرد، نمایش داده‌های ذخیره شده‌ی در بانک اطلاعاتی، به صورت صفحه بندی شده به کاربر می‌باشد. قبلا در زمینه بحث Paging، مطلبی تهیه شده بود و در این مقاله قصد داریم کتابخانه‌ای را مورد بررسی قرار دهیم که علاوه بر ارسال داده به صورت Ajax ایی، بتواند همچنین پارامترهای مورد نظر را به صورت Query String نیز در آدرس بار نمایش دهد.
اگر به جستجوی گوگل دقت کرده باشید، به صورت Ajax ایی پیاده سازی شده‌است، با این تفاوت که بعد از هر تغییر درجستجوی مورد نظر، Url صفحه نیز تغییر میکند (برای مثال بعد از جستجوی عبارت dotNetTips  آدرس بار صفحه به شکل https://www.google.com/#q=dotNetTips&* تغییر می‌کند). برای پیاده سازی این ویژگی باید از تکنیکی به نام HashChange استفاده کرد. در نتیجه با این روش مشکل ارسال صفحه‌ای خاص در یک گرید برای دیگران، به صورت Ajax ایی و بدون مشکل انجام می‌شود. از این رو با توجه به داشتن Url‌های منحصر به فرد برای هر صفحه، تا حدی مشکل سئو سایت را نیز برطرف می‌کنیم.

برای استفاده از این ویژگی در ادامه قصد داریم پیاده سازی کتابخانه‌ی MvcAjaxPager را مورد بررسی قرار دهیم. ابتدا قبل از هر کاری، با استفاده از دستور زیر اقدام به نصب کتابخانه آن می‌نماییم:
 Install-Package MvcAjaxPager

در ادامه نحوه پیاده سازی آن را به همراه مثالی، مورد بررسی قرار می‌دهیم:

ابتدا یک مدل فرضی را همانند زیر تهیه می‌کنیم :
public class Topic
{
   public int Id;
   public string Title;
   public string Text;
}
و کلاسی را همانند زیر برای دریافت یک لیست از مطالب می‌نویسیم:
public class TopicService
{
    public static IEnumerable<Topic> Topics = new List<Topic>() {
       new Topic{Id=1,Title="Title 1",Text= "Text 1"},
       new Topic{Id=2,Title="Title 2",Text="Text 2"},
       new Topic{Id=3,Title="Title 3",Text="Text 3"},
       new Topic{Id=4,Title="Title 4",Text="Text 4"},
       new Topic{Id=5,Title="Title 5",Text="Text 5"},
       new Topic{Id=6,Title="Title 6",Text="Text 6"},
       new Topic{Id=7,Title="Title 7",Text="Text 7"},
       new Topic{Id=8,Title="Title 8",Text="Text 8"},
       new Topic{Id=9,Title="Title 9",Text="Text 9"},
       new Topic{Id=10,Title="Title 10",Text="Text 10"},
       new Topic{Id=11,Title="Title 11",Text="Text 11"},
       new Topic{Id=12,Title="Title 12",Text="Text 12"},
       new Topic{Id=13,Title="Title 13",Text="Text 13"},
       new Topic{Id=14,Title="Title 14",Text="Text 14"},
       new Topic{Id=15,Title="Title 15",Text="Text 15"},
       new Topic{Id=16,Title="Title 16",Text="Text 16"},
       new Topic{Id=17,Title="Title 17",Text="Text 17"},
       new Topic{Id=18,Title="Title 18",Text="Text 18"},
       new Topic{Id=19,Title="Title 19",Text="Text 19"},
       new Topic{Id=20,Title="Title 20",Text="Text 20"},
       new Topic{Id=21,Title="Title 21",Text="Text 21"},
       new Topic{Id=22,Title="Title 22",Text="Text 22"},
      };

    public static IEnumerable<Topic> GetAll()
    {
       return Topics.OrderBy(row => row.Id);
    }
}
همچنین کلاس زیر را اضافه میکنیم:
public class ListViewModel
{
   public IEnumerable<Topic> Topics { get; set; }
   public int PageIndex { get; set; }
   public int TotalItemCount { get; set; }
}
ابتدا یک کنترلر را ایجاد می‌کنیم به همراه اکشن متدی که قصد داریم لیستی از اطلاعات را به کاربر نمایش دهیم:
public ActionResult Index(int page = 1)
{
       var topics = TopicService.GetAll ();
       int totalItemCount = topics.Count();
       var model = new ListViewModel()
       {
              PageIndex = page,
              Topics = topics.OrderBy(p => p.Id).Skip((page - 1) * 10).Take(10).ToList(),
              TotalItemCount = totalItemCount
       };

       if (!Request.IsAjaxRequest())
       {
              return View(model);
       }

       return PartialView("_TopicList", model);
}
در اینجا بعد از واکشی اطلاعات، تعداد 10 رکورد را در هر صفحه نمایش می‌دهیم. 

و در Partial view مربوطه نیز داریم :
@using MvcAjaxPager
@model ListViewModel

@Html.AjaxPager(Model.TotalItemCount, 10, Model.PageIndex, "Index", "Home", null, new PagerOptions
   {
       ShowDisabledPagerItems = true,
       AlwaysShowFirstLastPageNumber = true,
       HorizontalAlign = "center",
       ShowFirstLast = false,
       CssClass = "NavigationBox",
       AjaxUpdateTargetId = "dvTopics",
       AjaxOnBegin = "AjaxStart",
       AjaxOnComplete = "AjaxStop"
   }, null, null)

<table>
    <tr>
        <th>
            @Html.DisplayName("ID")
        </th>
        <th>
            @Html.DisplayName("Title")
        </th>
        <th>
            @Html.DisplayName("Text")
        </th>
    </tr>

    @foreach (var topic in Model.Topics)
    {
        <tr>
            <td>
                @topic.Id
        </td>
        <td>
            @topic.Title
        </td>
        <td>
            @topic.Text
        </td>
    </tr>
    }
</table>

@Html.AjaxPager(Model.TotalItemCount, 10, Model.PageIndex, "Index", "Home", null, new PagerOptions
   {
       ShowDisabledPagerItems = true,
       AlwaysShowFirstLastPageNumber = true,
       HorizontalAlign = "center",
       ShowFirstLast = true,
       FirstPageText = "اولین",
       LastPageText = "آخرین",
       MorePageText = "...",
       PrevPageText = "قبلی",
       NextPageText = "بعدی",
       CssClass = "NavigationBox",
       AjaxUpdateTargetId = "dvTopics",
       AjaxOnBegin = "AjaxStart",
       AjaxOnComplete = "AjaxStop"
   }, null, null)

 حال برای استفاده از pager مورد نظر فقط کافیست متد AjaxPager آن را فراخوانی کنیم. این متد شامل 11  OverLoad مختلف هست.
در این قسمت TotalItemCount جمع کل رکورد‌ها، PageSize تعداد رکورد‌های هر صفحه و PageIndex آدرس صفحه جاری می‌باشد.

مهمترین بخش این pager  که قابلیت‌های زیادی را به کاربر می‌دهد، قسمت PagerOptions آن است و تعدادی از پارامتر‌های آن شامل AjaxOnBeginAjaxOnCompelte، AjaxOnSuccess ،  AjaxOnFailure میتوان تعیین کرد تا بعد از شروع، وقوع خطا، موفقیت و یا خاتمه عملیات جاوا اسکریپتی، اجرا شود. 

AlwaysShowFirstLastPageNumber جهت نمایش صفحه اول و آخر
FirstPageText جهت تعیین متن اولین صفحه
LastPageText جهت تعیین متن آخرین صفحه
CssClass ، Id  جهت تعیین Id خاص

و در انتها، در view مربوطه داریم:
@using MvcAjaxPager
@model ListViewModel
@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
</head>
<body>
    <div id="dvTopics">
        @{
            @Html.Partial("_TopicList", Model);
        }
    </div>

    <script type="text/javascript" src="@Url.Content("~/Scripts/jquery-1.7.2.min.js")"></script>
    <script type="text/javascript" src="@Url.Content("~/Scripts/path.min.js")"></script>
    <script type="text/javascript" src="@Url.Content("~/Scripts/jquery.pager-1.0.1.min.js")"></script>
    <script type="text/javascript">
        $('.NavigationBox').pager();

        //pagination before start
        function AjaxStart() {
            console.log('Start AJAX call. Loading message can be shown');
        }
        // pagination - after request
        function AjaxStop() {
            console.log('Stop AJAX call. Loading message can be hidden');
        };
    </script>
</body>
</html>
در انتهای صفحه مورد نظر می‌بایست دو فایل جاوااسکریپتی jquerypager و Path را که هنگام نصب Pager، به برنامه اضافه شده اند، فراخوانی کنیم و با استفاده از CssClass  یا Id که قبلا در بخش PagerOption تعیین کردیم، آن را انتخاب و متدpager را فراخوانی کنیم.
نظرات مطالب
معرفی ASP.NET Identity
در پست‌های آتی پیاده سازی یک تامین کننده (Provider) برای MySQL رو بررسی می‌کنم. برای اطلاعات بیشتر به Implementing a Custom MySQL ASP.NET Identity Storage Provider مراجعه کنید.
هنوز مستندات کامل و رسمی برای این فریم ورک عرضه نشده اما مطالب مفید زیادی در اینترنت وجود دارند. چند لینک در زیر لیست شده:
درباره تطابق با آموزش‌های سایت: دقیقا متوجه نشدم منظورتون کدوم الگوها است، اما چند نکته تکمیلی:
بصورت پیش فرض وقتی ASP.NET Identity به پروژه اضافه میشه کلاسی بنام ApplicationDbContext ایجاد میشه که بعنوان DbContext پیش فرض برای دیتابیس عضویت استفاده خواهد شد. اگر موجودیت جدیدی برای پروفایل کاربران بسازید باید بعنوان یک <DbSet<T به این کلاس افزوده بشه. اگر نیازی به تفکیک دیتابیس سیستم عضویت و دیتابیس اپلیکیشن نیست، بهتره از یک DbContext استفاده کنید. لایه بندی مدل ها، سرویس‌ها و کد دسترسی به داده‌ها هم ساده است چرا که کل سیستم توسط EF Code First مدیریت میشه. بنابراین استفاده از الگوهای رایج مانند تزریق وابستگی‌ها و غیره مشابه دیگر سناریو‌های EF Code First است.