مطالب
بررسی مفاهیم Covariant و Contravariant در زبان سی‌شارپ
یکی از مفاهیمی که بنظر پیچیده می‌آمد و هر دفعه موقع مطالعه از آن فرار می‌کردم، همین بحث COVARIANCE و CONTRAVARIANCE بود. در اینجا قصد دارم به زبان ساده این مفاهیم را شرح دهم.

Covariance 
A را در نظر بگیرید که قابل تبدیل به B باشد. در اینصورت X، دارای پارامتر کواریانس است اگر <X<A قابل تبدیل به <X<B باشد. بدون ذکر مثال شاید این تعریف خیلی ملموس نباشد. پس بهتر است با ذکر مثال به تشریح مفاهیم بپردازیم.
نکته: در اینجا منظور از قابل تبدیل بودن، قابل تبدیل بودن به صورت ضمنی (implicit) می‌باشد. برای مثال A از B ارث بری داشته باشد و یا A، تایپ B را پیاده سازی کند (در صورتی که B یک اینترفیس باشد). تبدیلات عددی، Boxing و تبدیلات کاستوم مجاز نیستند.
برای نمونه نوع <IFoo<T پارامتر کوواریانس T دارد، اگر کد زیر معتبر باشد:
IFoo<string> s = ...;
IFoo<object> b = s;
از C# 4.0، اینترفیسها و delegateها مجاز به استفاده از پارامتر کوواریانس T هستند؛ اما در مورد کلاس‌ها اینطور نیست. آرایه‌ها نیز مجاز هستند که در ادامه تشریح خواهند شد (اگر A قابل تبدیل به B باشد در اینصورت []A قابل تبدیل به []B خواهد بود. هر چند ممکن است به run-time exception منجر گردد که ظاهرا این پشتیبانی آرایه‌ها از پارامترهای کوواریانس دلایل تاریخی دارد!).

Variance is not automatic
برای حصول اطمینان از static type safety، پارامترها به صورت پیش فرض variant نمی‌باشند:
class Animal {}
class Bear : Animal {}
class Camel : Animal {}
public class Stack<T>
{
   int position;
   T[] data = new T[100];
   public void Push (T obj) => data[position++] = obj;
   public T Pop() => data[--position];
}
کد زیر کامپایل نخواهد شد:
Stack<Bear> bears = new Stack<Bear>();
Stack<Animal> animals = bears; // Compile-time error 
دلیل اینکه کد فوق کامپایل نمی‌شود، در کد زیر آورده شده است:
animals.Push (new Camel()); // Trying to add Camel to bears
اگر کامپایل انجام می‌شد، کد بالا در زمان اجرا خطا صادر می‌کرد؛ چرا که نوع واقعی animals، در واقع <Stack<Bear بوده و نمی‌توان به آن، شیء ای از جنس Camel اضافه کرد. عدم پشتیبانی از کوواریانس، به هرحال مانع از امکان استفاده مجدد (re-usability) خواهد شد. برای مثال فرض کنید می‌خواهیم متدی بنویسیم که وظیفه آن صادر کردن دستور شستن حیوانات موجود در پشته باشد:
public class ZooCleaner
{
  public static void Wash (Stack<Animal> animals) {...}
}
فراخوانی متد Wash با پارامتری از جنس <Stack<Bear در زمان کامپایل خطا خواهد داد (اعمال این محدودیت منطقی است. برای مثال ممکن است مثلا در بدنه متد Wash با استفاده از متد Pop کلاس Stack یک Animal برداشته شده و به Camel کست گردد که با توجه به نوع اصلی آن (Bear) خطای run-time صادر خواهد شد. اما به هرحال محدودیت ایجاد شده، جلوی خطاهایی که ممکن است در run-time اتفاق بیافتد را می‌گیرد). 
یک راه حل برای این موضوع، تعریف متد Wash به صورت جنریک و با constraint است:
class ZooCleaner
{
  public static void Wash<T> (Stack<T> animals) where T : Animal { ... }
}
با کد فوق می‌توان متد Wash را به صورت زیر فراخوانی نمود:
Stack<Bear> bears = new Stack<Bear>();
ZooCleaner.Wash(bears);
کامپایلر، ورژن جنریک متد Wash را کامپایل میکند. در این حالت میتوان با چک کردن نوع واقعی T و کست کردن به آن نوع، عملیات را بدون خطا انجام داد.
نکته: اگر reusable بودن مد نظر نبود، باید برای هر sub-type از Animal یک متد جداگانه Wash مینوشتیم (یکی برای Bear، یکی برای Camel،...).

راه حل دیگر این است که کلاس <Stack<T یک اینترفیس با پارامتر covariant پیاده سازی نماید که در ادامه به این مورد بازخواهیم گشت.

Arrays 
آرایه‌ها از covariance پشتیبانی می‌کنند. برای مثال:
Bear[] bears = new Bear[3];
Animal[] animals = bears; // OK
این مورد باعث ایجاد قابلیت استفاده مجدد می‌شود؛ به قیمت اینکه ممکن است چنین خطاهایی ایجاد شوند:
animals[0] = new Camel(); // Runtime error

Declaring a covariant type parameter  
از C# 4.0 و بالاتر، پارامترهای اینترفیسها و delegateها می‌توانند با استفاده از کلمه کلیدی out از covariance پشتیبانی کنند؛ یا به زبان ساده‌تر covariant گردند. در این صورت برخلاف آرایه‌ها از type safety اطمینان کامل خواهیم داشت.
برای نشان دادن این مورد، در کلاس <Stack<T اینترفیس زیر را پیاده سازی می‌کنیم:
public interface IPoppable<out T> { T Pop(); }
کلمه کلیدی out نشان می‌دهد که T فقط در موقعیت خروجی مورد استفاده واقع می‌گردد (برای مثال نوع برگشتی یک متد). این مورد سبب می‌شود تا پارامتر covariant باشد و کد زیر کامپایل گردد:
var bears = new Stack<Bear>();
bears.Push (new Bear());
// Bears implements IPoppable<Bear>. We can convert to IPoppable<Animal>:
IPoppable<Animal> animals = bears; // Legal
Animal a = animals.Pop();
در اینجا کامپایلر اجازه تبدیل bears را به animals می‌دهد. چرا که موردی که کامپایلر از آن جلوگیری می‌کرد (Push کردن Camel به Stack با اعضایی از جنس Bear) در اینجا نمی‌تواند رخ دهد. چرا که در اینجا پارامتر T فقط می‌تواند به عنوان خروجی استفاده گردد و امکان Push کردن وجود ندارد.

نکته: پارامترهای متدی که مزین به کلمه کلیدی out شده‌اند، واجد شرایط covariant بودن نمی‌باشند (به دلیل وجود محدودیتی در CLR).

با استفاده از کد زیر قابلیت استفاده مجددی که در ابتدا بحث کردیم فراهم می‌شود:
public class ZooCleaner
{
 public static void Wash (IPoppable<Animal> animals) { ... } //cast covariantly to solve the reusability problem 
}

نکته: Covariance (و contravariance) فقط در موارد تبدیل ارجاعی کار می‌کنند (نه تبدیل boxing). بنابراین اگر متدی داشته باشیم که دارای پارامتری از جنس IPoppa
<ble<object باشد، امکان فراخوانی آن متد با ورودی از جنس <IPoppable<string وجود دارد؛ اما پاس دادن متغیر از جنس <IPoppable<int امکانپذیر نمی‌باشد.

Contravariance   
در تعریف covaraince داشتیم:  A را در نظر بگیرید که قابل تبدیل به B باشد. در اینصورت X، دارای پارامتر کواریانس است اگر <X<A قابل تبدیل به <X<B باشد.  Contravariance 
زمانی است که تبدیل در جهت عکس صورت گیرد (تبدیل از <X<B به <X<A). این مورد فقط برای پارامترهای ورودی صحیح است و با کلمه کلیدی in تعیین می‌گردد. با استفاده از پیاده سازی اینترفیس:
public interface IPushable<in T> { void Push (T obj); }
می‌توانیم کد زیر را بنویسیم:
IPushable<Animal> animals = new Stack<Animal>();
IPushable<Bear> bears = animals; // Legal
bears.Push (new Bear());
هیچ عضوی از اینترفیس IPushable خروجی T را بر نمی‌گرداند و لذا با casting اشتباه، مواجه نخواهیم شد (برای نمونه از طریق این اینترفیس راهی برای Pop کردن نداریم).
توجه: کلاس <Stack<T هر دو اینترفیس <IPushable<T و <IPoppable<T را پیاده سازی کرده است (با وجود اینکه T هم out است و هم in). اما این مورد مشکلی ایجاد نمی‌کند. زیرا قبل از تبدیل، ارجاعی فقط به یکی از اینترفیسها صورت می‌گیرد (نه همزمان به هردو!). این مورد نشان می‌دهد که چرا class‌ها از پارامترهای variant پشتیبانی نمی‌کنند. 

برای مثال اینترفیس زیر را در نظر بگیرید:
public interface IComparer<in T>
{
// Returns a value indicating the relative ordering of a and b
  int Compare (T a, T b);
}
از آنجاییکه T در اینجا contravariant است می‌توان از <IComparer<object برای مقایسه دو string استفاده نمود:
var objectComparer = Comparer<object>.Default;
// objectComparer implements IComparer<object>
IComparer<string> stringComparer = objectComparer;
int result = stringComparer.Compare ("Hashem", "hashem");


برای مطالعه‌ی بیشتر
Covariant and Contravariant  
مطالب
مرتب سازی رکوردها به صورت اتفاقی در Entity framework
یکی از انواع روش‌هایی که در SQL Server و مشتقات آن برای نمایش رکوردها به صورت اتفاقی مورد استفاده قرار می‌گیرد، استفاده از کوئری زیر است:
SELECT * FROM table
ORDER BY NEWID()
سؤال: ترجمه و معادل کوئری فوق در Entity framework به چه صورتی است؟
پاسخ:
یک مثال کامل را در این زمینه در ادامه ملاحظه می‌کنید:
using System;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Linq;

namespace Sample
{
    public class User
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Age { get; set; }
    }

    public class MyContext : DbContext
    {
        public DbSet<User> Users { get; set; }
    }

    public class Configuration : DbMigrationsConfiguration<MyContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
            AutomaticMigrationDataLossAllowed = true;
        }

        protected override void Seed(MyContext context)
        {
            context.Users.Add(new User { Name = "User 1", Age = 20 });
            context.Users.Add(new User { Name = "User 2", Age = 25 });
            context.Users.Add(new User { Name = "User 3", Age = 30 });
            context.Users.Add(new User { Name = "User 4", Age = 35 });
            context.Users.Add(new User { Name = "User 5", Age = 40 });
            base.Seed(context);
        }
    }

    public static class Test
    {
        public static void RunTests()
        {
            Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>());

            using (var context = new MyContext())
            {
               var randomListOfUsers =
                        context.Users
                               .Where(person => person.Age >= 25 && person.Age < 40)
                               .OrderBy(person => Guid.NewGuid())
                               .ToList();

               foreach (var person in randomListOfUsers)
                   Console.WriteLine("{0}:{1}", person.Name, person.Age);
            }
        }
    }
}
تنها نکته مهم آن سطر ذیل است که برای مرتب سازی اتفاقی استفاده شده است:
.OrderBy(person => Guid.NewGuid())
که معادل
ORDER BY NEWID()
در SQL Server است.

خروجی SQL تولیدی کوئری LINQ فوق را نیز در ادامه مشاهده می‌کنید:
SELECT 
[Project1].[Id] AS [Id], 
[Project1].[Name] AS [Name], 
[Project1].[Age] AS [Age]
FROM ( SELECT 
NEWID() AS [C1], ------ Guid created here
[Extent1].[Id] AS [Id], 
[Extent1].[Name] AS [Name], 
[Extent1].[Age] AS [Age]
FROM [dbo].[Users] AS [Extent1]
WHERE ([Extent1].[Age] >= 25) AND ([Extent1].[Age] < 40)
)  AS [Project1]
ORDER BY [Project1].[C1] ASC  ------ Used for sorting here
مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت هفتم- امن سازی Web API
تا اینجا بجای قرار دادن مستقیم قسمت مدیریت هویت کاربران، داخل یک یا چند برنامه‌ی مختلف، این دغدغه‌ی مشترک (common concern) بین برنامه‌ها را به یک برنامه‌ی کاملا مجزای دیگری به نام Identity provider و یا به اختصار IDP منتقل و همچنین دسترسی به کلاینت MVC برنامه‌ی گالری تصاویر را نیز توسط آن امن سازی کردیم. اما هنوز یک قسمت باقی مانده‌است: برنامه‌ی کلاینت MVC، منابع خودش را از یک برنامه‌ی Web API دیگر دریافت می‌کند و هرچند دسترسی به برنامه‌ی MVC امن شده‌است، اما دسترسی به منابع برنامه‌ی Web API آن کاملا آزاد و بدون محدودیت است. بنابراین امن سازی Web API را توسط IDP، در این قسمت پیگیری می‌کنیم. پیش از مطالعه‌ی این قسمت نیاز است مطلب «آشنایی با JSON Web Token» را مطالعه کرده و با ساختار ابتدایی یک JWT آشنا باشید.


بررسی Hybrid Flow جهت امن سازی Web API

این Flow را پیشتر نیز مرور کرده بودیم. تفاوت آن با قسمت‌های قبل، در استفاده از توکن دومی است به نام access token که به همراه identity token از طرف IDP صادر می‌شود و تا این قسمت از آن بجز در قسمت «دریافت اطلاعات بیشتری از کاربران از طریق UserInfo Endpoint» استفاده نکرده بودیم.


در اینجا، ابتدا برنامه‌ی وب، یک درخواست اعتبارسنجی را به سمت IDP ارسال می‌کند که response type آن از نوع code id_token است (یا همان مشخصه‌ی Hybrid Flow) و همچنین تعدادی scope نیز جهت دریافت claims متناظر با آن‌ها در این درخواست ذکر شده‌اند. در سمت IDP، کاربر با ارائه‌ی مشخصات خود، اعتبارسنجی شده و پس از آن IDP صفحه‌ی اجازه‌ی دسترسی به اطلاعات کاربر (صفحه‌ی consent) را ارائه می‌دهد. پس از آن IDP اطلاعات code و id_token را به سمت برنامه‌ی وب ارسال می‌کند. در ادامه کلاینت وب، توکن هویت رسیده را اعتبارسنجی می‌کند. پس از موفقیت آمیز بودن این عملیات، اکنون کلاینت درخواست دریافت یک access token را از IDP ارائه می‌دهد. اینکار در پشت صحنه و بدون دخالت کاربر صورت می‌گیرد که به آن استفاده‌ی از back channel هم گفته می‌شود. یک چنین درخواستی به token endpoint، شامل اطلاعات code و مشخصات دقیق کلاینت جاری است. به عبارتی نوعی اعتبارسنجی هویت برنامه‌ی کلاینت نیز می‌باشد. در پاسخ، دو توکن جدید را دریافت می‌کنیم: identity token و access token. در اینجا access token توسط خاصیت at_hash موجود در id_token به آن لینک می‌شود. سپس هر دو توکن اعتبارسنجی می‌شوند. در این مرحله، میان‌افزار اعتبارسنجی، هویت کاربر را از identity token استخراج می‌کند. به این ترتیب امکان وارد شدن به برنامه‌ی کلاینت میسر می‌شود. در اینجا همچنین access token ای نیز صادر شده‌است.
اکنون علاقمند به کار با Web API برنامه‌ی کلاینت MVC خود هستیم. برای این منظور access token که اکنون در برنامه‌ی MVC Client در دسترس است، به صورت یک Bearer token به هدر ویژه‌ای با کلید Authorization اضافه می‌شود و به همراه هر درخواست، به سمت API ارسال خواهد شد. در سمت Web API این access token رسیده، اعتبارسنجی می‌شود و در صورت موفقیت آمیز بودن عملیات، دسترسی به منابع Web API صادر خواهد شد.


امن سازی دسترسی به Web API

تنظیمات برنامه‌ی IDP
برای امن سازی دسترسی به Web API از کلاس src\IDP\DNT.IDP\Config.cs در سطح IDP شروع می‌کنیم. در اینجا باید یک scope جدید مخصوص دسترسی به منابع Web API را تعریف کنیم:
namespace DNT.IDP
{
    public static class Config
    {
        // api-related resources (scopes)
        public static IEnumerable<ApiResource> GetApiResources()
        {
            return new List<ApiResource>
            {
                new ApiResource(
                    name: "imagegalleryapi",
                    displayName: "Image Gallery API",
                    claimTypes: new List<string> {"role" })
            };
        }
هدف آن داشتن access token ای است که در قسمت Audience آن، نام این ApiResource، درج شده باشد؛ پیش از اینکه دسترسی به API را پیدا کند. برای تعریف آن، متد جدید GetApiResources را به صورت فوق به کلاس Config اضافه می‌کنیم.
پس از آن در قسمت تعریف کلاینت، مجوز درخواست این scope جدید imagegalleryapi را نیز صادر می‌کنیم:
AllowedScopes =
{
  IdentityServerConstants.StandardScopes.OpenId,
  IdentityServerConstants.StandardScopes.Profile,
  IdentityServerConstants.StandardScopes.Address,
  "roles",
  "imagegalleryapi"
},
اکنون باید متد جدید GetApiResources را به کلاس src\IDP\DNT.IDP\Startup.cs معرفی کنیم که توسط متد AddInMemoryApiResources به صورت زیر قابل انجام است:
namespace DNT.IDP
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            services.AddIdentityServer()
             .AddDeveloperSigningCredential()
             .AddTestUsers(Config.GetUsers())
             .AddInMemoryIdentityResources(Config.GetIdentityResources())
             .AddInMemoryApiResources(Config.GetApiResources())
             .AddInMemoryClients(Config.GetClients());
        }

تنظیمات برنامه‌ی MVC Client
اکنون نوبت انجام تنظیمات برنامه‌ی MVC Client در فایل ImageGallery.MvcClient.WebApp\Startup.cs است. در اینجا در متد AddOpenIdConnect، درخواست scope جدید imagegalleryapi را صادر می‌کنیم:
options.Scope.Add("imagegalleryapi");

تنظیمات برنامه‌ی Web API
اکنون می‌خواهیم مطمئن شویم که Web API، به access token ای که قسمت Audience آن درست مقدار دهی شده‌است، دسترسی خواهد داشت.
برای این منظور به پوشه‌ی پروژه‌ی Web API در مسیر src\WebApi\ImageGallery.WebApi.WebApp وارد شده و دستور زیر را صادر کنید تا بسته‌ی نیوگت AccessTokenValidation نصب شود:
dotnet add package IdentityServer4.AccessTokenValidation
اکنون کلاس startup در سطح Web API را در فایل src\WebApi\ImageGallery.WebApi.WebApp\Startup.cs به صورت زیر تکمیل می‌کنیم:
using IdentityServer4.AccessTokenValidation;

namespace ImageGallery.WebApi.WebApp
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(defaultScheme: IdentityServerAuthenticationDefaults.AuthenticationScheme)
               .AddIdentityServerAuthentication(options =>
               {
                   options.Authority = Configuration["IDPBaseAddress"];
                   options.ApiName = "imagegalleryapi";
               });
متد AddAuthentication یک defaultScheme را تعریف می‌کند که در بسته‌ی IdentityServer4.AccessTokenValidation قرار دارد و این scheme در اصل دارای مقدار Bearer است.
سپس متد AddIdentityServerAuthentication فراخوانی شده‌است که به آدرس IDP اشاره می‌کند که مقدار آن‌را در فایل appsettings.json قرار داده‌ایم. از این آدرس برای بارگذاری متادیتای IDP استفاده می‌شود. کار دیگر این میان‌افزار، اعتبارسنجی access token رسیده‌ی به آن است. مقدار خاصیت ApiName آن، به نام API resource تعریف شده‌ی در سمت IDP اشاره می‌کند. هدف این است که بررسی شود آیا خاصیت aud موجود در access token رسیده به مقدار imagegalleryapi تنظیم شده‌است یا خیر؟

پس از تنظیم این میان‌افزار، اکنون نوبت به افزودن آن به ASP.NET Core request pipeline است:
namespace ImageGallery.WebApi.WebApp
{
    public class Startup
    {
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseAuthentication();
محل فراخوانی UseAuthentication باید پیش از فراخوانی app.UseMvc باشد تا پس از اعتبارسنجی درخواست، به میان‌افزار MVC منتقل شود.

اکنون می‌توانیم اجبار به Authorization را در تمام اکشن متدهای این Web API در فایل ImageGallery.WebApi.WebApp\Controllers\ImagesController.cs فعالسازی کنیم:
namespace ImageGallery.WebApi.WebApp.Controllers
{
    [Route("api/images")]
    [Authorize]
    public class ImagesController : Controller
    {


ارسال Access Token به همراه هر درخواست به سمت Web API

تا اینجا اگر مراحل اجرای برنامه‌ها را طی کنید، مشاهده خواهید کرد که برنامه‌ی MVC Client دیگر کار نمی‌کند و نمی‌تواند از فیلتر Authorize فوق رد شود. علت اینجا است که در حال حاضر، تمامی درخواست‌های رسیده‌ی به Web API، فاقد Access token هستند. بنابراین اعتبارسنجی آن‌ها با شکست مواجه می‌شود.
برای رفع این مشکل، سرویس ImageGalleryHttpClient را به نحو زیر اصلاح می‌کنیم تا در صورت وجود Access token، آن‌را به صورت خودکار به هدرهای ارسالی توسط HttpClient اضافه کند:
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

namespace ImageGallery.MvcClient.Services
{
    public interface IImageGalleryHttpClient
    {
        Task<HttpClient> GetHttpClientAsync();
    }

    /// <summary>
    /// A typed HttpClient.
    /// </summary>
    public class ImageGalleryHttpClient : IImageGalleryHttpClient
    {
        private readonly HttpClient _httpClient;
        private readonly IConfiguration _configuration;
        private readonly IHttpContextAccessor _httpContextAccessor;

        public ImageGalleryHttpClient(
            HttpClient httpClient,
            IConfiguration configuration,
            IHttpContextAccessor httpContextAccessor)
        {
            _httpClient = httpClient;
            _configuration = configuration;
            _httpContextAccessor = httpContextAccessor;
        }

        public async Task<HttpClient> GetHttpClientAsync()
        {
            var currentContext = _httpContextAccessor.HttpContext;
            var accessToken = await currentContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
            if (!string.IsNullOrWhiteSpace(accessToken))
            {
                _httpClient.SetBearerToken(accessToken);
            }

            _httpClient.BaseAddress = new Uri(_configuration["WebApiBaseAddress"]);
            _httpClient.DefaultRequestHeaders.Accept.Clear();
            _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            return _httpClient;
        }
    }
}
اسمبلی این سرویس برای اینکه به درستی کامپایل شود، نیاز به این وابستگی‌ها نیز دارد:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.1.1.0" />
    <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.1.1.0" />
    <PackageReference Include="Microsoft.AspNetCore.Authentication.Abstractions" Version="2.1.1.0" />
    <PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="5.2.0.0" />
    <PackageReference Include="IdentityModel" Version="3.9.0" />
  </ItemGroup>
</Project>
در اینجا با استفاده از سرویس IHttpContextAccessor، به HttpContext جاری درخواست دسترسی یافته و سپس توسط متد GetTokenAsync، توکن دسترسی آن‌را استخراج می‌کنیم. سپس این توکن را در صورت وجود، توسط متد SetBearerToken به عنوان هدر Authorization از نوع Bearer، به سمت Web API ارسال خواهیم کرد.
البته پس از این تغییرات نیاز است به کنترلر گالری مراجعه و از متد جدید GetHttpClientAsync بجای خاصیت HttpClient قبلی استفاده کرد.

اکنون اگر برنامه را اجرا کنیم، پس از لاگین، دسترسی به Web API امن شده، برقرار شده و برنامه بدون مشکل کار می‌کند.


بررسی محتوای Access Token

اگر بر روی سطر if (!string.IsNullOrWhiteSpace(accessToken)) در سرویس ImageGalleryHttpClient یک break-point را قرار دهیم و محتویات Access Token را در حافظه ذخیره کنیم، می‌توانیم با مراجعه‌ی به سایت jwt.io، محتویات آن‌را بررسی نمائیم:


که در حقیقت این محتوا را به همراه دارد:
{
  "nbf": 1536394771,
  "exp": 1536398371,
  "iss": "https://localhost:6001",
  "aud": [
    "https://localhost:6001/resources",
    "imagegalleryapi"
  ],
  "client_id": "imagegalleryclient",
  "sub": "d860efca-22d9-47fd-8249-791ba61b07c7",
  "auth_time": 1536394763,
  "idp": "local",  
  "role": "PayingUser",
  "scope": [
    "openid",
    "profile",
    "address",
    "roles",
    "imagegalleryapi"
  ],
  "amr": [
    "pwd"
  ]
}
در اینجا در لیست scope، مقدار imagegalleryapi وجود دارد. همچنین در قسمت audience و یا aud نیز ذکر شده‌است. بنابراین یک چنین توکنی قابلیت دسترسی به Web API تنظیم شده‌ی ما را دارد.
همچنین اگر دقت کنید، Id کاربر جاری در خاصیت sub آن قرار دارد.


مدیریت صفحه‌ی عدم دسترسی به Web API

با اضافه شدن scope جدید دسترسی به API در سمت IDP، این مورد در صفحه‌ی دریافت رضایت کاربر نیز ظاهر می‌شود:


در این حالت اگر کاربر این گزینه را انتخاب نکند، پس از هدایت به برنامه‌ی کلاینت، در سطر response.EnsureSuccessStatusCode استثنای زیر ظاهر خواهد شد:
An unhandled exception occurred while processing the request.
HttpRequestException: Response status code does not indicate success: 401 (Unauthorized).
 System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()
برای اینکه این صفحه‌ی نمایش استثناء را با صفحه‌ی عدم دسترسی جایگزین کنیم، می‌توان پس از دریافت response از سمت Web API، به StatusCode مساوی Unauthorized = 401 به صورت زیر عکس‌العمل نشان داد:
        public async Task<IActionResult> Index()
        {
            var httpClient = await _imageGalleryHttpClient.GetHttpClientAsync();
            var response = await httpClient.GetAsync("api/images");

            if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized ||
                response.StatusCode == System.Net.HttpStatusCode.Forbidden)
            {
                return RedirectToAction("AccessDenied", "Authorization");
            }
            response.EnsureSuccessStatusCode();


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

تا اینجا هرچند دسترسی به API امن شده‌است، اما هنوز کاربر وارد شده‌ی به سیستم می‌تواند تصاویر سایر کاربران را نیز مشاهده کند. بنابراین قدم بعدی امن سازی API، عکس العمل نشان دادن به هویت کاربر جاری سیستم است.
برای این منظور به کنترلر ImageGallery.WebApi.WebApp\Controllers\ImagesController.cs سمت API مراجعه کرده و Id کاربر جاری را از لیست Claims او استخراج می‌کنیم:
namespace ImageGallery.WebApi.WebApp.Controllers
{
    [Route("api/images")]
    [Authorize]
    public class ImagesController : Controller
    {
        [HttpGet()]
        public async Task<IActionResult> GetImages()
        {
            var ownerId = this.User.Claims.FirstOrDefault(claim => claim.Type == "sub").Value;
اگر به قسمت «بررسی محتوای Access Token» مطلب جاری دقت کنید، مقدار Id کاربر در خاصیت sub این Access token قرار گرفته‌است که روش دسترسی به آن‌را در ابتدای اکشن متد GetImages فوق ملاحظه می‌کنید.
مرحله‌ی بعد، مراجعه به ImageGallery.WebApi.Services\ImagesService.cs و تغییر متد GetImagesAsync است تا صرفا بر اساس ownerId دریافت شده کار کند:
namespace ImageGallery.WebApi.Services
{
    public class ImagesService : IImagesService
    {
        public Task<List<Image>> GetImagesAsync(string ownerId)
        {
            return _images.Where(image => image.OwnerId == ownerId).OrderBy(image => image.Title).ToListAsync();
        }
پس از این تغییرات، اکشن متد GetImages سمت API چنین پیاده سازی را پیدا می‌کند که در آن بر اساس Id شخص وارد شده‌ی به سیستم، صرفا لیست تصاویر مرتبط با او بازگشت داده خواهد شد و نه لیست تصاویر تمام کاربران سیستم:
namespace ImageGallery.WebApi.WebApp.Controllers
{
    [Route("api/images")]
    [Authorize]
    public class ImagesController : Controller
    {
        [HttpGet()]
        public async Task<IActionResult> GetImages()
        {
            var ownerId = this.User.Claims.FirstOrDefault(claim => claim.Type == "sub").Value;
            var imagesFromRepo = await _imagesService.GetImagesAsync(ownerId);
            var imagesToReturn = _mapper.Map<IEnumerable<ImageModel>>(imagesFromRepo);
            return Ok(imagesToReturn);
        }
اکنون اگر از برنامه‌ی کلاینت خارج شده و مجددا به آن وارد شویم، تنها لیست تصاویر مرتبط با کاربر وارد شده، نمایش داده می‌شوند.

هنوز یک مشکل دیگر باقی است: سایر اکشن متدهای این کنترلر Web API همچنان محدود به کاربر جاری نشده‌اند. یک روش آن تغییر دستی تمام کدهای آن است. در این حالت متد IsImageOwnerAsync زیر، جهت بررسی اینکه آیا رکورد درخواستی متعلق به کاربر جاری است یا خیر، به سرویس تصاویر اضافه می‌شود:
namespace ImageGallery.WebApi.Services
{
    public class ImagesService : IImagesService
    {
        public Task<bool> IsImageOwnerAsync(Guid id, string ownerId)
        {
            return _images.AnyAsync(i => i.Id == id && i.OwnerId == ownerId);
        }
و سپس در تمام اکشن متدهای دیگر، در ابتدای آن‌ها باید این بررسی را انجام دهیم و در صورت شکست آن return Unauthorized را بازگشت دهیم.
اما روش بهتر انجام این عملیات را که در قسمت بعدی بررسی می‌کنیم، بر اساس بستن دسترسی ورود به اکشن متدها بر اساس Authorization policy است. در این حالت اگر کاربری مجوز انجام عملیاتی را نداشت، اصلا وارد کدهای یک اکشن متد نخواهد شد.


ارسال سایر User Claims مانند نقش‌ها به همراه یک Access Token

برای تکمیل قسمت ارسال تصاویر می‌خواهیم تنها کاربران نقش خاصی قادر به انجام اینکار باشند. اما اگر به محتوای access token ارسالی به سمت Web API دقت کرده باشید، حاوی Identity claims نیست. البته می‌توان مستقیما در برنامه‌ی Web API با UserInfo Endpoint، برای دریافت اطلاعات بیشتر، کار کرد که نمونه‌ای از آن‌را در قسمت قبل مشاهده کردید، اما مشکل آن زیاد شدن تعداد رفت و برگشت‌های به سمت IDP است. همچنین باید درنظر داشت که فراخوانی مستقیم UserInfo Endpoint جهت برنامه‌ی MVC client که درخواست دریافت access token را از IDP می‌دهد، متداول است و نه برنامه‌ی Web API.
برای رفع این مشکل باید در حین تعریف ApiResource، لیست claim مورد نیاز را هم ذکر کرد:
namespace DNT.IDP
{
    public static class Config
    {
        // api-related resources (scopes)
        public static IEnumerable<ApiResource> GetApiResources()
        {
            return new List<ApiResource>
            {
                new ApiResource(
                    name: "imagegalleryapi",
                    displayName: "Image Gallery API",
                    claimTypes: new List<string> {"role" })
            };
        }
در اینجا ذکر claimTypes است که سبب خواهد شد نقش کاربر جاری به توکن دسترسی اضافه شود.

سپس کار با اکشن متد CreateImage در سمت API را به نقش PayingUser محدود می‌کنیم:
namespace ImageGallery.WebApi.WebApp.Controllers
{
    [Route("api/images")]
    [Authorize]
    public class ImagesController : Controller
    {
        [HttpPost]
        [Authorize(Roles = "PayingUser")]
        public async Task<IActionResult> CreateImage([FromBody] ImageForCreationModel imageForCreation)
        {
همچنین در این اکشن متد، پیش از فراخوانی متد AddImageAsync نیاز است مشخص کنیم OwnerId این تصویر کیست تا رکورد بانک اطلاعاتی تصویر آپلود شده، دقیقا به اکانت متناظری در سمت IDP مرتبط شود:
var ownerId = User.Claims.FirstOrDefault(c => c.Type == "sub").Value;
imageEntity.OwnerId = ownerId;
// add and save.
await _imagesService.AddImageAsync(imageEntity);

نکته‌ی مهم: در اینجا نباید این OwnerId را از سمت برنامه‌ی کلاینت MVC به سمت برنامه‌ی Web API ارسال کرد. برنامه‌ی Web API باید این اطلاعات را از access token اعتبارسنجی شده‌ی رسیده استخراج و استفاده کند؛ از این جهت که دستکاری اطلاعات اعتبارسنجی نشده‌ی ارسالی به سمت Web API ساده‌است؛ اما access tokenها دارای امضای دیجیتال هستند.

در سمت کلاینت نیز در فایل ImageGallery.MvcClient.WebApp\Views\Shared\_Layout.cshtml نمایش لینک افزودن تصویر را نیز محدود به PayingUser می‌کنیم:
@if(User.IsInRole("PayingUser"))
{
  <li><a asp-area="" asp-controller="Gallery" asp-action="AddImage">Add an image</a></li>
  <li><a asp-area="" asp-controller="Gallery" asp-action="OrderFrame">Order a framed picture</a></li>
}
علاوه بر آن، در کنترلر ImageGallery.MvcClient.WebApp\Controllers\GalleryController.cs نیاز است فیلتر Authorize زیر نیز به اکشن متد نمایش صفحه‌ی AddImage اضافه شود تا فراخوانی مستقیم آدرس آن در مرورگر، توسط سایر کاربران میسر نباشد:
namespace ImageGallery.MvcClient.WebApp.Controllers
{
    [Authorize]
    public class GalleryController : Controller
    {
        [Authorize(Roles = "PayingUser")]
        public IActionResult AddImage()
        {
            return View();
        }
این مورد را باید به متد AddImage در حالت دریافت اطلاعات از کاربر نیز افزود تا اگر شخصی مستقیما با این قسمت کار کرد، حتما سطح دسترسی او بررسی شود:
[HttpPost]
[Authorize(Roles = "PayingUser")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> AddImage(AddImageViewModel addImageViewModel)

برای آزمایش این قسمت یکبار از برنامه خارج شده و سپس با اکانت User 1 که PayingUser است به سیستم وارد شوید. در ادامه از منوی بالای سایت، گزینه‌ی Add an image را انتخاب کرده و تصویری را آپلود کنید. پس از آن، این تصویر آپلود شده را در لیست تصاویر صفحه‌ی اول سایت، مشاهده خواهید کرد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشه‌ی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشه‌ی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آن‌را اجرا کنید تا برنامه‌ی IDP راه اندازی شود.
- در آخر به پوشه‌ی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحه‌ی login نام کاربری را User 1 و کلمه‌ی عبور آن‌را password وارد کنید.
مطالب
استخراج تمام XPathهای یک محتوای HTMLایی به کمک کتابخانه HtmlAgilityPack
اولین قدم کار کردن با کتابخانه قدرتمند HtmlAgilityPack، داشتن XPath معتبر و متناظر با یک گره خاص می‌باشد. هرچند به ظاهر تعدادی از مرورگرها با کمک افزونه‌های خود امکان استخراج این XPathها را فراهم کرده‌اند اما ... عموما این مقادیر ارائه شده، نادرست هستند و بر روی محتوای HTML اصلی یک سایت قابل اجرا نیستند؛ علت هم به نرمال سازی‌های انجام شده بر روی محتوای یک سایت، توسط موتور مرورگر بر می‌گردد.
خود کتابخانه HtmlAgilityPack به ازای هر HtmlNode ایی که ارائه می‌دهد، خاصیت XPath معتبری را نیز به همراه دارد. در ادامه قصد داریم از این امکان توکار استفاده کرده و کلیه XPath‌های یک محتوای HTML ایی را استخراج کنیم.


پردازش تگ‌های تو در توی یک HTML به کمک کتابخانه HtmlAgilityPack

using System;
using System.Linq;
using System.Net;
using System.Text;
using HtmlAgilityPack;

namespace HapTests
{
    public class HtmlReader
    {
        public Action<string> ParseError { set; get; }

        public Func<HtmlNode, bool> ParserHtmlNode { set; get; }

        public void StartParsingHtml(Uri url)
        {
            using (var client = new WebClient { Encoding = Encoding.UTF8 })
            {
                client.Headers.Add("user-agent", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)");
                StartParsingHtml(client.DownloadString(url));
            }
        }

        public void StartParsingHtml(string htmlContent)
        {
            if (string.IsNullOrWhiteSpace(htmlContent))
                throw new ArgumentNullException("content");

            var doc = new HtmlDocument
            {
                OptionCheckSyntax = true,
                OptionFixNestedTags = true,
                OptionAutoCloseOnEnd = true,
                OptionDefaultStreamEncoding = Encoding.UTF8
            };
            doc.LoadHtml(htmlContent);

            if (doc.ParseErrors != null && doc.ParseErrors.Any())
            {
                foreach (var error in doc.ParseErrors)
                {
                    if (ParseError != null)
                        ParseError(error.Code + " - " + error.Reason);
                }
            }

            if (!doc.DocumentNode.HasChildNodes)
                return;

            handleChildren(doc.DocumentNode.ChildNodes);
        }

        private void handleChildren(HtmlNodeCollection nodes)
        {
            foreach (var itm in nodes)
            {
                if (itm.Name.ToLower().Equals("html"))
                {
                    if (itm.Element("body") != null)
                        handleChildren(itm.Element("body").ChildNodes);
                }
                else
                    handleHtmlNode(itm);
            }
        }

        private void parserChildNodes(HtmlNode content)
        {
            foreach (var item in content.ChildNodes)
            {
                handleHtmlNode(item);
            }
        }

        private void handleHtmlNode(HtmlNode htmNode)
        {
            switch (htmNode.Name.ToLower())
            {
                case "html":
                case "body":
                    handleChildren(htmNode.ChildNodes);
                    break;

                default:
                    if (ParserHtmlNode == null)
                        throw new ArgumentNullException("ParserHtmlNode");

                    if (ParserHtmlNode(htmNode))
                        parserChildNodes(htmNode);

                    break;
            }
        }
    }
}
در اینجا کدهایی را ملاحظه می‌کنید که علاوه بر ارائه تنظیمات اولیه HtmlAgilityPack (خصوصا با درنظر گرفتن مباحث ورودی یونیکد)، به صورت بازگشتی (با توجه به اینکه الزاما مسیر یا Node خاصی مدنظر نیست)، کلیه گره‌های یک HTML را بررسی و ارائه می‌دهند.
این کد برای نوشتن مبدل‌های HTML به XYZ بسیار مناسب است. برای مثال اگر بخواهید یک مبدل HTML به PDF را تهیه کنید، کدهای ابتدایی آن همین موارد است:
new HtmlReader
{
    ParseError = error => Console.WriteLine(error),
    ParserHtmlNode = htmlNode =>
    {
        //switch(htmlNode.Name) { }
        return true; //it's a nested node.
    }
}.StartParsingHtml(html);
نمونه‌ای از نحوه استفاده از کدهای کلاس HtmlReader را ملاحظه می‌کنید.
در اینجا html، محتوای HTMLایی در حال بررسی است. ParserHtmlNode یک callback است. هر زمانیکه به یک گره HTML برخورد، آن‌را در اختیار شما قرار می‌دهد. در ادامه فرصت خواهید داشت تا برای نمونه یک swicth را تهیه کرده و مثلا به ازای تگ hr یک خط رسم کنید، به ازای تگ br یک سطر جدید را درنظر بگیرید و الی آخر. اگر خروجی این Func را true درنظر بگیرید، فرض بر این خواهد بود که گره جاری تو در تو است (حالت دنیای واقعی)؛ در غیراینصورت، یک سطح این گره، بیشتر بررسی نخواهد شد.
در این کلاس، ParseError نیز یک callback است و اگر کتابخانه HtmlAgilityPack، در حین آنالیز کدهای HTML دریافتی به خطایی برخورد، آن‌را گزارش خواهد داد.
در کلاس فوق، دو حالت برای متد StartParsingHtml در نظر گرفته شده است. در حالت اول، یک Uri یا آدرس اینترنتی دریافت و سپس آنالیز می‌گردد. در حالت دوم، فرض بر این است که محتوای کدهای HTML مدنظر به هر نحوی پیشتر تهیه شده و به صورت string موجود است.


استخراج کلیه XPathها از یک فایل HTML به کمک کتابخانه HtmlAgilityPack

اکنون که یک HTML Parser عمومی را تهیه کرده‌ایم، استخراج XPathها توسط آن کار ساده‌ای خواهد بود. یک مثال کامل را در این زمینه در ادامه ملاحظه می‌کنید:
using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using HtmlAgilityPack;

namespace HapTests
{
    class Program
    {
        static void Main(string[] args)
        {
            var html = 
                @"<table width='750' border='0' style='font-size: 10pt; width: 736px' class='boxcar2 gerd'>
            <tbody><tr>
            <td height='70' colspan='4' class='boxcart1 gerd'>
            <iframe width='718' scrolling='no'>
            </iframe></td>
            </tr>
            <tr>
            <td height='70' colspan='4' class='boxcart1 gerd'>
    </td>
            </tr>
            <tr>
            <td width='193' height='36' class='boxcart2 gerd'>
            <a target='_self' href='Curr.cbi.2.php'>نرخ ارز مبادله ای بانک مرکزی</a></td>
            <td width='181' height='36' class='boxcart2 gerd'>
            <a target='_self' href='Curr.cbi.php'>نرخ ارز مرجع بانک مرکزی</a></td>
            <td width='149' height='36' class='boxcart2 gerd'>
            <a target='_self' href='curv.htm'>نمودار قیمت طلا</a></td>
            <td width='199' height='36' class='boxcart2 gerd'>
            <a target='_self' href='index.php'>قیمت طلا و سکه در بازار ایران</a></td>
            </tr>
            <tr>
            <td height='48' colspan='4' class='boxcart1 gerd'>
            <p dir='rtl'><span style='font-size: 13pt;'>تابلو آنلاین قیمت جهانی طلا و نقره ( دلار 
            )</span></p></td>
            </tr>
            <tr>
            <td height='57' colspan='2' class='boxcart1 gerd'>قیمت لحظه ای هر انس 
            نقره در بازارهای جهانی<br>
            <span style='font-size: 9pt;'>
            </span></td>
            <td height='57' colspan='2' class='boxcart1 gerd'>قیمت لحظه ای هر انس 
            طلا در بازارهای جهانی<br>
            <span style='font-size: 9pt;'>
            </span></td>
            </tr>
            <tr>
            <td height='48' colspan='4' class='boxcart1 gerd'>
            <p dir='rtl'><span style='font-size: 13pt'>تابلو آنلاین قیمت طلا ، سکه 
            و نقره در بازار ایران ( ریال )</span></p>
            </td>
            </tr>
            <tr>
            <td style='direction: rtl; font-size: 8pt' colspan='4'><div align='center'>
                            <table id='gold_tbl'><tbody><tr><th>قیمت طلا</th><th>قیمت زنده</th><th>تغییر</th>
                            <th>کمترین</th><th>بیشترین</th><th>زمان</th></tr><tr><td>انس طلا <sup>دلار</sup></td>
                            <td class='s0_1'>1,375.90</td><td class='c0_1 neg'>(-0.34%) -4.70</td>
                            <td class='l0_1'>1,374.90</td><td class='h0_1'>1,380.80</td><td class='z0_1 fa'>17:53</td>
                            </tr><tr><td>مثقال طلا</td><td class='s3_2'>5,290,000</td>
                            <td class='c3_2 pos'>(1.63%) 85,000</td><td class='l3_2'>5,200,000</td><td class='h3_2'>5,320,000</td><td class='z3_2 fa'>17:50</td></tr><tr><td>گرم طلای 18</td>
                            <td class='s3_3'>1,221,200</td><td class='c3_3 pos'>(1.63%) 19,600</td><td class='l3_3'>1,200,400</td><td class='h3_3'>1,228,100</td><td class='z3_3 fa'>17:50</td>
                            </tr><tr><td>انس نقره <sup>دلار</sup></td><td class='s0_5'>21.83</td><td class='c0_5'>(0.00%) 0.00</td><td class='l0_5'>21.67</td><td class='h0_5'>21.96</td>
                            <td class='z0_5 fa'>17:53</td></tr></tbody></table><br><table id='coin_tbl'><tbody><tr><th>سکه</th><th>قیمت زنده</th><th>تغییر</th><th>کمترین</th>
                            <th>بیشترین</th><th>ارزش طلا</th><th>زمان</th></tr><tr><td>بهار آزادی</td><td class='s3_10'>12,650,000</td><td class='c3_10 pos'>(2.68%) 330,000</td>
                            <td class='l3_10'>12,320,000</td><td class='h3_10'>12,650,000</td><td class='z4_10'>11,918,400</td><td class='z3_10 fa'>16:07</td></tr><tr><td>امامی</td>
                            <td class='s3_11'>12,960,000</td><td class='c3_11 pos'>(2.61%) 330,000</td><td class='l3_11'>12,630,000</td><td class='h3_11'>13,050,000</td><td class='z4_11'>11,918,400</td>
                            <td class='z3_11 fa'>17:43</td></tr><tr><td>نیم</td><td class='s3_12'>6,880,000</td><td class='c3_12 pos'>(2.69%) 180,000</td><td class='l3_12'>6,700,000</td>
                            <td class='h3_12'>6,900,000</td><td class='z4_12'>5,959,200</td><td class='z3_12 fa'>16:08</td></tr><tr><td>ربع</td><td class='s3_13'>4,250,000</td><td class='c3_13 pos'>(2.41%) 100,000</td>
                            <td class='l3_13'>4,150,000</td><td class='h3_13'>4,300,000</td><td class='z4_13'>2,978,100</td><td class='z3_13 fa'>17:42</td></tr><tr><td>گرمی</td><td class='s3_14'>2,940,000</td>   
                            <td class='c3_14 pos'>(3.16%) 90,000</td><td class='l3_14'>2,850,000</td><td class='h3_14'>2,940,000</td><td class='z4_14'>1,465,400</td><td class='z3_14 fa'>17:40</td></tr></tbody></table></div></td>
            </tr>
            </tbody></table>
                ";

            extractXPath(html);
            test(html);
        }

        /// <summary>
        /// Converts /#comment[1] to /comment()[1] 
        /// or /#text[1] to /text()[1]
        /// </summary>
        private static string GetValidXPath(string xpath)
        {
            var index = xpath.LastIndexOf("/");
            var lastPath = xpath.Substring(index);

            if (lastPath.Contains("#"))
            {
                xpath = xpath.Substring(0, index);
                lastPath = lastPath.Replace("#", "");
                lastPath = lastPath.Replace("[", "()[");
                xpath = xpath + lastPath;
            }

            return xpath;
        }

        private static void extractXPath(string html)
        {
            var sb = new StringBuilder();
            new HtmlReader
            {
                ParseError = error => Console.WriteLine(error),
                ParserHtmlNode = htmlNode =>
                {
                    if (htmlNode is HtmlTextNode)
                    {
                        sb.AppendLine("Text NodeName: " + htmlNode.Name.Trim());
                        sb.AppendLine("InnerText: " + htmlNode.InnerText.Trim());
                    }
                    else
                    {
                        sb.AppendLine("NodeName: " + htmlNode.Name.Trim());
                        var nodeText = new StringBuilder();
                        for (int i = 0; (i < htmlNode.OuterHtml.Length && htmlNode.OuterHtml[i] != '>'); i++)
                            nodeText.Append(htmlNode.OuterHtml[i]);

                        nodeText.Append(">");

                        sb.AppendLine("Node Start: " + nodeText.ToString());
                    }

                    sb.AppendLine("XPath: " + GetValidXPath(htmlNode.XPath.Trim()));
                    sb.AppendLine(Environment.NewLine);

                    return true; //it's a nested node.
                }
            }.StartParsingHtml(html);

            File.WriteAllText("xpath.txt", sb.ToString());
            Process.Start("xpath.txt");
        }

        private static void test(string html)
        {
            var doc = new HtmlDocument
            {
                OptionCheckSyntax = true,
                OptionFixNestedTags = true,
                OptionAutoCloseOnEnd = true,
                OptionDefaultStreamEncoding = Encoding.UTF8
            };
            doc.LoadHtml(html);
            var node = doc.DocumentNode.SelectSingleNode("/table[1]/tbody[1]/tr[7]/td[1]/div[1]/table[2]/tbody[1]/tr[6]/td[7]/text()[1]");
            Console.WriteLine(node.InnerText);
        }
    }
}
در این مثال html مقداری است که از یک سایت عمومی دریافت شده است.
سپس نمونه‌ای دیگر از نحوه استفاده از کلاس HtmlReader قسمت قبل را در ادامه، در متد extractXPath ملاحظه می‌کنید. در اینجا کلاس HtmlReader در یک عملیات بازگشتی، کلیه گره‌های تو در توی HTML مورد نظر را آنالیز کرده و توسط callback ایی به نام ParserHtmlNode در اختیار ما قرار می‌دهد. اکنون که این htmlNode را داریم، خاصیت XPath آن دقیقا مقداری است که به دنبالش هستیم.
در اینجا چند نکته حائز اهمیت هستند:
- با بررسی HtmlTextNode، به نودهایی خواهیم رسید که دارای مقدار متنی هستند. در غیراینصورت این گره، خود ابتدای یک سری گره تو در توی دیگر است.
- XPath بازگشتی توسط کتابخانه HtmlAgilityPack نیاز به کمی تمیز سازی دارد. اینکار در متد GetValidXPath انجام شده است.
- در متد test انتهایی، نمونه‌ای از نحوه استفاده از XPathهای استخراجی را ملاحظه می‌کنید.
Text NodeName: #text
InnerText: 17:40
XPath: /table[1]/tbody[1]/tr[7]/td[1]/div[1]/table[2]/tbody[1]/tr[6]/td[7]/text()[1]
برای نمونه سه سطر فوق، یکی از مداخل فایل نهایی تولیدی مثال جاری است. اکنون که XPath را داریم، استفاده از آن جهت استخراج مقدار InnerText مدنظر، ساده خواهد بود.
مطالب
آشنایی با Should Library
 نوشتن Assert در کد‌های تست، وابستگی مستقیم به انتخاب کتابخانه تست دارد. برای مثال:
NUnit:
using NUnit.Framework;
using NUnit.Framework.SyntaxHelpers;
 
namespace TestLibrary
{
   [TestFixture]
   public class MyTest
   {
       [Test]
       public void Test1()
       {
           var expectedValue = 2;
           Assert.That(expectedValue , Is.EqualTo(2));
       }
   }
}

Microsoft UnitTesting :

using Microsoft.VisualStudio.TestTools.UnitTesting ;
 
namespace TestLibrary
{
   [TesClass]
   public class MyTest
   {
       [TestMethod]
       public void Test1()
       {
           var expectedValue = 2;
           Assert.AreEqual (expectedValue , 2);
       }
   }
}
کد‌های Assert نوشته شده در مثال بالا با توجه به فریم ورک مورد استفاده متفاوت است. در حالی که کتابخانه Should، مجموعه ای از Extension Method هاست برای قسمت Assert در UnitTest‌های نوشته شده. با استفاده از این کتابخانه دیگر نیازی به نوشتن Assert به سبک و سیاق فعلی نیست. کدهای Assert بسیار خواناتر و قابل درک خواهند بود و از طرفی وابستگی به سایر کتابخانه‌های تست از بین خواهد رفت.
نکته: مورد استفاده این کتابخانه فقط در قسمت Assert کد‌های تست است و استفاده از سایر کتابخانه‌های جانبی الزامی است.
این کتابخانه به دو صورت مورد استفاده قرار می‌گیرد:
»Standard  که باید از Should.dll استفاده نمایید؛
»Fluent که باید از Should.Fluent.dll استفاده نمایید؛(پیاده سازی همان فریم ورک Should به صورت Static Reflection)

نصب  کتابخانه Should با استفاده از nuget (آخرین نسخه آن در حال حاضر 1.1.20 است ) :
Install-Package Should
  نصب کتابخانه Should.Fluent با استفاده از nuget(آخرین نسخه آن در حال حاضر 1.1.19 است):
Install-Package ShouldFluent

در ابتدا همان مثال قبلی را با این کتابخانه بررسی خواهیم کرد:
using Microsoft.VisualStudio.TestTools.UnitTesting;
 
namespace TestLibrary
{
   [TesClass]
   public class MyTest
   {
       [TestMethod]
       public void Test1()
       {          
           var expectedValue = 2;
           expectedValue.Should().Equal( 2 );
       }
   }
}
در نگاه اول چیز خاصی به چشم نمی‌خورد، اما اگر از این پس قصد داشته باشیم کد‌های تست خود را تحت فریم ورک NUnit پیاده سازی کنیم در قسمت Assert کد‌های خود هیچ گونه خطایی را مشاهده نخواهیم کرد.

مثال:
[TestMethod]        
public void AccountConstructorTest()        
{
     const int expectedBalance = 1000;    
     Account bankAccount = new Account();       
         
     // Assert.IsNotNull(bankAccount, "Account was null.");         
    // Assert.AreEqual(expectedBalance, bankAccount.AccountBalance, "Account balance not mathcing");         
bankAccount.ShouldNotBeNull("Account was null"); bankAccount.AccountBalance.ShouldEqual(expectedBalance, "Account balance not matching"); } 
در مثال بالا ابتدا با استفاده از Ms UnitTesting دو Assert نوشته شده است سپس در خطوط بعدی همان دو شرط با استفاده از کتابخانه Should نوشتم. در ذیل چند مثال از استفاده این کتابخانه (البته نوع Fluent آن) در هنگام کار با رشته ها، آبجکت ها، boolean و Collection‌ها را بررسی خواهیم کرد:

#Should.Fluent
public void Should_fluent_assertions()
{
    object obj = null;
    obj.Should().Be.Null();

    obj = new object();
    obj.Should().Be.OfType(typeof(object));
    obj.Should().Equal(obj);
    obj.Should().Not.Be.Null();
    obj.Should().Not.Be.SameAs(new object());
    obj.Should().Not.Be.OfType<string>();
    obj.Should().Not.Equal("foo");

    obj = "x";
    obj.Should().Not.Be.InRange("y", "z");
    obj.Should().Be.InRange("a", "z");
    obj.Should().Be.SameAs("x");

    "This String".Should().Contain("This");
    "This String".Should().Not.Be.Empty();
    "This String".Should().Not.Contain("foobar");

    false.Should().Be.False();
    true.Should().Be.True();

    var list = new List<object>();
    list.Should().Count.Zero();
    list.Should().Not.Contain.Item(new object());

    var item = new object();
    list.Add(item);
    list.Should().Not.Be.Empty();
    list.Should().Contain.Item(item);
};

#مثال‌های استفاده از متغیر‌های DateTime و Guid
public void Should_fluent_assertions()
{   
    var id = new Guid();
    id.Should().Be.Empty();

    id = Guid.NewGuid();
    id.Should().Not.Be.Empty();

    var date = DateTime.Now;
    date1.Should().Be.Today();

    var str = "";
    str.Should().Be.NullOrEmpty();                

    var one = "1";
    one.Should().Be.ConvertableTo<int>();

    var idString = Guid.NewGuid().ToString();
    idString.Should().Be.ConvertableTo<Guid>();
}


مطالب
کوئری نویسی در EF Core - قسمت دوم - کوئری‌های ساده
پس از تشکیل ساختار بانک اطلاعاتی و مقدار دهی اولیه‌ی آن در قسمت قبل، در ادامه به بررسی نحوه‌ی انجام تعدادی کوئری‌های ساده و ابتدایی با EF Core خواهیم پرداخت. در قسمت‌های بعدی حالت‌های پیچیده‌تری را بررسی می‌کنیم.


مثال 1: تمام اطلاعات یک جدول را دریافت کنید.

هدف دریافت تمام اطلاعات جدول facilities است.


برای انجام اینکار فقط کافی‌است بر روی DbSet متناظر با آن، متد ToList فراخوانی شود:
var facilities = context.Facilities.ToList();
حاصل آن، کوئری زیر خواهد بود که در آن، تمام ستون‌های جدول Facilities به صورت خودکار قید می‌شوند:


یک نکته: به فراخوانی متد ToList، اصطلاحا materialization گفته می‌شود و هدف آن تبدیل یک IQueryable، به یک IEnumerable است. اطلاعات بیشتر


مثال 2: اطلاعات ستون‌های خاصی از یک جدول را دریافت کنید.

می‌خواهیم لیست نام امکانات مجموعه را به همراه هزینه‌ی مرتبط با آن‌ها، نمایش دهیم:
var facilities = context.Facilities.Select(x =>
                    new
                    {
                        x.Name,
                        x.MemberCost
                    }).ToList();
برای انتخاب ستون‌هایی خاص از یک جدول، نیاز است از متد Select استفاده کرد و سپس نام دقیق آن‌ها را ذکر نمود. در غیراینصورت همانند مثال1، تمام ستون‌ها بازگشت داده می‌شوند. در اینجا خروجی حاصل، یک anonymous list است که می‌توان آن‌را با یک کلاس و یا حتی یک tuple نیز جایگزین کرد.



مثال 3: نحوه‌ی بازگشت ردیف‌ها را کنترل کنید.

چگونه می‌توان لیست امکاناتی را بازگشت داد که برای کاربران رایگان نیستند؟
var facilities = context.Facilities.Where(x => x.MemberCost > 0).ToList();
برای فیلتر کردن ردیف‌هایی خاص می‌توان از متد Where استفاده کرد. در اینجا امکان نوشتن شرط مدنظر وجود دارد که به آن predicate هم گفته می‌شود و می‌تواند ترکیبی از چندین شرط نیز باشد. در این کوئری چون از متد Select استفاده نشده‌است، تمام ستون‌های جدول بازگشت داده می‌شوند:



مثال 4: نحوه‌ی بازگشت ردیف‌ها را کنترل کنید؛ قسمت دوم.

چگونه می‌توان لیست امکاناتی را بازگشت داد که برای کاربران رایگان نیستند و همچنین هزینه‌ی آن‌ها، 1/50 ام هزینه‌ی نگهداری ماهیانه‌ی آن‌ها است؟ خروجی این کوئری باید تنها به همراه ستون‌های FacId, Name, MemberCost, MonthlyMaintenance باشد.
var facilities = context.Facilities.Where(x => x.MemberCost > 0
                                                            && x.MemberCost < (x.MonthlyMaintenance / 50))
                                                    .Select(x =>
                                                        new
                                                        {
                                                            x.FacId,
                                                            x.Name,
                                                            x.MemberCost,
                                                            x.MonthlyMaintenance
                                                        }).ToList();


در این مثال نحوه‌ی ترکیب چند شرط را با هم در قسمت Where، مشاهده می‌کنید و همچنین با استفاده از متد Select، تعداد ستون‌های بازگشتی نیز کنترل شده‌اند.


مثال 5: جستجوهای ساده‌ی رشته‌ای

لیستی از امکاناتی را تهیه کنید که واژه‌ی «Tennis» در نام آن‌ها بکار رفته‌است.
var facilities = context.Facilities.Where(x => x.Name.Contains("Tennis")).ToList();
یک چنین جستجو‌هایی را می‌توان توسط متد Contains انجام داد که در EF-Core، خروجی زیر را تولید می‌کند:



مثال 6: ردیف‌هایی را که با چندین مقدار ممکن تطابق دارند، بازگشت دهید.

چگونه می‌توان امکانات دارای ID مساوی 1 و 5 را بازگشت داد؟ برای اینکار از ترکیب شرط‌ها با استفاده از OR استفاده نکنید.
int[] ids = { 1, 5 };
var facilities = context.Facilities.Where(x => ids.Contains(x.FacId)).ToList();
یک روش حل این مساله، رسیدن به یک کوئری دارای where facid = 1 or facid = 5 است. اگر تعداد این IDها بیشتر شد، روش Where In که بر روی یک لیست از آن‌ها کار می‌کند، مرسوم‌تر است که نحوه‌ی تهیه‌ی یک چنین کوئری‌هایی را با استفاده از تعریف یک آرایه و سپس فراخوانی متد Contains بر روی آن، در اینجا مشاهده می‌کنید.



مثال 7: نتایج بازگشت داده شده را طبقه بندی کنید.

گزارشی از امکانات را تهیه کنید که در آن اگر هزینه‌ی نگهداری ماهیانه‌ی امکاناتی بیشتر از 100 دلار بود، به صورت expensive و در غیراینصورت cheap، طبقه بندی شوند.
var facilities = context.Facilities
                        .Select(x =>
                                    new
                                    {
                                        x.Name,
                                        Cost = x.MonthlyMaintenance > 100 ? "expensive" : "cheap"
                                    }).ToList();
می‌توان بر روی هر کدام از ستون‌های ذکر شده‌ی در متد Select، شرط‌هایی را نیز اعمال کرد و توانایی آن تنها به ذکر نام ستون‌ها خلاصه نمی‌شود. برای مثال در اینجا اگر MonthlyMaintenance بیشتر از مقداری بود، برچسب خاصی بجای این مقدار اصلی، نمایش داده می‌شود و چون خروجی نهایی محاسباتی آن دیگر یک ستون اصلی جدول نیست، نیاز است نام دلخواهی را برای آن انتخاب کرد که در کوئری نهایی به صورت AS Cost ظاهر می‌شود؛ البته می‌توان اینکار را در مورد ستون Name نیز انجام داد و در صورت لزوم، نام ستون دلخواه دیگری را برای آن قید کرد.



مثال 8: کار با تاریخ و زمان

لیست کاربرانی را بازگشت دهید که پس از September 2012 عضو این مجموعه شده‌اند. این گزارش باید تنها به همراه ستون‌های MemId, Surname, FirstName, JoinDate باشد.
var date = new DateTime(2012, 09, 01);
var members = context.Members.Where(x => x.JoinDate >= date)
                                            .Select(x =>
                                                        new
                                                        {
                                                            x.MemId,
                                                            x.Surname,
                                                            x.FirstName,
                                                            x.JoinDate
                                                        }).ToList();
در EF Core امکان مقایسه‌ی معمولی خواصی از نوع DateTime با وهله‌ای/مقداری از این نوع وجود دارد که در نهایت یک چنین خروجی را تولید می‌کند:



مثال 9: نتایج تکراری را از اطلاعات بازگشتی حذف کرده و آن‌ها را مرتب کنید.

گزارشی را تهیه کنید که در آن تنها فیلد Surname مرتب شده‌ی کاربران وجود دارد. از لیست Surnameها، تنها 10 مورد غیر تکراری را بازگشت دهید.
var members = context.Members.OrderBy(x => x.Surname)
                                            .Select(x =>
                                                        new
                                                        {
                                                            x.Surname
                                                        })
                                            .Distinct()
                                            .Take(10)
                                            .ToList();
با استفاده از متد OrderBy، می‌توان نتایج حاصل از کوئری را بر اساس خاصیت مشخصی مرتب کرد. سپس تعداد ستون‌های بازگشتی، توسط متد Select مشخص شده‌اند و در آخر متد Distinct سبب بازگشت موارد غیرتکراری شده (به SELECT DISTINCT ترجمه می‌شود) و متد Take، تعداد ردیف‌های بازگشت داده شده را محدود می‌کند (به SELECT  TOP 10 ترجمه می‌شود).



مثال 10: نتایج چند کوئری را با هم ترکیب کنید.

لیست نام‌های امکانات و نام‌های اشخاص را با هم ترکیب کنید.
var names = context.Members.Select(m => m.Surname).ToList()
                            .Union(context.Facilities.Select(f => f.Name).ToList()) // For now we have to use `.ToList()` here
                            .ToList();
برای ترکیب نتایج کوئری حاصل از دو جدول یا بیشتر از union استفاده می‌شود (در قالب یک کوئری):
SELECT surname
FROM members
UNION
SELECT name
FROM facilities;
 اما ... EF-Core 3x فعلا از آن به صورت تولید تنها یک کوئری SQL پشتیبانی نمی‌کند. به همین جهت در اینجا ترکیبی از LINQ to Entities و LINQ to Objects را مشاهده می‌کنید. هر جائیکه متد ToList ذکر شده، یعنی تبدیل LINQ to Entities به نتیجه‌ی حاصل یا همان materialization و از اینجا به بعد با داشتن لیستی از اشیاء درون حافظه‌ای می‌توان از LINQ to Objects استفاده کرد که استفاده‌ی از تمام امکانات زبان #C در آن میسر است.
یعنی در مثال فوق، دوبار رفت و برگشت به بانک اطلاعاتی صورت گرفته (به ازای هر ToList ذکر شده) و سپس نتیجه‌ی حاصل، در سمت کلاینت با هم Union شده‌اند و نه در سمت دیتابیس.


مثال 11: محاسبات تجمعی ابتدایی

زمان ثبت نام آخرین عضو مجموعه چیست؟

برای حل این مثال می‌توان از روش‌های مختلفی استفاده کرد:

الف) استفاده از متد تجمعی Max برای یافتن بزرگترین مقدار JoinDate
var latest = context.Members.Max(x => x.JoinDate);


متد Max برای خواص nullable می‌تواند null را بازگشت دهد و همچنین اگر این مجموعه دارای مقداری نباشد و آن خاصیت نیز nullable نباشد، استثنای Sequence contains no element را صادر می‌کند. می‌توان این استثناء را به صورت زیر با استفاده از متد DefaultIfEmpty کنترل کرد:
var latest2 = context.Members.Select(m => m.JoinDate).DefaultIfEmpty().Max();
که به صورت خاص زیر ترجمه می‌شود:
SELECT MAX([m].[JoinDate])
FROM   (SELECT NULL AS [empty]) AS [empty]
       LEFT OUTER JOIN
       [Members] AS [m]
       ON 1 = 1;
یا حتی می‌توان JoinDate را که nullable نیست، به صورت nullable معرفی کرد و سبب شد تا در صورت عدم وجود ردیفی در جدول، نال بازگشت داده شود:
var latest3 = context.Members.Max(m => (DateTime?)m.JoinDate) ?? DateTime.Now;
این روش همان کوئری «SELECT MAX([m].[JoinDate]) FROM [Members] AS [m]» را تولید می‌کند و کنترل استثنای آن در سمت کلاینت صورت می‌گیرد.

ب) بجای استفاده از متد Max می‌توان ابتدا رکوردها را بر اساس JoinDate به صورت نزولی مرتب کرد و سپس اولین عضو حاصل را بازگشت داد؛ چون اکنون بر اساس مرتب سازی صورت گرفته، در بالای لیست قرار دارد:
var latest4 = context.Members.OrderByDescending(m => m.JoinDate).Select(m => m.JoinDate).FirstOrDefault();



مثال 12: مثالی دیگر از محاسبات تجمعی ابتدایی

در مثال قبلی، نام و نام خانوادگی آخرین شخص ثبت نام شده را نیز به گزارش اضافه کنید؛ یعنی Select انجام شده شامل x.FirstName, x.Surname, x.JoinDate باشد.

یک روش انجام اینکار، همان کوئری ب مثال قبلی است که اینبار فقط Select آن فرق می‌کند:
var lastMember = context.Members.OrderByDescending(m => m.JoinDate)
                            .Select(x => new { x.FirstName, x.Surname, x.JoinDate })
                            .FirstOrDefault();


روش دیگر آن نوشتن یک sub-query در قسمت Where است:
var members = context.Members.Select(x => new { x.FirstName, x.Surname, x.JoinDate })
                                    .Where(x => x.JoinDate == context.Members.Max(x => x.JoinDate))
                                    .ToList();
می‌توان ردیفی را بازگشت داد که JoinDate آن همان بزرگترین مقدار JoinDate جدول کاربران است. یک چنین کوئری خاصی که به همراه دوبار فراخوانی context است، با فراخوانی ToList انتهایی، تنها یک کوئری را تولید می‌کند:



کدهای کامل این قسمت را در اینجا می‌توانید مشاهده کنید.
مطالب
Kendo UI MVVM
پیشنیازها
- «استفاده از Kendo UI templates »
- «اعتبار سنجی ورودی‌های کاربر در Kendo UI»
- «فعال سازی عملیات CRUD در Kendo UI Grid» جهت آشنایی با نحوه‌ی تعریف DataSource ایی که می‌تواند اطلاعات را ثبت، حذف و یا ویرایش کند.


در این مطلب قصد داریم به یک چنین صفحه‌ای برسیم که در آن در ابتدای نمایش، لیست ثبت نام‌های موجود، از سرور دریافت و توسط یک Kendo UI template نمایش داده می‌شود. سپس امکان ویرایش و حذف هر ردیف، وجود خواهد داشت، به همراه امکان افزودن ردیف‌های جدید. در این بین مدیریت نمایش لیست ثبت نام‌ها توسط امکانات binding توکار فریم ورک MVVM مخصوص Kendo UI صورت خواهد گرفت. همچنین کلیه اعمال مرتبط با هر ردیف نیز توسط data binding دو طرفه مدیریت خواهد شد.



Kendo UI MVVM

الگوی MVVM یا Model-View-ViewModel که برای اولین بار جهت کاربردهای WPF و Silverlight معرفی شد، برای ساده سازی اتصال تغییرات کنترل‌های برنامه به خواص ViewModel یک View کاربرد دارد. برای مثال با تغییر عنصر انتخابی یک DropDownList در یک View، بلافاصله خاصیت متصل به آن که در ViewModel برنامه تعریف شده‌است، مقدار دهی و به روز خواهد شد. هدف نهایی آن نیز جدا سازی منطق کدهای UI، از کدهای جاوا اسکریپتی سمت کاربر است. برای این منظور کتابخانه‌هایی مانند Knockout.js به صورت اختصاصی برای این کار تهیه شده‌اند؛ اما Kendo UI نیز جهت یکپارچگی هرچه تمامتر اجزای آن، دارای یک فریم ورک MVVM توکار نیز می‌باشد. طراحی آن نیز بسیار شبیه به Knockout.js است؛ اما با سازگاری 100 درصد با کل مجموعه.
پیاده سازی الگوی MVVM از 4 قسمت تشکیل می‌شود:
- Model که بیانگر خواص متناظر با اشیاء رابط کاربری است.
- View همان رابط کاربری است که به کاربر نمایش داده می‌شود.
- ViewModel واسطی است بین Model و View. کار آن انتقال داده‌ها و رویدادها از View به مدل است و در حالت binding دوطرفه، عکس آن نیز صحیح می‌باشد.
- Declarative data binding جهت رهایی برنامه نویس‌ها از نوشتن کدهای هماهنگ سازی اطلاعات المان‌های View و خواص ViewModel کاربرد دارد.

در ادامه این اجزا را با پیاده سازی مثالی که در ابتدای بحث مطرح شد، دنبال می‌کنیم.


تعریف Model و ViewModel

در سمت سرور، مدل ثبت نام برنامه چنین شکلی را دارد:
namespace KendoUI07.Models
{
    public class Registration
    {
        public int Id { set; get; }
        public string UserName { set; get; }
        public string CourseName { set; get; }
        public int Credit { set; get; }
        public string Email { set; get; }
        public string Tel { set; get; }
    }
}
در سمت کاربر، این مدل را به نحو ذیل می‌توان تعریف کرد:
    <script type="text/javascript">
        $(function () {
            var model = kendo.data.Model.define({
                id: "Id",
                fields: {
                    Id: { type: 'number' }, // leave this set to 0 or undefined, so Kendo knows it is new.
                    UserName: { type: 'string' },
                    CourseName: { type: 'string' },
                    Credit: { type: 'number' },
                    Email: { type: 'string' },
                    Tel: { type: 'string' }
                }
            });
        });
    </script>
و ViewModel برنامه در ساده‌ترین شکل آن اکنون چنین تعریفی را خواهد یافت:
    <script type="text/javascript">
        $(function () {
            var viewModel = kendo.observable({
                accepted: false,
                course: new model()
            });
        });
    </script>
یک viewModel در Kendo UI به صورت یک observable object تعریف می‌شود که می‌تواند دارای تعدادی خاصیت و متد دلخواه باشد. هر خاصیت آن به یک عنصر HTML متصل خواهد شد. در اینجا این اتصال دو طرفه است؛ به این معنا که تغییرات UI به خواص viewModel و برعکس منتقل و منعکس می‌شوند.


اتصال ViewModel به View برنامه

تعریف فرم ثبت نام را در اینجا ملاحظه می‌کنید. فیلدهای مختلف آن بر اساس نکات اعتبارسنجی HTML 5 با ویژگی‌های خاص آن، مزین شده‌اند. جزئیات آن‌را در مطلب «اعتبار سنجی ورودی‌های کاربر در Kendo UI» پیشتر بررسی کرده‌ایم.
اگر به تعریف هر فیلد دقت کنید، ویژگی data-bind جدیدی را هم ملاحظه خواهید کرد:
    <div id="coursesSection" class="k-rtl k-header">
        <div class="box-col">
            <form id="myForm" data-role="validator" novalidate="novalidate">
                <h3>ثبت نام</h3>
                <ul>
                    <li>
                        <label for="Id">Id</label>
                        <span id="Id" data-bind="text:course.Id"></span>
                    </li>
                    <li>
                        <label for="UserName">نام</label>
                        <input type="text" id="UserName" name="UserName" class="k-textbox"
                               data-bind="value:course.UserName"
                               required />
                    </li>
                    <li>
                        <label for="CourseName">دوره</label>
                        <input type="text" dir="ltr" id="CourseName" name="CourseName" required
                               data-bind="value:course.CourseName" />
                        <span class="k-invalid-msg" data-for="CourseName"></span>
                    </li>
                    <li>
                        <label for="Credit">مبلغ پرداختی</label>
                        <input id="Credit" name="Credit" type="number" min="1000" max="6000"
                               required data-max-msg="عددی بین 1000 و 6000" dir="ltr"
                               data-bind="value:course.Credit"
                               class="k-textbox k-input" />
                        <span class="k-invalid-msg" data-for="Credit"></span>
                    </li>
                    <li>
                        <label for="Email">پست الکترونیک</label>
                        <input type="email" id="Email" dir="ltr" name="Email"
                               data-bind="value:course.Email"
                               required class="k-textbox" />
                    </li>
                    <li>
                        <label for="Tel">تلفن</label>
                        <input type="tel" id="Tel" name="Tel" dir="ltr" pattern="\d{8}"
                               required class="k-textbox"
                               data-bind="value:course.Tel"
                               data-pattern-msg="8 رقم" />
                    </li>
                    <li>
                        <input type="checkbox" name="Accept"
                               data-bind="checked:accepted"
                               required />
                        شرایط دوره را قبول دارم.
                        <span class="k-invalid-msg" data-for="Accept"></span>
                    </li>
                    <li>
                        <button class="k-button"
                                data-bind="enabled: accepted, click: doSave"
                                type="submit">
                            ارسال
                        </button>
                        <button class="k-button" data-bind="click: resetModel">از نو</button>
                    </li>
                </ul>
                <span id="doneMsg"></span>
            </form>
        </div>
برای اتصال ViewModel تعریف شده به ناحیه‌ی مشخص شده با DIV ایی با Id مساوی coursesSection، می‌توان از متد kendo.bind استفاده کرد.
    <script type="text/javascript">
        $(function () {
            var model = kendo.data.Model.define({
            // ...
            });

            var viewModel = kendo.observable({
            // ...
            });

            kendo.bind($("#coursesSection"), viewModel);
        });
    </script>
به این ترتیب Kendo UI به بر اساس تعریف data-bind یک فیلد، برای مثال تغییرات خواص course.UserName را به text box نام کاربر منتقل می‌کند و همچنین اگر کاربر اطلاعاتی را در این text box وارد کند، بلافاصله این تغییرات در خاصیت course.UserName منعکس خواهند شد.
<input type="text" id="UserName" name="UserName" class="k-textbox"
       data-bind="value:course.UserName"
       required />

بنابراین تا اینجا به صورت خلاصه، مدلی را توسط متد kendo.data.Model.define، معادل مدل سمت سرور خود ایجاد کردیم. سپس وهله‌ای از این مدل را به صورت یک خاصیت جدید دلخواهی در ViewModel تعریف شده توسط متد kendo.observable در معرض دید View برنامه قرار دادیم. در ادامه اتصال ViewModel و View، با فراخوانی متد kendo.bind انجام شد. اکنون برای دریافت تغییرات کنترل‌های برنامه، تنها کافی است ویژگی‌های data-bind ایی را به آن‌ها اضافه کنیم.
در ناحیه‌ی تعریف شده توسط متد kendo.bind، کلیه خواص ViewModel در دسترس هستند. برای مثال اگر به تعریف ViewModel دقت کنید، یک خاصیت دیگر به نام accepted با مقدار false نیز در آن تعریف شده‌است (این خاصیت چون صرفا کاربرد UI داشت، در model برنامه قرار نگرفت). از آن برای اتصال checkbox تعریف شده، به button ارسال اطلاعات، استفاده کرده‌ایم:
<input type="checkbox" name="Accept"
       data-bind="checked:accepted"
       required />

<button class="k-button"
        data-bind="enabled: accepted, click: doSave"
        type="submit">
       ارسال
</button>
برای مثال اگر کاربر این checkbox را انتخاب کند، مقدار خاصیت accepted، مساوی true خواهد شد. تغییر مقدار این خاصیت، توسط ViewModel بلافاصله در کل ناحیه coursesSection منتشر می‌شود. به همین جهت ویژگی enabled: accepted که به معنای مقید بودن فعال یا غیرفعال بودن دکمه بر اساس مقدار خاصیت accepted است، دکمه را فعال می‌کند، یا برعکس و برای انجام این عملیات نیازی نیست کدنویسی خاصی را انجام داد. در اینجا بین checkbox و button یک سیم کشی برقرار است.


ارسال داده‌های تغییر کرده‌ی ViewModel به سرور

تا اینجا 4 جزء اصلی الگوی MVVM که در ابتدای بحث عنوان شد، تکمیل شده‌اند. مدل اطلاعات فرم تعریف گردید. ViewModel ایی که این خواص را به المان‌های فرم متصل می‌کند نیز در ادامه اضافه شده‌است. توسط ویژگی‌های data-bind کار Declarative data binding انجام می‌شود.
در ادامه نیاز است تغییرات ViewModel را به سرور، جهت ثبت، به روز رسانی و حذف نهایی منتقل کرد.
    <script type="text/javascript">
        $(function () {
            var model = kendo.data.Model.define({
                //...
            });

            var dataSource = new kendo.data.DataSource({
                type: 'json',
                transport: {
                    read: {
                        url: "api/registrations",
                        dataType: "json",
                        contentType: 'application/json; charset=utf-8',
                        type: 'GET'
                    },
                    create: {
                        url: "api/registrations",
                        contentType: 'application/json; charset=utf-8',
                        type: "POST"
                    },
                    update: {
                        url: function (course) {
                            return "api/registrations/" + course.Id;
                        },
                        contentType: 'application/json; charset=utf-8',
                        type: "PUT"
                    },
                    destroy: {
                        url: function (course) {
                            return "api/registrations/" + course.Id;
                        },
                        contentType: 'application/json; charset=utf-8',
                        type: "DELETE"
                    },
                    parameterMap: function (data, type) {
                        // Convert to a JSON string.  Without this step your content will be form encoded.
                        return JSON.stringify(data);
                    }
                },
                schema: {
                    model: model
                },
                error: function (e) {
                    alert(e.errorThrown);
                },
                change: function (e) {
                    // فراخوانی در زمان دریافت اطلاعات از سرور و یا تغییرات محلی
                    viewModel.set("coursesDataSourceRows", new kendo.data.ObservableArray(this.view()));
                }
            });

            var viewModel = kendo.observable({
                //...
            });

            kendo.bind($("#coursesSection"), viewModel);
            dataSource.read(); // دریافت لیست موجود از سرور در آغاز کار
        });
    </script>
در اینجا تعریف DataSource کار با منبع داده راه دور ASP.NET Web API را مشاهده می‌کنید. تعاریف اصلی آن با تعاریف مطرح شده در مطلب «فعال سازی عملیات CRUD در Kendo UI Grid» یکی هستند. هر قسمت آن مانند read، create، update و destory به یکی از متدهای کنترلر ASP.NET Web API اشاره می‌کنند. حالت‌های update و destroy بر اساس Id ردیف انتخابی کار می‌کنند. این Id را باید در قسمت model مربوط به اسکیمای تعریف شده، دقیقا مشخص کرد. عدم تعریف فیلد id، سبب خواهد شد تا عملیات update نیز در حالت create تفسیر شود.


متصل کردن DataSource به ViewModel

تا اینجا DataSource ایی جهت کار با سرور تعریف شده‌است؛ اما مشخص نیست که اگر رکوردی اضافه شد، چگونه باید اطلاعات خودش را به روز کند. برای این منظور خواهیم داشت:
    <script type="text/javascript">
        $(function () {
            $("#coursesSection").kendoValidator({
                // ...
            });

            var model = kendo.data.Model.define({
                // ...
            });

            var dataSource = new kendo.data.DataSource({
                // ...
            });

            var viewModel = kendo.observable({
                accepted: false,
                course: new model(),
                doSave: function (e) {
                    e.preventDefault();
                    console.log("this", this.course);
                    var validator = $("#coursesSection").data("kendoValidator");
                    if (validator.validate()) {
                        if (this.course.Id == 0) {
                            dataSource.add(this.course);
                        }
                        dataSource.sync(); // push to the server
                        this.set("course", new model()); // reset controls
                    }
                },
                resetModel: function (e) {
                    e.preventDefault();
                    this.set("course", new model());
                }            
             });

            kendo.bind($("#coursesSection"), viewModel);
            dataSource.read(); // دریافت لیست موجود از سرور در آغاز کار
        });
    </script>
همانطور که در تعاریف تکمیلی viewModel مشاهده می‌کنید، اینبار دو متد جدید دلخواه doSave و resetModel را اضافه کرده‌ایم.
در متد doSave، ابتدا بررسی می‌کنیم آیا اعتبارسنجی فرم با موفقیت انجام شده‌است یا خیر. اگر بله، توسط متد add منبع داده، اطلاعات فرم جاری را توسط شیء course که هم اکنون به تمامی فیلدهای آن متصل است، اضافه می‌کنیم. در اینجا بررسی شده‌است که آیا Id این اطلاعات صفر است یا خیر. از آنجائیکه از همین متد برای به روز رسانی نیز در ادامه استفاده خواهد شد، در حالت به روز رسانی، Id شیء ثبت شده، از طرف سرور دریافت می‌گردد. بنابراین غیر صفر بودن این Id به معنای عملیات به روز رسانی است و در این حالت نیازی نیست کار بیشتری را انجام داد؛ زیرا شیء متناظر با آن پیشتر به منبع داده اضافه شده‌است.
استفاده از متد add صرفا به معنای مطلع کردن منبع داده محلی از وجود رکوردی جدید است. برای ارسال این تغییرات به سرور، از متد sync آن می‌توان استفاده کرد. متد sync بر اساس متد add یک درخواست POST، بر اساس شیءایی که Id غیر صفر دارد، یک درخواست PUT و با فراخوانی متد remove بر روی منبع داده، یک درخواست DELETE را به سمت سرور ارسال می‌کند.
متد دلخواه  resetModel سبب مقدار دهی مجدد شیء course با یک وهله‌ی جدید از شیء model می‌شود. همینقدر برای پاک کردن تمامی کنترل‌های صفحه کافی است.

تا اینجا دو متد جدید را در ViewModel برنامه تعریف کرده‌ایم. در مورد نحوه‌ی اتصال آن‌ها به View، به کدهای دو دکمه‌ی موجود در فرم دقت کنید:
<button class="k-button"
        data-bind="enabled: accepted, click: doSave"
        type="submit">
       ارسال
</button>
<button class="k-button" data-bind="click: resetModel">از نو</button>
این متدها نیز توسط ویژگی‌های data-bind به هر دکمه نسبت داده شده‌اند. به این ترتیب برای مثال با کلیک کاربر بر روی دکمه‌ی submit، متد doSave موجود در ViewModel فراخوانی می‌شود.


مدیریت سمت سرور ثبت، ویرایش و حذف اطلاعات

در حالت ثبت، متد Post توسط آدرس مشخص شده در قسمت create منبع داده، فراخوانی می‌گردد. نکته‌ی مهمی که در اینجا باید به آن دقت داشت، نحوه‌ی بازگشت Id رکورد جدید ثبت شده‌است.  اگر این تنظیم صورت نگیرد، Id رکورد جدید را در لیست، مساوی صفر مشاهده خواهید کرد و منبع داده این رکورد را همواره به عنوان یک رکورد جدید، مجددا به سرور ارسال می‌کند.
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using KendoUI07.Models;

namespace KendoUI07.Controllers
{
    public class RegistrationsController : ApiController
    {
        public HttpResponseMessage Delete(int id)
        {
            var item = RegistrationsDataSource.LatestRegistrations.FirstOrDefault(x => x.Id == id);
            if (item == null)
                return Request.CreateResponse(HttpStatusCode.NotFound);

            RegistrationsDataSource.LatestRegistrations.Remove(item);
            return Request.CreateResponse(HttpStatusCode.OK, item);
        }

        public IEnumerable<Registration> Get()
        {
            return RegistrationsDataSource.LatestRegistrations;
        }

        public HttpResponseMessage Post(Registration registration)
        {
            if (!ModelState.IsValid)
                return Request.CreateResponse(HttpStatusCode.BadRequest);

            var id = 1;
            var lastItem = RegistrationsDataSource.LatestRegistrations.LastOrDefault();
            if (lastItem != null)
            {
                id = lastItem.Id + 1;
            }
            registration.Id = id;
            RegistrationsDataSource.LatestRegistrations.Add(registration);

            // ارسال آی دی مهم است تا از ارسال رکوردهای تکراری جلوگیری شود
            return Request.CreateResponse(HttpStatusCode.Created, registration);
        }

        [HttpPut] // Add it to fix this error: The requested resource does not support http method 'PUT'
        public HttpResponseMessage Update(int id, Registration registration)
        {
            var item = RegistrationsDataSource.LatestRegistrations
                                        .Select(
                                            (prod, index) =>
                                                new
                                                {
                                                    Item = prod,
                                                    Index = index
                                                })
                                        .FirstOrDefault(x => x.Item.Id == id);
            if (item == null)
                return Request.CreateResponse(HttpStatusCode.NotFound);


            if (!ModelState.IsValid || id != registration.Id)
                return Request.CreateResponse(HttpStatusCode.BadRequest);

            RegistrationsDataSource.LatestRegistrations[item.Index] = registration;
            return Request.CreateResponse(HttpStatusCode.OK);
        }
    }
}
در اینجا بیشتر امضای این متدها مهم هستند، تا منطق پیاده سازی شده در آن‌ها. همچنین بازگشت Id رکورد جدید، توسط متد Post نیز بسیار مهم است و سبب می‌شود تا DataSource بداند با فراخوانی متد sync آن، باید عملیات Post یا create انجام شود یا Put و update.


نمایش آنی اطلاعات ثبت شده در یک لیست

ردیف‌های اضافه شده به منبع داده را می‌توان بلافاصله در همان سمت کلاینت توسط Kendo UI Template که قابلیت کار با ViewModelها را دارد، نمایش داد:
    <div id="coursesSection" class="k-rtl k-header">
        <div class="box-col">
            <form id="myForm" data-role="validator" novalidate="novalidate">
                           <!--فرم بحث شده در ابتدای مطلب-->
            </form>
        </div>
        <div id="results">
            <table class="metrotable">
                <thead>
                    <tr>
                        <th>Id</th>
                        <th>نام</th>
                        <th>دوره</th>
                        <th>هزینه</th>
                        <th>ایمیل</th>
                        <th>تلفن</th>
                        <th></th>
                        <th></th>
                    </tr>
                </thead>
                <tbody data-template="row-template" data-bind="source: coursesDataSourceRows"></tbody>
                <tfoot data-template="footer-template" data-bind="source: this"></tfoot>
            </table>
            <script id="row-template" type="text/x-kendo-template">
                <tr>
                    <td data-bind="text: Id"></td>
                    <td data-bind="text: UserName"></td>
                    <td dir="ltr" data-bind="text: CourseName"></td>
                    <td>
                        #: kendo.toString(get("Credit"), "c0") #
                    </td>
                    <td data-bind="text: Email"></td>
                    <td data-bind="text: Tel"></td>
                    <td><button class="k-button" data-bind="click: deleteCourse">حذف</button></td>
                    <td><button class="k-button" data-bind="click: editCourse">ویرایش</button></td>
                </tr>
            </script>
            <script id="footer-template" type="text/x-kendo-template">
                <tr>
                    <td colspan="3"></td>
                    <td>
                        جمع کل: #: kendo.toString(totalPrice(), "c0") #
                    </td>
                    <td colspan="2"></td>
                    <td></td>
                    <td></td>
                </tr>
            </script>
        </div>
    </div>
در ناحیه‌ی coursesSection که توسط متد kendo.bind به viewModel برنامه متصل شده‌است، یک جدول را برای نمایش ردیف‌های ثبت شده توسط کاربر اضافه کرده‌ایم. thead آن بیانگر سر ستون جدول است. قسمت tbody و tfoot این جدول توسط دو Kendo UI Template مقدار دهی شد‌ه‌اند. هر کدام نیز منبع داده‌اشان را از view model دریافت می‌کنند. در row-template معادل خواص شیء course را مشاهده می‌کنید. در footer-template متد totalPrice برای نمایش جمع ستون هزینه اضافه شده‌است. بنابراین مطابق این قسمت از View، به یک خاصیت جدید coursesDataSourceRows و سه متد deleteCourse، editCourse و totalPrice نیاز است:
    <script type="text/javascript">
        $(function () {
            // ...
            var viewModel = kendo.observable({
                accepted: false,
                course: new model(),
                coursesDataSourceRows: new kendo.data.ObservableArray([]),
                doSave: function (e) {
                       // ...
                },
                resetModel: function (e) {
                      // ...
                },
                totalPrice: function () {
                    var sum = 0;
                    $.each(this.get("coursesDataSourceRows"), function (index, item) {
                        sum += item.Credit;
                    });
                    return sum;
                },
                deleteCourse: function (e) {
                    // the current data item is passed as the "data" field of the event argument
                    var course = e.data;
                    dataSource.remove(course);
                    dataSource.sync(); // push to the server
                },
                editCourse: function(e) {
                    // the current data item is passed as the "data" field of the event argument
                    var course = e.data;
                    this.set("course", course);
                }
            });

            kendo.bind($("#coursesSection"), viewModel);
            dataSource.read(); // دریافت لیست موجود از سرور در آغاز کار
        });
    </script>
نحوه‌ی اتصال خاصیت جدید coursesDataSourceRows که به عنوان منبع داده ردیف‌های row-template عمل می‌کند، به این صورت است:
- ابتدا خاصیت دلخواه coursesDataSourceRows به viewModel اضافه می‌شود تا در ناحیه‌ی coursesSection در دسترس قرار گیرد.
- سپس اگر به انتهای تعریف DataSource دقت کنید، داریم:
    <script type="text/javascript">
        $(function () {
            var dataSource = new kendo.data.DataSource({
                //...
                change: function (e) {
                    // فراخوانی در زمان دریافت اطلاعات از سرور و یا تغییرات محلی
                    viewModel.set("coursesDataSourceRows", new kendo.data.ObservableArray(this.view()));
                }
            });
        });
    </script>
متد change آن، هر زمانیکه اطلاعاتی در منبع داده تغییر کنند یا اطلاعاتی به سمت سرور ارسال یا دریافت گردد، فراخوانی می‌شود. در همینجا فرصت خواهیم داشت تا خاصیت coursesDataSourceRows را جهت نمایش اطلاعات موجود در منبع داده، مقدار دهی کنیم. همین مقدار دهی ساده سبب اجرای row-template برای تولید ردیف‌های جدول می‌شود. استفاده از new kendo.data.ObservableArray سبب خواهد شد تا اگر اطلاعاتی در فرم برنامه تغییر کند، این اطلاعات بلافاصله در لیست گزارش برنامه نیز منعکس گردد.



کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید:
KendoUI07.zip
مطالب
بررسی تفاوت بین DTO و POCO
در ابتدا اجازه بدهید تعریف درستی از این دو واژه، ارائه کنیم.

DTO (Data Transfer Object)
به بیان خیلی ساده، DTO‌ها برای انتقال اطلاعات استفاده می‌شوند؛ پس هیچ منطق و رفتاری در این اشیاء تعریف نمی‌شود .اگر در DTO منطقی پیاده سازی شود، دیگر به آن DTO گفته نمی‌شود. اجازه بدید منظورمان را از منطق یا رفتار مشخص کنیم. منطق یا رفتار، همان متدهایی هستند که در نوع داده خود تعریف میکنیم. در #C، یک DTO تنها از خصوصیت‌ها (Properties) که از بلوک‌های Get و Set تشکیل شده‌اند، ساخته می‌شود. البته بدون کدهایی جهت اعتبار سنجی (Validation) مقادیر.

سؤال: وضعیت attribute ‌ها و Metadata‌ها چه می‌شود؟
خیلی غیر معمول نیست که از metadata‌ها در DTO، به‌منظور اعتبار سنجی یا اهداف خاص، استفاده کنیم. بعضی از attribute‌ها هیچ رفتاری را به DTO‌ها اضافه نمی‌کنند؛ ولی استفاده از DTO‌ها را در بخش‌های دیگر سیستم، ساده‌تر می‌کنند. در نتیجه هیچکدام از attribute ‌ها و metadata‌ها، شرایط DTO بودن را نقض نمی‌کنند.

مدل‌های دیگری مثل ViewModels‌ها و API Model‌ها چه می‌شوند؟
واژه DTO خیلی مبهم است. تنها چیزی که بیان می‌کند این است که شیء است و فقط و فقط شامل اطلاعات است و رفتاری ندارد. در این تعریف درباره‌ی کاربرد مورد نظر یک DTO چیزی گفته نشده. در بسیاری از معماری‌ها، DTO نقش خاصی را ایفا می‌کند. بطور مثال در معماری MVC، از DTO‌ها برای انقیاد داده (Binding) و ارسال اطلاعات به یک View استفاده میکنند. به همین خاطر این DTO‌ها بعنوان ViewModel، در معماری MVC شناخته می‌شوند که رفتاری را در خود تعریف نمی‌کنند و تنها فرمت اطلاعات مورد انتظار یک View را مهیا می‌کنند.
پس در این سناریوی خاص، ViewModel نوعی DTO می‌باشد. اما باید دقت داشته باشید، همه ViewModel‌‌ها را نمی‌توان DTO محسوب کرد؛ مثلا در معماری MVVM، ویوو مدل‌های تعریف شده، شامل رفتار هم می‌باشند. حتی در معماری MVC نیز گاهی اوقات منطقی به  ViewModel‌‌ها اضافه می‌شود که دیگر به آنها DTO نمی‌گوییم.

 
در صورت امکان، نام DTO‌ها را بر اساس استفاده‌ی آنها تعیین کنید. بطور مثال کلاسی با نام FoodDTO، مشخص نمی‌کند که این نوع، کجا و چگونه قرار است در معماری برنامه شما مورد استفاده قرار  بگیرید؛ برعکس نامگذاری به صورت FoodViewModels کاربرد آن را صراحتا بیان می‌کند.
مثالی از DTO در زبان سی شارپ :
public class ProductViewModel
{
  public int ProductId { get; set; }
  public string Name { get; set; }
  public string Description { get; set; }
  public string ImageUrl { get; set; }
  public decimal UnitPrice { get; set; }
}

کپسوله سازی و DTO ها 
کپسوله سازی، یکی از اصول برنامه نویسی شیءگرا می‌باشد. اما این کپسوله سازی به DTO‌ها اعمال نمی‌شوند. به این علت که هدف کپسوله سازی، پنهان کردن فرآیند پشت صحنه‌ی ذخیره سازی اطلاعات است؛ اما در DTO هیچ فرآیندی پیاده سازی نشده و نباید هیچ State پنهانی وجود داشته باشد. پس بحث Encapsulation در DTO منتفی است. پس کار را برای خودتان سخت نکنید؛ با تعریف private setter ‌ها یا تبدیل کردن DTO به یک شیء غیرقابل تغییر (immutable). شما باید به‌راحتی بتوانید عملیات ایجاد، نوشتن و خواندن DTO‌‌ها را انجام دهید؛ همچنین باید بتوانید عملیات سریالایز کردن بر روی DTO‌‌ها را بدون فرآیند سفارشی اضافه‌ای، انجام دهید.

Field ها  یا Property ها 
سؤالی که مطرح می‌شود این است که وقتی کپسوله سازی در DTO مفهومی ندارد، چرا باید همیشه از property ‌ها استفاده کنیم؟ چرا از فیلد‌ها استفاده نکنیم (فیلد‌های public )؟
ما می‌توانیم هم از property استفاده کنیم و هم از field‌ها؛ اما بعضی از فریم ورک‌ها که کار Serialization را انجام می‌دهند، فقط با property ‌ها کار می‌کنند. بنابراین بسته به نیاز خودتان، از field‌های عمومی یا property‌ها استفاده کنید. اما عموما از Property استفاده میکنند. البته در این پیوند، پرسش و پاسخ مفصلی در این رابطه وجود دارد.

غیرقابل تغییر بودن (Immutability) و نوع رکورد ( Record Type )
غیرقابل تغییر بودن، یکی از مزیت‌های مهم در توسعه نرم افزار است. اما همانطور که در مثل بالا بیان شد، نیازی به غیرقابل تغییر کردن DTO‌‌ها نیست. با ارائه رکورد در سی شارپ 9  شرایط کمی تغییر کرد. شاید عبارت مخفف دیگری که اضافه شده Data transfer Records یا (DTRs) است. یکی از روش‌های تعریف DTR در سی شارپ 9، به شکل زیر است:
public record ProductDTO(int Id, string Name, string Description);

البته روش دیگری هم وجود دارد که شما property‌ها را تعریف کنید و از طریق سازنده، مقدار دهی شوند. ویژگی جدید init-only این امکان را فراهم می‌کند که فقط در زمان مقدار دهی اولیه (initialization)، خصوصیات مقداردهی شوند و در ادامه‌ی چرخه حیات شیء، property ‌ها فقط خواندنی هستند. این ویژگی، record‌ها را غیر قابل تغییر می‌کند.
مثال:
public record ProductDTO
{
  public int Id { get; init; }
  public string Name { get; init; }
}
var dto = new ProductDTO { Id = 1, Name = "some name" };

کلاس‌های POCO یا همان Plain Old CLR/C# Object
شی Plain Old چیست؟ هر شیءای که Plain Old باشد، می‌تواند در هر جایی از برنامه‌ی ما مورد استفاده قرار بگیرد؛ حتی در کلاس‌های Test برنامه. این اشیاء هیچگونه وابستگی برای اجرا وظایف خود، به بانک‌های اطلاعاتی و کتابخانه‌های ثالت ندارند.
برای درک بهتر این نوع کلاس‌ها، به مثال زیر دقت کنید:
public class Product : DataObject<Product>
{
  public Product(int id)
  {
    Id = id;
    InitializeFromDatabase();
  }
  private void InitializeFromDatabase()
  {
    DataHelpers.LoadFromDatabase(this);
  }
  public int Id { get; private set; }
  // other properties and methods
}
همانطور که مشاهده میکنید، این کلاس به متد استاتیکی برای کار با دیتابیس وابسته است؛ در نتیجه باعث میشود که کل کلاس، به وجود بانک اطلاعاتی وابسته شود. همچنین با ارث بری از کلاس پایه‌ی دیگری، وابستگی به یک کتابخانه‌ی ثالث ایجاد شده‌است. اجرای آزمون واحد برای چنین کلاسی، سبب fail شدن عملیات می‌شود. به این علت که ارتباط با بانک اطلاعاتی مورد نیاز متد DataHelpers، تامین نشده‌است. این شرایط، مثالی از الگوی Active Record Pattern می‌باشند. همچنین این کلاس دسترسی به منبع داده را در درون خود گنجانده است که این به معنای نقض اصل Persistence Ignorant (اصل Persistence Ignorance به طور خلاصه بیان می‌کند که در تحلیل و طراحی Business Logic به موضوع ذخیره‌سازی (Persistence) فکر نکنید (تا جای ممکن) یا به عبارت دیگر، ذهن خود را درگیر پیچیدگی‌های ذخیره سازی نکنید. برگرفته شده از breakpoint.blog.ir : روح الله دلپاک)می باشد. یکی از ویژگی‌های POCO عدم نقض الگوی فوق است.

مثالی از POCO : 
public class Product
{
  public Product(int id)
  {
    Id = id;
  }

  private Product()
  {
    // required for EF
  }

  public int Id { get; private set; }
  // other properties and methods
}
این کلاس یک POCO است:
  • برای اجرای وظایف خود به فریم ورک ثالثی وابسته نیست.
  • به کلاس پایه‌ای ( Base class) نیاز ندارد.
  • وابستگی به متد استاتیکی ندارد.
  • می تواند در هر جایی از پروژه، نمونه سازی شود.
  • اصل Persistence Ignorant را بیشتر رعایت کرده، نه بطور کامل؛ چون یک سازنده دارد که به کتابخانه‌ی ثالثی نیازمند است (سازنده‌ی بدون پارامتر که مورد نیاز EF می‌باشد).

POCO و DTO :

شاید این دو مفهموم گیج کننده باشند، ولی DTO همان POCO هست. اگر یک کلاس، DTO باشد، حتما POCO نیز هست. (مرور ویژگی‌های دو مورد در بخش‌های قبلی) ولی برعکس این وضعیت ممکن است صادق نباشد؛ مثال قبلی که در آن وابستگی به کتابخانه‌ی ثالثی در سازنده‌ی بدون پارامتر وجود داشت، DTO بودن را نقض می‌کرد. پس اگر هر دو حالت صادق بود، میتوان گفت این دو مفهوم یکی است.
مطالب
FluentValidation #1
FluentValidation یک پروژه سورس باز برای اعتبارسنجی Business Object‌ها با استفاده از Fluent Interface و Lambada Expressions می‌باشد.
جهت نصب این کتابخانه دستور زیر را در Package Manager Console وارد نمایید:
PM> Install-Package FluentValidation

ایجاد یک Validator
برای تعریف مجموعه قوانین اعتبارسنجی برای یک موجودیت ابتدا بایستی یک کلاس ایجاد کرد که از AbstractValidator<T> مشتق می‌شود که T در اینجا برابر موجودیتی است که می‌خواهیم اعتبارسنجی کنیم. به عنوان مثال کلاس مشتری به صورت زیر را در نظر بگیرید:
public class Customer
{
      public int Id { get; set; }
      public string Surname { get; set; }
      public string Forename { get; set; }
      public decimal Discount { get; set; }
      public string Address { get; set; }
}
مجموعه قوانین اعتبارسنجی با استفاده از متد RuleFor و داخل متد سازنده کلاس Validator تعریف می‌شوند. به عنوان مثال برای اطمینان از اینکه مقدار خاصیت Surname برابر Null نباشد باید به صورت زیر عمل کرد:
using FluentValidation;

public class CustomerValidator : AbstractValidator<Customer>
{
      public CustomerValidator
      {
           RuleFor(customer => customer.Surname).NotNull();
      }
}


اعتبارسنجی زنجیره ای برای یک خاصیت

برای اعتبارسنجی یک خاصیت، می‌توان از چندین Validator باهم نیز استفاده کرد:
RuleFor(customer => customer.Surname).NotNull().NotEqual("foo");
در اینجا خاصیت Surname نباید Null باشد و همچنین مقدار آن نباید برابر "Foo" باشد.
برای اجراکردن اعتبارسنجی، ابتدا یک نمونه از کلاس Validator مان را ساخته و شیء ای را که می‌خواهیم اعتبارسنجی کنیم به متد Validate آن میفرستیم:
Customer customer = new Customer();
CustomerValidator validator = new CustomerValidator();
ValidationResult results = validator.Validate(customer);

خروجی متد Validate، یک ValidationResult است که شامل دو خاصیت زیر می‌باشد:

  • IsValid: از نوع bool برای تعیین اینکه اعتبارسنجی موفقیت آمیز بوده یا خیر.
  • Errors: یک مجموعه از ValidationFailure که جزئیات تمام اعتبارسنجی‌های ناموفق را شامل می‌شود.
به عنوان مثال قطعه کد زیر، جزئیات اعتبارسنجی‌های ناموفق را نمایش می‌دهد:
Customer customer = new Customer();
CustomerValidator validator = new CustomerValidator();

ValidationResult results = validator.Validate(customer);

if(! results.IsValid) 
{
     foreach(var failure in results.Errors)
     {
          Console.WriteLine("Property " + failure.PropertyName + " failed validation. Error was: " +      failure.ErrorMessage);
     }
}


پرتاب استثناها (Throwing Exceptions)

به جای برگرداندن ValidationResult شما میتوانید با کمک متد ValidateAndThrow به FluentValidation بگویید که هنگام اعتبارسنجی ناموفق یک استثنا پرتاب کند:
Customer customer = new Customer();
CustomerValidator validator = new CustomerValidator();
validator.ValidateAndThrow(customer);
در این صورت Validator یک ValidationException را پرتاب خواهد کرد که دربردارنده‌ی پیام‌های خطا در خاصیت Errors خود می‌باشد.

استفاده از Validator‌ها برای Complex Properties

جهت درک این ویژگی تصور کنید که کلاس‌های مشتری و آدرس و همچنین کلاس‌های مربوط به اعتبارسنجی آن‌ها را به صورت زیر داریم:
public class Customer
{
     public string Name { get; set; }
     public Address Address { get; set; }
}

public class Address 
{
     public string Line1 { get; set; }
     public string Line2 { get; set; }
     public string Town { get; set; }
     public string County { get; set; }
     public string Postcode { get; set; }
}

public class AddressValidator : AbstractValidator<Address>
{
     public AddressValidator() 
     {
          RuleFor(address => address.Postcode).NotNull();
          //etc
     }
}

public class CustomerValidator : AbstractValidator<Customer> 
{
     public CustomerValidator()
     {
          RuleFor(customer => customer.Name).NotNull();
          RuleFor(customer => customer.Address).SetValidator(new AddressValidator())
      }
}
در این صورت وقتی متد Validate کلاس CustomerValidator را فراخوانی نمایید AddressValidator نیز فراخوانی خواهد شد و نتیجه این اعتبارسنجی به صورت یکجا در یک ValidationResult برگشت داده خواهد شد.


استفاده از Validator‌ها برای مجموعه‌ها (Collections)

Validator‌ها همچنین می‌توانند بر روی خاصیت هایی که شامل مجموعه ای از یک شیء دیگر هستند نیز استفاده شوند. به عنوان مثال یک مشتری که دارای لیستی از سفارشات است را در نظر بگیرید:
public class Customer
{
     public IList<Order> Orders { get; set; }
}

public class Order 
{
     public string ProductName { get; set; }
     public decimal? Cost { get; set; }
}

var customer = new Customer();
customer.Orders = new List<Order> 
{
     new Order { ProductName = "Foo" },
     new Order { Cost = 5 } 
};
کلاس OrderValidator نیز به صورت زیر خواهد بود:
public class OrderValidator : AbstractValidator<Order>
{
     public OrderValidator() 
     {
          RuleFor(x => x.ProductName).NotNull();
          RuleFor(x => x.Cost).GreaterThan(0);
     }
}

این Validator می‌تواند داخل CustomerValidator مورد استفاده قرار بگیرد (با استفاده از متد SetCollectionValidator):

public class CustomerValidator : AbstractValidator<Customer>
{
     public CustomerValidator()
     {
          RuleFor(x => x.Orders).SetCollectionValidator(new OrderValidator());
     }
}

می توان با استفاده از متد Where یا Unless روی اعتبارسنجی شرط گذاشت:

RuleFor(x => x.Orders).SetCollectionValidator(new OrderValidator()).Where(x => x.Cost != null);


گروه بندی قوانین اعتبارسنجی

RuleSet‌‌ها به شما این امکان را می‌دهند تا بعضی از قوانین اعتبارسنجی را داخل یک گروه قرار دهید تا با یکدیگر اجرا شوند. در حالی که دیگر قوانین نادیده گرفته می‌شوند.
برای مثال تصور کنید شما سه خاصیت در کلاس Person دارید که شامل (Id, Surname, Forename) می‌باشند و همچنین یک قانون برای هرکدام از آن ها. میتوان قوانین مربوط به Surname و Forename را در یک RuleSet مجزا به نام Names قرار داد:
public class PersonValidator : AbstractValidator<Person>
{
     public PersonValidator() 
     {
          RuleSet("Names", () =>
          {
               RuleFor(x => x.Surname).NotNull();
               RuleFor(x => x.Forename).NotNull();
          });
 
          RuleFor(x => x.Id).NotEqual(0);
      }
}
در اینجا دو خاصیت Surname و Forename با یکدیگر داخل یک RuleSet به نام Names گروه شده اند. برای اعتبارسنجی جداگانه این گروه نیز به صورت زیر می‌توان عمل کرد:
var validator = new PersonValidator();
var person = new Person();
var result = validator.Validate(person, ruleSet: "Names");
این ویژگی به شما این امکان را می‌دهد تا یک Validator پیچیده را به چندین قسمت کوچکتر تقسیم کرده و توانایی اعتبارسنجی این قسمت‌ها را به صورت جداگانه داشته باشید.
بازخوردهای دوره
حذف یک ردیف از اطلاعات به همراه پویانمایی محو شدن اطلاعات آن توسط jQuery در ASP.NET MVC
البته راه دیگری هم پیدا کردم.
به نظرم جالب اومد.
ابتدا یک اکشن فیلتر تعریف شده و در ان هدر درخواست خوانده شده و با محتوی کوکی چک می‌شود در صورتی که برابر باشند عمل انجام خواهد شد با این تفاوت باید توکن در زمان ارسال در هدر درخواست قرار گیرد مانند زیر:
کد اکشن فیلتر:
public class ValidateJsonAntiForgeryTokenAttribute : ActionFilterAttribute
    {
#region Methods (1) 

// Public Methods (1) 

        /// <summary>
        /// Called when [action executing].
        /// </summary>
        /// <param name="actionContext">The action context.</param>
        public void OnActionExecuting(HttpActionContext actionContext)
        {
            try
            {
                var cookieName = AntiForgeryConfig.CookieName;
                var headers = actionContext.Request.Headers;
                var cookie = headers
                    .GetCookies()
                    .Select(c => c[AntiForgeryConfig.CookieName])
                    .FirstOrDefault();
                var rvt = headers.GetValues("__RequestVerificationToken").FirstOrDefault();
                AntiForgery.Validate(cookie != null ? cookie.Value : null, rvt);
            }
            catch
            {
                actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Forbidden, "Unauthorized request.");
            }
        }

#endregion Methods 
    }
سپس در اکشن دلخواه باید این اکشن فیلتر را بکار برد (در درخواست‌های json)
[HttpPost]
        //[ValidateAntiForgeryToken]
        [ValidateJsonAntiForgeryToken]
        [OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
        public virtual ActionResult DeletePhoto(int? id)
        {
            string ret="error";
            if (id != null && id > 0)
            {
                bool result = _imageGalleryService.Value.Photos.Value.Delete(id.ToInt32());
                ret = result ? "ok" : "nok";
            }
            return Content(ret);
            //return Json(new { ret });
        }
سپس در طرف ویو به صورت زیر عمل شود
$.ajax({
                       ....
                    type: "POST",
                    url: '@Url',
                    data: JSON.stringify({ id: Id }),
                    contentType: "application/json; charset=utf-8",
                    // این قسمت اضافه شود
                    headers: { __RequestVerificationToken: $("input[name=__RequestVerificationToken]").val() },
                    dataType: "json",
                     ..........
امیدوارم برای دوستان هم کاربرد داشته باشد