نظرات مطالب
C# 7 - Tuple return types and deconstruction
برای بسط دادن یک چندتایی به آرگومان‌های ورودی یک متد، آیا راهکاری در نظر گرفته شده است؟
مثلا چیزی شبیه spread در ES6
public void showMsg(int age, string name){/*...*/}

 (int age, string name) value =(20, "Jessy"); showMsg(...value); //? or something else
مطالب
آشنایی با NHibernate - قسمت پنجم

استفاده از LINQ جهت انجام کوئری‌ها توسط NHibernate

نگارش نهایی 1.0 کتابخانه‌ی LINQ to NHibernate اخیرا (حدود سه ماه قبل) منتشر شده است. در این قسمت قصد داریم با کمک این کتابخانه، اعمال متداول انجام کوئری‌ها را بر روی دیتابیس قسمت قبل انجام دهیم.
توسط این نگارش ارائه شده، کلیه اعمال قابل انجام با criteria API این فریم ورک را می‌توان از طریق LINQ نیز انجام داد (NHibernate برای کار با داده‌ها و جستجوهای پیشرفته بر روی آن‌ها، HQL : Hibernate Query Language و Criteria API را سال‌ها قبل توسعه داده است).

جهت دریافت پروایدر LINQ مخصوص NHibernate به آدرس زیر مراجعه نمائید:


پس از دریافت آن، به همان برنامه کنسول قسمت قبل، دو ارجاع را باید افزود:
الف) ارجاعی به اسمبلی NHibernate.Linq.dll
ب) ارجاعی به اسمبلی استاندارد System.Data.Services.dll دات نت فریم ورک سه و نیم

در ابتدای متد Main برنامه قصد داریم تعدادی مشتری را به دیتابیس اضافه نمائیم. به همین منظور متد AddNewCustomers را به کلاس CDbOperations برنامه کنسول قسمت قبل اضافه نمائید. این متد لیستی از مشتری‌ها را دریافت کرده و آن‌ها را در طی یک تراکنش به دیتابیس اضافه می‌کند:

public void AddNewCustomers(params Customer[] customers)
{
using (ISession session = _factory.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
foreach (var data in customers)
session.Save(data);

session.Flush();

transaction.Commit();
}
}
}
در اینجا استفاده از واژه کلیدی params سبب می‌شود که بجای تعریف الزامی یک آرایه از نوع مشتری‌ها، بتوانیم تعداد دلخواهی پارامتر از نوع مشتری را به این متد ارسال کنیم.

پس از افزودن این ارجاعات، کلاس جدیدی را به نام CLinqTest به برنامه کنسول اضافه نمائید. ساختار کلی این کلاس که قصد استفاده از پروایدر LINQ مخصوص NHibernate را دارد باید به شکل زیر باشد (به کلاس پایه NHibernateContext دقت نمائید):

using System.Collections.Generic;
using System.Linq;
using NHibernate;
using NHibernate.Linq;
using NHSample1.Domain;

namespace ConsoleTestApplication
{
class CLinqTest : NHibernateContext
{ }
}
اکنون پس از مشخص شدن context یا زمینه، نحوه ایجاد یک کوئری ساده LINQ to NHibernate به صورت زیر می‌تواند باشد:

using System.Collections.Generic;
using System.Linq;
using NHibernate;
using NHibernate.Linq;
using NHSample1.Domain;

namespace ConsoleTestApplication
{
class CLinqTest : NHibernateContext
{
ISessionFactory _factory;

public CLinqTest(ISessionFactory factory)
{
_factory = factory;
}

public List<Customer> GetAllCustomers()
{
using (ISession session = _factory.OpenSession())
{
var query = from x in session.Linq<Customer>() select x;
return query.ToList();
}
}
}
}
ابتدا علاوه بر سایر فضاهای نام مورد نیاز، فضای نام NHibernate.Linq به پروژه افزوده می‌شود. سپس از extension متدی به نام Linq بر روی اشیاء ISession از نوع یکی از موجودیت‌های تعریف شده در برنامه در قسمت‌های قبل، می‌توان جهت تهیه کوئری‌های Linq مورد نظر بهره برد.
در این کوئری، لیست تمامی مشتری‌ها بازگشت داده می‌شود.

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

static void Main(string[] args)
{
using (ISessionFactory session = Config.CreateSessionFactory(
MsSqlConfiguration
.MsSql2008
.ConnectionString("Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true")
.ShowSql()
))
{

var customer1 = new Customer()
{
FirstName = "Vahid",
LastName = "Nasiri",
AddressLine1 = "Addr1",
AddressLine2 = "Addr2",
PostalCode = "1234",
City = "Tehran",
CountryCode = "IR"
};

var customer2 = new Customer()
{
FirstName = "Ali",
LastName = "Hasani",
AddressLine1 = "Addr..1",
AddressLine2 = "Addr..2",
PostalCode = "4321",
City = "Shiraz",
CountryCode = "IR"
};

var customer3 = new Customer()
{
FirstName = "Mohsen",
LastName = "Shams",
AddressLine1 = "Addr...1",
AddressLine2 = "Addr...2",
PostalCode = "5678",
City = "Ahwaz",
CountryCode = "IR"
};

CDbOperations db = new CDbOperations(session);
db.AddNewCustomers(customer1, customer2, customer3);

CLinqTest lt = new CLinqTest(session);
foreach (Customer customer in lt.GetAllCustomers())
{
Console.WriteLine("Customer: LastName = {0}", customer.LastName);
}
}

Console.WriteLine("Press a key...");
Console.ReadKey();

}
در این متد ابتدا تعدادی رکورد تعریف و سپس به دیتابیس اضافه شدند. در ادامه لیست تمامی آن‌ها از دیتابیس دریافت و نمایش داده می‌شود.

مهمترین مزیت استفاده از LINQ در این نوع کوئری‌ها نسبت به روش‌های دیگر، استفاده از کدهای strongly typed دات نتی تحت نظر کامپایلر است، نسبت به رشته‌های معمولی SQL که کامپایلر کنترلی را بر روی آن‌ها نمی‌تواند داشته باشد (برای مثال اگر نوع یک ستون تغییر کند یا نام آن‌، در حالت استفاده از LINQ بلافاصله یک خطا را از کامپایلر جهت تصحیح مشکلات دریافت خواهیم کرد که این مورد در زمان استفاده از یک رشته معمولی صادق نیست). همچنین مزیت فراهم بودن Intellisense را حین نوشتن کوئری‌هایی از این دست نیز نمی‌توان ندید گرفت.

مثالی دیگر:
لیست تمام مشتری‌های شیرازی را نمایش دهید:
ابتدا متد GetCustomersByCity را به کلاس CLinqTest فوق اضافه می‌کنیم:

public List<Customer> GetCustomersByCity(string city)
{
using (ISession session = _factory.OpenSession())
{
var query = from x in session.Linq<Customer>()
where x.City == city
select x;
return query.ToList();
}
}
سپس برای استفاده از آن، چند سطر ساده زیر به ادامه متد Main اضافه می‌شوند:

foreach (Customer customer in lt.GetCustomersByCity("Shiraz"))
{
Console.WriteLine("Customer: LastName = {0}", customer.LastName);
}
یکی دیگر از مزایای استفاده از LINQ to NHibernate ، امکان بکارگیری LINQ بر روی تمامی دیتابیس‌های پشتیبانی شده توسط NHibernate است؛ برای مثال مای اس کیوال، اوراکل و ....
لیست کامل دیتابیس‌های پشتیبانی شده توسط NHibernate را در این آدرس می‌توانید مشاهده نمائید. (البته به نظر لیست آن، آنچنان هم به روز نیست؛ چون در نگارش آخر NHibernate ، پشتیبانی از اس کیوال سرور 2008 هم اضافه شده است)


نکته:
در کوئری‌های مثال‌های فوق همواره باید session.Linq<T> را ذکر کرد. اگر علاقمند بودید شبیه به روشی که در LINQ to SQL موجود است مثلا db.TableName بجای session.Linq<T> در کوئری‌ها ذکر گردد، می‌توان اصلاحاتی را به صورت زیر اعمال کرد:
یک کلاس جدید را به نام SampleContext به برنامه کنسول جاری با محتویات زیر اضافه نمائید:

using System.Linq;
using NHibernate;
using NHibernate.Linq;
using NHSample1.Domain;

namespace ConsoleTestApplication
{
class SampleContext : NHibernateContext
{
public SampleContext(ISession session)
: base(session)
{ }

public IOrderedQueryable<Customer> Customers
{
get { return Session.Linq<Customer>(); }
}

public IOrderedQueryable<Employee> Employees
{
get { return Session.Linq<Employee>(); }
}

public IOrderedQueryable<Order> Orders
{
get { return Session.Linq<Order>(); }
}

public IOrderedQueryable<OrderItem> OrderItems
{
get { return Session.Linq<OrderItem>(); }
}

public IOrderedQueryable<Product> Products
{
get { return Session.Linq<Product>(); }
}
}
}
در این کلاس به ازای تمام موجودیت‌های تعریف شده در پوشه domain برنامه اصلی خود (همان NHSample1 قسمت‌های اول و دوم)، یک متد از نوع IOrderedQueryable را باید تشکیل دهیم که پیاده سازی آن‌را ملاحظه می‌نمائید.
سپس بازنویسی متد GetCustomersByCity بر اساس SampleContext فوق به صورت زیر خواهد بود که به کوئری‌های LINQ to SQL بسیار شبیه است:

using System.Collections.Generic;
using System.Linq;
using NHibernate;
using NHSample1.Domain;

namespace ConsoleTestApplication
{
class CSampleContextTest
{
ISessionFactory _factory;

public CSampleContextTest(ISessionFactory factory)
{
_factory = factory;
}

public List<Customer> GetCustomersByCity(string city)
{
using (ISession session = _factory.OpenSession())
{
using (SampleContext db = new SampleContext(session))
{
var query = from x in db.Customers
where x.City == city
select x;
return query.ToList();
}
}
}
}
}
دریافت سورس برنامه تا این قسمت

و در تکمیل این بحث، می‌توان به لیستی از 101 مثال LINQ ارائه شده در MSDN اشاره کرد که یکی از بهترین و سریع ترین مراجع یادگیری مبحث LINQ است.


ادامه دارد ...


نظرات مطالب
آرگومان‌های نامگذاری شده (named arguments/parameters) در C#4
یک نکته‌ی تکمیلی: بهبود جزئی آرگومان‌های نامدار در C# 7.2

تا پیش از C# 7.2، آرگومان‌های نامدار، تنها پس از ذکر آرگومان‌های بدون نام، مجاز بودند. برای مثال اگر امضای متدی به صورت زیر باشد:
public static void Write(int age, string name, string homeTown)
فراخوانی آن به صورت زیر تا C# 7.2 مجاز نبود:
Write(age: 20, "User 1", homeTown: "Tehran");
و باخطای کامپایلر زیر، کامپایل نمی‌شد:
Named argument specifications must appear after all fixed arguments have been specified.
این محدودیت در C# 7.2 برطرف شده‌است؛ به این شرط که موقعیت پارامترها تغییری نکنند و پارامترها دقیقا در همانجایی که قرار است باشند، معرفی شوند.
در این حالت تمام فراخوانی‌های ذیل در C# 7.2 مجاز هستند:
Write(age: 20, name: "User 1", "T1");
Write(age: 21, "User 2", homeTown: "T2");
Write(age: 22, "User 3", "T3");
Write(23, name: "User 4", "T4");
مطالب
ساخت گزارش با استفاده از FastReport & Linq
یک روش ساده جهت ساخت گزارش به کمک FastReport استفاده از منبع داده ایجاد شده توسط Linq است. بعنوان نمونه در اینجا اطلاعات داخل یک فایل متنی (List.txt) ذخیره شده است. با استفاده از دستورات زبان Linq اطلاعات فایل متنی استخراج و داخل Query قرار گرفته است. یک نمونه از Report ایجاد و با استفاده از report.RegisterData منبع داده را به FastReport معرفی می‌کنیم. ابتدا از report.Design جهت طراحی گزارش استفاده و سپس با report.Load گزارش ساخته شده (در اینجا با نام List.frx ذخیره شده) را بارگذاری و توسط report.Show نمایش میدهیم 
محتوای فایل نمونه List.txt 


افزودن اسمبلی‌های مورد نیاز به مجموع Referencesها 

کد استفاده شده جهت طراحی گزارش 

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using FastReport;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            string[] List = System.IO.File.ReadAllLines("List.TXT");
            var Query = from list in List
                        let items = list.Split(',')
                        select new
                        {
                            Id = Convert.ToInt32(items[0]),
                            FName = items[1],
                            LName = items[2]
                        };

            using (Report report = new Report())
            {
                report.RegisterData(Query.ToList(), "myQuery");
                report.Design();
            }
        }
    }
}

نحوه شناسایی منبع و فیلدها در FastReport 

نمایش گزارش ذخیره شده در List.frx با استفاده از کد زیر

 report.Load("List.frx");

 report.Show();

خروجی گزارش ساخته شده

سورس برنامه نمونه

Linq_FastReport-sample.rar

مطالب
وی‍‍ژگی های پیشرفته ی AutoMapper - قسمت اول
در پست قبلی مقدمه‌ای داشتیم بر AutoMapper؛ مثالی که در اون پست عنوان شد ساده‌ترین و پرکاربردترین روش استفاده از AutoMapper هست بنام Flattening که در واقع از یک شیء کل به یک شیء کوچکتر می‌رسیم.
همانطور که در قسمت اول گفتم AutoMapper کارش رو بر اساس قراردادها انجام میده یا همون Convention Base. یکی از قردادهای AutoMapper، نگاشت براساس نام اعضای اون شی هست؛ مثلا در مثال قبلی FirstName در مبداء، به خاصیتی با همین نام نگاشت شد و ...

Projection
روش استفاده بعدی که به اون Projection (معنی فارسی خوب برا Projection چیه ؟) میگن برای مواقعی هست که اعضای یک شیء در مبداء، همتایی در مقصد ندارد (از نظر نام) و در واقع می‌خواهیم یک شیء رو به یک شیء دیگه تغییر شکل بدیم.
مدل زیر رو در نظر بگیرید:
  public class Person
    {
        public int Id { get; set; }

        public string FirstName { get; set; }       

        public string LastName { get; set; }

    }

  که قرار است به مدل PersonDTO نگاشت بشه.
  public class PersonDTO
    {
        public int Id { get; set; }

        public string Name { get; set; }

        public string Family { get; set; }

public string FullName { get; set; }

        public string Email { get; set; }
    }
همنطور که می‌بینید در مقصد خاصیت‌هایی داریم که در مبداء همتایی ندارند. برای نگاشت چنین اشیایی، از متد ForMember  استفاده و نگاشت‌های شی‌های موردنظر رو Custom می‌کنیم.
 Mapper.CreateMap<Person, PersonDTO>().ForMember(des => des.Name, op => op.MapFrom(src => src.FirstName)).
                ForMember(des => des.FullName, op => op.MapFrom(src => src.FirstName + " " + src.LastName)).ForMember(
                    des => des.Email, op => op.Ignore());
متد ForMember  از یک Action Delegate  برای کانفیگ کردن هر عضو استفاده میکنه. در مثال بالا ما از MapFrom برای اعمال نگاشت Custom  استفاده کردیم.
AutoMapper.IMappingExpression<TSource,TDestination>.ForMember(System.Linq.Expressions.Expression<System.Func<TDestination,object>>, System.Action<AutoMapper.IMemberConfigurationExpression<TSource>>)

نکته:برای تست کردن اینکه آیا نگاشت ما Exception  داره یا نه میتوان از متد زیر استفاده کرد.
Mapper.AssertConfigurationIsValid();

نکته
: همیشه موقع نگاشت، اعضای شیء مقصد برای AutoMapper  مهمن؛ مثلا در مثال بالا خاصیت LastName در مبداء به هیچ عضوی در مقصد نگاشت نشده (به صورت مستقیم) و این تولید Exception  نمیکنه ولی برعکس اون باعث تولید Exception میشه؛ مثلا اگه خاصیت Email  رو که در مبداء همتایی نداره رو کانفیگ نکنیم، باعث تولید Exception میشه.
 
نحوه استفاده
var person = new Person
                {
                    Id = 1,
                    FirstName = "Mohammad",
                    LastName = "Saheb",
                    
                };
var personDTO = Mapper.Map<Person, PersonDTO>(person);
خروجی به شکل زیر میشه


Collection
بعد از نوشتن کانفیگ نگاشت‌ها، بدون نیاز به تنظیمات خاصی میتونیم مجموعه ای از شی‌های مبداء رو به مقصد نگاشت کنیم. مجموعه‌های پشتیبانی شده به شرح زیرن.
IEnumerable, IEnumerable<T>, ICollection, ICollection<T>, IList, IList<T>, List<T>.
و البته آرایه ها.
مثال
var persons = new[]
                {
                    new Person { Id = 1, FirstName = "Mohammad", LastName = "Saheb" },
                    new Person { Id = 2, FirstName = "Babak", LastName = "Saheb" }
                };

var personDTOs = Mapper.Map<Person[], List<PersonDTO>>(persons);
خروجی به شکل زیر میشه

Nested Mappings
برای نگاشت کلاس‌های تو در تو از این روش استفاده می‌کنیم و ...
فرض کنید در کلاس Person خاصیتی از نوع Address داریم و در کلاس PersonDTO  خاصیتی از نوع AddressDTO.
public class Address
    {
        public string Ad1 { get; set; }
        public string Ad2 { get; set; }
    }


public class AddressDTO
    {
        public string Ad1 { get; set; }
        public string Ad2 { get; set; }
    }
برای نگاشت‌هایی از این دست باید تنظیمات نگاشت مربوط به نوع‌های تودرتو را صریحا معین کنیم.
Mapper.CreateMap<Address, AddressDTO>();
نکته: هرچند منطقی‌تر بنظر میرسه که تعریف نگاشت‌های داخلی ابتدا بیاد، ولی فرقی نمیکنه که تعریف این نگاشت قبل یا بعد از نگاشت اصلی باشه.
ادامه دارد...
مطالب
بررسی معادل‌های LINQ to Objects در TypeScript
اگر برنامه نویس NET. باشید، پس از مدتی کار با LINQ، در سایر زبان‌های دیگر نیز به دنبال این قابلیت فوق العاده‌ی functional یا تابعی خواهید گشت. در این مطلب، خلاصه‌ای از متدهای توکار جاوا اسکریپت را که می‌توانند معادل‌هایی برای متدهای LINQ to Objects دات نت باشند، بررسی خواهیم کرد.


تدارک ساختار ابتدایی این مطلب

در اینجا اینترفیسی را که بیانگر ساختار شیء شخص است، به صورت ذیل ایجاد می‌کنیم:
export interface Person {
  name: string;
  age: number;
}
سپس آرایه‌ای را بر اساس این شیء تدارک خواهیم دید:
export class LinqTestsComponent {

  people: Person[] = [
    { name: "User 4", age: 27 },
    { name: "User 5", age: 42 },
    { name: "User 6", age: 8 },
    { name: "User 1", age: 20 },
    { name: "User 2", age: 35 },
    { name: "User 3", age: 78 }
  ];

}
در ادامه تمام اعمال مدنظر را بر روی این آرایه انجام می‌دهیم.
همچنین سه متد ذیل را نیز برای لاگ کردن عنوان آزمایش، نمایش محتوای آرایه‌ی اصلی و نمایش نتیجه‌ی آزمایش، به این کلاس اضافه می‌کنیم:
  logTitle(title: string) {
    console.log(`%c${title}`, "background: #222; color: #bada55");
  }

  logOriginalArray() {
    console.log(`original this.people:${JSON.stringify(this.people)}`);
  }

  logResult(message: string, result: any) {
    console.log(`${message}:${JSON.stringify(result)}`);
  }


معادل متد Where در TypeScript

متد filter که جزو متدهای توکار ES5 است، می‌تواند معادلی برای متد Where، جهت فیلتر کردن عناصر بر اساس یک خاصیت، یا چندین خاصیت باشد:
  equivalentToWhere() {
    const youngerThan40 = this.people.filter(person => person.age < 40); // ECMAScript 5
    this.logResult("People younger than 40", youngerThan40);

    // Filtering on Multiple Criteria
    const youngsters = this.people.filter(
      person => person.age < 40 && person.name.toLocaleLowerCase().indexOf("user") !== -1);
    this.logResult("Users younger than 40", youngsters);
  }
با این خروجی
People younger than 40:[
{"name":"User 4","age":27},
{"name":"User 6","age":8},
{"name":"User 1","age":20},
{"name":"User 2","age":35}
]

Users younger than 40:[
{"name":"User 4","age":27},
{"name":"User 6","age":8},
{"name":"User 1","age":20},
{"name":"User 2","age":35}
]
در اینجا نحوه‌ی استفاده‌ی از arrow functions ES6 را نیز جهت ساده سازی تعریف callback متد filter مشاهده می‌کنید که نمایش آن بسیار شبیه به عبارات LINQ در دات نت است.


معادل متد Any در TypeScript

متد some که جزو متدهای توکار ES5 است، می‌تواند معادلی برای متد Any باشد. اگر یکی از عناصر آرایه، بر اساس شرط تعیین شده یافت شود، این متد مقدار true را باز می‌گرداند:
  equivalentToAny() {

    const anyUnder40 = this.people.some(person => person.age < 40); // ECMAScript 5
    this.logResult("Are any people under 40?", anyUnder40); // true

    // Filtering on Criteria Matching any Object Properties
    const filterBy = "user";
    const anyUsers = this.people.filter(person =>
      Object.keys(person).some(property => {
        let value = (<any>person)[property];
        if (typeof value === "string") {
          value = value.toLocaleLowerCase();
        }
        return value.toString().indexOf(filterBy) !== -1;
      })
    );
    this.logResult("anyUsers", anyUsers);
  }
با این خروجی:
Are any people under 40?:true
anyUsers:[
{"name":"User 4","age":27},
{"name":"User 5","age":42},
{"name":"User 6","age":8},
{"name":"User 1","age":20},
{"name":"User 2","age":35},
{"name":"User 3","age":78}
]
در مثال اول، بررسی شده‌است که آیا شخصی با سن کمتر از 40 در این لیست وجود دارد؟
در مثال دوم، جستجویی بر روی تمام خواص شیء شخص انجام شده‌است. در اینجا توسط متد Object.keys، لیست خواص شیء یافت شده‌است. سپس بر روی این لیست توسط متد some، بررسی شده‌است که آیا خاصیت رشته‌ای وجود دارد که مساوی عبارت filterBy باشد؟ حاصل این بررسی به عنوان شرط متد filter جهت بازگشت آرایه‌ی متناظری از اشخاص یافت شده، استفاده شده‌است.


معادل متد ‍Contains در TypeScript

متد includes که جزو متدهای توکار ES7 است، می‌تواند معادلی برای متد Contains باشد و کار آن بررسی وجود عنصری در یک لیست است:
  equivalentToContains() {

    const searchElement: Person = { name: "User 4", age: 27 };
    const containsUser4 = this.people.includes(searchElement); // ECMAScript 2016 = ECMAScript 7
    this.logResult("Contains searchElement", containsUser4); // false -> only compares by reference and not by value.

    const indexOfUser4 = this.people.indexOf(searchElement); // ECMAScript 5
    this.logResult("indexOfUser4", indexOfUser4); // -1 -> only compares by reference and not by value.

    const stringifiedObj = JSON.stringify(searchElement);
    const includesUser4 = this.people.some(person => JSON.stringify(person) === stringifiedObj);
    this.logResult("includesUser4", includesUser4); // true -> compares by by value.
  }
در اینجا باید دقت داشت که اگر آرایه‌ی مدنظر رشته‌ای و یا عددی باشد، متد includes نتیجه‌ی مطلوبی را بازگشت می‌دهد. اما چون در اینجا وجود یک شیء را در لیست اشخاص بررسی می‌کنیم، این مقایسه بر اساس ارجاع عناصر خواهد بود و نتیجه‌ی نهایی یافت شدن آن، منفی است (شیء searchElement هیچ ارجاعی را در آرایه‌ی اشخاص ندارد، هرچند ظاهر آن شبیه به یکی از عناصر آن است). حتی متد indexOf نیز به همین صورت عمل می‌کند.
یکی از روش‌های مقایسه‌ی بر اساس تمام مقادیر خواص یک شیء، استفاده از متد JSON.stringify است که اگر آن‌را با متد some ترکیب کنیم، می‌توان به نتیجه‌ی مطلوبی رسید:
Contains searchElement:false
indexOfUser4:-1
includesUser4:true


معادل متد ‍All در TypeScript

متد every که جزو متدهای توکار ES5 است، می‌تواند معادلی برای متد All باشد و کار آن بررسی صحت شرط اعمالی، بر روی تک تک عناصر لیست است. اگر این بررسی با موفقیت صورت گرفت، مقدار true را بازگشت می‌دهد:
  equivalentToAll() {
    const allUnder30 = this.people.every(person => person.age < 30); // ECMAScript 5
    this.logResult("Are all people under 30?", allUnder30); // false
  }
با این خروجی:
 Are all people under 30?:false


معادل متدهای First و FirstOrDefault در TypeScript

می‌توان از متدهای filter و یا find بومی ES5 و ES 6 برای شبیه سازی متدهای First  (یافتن اولین عنصر درخواستی در یک لیست) و  FirstOrDefault استفاده کرد:
  equivalentToFirstOrDefault() {
    const vahidOrDefault = this.people.filter(item => item.name === "Vahid")[0] || null; // ECMAScript 5
    this.logResult("vahidOrDefault", vahidOrDefault);

    const user1OrDefault = this.people.find(item => item.name === "User 1") || null; // ECMAScript 2015 = ECMAScript 6
    this.logResult("user1OrDefault", user1OrDefault);
  }
متد filter، در صورت برآورده نشدن شرط آن، یک آرایه‌ی خالی را بازگشت می‌دهد که مقدار [0] آن، undefined است. بنابراین ترکیب آن با null ||، سبب بازگشت نال، در صورت خالی بودن آرایه می‌شود؛ یا همان حالت OrDefault (یا بازگشت مقدار پیش فرض، یا نال در اینجا). متد find نیز در صورت نیافتن عنصر درخواستی، مقدار undefined را بازگشت می‌دهد.


معادل متد FindIndex در TypeScript

متد findIndex که جزو متدهای توکار ES6 است، می‌تواند معادلی برای متد FindIndex در جهت یافتن ایندکس عنصری در یک لیست، بر اساس شرط درخواستی، باشد.
  equivalentToFindIndex() {
    const index = this.people.findIndex(person => person.age === 8); // ECMAScript 2015 = ECMAScript 6
    this.logResult("index of the user with age 8", index)
  }
با این خروجی:
 index of the user with age 8:2


معادل متد Select در TypeScript

متد map که جزو متدهای توکار ES5 است، می‌تواند معادلی برای متد Select، برای تغییر شکل نهایی خروجی یک لیست باشد:
  equivalentToSelect() {
    const names = this.people.map(person => person.name); // ECMAScript 5
    this.logResult("Selected the names of people", names);
  }
برای مثال در اینجا در لیست اشخاص، تنها خاصیت name آن‌ها، انتخاب و بازگشت داده شده‌است:
 Selected the names of people:["User 4","User 5","User 6","User 1","User 2","User 3"]


معادل متد Aggregate در TypeScript

متد reduce که جزو متدهای توکار ES5 است، می‌تواند شبیه به متد Aggregate عمل کند و لیستی از عناصر را به یک مقدار کاهش دهد:
  equivalentToAggregate() {
    // ECMAScript 5
    const aggregate = this.people.reduce((person1, person2) => {
      return { name: "", age: person1.age + person2.age };
    });
    this.logResult("Aggregate age", aggregate.age); // { age: 210 }
  }
با این خروجی:
 Aggregate age:210


معادل متد ForEach در TypeScript

متد forEach که جزو متدهای توکار ES5 است، می‌تواند معادلی برای متد ForEach باشد که روشی functional برای پیمودن عناصر یک لیست است:
  equivalentToForEach() {
    // ECMAScript 5
    this.people.forEach(person => {
      this.logResult("person", person);
    });
  }
با این خروجی:
 person:{"name":"User 4","age":27}
 person:{"name":"User 5","age":42}
 person:{"name":"User 6","age":8}
 person:{"name":"User 1","age":20}
 person:{"name":"User 2","age":35}
 person:{"name":"User 3","age":78}


معادل متد OrderBy در TypeScript

متد sort که جزو متدهای توکار ES5 است، می‌تواند معادلی برای متد OrderBy باشد که جهت مرتب سازی عناصر یک لیست از آن استفاده می‌شود:
    // ECMAScript 5
    let orderedByAgeAscending = this.people.sort((person1, person2) => {
      const a = person1.age;
      const b = person2.age;
      return a > b ? 1 : -1;
    });
    this.logResult("Ordered by age ascending", orderedByAgeAscending);
متد sort یک callback را می‌پذیرد و هر بار دو آیتم در حال مرتب سازی را به آن ارسال می‌کند. در این حالت اگر خروجی این callback:
 - مساوی صفر باشد، تغییری را به وجود نمی‌آورد.
 - کمتر از صفر باشد، اولین عنصر را قبل از دومین عنصر قرار می‌دهد.
 - بیشتر از صفر باشد، دومین عنصر را پس از اولین عنصر قرار می‌دهد.

منطق مقایسه‌ی فوق را به صورت ذیل نیز می‌توان خلاصه کرد:
    orderedByAgeAscending = this.people.sort((person1, person2) => person1.age - person2.age);
    this.logResult("Ordered by age ascending", orderedByAgeAscending);
با این خروجی:
Ordered by age ascending:[
{"name":"User 6","age":8},
{"name":"User 1","age":20},
{"name":"User 4","age":27},
{"name":"User 2","age":35},
{"name":"User 5","age":42},
{"name":"User 3","age":78}
]
و یا اگر بخواهیم این لیست را بر اساس نام اشخاص مرتب سازی کنیم، به منطق ذیل خواهیم رسید:
    const orderedByName = this.people.sort((person1, person2) => {
      // name1.localeCompare(name2) // is case insensitive
      // or use toUpperCase() to ignore character casing
      const name1 = person1.name.toUpperCase();
      const name2 = person2.name.toUpperCase();
      return name1 > name2 ? 1 : -1;
    })
    this.logResult("Ordered by name", orderedByName);
    this.logOriginalArray();
با این خروجی:
Ordered by name:[
{"name":"User 1","age":20},
{"name":"User 2","age":35},
{"name":"User 3","age":78},
{"name":"User 4","age":27},
{"name":"User 5","age":42},
{"name":"User 6","age":8}
]

original this.people:[
{"name":"User 1","age":20},
{"name":"User 2","age":35},
{"name":"User 3","age":78},
{"name":"User 4","age":27},
{"name":"User 5","age":42},
{"name":"User 6","age":8}
]
نکته‌ی مهم: همانطور که ملاحظه می‌کنید، متد sort نه فقط یک خروجی مرتب شده را بازگشت داده‌است، بلکه اصل آرایه را نیز درجا مرتب سازی کرده‌است و ترتیب عناصر این آرایه، دیگر با آرایه‌ی قبلی و اصلی یکی نیست.


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

همانند LINQ، در اینجا نیز می‌توان متدهای فوق را به صورت زنجیروار بر روی یک لیست فراخوانی و اجرا کرد:
  chainFunctionCalls() {
    const namesOfPeopleOver30OrderedDesc =
      this.people
        .filter(person => person.age > 30)
        .map(person => person.name)
        .sort((name1, name2) => {
          // name1.localeCompare(name2) // is case insensitive
          // or use toUpperCase() to ignore character casing
          name1 = name1.toUpperCase();
          name2 = name2.toUpperCase();
          return name2 > name1 ? 1 : -1;
        });
    this.logResult("the names of all people over 30 ordered by name descending", namesOfPeopleOver30OrderedDesc);
  }
با این خروجی:
 the names of all people over 30 ordered by name descending:["User 5","User 3","User 2"]
در اینجا ابتدا اشخاص بالای 30 سال فیلتر شده‌اند. سپس فقط خاصیت رشته‌ای نام آن‌ها انتخاب شده‌است و در آخر این نام‌ها به صورت نزولی مرتب شده‌اند.


معادل متد Skip در TypeScript

متد splice که جزو متدهای توکار ES5 است، می‌تواند شبیه به متد Skip عمل کند. این متد آرایه‌ای را بازگشت می‌دهد که حاوی عناصری است که پس از تعداد ذکر شده، در آرایه‌ی اصلی وجود دارند:
  equivalentToSkip() {
    const skip2 = this.people.splice(2); // ECMAScript 5
    this.logResult("skip2 -> the deleted elements", skip2);
    this.logOriginalArray();
  }
با این خروجی:
skip2 -> the deleted elements:[
{"name":"User 3","age":78},
{"name":"User 4","age":27},
{"name":"User 5","age":42},
{"name":"User 6","age":8}
]

original this.people:[
{"name":"User 1","age":20},
{"name":"User 2","age":35}
]
کار واقعی متد splice، حذف عناصر باقیمانده‌ی در آرایه‌است و خروجی آن دقیقا لیست موارد حذف شده‌است. به همین جهت است که در نتیجه‌ی فوق، اکنون آرایه‌ی اصلی تنها دارای دو عضو باقیمانده است (و دیگر با آرایه‌ی اصلی و ابتدایی یکی نیست).
مطالب
intellisense دار نمودن ViewBag در ASP.NET MVC
در اینجا  و اینجا  با تفاوت‌های ViewData و ViewBag و TempData در ASP.NET MVC آشنا شدید. هدف ما در این مقاله intellisense  دار کردن شیء پویای ViewBag در فایل‌هاب cshtml می‌باشد که گاها در پروژها پیش می‌آید، برنامه نویس، لیستی را به صورت ViewBag به سمت View ارسال نماید.
 ViewBag :
 • یک نوع dynamic است (این نوع در c# 4 معرفی شده است).
• مانند ViewData برای ارسال اطلاعات از کنترلر به view استفاده می‌شود.
• مدت زمان اعتبار مقادیر آن تنها در request جاری است.
• اگر redirect ایی بین صفحات رخ دهد، مقدار آن null خواهد شد.
• به دلایل امنیتی باید قبل از استفاده، null بودن آن تست شود.
• برای استفاده‌ی از آن، cast نیاز نیست. بنابراین سریعتر عمل می‌کند.
در پوشه‌ی Models یک کلاس با نام Persons ایجاد شده که داری پراپرتی‌های زیر می‌باشد:
using System.Web;

namespace Intellisense.Models
{
    public class Persons
    {
        // کلید
        public int Id { get; set; }
        // نام
        public string FirstName { get; set; }
        // نام خانوادگی
        public string LastName { get; set; }
        // نام پدر
        public string FatherName { get; set; }
        // سن
        public int Age { get; set; }
        // شماره تلفن
        public int Mobile { get; set; }
        // آدرس
        public string Address { get; set; }
    }
}
حال نوبت به ایجاد یک اکشن و مقدار دهی ViewBag با لیستی از اشخاص و پاس دادن به سمت View است:
using System.Collections.Generic;
using System.Web.Mvc;
using Intellisense.Models;

namespace Intellisense.Controllers
{
    public class HomeController : Controller
    {
        // GET: Home
        public ActionResult Index()
        {
            // List of person
            var listOfPerson = new List<Persons>
            {
                new Persons() {Id = 1, FirstName = "Jone", LastName = "liy", FatherName = "Sobin", Age = 36, Mobile = +982015222, Address = "..."},
                new Persons() {Id = 2, FirstName = "kety", LastName = "sory", FatherName = "petter", Age = 19, Mobile = +962222155, Address = "..."},
            };
            // Set ViewBag.Persons data from listOfPerson
            ViewBag.Persons = listOfPerson;
            // Show and send ViewBag.Persons to view
            return View();
        }
    }
}
در View می‌توان به دو روش لیست ارسالی موجود در ViewBag.Persons فراخوانی نمود:
  1. استفاده از دات ( . ) 
  2. عمل Cast
در کد زیر نحوه‌ی استفاده از دات را مشاهده خواهید کرد. از معایب استفاده از این روش اشتباهات تایپی است که نام پراپرتی بعد از دات (.) قرار خواهد گرفت و همچنین intellisense برای آن فعال نیست.
@{
    ViewBag.Title = "ViewBag";
}
<table>
    <thead>
        <tr>
            <th>
                Id
            </th>
            <th>
                First Name
            </th>
            <th>
                Last Name
            </th>
            <th>
                Father Name
            </th>
            <th>
                Age
            </th>
            <th>
               Mobile
            </th>
            <th>
                Address
            </th>
        </tr>
    </thead>
    <tbody>
        @{foreach (var item in ViewBag.Persons)
            {
                <tr>
                    <td>
                        @item.Id
                    </td>
                    <td>
                        @item.FirstName
                    </td>
                    <td>
                        @item.LastName
                    </td>
                    <td>
                        @item.FatherName
                    </td>
                    <td>
                        @item.Age
                    </td>
                    <td>
                        @item.Mobile
                    </td>
                    <td>
                        @item.Address
                    </td>
                </tr>
            }
        }
    </tbody>
</table>

Cast:
با استفاده از کلمه کلیدی as عمل casting انجام پذیرفته است که در زیر، دو روش برای casting آورده شده است. در این حالت intellisense نیز فعال می‌گردد:
@using Intellisense.Models
@{
    ViewBag.Title = "ViewBag";
    // روش اول
    // پیشنهاد می‌شود که از روش اول استفاده شود
    // var listOfPerson = ViewBag.Persons as IEnumerable<Persons>;
    // روش دوم
    // var listOfPerson = (IEnumerable<Persons>)ViewBag.Persons;
    var listOfPerson = ViewBag.Persons as IEnumerable<Persons>;
}
<table>
    <thead>
        <tr>
            <th>
                Id
            </th>
            <th>
                First Name
            </th>
            <th>
                Last Name
            </th>
            <th>
                Father Name
            </th>
            <th>
                Age
            </th>
            <th>
               Mobile
            </th>
            <th>
                Address
            </th>
        </tr>
    </thead>
    <tbody>
        @{foreach (var item in listOfPerson)
            {
                <tr>
                    <td>
                        @item.Id
                    </td>
                    <td>
                        @item.FirstName
                    </td>
                    <td>
                        @item.LastName
                    </td>
                    <td>
                        @item.FatherName
                    </td>
                    <td>
                        @item.Age
                    </td>
                    <td>
                        @item.Mobile
                    </td>
                    <td>
                        @item.Address
                    </td>
                </tr>
            }
        }
    </tbody>
</table>
پروژه جاری را می‌توان از اینجا دانلود نمود.
مطالب
طراحی یک گرید با jQuery Ajax و ASP.NET MVC به همراه پیاده سازی عملیات CRUD

هدف، ارائه راه‌حلی برای نمایش جدولی اطلاعات، جستجو، مرتب سازی و صفحه بندی و همچنین انجام عملیات ثبت، ویرایش و حذف بر روی آنها به صورت Ajaxای در بخش back office نرم افزار می‌باشد.

پیش نیازها:

ایده کار به این شکل می‌باشد که برای نمایش اطلاعات به صورت جدولی با قابلیت‌های مذکور، لازم است یک اکشن Index برای نمایش اولیه و صفحه اول اطلاعات صفحه بندی شده و اکشن متدی به نام List برای پاسخ به درخواست‌های صفحه بندی، مرتب سازی، تغییر تعداد آیتم‌ها در هر صفحه و همچنین جستجو، داشته باشیم که این اکشن متد List، بعد از واکشی اطلاعات مورد نظر از منبع داده، آنها را به همراه اطلاعاتی که در کوئری استرینگ درخواست جاری وجود دارد در قالب یک PartialView به کلاینت ارسال کند.


ایجاد مدل‌های پایه

همانطور که در مقاله «طراحی و پیاده سازی ServiceLayer به همراه خودکارسازی Business Validationها» مطرح شد، برای پیاده سازی متدهای GetPagedList در ApplicationService‌ها از الگوی Request/Response استفاده می‌کنیم. برای این منظور واسط و کلاس‌های زیر را خواهیم داشت:

واسط IPagedQueryModel

    public interface IPagedQueryModel
    {
        int Page { get; set; }
        int PageSize { get; set; }

        /// <summary>
        ///     Expression of Sorting.
        /// </summary>
        /// <example>
        ///     Examples:
        ///     "Name_ASC"
        /// </example>
        string SortExpression { get; set; }
    }

این واسط قراردادی می‌باشد برای نوع و نام پارامترهایی که توسط کلاینت به سرور ارسال می‌شود. پراپرتی SortExpression آن، نام و ترتیب مرتب سازی را مشخص می‌کند؛ برای این منظور FieldName_ASC و FieldName_DESC به ترتیب برای حالات مرتب سازی صعودی و نزولی براساس FieldName مقدار دهی خواهد شد.

برای جلوگیری از تکرار این خصوصیات در مدل‌های کوئری مربوط به موجودیت‌ها، میتوان کلاس پایه‌ای به شکل زیر در نظر گرفت که پیاده ساز واسط بالا می‌باشد:

  public class PagedQueryModel : IPagedQueryModel, IShouldNormalize
    {
        public int Page { get; set; }
        public int PageSize { get; set; }

        /// <summary>
        ///     Expression of Sorting.
        /// </summary>
        /// <example>
        ///     Examples:
        ///     "Name_ASC"
        /// </example>
        public string SortExpression { get; set; }

        public virtual void Normalize()
        {
            if (Page < 1)
                Page = 1;

            if (PageSize < 1)
                PageSize = 10;

            if (SortExpression.IsEmpty())
                SortExpression = "Id_DESC";
        }
    }

مدل بالا علاوه بر پیاده سازی واسط IPagedQueryModel، پیاده ساز واسط IShouldNormalize نیز می‌باشد؛ دلیل وجود چنین واسطی در مقاله «طراحی و پیاده سازی ServiceLayer به همراه خودکارسازی Business Validationها» توضیح داده شده است:

پیاده سازی واسط IShouldNormalize باعث خواهد شد که قبل از اجرای خود متد، این نوع پارامترها با استفاده از یک Interceptor شناسایی شده و متد Normalize آنها اجرا شود.

کلاس PagedQueryResult

    public class PagedQueryResult<TModel>
    {
        public PagedQueryResult()
        {
            Items = new List<TModel>();
        }
        public IEnumerable<TModel> Items { get; set; }
        public long TotalCount { get; set; }
    }

دلیل وجود کلاس بالا در مقاله «طراحی یک گرید با Angular و ASP.NET Core - قسمت اول - پیاده سازی سمت سرور» توضیح داده شده است:

عموما ساختار اطلاعات صفحه بندی شده، شامل تعداد کل آیتم‌های تمام صفحات (خاصیت TotalItems) و تنها اطلاعات ردیف‌های صفحه‌ی جاری درخواستی (خاصیت Items) است و چون در اینجا این Items از هر نوعی می‌تواند باشد، بهتر است آن‌را جنریک تعریف کنیم.

کلاس PagedListModel

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

    public class PagedListModel<TModel>
    {
        public IPagedQueryModel Query { get; set; }

        public PagedQueryResult<TModel> Result { get; set; }
    }

پراپرتی Query در برگیرنده پارامتر ورودی اکشن متد List می‌باشد که پراپرتی‌های آن با مقادیر موجود در کوئری استرینگ درخواست جاری مقدار دهی شده‌اند؛ البته بدون وجود کلاس بالا نیز به کمک ViewBag می‌شود این اطلاعات ترکیبی را به ویو ارسال کرد که پیشنهاد نمی‌شود.


متد GetPagedListAsync موجود در CrudApplicationService

    public abstract class CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel,
        TPagedQueryModel, TDynamicQueryModel> : ApplicationService,
        ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, TPagedQueryModel, TDynamicQueryModel>
        where TEntity : Entity, new()
        where TCreateModel : class
        where TEditModel : class, IModel
        where TModel : class, IModel
        where TDeleteModel : class, IModel
        where TPagedQueryModel : PagedQueryModel, new()
        where TDynamicQueryModel : DynamicQueryModel

    {

        #region Properties

        protected IQueryable<TEntity> UnTrackedEntitySet => EntitySet.AsNoTracking();
        public IUnitOfWork UnitOfWork { get; set; }
        public IMapper Mapper { get; set; }
        protected IDbSet<TEntity> EntitySet => UnitOfWork.Set<TEntity>();

        #endregion

        #region ICrudApplicationService Members

        #region Methods

        public virtual async Task<PagedQueryResult<TModel>> GetPagedListAsync(TPagedQueryModel model)
        {
            Guard.ArgumentNotNull(model, nameof(model));

            var query = ApplyFiltering(model);

            var totalCount = await query.LongCountAsync().ConfigureAwait(false);

            var result = query.ProjectTo<TModel>(Mapper.ConfigurationProvider);

            result = result.ApplySorting(model);
            result = result.ApplyPaging(model);

            return new PagedQueryResult<TModel>
            {
                Items = await result.ToListAsync().ConfigureAwait(false),
                TotalCount = totalCount
            };
        }
        #endregion

        #endregion

        #region Protected Methods

        /// <summary>
        ///     Apply Filtering To GetPagedList and GetPagedListAsync
        /// </summary>
        /// <param name="model"></param>
        /// <returns></returns>
        protected virtual IQueryable<TEntity> ApplyFiltering(TPagedQueryModel model)
        {
            Guard.ArgumentNotNull(model, nameof(model));

            return UnTrackedEntitySet;
        }
        #endregion
    }

در بدنه این متد، ابتدا عملیات جستجو توسط متد ApplyFiltering انجام می‌شود. این متد به صورت پیش فرض هیچ شرطی را بر روی کوئری ارسالی به منبع داده اعمال نمی‌کند؛ مگر اینکه توسط زیر کلاس‌ها بازنویسی شود و فیلترهای مورد نیاز اعمال شوند. سپس تعداد کل آیتم‌های فیلتر شده محاسبه شده و بعد از عملیات Projection، مرتب سازی و صفحه بندی انجام می‌گیرد. برای مباحث مرتب سازی و صفحه بندی از دو متد زیر کمک گرفته شده‌است:

    public static class QueryableExtensions
    {
        public static IQueryable<TModel> ApplySorting<TModel>(this IQueryable<TModel> query, IPagedQueryModel request)
        {
            Guard.ArgumentNotNull(request, nameof(request));
            Guard.ArgumentNotNull(query, nameof(query));

            return query.OrderBy(request.SortExpression.Replace('_', ' '));
        }

        public static IQueryable<TModel> ApplyPaging<TModel>(this IQueryable<TModel> query, IPagedQueryModel request)
        {
            Guard.ArgumentNotNull(request, nameof(request));
            Guard.ArgumentNotNull(query, nameof(query));

            return request != null
                ? query.Page((request.Page - 1) * request.PageSize, request.PageSize)
                : query;
        }
    }

به منظور مرتب سازی از کتابخانه  System.Liq.Dynamic کمک گرفته شده‌است.

نکته: مشخص است که این روش، وابستگی به وجود متد GetPagedListAsync ندارد و صرفا برای تشریح ارتباط مطالبی که قبلا منتشر شده بود، مطرح شد.


پیاده سازی اکشن متدهای Index و List

public partial class RolesController : BaseController
{
    #region Fields
        private readonly IRoleService _service;
        private readonly ILookupService _lookupService;

        #endregion

    #region Constractor
        public RolesController(IRoleService service,  ILookupService lookupService)
        {
            Guard.ArgumentNotNull(service, nameof(service));
            Guard.ArgumentNotNull(lookupService, nameof(lookupService));

            _service = service;
            _lookupService = lookupService;
        }
        #endregion

    #region Index / List
    [HttpGet]
    public virtual async Task<ActionResult> Index()
    {
        var query = new RolePagedQueryModel();
        var result = await _service.GetPagedListAsync(query).ConfigureAwait(false);

        var pagedList = new PagedListModel<RoleModel>
        {
            Query = query,
            Result = result
        };

        var model = new RoleIndexViewModel
        {
            PagedListModel = pagedList,
            Permissions = _lookupService.GetPermissions()
        };
        return View(model);
    }
    [HttpGet, AjaxOnly, NoOutputCache]
    public virtual async Task<ActionResult> List(RolePagedQueryModel query)
    {
        var result = await _service.GetPagedListAsync(query).ConfigureAwait(false);

        var model = new PagedListModel<RoleModel>
        {
            Query = query,
            Result = result
        };

        return PartialView(MVC.Administration.Roles.Views._List, model);
    }
    #endregion
}

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

    public class RolePagedQueryModel : PagedQueryModel
    {
        public string Name { get; set; }
        public string Permission { get; set; }
    }

در این مورد خاص لازم است لیست دسترسی‌های موجود درسیستم به صورت لیستی برای انتخاب در فرم جستجو مهیا باشد. فرم جستجو در ویو مربوط به اکشن Index قرار می‌گیرد و قرار نیست به همراه پارشال ویو List_ در هر درخواستی از سرور دریافت شود. لذا لازم است مدلی برای ویو Index در نظر بگیریم که به شکل زیر می‌باشد:

    public class RoleIndexViewModel
    {   
        public RoleIndexViewModel()
        {
            Permissions = new List<LookupItem>();
        }
        public IReadOnlyList<LookupItem> Permissions { get; set; }
        public PagedListModel<RoleModel> PagedListModel { get; set; }
    }

پراپرتی PagedListModel در برگیرنده اطلاعات مربوط به نمایش اولیه جدول اطلاعات می‌باشد و پراپرتی Permissions لیست دسترسی‌های موجود درسیستم را به ویو منتقل خواهد کرد. اگر ویو ایندکس شما به داده اضافه ای نیاز ندارد، از ایجاد مدل بالا صرف نظر کنید.


ویو Index.cshtml

@model RoleIndexViewModel

@{
    ViewBag.Title = L("Administration.Views.Role.Index.Title");
    ViewBag.ActiveMenu = AdministrationMenuNames.RoleManagement;
}

<div class="row">
    <div class="col-md-12">
        <div id="filterPanel" class="panel-collapse collapse" role="tabpanel" aria-labelledby="filterPanel">
            <div class="panel panel-default margin-bottom-5">

                <div class="panel-body">
                    @using (Ajax.BeginForm(MVC.Administration.Roles.List(),
new AjaxOptions { UpdateTargetId = "RolesList", HttpMethod = "GET" }, new { id = "filterForm", data_submit_on_reset = "true" }))
                    {
                        <div class="row">
                            <div class="col-md-3">
                                <input type="text" name="Name" class="form-control" value="" placeholder="@L("Administration.Role.Fields.Name")" />
                            </div>
                            <div class="col-md-3">
                                @Html.DropDownList("Permission", Model.Permissions.ToSelectListItems(), L("Administration.Views.Role.FilterBy.Permission"),new {@class="form-control"})
                            </div>
                            <div class="col-md-3">

                                <button type="submit"
                                        role="button"
                                        class="btn btn-info">
                                    @L("Commands.Filter")
                                </button>
                                <button type="reset"
                                        role="button"
                                        class="btn btn-default">
                                    <i class="fa fa-close"></i>
                                    @L("Commands.Reset")
                                </button>
                            </div>
                        </div>
                    }
                </div>
            </div>
        </div>
    </div>
</div>

<div class="row">
    <div class="col-md-12" id="RolesList">
        @{Html.RenderPartial(MVC.Administration.Roles.Views._List, Model.PagedListModel);}
    </div>
</div>

فرم جستجو باید دارای ویژگی data_submit_on_reset با مقدار "true" باشد. به منظور پاکسازی فرم جستجو و ارسال درخواست جستجو با فرمی خالی از داده، برای بازگشت به حالت اولیه از تکه کد زیر استفاده خواهد شد:

  $(document).on("reset", "form[data-submit-on-reset]",
            function () {
                var form = this;
                setTimeout(function () {
                    $(form).submit();
                });
            });

در ادامه پارشال ویو List_ با داده ارسالی به ویو Index، رندر شده و کار نمایش اولیه اطلاعات به صورت جدولی به اتمام می‌رسد.


پارشال ویو List.cshtml_

@model PagedListModel<RoleModel>
@{
    Layout = null;
    var rowNumber = (Model.Query.Page - 1) * Model.Query.PageSize + 1;
    var refreshUrl = Url.Action(MVC.Administration.Roles.List().AddRouteValues(new RouteValueDictionary(Model.Query.ToDictionary())));
}

<div class="panel panel-default margin-bottom-5">
    <table class="table table-bordered table-hover" id="RolesListTable" data-ajax-refresh-url="@refreshUrl" data-ajax-refresh-update="#RolesList">
        <thead>
            <tr>
                <th style="width: 5%;">
                    #
                </th>
                <th class="col-md-3 sortable">
                    @Html.SortableColumn("Name", L("Administration.Role.Fields.Name"), Model.Query, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))
                </th>
                <th class="col-md-3 sortable">
                    @Html.SortableColumn("DisplayName", L("Administration.Role.Fields.DisplayName"), Model.Query, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))
                </th>
                <th class="col-md-3 sortable">
                    @Html.SortableColumn("IsDefault", L("Administration.Role.Fields.IsDefault"), Model.Query, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))
                </th>
               
                <th style="width: 5%;"></th>
            </tr>
        </thead>

        <tbody>
           @foreach (var role in Model.Result.Items)
            {
                <tr>
                    <td>@(rowNumber++.ToPersianNumbers())</td>
                    <td>@role.Name</td>
                    <td>@role.DisplayName</td>
                    <td class="text-center">@Html.DisplayFor(a => role.IsDefault)</td>
                    <td class="text-center operations">
                      
                        <div class="btn-group">

                            <span class="fa fa-ellipsis-h dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
                            <ul class="dropdown-menu dropdown-menu-left">
                                <li>
                                    <a href="#"
                                       role="button"
                                       data-ajax="true"
                                       data-ajax-method="GET"
                                       data-ajax-update="#main-modal div.modal-content"
                                       data-ajax-url="@Url.Action(MVC.Administration.Roles.Edit(role.Id))"
                                       data-toggle="modal"
                                       data-target="#main-modal">
                                        <i class="fa fa-pencil"></i>
                                        @L("Commands.Edit")
                                    </a>
                                </li>
                                <li>
                                    <a href="#"
                                       role="button"
                                       id="delete-@role.Id"
                                       data-delete-url="@Url.Action(MVC.Administration.Roles.Delete())"
                                       data-delete-model='{"Id":"@role.Id","RowVersion":"@Convert.ToBase64String(role.RowVersion)"}'>
                                        <i class="fa fa-trash"></i>
                                        @L("Commands.Delete")
                                    </a>
                                </li>
                            </ul>
                        </div>
                    </td>
                </tr>
            }
        </tbody>
    </table>

</div>

<div class="row">
    <div class="col-md-8">
        @Html.Pager(Model, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))
        @Html.PageSize("RolesList", Model.Query, routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)), new { @class = "margin-right-5" }, "filterForm")
        @Html.Refresh("RolesList", Model.Query, routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)), L("Commands.Refresh"))
    </div>
</div>

به ترتیب  فایل بالا را بررسی می‌کنیم:

    var refreshUrl = Url.Action(MVC.Administration.Roles.List().AddRouteValues(new RouteValueDictionary(Model.Query.ToDictionary())));

refreshUrl برای ارسال درخواست به اکشن متد List در نظر گرفته شده‌است که در کوئری استرینگ مربوط به خود، اطلاعاتی (مرتب سازی، شماره صفحه، اطلاعات جستجو و همچنین تعداد آیتم‌های موجود در هر صفحه) را دارد که حالت فعلی گرید را می‌توانیم دوباره از سرور درخواست کنیم.

<table class="table table-bordered table-hover" id="RolesListTable" data-ajax-refresh-url="@refreshUrl" data-ajax-refresh-update="#RolesList">

دو ویژگی data-ajax-refresh-url و data-ajax-refresh-update برای جدولی که لازم است عملیات CRUD را پشتیبانی کند، لازم می‌باشد. در قسمت دوم به استفاده از این دو ویژگی در هنگام عملیات ثبت، ویرایش و حذف خواهیم پرداخت.

<th class="col-md-3 sortable">
    @Html.SortableColumn("Name", L("Administration.Role.Fields.Name"), Model.Query, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))
</th>

ستونی که امکان مرتب سازی را دارد باید th آن، کلاس sortable را داشته باشد. همچنین باید از هلپری که پیاده سازی آن را در ادامه خواهیم دید، استفاده کنیم. این هلپر، نام فیلد، عنوان ستون، مدل Query و همچین یک urlFactory را در قالب یک ‎Func<RouteValueDictionary,string>‎ دریافت می‌کند.


پیاده سازی هلپر SortableColumn

        public static MvcHtmlString SortableColumn(this HtmlHelper html, string columnName,
            string columnDisplayName, IPagedQueryModel queryModel, string updateTargetId, Func<RouteValueDictionary, string> urlFactory)
        {
            var dictionary = queryModel.ToDictionary();

            var routeValueDictionary = new RouteValueDictionary(dictionary)
            {
                ["SortExpression"] = !queryModel.SortExpression.StartsWith(columnName)
                    ? $"{columnName}_DESC" : queryModel.SortExpression.EndsWith("DESC")
                        ? $"{columnName}_ASC" : queryModel.SortExpression.EndsWith("ASC")
                            ? string.Empty : $"{columnName}_DESC"
            };

            var url = urlFactory(routeValueDictionary);

            var aTag = new TagBuilder("a");
            aTag.Attributes.Add("href", "#");
            aTag.Attributes.Add("data-ajax", "true");
            aTag.Attributes.Add("data-ajax-method", "GET");
            aTag.Attributes.Add("data-ajax-update", updateTargetId.StartsWith("#") ? updateTargetId : $"#{updateTargetId}");
            aTag.Attributes.Add("data-ajax-url", url);
            aTag.InnerHtml = columnDisplayName;

            var iconCssClass = !queryModel.SortExpression.StartsWith(columnName)
                ? "fa-sort"
                : queryModel.SortExpression.EndsWith("DESC")
                    ? "fa-sort-down"
                    : "fa-sort-up";

            var iTag = new TagBuilder("i");
            iTag.AddCssClass($"fa {iconCssClass}");

            return new MvcHtmlString($"{aTag}\n{iTag}");
        }

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

public static IDictionary<string, object> ToDictionary(this object source)
{
    return source.ToDictionary<object>();
}

public static IDictionary<string, T> ToDictionary<T>(this object source)
{
    if (source == null)
        throw new ArgumentNullException(nameof(source));

    var dictionary = new Dictionary<string, T>();

    foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(source))
    {
        AddPropertyToDictionary(property, source, dictionary);
    }
    return dictionary;
}

private static void AddPropertyToDictionary<T>(PropertyDescriptor property, object source,
    IDictionary<string, T> dictionary)
{
    var value = property.GetValue(source);

    var items = value as IEnumerable;

    if (items != null && !(items is string))
    {
        var i = 0;
        foreach (var item in items)
        {
            dictionary.Add($"{property.Name}[{i++}]", (T)item);
        }
    }
    else if (value is T)
    {
        dictionary.Add(property.Name, (T)value);
    }

}

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

در ادامه پیاده سازی هلپر SortableColumn، از دیکشنری حاصل، یک وهله از RouteValueDictionary ساخته می‌شود. در زمان رندر شدن PartialView لازم است مشخص شود که برای دفعه بعدی که بر روی این ستون کلیک می‌شود، باید چه مقداری با پارامتر SortExpression موجود در کوئری استرینگ ارسال شود. از این جهت برای پشتیبانی ستون، از حالت‌های مرتب سازی صعودی، نزولی و برگشت به حالت اولیه بدون مرتب سازی، کد زیر را خواهیم داشت:

var routeValueDictionary = new RouteValueDictionary(dictionary)
{
    ["SortExpression"] = !queryModel.SortExpression.StartsWith(columnName)
        ? $"{columnName}_DESC" : queryModel.SortExpression.EndsWith("DESC")
            ? $"{columnName}_ASC" : queryModel.SortExpression.EndsWith("ASC")
                ? string.Empty : $"{columnName}_DESC"
};

در ادامه urlFactory با routeValueDictionary حاصل، Invoke می‌شود تا url نهایی برای مرتب سازی‌های بعدی را  از طریق یک لینک تزئین شده با data اتریبیوت‌های Unobtrusive Ajax در th مربوطه قرار دهیم.

برای مباحث صفحه بندی، بارگزاری مجدد و تغییر تعداد آیتم‌ها در هر صفحه، از سه هلپر زیر کمک خواهیم گرفت:

<div class="row">
    <div class="col-md-8">
        @Html.Pager(Model, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))
        @Html.PageSize("RolesList", Model.Query, routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)), new { @class = "margin-right-5" }, "filterForm")
        @Html.Refresh("RolesList", Model.Query, routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)), L("Commands.Refresh"))
    </div>
</div>


پیاده سازی هلپر Pager

public static MvcHtmlString Pager<TModel>(this HtmlHelper html, PagedListModel<TModel> model,
        string updateTargetId, Func<RouteValueDictionary, string> urlFactory)
{
    return html.PagedListPager(
        new StaticPagedList<TModel>(model.Result.Items, model.Query.Page, model.Query.PageSize,
            (int)model.Result.TotalCount), page =>
       {
           var dictionary = model.Query.ToDictionary();
           var routeValueDictionary = new RouteValueDictionary(dictionary) { ["Page"] = page };
           return urlFactory(routeValueDictionary);
       }, PagedListRenderOptions.EnableUnobtrusiveAjaxReplacing(
            new PagedListRenderOptions
            {
                DisplayLinkToFirstPage = PagedListDisplayMode.Always,
                DisplayLinkToLastPage = PagedListDisplayMode.Always,
                DisplayLinkToPreviousPage = PagedListDisplayMode.Always,
                DisplayLinkToNextPage = PagedListDisplayMode.Always,
                MaximumPageNumbersToDisplay = 6,
                DisplayItemSliceAndTotal = true,
                DisplayEllipsesWhenNotShowingAllPageNumbers = true,
                ItemSliceAndTotalFormat = $"تعداد کل: {model.Result.TotalCount.ToPersianNumbers()}",
                FunctionToDisplayEachPageNumber = page => page.ToPersianNumbers(),
            },
            new AjaxOptions
            {
                AllowCache = false,
                HttpMethod = "GET",
                InsertionMode = InsertionMode.Replace,
                UpdateTargetId = updateTargetId
            }));
}

در متد بالا از کتابخانه PagedList.Mvc استفاده شده‌است. یکی از overload‌های متد PagedListPager آن، یک پارامتر از نوع Func<int, string>‎ به نام generatePageUrl را دریافت می‌کند که امکان شخصی سازی فرآیند تولید لینک به صفحات بعدی و قبلی را به ما می‌دهد. ما نیز از این امکان برای افزودن اطلاعات موجود در مدل Query، به کوئری استرینگ لینک‌های تولیدی استفاده کردیم و صرفا برای لینک‌های ایجادی لازم بود مقادیر پارامتر Page موجود در کوئری استرینگ تغییر کند که در کد بالا مشخص می‌باشد.


پیاده سازی هلپر PageSize

public static MvcHtmlString PageSize(this HtmlHelper html, string updateTargetId, IPagedQueryModel queryModel, Func<RouteValueDictionary, string> urlFactory, object htmlAttributes = null, string filterFormId = null, params int[] numbers)
{
    if (numbers.Length == 0)
        numbers = new[] { 10, 20, 30, 50, 100 };

    var dictionary = queryModel.ToDictionary();

    var routeValueDictionary = new RouteValueDictionary(dictionary)
    {
        [nameof(IPagedQueryModel.Page)] = 1
    };
    routeValueDictionary.Remove(nameof(IPagedQueryModel.PageSize));

    var url = urlFactory(routeValueDictionary);

    var formTag = new TagBuilder("form");
    formTag.Attributes.Add("action", url);
    formTag.Attributes.Add("method", "GET");
    formTag.Attributes.Add("data-ajax", "true");
    formTag.Attributes.Add("data-ajax-method", "GET");
    formTag.Attributes.Add("data-ajax-update", updateTargetId.StartsWith("#") ? updateTargetId : $"#{updateTargetId}");
    formTag.Attributes.Add("data-ajax-url", url);

    if (htmlAttributes != null)
        formTag.MergeAttributes(HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));

    formTag.AddCssClass("form-inline inline");

    var items = numbers.Select(number =>
        new SelectListItem
        {
            Value = number.ToString(),
            Text = number.ToString().ToPersianNumbers(),
            Selected = queryModel.PageSize == number
        });

    formTag.InnerHtml = html.DropDownList(nameof(IPagedQueryModel.PageSize), items, new { @class = "form-control page-size", onchange = "$(this.form).submit();" }).ToString();

    if (filterFormId.IsEmpty()) return new MvcHtmlString($"{formTag}");

    // ReSharper disable once MustUseReturnValue
    var scriptBlock = $"<script type=\"text/javascript\"> if(window.jQuery){{$('form#{filterFormId}').find('input[name=\"{nameof(IPagedQueryModel.PageSize)}\"]').remove();\n $('form#{filterFormId}').append(\"<input type='hidden' name='{nameof(IPagedQueryModel.PageSize)}' value='{queryModel.PageSize}'/>\")}}</script>";

    return new MvcHtmlString($"{formTag}\n{scriptBlock}");
}

ایده کار به این صورت است که یک المنت select، درون یک المنت form قرار می‌گیرد و در زمان change آن، فرم مربوطه submit می‌شود.

    formTag.InnerHtml = html.DropDownList(nameof(IPagedQueryModel.PageSize), items, new { @class = "form-control page-size", onchange = "$(this.form).submit();" }).ToString();

در زمان تغییر تعداد نمایشی آیتم‌ها در هر صفحه، لازم است حالت فعلی گرید حفظ شود و صرفا پارامتر Page ریست شود.


نکته مهم: در این طراحی اگر فرم جستجویی دارید، در زمان جستجو هیچیک از پارامتر‌های مربوط به صفحه بندی و مرتب سازی به سرور ارسال نخواهند شد (در واقع ریست می‌شوند) و کافیست یک درخواست GET معمولی با ارسال محتویات فرم به سرور صورت گیرد؛ ولی لازم است PageSize تنظیم شده، در زمان اعمال فیلتر نیز به سرور ارسال شود. از این جهت اسکریپتی برای ایجاد یک input مخفی در فرم جستجو نیز هنگام رندر شدن PartialView در صفحه تزریق می‌شود.

  var scriptBlock = $"<script type=\"text/javascript\"> if(window.jQuery){{$('form#{filterFormId}').find('input[name=\"{nameof(IPagedQueryModel.PageSize)}\"]').remove();\n $('form#{filterFormId}').append(\"<input type='hidden' name='{nameof(IPagedQueryModel.PageSize)}' value='{queryModel.PageSize}'/>\")}}</script>";


پیاده سازی هلپر Refresh

public static MvcHtmlString Refresh(this HtmlHelper html, string updateTargetId, IPagedQueryModel queryModel,
    Func<RouteValueDictionary, string> urlFactory, string label = null)
{
    var dictionary = queryModel.ToDictionary();

    var routeValueDictionary = new RouteValueDictionary(dictionary);

    var url = urlFactory(routeValueDictionary);

    var aTag = new TagBuilder("a");
    aTag.Attributes.Add("href", "#");
    aTag.Attributes.Add("role", "button");
    aTag.Attributes.Add("data-ajax", "true");
    aTag.Attributes.Add("data-ajax-method", "GET");
    aTag.Attributes.Add("data-ajax-update", updateTargetId.StartsWith("#") ? updateTargetId : $"#{updateTargetId}");
    aTag.Attributes.Add("data-ajax-url", url);
    aTag.AddCssClass("btn btn-default");

    var iTag = new TagBuilder("i");
    iTag.AddCssClass("fa fa-refresh");

    aTag.InnerHtml = $"{iTag} {label}";

    return new MvcHtmlString(aTag.ToString());
}

متد بالا نیز به مانند refreshUrl که پیشتر مطرح شد، برای بارگزاری مجدد حالت فعلی گرید استفاده می‌شود و از این جهت است که مقادیر مربوط به کلیدهای routeValueDictionary  را تغییر نداده‌ایم.


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

در قسمت دوم، به پیاده سازی عملیات ثبت، ویرایش و حذف برپایه مودال‌های بوت استرپ و افزونه Unobtrusive Ajax خواهیم پرداخت.
کدهای کامل قسمت جاری، بعد از انتشار قسمت دوم، در مخزن گیت هاب شخصی قرار خواهد گرفت.

مطالب
سعی مجدد خودکار درخواست‌های با شکست مواجه شده در برنامه‌های Angular
در دنیای واقعی، تمام درخواست‌های HTTP ارسالی به سمت سرور، با موفقیت به آن نمی‌رسند. ممکن است در یک لحظه سرور در دسترس نباشد. در لحظه‌ای دیگر آنقدر بار آن بالا باشد که نتواند درخواست شما را پردازش کند و یا ممکن است درست در لحظه‌ای که توکن دسترسی به برنامه در حال به روز رسانی است، درخواست دیگری به سمت سرور ارسال شده باشد که حتما برگشت خواهد خورد؛ چون حاوی توکن جدید صادر شده نیست. در تمام این موارد ضرورت تکرار و سعی مجدد درخواست‌های شکست خورده وجود دارد. برای مدیریت این مساله در برنامه‌های Angular می‌توان از امکانات توکار کتابخانه‌ی RxJS به همراه آن کمک گرفت.


سعی مجدد خودکار درخواست‌ها توسط کتابخانه‌ی RxJS

با استفاده از عملگر retry می‌توان به صورت خودکار، درخواست‌های شکست خورده را به تعداد باری که مشخص می‌شود، تکرار کرد:
import { Observable } from "rxjs";
import { catchError, map, retry } from "rxjs/operators";

postEmployeeForm(employee: Employee): Observable<Employee> {
    const headers = new HttpHeaders({ "Content-Type": "application/json" });
    return this.http
      .post(this.baseUrl, employee, { headers: headers })
      .pipe(
        map((response: any) => response["fields"] || {}),
        catchError(this.handleError),
        retry(3)
      );
  }
در اینجا اگر درخواست اول با شکست مواجه شود، اصل آن‌را سه بار دیگر به سمت سرور ارسال می‌کند (البته در صورت بروز و دریافت خطای مجدد).
مشکل این روش در عدم وجود مکثی بین درخواست‌ها است. در اینجا تمام درخواست‌های سعی مجدد، بلافاصله به سمت سرور ارسال می‌شوند. همچنین نمی‌توان مشخص کرد که اگر مثلا خطای timeout وجود داشت، اینکار را تکرار کن و نه برای سایر حالات.


سفارشی سازی سعی مجدد خودکار درخواست‌ها، توسط کتابخانه‌ی RxJS

برای اینکه بتوان کنترل بیشتری را بر روی سعی‌های مجدد انجام شده داشت، می‌توان از عملگر retryWhen بجای retry استفاده کرد:
import { Observable, of, throwError as observableThrowError, throwError } from "rxjs";
import { catchError, delay, map, retryWhen, take } from "rxjs/operators";

postEmployeeForm(employee: Employee): Observable<Employee> {
    const headers = new HttpHeaders({ "Content-Type": "application/json" });
    return this.http
      .post(this.baseUrl, employee, { headers: headers })
      .pipe(
        map((response: any) => response["fields"] || {}),
        retryWhen(errors => errors.pipe(
          delay(1000),
          take(3)
        )),
        catchError(this.handleError)
      );
  }
در اینجا توسط عملگر retryWhen، کار سفارشی سازی سعی‌های مجدد درخواست‌های شکست خورده انجام شده‌است. در این مثال پس از هر درخواست مجدد، 1000ms صبر شده و سپس درخواست دیگری درصورت وجود خطا، به سمت سرور تا حداکثر 3 بار، ارسال می‌شود.

در ادامه اگر بخواهیم صرفا به خطاهای خاصی واکنش نشان دهیم می‌توان به صورت زیر عمل کرد:
import { Observable, of, throwError as observableThrowError, throwError } from "rxjs";
import { catchError, delay, map, mergeMap, retryWhen, take } from "rxjs/operators";

postEmployeeForm(employee: Employee): Observable<Employee> {
    const headers = new HttpHeaders({ "Content-Type": "application/json" });
    return this.http
      .post(this.baseUrl, employee, { headers: headers })
      .pipe(
        map((response: any) => response["fields"] || {}),
        retryWhen(errors => errors.pipe(
          mergeMap((error: HttpErrorResponse, retryAttempt: number) => {
            if (retryAttempt === 3 - 1) {
              console.log(`HTTP call failed after 3 retries.`);
              return throwError(error); // no retry
            }
            switch (error.status) {
              case 400:
              case 404:
                return throwError(error); // no retry
            }
            return of(error); // retry
          }),
          delay(1000),
          take(3)
        )),
        catchError(this.handleError)
      );
  }
در اینجا با اضافه شدن یک mergeMap، پیش از ارسال درخواست مجدد، به اطلاعات خطای رسیده‌ی از سمت سرور دسترسی خواهیم داشت. همچنین پارامتر دوم mergeMap، شماره سعی جاری را نیز بر می‌گرداند.
در داخل mergeMap اگر یک Observable معمولی بازگشت داده شود، به معنای صدور مجوز سعی مجدد است؛ اما اگر throwError بازگشت داده شود، دقیقا در همان لحظه کار retryWhen و سعی‌های مجدد خاتمه خواهد یافت. برای مثال در اینجا پس از 2 بار سعی مجدد، اصل خطا صادر می‌شود که سبب خواهد شد قسمت catchError اجرا شود و یا روش صرفنظر کردن از خطاهای با شماره‌های 400 یا 404 را نیز مشاهده می‌کنید. برای مثال اگر از سمت سرور خطای 404 و یا «یافت نشد» صادر شد، return throwError سبب خاتمه‌ی سعی‌های مجدد و خاتمه‌ی عملیات retryWhen می‌شود.


سعی مجدد تمام درخواست‌های شکست خورده‌ی کل برنامه

روش فوق را باید به ازای تک تک درخواست‌های HTTP برنامه تکرار کنیم. برای مدیریت یک چنین اعمال تکراری در برنامه‌های Angular می‌توان یک HttpInterceptor سفارشی را تدارک دید و توسط آن تمام درخواست‌های HTTP سراسر برنامه را به صورت متمرکز تحت نظر قرار داد:
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable, of, throwError } from "rxjs";
import { catchError, delay, mergeMap, retryWhen, take } from "rxjs/operators";

@Injectable()
export class RetryInterceptor implements HttpInterceptor {

  private delayBetweenRetriesMs = 1000;
  private numberOfRetries = 3;

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(request).pipe(
      retryWhen(errors => errors.pipe(
        mergeMap((error: HttpErrorResponse, retryAttempt: number) => {
          if (retryAttempt === this.numberOfRetries - 1) {
            console.log(`HTTP call '${request.method} ${request.url}' failed after ${this.numberOfRetries} retries.`);
            return throwError(error); // no retry
          }

          switch (error.status) {
            case 400:
            case 404:
              return throwError(error); // no retry
          }

          return of(error); // retry
        }),
        delay(this.delayBetweenRetriesMs),
        take(this.numberOfRetries)
      )),
      catchError((error: any, caught: Observable<HttpEvent<any>>) => {
        console.error({ error, caught });
        if (error.status === 401 || error.status === 403) {
          // this.router.navigate(["/accessDenied"]);
        }
        return throwError(error);
      })
    );
  }
}
RetryInterceptor فوق، تمام درخواست‌های با شکست مواجه شده را دو بار با فاصله زمانی یک ثانیه تکرار می‌کند. البته در این بین همانطور که توضیح داده شد، از خطاهای 400 و 404 صرفنظر خواهد شد. همچنین در پایان کار اگر سعی‌های مجدد با موفقیت به پایان نرسند، قسمت catchError، اصل خطای رخ داده را دریافت می‌کند که در اینجا نیز می‌توان به این خطا عکس العمل نشان داد.
روش ثبت آن در قسمت providers مربوط به core.module.ts به صورت زیر است:
providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: RetryInterceptor,
      multi: true
    },
نظرات مطالب
ارسال انواع بی نام (Anonymous) بازگشتی توسط Entity framework به توابع خارجی
خواهش میکنم قابل نداشت . دوست عزیز این مطلب واقعا پیچیده و تخصصی هستش من یک نمونه واستون نوشتم که کد رو براتون میزارم ولی برای فهم کاملش نیاز به اشنایی عمقی با ساختار دات نت و کار با رفلکشن داره که توضیحش در چند خط نمیگنجه بحثه یک کتاب کامله. این نمونه کد که نوشتم دقیقا همون چیزی هست که درخواست توضیحش رو دادید .
        public static List<T> CreateGenericListFromAnonymous<T>(object obj, T example)
        {
            var newquery = new List<T>();
            var constructor = typeof(T).GetConstructors(
            System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance
            ).OrderBy(c => c.GetParameters().Length).First();
            foreach (var item in ((IEnumerable<object>)obj))
            {
                var mapobj = new object[example.GetType().GetProperties().Count()];
                int counter = 0;
                foreach (var itemmap in example.GetType().GetProperties())
                {

                    object value = item.GetType().GetProperty(itemmap.Name).GetValue(item, null);
                    Type t = itemmap.PropertyType;
                    mapobj[counter] = Convert.ChangeType(value, t);
                    counter++;
                }
                newquery.Add((T)constructor.Invoke(mapobj));
            }

            return newquery;
        }


var context = new Models.EntitiesConnection();
var query = context.Posts.Select(pst => new
                              { 
                                   id = pst.id, 
                                   Title = pst.Title,
                                    Likes = pst.Likes,
                                    Unlikes = pst.Unlikes
                             }).OrderByDescending(c => c.id);

object test_custom_casting = CreateGenericListFromAnonymous(query.ToList(), new { id = 0, Title = string.Empty });
 همینطور که میبینید نتیجه‌ی بازگشتی از یک query مرجع یک list شامل فقط و فقط فیلدهایی هست که به صورت inline توسط انواع بی نام مشخص شده ! امیدوارم مفید واقع شده باشه . یا حق