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


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

در اینجا کنترلر و اکشن متدی را جهت جستجوی قسمتی از نام کشورها، مشاهده می‌کنید:
    [Route("api/[controller]")]
    public class TypeaheadController : Controller
    {
        [HttpGet("[action]")]
        public async Task<IActionResult> SearchCountries(string term)
        {
            await Task.Delay(1000); // simulating a slow operation

            var items = new[]
                {
                     "Afghanistan",
                     "Albania",
                     "Algeria",
                     "American Samoa",
                     "Andorra",
                     "Angola",
                     "Anguilla",
                     "Antarctica",
                     "Antigua and/or Barbuda"
                };
            var results = string.IsNullOrWhiteSpace(term) ? items :
                           items.Where(item => item.StartsWith(term, StringComparison.OrdinalIgnoreCase));
            return Json(results.ToArray());
        }
    }
از این کنترلر به نحو ذیل در برنامه‌ی Angular برای ارسال اطلاعات و انجام جستجو استفاده می‌شود:
import { HttpClient, HttpErrorResponse } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Observable";
import { ErrorObservable } from "rxjs/observable/ErrorObservable";
import { catchError, map } from "rxjs/operators";

@Injectable()
export class SearchService {

  constructor(private http: HttpClient) { }

  searchCountries(term: string): Observable<string[]> {
    return this.http
      .get(`/api/Typeahead/SearchCountries?term=${encodeURIComponent(term)}`)
      .pipe(
        map(response => response || {}),
        catchError((error: HttpErrorResponse) => ErrorObservable.create(error))
      );
  }
}
در اینجا از اپراتور pipe مخصوص RxJS 5.5 استفاده شده‌است.


جستجوی ورودی کاربر به ازای هربار ورود اطلاعات توسط او

صرفنظر از نوع فرمی که استفاده می‌کنید (مبتنی بر قالب‌ها و یا واکنشی)، جهت انتقال هربار فشرده شدن کلیدی به کدهای کامپوننت، می‌توان از رخ‌داد input استفاده کرد:
<label>Country: </label>
<input type="text" (input)="onSearch1Change($event.target.value)" />
<ul class="list-group">
   <li class="list-group-item" *ngFor="let country of countries1">
        {{country}}
   </li>
</ul>
و سپس متد مدیریت کننده‌ی آن در کامپوننت نیز به صورت زیر تعریف می‌شود:
onSearch1Change(value: string) {
 
}
در این حالت روش ابتدایی واکنش نشان دادن به هر ورودی، تزریق SearchService فوق به سازنده‌ی این کامپوننت
 constructor(private searchService: SearchService) { }
و سپس مشترک متد جستجوی سمت سرور آن، شدن است.
این روش ابتدایی سه مشکل را به همراه دارد:
الف) به ازای هر بار فشرده شدن کلیدی در Input box، یک درخواست به سمت سرور ارسال می‌شود. برای مثال اگر هدف اصلی کاربر، جستجوی کشورهای شروع شده‌ی با alg باشد، سه درخواست به سمت سرور ارسال می‌شوند و سه بار هم رابط کاربری به روز می‌شود.
ب) اگر در این بین، کاربر حرفی را کم و زیاد کند، درخواست‌های قبلی لغو نمی‌شوند.
ج) درخواست‌ها به صورت موازی به سرور ارسال می‌شوند. ممکن است نتیجه‌ی یکی زودتر و دیگری دیرتر دریافت شود. در این حالت آخرین نتیجه‌ی رسیده، نتایج قبلی را بازنویسی می‌کند که ممکن است الزاما نتیجه‌ای نباشد که کاربر درخواست کرده‌است.


کنترل نرخ ورود اطلاعات توسط متد debounceTime

با اعمال اپراتور debounceTime به رخ‌داد تغییرات ورودی، می‌توان نرخ ورودی کاربر و واکنش نشان دادن به آن‌را کاهش داد. برای مثال اگر این عدد به 300 میلی ثانیه تنظیم شده باشد، صرفا به اولین ورودی رسیده‌ی پس از 300 میلی ثانیه واکنش نشان داده می‌شود و از مابقی صرفنظر خواهد شد. به این ترتیب دیگر به ازای هربار فشرده شدن کلیدی توسط کاربر جستجو صورت نمی‌گیرد. همچنین با ترکیب آن با اپراتور distinctUntilChanged می‌توان تنها به تغییرات غیرتکراری واکنش نشان داد:
export class AutocompleteSampleComponent implements OnInit {

  countries1: string[] = [];
  private model1Changed: Subject<string> = new Subject<string>();
  private dueTime = 300;

  constructor(private searchService: SearchService) { }

  ngOnInit() {
    this.model1Changed
      .pipe(
        debounceTime(this.dueTime),
        distinctUntilChanged(),
        flatMap(inputValue => {
          console.log("debounced input value1", inputValue);
          return this.searchService.searchCountries(inputValue);
        })
      )
      .subscribe(countries => {
        this.countries1 = countries;
      });
  }

  onSearch1Change(value: string) {
    this.model1Changed.next(value);
  }
}
بنابراین بجای اینکه متد this.searchService.searchCountries دقیقا داخل onSearch1Change فراخوانی شود، باید بتوان تغییرات صورت گرفته‌ی نهایی را پس از اعمال debounceTime و distinctUntilChanged به آن ارسال کرد و سپس نتیجه را به کاربر نمایش داد.
برای این منظور یک Subject تعریف شده‌است تا کار مدیریت تغییرات رسیده (کلیدهای فشرده شده‌ی توسط کاربر) را انجام دهد. در این‌حالت فرصت خواهیم داشت تا انواع و اقسام اپراتورهای RxJS را با هم ترکیب و صرفا نتیجه‌ی نهایی (آخرین ورودی یکتای با تاخیر او) را به searchService ارسال کنیم.
متد onSearch1Change نیز تنها کافی است با فراخوانی متد next این Subject‌، جریان تغییرات رسیده را به آن انتقال دهد.
در اینجا برای انتقال آخرین ورودی یکتای با تاخیر به متد this.searchService.searchCountries از اپراتور flatMap استفاده شده‌است. این اپراتور، آخرین ورودی فیلتر شده را دریافت کرده و به متد searchCountries ارسال می‌کند. همچنین خروجی آن نیز یک Observable است. به همین جهت در ادامه می‌توان توسط متد subscribe، مشترک آن شد و آرایه‌ی countries دریافتی از سرور را به کاربر نمایش داد.



بهبود کارآیی جستجو با لغو درخواست‌های پیشین

تا اینجا توانستیم نرخ ورود اطلاعات کاربر را به صورت کنترل شده‌ای به متد this.searchService.searchCountries ارسال کنیم و نه اینکه به ازای هر بار ورود اطلاعات توسط آن، یکبار این متد فراخوانی شود. اما همانطور که در تصویر فوق مشاهده می‌کنید، در اینجا هدف نهایی کاربر، جستجوی نام کشورهای شروع شده‌ی با alg بوده است و در این بین چندین بار سعی و خطا انجام داده‌است تا به alg رسیده‌است. مشکل اینجا است که هیچکدام از درخواست‌های قبلی او که مدنظر نبوده‌اند، لغو نشده‌اند و تمام آن‌ها صورت گرفته و همچنین سبب به روز رسانی‌های مکرر رابط کاربری شده‌اند.
برای رفع یک چنین مشکلی و لغو خودکار درخواست‌های قبلی، اپراتور دیگری به نام switchMap وجود دارد که دقیقا یک چنین کاری را انجام می‌دهد. در اینجا برخلاف اپراتور flatMap، تمام درخواست‌های تمام نشده‌ی قبلی، لغو شده و صرفا آخرین مورد پردازش می‌شود.


برای اعمال آن نیز در کدهای فوق تنها کافی است flatMap را با switchMap جایگزین کنید. پس از آن نتیجه را در تصویر فوق ملاحظه می‌کنید. اینبار اگر هدف نهایی کاربر جستجوی alg باشد، تمام ورودی‌های قبلی او به صورت خودکار لغو می‌شوند و دیگر پردازش نخواهند شد که در نهایت سبب بالا رفتن کارآیی برنامه با کاهش تعداد بار به روز رسانی رابط کاربری خواهد شد.

همچنین در حالت استفاده‌ی از flatMap، ممکن است کاربر نتیجه‌ی اشتباهی را نیز دریافت کند. از این جهت که درخواست‌های ارسالی به سمت سرور، به صورت موازی اجرا می‌شوند. در این حالت ممکن است یکی زودتر و دیگری دیرتر به پایان برسد و کاربر نتیجه‌ای را که مشاهده می‌کند، دقیقا آن چیزی نباشد که جستجو کرده‌است (رابط کاربری آخرین درخواست پایان یافته را نمایش می‌دهد که نتیجه‌ی آن الزاما به ترتیب ورود اطلاعات کاربر نیست).
// A1: Request for `ABC`
// A2: Response for `ABC`
// B1: Request for `ABCX`
// B2: Response for `ABCX`
--A1----------A2-->
------B1--B2------>
برای نمونه فرض کنید دو درخواست A1 و B1 به همراه پاسخ‌های A2 و B2 را داریم. درخواست A1 پیش از B1 ارسال شده‌است؛ اما پاسخ B1 زودتر از پاسخ A2 از سرور دریافت شده‌است. در این حالت کاربر عبارت ABCX را وارد کرده‌است اما پاسخ عبارت ABC پیشین را در رابط کاربری مشاهده می‌کند (آخرین پاسخ رسیده در رابط کاربری (یا همان A2)، پاسخ‌های قبلی (یا همان B2) را بازنویسی می‌کند).

در حالت استفاده‌ی از flatMap‌، مشترک هر رخ‌داد رسیده خواهیم شد؛ بدون قطع اشتراک خودکار از سایر observableهای ایجاد شده‌ی پیشین. اما در حالت استفاده‌ی از switchMap‌، ابتدا کار لغو اشتراک خودکار از تمام observableهای قبلی صورت می‌گیرد و سپس یک observable جدید را ایجاد می‌کند. به همین جهت است که استفاده‌ی از switchMap‌  به همراه درخواست‌های http، سبب لغو خودکار درخواست‌های پیشین می‌شود. در این حالت نه تنها تعداد بار به روز رسانی رابط کاربری کاهش پیدا می‌کند، بلکه تضمین خواهد شد دیگر کاربر نتیجه‌ی اشتباهی را نیز مشاهده نکند.



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

دو تنظیم زیر را در نظر بگیرید:
<add key="nhibernate-logger" value="NHibernate.Helper.Logging.LoggerFactory, NHibernate.Helper" />
و یا
<add name="StaticContentCacheModule" type="StaticContentCacheModule.StaticCache, StaticContentCacheModule"/>
این نوع موارد را در فایل‌های app.config و یا web.config زیاد می‌توان یافت.
الان فرض کنید کلاس StaticCache مربوط به StaticContentCacheModule فرضی فوق را به صورت دستی به برنامه‌ی خود اضافه کرده‌اید. همچنین سطر فوق را نیز بدون هیچ تغییری در قسمت http modules مربوط به web.config برنامه معرفی نموده‌اید. برنامه را اجرا می‌کنید، اما ماژول ذکر شده کار نمی‌کند! چرا؟
چون نام رشته‌ای متناظر با کلاس StaticCache ایی که اکنون به پروژه‌ی خود اضافه کرده‌اید، با توجه به فضاهای نام پروژه‌ی جدید، کاملا دگرگون شده است. بنابراین، سؤال مهم اینجا است که این نام را بر اساس تنظیمات پروژه‌ی جاری چگونه می‌توان یافت؟
خوشبختانه دات نت فریم ورک، ابزاری توکار را برای تولید این نام رشته‌ای، به همراه دارد:
class Test
{
static void Main()
{
string name = typeof(System.Data.DataView).AssemblyQualifiedName;
Console.WriteLine(name);
}
}
خاصیت AssemblyQualifiedName ذکر شده در مثال فوق، دقیق‌ترین نامی است که می‌توانید در پروژه‌ی خود استفاده نمائید.
خروجی این مثال جهت نمایش نام رشته‌ای معادل کلاس System.Data.DataView به صورت زیر است:
System.Data.DataView, System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

سؤال: از کجا متوجه شوم که رشته‌ی فوق واقعا کار می‌کند؟
مقدار متغیر name مثال فوق باید پس از بکارگیری در متد Type.GetType ، حاصلی غیر null را بازگشت دهد.
var name = typeof(System.Data.DataView).AssemblyQualifiedName;
var type = Type.GetType(name);
اگر حاصل نال بود، یعنی همان مشکلی که در ابتدای مطلب ذکر شد: ماژول مشخص شده در web.config برنامه توسط رشته‌ی مورد نظر کار نخواهد کرد.

نکته: اگر قصد معرفی اسمبلی دیگری را به برنامه دارید و این اسمبلی امضای دیجیتال دارد (strong name signature)، باید تمام اطلاعات حاصل را ذکر کنید (مانند مثال فوق که شامل Version ، Public key token و غیره است). در غیر اینصورت (عدم وجود امضای دیجیتال) ذکر دو قسمت اول خروجی خاصیت AssemblyQualifiedName کافی خواهند بود.

مطالب
آشنایی با جنریک‌ها #1
طبق این معرفی ، جنریک‌ها باعث می‌شوند که نوع داده‌ای (data type) المان‌های برنامه در زمان استفاده از آن‌ها در برنامه مشخص شوند. به عبارت دیگر، جنریک به ما اجازه می‌دهد کلاس‌ها یا متدهایی بنویسیم که می‌توانند با هر نوع داده‌ای کار کنند.

نکاتی از جنریک‌ها:
  • برای به حداکثر رسانی استفاده مجدد از کد، type safety و کارایی است.
  • بیشترین استفاده مشترک از جنریک‌ها جهت ساختن کالکشن کلاس‌ها (collection classes) است.
  • تا حد ممکن از جنریک کالکشن کلاسها (generic collection classes) جدید فضای نام System.Collections.Generic بجای کلاس‌هایی مانند ArrayList در فضای نام System.Collections استفاده شود.
  • شما می‌توانید اینترفیس جنریک ، کلاس جنریک ، متد جنریک و عامل جنریک سفارشی خودتان تهیه کنید.
  • جنریک کلاس‌ها، ممکن است در دسترسی به متدهایی با نوع داده‌ای خاص محدود شود.
  • بوسیله reflection، می‌توانید اطلاعاتی که در یک جنریک در زمان اجرا (run-time) قرار دارد بدست آورید.
انواع جنریک ها:
  1. کلاس‌های جنریک
  2. اینترفیس‌های جنریک
  3. متدهای جنریک
  4. عامل‌های جنریک
در قسمت اول به معرفی کلاس جنریک می‌پردازیم.

کلاس‌های جنریک
کلاس جنریک یعنی کلاسی که می‌تواند با چندین نوع داده کار کند برای آشنایی با این نوع کلاس به کد زیر دقت کنید:
using System;
using System.Collections.Generic;

namespace GenericApplication
{
    public class MyGenericArray<T>
    {
        // تعریف یک آرایه از نوع جنریک
        private T[] array;

        public MyGenericArray(int size)
        {
            array = new T[size + 1];
        }

        // بدست آوردن یک آیتم جنریک از آرایه جنریک
        public T getItem(int index)
        {
            return array[index];
        }

        // افزودن یک آیتم جنریک به آرایه جنریک
        public void setItem(int index, T value)
        {
            array[index] = value;
        }
    }
}
در کد بالا کلاسی تعریف شده است که می‌تواند بر روی آرایه‌هایی از نوع داده‌ای مختلف عملیات درج و حذف را انجام دهد. برای تعریف کلاس جنریک کافی است عبارت <T> بعد از نام کلاس خود اضافه کنید، سپس همانند سایر کلاس‌ها از این نوع داده ای در کلاس استفاده کنید. در مثال بالا یک آرایه از نوع T تعریف شده است که این نوع، در زمان استفاده مشخص خواهد شد. (یعنی در زمان استفاده از کلاس مشخص خواهد شد که چه نوع آرایه ای ایجاد می‌شود)
در کد زیر نحوه استفاده از کلاس جنریک نشان داده شده است، همانطور که مشاهده می‌کنید نوع کلاس int و char در نظر گرفته شده است (نوع کلاس، زمان استفاده از کلاس مشخص می‌شود) و سپس آرایه هایی از نوع int و char ایجاد شده است و 5 آیتم از نوع int و char به آرایه‌های هم نوع افزوده شده است. 
class Tester
{
        static void Main(string[] args)
        {
            // تعریف یک آرایه از نوع عدد صحیح
            MyGenericArray<int> intArray = new MyGenericArray<int>(5);

            // افزودن اعداد صحیح به آرایه ای از نوع عدد صحیح
            for (int c = 0; c < 5; c++)
            {
                intArray.setItem(c, c*5);
            }

            // بدست آوردن آیتم‌های آرایه ای از نوع عدد صحیح
            for (int c = 0; c < 5; c++)
            {
                Console.Write(intArray.getItem(c) + " ");
            }
            Console.WriteLine();

            // تعریف یک آرایه از نوع کاراکتر
            MyGenericArray<char> charArray = new MyGenericArray<char>(5);

            // افزودن کاراکترها به آرایه ای از نوع کاراکتر
            for (int c = 0; c < 5; c++)
            {
                charArray.setItem(c, (char)(c+97));
            }

            // بدست آوردن آیتم‌های آرایه ای از نوع کاراکتر
            for (int c = 0; c< 5; c++)
            {
                Console.Write(charArray.getItem(c) + " ");
            }
            Console.WriteLine();
            Console.ReadKey();
        }
}
زمانی که کد بالا اجرا می‌شود خروجی زیر بدست می‌آید:
0 5 10 15 20
a b c d e
مطالب
استفاده از EF در اپلیکیشن های N-Tier : قسمت سوم

در قسمت قبلی بروز رسانی موجودیت‌های منفصل با WCF را بررسی کردیم. در این قسمت خواهیم دید چگونه می‌توان تغییرات موجودیت‌ها را تشخیص داد و عملیات CRUD را روی یک Object Graph اجرا کرد.

تشخیص تغییرات با Web API

فرض کنید می‌خواهیم از سرویس‌های Web API برای انجام عملیات CRUD استفاده کنیم، اما بدون آنکه برای هر موجودیت متدهایی مجزا تعریف کنیم. به بیان دیگر می‌خواهیم عملیات مذکور را روی یک Object Graph انجام دهیم. مدیریت داده‌ها هم با مدل Code-First پیاده سازی می‌شود. در مثال جاری یک اپلیکیشن کنسول خواهیم داشت که بعنوان یک کلاینت سرویس را فراخوانی می‌کند. هر پروژه نیز در Solution مجزایی قرار دارد، تا یک محیط n-Tier را شبیه سازی کنیم.

مدل زیر را در نظر بگیرید.

همانطور که می‌بینید مدل ما آژانس‌های مسافرتی و رزرواسیون آنها را ارائه می‌کند. می‌خواهیم مدل و کد دسترسی داده‌ها را در یک سرویس Web API پیاده سازی کنیم تا هر کلاینتی که به HTTP دسترسی دارد بتواند عملیات CRUD را انجام دهد. برای ساختن سرویس مورد نظر مراحل زیر را دنبال کنید:

  • در ویژوال استودیو پروژه جدیدی از نوع ASP.NET Web Application بسازید و قالب پروژه را Web API انتخاب کنید. نام پروژه را به Recipe3.Service تغییر دهید.
  • کنترلر جدیدی بنام TravelAgentController به پروژه اضافه کنید.
  • دو کلاس جدید با نام‌های TravelAgent و Booking بسازید و کد آنها را مطابق لیست زیر تغییر دهید.
public class TravelAgent
{
    public TravelAgent()
    {
        this.Bookings = new HashSet<Booking>();
    }

    public int AgentId { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Booking> Bookings { get; set; }
}

public class Booking
{
    public int BookingId { get; set; }
    public int AgentId { get; set; }
    public string Customer { get; set; }
    public DateTime BookingDate { get; set; }
    public bool Paid { get; set; }
    public virtual TravelAgent TravelAgent { get; set; }
}
  • با استفاده از NuGet Package Manager کتابخانه Entity Framework 6 را به پروژه اضافه کنید.
  • کلاس جدیدی بنام Recipe3Context بسازید و کد آن را مطابق لیست زیر تغییر دهید.
public class Recipe3Context : DbContext
{
    public Recipe3Context() : base("Recipe3ConnectionString") { }
    public DbSet<TravelAgent> TravelAgents { get; set; }
    public DbSet<Booking> Bookings { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<TravelAgent>().HasKey(x => x.AgentId);
        modelBuilder.Entity<TravelAgent>().ToTable("TravelAgents");
        modelBuilder.Entity<Booking>().ToTable("Bookings");
    }
}

  • فایل Web.config پروژه را باز کنید و رشته اتصال زیر را به قسمت ConnectionStrings اضافه کنید.
<connectionStrings>
  <add name="Recipe3ConnectionString"
    connectionString="Data Source=.;
    Initial Catalog=EFRecipes;
    Integrated Security=True;
    MultipleActiveResultSets=True"
    providerName="System.Data.SqlClient" />
</connectionStrings>
  • فایل Global.asax را باز کنید و کد زیر را به متد Application_Start اضافه نمایید. این کد بررسی Model Compatibility در EF را غیرفعال می‌کند. همچنین به JSON serializer می‌گوییم که self-referencing loop خاصیت‌های پیمایشی را نادیده بگیرد. این حلقه بدلیل ارتباط bidirectional بین موجودیت‌ها بوجود می‌آید.
protected void Application_Start()
{
    // Disable Entity Framework Model Compatibilty
    Database.SetInitializer<Recipe1Context>(null);

    // The bidirectional navigation properties between related entities
    // create a self-referencing loop that breaks Web API's effort to
    // serialize the objects as JSON. By default, Json.NET is configured
    // to error when a reference loop is detected. To resolve problem,
    // simply configure JSON serializer to ignore self-referencing loops.
    GlobalConfiguration.Configuration.Formatters.JsonFormatter
        .SerializerSettings.ReferenceLoopHandling =
        Newtonsoft.Json.ReferenceLoopHandling.Ignore;
    ...
}
  • فایل RouteConfig.cs را باز کنید و قوانین مسیریابی را مانند لیست زیر تغییر دهید.
public static void Register(HttpConfiguration config)
{
    config.Routes.MapHttpRoute(
      name: "ActionMethodSave",
      routeTemplate: "api/{controller}/{action}/{id}",
      defaults: new { id = RouteParameter.Optional });
}
  • در آخر کنترلر TravelAgent را باز کنید و کد آن را مطابق لیست زیر بروز رسانی کنید.
public class TravelAgentController : ApiController
{
    // GET api/travelagent
    [HttpGet]
    public IEnumerable<TravelAgent> Retrieve()
    {
        using (var context = new Recipe3Context())
        {
            return context.TravelAgents.Include(x => x.Bookings).ToList();
        }
    }

    /// <summary>
    /// Update changes to TravelAgent, implementing Action-Based Routing in Web API
    /// </summary>
    public HttpResponseMessage Update(TravelAgent travelAgent)
    {
        using (var context = new Recipe3Context())
        {
            var newParentEntity = true;
            // adding the object graph makes the context aware of entire
            // object graph (parent and child entities) and assigns a state
            // of added to each entity.
            context.TravelAgents.Add(travelAgent);
            if (travelAgent.AgentId > 0)
            {
                // as the Id property has a value greater than 0, we assume
                // that travel agent already exists and set entity state to
                // be updated.
                context.Entry(travelAgent).State = EntityState.Modified;
                newParentEntity = false;
            }

            // iterate through child entities, assigning correct state.
            foreach (var booking in travelAgent.Bookings)
            {
                if (booking.BookingId > 0)
                    // assume booking already exists if ID is greater than zero.
                    // set entity to be updated.
                    context.Entry(booking).State = EntityState.Modified;
            }

            context.SaveChanges();
            HttpResponseMessage response;
            // set Http Status code based on operation type
            response = Request.CreateResponse(newParentEntity ? HttpStatusCode.Created : HttpStatusCode.OK, travelAgent);
            return response;
        }
    }

    [HttpDelete]
    public HttpResponseMessage Cleanup()
    {
        using (var context = new Recipe3Context())
        {
            context.Database.ExecuteSqlCommand("delete from [bookings]");
            context.Database.ExecuteSqlCommand("delete from [travelagents]");
        }
        return Request.CreateResponse(HttpStatusCode.OK);
    }
}

در قدم بعدی کلاینت پروژه را می‌سازیم که از سرویس Web API مان استفاده می‌کند.

  • در ویژوال استودیو پروژه جدیدی از نوع Console application بسازید و نام آن را به Recipe3.Client تغییر دهید.
  • فایل program.cs را باز کنید و کد آن را مطابق لیست زیر بروز رسانی کنید.
internal class Program
{
    private HttpClient _client;
    private TravelAgent _agent1, _agent2;
    private Booking _booking1, _booking2, _booking3;
    private HttpResponseMessage _response;

    private static void Main()
    {
        Task t = Run();
        t.Wait();
        Console.WriteLine("\nPress <enter> to continue...");
        Console.ReadLine();
    }

    private static async Task Run()
    {
        var program = new Program();
        program.ServiceSetup();
        // do not proceed until clean-up is completed
        await program.CleanupAsync();
        program.CreateFirstAgent();
        // do not proceed until agent is created
        await program.AddAgentAsync();
        program.CreateSecondAgent();
        // do not proceed until agent is created
        await program.AddSecondAgentAsync();
        program.ModifyAgent();
        // do not proceed until agent is updated
        await program.UpdateAgentAsync();
        // do not proceed until agents are fetched
        await program.FetchAgentsAsync();
    }

    private void ServiceSetup()
    {
        // set up infrastructure for Web API call
        _client = new HttpClient {BaseAddress = new Uri("http://localhost:6687/")};
        // add Accept Header to request Web API content negotiation to return resource in JSON format
        _client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    }

    private async Task CleanupAsync()
    {
        // call cleanup method in service
        _response = await _client.DeleteAsync("api/travelagent/cleanup/");
    }

    private void CreateFirstAgent()
    {
        // create new Travel Agent and booking
        _agent1 = new TravelAgent {Name = "John Tate"};
        _booking1 = new Booking
        {
            Customer = "Karen Stevens",
            Paid = false,
            BookingDate = DateTime.Parse("2/2/2010")
        };

        _booking2 = new Booking
        {
            Customer = "Dolly Parton",
            Paid = true,
            BookingDate = DateTime.Parse("3/10/2010")
        };
  
        _agent1.Bookings.Add(_booking1);
        _agent1.Bookings.Add(_booking2);
    }

    private async Task AddAgentAsync()
    {
        // call generic update method in Web API service to add agent and bookings
        _response = await _client.PostAsync("api/travelagent/update/",
            _agent1, new JsonMediaTypeFormatter());

        if (_response.IsSuccessStatusCode)
        {
            // capture newly created travel agent from service, which will include
            // database-generated Ids for each entity
            _agent1 = await _response.Content.ReadAsAsync<TravelAgent>();
            _booking1 = _agent1.Bookings.FirstOrDefault(x => x.Customer == "Karen Stevens");
            _booking2 = _agent1.Bookings.FirstOrDefault(x => x.Customer == "Dolly Parton");

            Console.WriteLine("Successfully created Travel Agent {0} and {1} Booking(s)",
            _agent1.Name, _agent1.Bookings.Count);
        }
        else
            Console.WriteLine("{0} ({1})", (int) _response.StatusCode, _response.ReasonPhrase);
    }

    private void CreateSecondAgent()
    {
        // add new agent and booking
        _agent2 = new TravelAgent {Name = "Perry Como"};
        _booking3 = new Booking {
            Customer = "Loretta Lynn",
            Paid = true,
            BookingDate = DateTime.Parse("3/15/2010")};
        _agent2.Bookings.Add(_booking3);
    }

    private async Task AddSecondAgentAsync()
    {
        // call generic update method in Web API service to add agent and booking
        _response = await _client.PostAsync("api/travelagent/update/", _agent2, new JsonMediaTypeFormatter());

        if (_response.IsSuccessStatusCode)
        {
            // capture newly created travel agent from service
            _agent2 = await _response.Content.ReadAsAsync<TravelAgent>();
            _booking3 = _agent2.Bookings.FirstOrDefault(x => x.Customer == "Loretta Lynn");
            Console.WriteLine("Successfully created Travel Agent {0} and {1} Booking(s)",
                _agent2.Name, _agent2.Bookings.Count);
        }
        else
            Console.WriteLine("{0} ({1})", (int) _response.StatusCode, _response.ReasonPhrase);
    }

    private void ModifyAgent()
    {
        // modify agent 2 by changing agent name and assigning booking 1 to him from agent 1
        _agent2.Name = "Perry Como, Jr.";
        _agent2.Bookings.Add(_booking1);
    }

    private async Task UpdateAgentAsync()
    {
        // call generic update method in Web API service to update agent 2
        _response = await _client.PostAsync("api/travelagent/update/", _agent2, new JsonMediaTypeFormatter());
        if (_response.IsSuccessStatusCode)
        {
            // capture newly created travel agent from service, which will include Ids
            _agent1 = _response.Content.ReadAsAsync<TravelAgent>().Result;
            Console.WriteLine("Successfully updated Travel Agent {0} and {1} Booking(s)", _agent1.Name, _agent1.Bookings.Count);
        }
        else
            Console.WriteLine("{0} ({1})", (int) _response.StatusCode, _response.ReasonPhrase);
    }

    private async Task FetchAgentsAsync()
    {
        // call Get method on service to fetch all Travel Agents and Bookings
        _response = _client.GetAsync("api/travelagent/retrieve").Result;
        if (_response.IsSuccessStatusCode)
        {
            // capture newly created travel agent from service, which will include Ids
            var agents = await _response.Content.ReadAsAsync<IEnumerable<TravelAgent>>();

            foreach (var agent in agents)
            {
                Console.WriteLine("Travel Agent {0} has {1} Booking(s)", agent.Name, agent.Bookings.Count());
            }
        }
        else
            Console.WriteLine("{0} ({1})", (int) _response.StatusCode, _response.ReasonPhrase);
    }
}
  • در آخر کلاس‌های TravelAgent و Booking را به پروژه کلاینت اضافه کنید. اینگونه کدها بهتر است در لایه مجزایی قرار گیرند و بین پروژه‌ها به اشتراک گذاشته شوند.

اگر اپلیکیشن کنسول (کلاینت) را اجرا کنید با خروجی زیر مواجه خواهید شد.

(Successfully created Travel Agent John Tate and 2 Booking(s
(Successfully created Travel Agent Perry Como and 1 Booking(s
(Successfully updated Travel Agent Perry Como, Jr. and 2 Booking(s
(Travel Agent John Tate has 1 Booking(s
(Travel Agent Perry Como, Jr. has 2 Booking(s


شرح مثال جاری

با اجرای اپلیکیشن Web API شروع کنید. این اپلیکیشن یک کنترلر MVC Web Controller دارد که پس از اجرا شما را به صفحه خانه هدایت می‌کند. در این مرحله سایت در حال اجرا است و سرویس‌ها قابل دسترسی هستند.

سپس اپلیکیشن کنسول را باز کنید، روی خط اول کد فایل program.cs یک breakpoint قرار دهید و آن را اجرا کنید. ابتدا آدرس سرویس Web API را نگاشت می‌کنیم و با تنظیم مقدار خاصیت Accept Header از سرویس درخواست می‌کنیم که اطلاعات را با فرمت JSON بازگرداند.

بعد از آن با استفاده از آبجکت HttpClient متد DeleteAsync را فراخوانی می‌کنیم که روی کنترلر TravelAgent تعریف شده است. این متد تمام داده‌های پیشین را حذف میکند.

در قدم بعدی سه آبجکت جدید می‌سازیم: یک آژانس مسافرتی و دو رزرواسیون. سپس این آبجکت‌ها را با فراخوانی متد PostAsync روی آبجکت HttpClient به سرویس ارسال می‌کنیم. اگر به متد Update در کنترلر TravelAgent یک breakpoint اضافه کنید، خواهید دید که این متد آبجکت آژانس مسافرتی را بعنوان یک پارامتر دریافت می‌کند و آن را به موجودیت TravelAgents در Context جاری اضافه می‌نماید. این کار آبجکت آژانس مسافرتی و تمام آبجکت‌های فرزند آن را در حالت Added اضافه می‌کند و باعث می‌شود که context جاری شروع به ردیابی (tracking) آنها کند.

نکته: قابل ذکر است که اگر موجودیت‌های متعددی با مقداری یکسان در خاصیت کلید اصلی (Primary-key value) دارید باید مجموعه آبجکت‌های خود را Add کنید و نه Attach. در مثال جاری چند آبجکت Booking داریم که مقدار کلید اصلی آنها صفر است (Bookings with Id = 0). اگر از Attach استفاده کنید EF پیغام خطایی صادر می‌کند چرا که چند موجودیت با مقادیر کلید اصلی یکسان به context جاری اضافه کرده اید.

بعد از آن بر اساس مقدار خاصیت Id مشخص می‌کنیم که موجودیت‌ها باید بروز رسانی شوند یا خیر. اگر مقدار این فیلد بزرگتر از صفر باشد، فرض بر این است که این موجودیت در دیتابیس وجود دارد بنابراین خاصیت EntityState را به Modified تغییر می‌دهیم. علاوه بر این فیلدی هم با نام newParentEntity تعریف کرده ایم که توسط آن بتوانیم کد وضعیت مناسبی به کلاینت بازگردانیم. در صورتی که مقدار فیلد Id در موجودیت TravelAgent برابر با یک باشد، مقدار خاصیت EntityState را به همان Added رها می‌کنیم.

سپس تمام آبجکت‌های فرزند آژانس مسافرتی (رزرواسیون ها) را بررسی میکنیم و همین منطق را روی آنها اعمال می‌کنیم. یعنی در صورتی که مقدار فیلد Id آنها بزرگتر از 0 باشد وضعیت EntityState را به Modified تغییر می‌دهیم. در نهایت متد SaveChanges را فراخوانی می‌کنیم. در این مرحله برای موجودیت‌های جدید اسکریپت‌های Insert و برای موجودیت‌های تغییر کرده اسکریپت‌های Update تولید می‌شود. سپس کد وضعیت مناسب را به کلاینت بر می‌گردانیم. برای موجودیت‌های اضافه شده کد وضعیت 201 (Created) و برای موجودیت‌های بروز رسانی شده کد وضعیت 200 (OK) باز می‌گردد. کد 201 به کلاینت اطلاع می‌دهد که رکورد جدید با موفقیت ثبت شده است، و کد 200 از بروز رسانی موفقیت آمیز خبر می‌دهد. هنگام تولید سرویس‌های REST-based بهتر است همیشه کد وضعیت مناسبی تولید کنید.

پس از این مراحل، آژانس مسافرتی و رزرواسیون جدیدی می‌سازیم و آنها را به سرویس ارسال می‌کنیم. سپس نام آژانس مسافرتی دوم را تغییر می‌دهیم، و یکی از رزرواسیون‌ها را از آژانس اولی به آژانس دومی منتقل می‌کنیم. اینبار هنگام فراخوانی متد Update تمام موجودیت‌ها شناسه ای بزرگتر از 1 دارند، بنابراین وضعیت EntityState آنها را به Modified تغییر می‌دهیم تا هنگام ثبت تغییرات دستورات بروز رسانی مناسب تولید و اجرا شوند.

در آخر کلاینت ما متد Retreive را روی سرویس فراخوانی می‌کند. این فراخوانی با کمک متد GetAsync انجام می‌شود که روی آبجکت HttpClient تعریف شده است. فراخوانی این متد تمام آژانس‌های مسافرتی بهمراه رزرواسیون‌های متناظرشان را دریافت می‌کند. در اینجا با استفاده از متد Include تمام رکوردهای فرزند را بهمراه تمام خاصیت هایشان (properties) بارگذاری می‌کنیم.

دقت کنید که مرتب کننده JSON تمام خواص عمومی (public properties) را باز می‌گرداند، حتی اگر در کد خود تعداد مشخصی از آنها را انتخاب کرده باشید.

نکته دیگر آنکه در مثال جاری از قرارداد‌های توکار Web API برای نگاشت درخواست‌های HTTP به اکشن متدها استفاده نکرده ایم. مثلا بصورت پیش فرض درخواست‌های POST به متدهایی نگاشت می‌شوند که نام آنها با "Post" شروع می‌شود. در مثال جاری قواعد مسیریابی را تغییر داده ایم و رویکرد مسیریابی RPC-based را در پیش گرفته ایم. در اپلیکیشن‌های واقعی بهتر است از قواعد پیش فرض استفاده کنید چرا که هدف Web API ارائه سرویس‌های REST-based است. بنابراین بعنوان یک قاعده کلی بهتر است متدهای سرویس شما به درخواست‌های متناظر HTTP نگاشت شوند. و در آخر آنکه بهتر است لایه مجزایی برای میزبانی کدهای دسترسی داده ایجاد کنید و آنها را از سرویس Web API تفکیک نمایید.

مطالب
چند نکته‌ی کاربردی در #C
استفاده از Tuple‌ها جهت مقدار برگشتی یک متد

اکثر مواقع برنامه نویسان برای بازگشت چند مقدار توسط یک متد، به روش‌هایی مثل تعریف کلاس‌های POCO یا پارامترهایی از نوع out متوسل می‌شوند. به وسیله‌ی Tuple‌ها می‌توان چند مقدار مختلف را به عنوان خروجی متد بازگشت داد:
private Tuple<string, string, int> GetPersonInfo()
{
     return new Tuple<string, string, int>("Steve", "Jobs", 56);
}
همچنین از Tuple‌ها می‌توان برای پاس دادن یکباره پارامترها به متد استفاده کرد و در مواقعی مانند ارسال پارامتر به Thread‌ها که در حالت عادی یک پارامتر را به عنوان ورودی قبول می‌کنند، کاربردی خواهند بود.


عدم نیاز به استفاده از کالکشن‌های موقتی در متدها جهت نگهداری مقدار بازگشتی متد

اکثر برنامه نویسان، در متدهایی که لیستی از مقادیر را بازگشت می‌دهند، از یک متغییر موقتی استفاده می‌کنند:
private IEnumerable<int> GetNumbers()
{
   var result = new List<int>();

   for (int i = 0; i <= 100; i++)
      result.Add(i);

   return result;
}
اما باید دانست که الزاما نیازی به انجام این کار نیست و به وسیله‌ی کلیدواژه‌ی yield می‌توان مقادیر را همزمان با تولید آنها در بدنه متد به عنوان خروجی متد بازگشت داد و به این ترتیب به مصرف حافظه‌ی کمتری رسید:
private IEnumerable<int> GetNumbers()
{
   for (int i = 0; i <= 100; i++)
   {
        yield return i;
   }
}

ملزم کردن نوع پایه یک کلاس Generic به رعایت قوانین پیاده سازی خاص

Generic‌ها میتوانند عملکرد یکسانی را برای نوع‌های داده‌ای متفاوت، پیاده سازی کنند. با توجه به ماهیت Generic‌ها ممکن است در سناریوهایی لازم باشد تا نوع داده‌ی اولیه‌ای که قرار است Generic پیاده سازی شود، از قوانین پیاده سازی خاصی پیروی کند. به صورت زیر می‌توان نوع پایه‌ی یک Generic را ملزم به رعایت قوانین خاص پیاده سازی به واسطه یک Interface کرد:
public interface ICar
{
   string GetName();
   string GetManufacturerCompany();
}

private class GenricClass<T> where T : ICar
{
}
نمونه‌ی استفاده:
public class Audi : ICar
{
   public string GetName()
   {
      throw new NotImplementedException();
   }

   public string GetManufacturerCompany()
   {
      throw new NotImplementedException();
   }
}

private static void Main(string[] args)
{
   var invalidTest = new GenricClass<int>();
   var validTest = new GenricClass<Audi>();
}
مطالب
امکان انجام محاسبات سمت کلاینت در EF Core
در دنیای NET. همواره دو نوع LINQ وجود داشته داشته است: LINQ to Objects و ... مابقی.  در حالت اول با <IEnumerable<T‌ها کار می‌کنیم که تمام عملیات در حافظه انجام می‌شود و در مابقی حالات یک <IQueryable<T وجود دارد که عبارت حاصل از آن جهت کاربردهای مختلفی به زبان‌های متفاوتی مانند SQL ترجمه می‌شوند. در هر دو حالت کلی، Syntax نهایی یکی است و تنها اگر به منبع داده‌ی آن‌ها دقت کنیم، می‌توانیم نوع آن‌ها را تشخیص دهیم. برای نمونه کوئری ذیل بر اساس منبع Blogs است که می‌تواند LINQ to Objects باشد و یا حالت <Queryable<Blog که قرار است به زبانی مشخص ترجمه شود:
var blogs = from blog in Blogs
   where blog.Name.Contains("Development")
   select blog;
اکنون فرض کنید که این عبارت قرار است به SQL ترجمه شده و سپس بر روی یک بانک اطلاعاتی اجرا شود. در این حالت مفسر LINQ باید بداند که متد Contains را چگونه به معادل SQL آن ترجمه کند و این ترجمه می‌تواند بر اساس بانک‌های اطلاعاتی مختلف، متفاوت نیز باشد. اما در حالت LINQ to Objects یک چنین مشکلی وجود ندارد و این ترجمه مستقیما بر روی متد Contains کلاس string انجام می‌شود.
اما اکنون چطور؟
var blogs = from blog in Blogs
   where blog.Name.ComputeHash() == 0
   select blog;
فرض کنید یک متد الحاقی را به نام ComputeHash به کلاس string اضافه کرده‌ایم. یک چنین کوئری را اگر بر روی EF 6.x اجرا کنیم، برنامه با یک استثناء متوقف خواهد شد؛ چون امکان ترجمه‌ی متد ComputeHash را به معادل SQL آن ندارد؛ اما EF Core برای انجام یک چنین کوئری‌هایی بهبود یافته‌است که به آن، محاسبات سمت کلاینت گفته می‌شود.


یک مثال: بررسی تاثیر ارزیابی‌های سمت کلاینت در EF Core

فرض کنید ساختار جدول بلاگ‌ها به صورت زیر است:
public class Blog
{
   public int BlogId { get; set; }
   public string Url { get; set; }  
}
همچنین یک متد الحاقی را به نام ComputeHash به صورت ذیل تعریف کرده‌ایم:
    public static class StringExtensions
    {
        public static int ComputeHash(this string str)
        {
            var hash = 0;
            foreach (var ch in str)
            {
                hash += (int)ch;
            }
            return hash;
        }
    }
اکنون می‌خواهیم بلاگ‌هایی را پیدا کنیم که Hash مربوط به Url آن‌ها بیشتر از 10 است (صرفا جهت نمایش این قابلیت جدید):
using (var context = new BloggingContext())
{
   var blogs = context.Blogs
     .Where(blog => blog.Url.ComputeHash() >= 10)
     .ToList();
   Console.WriteLine(blogs.First().Url);
}
اگر این کوئری را اجرا کنیم، یک چنین خروجی SQL ایی تولید خواهد شد و همچنین برنامه کرش هم نمی‌کند:
SELECT [blog].[BlogId], [blog].[Url]
   FROM [Blogs] AS [blog]
به این معنا که در ارزیابی‌های سمت کلاینت:
الف) مفسر LINQ در EF Core، شروع به ارزیابی کوئری نوشته شده می‌کند و هرجائیکه متدی را یافت و از درک آن عاجز بود (معادل SQL ایی را برای آن نیافت)، آن‌را از کوئری حذف می‌کند.
ب) کوئری SQL نهایی بدون متد ComputeHash بر روی بانک اطلاعاتی اجرا شده و نتیجه به سمت کلاینت بازگشت داده می‌شود. به همین جهت است که در خروجی SQL فوق خبری از متد ComputeHash نیست.
ج) اکنون که EF Core اطلاعات لازم را از سمت سرور دریافت کرده‌است، متد ComputeHash را در سمت کلاینت بر روی این نتیجه‌ی دریافتی اعمال می‌کند. یعنی مرحله‌ی آخر همان LINQ to Objects متداول خواهد بود.
به این ترتیب است که EF Core قابلیت اجرای هر نوع متدی را که معادل SQL ایی برای آن وجود ندارد، خواهد یافت.


چگونه متوجه شویم که ارزیابی سمت کلاینت رخ داده‌است؟

EF Core این قابلیت را دارد تا گزارش کاملی را از ارزیابی‌های سمت کلاینت صورت گرفته ارائه دهد. هرچند در مثال فوق متد الحاقی ComputeHash بسیار واضح است، اما برای نمونه متد string.Join نیز معادل SQL ایی ندارد:
var idUrls = context.Blogs
   .Select(b => new
   {
      IdUrlString = string.Join(", ", b.BlogId, b.Url),
   }).ToList();
این مثال بدون مشکل توسط EF Core و قابلیت جدید ارزیابی سمت کلاینت آن اجرا می‌شود، اما بهتر است از وقوع یک چنین رخ‌دادهایی مطلع شویم:
    public class BloggingContext : DbContext
    {
        public BloggingContext()
        { }

        public BloggingContext(DbContextOptions options)
            : base(options)
        { }

        public DbSet<Blog> Blogs { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            if (!optionsBuilder.IsConfigured)
            {
                optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Demo.ClientSideEvaluation;Trusted_Connection=True;");
                optionsBuilder.ConfigureWarnings(warnings =>
                {
                    warnings.Log(CoreEventId.IncludeIgnoredWarning);
                    warnings.Log(RelationalEventId.QueryClientEvaluationWarning);
                });
            }
        }
    }
برای این منظور تنها کافی است درخواست فعالسازی لاگ کردن QueryClientEvaluationWarning را در قسمت ConfigureWarnings آن ارائه دهیم. در این حالت اگر برنامه را مجددا اجرا کنیم، ابتدا یک چنین خروجی لاگ می‌شود:
 warn: Microsoft.EntityFrameworkCore.Query[200500]
The LINQ expression 'where ([blog].Url.ComputeHash() >= 10)' could not be translated and will be evaluated locally.
عنوان کرده‌است که قابلیت ترجمه‌ی ComputeHash را به SQL نداشته و آن‌را در نهایت به صورت محلی و در سمت کلاینت محاسبه می‌کند.

اگر می‌خواهید ارزیابی سمت کلاینت را ممنوع کنید، در تنظیمات فوق warnings.Log را به warnings.Throw تغییر دهید. این مورد سبب خواهد شد تا اگر برنامه به این نوع ارزیابی‌ها رسید، با یک استثناء متوقف شود (شبیه به حالت EF 6.x).


تاثیر ارزیابی‌های سمت کلاینت بر روی کارآیی برنامه

هرچند قابلیت ارزیابی‌های سمت کلاینت بسیار مفید است اما باید دقت داشت:
الف) در این حالت چون ابتدا متدهایی که قابلیت ارزیابی در سمت سرور را دارا نیستند، حذف خواهند شد، ممکن است تمام رکوردها به سمت کلاینت بازگشت داده شده و سپس فیلترینگ نهایی در سمت کلاینت صورت گیرد. مانند مثال محاسبه‌ی hash که در SQL تولیدی آن، خبری از قسمت where نیست و این شرط در انتهای کار، در سمت کلاینت و به صورت LINQ to Objects اعمال می‌شود.
ب) این قابلیت ممکن است برنامه نویس‌ها را از تفکر در مورد یافتن روش‌های محاسباتی سمت سرور دور کند. برای مثال هر چند مثال string.Join نوشته شده در سمت کلاینت محاسبه خواهد شد و این کوئری بدون مشکل اجرا می‌شود، اما اگر آن‌را به صورت ذیل جایگزین کنیم:
var idUrls2 = context.Blogs
   .Select(b => new
   {
     IdUrlString = b.BlogId + "," + b.Url
   }).ToList();
اینبار به یک خروجی SQL قابل محاسبه‌ی در سمت سرور، خواهیم رسید:
SELECT (CAST([b].[BlogId] AS nvarchar(max)) + N',') + [b].[Url] AS [IdUrlString]
FROM [Blogs] AS [b]
به همین جهت حداقل لاگ کردن ارزیابی‌های سمت کلاینت را روشن کنید تا از وقوع یک چنین مسایلی مطلع گردید.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید: ClientSideEvaluation.zip
مطالب
ویژگی های کمتر استفاده شده در NET. - بخش سوم

__arglist __reftype __makeref __refvalue کلمات کلیدی

در حالیکه، ویرایشگر Visual Studio این کلمات را به صورت رنگی و جزء کلمات کلیدی نمایش می‌دهد، ولی به دلیل عدم وجود مستندات برای این کلمات کلیدی، برای استفاده از آنها باید مراقب باشید؛ چرا که ممکن است به اندازه کافی تست نشده باشند. 
شما می‌توانید با استفاده از کلمه کلیدی makeref__ یک TypeReference را از یک متغیر، ایجاد کنید. با استفاده از کلمه کلیدی reftype__ می‌توانید نوع اصلی از متغیری را که TypeReference را از آن ایجاد کرده اید، استخراج کنید. در انتها می‌توانید با استفاده از کلمه کلیدی refvalue__ مقدار متغیر را از TypeReference ایجاد شده، بدست آورد. با استفاده از کلمه کلیدی arglist__ همانند کلمه کلیدی params می‌توانید به لیستی از پارامترهای یک تابع دسترسی داشته باشید.
var i = 28;
TypedReference tr = __makeref( i );
Type t = __reftype( tr );
Console.WriteLine( t );
int rv = __refvalue( tr, int );
Console.WriteLine( rv );
ArglistTest.DisplayNumbers( __arglist( 1, 2, 3, 5, 6 ) );
و برای استفاده از arglist__ کلاس ArglistTest را پیاده سازی میکنیم.
public static class ArglistTest
{
    public static void DisplayNumbers( __arglist )
    {
        var ai = new ArgIterator( __arglist );
        while ( ai.GetRemainingCount() > 0 )
        {
            var tr = ai.GetNextArg();
            Console.WriteLine( TypedReference.ToObject( tr ) );
        }
    }
}
شی ArgIterator لیست آرگومان‌ها را از اولین آرگومان اختیاری، شروع به شمارش می‌کند. این سازنده برای استفاده در زبان C++/C ایجاد شده است.

Environment.NewLine

رشته خط جدید (↵  Enter) تعریف شده در محیط در حال استفاده را می‌توان با استفاده از این دستور بدست آورد.
Console.WriteLine( "NewLine: {0}first line{0}second line{0}third line", Environment.NewLine );
این رشته شامل "r\n\" برای پلتفرم‌های غیر یونیکس و رشته "n\" برای پلتفرم‌های یونیکس است.

ExceptionDispatchInfo

ExceptionDispatchInfo بیان کننده یک استثناء در یک نقطه خاص از کد، که وضعیت آن قبلا کپچر شده‌است، می‌باشد. شما می‌توانید با استفاده از متد ExceptionDispatchInfo.Throw  (در فضای نام System.Runtime.ExceptionServices) یک استثناء را (با حفظ Stack Trace اصلی) ایجاد کنید.
ExceptionDispatchInfo possibleException = null;
try
{
    int.Parse( "a" );
}
catch ( FormatException ex )
{
    possibleException = ExceptionDispatchInfo.Capture( ex );
}
possibleException?.Throw();


Debug.Assert & Debug.WriteIf & Debug.Indent 

Debug.Assert  – بررسی صحت شرط تعیین شده و در صورت false بودن شرط، نمایش پیام نوشته شده به همراه call stack مربوطه می‌شود.
Debug.Assert(1 == 0, "عدد 1 برابر با 0 نیست");
و خروجی آن در تصویر زیر قابل مشاهده است:

Debug.WriteIf  – در صورت صحت شرط تعیین شده، پیام مشخص شده‌ای را در پنجره output نشان می‌دهد.
Debug.WriteIf( 1 == 1, "display message in output window :D" );

Debug.Indent/Debug.Unindent - برای افزایش/کاهش یک واحد تورفتگی در خروجی نمایش داده شده در پنجره Output، استفاده می‌شود.
Debug.WriteLine("تست تورفتگی");
Debug.Indent();
Debug.WriteLine("یک واحد افزایش داده شد");
Debug.Unindent();
Debug.WriteLine("یک واحد کاهش داده شد");
Debug.WriteLine("پایان تست");

مطالب
بهبود کارآیی LINQ در دات نت 7
LINQ یا همان Language-Integrated Query، یک زبان ساده‌ی کوئری نوشتن یکپارچه‌ی با دات نت است. به کمک آن می‌توان اعمال پیچیده‌ای را بر روی اشیاء، به زبانی ساده بیان کرد و امروزه تقریبا توسط تمام توسعه دهندگان دات نت مورد استفاده قرار می‌گیرد. اما ... این سادگی، بهایی را نیز به همراه دارد: کمتر بودن سرعت اجرا و همچنین افزایش مصرف حافظه. با توجه به گستردگی استفاده‌ی از LINQ، اگر بهبودی در این زمینه حاصل شود، بر روی کارآیی تمام برنامه‌های دات نتی تاثیر خواهد گذاشت و این امر در دات نت 7 محقق شده‌است. کارآیی متدهای LINQ to Objects در دات نت 7 (مانند متدهای Enumerable.Max, Enumerable.Min, Enumerable.Average, Enumerable.Sum) به شدت افزایش یافته و این افزایش گاهی حتی بیشتر از 10 برابر نسبت به نگارش‌های قبلی دات نت است؛ اما چگونه به چنین کارآیی رسیده‌اند؟


تدارک یک آزمایش برای بررسی میزان افزایش کارآیی متدهای LINQ در دات نت 7

در ادامه یک آزمایش ساده‌ی بررسی کارآیی متدهای Enumerable.Max, Enumerable.Min, Enumerable.Average, Enumerable.Sum را با استفاده از کتابخانه‌ی معروف BenchmarkDotNet مشاهده می‌کنید:
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Collections.Generic;
using System.Linq;


[MemoryDiagnoser(displayGenColumns: false)]
public partial class Program
{
  static void Main(string[] args) =>
    BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);

  [Params (10, 10000)]
  public int Size { get; set; }
  private IEnumerable<int> items;

  [GlobalSetup]
  public void Setup()
  {
    items = Enumerable.Range(1, Size).ToArray();
  }  

  [Benchmark]
  public int Min() => items.Min();

  [Benchmark]
  public int Max() => items.Max();

  [Benchmark]
  public double Average() => items.Average();

  [Benchmark]
  public int Sum() => items.Sum();
}
برای آزمایش آن، یکبار target framework پروژه را بر روی net6.0 و بار دیگر بر روی net7.0 قرار داده و برنامه را اجرا می‌کنیم. خلاصه‌ی مفهومی نتایج حاصل به صورت زیر است که ... شگفت‌انگیز هستند!
در مورد کار با آرایه‌ها:


- زمان اجرای یافتن Min در آرایه‌های کوچک، در دات نت 7، نسبت به دات نت 6، حدودا 10 برابر کاهش یافته و اگر این آرایه بزرگتر شود و برای مثال حاوی 10 هزار المان باشد، این زمان 20 برابر کاهش یافته‌است.
- این کاهش زمان‌ها برای سایر متدهای LINQ نیز تقریبا به همین صورت است؛ منها متد Sum که اندازه‌ی آرایه، تاثیری را بر روی نتیجه‌ی نهایی ندارد.
- همچنین در دات نت 7، با فراخوانی متدهای LINQ، افزایش حافظه‌ای مشاهده نمی‌شود.

در مورد کار با لیست‌ها:


- در دات نت 6، اعمال صورت گرفته‌ی توسط LINQ بر روی آرایه‌ها، نسبت به لیست‌ها، همواره سریعتر است.
- در دات نت 7 هم در مورد مجموعه‌های کوچک، وضعیت همانند دات نت 6 است. اما اگر مجموعه‌ها بزرگتر شوند، تفاوتی بین مجموعه‌ها و آرایه‌ها وجود ندارد و حتی وضعیت مجموعه‌ها بهتر است: کارآیی کار با لیست‌ها 32 برابر بیشتر شده‌است!


اما چگونه در دات نت 7، چنین بهبود کارآیی خیره‌کننده‌ای در متدهای LINQ حاصل شده‌است؟

برای بررسی چگونگی بهبود کارآیی متدهای LINQ در دات نت 7 باید به نحوه‌ی پیاده سازی آن‌ها در نگارش‌های مختلف دات نت مراجعه کرد. برای مثال پیاده سازی متد الحاقی Min تا دات نت 6 به صورت زیر است:
public static int Min(this IEnumerable<int> source)
{
  if (source == null)
  {
    ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
  }

  int value;
  using (IEnumerator<int> e = source.GetEnumerator())
  {
    if (!e.MoveNext())
    {
      ThrowHelper.ThrowNoElementsException();
    }

    value = e.Current;
    while (e.MoveNext())
    {
      int x = e.Current;
      if (x < value)
      {
        value = x;
      }
    }
  }
  return value;
}
این متد نسبتا ساده‌است. یک IEnumerable را دریافت کرده و سپس با استفاده از متد MoveNext، مقدار فعلی را با مقدار بعدی مقایسه می‌کند. در این مقایسه، کوچکترین مقدار ذخیره می‌شود تا در نهایت به انتهای مجموعه برسیم.
اما ... پیاده سازی این متد در دات نت 7 متفاوت است:
public static int Min(this IEnumerable<int> source) => MinInteger(source);

private static T MinInteger<T>(this IEnumerable<T> source)
  where T : struct, IBinaryInteger<T>
{
  T value;

  if (source.TryGetSpan(out ReadOnlySpan<T> span))
  {
    if (Vector.IsHardwareAccelerated && 
        span.Length >= Vector<T>.Count * 2)
    {
      .... // Optimized implementation
      return ....;
    }
  }
  .... //Implementation as in .NET 6
}
در اینجا در ابتدا سعی می‌شود تا یک ReadOnlySpan از مجموعه‌ی ارائه شده، تهیه شود. اگر این کار میسر نشد، کدهای همان روش قبلی دات نت 6 که توضیح داده شد، اجرا می‌شود. البته در آزمایشی که ما تدارک دیدیم، چون از لیست‌ها و آرایه‌ها استفاده شده بود، همواره امکان تهیه‌ی یک ReadOnlySpan از آن‌ها میسر است. بنابراین به قسمت اجرایی همانند دات نت 6 نمی‌رسیم.
اما ... ReadOnlySpan چیست؟ نوع‌های Span و ReadOnlySpan، یک ناحیه‌ی پیوسته‌ی مدیریت شده و مدیریت نشده‌ی حافظه را بیان می‌کنند. یک Span از نوع ref struct است؛ یعنی تنها می‌تواند بر روی stack قرار گیرد که مزیت آن، عدم نیاز به تخصیص حافظه‌ی اضافی و بهبود کارآیی است. همچنین ساختار داخلی Span در سی شارپ 11 اندکی تغییر کرده‌است که در آن از ref fields جهت دسترسی امن به این ناحیه‌ی از حافظه استفاده می‌شود. پیشتر از نوع داخلی ByReference برای اشاره به ابتدای این ناحیه‌ی از حافظه استفاده می‌شد که به همراه بررسی امنیتی در این باره نبود.

پس از دریافت ReadOnlySpan، به سطر زیر می‌رسیم:
if (Vector.IsHardwareAccelerated && span.Length >= Vector<T>.Count * 2)
که بررسی می‌کند آیا سخت افرار فعلی از قابلیت‌های SIMD برخوردار است یا خیر؟ اگر بله، اینبار با استفاده از ریاضیات برداری شتاب یافته‌ی توسط سخت افزار، محاسبات را انجام می‌دهد:
private static T MinInteger<T>(this IEnumerable<T> source)
where T : struct, IBinaryInteger<T>
{
  .... 
  if (Vector.IsHardwareAccelerated && span.Length >= Vector<T>.Count * 2)
  {
    var mins = new Vector<T>(span);
    index = Vector<T>.Count;
    do
    {
      mins = Vector.Min(mins, new Vector<T>(span.Slice(index)));
      index += Vector<T>.Count;
    }
    while (index + Vector<T>.Count <= span.Length);

    value = mins[0];
    for (int i = 1; i < Vector<T>.Count; i++)
    {  
      if (mins[i] < value)
      {
        value = mins[i];
      }
    }
  ....
}
بنابراین به صورت خلاصه در دات نت 7 با استفاده از بکارگیری نوع‌های ویژه‌ی Span و نوع‌های برداری شتاب‌یافته‌ی توسط اکثر سخت افزارهای امروزی، سبب بهبود قابل ملاحظه‌ی کارآیی متدهای LINQ شده‌اند.
مطالب
پیدا کردن آیتم‌های تکراری در یک لیست به کمک LINQ

گاهی از اوقات نیاز می‌شود تا در یک لیست، آیتم‌های تکراری موجود را مشخص کرد. به صورت پیش فرض متد Distinct برای حذف مقادیر تکراری در یک لیست با استفاده از LINQ موجود است که البته آن‌هم اما و اگرهایی دارد که در ادامه به آن پرداخته خواهد شد، اما باز هم این مورد پاسخ سؤال اصلی نیست (نمی‌خواهیم موارد تکراری را حذف کنیم).

برای حذف آیتم‌های تکراری از یک لیست جنریک می‌توان متد زیر را نوشت:
public static List<T> RemoveDuplicates<T>(List<T> items)
{
return (from s in items select s).Distinct().ToList();
}
برای مثال:
public static void TestRemoveDuplicates()
{
List<string> sampleList =
new List<string>() { "A1", "A2", "A3", "A1", "A2", "A3" };
sampleList = RemoveDuplicates(sampleList);
foreach (var item in sampleList)
Console.WriteLine(item);
}
این متد بر روی لیست‌هایی با نوع‌های اولیه مانند string‌ و int و امثال آن درست کار می‌کند. اما اکنون مثال زیر را در نظر بگیرید:
public class Employee
{
public int ID { get; set; }
public string FName { get; set; }
public int Age { get; set; }
}

public static void TestRemoveDuplicates()
{
List<Employee> lstEmp = new List<Employee>()
{
new Employee(){ ID=1, Age=20, FName="F1"},
new Employee(){ ID=2, Age=21, FName="F2"},
new Employee(){ ID=1, Age=20, FName="F1"},
};

lstEmp = RemoveDuplicates<Employee>(lstEmp);

foreach (var item in lstEmp)
Console.WriteLine(item.FName);
}
اگر متد TestRemoveDuplicates را اجرا نمائید، رکورد تکراری این لیست جنریک حذف نخواهد شد؛ زیرا متد distinct بکارگرفته شده نمی‌داند اشیایی از نوع کلاس سفارشی Employee را چگونه باید با هم مقایسه نماید تا بتواند موارد تکراری آن‌ها را حذف کند.
برای رفع این مشکل باید از آرگومان دوم متد distinct جهت معرفی وهله‌ای از کلاسی که اینترفیس IEqualityComparer را پیاده سازی می‌کند، کمک گرفت.
public static IEnumerable<TSource> Distinct<TSource>(this IEnumerable<TSource> source, IEqualityComparer<TSource> comparer);
که نمونه‌ای از پیاده سازی آن به شرح زیر می‌تواند باشد:

public class EmployeeComparer : IEqualityComparer<Employee>
{
public bool Equals(Employee x, Employee y)
{
//آیا دقیقا یک وهله هستند؟
if (Object.ReferenceEquals(x, y)) return true;

//آیا یکی از وهله‌ها نال است؟
if (Object.ReferenceEquals(x, null) ||
Object.ReferenceEquals(y, null))
return false;

return x.Age == y.Age && x.FName == y.FName && x.ID == y.ID;
}

public int GetHashCode(Employee obj)
{
if (Object.ReferenceEquals(obj, null)) return 0;
int hashTextual = obj.FName == null ? 0 : obj.FName.GetHashCode();
int hashDigital = obj.Age.GetHashCode();
return hashTextual ^ hashDigital;
}
}
اکنون اگر یک overload برای متد RemoveDuplicates با درنظر گرفتن IEqualityComparerتهیه کنیم، به شکل زیر خواهد بود:
public static List<T> RemoveDuplicates<T>(List<T> items, IEqualityComparer<T> comparer)
{
return (from s in items select s).Distinct(comparer).ToList();
}
به این صورت متد آزمایشی ما به شکل زیر (که وهله‌ای از کلاس EmployeeComparer‌ به آن ارسال شده) تغییر خواهد کرد:
public static void TestRemoveDuplicates()
{
List<Employee> lstEmp = new List<Employee>()
{
new Employee(){ ID=1, Age=20, FName="F1"},
new Employee(){ ID=2, Age=21, FName="F2"},
new Employee(){ ID=1, Age=20, FName="F1"},
};

lstEmp = RemoveDuplicates(lstEmp, new EmployeeComparer());

foreach (var item in lstEmp)
Console.WriteLine(item.FName);
}
پس از این تغییر، حاصل این متد تنها دو رکورد غیرتکراری می‌باشد.

سؤال: برای یافتن آیتم‌های تکراری یک لیست چه باید کرد؟
احتمالا مقاله "روش‌هایی برای حذف رکوردهای تکراری" را به خاطر دارید. اینجا هم می‌توان کوئری LINQ ایی را نوشت که رکوردها را بر اساس سن، گروه بندی کرده و سپس گروه‌هایی را که بیش از یک رکورد دارند، انتخاب نماید.
public static void FindDuplicates()
{
List<Employee> lstEmp = new List<Employee>()
{
new Employee(){ ID=1, Age=20, FName="F1"},
new Employee(){ ID=2, Age=21, FName="F2"},
new Employee(){ ID=1, Age=20, FName="F1"},
};

var query = from c in lstEmp
group c by c.Age into g
where g.Count() > 1
select new { Age = g.Key, Count = g.Count() };

foreach (var item in query)
{
Console.WriteLine("Age {0} has {1} records", item.Age, item.Count);
}
}


Vote on iDevCenter
مطالب
KnockoutJs #6 - آشنایی با extender ها
پیاده سازی Extender
همان طور که در پست‌های و مثال‌های قبلی مشاهده شد با استفاده از Ko.Observable توانستیم عملیات مقید سازی را به کمک ویژگی‌های خواندن و نوشتن ساده، پیاده سازی نماییم. اما قصد داریم در طی عملیات نوشتن به جای یک tracking ساده تغییرات، بتوانیم یک سری عملیات مشخص را نیز اجرا نماییم. چیزی شبیه به AOP  دنیای back-end . یعنی بتوانیم کد اصلی برنامه را در هنگام عملیات خواندن و نوشتن خاصیت‌ها، با یک سری کد مورد نظر مزین نماییم. برای این کار مفهوم extender در KO تعبیه شده است.

برای ساخت یک extender کافیست تابع مورد نظر را به عنوان آرگومان به شی ko.extenders پاس دهیم. پارامتر اول این تابع، شیء observable شده مورد نظر و پارامتر دوم آن، شیء option برای انجام یک سری تنظیمات یا فرستادن مقادیر مورد نظر به تابع است. خروجی این تابع نیز می‌تواند یک شی observable یا حتی یک شی computed/observable نیز باشد.
یک مثال ساده برای extender‌ها به صورت زیر است:
ko.extenders.logOpt = function(target, option) {
    target.subscribe(function(newValue) {
       console.log(option + ": " + newValue);
    });
    return target;
};
در مثال بالا با ایجاد یک extender برای شی target که خود آن به عنوان آرگومان به تابع پاس داده می‌شود، به ازای هر تغییر در مقدار شیء target، مقدار جدید نیز در console نمایش داده خواهد شد. مقدار چاپ شده در console برابر است با مقدار شی option + مقدار جدید شی target به ازای هر تغییر.

برای استفاده از این extender کافیست آن را در هنگام تعریف تابع observable برای خواص، به صورت زیر فراخوانی نمایید:
this.firstName = ko.observable("Masoud").extend({logOpt: "my first name"});
مقدار 'my first name' همان مقدار پاس داده شده در قالب شی option است. در نتیجه خروجی console به صورت زیر خواهد شد:
my first name : Masoud

پیاده سازی یک extender جهت اعلام هشدار برای مقادیر منفی

برای اینکه هنگام ورود داده‌ها توسط کاربر، بتوانیم با ورود مقادیر منفی یک هشدار (تغییر رنگ ورودی) اعلام کنیم، می‌توان به صورت زیر عمل نمود:
ko.extenders.negativeValueWarn = function (target, option) {
    target.hasWarning = ko.observable();

    function warn(newValue) {
        if(newValue && newValue.substring) {
            newValue = parseFloat(newValue);
        }
        target.hasWarning(newValue < 0 ? true : false);
    }

    warn(target());

    target.subscribe(warn);

    return target;
};
تابع warn با در اختیار داشتن مقدار جدید و بررسی منفی یا مثبت بودن آن  نتیجه را به تابع set شی  hasWarning ارسال می‌کند.

یاد آوری
: در KO برای انتساب مقدار جدید به خواصی که به صورت observable تعریف شده اند به صورت زیر:
target(NewValue) => فراخوانی به صورت تابع و پاس دادن مقدار جدید به آن
و برای به به دست آوردن این مقادیر از اشیای Observable به صورت زیر عمل می‌نماییم:
target() => فراخوانی به صورت تابع بدون آرگومان
خروجی مثال بالا

پیاده سازی یک extender برای انتساب مقادیر Boolean به Radio Button ها
برای اینکه radio button‌ها نیز بتوانند فقط با مقادیر Boolean مقدار دهی شوند و از طرفی در هنگام عملیات مقید سازی و ارسال نتایج در قالب شی Json به سرور، بدون هیچ گونه تغییر و محاسبات مقادیر مورد نظر به صورت true/false (از نوع Boolean) باشند می‌توان به صورت زیر عمل نمود:
ko.extenders["booleanValue"] = function (target) {
    target.formattedValue = ko.computed({
        read: function () {
            if (target() === true) return "True";
            else if (target() === false) return "False";
        },
        write: function (newValue) {
            if (newValue) {
                if (newValue === "False") target(false);
                else if (newValue === "True") target(true);
            }
        }
    });

    target.formattedValue(target());
    return target;
};
در کد بالا یک sub-observable به نام formattedValue ایجا شده است و همان طور که ملاحظه می‌نمایید از نوع computed می‌باشد. در تابع read آن (هنگام عملیات مقید سازی برای خواندن مقادیر) اگر مقدار مورد نظر برابر با true از نوع boolean بود مقدار True (به صورت string) و اگر برابر با false بود مقدار False برگشت داده می‌شود. هنگام عملیات write بر عکس عمل خواهد شد.
با فرض اینکه کد‌های Html صفحه به صورت زیر است:
<span>Do you want fries with that?</span>
<label>
  <input type="radio" name="question" value="True"
             data-bind="value: myValue.formattedValue" /> Yes
</label>
<label>
  <input type="radio" name="question" value="False"
             data-bind="value: myValue.formattedValue" /> No
</label>
 Json Object مورد نظر که مقادیر boolean در آن به صورت true یا false است و بعد از عملیات مقید سازی در هنگام انتساب مقادیر، آن‌ها را تبدیل به True یا False برای المان‌های Html می‌کند. و در هنگام ورود اطلاعات توسط کاربر و انتساب آن‌ها به شی Json ، مقادیر تبدیل به true یا false از نوع boolean خواهند شد.
برای استفاده از آن کافیست به صورت زیر عمل نمایید:
 this.myValue= ko.observable(false).extend({ booleanValue: null });