مطالب دوره‌ها
نکاتی درباره برنامه نویسی دستوری(امری)
در این فصل نکاتی را درباره برنامه نویسی دستوری در #F فرا خواهیم گرفت. برای شروع از mutale خواهیم گفت.

mutable
Keyword
در فصل دوم(شناسه ها) گفته شد که برای یک شناسه امکان تغییر مقدار وجود ندارد. اما در #F راهی وجود دارد که در صورت نیاز بتوانیم مقدار یک شناسه را تغییر دهیم.در #F هرگاه بخواهیم شناسه ای تعریف کنیم که بتوان در هر زمان مقدار شناسه رو به دلخواه تغییر داد از کلمه کلیدی mutable کمک می‌گیریم و برای تغییر مقادیر شناسه‌ها کافیست از علامت (->) استفاده کنیم. به یک مثال در این زمینه دقت کنید:
#1 let mutable phrase = "Can it change? "

#2 printfn "%s" phrase

#3 phrase <- "yes, it can."

#4 printfn "%s" phrase


در خط اول یک شناسه را به صورت mutable(تغییر پذیر) تعریف کردیم و در خط سوم با استفاده از (->) مقدار شناسه رو update کردیم. خروجی مثال بالا به صورت زیر است:
Can it change?  
yes, it can.

نکته اول: در این روش هنگام update کردن مقدار شناسه حتما باید مقدار جدید از نوع مقدار قبلی باشد در غیر این صورت با خطای کامپایلری متوقف خواهید شد.
#1 let mutable phrase = "Can it change?  "

#3 phrase <- 1

اجرای کد بالا خطای زیر را به همراه خواهد داشت.(خطا کاملا واضح است و نیاز به توضیح دیده نمی‌شود)
Prog.fs(9,10): error: FS0001: This expression has type
int
but is here used with type
string
نکته دوم :ابتدا به مثال زیر توجه کنید.
let redefineX() =
let x = "One"
printfn "Redefining:\r\nx = %s" x
if true then
   let x = "Two"
printfn "x = %s" x
printfn "x = %s" x

در مثال بالا در تابع redefineX یک شناسه به نام x تعریف کردم با مقدار "One". یک بار مقدار شناسه x رو چاپ می‌کنیم و بعد دوباره بعد از شرط true یک شناسه دیگر با همون نام یعنی x تعریف شده است و در انتها هم دو دستور چاپ. ابتدا خروجی مثال بالا رو با هم مشاهده می‌کنیم.
Redefining:
x = One
x = Two
x = One
همان طور که میبینید شناسه دوم x بعد از تعریف دارای مقدار جدید Two بود و بعد از اتمام محدوده(scope) مقدار x دوباره به One تغییر کرد.(بهتر است بگوییم منظور از دستور print x سوم اشاره به شناسه x اول برنامه است). این رفتار مورد انتظار ما در هنگام استفاده از روش تعریف مجدد شناسه هاست. حال به بررسی رفتار muatable در این حالت می‌پردازیم.
let mutableX() =
let mutable x = "One"
printfn "Mutating:\r\nx = %s" x
if true then
   x <- "Two"
printfn "x = %s" x
printfn "x = %s" x
تنها تفاوت در استفاده از mutable keyword و (->) است. خروجی مثال بالا نیز به صورت زیر خواهد بود. کاملا واضح است که مقدار شناسه x بعد از تغییر و اتمام محدوده(scope) هم چنان Two خواهد بود.
Mutating:
x = One
x = Two
x = Two

Reference Cells

روشی برای استفاده از شناسه‌ها به صورت mutable است. با این روش می‌تونید شناسه هایی تعریف کنید که امکان تغییر مقدار برای اون‌ها وجود دارد. زمانی که از این روش برای مقدار دهی به شناسه‌ها استفاده کنیم یک کپی از مقدار مورد نظر به شناسه اختصاص داده می‌شود نه آدرس مقدار در حافظه.
به جدول زیر توجه کنید:

 Member Or Field
Description
 Definition
(derefence operator)!
 مقدار مشخص شده را برگشت می‌دهد
 

let (!) r = r.contents 

 (Assignment operator)=: مقدار مشخص شده را تغییر می‌دهد
 

let (:=) r x = r.contents <- x 

ref operator
 یک مقدار را در یک reference cell  جدید کپسوله می‌کند
 

let ref x = { contents = x } 

Value Property
 برای عملیات get  یا set  مقدار مشخص شده
 

member x.Value = x.contents 

 contents record field
 برای عملیات get  یا set  مقدار مشخص شده

let ref x = { contents = x } 

  یک مثال:
let refVar = ref 6

refVar := 50
printfn "%d" !refVar
خروجی مثال بالا 50 خواهد بود.
let xRef : int ref = ref 10

printfn "%d" (xRef.Value)
printfn "%d" (xRef.contents)

xRef.Value <- 11
printfn "%d" (xRef.Value)
xRef.contents <- 12
printfn "%d" (xRef.contents)
خروجی مثال بالا:
10
10
11
12 

خصیصه اختیاری در #F
در #F زمانی از خصیصه اختیاری استفاده می‌کنیم که برای یک متغیر مقدار وجود نداشته باشد. option  در #F نوعی است که می‌تواند هم مقدار داشته باشد و هم نداشته باشد.
let keepIfPositive (a : int) = if a > 0 then Some(a) else None
از None زمانی استفاده می‌کنیم که option مقدار نداشته باشد و از Some  زمانی استفاده می‌کنیم که option مقدار داشته باشد.
let exists (x : int option) = 
    match x with
    | Some(x) -> true
    | None -> false
در مثال بالا ورودی تابع exists از نوع int و به صورت اختیاری تعریف شده است.(معادل با ?int یا<Nullable<int در #C) در صورتی که x مقدار داشته باشد مقدار true در غیر این صورت مقدار false را برگشت می‌دهد.

چگونگی استفاده  از option
مثال
let rec tryFindMatch pred list =
    match list with
    | head :: tail -> if pred(head)
                        then Some(head)
                        else tryFindMatch pred tail
    | [] -> None

let result1 = tryFindMatch (fun elem -> elem = 100) [ 200; 100; 50; 25 ] //برابر با 100 است

let result2 = tryFindMatch (fun elem -> elem = 26) [ 200; 100; 50; 25 ]// برابر با None است
یک تابع به نام tryFindMatch داریم با دو پارامتر ورودی. با استفاده از الگوی Matching از عنصر ابتدا تا انتها را در لیست (پارامتر list) با مقدار پارامتر pred مقایسه می‌کنیم. اگر مقادیر برابر بودند مقدار head در غیر این صورت None(یعنی option مقدار ندارد) برگشت داده می‌شود.
یک مثال کاربردی تر
open System.IO
let openFile filename =
    try 
        let file = File.Open (filename, FileMode.Create)
        Some(file)
    with
        | ex -> eprintf "An exception occurred with message %s" ex.Message
                None
در مثال بالا از option‌ها برای بررسی وجود یا عدم وجود فایل‌های فیزیکی استفاده کردم.

Enumeration
تقریبا همه با نوع داده شمارشی یا enums آشنایی دارند. در اینجا فقط به نحوه پیاده سازی آن در #F می‌پردازیم. ساختار کلی تعریف آن به صورت زیر است:
type enum-name =
   | value1 = integer-literal1
   | value2 = integer-literal2
   ...
یک مثال از تعریف:
type Color =
   | Red = 0
   | Green = 1
   | Blue = 2
نحوه استفاده
let col1 : Color = Color.Red
enums فقط از انواع داده ای sbyte, byte, int16, uint16, int32, uint32, int64, uint16, uint64, char پشتیبانی می‌کند که البته مقدار پیش فرض آن Int32 است. در صورتی که بخواهیم صریحا نوع داده ای را ذکر کنیم به صورت زیر عمل می‌شود.
type uColor =
   | Red = 0u
   | Green = 1u
   | Blue = 2u
let col3 = Microsoft.FSharp.Core.LanguagePrimitives.EnumOfValue<uint32, uColor>(2u)

توضیح درباره use
در دات نت خیلی از اشیا هستند که اینترفیس IDisposable رو پیاده سازی کرده اند. این بدین معنی است که حتما یک متد به نام dispose برای این اشیا وجود دارد که فراخوانی آن به طور قطع باعث بازگرداندن حافظه ای که در اختیار این کلاس‌ها بود می‌شود. برای راحتی کار در #C یک عبارت به نام using وجود دارد که در انتها بلاک متد dispose شی مربوطه را فراخوانی می‌کند.
using(var writer = new StreamWriter(filePath))
{
   
}
در #F نیز امکان استفاده از این عبارت با اندکی تفاوت وجود دارد.مثال:
let writeToFile fileName =
    use sw = new System.IO.StreamWriter(fileName : string)
    sw.Write("Hello ")
Units Of Measure
در #F اعداد دارای علامت و اعداد شناور دارای وابستگی با واحد‌های اندازه گیری هستند که به نوعی معرف اندازه و حجم و مقدار و ... هستند. در #F شما مجاز به تعریف واحد‌های اندازه گیری خاص خود هستید و در این تعاریف نوع عملیات اندازه گیری را مشخص می‌کنید. مزیت اصلی استفاده از این روش جلوگیری از رخ دادن خطاهای کامپایلر در پروژه است. ساختار کلی تعریف:
[<Measure>] type unit-name [ = measure ]
یک مثال از تعریف واحد cm:
[<Measure>] type cm
مثالی از تعریف میلی لیتر:
[<Measure>] type ml = cm^3
برای استفاده از این واحد‌ها می‌تونید به روش زیر عمل کنید.
let value = 1.0<cm>
توابع تبدیل واحد ها
قدرت اصلی واحد‌های اندازه گیری  #F در توابع تبدیل است. تعریف توابع تبدیل به صورت زیر می‌باشد:
[<Measure>] type g                 تعریف واحد گرم
[<Measure>] type kg               تعریف واحد کیلوگرم
let gramsPerKilogram : float<g kg^-1> = 1000.0<g/kg>    تعریف تابع تبدیل
یک مثال دیگر :
[<Measure>] type degC // دما بر حسب سلسیوس
[<Measure>] type degF // دما بر حسب فارنهایت

let convertCtoF ( temp : float<degC> ) = 9.0<degF> / 5.0<degC> * temp + 32.0<degF> // تابع تبدیل سلسیوس به فارنهایت
let convertFtoC ( temp: float<degF> ) = 5.0<degC> / 9.0<degF> * ( temp - 32.0<degF>) // تابع تبدیل فارنهایت به سلسیوس

let degreesFahrenheit temp = temp * 1.0<degF> // درجه به فارنهایت
let degreesCelsius temp = temp * 1.0<degC> // درجه به سلسیوس

printfn "Enter a temperature in degrees Fahrenheit." 
let input = System.Console.ReadLine()
let mutable floatValue = 0.
if System.Double.TryParse(input, &floatValue)// اگر ورودی عدد بود
   then 
      printfn "That temperature in Celsius is %8.2f degrees C." ((convertFtoC (degreesFahrenheit floatValue))/(1.0<degC>))
   else
      printfn "Error parsing input."

خروجی مثال بالا :

Enter a temperature in degrees Fahrenheit.
90
That temperature in degrees Celsius is    32.22.
مطالب
رسم نمودار توسط Kendo Chart
پیشتر مطالبی در سایت، درباره KenoUI و همچنین ویجت‌های وب آن منتشر گردید. در این مطلب نگاهی خواهیم داشت بر تعدادی از ویجت‌های Kendo UI جهت رسم نمودار. توسط Kendo UI می‌توانیم نمودار‌های زیر را ترسیم کنیم:
  • Bar and Column
  • Line and Vertical Line
  • Area and Vertical Area
  • Bullet
  • Pie and Donut
  • Scatter
  • Scatter Line
  • Bubble
  • Radar and Polar

برای رسم نمودار می‌توانیم به صورت زیر عمل کنیم:

1- ابتدا باید استایل‌های مربوط به Data Visualization را به صفحه اضافه کنیم:

<link href="Content/kendo.dataviz.min.css" rel="stylesheet" />
<link href="Content/kendo.dataviz.default.min.css" rel="stylesheet" />
2- سپس یک عنصر را بر روی صفحه جهت نمایش نمودار، تعیین می‌کنیم:
<div id="chart"></div>
برای عنصر فوق می‌توانیم درون CSS و یا به صورت inline طول و عرضی را برای چارت تعیین کنیم:
<div id="chart" style="width: 400px; height: 600px"></div>
با فراخوانی تابع KendoChart، چارت بر روی صفحه نمایش داده می‌شود:
$("#chart").kendoChart();

همانطور که مشاهده می‌کنید هیچ داده‌ایی را هنوز برای چارت تعیین نکرده‌ایم؛ در نتیجه همانند تصویر فوق یک چارت خالی بر روی صفحه نمایش داده می‌شود. برای چارت فوق می‌توانیم خواصی از قبیل عنوان و ... را تعیین کنیم:
$("#chart").kendoChart({
    title: {
         text: "چارت آزمایشی"
    }
});

نمایش داده‌ها بر روی چارت:

داده‌ها را می‌توان هم به صورت local و هم به صورت remote دریافت و بر روی چارت نمایش داد. اینکار را می‌توانیم توسط قسمت series انجام دهیم:
$("#chart").kendoChart({
    title: {
         text: "عنوان چارت"
    },
    series: [
         { name: "داده‌های چارت", data: [200, 450, 300, 125] }
    ]
});
برای تعیین برچسب برای هر یک از داده‌ها نیز می‌توانیم خاصیت category axis را مقداردهی کنیم:
$("#chart").kendoChart({
                title: {
                    text: "عنوان چارت"
                },
                series: [
                     {
                         name: "داده‌های چارت",
                         data: [200, 450, 300, 125]
                     }
                ],
                categoryAxis: {
                    categories: [2000, 2001, 2002, 2003]
                }
            });

دریافت اطلاعات از سرور:
کدهای سمت سرور:
public class ProductsController : ApiController
    {
        public IEnumerable<ProductViewModel> Get()
        {
            var products = _productService.GetAllProducts();
            var query = products.GroupBy(p => p.Name).Select(p => new ProductViewModel
            {
                Value = p.Key,
                Count = p.Count()
            });
            return query;
        }
    }

    public class ProductViewModel
    {
        public string Value { get; set; }
        public int Count { get; set; }
    }

سپس برای دریافت اطلاعات از سمت سرور باید DataSource مربوط به چارت را مقداردهی کنیم:
var productsDataSource = new kendo.data.DataSource({
                transport: {
                    read: {
                        url: "api/products",
                        dataType: "json",
                        contentType: 'application/json; charset=utf-8',
                        type: 'GET'
                    }
                },
                error: function (e) {
                    alert(e.errorThrown.stack);
                },
                pageSize: 5,
                sort: { field: "Id", dir: "desc" }
            });

            $("#chart").kendoChart({
                title: {
                    text: "عنوان چارت"
                },
                dataSource: productsDataSource,
                series: [
                    {
                        field: "Count",
                        categoryField: "Value",
                        aggregate: "sum"
                    }
                ]
            });
همانطور که مشاهده می‌کنید در این حالت باید برای سری، field و categoryField را مشخص کنیم.
موارد فوق را می‌توانیم به صورت یک افزونه نیز کپسوله کنیم.

کدهای افزونه jquery.ChartAjax:
(function($) {
    $.fn.ShowChart = function(options) {
        var defaults = {
            url: '/',
            text: 'نمودار دایره ایی',
            theme: 'blueOpal',
            font: '13px bbc-nassim-bold',
            legendPosition: 'left',
            seriesField: 'Count',
            seriesCategoryField: 'Value',
            titlePosition: 'top',
            chartWidth: 400,
            chartHeight: 400
        };
        var options = $.extend(defaults, options);
        return this.each(function() {
            var chartDataSource = new kendo.data.DataSource({
                transport: {
                    read: {
                        url: options.url,
                        dataType: "json",
                        contentType: 'application/json; charset=utf-8',
                        type: 'GET'
                    }
                },
                error: function (e) {
                    // handle error
                }
            });
            $(this).kendoChart({
                chartArea: {
                    height: options.chartHeight
                },
                theme: options.theme,
                title: {
                    text: options.text,
                    font: options.font,
                    position: options.titlePosition
                },
                legend: {
                    position: options.legendPosition,
                    labels: {
                        font: options.font
                    }
                },
                seriesDefaults: {
                    labels: {
                        visible: false,
                        format: "{0}%"
                    }
                },
                dataSource: chartDataSource,
                series: [
                    {
                        type: "pie",
                        field: options.seriesField,
                        categoryField: options.seriesCategoryField,
                        aggregate: "sum",
                    }
                ],
                tooltip: {
                    visible: true,
                    template: "${category}: ${value}",
                    font: options.font
                }
            });
            
        });
    };
})(jQuery);
برای افزونه فوق موارد زیر در نظر گرفته شده است:
chartArea : جهت تعیین طول و عرض چارت
theme : جهت تعیین قالب‌های از پیش‌تعریف شده:
  • Black
  • BlueOpal
  • Bootstrap
  • Default
  • Flat
  • HighContrast
  • Material
  • MaterialBlack
  • Metro
  • MetroBlack
  • Moonlight
  • Silver
  • Uniform

title : جهت تعیین عنوان چارت

legend : جهت تنظیم ویژگی‌های قسمت گروه‌بندی چارت

tooltip : جهت تنظیم ویژگی‌های مربوط به نمایش tooltip در هنگام hover بر روی چارت.

لازم به ذکر است در قسمت series می‌توانید نوع چارت را تعیین کنید.

نحوه استفاده از افزونه فوق:
$('#chart').ShowChart({
                        url: "/Report/ByUnit",
                        legendPosition: "bottom"
});


دریافت سورس مثال جاری (KendoChart.zip)

مطالب
روش ترجیح داده شده‌ی مقایسه مقادیر اشیاء با null از زمان C# 7.0 به بعد
روش سنتی بررسی نال بودن اشیاء و متغیرها در زبان #C، استفاده از اپراتور == است:
if(person == null) { }
اما از زمان C# 7.0 و معرفی pattern matching، از واژه‌ی کلیدی is نیز می‌توان برای اینکار استفاده کرد (که به آن constant pattern هم می‌گویند):
if(person is null) { }
اکنون سؤال اینجا است که امروز بهتر است از کدامیک استفاده کنیم؟


سربارگذاری عملگرها و مقایسه‌ی وهله‌های اشیاء با null

در عمل، تفاوتی بین استفاده‌ی از عملگر == و واژه‌ی کلیدی is برای بررسی نال بودن وهله‌ای از یک شیء وجود ندارد؛ اما ... با یک شرط! فقط در حالتیکه عملگر == سربارگذاری نشده باشد.
برای نمونه کلاس Person زیر را در نظر بگیرید که عملگرهای == و =! آن بازنویسی شده‌اند:
public class Person
{
  public static bool operator ==(Person x, Person y)
  {
    return false;
  }
  public static bool operator !=(Person x, Person y)
  {
    return !(x == y);
  }
  public override bool Equals(object obj)
  {
    return base.Equals(obj);
  }
}
در این حالت اگر قطعه کد زیر را اجرا کنیم که در یک سطر آن، وهله‌ی person که مقدار نال را دارد، توسط عملگر == با null مقایسه شده‌است و در سطر بعدی با کمک واژه‌ی کلیدی is با نال مقایسه شده‌است:
Person person = null;
Console.WriteLine("Is Person null?");
Console.WriteLine($"== says: {person == null}");
Console.WriteLine($"is says: {person is null}");
به نظر شما خروجی این قطعه کد چیست؟
اگر در کلاس Person سربارگذاری عملگر == صورت نمی‌گرفت، خروجی زیر را مشاهده می‌کردیم:
Is Person null?
== says: True
is says: True
اما اینبار خروجی واقعی قطعه کد فوق، با چیزی که انتظار داریم متفاوت است:
Is Person null?
== says: False
is says: True
مزیت کار با واژه‌ی کلیدی is، صرفنظر کردن از operator overloads یا همان سربارگذاری عملگرها است. در حین حالت فقط مقدار person، با null مقایسه می‌شود و دیگر، کار به بررسی خروجی false زیر نمی‌رسد (کاری که با استفاده از عملگر == حتما انجام خواهد شد):
public static bool operator ==(Person x, Person y)
{
   return false;
}
به عبارتی استفاده‌ی از عملگر == جهت مقایسه‌ی با null، حتما نیاز به بررسی کدهای کلاس Person را جهت مشاهده‌ی کدهای تغییر یافته‌ی عملگر == را نیز دارد؛ اما زمانیکه از وژه‌ی کلیدی is استفاده می‌کنیم، مقصود اصلی ما را که بررسی مقدار جاری با null است، برآورده می‌کند (و در 99 درصد موارد، ما هدف دیگری را دنبال نمی‌کنیم و برای ما مهم نیست که عملگر == سربارگذاری شده‌است یا خیر). همچنین سرعت مقایسه در حالت استفاده از واژه‌ی کلیدی is نیز بیشتر است؛ چون دیگر فراخوانی کدی که عملگر == را سربارگذاری می‌کند، صورت نخواهد گرفت و از زمان C# 9.0، برای حالت بررسی حالت عکس آن می‌توان از  if (obj is not null) نیز استفاده کرد (بجای if (!(obj is null))) که از حالت سربارگذاری عملگر =! صرفنظر می‌کند.


یک نکته: null coalescing operator یعنی ?? و null coalescing assignment operator یعنی =?? نیز همانند واژه‌ی کلیدی is عمل می‌کنند. یعنی از == سربارگذاری شده صرفنظر خواهند کرد.
اشتراک‌ها
ویژگی‌های جدید سی‌شارپ 7
PathInfo pathInfo = new PathInfo(@"\\test\unc\path\to\something.ext");
{
  // Example 1: Deconstructing declaration and assignment.
  (string directoryName, string fileName, string extension) = pathInfo;
  VerifyExpectedValue(directoryName, fileName, extension);
}
{
  string directoryName, fileName, extension = null;
  // Example 2: Deconstructing assignment.
  (directoryName, fileName, extension) = pathInfo;
  VerifyExpectedValue(directoryName, fileName, extension);
}
{
  // Example 3: Deconstructing declaration and assignment with var.
  var (directoryName, fileName, extension) = pathInfo;
  VerifyExpectedValue(directoryName, fileName, extension);
}
ویژگی‌های جدید سی‌شارپ 7
مطالب
کنترل نرخ ورود اطلاعات در برنامه‌های 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، سبب لغو خودکار درخواست‌های پیشین می‌شود. در این حالت نه تنها تعداد بار به روز رسانی رابط کاربری کاهش پیدا می‌کند، بلکه تضمین خواهد شد دیگر کاربر نتیجه‌ی اشتباهی را نیز مشاهده نکند.



کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید.
نظرات مطالب
کار با Kendo UI DataSource
سلام؛ من در حل این مشکل به یک نکته برخوردم و آن اینه که وقتی view من دقیقا مشابه آن چیزی هست که شما در جواب فوق آدرس دادید من موفق به دریافت پارامتر‌ها میگردم. اما اگر ویو من به شکل زیر باشد پارامتر‌ها را نال بر میگرداند.
   <script type="text/javascript">
        $(function () {
            //            var r = "12";
            var productsDataSource = new kendo.data.DataSource({
                transport: {
                    read: {
                        url: "@Url.Action("GetProducts", "Home")",
                        dataType: "json",
                        contentType: 'application/json; charset=utf-8',
                        type: 'GET',
                        data: { param1: "dfvdf", param2: "val2" } // ارسال اطلاعات اضافی و سفارشی به سرور در حین درخواست
                    },
                    create: {
                        url: "@Url.Action("PostProduct","Home")",
                        contentType: 'application/json; charset=utf-8',
                        type: "POST"
                    },
                    update: {
                        url:// function (product) {
                             "@Url.Action("UpdateProduct","Home")",//, +product.Id;
                        //},
                        contentType: 'application/json; charset=utf-8',
                        type: "PUT"
                    },
                    destroy: {
                        url: function (p) {
                            return "@Url.Action("DeleteProduct","Home")/" + p.Id;
                        },
                        contentType: 'application/json; charset=utf-8',
                        type: "DELETE"
                    },
                    parameterMap: function (options) {
                        return kendo.stringify(options);
                    }
                },
                schema: {
                    parse: function (data) {
                        return data;
                    },
                    data: "Data",
                    total: "Total",
                    model: {
                        id: "Id", // define the model of the data source. Required for validation and property types.
                        fields: {
                            "Id": { type: "number", editable: false }, //تعیین نوع فیلد برای جستجوی پویا مهم است
                            "Name": { type: "string", validation: { required: true }, editable: true },
                            "Discription": { type: "string", },
                            "Title": { type: "string", editable: false },
                            "GroupName": { type: "string", },
                            "Link": { type: "string" }

                        }

                    },
                    batch: false,


                },
                error: function (e) {
                    alert(e.errorThrown.stack);
                },
                pageSize: 5,
                sort: { field: "Id", dir: "desc" }

            });
            $("#report-grid").kendoGrid({
                dataSource: productsDataSource,
                autoBind: true,
                scrollable: false,
                pageable: true,
                sortable: true,
                columns: [
                { field: "Id", title: "#" },
                { field: "Name", title: "Product" }
                ]
            });
        });
    </script>
مطالب
آموزش MEF#1
Managed Extensibility Framework یا MEF کامپوننتی از Framework 4 است که برای ایجاد برنامه‌های توسعه پذیر (Extensible) با حجم کم کد استفاده میشه.این تکنولوژی به برنامه نویسان این امکان رو میده که توسعه‌های (Extension) برنامه رو بدون پیکربندی استفاده کنند. همچنین به توسعه دهندگان این اجازه رو می‌ده که به آسانی کدها رو کپسوله کنند .
MEF به عنوان بخشی از 4 NET. و Silverlight 4 معرفی شد. MEF یک راه حل ساده برای مشکل توسعه در حال اجرای برنامه‌ها ارائه می‌کند.تا قبل از این تکنولوژی ، هر برنامه‌ای که می‌خواست یک مدل Plugin را پشتیبانی کنه لازم بود که خودش زیر ساخت‌ها را از ابتدا ایجاد کنه . این Plugin‌ها اغلب برای برنامه‌های خاصی بودند و نمی‌توانستند در پیاده سازی‌های چندگانه دوباره استفاده شوند. ولی MEF در راستای حل این مشکلات ، روش استانداردی رو برای میزبانی برنامه‌های کاربردی پیاده کرده است. 
برای فهم بهتر مفاهیم یک مثال ساده رو با MEF پیاده سازی می‌کنم.
ابتدا یک پروژه از نوع Console Application ایجاد کنید . بعد با استفاده از Add Reference یک ارجاع به System.ComponentModel.Composition بدید. سپس یک Interface به نام IViewModel را به صورت زیر ایجاد کنید:
public interface IViewModel
    {
        string Name { get; set; }
    }

یک خاصیت به نام Name برای دسترسی به نام ViewModel ایجاد می‌کنیم.
سپس 2 تا ViewModel دیگه ایجاد می‌کنیم که IViewModel را پیاده سازی کنند. به صورت زیر:
ViewModelFirst:
[Export( typeof( IViewModel ) )]
    public class ViewModelFirst : IViewModel
    {
        public ViewModelFirst()
        {
            this.Name = "ViewModelFirst";
        }

        public string Name
        {
            get
            {
                return _name;
            }
            set
            {
                _name = value;
            }
        }
        private string _name;
    }


ViewModelSecond:
[Export( typeof( IViewModel ) )]
    public class ViewModelSecond : IViewModel
    {
        public ViewModelSecond()
        {
            this.Name = "ViewModelSecond";
        }

        public string Name
        {
            get
            {
                return _name;
            }
            set
            {
                _name = value;
            }
        }
        private string _name;
    }


Export Attribute استفاده شده در بالای کلاس‌های ViewModel به این معنی است که این کلاس‌ها اینترفیس IViewModel رو Export کردند تا در جای مناسب بتونیم این ViewModel ‌ها Import کنیم.(Import , Export از مفاهیم اصلی در MEF هستند)
حالا نوبت به پیاده سازی کلاس Plugin می‌رسه.
public class PluginManager
    {
        public PluginManager()
        {

        }

        public IList<IViewModel> ViewModels
        {
            get
            {
                return _viewModels;
            }
            private set
            {
                _viewModels = value;
            }
        }

        [ImportMany( typeof( IViewModel ) )]
        private IList<IViewModel> _viewModels = new List<IViewModel>();

        public void SetupManager()
        {
            AggregateCatalog aggregateCatalog = new AggregateCatalog();

            CompositionContainer container = new CompositionContainer( aggregateCatalog );

            CompositionBatch batch = new CompositionBatch();

            batch.AddPart( this );

            aggregateCatalog.Catalogs.Add( new AssemblyCatalog( Assembly.GetExecutingAssembly() ) );           

            container.Compose( batch );
        }

کلاس PluginManager برای شناسایی و استفاده از کلاس هایی که صفت‌های Export رو دارند نوشته شده(دقیقا شبیه یک UnityContainer در Microsoft Unity Application Block یا IKernel در Ninject) عمل می‌کنه با این تفاوت که نیازی به Register با Bind کردن ندارند)
ابتدا بک لیست از کلاس هایی که IViewModel رو Export کردند داریم.
بعد در متد SetupManager ابتدا یک AggregateCatalog نیاز داریم تا بتونیم Composition Part‌ها رو بهش اضافه کنیم. به کد زیر توجه کنید:
 aggregateCatalog.Catalogs.Add( new AssemblyCatalog( Assembly.GetExecutingAssembly() ) );

تو این قطعه کد من یک Assembly Catalog رو که به Assembly جاری برنامه اشاره می‌کنه به AggregateCatalog اضافه کردم.
متد (batch.AddPart(this در واقع به این معنی است که به MEF گفته می‌شود این کلاس ممکن است شامل Export هایی باشد که به یک یا چند Import وابستگی دارند.
متد (AddExport(this در CompositionBatch به این معنی است که این کلاس ممکن است شامل Exportهایی باشد که به Import وابستگی ندارند.
حالا برای مشاهده نتایج کد زیر را در کلاس Program اضافه می‌کنیم:
static void Main( string[] args )
        {
            PluginManager plugin = new PluginManager();

            Console.WriteLine( string.Format( "Number Of ViewModels Before Plugin Setup Is [ {0} ]", plugin.ViewModels.Count ) );

            Console.WriteLine( Environment.NewLine );

            plugin.SetupManager();

            Console.WriteLine( string.Format( "Number Of ViewModels After Plugin Setup Is [ {0} ]", plugin.ViewModels.Count ) );

            Console.ReadLine();
        }

در کلاس بالا ابتدا تعداد کلاس‌های موجود در لیست ViewModels رو قبل از Setup کردن Plugin نمایش داده سپس بعد از Setup  کردن Plugin  دوباره تعداد کلاس‌های موجود در لیست ViewModel رو مشاهده می‌کنیم.که خروجی به شکل زیر تولید خواهد شد.


متد SetupManager در کلاس Plugin (با توجه به AggregateCatalog) که در این برنامه فقط Assembly  جاری رو بهش اضافه کردیم تمام کلاس هایی رو که نوع IViewModel رو Export کردند پیدا کرده و در لیست اضافه می‌کنه(این کار رو با توجه به ImportMany Attribute) انجام میده. در پست‌های بعدی روش استفاده از MEF رو در Prism یا WAF توضیح می‌دم.
مطالب
طراحی و پیاده سازی ServiceLayer به همراه خودکارسازی Business Validationها

در این مطلب قصد داریم علاوه بر طراحی زیرساختی برای راه اندازی هرچه سریعتر ServiceLayer، طراحی ای برای مکانیزم Validation به عنوان یک Cross Cutting Concern، نیز ارائه داده و آن را پیاده سازی کنیم.

پیش نیازها:

 ServiceLayer در معماری لایه‌ای، در برگیرنده ApplicationService هایی می‌باشد که به عنوان مدخل ورودی (Entry Point) برنامه، در معرض دید لایه Presentation قرار گرفته و داده را به فرمت مورد نیاز Presentation در اختیارش قرار خواهند داد.
 این سرویس‌ها DTO‌ها را به عنوان پارامتر دریافت کرده و DTO هایی را به عنوان خروجی برگشت خواهند داد. مباحثی مانند Logging، Caching، Business Validation Authorization و مدیریت تراکنش‌ها را می‌توان در این لایه در نظر گرفت.

در ادامه اگر واژه «سرویس» به کار گرفته می‌شود منظور ما ApplicationServiceها می‌باشند.

کار را با ارائه یکسری واسط و کلاس پایه برای عملیات CRUD در سرویس‌ها به صورت زیر پیش می‌بریم.

قرار است به صورت قراردادی، تمام سرویس‌های ما واسط زیر را پیاده سازی کرده باشند. این مورد در مباحث تعریف Policy‌های مربوط به StructureMap مفید خواهد بود.

namespace MvcFramework.Framework.Application.Services
{
    public interface IApplicationService : ITransientDependency
    {
    }
}

دو واسط دیگر برای اعمال طول عمر اشیاء به صورت قراردادی در StructureMap به شکل زیر در نظر گرفته شده‌اند.

namespace MvcFramework.Framework.Dependency
{
    public interface ISingletonDependency
    {
    }
}
namespace MvcFramework.Framework.Dependency
{
    public interface ITransientDependency
    {
    }
}

و با پیاده سازی یک LifeCyclePolicy از دو واسط بالا به شکل زیر استفاده خواهیم کرد.

namespace MvcFramework.Framework.Dependency
{
    public class LifeCyclePolicy : IInstancePolicy
    {
        public void Apply(Type pluginType, Instance instance)
        {
            if (typeof(ISingletonDependency).IsAssignableFrom(instance.ReturnedType))
                instance.SetLifecycleTo<SingletonLifecycle>();
            else if (typeof(ITransientDependency).IsAssignableFrom(instance.ReturnedType))
                instance.SetLifecycleTo<TransientLifecycle>();
        }
    }
}

به این صورت تنظیم طول عمر اشیاء ساخته شده توسط StructureMap این بار به صورت قرادادی بوده و لازم به ذکر تک تک این موارد در تنظیمات اولیه مربوط به Container آن نیست.

کلاس پایه‌ای را که پیاده ساز واسط IApplicationService می‌باشد، برای مقابله با عدم نگارش پذیری واسط‌ها، به شکل زیر در نظر میگیریم. 

namespace MvcFramework.Framework.Application.Services
{
    public abstract class ApplicationService : IApplicationService
    {
    }
}

بسته به نیاز پروژه خودتان می‌توانید اعضای مشترک بین سرویس‌ها را در دل این کلاس قرار دهید.

در ادامه واسط ICrudApplicationSevie را به شکل زیر طراحی خواهیم کرد.

namespace MvcFramework.Framework.Application.Services
{
    public interface ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel> :
        ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, PagedListRequest,
            PagedListResponse<TModel, PagedListRequest>, DynamicListRequest>
        where TEditModel : class, IEditModel
        where TModel : class, IModel
        where TDeleteModel : class, IDeleteModel
    {
    }

    public interface ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, in TDynamicListRequest> :
        ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, PagedListRequest,
            PagedListResponse<TModel, PagedListRequest>, TDynamicListRequest>
        where TEditModel : class, IEditModel
        where TModel : class, IModel
        where TDeleteModel : class, IDeleteModel
        where TDynamicListRequest : DynamicListRequest
    {
    }

    public interface ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, in TPagedListRequest,
        TPagedListResponse> :
        ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, TPagedListRequest, TPagedListResponse,
            DynamicListRequest>
        where TEditModel : class, IEditModel
        where TModel : class, IModel
        where TDeleteModel : class, IDeleteModel
        where TPagedListRequest : PagedListRequest, new()
        where TPagedListResponse : PagedListResponse<TModel, TPagedListRequest>
    {
    }

    public interface ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, in TPagedListRequest,
        TPagedListResponse,
        in TDynamicListRequest> : IApplicationService
        where TEditModel : class, IEditModel
        where TModel : class, IModel
        where TDeleteModel : class, IDeleteModel
        where TPagedListRequest : PagedListRequest, new()
        where TPagedListResponse : PagedListResponse<TModel, TPagedListRequest>
        where TDynamicListRequest : DynamicListRequest
    {
        void Create(TCreateModel model);
        void Create(IList<TCreateModel> models);
        Task CreateAsync(TCreateModel model);
        Task CreateAsync(IList<TCreateModel> models);

        IList<TModel> GetList();
        DynamicListResponse GetDynamicList(TDynamicListRequest request);
        TPagedListResponse GetPagedList(TPagedListRequest request);
        IList<LookupItem> GetLookup();
        TModel GetById(long id);
        TEditModel GetForEdit(long id);
        bool Exists(long id);
        Task<IList<TModel>> GetListAsync();
        Task<DynamicListResponse> GetDynamicListAsync(TDynamicListRequest request);
        Task<TPagedListResponse> GetPagedListAsync(TPagedListRequest request);
        Task<IList<LookupItem>> GetLookupAsync();
        Task<TModel> GetByIdAsync(long id);
        Task<TEditModel> GetForEditAsync(long id);
        Task<bool> ExistsAsync(long id);

        void Edit(TEditModel model);
        void Edit(IList<TEditModel> models);
        Task EditAsync(TEditModel model);
        Task EditAsync(IList<TEditModel> models);
        
        void Delete(TDeleteModel model);
        void Delete(IList<TDeleteModel> models);
        Task DeleteAsync(TDeleteModel model);
        Task DeleteAsync(IList<TDeleteModel> models);
    }
}

سرویسی که نیاز دارد از عملیات CRUD نیز پشتیبانی داشته باشد، بهتر است واسط آن از یک چنین واسطی که در بالا معرفی شد، ارث بری کند. 

مدل‌ها و واسط‌های پیش فرضی را که در واسط بالا از آنها استفاده شده است، در زیر مشاهده می‌کنید:

 واسط IModel

namespace MvcFramework.Framework.Application.Models
{
    public interface IModel
    {
        long Id { get; set; }
    }
}

واسط IEditModel

namespace MvcFramework.Framework.Application.Models
{
    public interface IEditModel : IModel
    {
        byte[] RowVersion { get; set; }
    }
}

واسط IDeleteModel

namespace MvcFramework.Framework.Application.Models
{
    public interface IDeleteModel : IModel
    {
        byte[] RowVersion { get; set; }
    }
}

کلاس LookupItem

namespace MvcFramework.Framework.Application.Models
{
    public class LookupItem
    {
        public string Value { get; set; }
        public string Text { get; set; }
        public bool Selected { get; set; }
    }
}

کلاس PagedListRequest

namespace MvcFramework.Framework.Application.Models
{
    public class PagedListRequest : IShouldNormalize
    {
        public long TotalCount { get; set; }
        public int PageNumber { get; set; }
        public int PageSize { get; set; }

        /// <summary>
        ///     Sorting information.
        ///     Should include sorting field and optionally a direction (ASC or DESC)
        ///     Can contain more than one field separated by comma (,).
        /// </summary>
        /// <example>
        ///     Examples:
        ///     "Name"
        ///     "Name DESC"
        ///     "Name ASC, Age DESC"
        /// </example>
        public string SortBy { get; set; }

        public void Normalize()
        {
            if (PageNumber < 1)
                PageNumber = 1;

            if (PageSize < 0)
                PageSize = 10;

            if (SortBy.IsEmpty())
                SortBy = "Id DESC";
        }
    }
}

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


کلاس PagedListResponse

namespace MvcFramework.Framework.Application.Models
{
    public class PagedListResponse<TModel, TPagedListRequest>
        where TPagedListRequest : PagedListRequest, new()
        where TModel : IModel
    {
        public PagedListResponse()
        {
            Result = new List<TModel>();
            Request = new TPagedListRequest();
        }
        public IList<TModel> Result { get; set; }
        public TPagedListRequest Request { get; set; }
    }
}

کلاس بالا به عنوان نوع خروجی متد GetPagedList مورد استفاده قرار میگرد. وجود خصوصیتی از نوع PagedListRequest هم برای مواردی مانند صفحه بندی نیز می‌تواند مفید باشد.


کلاس‌های DynamicListRequest و DynamicListResponse برگرفته از کتابخانه Kendo.DynamicLinq می باشند.


کلاس Entity

namespace MvcFramework.Framework.Domain.Entities
{
    public abstract class Entity
    {
        #region Properties

        public long Id { get; set; }
        public byte[] RowVersion { get; set; }
        public EntityChangeState State { get; set; }

        #endregion

        #region Public Methods

        [SuppressMessage("ReSharper", "BaseObjectGetHashCodeCallInGetHashCode")]
        [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")]
        public override int GetHashCode()
        {
            if (IsTransient())
                return base.GetHashCode();

            unchecked
            {
                var hash = this.GetRealType().GetHashCode();
                return (hash * 31) ^ Id.GetHashCode();
            }
        }

        public virtual bool IsTransient()
        {
            return Id == 0;
        }

        public override bool Equals(object obj)
        {
            var other = obj as Entity;
            if (ReferenceEquals(other, null)) return false;

            if (ReferenceEquals(this, other)) return true;

            var typeOfThis = this.GetRealType();
            var typeOfOther = other.GetRealType();

            if (typeOfThis != typeOfOther) return false;

            if (IsTransient() || other.IsTransient()) return false;

            return Id.Equals(other.Id);
        }

        public override string ToString()
        {
            return $"[{this.GetRealType().Name} : {Id}]";
        }

        #endregion

        #region Operators

        public static bool operator ==(Entity left, Entity right)
        {
            return Equals(left, right);
        }

        public static bool operator !=(Entity left, Entity right)
        {
            return !(left == right);
        }

        #endregion
    }
}

در این کلاس یکسری خصوصیات پایه ای مانند Id و متدهای مشترک بین Entityها قرار گرفته شده است. این کلاس پایه تمام Entity‌های سیستم می‌باشد.

پیاده سازی پیش فرض از واسط ICrudApplicationService به شکل زیر می‌باشد.

namespace MvcFramework.Framework.Application.Services
{
    public abstract class CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel> :
        CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel, PagedListRequest,
            PagedListResponse<TModel, PagedListRequest>, DynamicListRequest>
        where TEntity : Entity
        where TCreateModel : class
        where TEditModel : class, IEditModel
        where TModel : class, IModel
        where TDeleteModel : class, IDeleteModel
    {
        protected CrudApplicationService(IUnitOfWork unitOfWork, IMapper mapper) : base(unitOfWork, mapper)
        {
        }
    }

    public abstract class CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel,
        TDynamicListRequest> :
        CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel, PagedListRequest,
            PagedListResponse<TModel, PagedListRequest>, TDynamicListRequest>
        where TEntity : Entity
        where TCreateModel : class
        where TEditModel : class, IEditModel
        where TModel : class, IModel
        where TDeleteModel : class, IDeleteModel
        where TDynamicListRequest : DynamicListRequest
    {
        protected CrudApplicationService(IUnitOfWork unitOfWork, IMapper mapper) : base(unitOfWork, mapper)
        {
        }
    }

    public abstract class CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel,
        TPagedListRequest,
        TPagedListResponse> :
        CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel, TPagedListRequest,
            TPagedListResponse,
            DynamicListRequest>
        where TEntity : Entity
        where TCreateModel : class
        where TEditModel : class, IEditModel
        where TModel : class, IModel
        where TDeleteModel : class, IDeleteModel
        where TPagedListRequest : PagedListRequest, new()
        where TPagedListResponse : PagedListResponse<TModel, TPagedListRequest>, new()
    {
        protected CrudApplicationService(IUnitOfWork unitOfWork, IMapper mapper) : base(unitOfWork, mapper)
        {
        }
    }

    public abstract class CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel,
        TPagedListRequest,
        TPagedListResponse, TDynamicListRequest> : ApplicationService,
        ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, TPagedListRequest, TPagedListResponse,
            TDynamicListRequest>
        where TEntity : Entity
        where TCreateModel : class
        where TEditModel : class, IEditModel
        where TModel : class, IModel
        where TDeleteModel : class, IDeleteModel
        where TPagedListRequest : PagedListRequest, new()
        where TPagedListResponse : PagedListResponse<TModel, TPagedListRequest>, new()
        where TDynamicListRequest : DynamicListRequest

    {
        #region Constructor

        protected CrudApplicationService(IUnitOfWork unitOfWork, IMapper mapper)
        {
            Guard.ArgumentNotNull(unitOfWork, nameof(unitOfWork));
            Guard.ArgumentNotNull(mapper, nameof(mapper));

            UnitOfWork = unitOfWork;
            Mapper = mapper;
            EntitySet = UnitOfWork.Set<TEntity>();
        }

        #endregion

        #region Properties

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

        #endregion

        #region ICrudApplicationService Members

        #region Methods

        [Transactional]
        public virtual void Create(TCreateModel model)
        {
            Guard.ArgumentNotNull(model, nameof(model));

            var entity = Mapper.Map<TEntity>(model);

            EntitySet.Add(entity);
            UnitOfWork.SaveChanges();
        }

        [Transactional]
        public virtual void Create(IList<TCreateModel> models)
        {
            Guard.ArgumentNotEmpty(models, nameof(models));

            var entities = Mapper.Map<IList<TEntity>>(models);

            UnitOfWork.AddRange(entities);
            UnitOfWork.SaveChanges();
        }

        [Transactional]
        public virtual Task CreateAsync(TCreateModel model)
        {
            Guard.ArgumentNotNull(model, nameof(model));

            var entity = Mapper.Map<TEntity>(model);

            EntitySet.Add(entity);
            return UnitOfWork.SaveChangesAsync();
        }

        [Transactional]
        public virtual Task CreateAsync(IList<TCreateModel> models)
        {
            Guard.ArgumentNotEmpty(models, nameof(models));

            var entities = Mapper.Map<IList<TEntity>>(models);

            UnitOfWork.AddRange(entities);
            return UnitOfWork.SaveChangesAsync();
        }


        [Transactional]
        public virtual void Edit(TEditModel model)
        {
            Guard.ArgumentNotNull(model, nameof(model));

            var entity = Mapper.Map<TEntity>(model);

            UnitOfWork.MarkAsChanged(entity);
            UnitOfWork.SaveChanges();
        }

        [Transactional]
        public virtual void Edit(IList<TEditModel> models)
        {
            Guard.ArgumentNotNull(models, nameof(models));
            Guard.ArgumentNotEmpty(models, nameof(models));

            var entities = Mapper.Map<IList<TEntity>>(models);

            UnitOfWork.UpdateRange(entities);
            UnitOfWork.SaveChanges();
        }

        [Transactional]
        public virtual Task EditAsync(TEditModel model)
        {
            Guard.ArgumentNotNull(model, nameof(model));

            var entity = Mapper.Map<TEntity>(model);

            UnitOfWork.MarkAsChanged(entity);
            return UnitOfWork.SaveChangesAsync();
        }

        [Transactional]
        public virtual Task EditAsync(IList<TEditModel> models)
        {
            Guard.ArgumentNotNull(models, nameof(models));
            Guard.ArgumentNotEmpty(models, nameof(models));

            var entities = Mapper.Map<IList<TEntity>>(models);

            UnitOfWork.UpdateRange(entities);
            return UnitOfWork.SaveChangesAsync();
        }


        public virtual IList<TModel> GetList()
        {
            return EntitySet.ProjectToList<TModel>(Mapper.ConfigurationProvider);
        }

        public virtual DynamicListResponse GetDynamicList(TDynamicListRequest request)
        {
            Guard.ArgumentNotNull(request, nameof(request));

            var query = ApplyFiltering(request);

            return query.ProjectTo<TModel>().ToListResponse(request);
        }

        public virtual TPagedListResponse GetPagedList(TPagedListRequest request)
        {
            Guard.ArgumentNotNull(request, nameof(request));

            var query = ApplyFiltering(request);

            request.TotalCount = query.LongCount();

            query = ApplySorting(query, request);
            query = ApplyPaging(query, request);

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

            return new TPagedListResponse
            {
                Result = result,
                Request = request
            };
        }

        public virtual IList<LookupItem> GetLookup()
        {
            return EntitySet.ProjectToList<LookupItem>(Mapper.ConfigurationProvider);
        }

        public virtual TModel GetById(long id)
        {
            Guard.ArgumentInRange(id, 1, long.MaxValue, nameof(id));

            var entity =
                EntitySet.Where(a => a.Id == id).ProjectToFirstOrDefault<TModel>(Mapper.ConfigurationProvider);

            if (entity == null)
                throw new EntityNotFoundException($"Couldn't Find Entity {id} When GetById");

            return entity;
        }

        public virtual TEditModel GetForEdit(long id)
        {
            Guard.ArgumentInRange(id, 1, long.MaxValue, nameof(id));

            var entity =
                EntitySet.Where(a => a.Id == id).ProjectToFirstOrDefault<TEditModel>(Mapper.ConfigurationProvider);

            if (entity == null)
                throw new EntityNotFoundException($"Couldn't Find Entity {id} When GetForEdit");

            return entity;
        }

        public virtual bool Exists(long id)
        {
            Guard.ArgumentInRange(id, 1, long.MaxValue, nameof(id));

            return EntitySet.Any(a => a.Id == id);
        }

        public virtual async Task<IList<TModel>> GetListAsync()
        {
            return await EntitySet.ProjectToListAsync<TModel>(Mapper.ConfigurationProvider);
        }

        public virtual Task<DynamicListResponse> GetDynamicListAsync(TDynamicListRequest request)
        {
            Guard.ArgumentNotNull(request, nameof(request));

            var query = ApplyFiltering(request);

            return query.ProjectTo<TModel>().ToListResponseAsync(request);
        }

        public virtual async Task<TPagedListResponse> GetPagedListAsync(TPagedListRequest request)
        {
            Guard.ArgumentNotNull(request, nameof(request));

            var query = ApplyFiltering(request);

            request.TotalCount = await query.LongCountAsync().ConfigureAwait(false);

            query = ApplySorting(query, request);
            query = ApplyPaging(query, request);

            var result = await query.ProjectToListAsync<TModel>(Mapper.ConfigurationProvider).ConfigureAwait(false);

            return new TPagedListResponse
            {
                Result = result,
                Request = request
            };
        }

        public virtual async Task<IList<LookupItem>> GetLookupAsync()
        {
            return await EntitySet.ProjectToListAsync<LookupItem>(Mapper.ConfigurationProvider);
        }

        public virtual async Task<TModel> GetByIdAsync(long id)
        {
            Guard.ArgumentInRange(id, 1, long.MaxValue, nameof(id));

            var entity = await UnTrackedEntitySet.Where(a => a.Id == id)
                .ProjectToFirstOrDefaultAsync<TModel>(Mapper.ConfigurationProvider);

            if (entity == null)
                throw new EntityNotFoundException($"Couldn't Find Entity {id} When GetByIdAsync");

            return entity;
        }

        public virtual async Task<TEditModel> GetForEditAsync(long id)
        {
            Guard.ArgumentInRange(id, 1, long.MaxValue, nameof(id));

            var entity = await UnTrackedEntitySet.Where(a => a.Id == id)
                .ProjectToFirstOrDefaultAsync<TEditModel>(Mapper.ConfigurationProvider);

            if (entity == null)
                throw new EntityNotFoundException($"Couldn't Find Entity {id} When GetForEditAsync");

            return entity;
        }

        public virtual Task<bool> ExistsAsync(long id)
        {
            Guard.ArgumentInRange(id, 1, long.MaxValue, nameof(id));

            return EntitySet.AnyAsync(a => a.Id == id);
        }


        [Transactional]
        public virtual void Delete(TDeleteModel model)
        {
            Guard.ArgumentNotNull(model, nameof(model));

            var entity = Mapper.Map<TEntity>(model);

            UnitOfWork.MarkAsDeleted(entity);
            UnitOfWork.SaveChanges();
        }

        [Transactional]
        public virtual void Delete(IList<TDeleteModel> models)
        {
            Guard.ArgumentNotEmpty(models, nameof(models));
            Guard.ArgumentNotEmpty(models, nameof(models));

            var entities = Mapper.Map<IList<TEntity>>(models);

            UnitOfWork.RemoveRange(entities);
            UnitOfWork.SaveChanges();
        }

        [Transactional]
        public virtual Task DeleteAsync(TDeleteModel model)
        {
            Guard.ArgumentNotNull(model, nameof(model));

            var entity = Mapper.Map<TEntity>(model);

            UnitOfWork.MarkAsDeleted(entity);
            return UnitOfWork.SaveChangesAsync();
        }

        [Transactional]
        public virtual Task DeleteAsync(IList<TDeleteModel> models)
        {
            Guard.ArgumentNotEmpty(models, nameof(models));
            Guard.ArgumentNotEmpty(models, nameof(models));

            var entities = Mapper.Map<IList<TEntity>>(models);

            UnitOfWork.RemoveRange(entities);
            return UnitOfWork.SaveChangesAsync();
        }

        #endregion

        #endregion

        #region Protected Methods

        /// <summary>
        ///     Apply Filtering To GetDynamicList
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        protected virtual IQueryable<TEntity> ApplyFiltering(TDynamicListRequest request)
        {
            Guard.ArgumentNotNull(request, nameof(request));

            return UnTrackedEntitySet;
        }

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

            return UnTrackedEntitySet;
        }

        /// <summary>
        ///     Apply Sorting To GetPagedList and GetPagedListAsync
        /// </summary>
        /// <param name="query">query</param>
        /// <param name="request">PagedListRequest</param>
        /// <returns></returns>
        protected virtual IQueryable<TEntity> ApplySorting(IQueryable<TEntity> query, TPagedListRequest request)
        {
            Guard.ArgumentNotNull(request, nameof(request));
            Guard.ArgumentNotNull(query, nameof(query));

            return !request.SortBy.IsEmpty() ? query.OrderBy(request.SortBy) : query.OrderByDescending(e => e.Id);
        }

        /// <summary>
        ///     Apply Paging To GetPagedList and GetPagedListAsync
        /// </summary>
        /// <param name="request">PagedListRequest</param>
        /// <param name="query">query</param>
        /// <returns></returns>
        protected virtual IQueryable<TEntity> ApplyPaging(IQueryable<TEntity> query, TPagedListRequest request)
        {
            Guard.ArgumentNotNull(request, nameof(request));
            Guard.ArgumentNotNull(query, nameof(query));

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

        #endregion
    }
}

همه متد‌های این کلاس پایه، قابلیت override شدن را دارند. به عنوان مثال یکسری متد با دسترسی protected مثلا ApplyFiltering هم برای بازنویسی نحوه فیلتر کردن خروجی GetPagedList می‌توانند در SubClassها مورد استفاده قرار گیرند. برای مباحث مرتب سازی هم از کتابخانه System.Linq.Dynamic استفاده شده است. 

برای مکانیزم Validation خودکار هم از کتابخانه FluentValidatoin کمک گرفته شده است و با استفاده از Interceptor زیر در صورت یافتن Validator مربوط به Model ورودی، عملیات اعتبارسنجی انجام میگرد و در صورت معتبر نبودن، استثنایی صادر خواهد شد که حاوی اطلاعات مربوط به جزئیات خطاها نیز می‌باشد.

ValidatorInterceptor

namespace MvcFramework.Framework.Aspects.Validation
{
    public class ValidatorInterceptor : ISyncInterceptionBehavior
    {
        private readonly IValidatorFactory _validatorFactory;

        public ValidatorInterceptor(IValidatorFactory validatorFactory)
        {
            _validatorFactory = validatorFactory;
        }

        public IMethodInvocationResult Intercept(ISyncMethodInvocation methodInvocation)
        {
            var argumentValues = methodInvocation.Arguments.Select(a => a.Value).ToArray();

            var validator = new MethodInvocationValidator(_validatorFactory, methodInvocation.MethodInfo,
                argumentValues);

            validator.Validate();

            return methodInvocation.InvokeNext();
        }
    }
}

کتابخانه جانبی دیگری برای AOP توسط تیم StructureMap به نام StructureMap.DynamicInterception ارائه شده است. نمونه‌ی استفاده از آن، در بالا مشخص می‌باشد. در اینجا انتقال مسئولیت اعتبارسنجی پارامترهای متدی که قرار است Intercept شود، به کلاسی به نام MethodInvocationValidator سپرده شده‌است.

کلاس MethodInvocationValidator

namespace MvcFramework.Framework.Aspects.Validation
{
    internal class MethodInvocationValidator
    {
        #region Constructor

        public MethodInvocationValidator(IValidatorFactory validatorFactory, MethodInfo method,
            object[] parameterValues)
        {
            Guard.ArgumentNotNull(method, nameof(method));
            Guard.ArgumentNotNull(parameterValues, nameof(parameterValues));
            Guard.ArgumentNotNull(validatorFactory, nameof(validatorFactory));

            _method = method;
            _parameterValues = parameterValues;
            _validatorFactory = validatorFactory;
            _parameters = method.GetParameters();

            _parametersToBeNormalized = new List<IShouldNormalize>();
        }

        #endregion

        #region Public Methods

        public void Validate()
        {
            if (!CheckShouldBeValidate()) return;

            foreach (var parameterValue in _parameterValues)
                ValidateMethodParameter(parameterValue);

            foreach (var parameterToBeNormalized in _parametersToBeNormalized)
                parameterToBeNormalized.Normalize();
        }

        #endregion

        #region Fields

        private readonly MethodInfo _method;
        private readonly object[] _parameterValues;
        private readonly ParameterInfo[] _parameters;
        private readonly IValidatorFactory _validatorFactory;
        private readonly List<IShouldNormalize> _parametersToBeNormalized;

        #endregion

        #region Private Methods

        private bool CheckShouldBeValidate()
        {
            if (!_method.IsPublic)
                return false;

            if (IsValidationDisabled())
                return false;

            if (_parameters.IsNullOrEmpty())
                return false;

            if (_parameters.Length != _parameterValues.Length)
                throw new Exception("Method parameter count does not match with argument count!");

            return true;
        }

        private bool IsValidationDisabled()
        {
            if (_method.IsDefined(typeof(EnableValidationAttribute), true))
                return false;

            return ReflectionHelper
                       .GetSingleAttributeOfMemberOrDeclaringTypeOrDefault<DisableValidationAttribute>(_method) != null;
        }

        private void ValidateMethodParameter(object parameterValue)
        {
            if (parameterValue == null) return;

            var parameterValueList = parameterValue as IEnumerable<object>;
            if (parameterValueList != null)
            {
                var valueList = parameterValueList.ToList();

                ValidateMethodParameterValues(valueList);
            }
            else
            {
                ValidateMethodParameterValues(new List<object> { parameterValue });
            }

            if (parameterValue is IShouldNormalize)
                _parametersToBeNormalized.Add(parameterValue as IShouldNormalize);
        }

        private void ValidateMethodParameterValues(List<object> valueList)
        {
            var ruleSet = GetRuleSet(_method);

            var validator = _validatorFactory.GetValidator(valueList.First().GetType());
            if (validator == null) return;

            foreach (var item in valueList)
                ValidateWithReflection(validator, item, ruleSet);
        }

        private static string GetRuleSet(MemberInfo method)
        {
            const string @default = "default";

            var attribute = method.GetCustomAttribute<ValidateWithRuleAttribute>();

            if (attribute == null)
                return @default;

            var rules = new List<string> { @default };

            rules.AddRange(attribute.RuleSetNames);

            return string.Join(",", rules).TrimEnd(',');
        }

        private static void ValidateAndThrow<T>(IValidator<T> validator, T argument, string ruleSet)
        {
            validator.ValidateAndThrow(argument, ruleSet);
        }

        private void ValidateWithReflection(IValidator validator, object argument, string ruleSet)
        {
            GetType().GetMethod(nameof(ValidateAndThrow), BindingFlags.Static | BindingFlags.NonPublic)
                .MakeGenericMethod(argument.GetType())
                .Invoke(null, new[] { validator, argument, ruleSet });
        }

        #endregion
    }
}

در متد Validate آن ابتدا چک می‌شود که آیا اعتبارسنجی می‌بایستی انجام شود یا خیر. سپس تک تک آرگومان‌های ارسالی را با استفاده از متد ValidateMethodParameter وارد مکانیزم اعتبارسنجی می‌کند. در داخل این متد ابتدا نوع آرگومان تشخیص داده شده و این مقادیر به متد ValidateMethodParameterValues ارسال شده و داخل آن ابتدا Validator مرتبط را یافته و آن را به متد ValidateWithReflection ارسال می‌کند. در این بین متد GetRuleSets وظیفه واکشی اسامی RuleSet هایی که بر روی متد مورد نظر تنظیم شده اند را دارد؛ برای مواقعی که از یک ویومدل برای ویرایش، درج و حذف استفاده کنید، در این صورت با توجه به اینکه برای یک ویومدل یک Validator خواهید داشت، امکانات RuleSet مربوط به FluentValidation کارساز خواهند بود. به این صورت که برای هر کدام از عملیات حذف، ویرایش و درج، RuleSet مناسب را تعریف کرده و با استفاده از ValidateWithRuleAttribute برروی متدهای مورد نظر، این ruleها در سیستم اعتبارسنجی ارائه شده اعمال خواهند شد.

با توجه به اینکه متد ValidateAndThrow در واسط IValidator‎<T>‎ تعریف شده‌است و از آنجاییکه ما نوع داده مدل مورد نظر را هم نداریم لازم است با استفاده از MakeGenericMethod به صورت داینامیک نوع داده T را مشخص کنیم و فراخوانی متد استاتیک ValidatorWithThrow‎<T>‎ را با Reflection انجام دهیم.

در ادامه لازم است ValidatorInterceptor معرفی شده را به StructureMap نیز معرفی کنیم. برای این منظور به شکل زیر عمل خواهیم کرد.

namespace MvcFramework.Framework
{
    public class FrameworkRegistry : Registry
    {
        public FrameworkRegistry()
        {
            For<IValidatorFactory>().Singleton().Use<StructureMapValidatorFactory>();

            Scan(scan =>
            {
                scan.TheCallingAssembly();
                scan.WithDefaultConventions();
                scan.LookForRegistries();
            });

            Policies.Interceptors(new DynamicProxyInterceptorPolicy(f => typeof(IApplicationService).IsAssignableFrom(f), typeof(ValidatorInterceptor),typeof(TransactionInterceptor)));
        }
    }
}

در کد بالا با استفاده از DynamicProxyInterceptorPolicy، یک Policy را برای Intercept کردن متدهای مربوط به کلاس هایی که پیاده ساز IApplicationService می‌باشند، معرفی کرده‌ایم.

کار اعتبارسنجی هم به پایان رسید؛ در زیر استفاده از سرویس پایه معرفی شده را می‌توانید مشاهده کنید.

namespace MyApp.ServiceLayer.Roles
{
    public interface IRoleApplicationService :
        ICrudApplicationService<RoleViewModel, RoleCreateViewModel, RoleEditViewModel, RoleDeleteViewModel, RolePagedListRequest, RoleListViewModel>
    {
    }
}

namespace MyApp.ServiceLayer.Roles
{
    public class RoleApplicationService :
        CrudApplicationService<Role, RoleViewModel, RoleCreateViewModel, RoleEditViewModel, RoleDeleteViewModel, RolePagedListRequest, RoleListViewModel>,
        IRoleApplicationService
    {
        #region Constructor

        public RoleApplicationService(IUnitOfWork unitOfWork, IMapper mapper) : base(unitOfWork, mapper)
        {
        }

        #endregion
    }
}


نکته: در این لایه بندی نکات مربوط به مطلب «پیاده سازی ماژولار Autofac» نیز با استفاده از StructureMap اعمال شده است. بدین ترتیب در هر لایه یک Registry مربوط به StructureMap ایجاد شده است. به شکل زیر:

FrameworkRegistry

namespace MyApp.Framework
{
    public class FrameworkRegistry : Registry
    {
        public FrameworkRegistry()
        {
            For<IValidatorFactory>().Singleton().Use<StructureMapValidatorFactory>();

            Scan(scan =>
            {
                scan.TheCallingAssembly();
                scan.WithDefaultConventions();
                scan.AssembliesFromApplicationBaseDirectory();
                scan.AddAllTypesOf<IRunOnEndTask>();
                scan.AddAllTypesOf<IRunOnOwinStartupTask>();
                scan.AddAllTypesOf<IRunOnStartTask>();
                scan.AddAllTypesOf<IRunOnBeginRequestTask>();
                scan.AddAllTypesOf<IRunOnErrorTask>();
                scan.AddAllTypesOf<IRunOnEndRequestTask>();

                scan.LookForRegistries();
            });

            Policies.Interceptors(new DynamicProxyInterceptorPolicy(f => typeof(IApplicationService).IsAssignableFrom(f), typeof(ValidatorInterceptor)/*, typeof(TransactionInterceptor)*/));
        }
    }
}


DataLayerRegistry

namespace MyApp.DataLayer
{
    public class DataLayerRegistry : Registry
    {
        public DataLayerRegistry()
        {
            Scan(scan =>
            {
                scan.TheCallingAssembly();
                scan.WithDefaultConventions();
                scan.AssembliesFromApplicationBaseDirectory();
                scan.AddAllTypesOf<IRunOnStartTask>();
            });

            //todo:use container per request (Nested Containers) instead of HttpContextLifeCycle
            For<IUnitOfWork>().Use<ApplicationDbContext>();
        }
    }
}


ServiceLayerRegistry

namespace MyApp.ServiceLayer
{
    public class ServiceLayerRegistry : Registry
    {
        #region Constructor

        public ServiceLayerRegistry()
        {
            Scan(scan =>
            {
                scan.TheCallingAssembly();
                scan.WithDefaultConventions();
                scan.AssembliesFromApplicationBaseDirectory();
                scan.AddAllTypesOf<IRunOnEndTask>();
                scan.AddAllTypesOf<IRunOnOwinStartupTask>();
                scan.AddAllTypesOf<IRunOnStartTask>();
                scan.AddAllTypesOf<IRunOnBeginRequestTask>();
                scan.AddAllTypesOf<IRunOnErrorTask>();
                scan.AddAllTypesOf<IRunOnEndRequestTask>();

                scan.Assembly(typeof(DataLayerRegistry).Assembly);
                scan.LookForRegistries();

                scan.AddAllTypesOf<Profile>().NameBy(item => item.FullName);
                scan.AddAllTypesOf<IHaveCustomMappings>().NameBy(item => item.FullName);
            });

            FluentValidationConfig();
            AutoMapperConfig();
        }

        #endregion

        #region Private Methods

        private void AutoMapperConfig()
        {
            For<MapperConfiguration>().Singleton().Use("MapperConfig", ctx =>
            {
                var config = new MapperConfiguration(cfg =>
                {
                    cfg.CreateMissingTypeMaps = true;
                    AddProfiles(ctx, cfg);
                    AddIHaveCustomMappings(ctx, cfg);
                    AddMapFrom(cfg);
                });

                config.AssertConfigurationIsValid();

                return config;
            });

            For<IMapper>().Singleton().Use(ctx => ctx.GetInstance<MapperConfiguration>().CreateMapper(ctx.GetInstance));
        }

        private void FluentValidationConfig()
        {
            AssemblyScanner.FindValidatorsInAssembly(Assembly.GetExecutingAssembly())
                .ForEach(result =>
                {
                    For(result.InterfaceType)
                        .Singleton()
                        .Use(result.ValidatorType);
                });
        }

        private static void AddMapFrom(IProfileExpression cfg)
        {
            var types = typeof(RoleViewModel).Assembly.GetExportedTypes();
            var maps = (from t in types
                        from i in t.GetInterfaces()
                        where i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IMapFrom<>) && !t.IsAbstract &&
                              !t.IsInterface
                        select new
                        {
                            Source = i.GetGenericArguments()[0],
                            Destination = t
                        }).ToArray();

            foreach (var map in maps)
                cfg.CreateMap(map.Source, map.Destination);
        }

        private static void AddProfiles(IContext ctx, IMapperConfigurationExpression cfg)
        {
            var profiles = ctx.GetAllInstances<Profile>().ToList();
            foreach (var profile in profiles)
                cfg.AddProfile(profile);
        }

        private static void AddIHaveCustomMappings(IContext ctx, IMapperConfigurationExpression cfg)
        {
            var mappings = ctx.GetAllInstances<IHaveCustomMappings>().ToList();
            foreach (var mapping in mappings)
                mapping.CreateMappings(cfg);
        }

        #endregion
    }
}


WebRegistry

namespace MyApp.Web
{
    public class WebRegistry : Registry
    {
        public WebRegistry()
        {
            Scan(scan =>
            {
                scan.TheCallingAssembly();
                scan.WithDefaultConventions();
                scan.AssembliesFromApplicationBaseDirectory();
                
                scan.AddAllTypesOf<IRunOnEndTask>();
                scan.AddAllTypesOf<IRunOnOwinStartupTask>();
                scan.AddAllTypesOf<IRunOnStartTask>();
                scan.AddAllTypesOf<IRunOnBeginRequestTask>();
                scan.AddAllTypesOf<IRunOnErrorTask>();
                scan.AddAllTypesOf<IRunOnEndRequestTask>();

                scan.Assembly(typeof(ServiceLayerRegistry).Assembly);
                scan.LookForRegistries();
            });
        }
    }
}

در این طراحی، لایه Web یا همان Presentation به DataLayer و DomainClasses هیچ ارجاعی ندارد.


در قسمت بعد استفاده از این سرویس را در یک برنامه ASP.NET MVC با هم بررسی خواهیم کرد. 

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

مطالب
بررسی تغییرات Blazor 8x - قسمت سوم - روش ارتقاء برنامه‌های Blazor Server قدیمی به دات نت 8
در قسمت قبل، با نحوه‌ی رندر سمت سرور و روش فعالسازی قابلیت‌های تعاملی در این حالت، آشنا شدیم. از این نکات می‌توان جهت ارتقاء ساختار پروژه‌های قدیمی Blazor Server به Blazor Server 8x استفاده کرد. البته همانطور که پیشتر نیز عنوان شد، در دات نت 8 دیگر خبری از قالب‌های قدیمی پروژه‌های blazor server و blazor wasm نیست و اگر دقیقا همین موارد مدنظر هستند، آن‌ها را می‌توان با تنظیم سطح رندر و میزان تعاملی که مدنظر است، شبیه سازی کرد و یا حتی هر دو را هم با هم در یک پروژه داشت.


1) به‌روز رسانی شماره نگارش دات‌نت

اولین قدم در جهت ارتقاء پروژه‌های قدیمی، تغییر شماره نگارش TargetFramework موجود در فایل csproj. به net8.0 است. پس از اینکار نیاز است تمام بسته‌های نیوگت موجود را نیز به نگارش‌های جدیدتر آن‌ها ارتقاء دهید.


2) فعالسازی حالت SSR تعاملی سمت سرور

پایه‌ی تمام تغییرات انجام شده‌ی در Blazor 8x، قابلیت SSR است و تمام امکانات دیگر برفراز آن اجرا می‌شوند. به همین جهت پس از ارتقاء شماره نگارش دات‌نت، نیاز است SSR را فعال کنیم و برای اینکار باید به هاست ASP.NET Core بگوئیم که درخواست‌های رسیده را به کامپوننت‌های Razor هدایت کند. بنابراین، به فایل Program.cs مراجعه کرده و دو تغییر زیر را به آن اعمال کنید:
// ...
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
// ...
app.MapRazorComponents<App>().AddInteractiveServerRenderMode();
یک نمونه‌ی کامل از فایل Program.cs را در قسمت قبل مشاهده کردید و یا حتی می‌توانید دستور dotnet new blazor --interactivity Server را جهت ساخت یک پروژه‌ی آزمایشی جدید بر اساس SDK دات نت 8 و ایده گیری از آن، اجرا کنید.

در اینجا ترکیب کامپوننت‌های تعاملی سمت سرور (AddInteractiveServerComponents) و رندر تعاملی سمت سرور (AddInteractiveServerRenderMode)، دقیقا همان Blazor Server قدیمی است که ما با آن آشنا هستیم.

یک نکته: اگر از قالب جدید dotnet new blazor --interactivity None استفاده کنیم، یعنی حالت تعاملی بودن آن‌را به None تنظیم کنیم، کلیات ساختار پروژه‌ای را که مشاهده خواهیم کرد، با حالت تعاملی Server آن یکی است؛ فقط در تنظیمات Program.cs آن، گزینه‌های فوق را نداریم و به صورت زیر ساده شده‌است:
// ...

builder.Services.AddRazorComponents();

// ...

app.MapRazorComponents<App>();
در این نوع برنامه‌ها نمی‌توان جزایر/قسمت‌های تعاملی Blazor Server را در صفحات و کامپوننت‌های SSR، تعریف کرد. در مورد جزایر تعاملی، در مطالب بعدی بیشتر بحث خواهیم کرد.


3) ایجاد فایل جدید App.razor

در دات نت 8، دیگر خبری از فایل آغازین Host.cshtml_ پروژه‌های Blazor Server قدیمی نیست و کدهای آن با تغییراتی، به فایل جدید App.razor منتقل شده‌اند. در این قسمت، کار هدایت درخواست‌های رسیده به کامپوننت‌های برنامه رخ می‌دهد و از این پس، صفحه‌ی ریشه‌ی برنامه خواهد بود.


در این تصویر، مقایسه‌ای را بین جریان پردازش یک درخواست رسیده در دات نت 8، با نگارش قبلی Blazor Server مشاهده می‌کنید. در دات نت 8، فایل Host.cshtml_ (یک Razor Page آغازین برنامه) با یک کامپوننت Razor به نام App.razor جایگزین شده‌است و فایل قدیمی App.razor این پروژه‌ها به Routes.razor، تغییر نام یافته‌است.

نمونه‌ای از فایل App.razor جدید را که در قسمت قبل نیز معرفی کردیم، در اینجا با جزئیات بیشتری بررسی می‌کنیم:
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <base href="/" />
    <link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
    <link rel="stylesheet" href="app.css" />
    <link rel="stylesheet" href="MyApp.styles.css" />
    <link rel="icon" type="image/png" href="favicon.png" />
    <HeadOutlet />
</head>

<body>
    <Routes />
    <script src="_framework/blazor.web.js"></script>
</body>

</html>
در این فایل جدید تغییرات زیر رخ داده‌اند:
- تمام دایرکتیوهای تعریف شده مانند page ،@addTagHelper@ و غیره حذف شده‌اند.
- base href تعریف شده اینبار فقط با یک / شروع می‌شود و نه با /~. این مورد خیلی مهم است! اگر به آن دقت نکنید، هیچکدام از فایل‌های استاتیک برنامه مانند فایل‌های css. و js.، بارگذاری نخواهند شد!
- پیشتر برای رندر HeadOutlet، از یک تگ‌هلپر استفاده می‌شد. این مورد در نگارش جدید با یک کامپوننت ساده جایگزین شده‌است.
- تمام component tag helper‌های پیشین حذف شده‌اند و نیازی به آن‌ها نیست.
- ارجاع پیشین فایل blazor.server.js با فایل جدید blazor.web.js جایگزین شده‌است.

یک نکته: همانطور که مشاهده می‌کنید، فایل App.razor یک کامپوننت است و اینبار به همراه تگ <script> نیز شده‌است. یعنی در این نگارش از Blazor می‌توان اسکریپت‌ها را در کامپوننت‌ها نیز ذکر کرد؛ فقط با یک شرط! این کامپوننت حتما باید SSR باشد. اگر این تگ اسکریپتی را در یک کامپوننت تعاملی ذکر کنید، همانند قابل (و نگارش‌های پیشین Blazor) با خطا مواجه خواهید شد.


4) ایجاد فایل جدید Routes.razor و مدیریت سراسری خطاها و صفحات یافت نشده

همانطور که عنوان شد، فایل قدیمی App.razor این پروژه‌ها به Routes.razor تغییر نام یافته‌است که درج آن‌را در قسمت body مشاهده می‌کنید. محتوای این فایل نیز به صورت زیر است:
<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
</Router>
این محتوای جدید، فاقد ذکر کامپوننت NotFound قبلی است؛ از این جهت که سیستم مسیریابی جدید Blazor 8x با ASP.NET Core 8x یکپارچه است و نیازی به این کامپوننت نیست. یعنی اگر قصد مدیریت آدرس‌های یافت نشده‌ی برنامه را دارید، باید همانند ASP.NET Core به صورت زیر در فایل Program.cs عمل کرده:
app.UseStatusCodePagesWithRedirects("/StatusCode/{0}");
و سپس کامپوننت جدید StatusCode.razor را برای مدیریت آن به نحو زیر به برنامه اضافه کنید و بر اساس responseCode دریافتی، واکنش‌های متفاوتی را ارائه دهید:
@page "/StatusCode/{responseCode}"

<h3>StatusCode @ResponseCode</h3>

@code {
    [Parameter] public string? ResponseCode { get; set; }
}

یک نکته: اگر پروژه‌ای را بر اساس قالب dotnet new blazor --interactivity Server ایجاد کنیم، در فایل Program.cs آن، چنین تنظیمی اضافه شده‌است:
if (!app.Environment.IsDevelopment())
{
   app.UseExceptionHandler("/Error", createScopeForErrors: true);
}
که دقیقا معادل رفتاری است که در برنامه‌های ASP.NET Core قابل مشاهده‌است. این مسیر Error/، به کامپوننت جدید Components\Pages\Error.razor نگاشت می‌شود. بنابراین اگر در برنامه‌های جدید Blazor Server، استثنائی رخ دهد، با استفاده از میان‌افزار ExceptionHandler فوق، کامپوننت Error.razor نمایش داده خواهد شد.  باید دقت داشت که این کامپوننت ویژه، تحت هر شرایطی در حالت یک static server component رندر می‌شود.

سؤال: در اینجا (برنامه‌های Blazor Server) چه تفاوتی بین UseExceptionHandler و UseStatusCodePagesWithRedirects وجود دارد؟
میان‌افزار UseExceptionHandler برای مدیریت استثناءهای آغازین برنامه، پیش از تشکیل اتصال دائم SignalR وارد عمل می‌شود. پس از آن و تشکیل اتصال وب‌سوکت مورد نیاز، فقط از میان‌افزار UseStatusCodePagesWithRedirects استفاده می‌کند.
اگر علاقمند نیستید تا تمام خطاهای رسیده را همانند مثال فوق در یک صفحه مدیریت کنید، می‌توانید حداقل سه فایل زیر را به برنامه اضافه کنید تا خطاهای متداول یافت نشدن آدرسی، بروز خطایی و یا عدم دسترسی را مدیریت کنند:

404.razor
@page "/StatusCode/404"

<PageTitle>Not found</PageTitle>

<h1>Not found</h1>
<p role="alert">Sorry, there's nothing at this address.</p>

500.razor
@page "/StatusCode/500"

<PageTitle>Unexpected error</PageTitle>

<h1>Unexpected error</h1>
<p role="alert">There was an unexpected error.</p>

401.razor
@page "/StatusCode/401"

<PageTitle>Not Authorized</PageTitle>

<h1>Not Authorized</h1>
<p role="alert">Sorry, you are not authorized to access this page.</p>


5) تعاملی کردن سراسری برنامه

پس از این تغییرات اگر برنامه را اجرا کنید، بر اساس روش جدید static server-side rendering کار می‌کند و تعاملی نیست. یعنی تمام کامپوننت‌های آن به صورت پیش‌فرض، یکبار بر روی سرور رندر شده و خروجی آن‌ها به مرورگر کاربر ارسال می‌شوند و هیچ اتصال دائم SignalR ای برقرار نخواهد شد. برای فعالسازی سراسری قابلیت‌های تعاملی برنامه و بازگشت به حالت Blazor Server قبلی، به فایل App.razor مراجعه کرده و دو تغییر زیر را اعمال کنید تا به صورت خودکار به تمام زیرکامپوننت‌ها، یعنی کل برنامه، اعمال شود:
<HeadOutlet @rendermode="@InteractiveServer" />
...
<Routes @rendermode="@InteractiveServer" />

نکته 1: اجرای دستور زیر در دات‌نت 8، قالب پروژه‌ای را ایجاد می‌کند که رفتار آن همانند پروژه‌های Blazor Server نگارش‌های قبلی دات‌نت است (این مورد را در قسمت قبل بررسی کردیم)؛ یعنی همه‌جای آن به صورت پیش‌فرض، تعاملی است:
dotnet new blazor --interactivity Server --all-interactive

نکته 2: البته ...  InteractiveServer، دقیقا همان حالت پیش‌فرض برنامه‌های Blazor Server قبلی نیست! این حالت رندر، به صورت پیش‌فرض به همراه پیش‌رندر (pre-rendering) هم هست. یعنی در این حالت، روال رویدادگردان OnInitializedAsync یک کامپوننت، دوبار فراخوانی می‌شود (که باید به آن دقت داشت و عدم توجه به آن می‌تواند سبب انجام دوباره‌ی کارهای سنگین آغازین یک کامپوننت شود)؛ یکبار برای پیش‌رندر صفحه به صورت یک HTML استاتیک (بدون فعال سازی هیچ قابلیت تعاملی) که برای موتورهای جستجو و بهبود SEO مفید است و بار دیگر برای فعالسازی قسمت‌های تعاملی آن، درست پس از زمانیکه اتصال SignalR صفحه، برقرار شد (البته امکان فعالسازی حالت پیش‌رندر در Blazor Server قبلی هم وجود داشت؛ ولی مانند Blazor 8x، به صورت پیش‌فرض فعال نبود). در صورت نیاز، برای سفارشی سازی و لغو آن می‌توان به صورت زیر عمل کرد:
@rendermode InteractiveServerRenderModeWithoutPrerendering

@code{
  static readonly IComponentRenderMode InteractiveServerRenderModeWithoutPrerendering = 
        new InteractiveServerRenderMode(false);
}

در مورد پیش‌رندر و روش مدیریت دوبار فراخوانی شدن روال رویدادگردان OnInitializedAsync یک کامپوننت در این حالت، در قسمت‌های بعدی این سری بیشتر بحث خواهد شد.
 
مطالب
PowerShell 7.x - قسمت پنجم - اسکریپت بلاک و توابع
همانطور که در قسمت قبل اشاره شد، توابع نیز یکی از ویژگی‌های اصلی PowerShell هستند. قبل از بررسی بیشتر توابع بهتر است ابتدا با مفهوم script block آشنا شویم. script blocks به مجموعه‌ایی از دستورات گفته میشود که داخل یک بلاک قرار میگیرند. در واقع هر چیزی داخل {} یک script block محسوب میشود (البته به جز hash tables). به عنوان مثال در کد زیر از یک script block مخصوص، با نام فیلتر استفاده شده است که یک ورودی برای پارامتر FilterScript مربوط به دستور Where-Object میباشد. چیزی که این script block را متمایز میکند، خروجی آن است. به این معنا که خروجی آن باید یک مقدار بولین باشد: 
Get-Process | Where-Object { $_.Name -eq 'Dropbox' }
script blocks را به صورت مستقیم درون command line هم میتوانیم استفاده کنیم. به محض تایپ کردن } و زدن کلید enter، امکان نوشتن اسکریپت‌های چندخطی را درون ترمینال خواهیم داشت. در نهایت با بستن script block و زدن کلید enter، از بلاک خارج خواهیم شد: 
PS /Users/sirwanafifi/Desktop> $block = {
>> $newVar = 10
>> Write-Host $newVar
>> }
با اینکار یک بلاک از کد را داخل متغیری با اسم block ذخیره کرده‌ایم. برای فراخوانی این قطعه کد میتوانیم از یک عملگر مخصوص با نام invocation operator یا call operator استفاده کنیم: 
PS /Users/sirwanafifi/Desktop> & $block
یا حتی میتوانیم از Invoke-Command نیز برای اجرای بلاک استفاده کنیم. همچنین از عملگر & برای فراخوانی یک expression رشته‌ایی نیز میتوان استفاده کرد: 
PS /Users/sirwanafifi/Desktop> & "Get-Process"
البته این نکته را در نظر داشته باشید که & قادر به پارز کردن (parse) یک expression نیست. به عنوان مثال اجرای کد زیر با خطا مواجه خواهد شد (برای حل این مشکل میتوانید بجای آن از Invoke-Expression استفاده کنید که امکان پارز کردن پارامترها را نیز دارد):
PS /Users/sirwanafifi/Desktop> & "1 + 1"
or
PS /Users/sirwanafifi/Desktop> & "Get-Process -Name Slack"

توابع
در قسمت قبل با نحوه ایجاد توابع آشنا شدیم. به این نوع توابع، basic functions گفته میشود و ساده‌ترین نوع توابع در PowerShell هستند. همچنین خیلی محدود نیز میباشند؛ یکسری ورودی/خروجی دارند. برای کنترل بیشتر روی نحوه فراخوانی توابع (به عنوان مثال دریافت ورودی از pipeline و…) باید از advanced functions یا توابع پیشرفته استفاده کنیم. در واقع به محض استفاده از اتریبیوتی با نام [()CmdletBinding] تابع ما تبدیل به یک advanced function خواهد شد. منظور از دریافت ورودی از pipeline این است که بتوانیم خروجی دستورات را به تابع‌مان pipe کنیم اینکار در basic function امکانپذیر نیست: 
Function Add-Something {
    Write-Host "$_ World"
}

"Hello" | Add-Something
اما با کمک advanced functions میتوانیم چنین قابلیتی را داشته باشیم: 
Function Add-Something {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline = $true)]
        [string]$Name
    )

    Write-Host "$Name World"
}

"Hello" | Add-Something
یکی دیگر از ویژگی‌های advanced functions امکان استفاده فلگ Verbose حین فراخوانی دستورات میباشد. به عنوان مثال قطعه کد زیر را در نظر بگیرید: 
$API_KEY = "...."

Function Read-WeatherData {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline = $true)]
        [string]$CityName
    )

    $Url = "https://api.openweathermap.org/data/2.5/forecast?q=$CityName&cnt=40&appid=$API_KEY&units=metric"
    Try {
        Write-Verbose "Reading weather data for $CityName"
        $Response = Invoke-RestMethod -Uri $Url
        $Response.list | ForEach-Object {
            Write-Verbose "Processing $($_.dt_txt)"
            [PSCustomObject]@{
                City               = $Response.city.name
                DateTime           = [DateTime]::Parse($_.dt_txt)
                Temperature        = $_.main.temp
                Humidity           = $_.main.humidity
                Pressure           = $_.main.pressure
                WindSpeed          = $_.wind.speed
                WindDirection      = $_.wind.deg
                Cloudiness         = $_.clouds.all
                Weather            = $_.weather.main
                WeatherDescription = $_.weather.description
            }
        } | Where-Object { $_.DateTime.Date -eq (Get-Date).Date }
        Write-Verbose "Done processing $CityName"
    }
    Catch {
        Write-Error $_.Exception.Message
    }
}
کاری که تابع فوق انجام میدهد، دریافت دیتای پیش‌بینی وضعیت آب‌وهوای یک شهر است. در حالت عادی فراخوانی تابع فوق پیام‌های Verbose را نمایش نمیدهد. از آنجائیکه تابع فوق یک advanced function است، میتوانیم فلگ Verbose را نیز وارد کنیم. با اینکار به صورت صریح گفته‌ایم که پیام‌های از نوع Verbose را نیز نمایش دهد: 
Read-WeatherData -CityName "London" -Verbose
هر چند این مقدار را همانطور که در قسمت‌های قبلی عنوان شد میتوانیم تغییر دهیم که دیگر مجبور نباشیم با فراخوانی هر تابع، این فلگ را نیز ارسال کنیم. بیشتر دستورات native نیز قابلیت نمایش پیام‌های Verbose را با ارسال همین فلگ در اختیارمان قرار میدهند. بنابراین بهتر است برای امکان مشاهده جزئیات بیشتر حین فراخوانی توابع‌مان از Write-Verbose استفاده کنیم. در ادامه اجزای دیگر توابع را بررسی خواهیم کرد (بیشتر این اجزا درون یک script block نیز قابل استفاده هستند)

کنترل کامل بر روی ورودی‌های توابع
بر روی ورودی‌های یک تابع میتوانیم کنترل نسبتاً کاملی داشتیم باشیم. PowerShell یک مجموعه وسیع از قابلیت‌ها را برای هندل کردن پارامترها و همچنین اعتبارسنجی ورودی‌ها ارائه میدهد. به عنوان مثال میتوانیم یک پارامتر را mandatory کنیم یا اینکه امکان positional binding و غیره را تعیین کنیم. اتریبیوت Parameter در واقع یک وهله از System.Management.Automation.ParameterAttribute میباشد. میتوانید با نوشتن دستور زیر لیستی از خواصی را که میتوانید همراه با این اتریبیوت تعیین کنید، مشاهده کنید: 
PS /> [Parameter]::new()

ExperimentName                  :
ExperimentAction                : None
Position                        : -2147483648
ParameterSetName                : __AllParameterSets
Mandatory                       : False
ValueFromPipeline               : False
ValueFromPipelineByPropertyName : False
ValueFromRemainingArguments     : False
HelpMessage                     :
HelpMessageBaseName             :
HelpMessageResourceId           :
DontShow                        : False
TypeId                          : System.Management.Automation.ParameterAttribute
در ادامه یک مثال از نحوه هندل کردن ورودی‌های یک تابع را بررسی خواهیم کرد. تابع زیر یک لیست از URLها را از کاربر دریافت کرده و یک health check توسط دستور Test-Connection انجام میدهد. در کد زیر پارامتر Websites را با تعدادی اتریبیوت مزین کرده‌ایم. توسط اتریبیوت Parameter تعیین کرده‌ایم که ورودی الزامی است و همچنین مقدار آن میتواند از pipeline نیز دریافت شود. در ادامه توسط ValidatePattern یک عبارت باقاعده را برای بررسی صحیح بودن URL دریافتی نوشته‌ایم. از آنجائیکه ورودی از نوع آرایه‌ایی از string تعریف شده است، این تست برای هر آیتم از آرایه بررسی خواهد شد. برای پارامتر دوم یعنی Count نیز رنج مقداری را که کاربر وارد میکند، حداقل ۳ و حداکثر ۳ انتخاب کرده‌ایم: 
Function Ping-Website {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidatePattern('^www\..*')]
        [string[]]$Websites,
        [ValidateRange(1, 3)]
        [int]$Count = 3
    )
    $Results = @()
    $Websites | ForEach-Object {
        $Website = $_
        $Result = Test-Connection -ComputerName $Website -Count $Count -Quiet
        $ResultText = $Result ? 'Success' : 'Failed'
        $Results += @{
            Website = $Website
            Result  = $ResultText
        }
        Write-Verbose "The result of pinging $Website is $ResultText"
    }
    $Results | ForEach-Object { 
        $_ | Select-Object @{ Name = "Website"; Expression = { $_.Website }; }, @{ Name = "Result"; Expression = { $_.Result }; }, @{ Name = "Number Of Attempts"; Expression = { $Count }; } 
    }
}
یکی دیگر از اعتبارسنجی‌هایی که میتوانیم برای پارامترهای یک تابع انتخاب کنیم، ValidateScript است. توسط این اتریبیوت میتوانیم یک منطق سفارشی برای اعتبارسنجی مقادیر پارامترها بنویسیم. به عنوان مثال تابع فوق را به گونه‌ایی تغییر خواهیم داد که لیست وب‌سایت‌ها را از طریق یک فایل JSON دریافت کند. میخواهیم قبل از دریافت فایل مطمئن شویم که فایل، به صورت فیزیکی روی دیسک وجود دارد، در غیراینصورت باید یک خطا را به کاربر نمایش دهیم: 
Function Ping-Website {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateScript({
                If (-Not ($_ | Test-Path) ) {
                    Throw "File or folder does not exist" 
                }
                If (-Not ($_ | Test-Path -PathType Leaf) ) {
                    Throw "The Path argument must be a file. Folder paths are not allowed."
                }
                If ($_ -NotMatch "(\.json)$") {
                    throw "The file specified in the path argument must be either of type json"
                }
                Return $true
            })]
        [Alias("src", "source", "file")]
        [System.IO.FileInfo]$Path,
        [int]$Count = 1
    )
    $Results = [System.Collections.ArrayList]@()
    $Urls = Get-Content -Path $Path | ConvertFrom-Json
    $Urls | ForEach-Object -Parallel {
        $Website = $_.url
        $Result = Test-Connection -ComputerName $Website -Count $using:Count -Quiet
        $ResultText = $Result ? 'Success' : 'Failed'
        $Item = @{
            Website = $Website
            Result  = $ResultText
        }
        $null = ($using:Results).Add($Item)
    }
    
    $Results | ForEach-Object -Parallel { 
        $_ | Select-Object @{ Name = "Website"; Expression = { $_.Website }; }, @{ Name = "Result"; Expression = { $_.Result }; }, @{ Name = "Number Of Attempts"; Expression = { $using:Count }; } 
    }
}
تابع Ping-Website را جهت بررسی فیچر جدیدی که همراه با دستور ForEach-Object استفاده میشود، تغییر داده‌ایم تا به صورت Parallel عمل کند؛ این قابلیت از نسخه ۷ به بعد به PowerShell اضافه شده است. از آنجائیکه این قابلیت باعث میشود script block مربوط به ForEach-Object درون یک context دیگر با نام runspace اجرا شود. در نتیجه برای دسترسی به متغیرهای بیرون از script block نیاز خواهیم داشت از یک متغیر خودکار تحت‌عنوان using قبل از نام متغیر و بعد از علامت $ استفاده کنیم. همچنین آرایه مثال قبل را نیز به ArrayList تغییر داده‌ایم. زیرا در حالت قبلی امکان تغییر سایز یک آرایه با سایز ثابت را نخواهیم داشت. نکته دیگری که در مورد کد فوق میتوان به آن توجه کرد، نال کردن خروجی متد Add مربوط به آرایه‌ی Results است. همانطور که در قسمت قبل توضیح دادیم، از این تکنیک برای suppress کردن خروجی استفاده میکنیم و چون در اینجا خروجی متد Add یک عدد میباشد، با تکنیک فوق، خروجی را دیگر درون کنسول مشاهده نخواهیم کرد. توسط اتریبیوت Alias نیز نام‌های دیگری را که میتوان برای پارامتر Path حین فراخوانی تابع استفاده کرد، تعیین کرده‌ایم. لیست کامل اتریبیوت‌هایی را که میتوان برای پارامترهای یک تابع تعیین کرد، میتوانید در مستندات PowerShell ببینید. 
نکته: اگر تابع فوق را همراه با فلگ Verbose فراخوانی کنیم، لاگ‌های موردنظر را درون کنسول مشاهده نخواهیم کرد؛ زیرا همانطور که اشاره شد script block درون یک context جدا اجرا میشود و باید متغیرهای خودکار مربوط به Output را مجدداً مقداردهی کنیم:
Function Ping-Website {
    [CmdletBinding()]
    Param(
        # As before
    )
    # As before
    $Urls | ForEach-Object -Parallel {
        $DebugPreference = $using:DebugPreference 
        $VerbosePreference = $using:VerbosePreference 
        $InformationPreference = $using:InformationPreference 
        
        # As before
    }
    
    # As before
}

قابلیت تعریف بلاک‌ها/توابع، به صورت تودرتو  
درون توابع و script block امکان نوشتن بلاک‌های تودرتو را نیز داریم:
$scriptBlock = {
    $logOutput = {
        param($message)
        Write-Host $message
    }

    [int]$someVariable = 10
    $doSomeWork = {
        & $logOutput -message "Some variable value: $someVariable"
    }
    $someVariable = 20

    & $doSomeWork
}
خروجی بلاک فوق  Some variable value: 20 خواهد بود؛ زیرا قبل از فراخوانی doSomeWork مقدار متغیر عددی someVariable را به ۲۰ تغییر داده‌ایم. برای script blocks این امکان را داریم که دقیقاً در همان جایی که بلاک را تعریف میکنیم، یک snapshot تهیه کنیم. در اینحالت خروجی، مقدار Some variable value: 10 خواهد شد: 
$scriptBlock = {
    $logOutput = {
        param($message)
        Write-Host $message
    }

    [int]$someVariable = 10
    $doSomeWork = {
        & $logOutput -message "Some variable value: $someVariable"
    }.GetNewClosure()
    $someVariable = 20

    & $doSomeWork
}
یکسری بلاک‌های ویژه نیز درون توابع و script blockها میتوانیم بنویسیم که اصطلاحاً به name blocks معروف هستند:
begin
process
end
dynamicparam
درون یک تابع اگر هیچکدام از بلاک‌های فوق استفاده نشود، به صورت پیش‌فرض بدنه تابع، درون بلاک end قرار خواهد گرفت. بلاک begin قبل از شروع pipeline اجرا میشود. process به ازای هر آیتم pipe شده اجرا خواهد شد. end نیز در پایان اجرا میشود. به عنوان مثال تابع زیر را در نظر بگیرید:
function Show-Pipeline {
    begin { 
        Write-Host "Pipeline start" 
    }
    process { 
        Write-Host  "Pipeline process $_" 
    }
    end { 
        Write-Host  "Pipeline end $_" 
    }
}
در ادامه یکسری آیتم را به ورودی این تابع pipe خواهیم کرد:
PS /> 1..2 | Show-Pipeline                                   
Pipeline start 
Pipeline process 1
Pipeline process 2
Pipeline end 2
همانطور که مشاهده میکنید، به ازای هر آیتم pipe شده، یکبار بلاک process اجرا شده است. همچنین برای دسترسی به مقدار آیتم pipe شده نیز از متغیر خودکار _$ استفاده کرده‌ایم (PSItem$ نیز به همین متغیر اشاره دارد).

با توجه به توضیحات named blockهای فوق، اکنون اگر بخواهیم نسخه اول تابع Ping-Website را با pipe کردن یک آرایه فراخوانی کنیم، خروجی که در کنسول نمایش داده خواهد شد، تنها آیتم آخر از آرایه خواهد بود:
PS /> "www.google.com", "www.yahoo.com" | Ping-Website                 

Website       Result  Number Of Attempts
-------       ------  ------------------
www.yahoo.com Success                  3
دلیل آن نیز این است که به صورت صریح کدها را درون بلاک process ننوشته بودیم. همانطور که عنوان شد، در حالت پیش‌فرض، بدنه توابع درون بلاک end قرار خواهند گرفت و تنها یکبار اجرا خواهند شد. بنابراین:
Function Ping-Website {
    [CmdletBinding()]
    Param(
        # As before
    )
    process {
        # As before
    }
}
اینبار اگر تابع را مجدداً فراخوانی کنیم، خروجی مطلوب را نمایش خواهد داد:
PS /> "www.google.com", "www.yahoo.com" | Ping-Website

Website        Result  Number Of Attempts
-------        ------  ------------------
www.google.com Success                  3
www.yahoo.com  Success                  3

بلاک dynamicparam
از این بلاک برای تعریف پارامترهای داینامیک که به صورت on the fly نیاز هست ایجاد شوند، استفاده میشود. برای درک بهتر آن فرض کنید میخواهیم تابعی را بنویسیم که امکان خواندن یک فایل CSV را به ما میدهد. تا اینجای کار توسط Import-CSV به یک خط دستور قابل انجام است. اما فرض کنید میخواهیم به کاربر این امکان را بدهیم که یک ستون موردنظر از فایل را مشاهده کند. همچنین میخواهیم یک اعتبارسنجی هم روی نام ستونی که کاربر قرار است وارد کند نیز داشته باشیم. به عنوان مثال یک فایل CSV با ستون‌های name, lname, age داریم و کاربر میخواهد تنها ستون اول یک name را واکشی کند:
PS /> Read-Csv ./users.csv -Columns name
برای اینکار میتوانیم با کمک dynamic param یک پارامتر را در زمان اجرا ایجاده کرده و مقادیری را که کاربر برای ستون‌ها مجاز است وارد کند، براساس هدر فایل CSV تنظیم کنیم:
using namespace System.Management.Automation
Function Read-Csv {
    Param (
        [Parameter(Mandatory = $true, Position = 0)]
        [string]$Path
    )
    DynamicParam {
        $firstLine = Get-Content $Path | Select-Object -First 1
        [String[]]$headers = $firstLine -split ', '
        $parameters = [RuntimeDefinedParameterDictionary]::new()
        $parameter = [RuntimeDefinedParameter]::new(
            'Columns', [String[]], [Attribute[]]@(
                [Parameter]@{ Mandatory = $false; Position = 1 }
                [ValidateSet]::new($headers)
            )
        )
        $parameters.Add($parameter.Name, $parameter) 
        Return $parameters
    }
    Begin {
        $csvContent = Import-Csv $Path
        If ($PSBoundParameters.ContainsKey('Columns')) {
            $columns = $PSBoundParameters['Columns']
            $csvContent | Select-Object -Property $columns
        }
        Else {
            $csvContent
        }
    }
}
درون کنسول PowerShell هم یک IntelliSense برای مقادیر مجاز نمایش داده خواهد شد: