مطالب
مایکرو سرویس‌ها - قسمت 2 - بررسی ویژگی‌ها
در قسمت قبل با مفهوم مایکرو سرویس‌ها آشنا شدیم. سرویس‌های کوچک و مجزایی که بصورت مستقل، قابلیت توسعه و استقرار دارند و در راستای انجام یک قابلیت کسب و کار در اختیار دیگران قرار می‌گیرند.

ویژگی‌های یک مایکرو سرویس چیست؟

بعد از آشنایی با معماری مایکرو سرویس‌ها می‌خواهیم با ویژگی‌های آن آشنا شویم. البته باید به این نکته توجه داشت که همه‌ی معماری‌های مایکروسرویس‌ها این ویژگی‌ها را ندارند؛ ولی میتوان انتظار داشت اکثر آن‌ها این ویژگی‌ها را از خود به نمایش بگذارند.

Componentization via Services 

تعریف ما از component، واحدی از نرم افزار می‌باشد که به تنهایی قابل جایگزینی و به‌روز رسانی می‌باشد.
همانطور که گفته شد، مایکرو سرویس‌ها یک نرم افزار را به سرویس‌های (business component) کوچکتری تقسیم میکنند که به تنهایی قابل توسعه و استقرار می‌باشند. ما کتابخانه‌ها را به عنوان component در نظر میگیریم که به نرم افزار ما پیوند خورده‌اند و به وسیله فراخوانی توابع در پروسه نرم افزار قابل استفاده می‌باشند. در حالیکه سرویس‌ها componentهایی هستند که خارج از پروسه نرم افزار ما می‌باشند و به وسیله مکانیزم‌های ارتباطی مانند Web Service Request و Remote Procedure Call قابل دسترسی هستند.
در سیستم های یکپارچه که از چندین  component تشکیل شده‌اند و این componentها در جریان یک پروسه با هم در ارتباط هستند، اگر بخواهیم در یک component تغییر ایجاد کنیم، این تغییر نیازمند استقرار مجدد کل سیستم می‌باشد. در حالیکه در معماری مایکرسرویس، کافی است شما  component (سرویس) ای را که تغییر کرده‌است، دوباره مستقر کنید و سایر بخشهای سیستم از این تغییر تاثیر نمی‌پذیرند.

Technology Heterogeneity 

یک سیستم را در نظر بگیرید که از همکاری  چندین سرویس تشکیل شده‌است. ما میتوانیم تصمیم بگیریم که برای هر کدام از سرویسها از تکنولوژی متفاوتی استفاده کنیم. این امر به ما اجازه می‌دهد برای حل هر مشکل، از بهترین ابزار و تکنولوژی استفاده کنیم. این امر بر خلاف سیستم‌هایی که تنها باید از تکنولوژی بکار رفته در سیستم استفاده کنند، انعطاف پذیری سیستم را بسیار بالا می‌برد.
برای روشن شدن موضوع فرض کنید می‌خواهیم در بخشی از سیستم، performance را افزایش دهیم. برای این امر میتوانیم از هر تکنولوژی که به ما کمک میکند، استفاده کنیم. برای نمونه در یک سیستم شبکه اجتماعی، می‌توانیم اطلاعات مربوط به کاربران و ارتباطات آن‌ها را در یک دیتابیس مبتنی بر گراف و اطلاعات مربوط به پست‌ها و نظرات کاربران را در یک دیتابیس سندگرا ذخیره کنیم. به این ترتیب مشاهده میکنیم که وابسته‌ی به تکنولوژی خاصی نمی‌باشیم و می‌توانیم بر اساس نیاز خود، از تکنولوژی مورد نظر بهره ببریم.


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

Resilience 

چنانچه یکی ازسرویسها دچار اشکال شود و این مسئله بصورت زنجیروار برای تمام سرویس‌ها رخ ندهد، با جدا شدن آن سرویس، درروند کار سیستم خللی بوجود نمی‌آید و سیستم بدون کمترین مشکلی به ادامه کار خود می‌پردازد. یعنی محدوده یک سرویس، دیوار حائلی می‌شود تا سایر بخشها تاثیر نپذیرند. در سیستم‌های یکپارچه چنانچه یک سرویس دچار اشکال شود، سایر بخش‌های سیستم نیز غیر قابل استفاده می‌شوند. البته می‌توان با نصب نسخه‌های متفاوتی بر روی ماشین‌های متفاوت (Scale out)، تاثیر اینگونه اختلالات را تا حدودی کاهش داد. اما بوسیله مایکرو سرویسها می‌توانیم سیستم‌هایی را بسازیم که قادرند با خطاهای بوجود آمده در سرویس‌ها به‌درستی رفتار کنند؛ تا خللی در کار سیستم ایجاد نشود.

Scaling

فرض کنید در بخشی ازیک سیستم، دغدغه Performance بوجود می‌آید. چنانچه ما یک معماری یکپارچه را داشته باشیم، مجبوریم کل آن را scale کنیم. درحالیکه این مشکل تنها در بخش کوچکی از نرم افزار ایجاد شده‌است. اما اگرمعماری ما مایکرو سرویس باشد، فقط همان سرویس مربوطه را که بر روی آن دغدغه داریم، scale می‌کنیم و سایر بخش‌های نرم افزار بدون نیاز به تغییر و بزرگ شدن، به کار خود ادامه می‌دهند.


Gilt  یک خرده فروش آنلاین در صنعت مد می‌باشد که  بخاطر مسائل Performance به معماری مایکروسرویس‌ها روی آورد. آنها در سال 2007 با یک نرم افزار یکپارچه شروع به کار کردند. اما بعد از دوسال سیستم آنها قادر به مقابله با ترافیک سایت نبود. آنها با تقسیم بندی قسمت‌های اصلی سیستم خود به مایکرو سرویسها، توانستند خیلی بهتر و موثرتر با ترافیک رسیده مقابله کنند و قابلیت دسترسی پذیری سیستم را افزایش دهند. در حال حاضر سیستم  آنها از حدود 450 مایکرو سرویس تشکیل شده که هر کدام روی چندین ماشین مختلف در حال اجرا می‌باشند.


Ease of Deployment 

تغییر حتی یک خط کد، در برنامه‌ای که از چندین میلیون خط تشکیل شده است، ما را ملزم به استقرار مجدد کل سیستم  و انتشار نسخه جدید می‌کند. این استقرار می‌تواند تاثیر و ریسک بالایی را داشته باشد و ما را مجبور به تغییرات بیشتر و انتشار نسخه‌های دیگر کند که این حجم زیاد تغییرات ممکن است به محصول ما ضربه بزند.
اما در مایکرو سرویس‌ها یک تغییر کوچک، تمام سیستم را تحت تاثیر قرار نمی‌دهد. بلکه تنها تغییرات مربوط به سرویس مورد نظر می‌باشد که به صورت مستقل قابلیت استقرار را دارد و چنانچه سیستم دچار مشکل شود، منشاء تغییرات کاملا مشخصی می‌باشد و می‌توان سریعتر سیستم را به وضعیت قابل اطمینان بازگرداند. این ویژگی یکی از مهمترین دلایلی می‌باشد که شرکت‌هایی مانند Netflix و Amazon  به پیاده سازی این معماری روی آورده‌اند.

Organizational Alignment 

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

Composability

یکی از ویژگی‌های کلیدی که در سیستم‌های توزیع شده و سرویس گرا به آن وعده داده می‌شود، قابلیت استفاده مجدد از functionality‌های سیستم می‌باشد. مایکرو سرویس‌ها این امکان را برای ما فراهم می‌کنند تا functionality ‌های سیستم، به روش‌های مختلف و با اهداف گوناگونی مورد استفاده قرار گیرند. اینکه استفاده کنندگان نرم‌افزار ما چگونه از آن استفاده می‌کنند، برای ما مهم است. در حال حاضر ما باید درباره راه‌های مختلفی که می‌توانند قابلیت‌های سیستم را باهم ترکیب کنند تا در برنامه‌های وب، موبایل و یا ابزار‌های پوشیدنی مورد استفاده قرار گیرند، فکر کنیم. در مایکرو سرویس تفکر ما این است که بخش‌های مختلف سیستم خود را از هم جدا کنیم و این بخشها توسط استفاده کننده‌های بیرونی قابل دسترسی باشند و با تغییر شرایط بتوانیم  بخش‌های گوناگونی را بسازیم.


Optimizing for Replaceability

اگر شما در یک سازمان متوسط و یا بزرگ، مشغول به کار باشید، ممکن است با یک سیستم بزرگ و قدیمی مواجه شده باشید که در گوشه‌ای از سازمان در حال استفاده می‌باشد و هیچ کسی رغبت نزدیک شدن به آن و دست بردن در آن را ندارد و اگر کسی از شما بپرسد «چرا نمی‌توان آن را تغییر داد؟» خواهید گفت این کار، بسیار پرریسک و پردردسر است.
با استفاده از معماری مایکروسرویس، هزینه این تغییرات به مراتب کمتر و تغییرات با ضریب اطمینان بالاتری انجام می‌شوند.
نظرات مطالب
اجرای وظایف زمان بندی شده با Quartz.NET - قسمت دوم

ممنون از شما آقا سعید

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

درمورد برنامه‌های وب برای این کار : من یکبار این کار را انجام دادم و از cache expiration استفاده کردم. ولی چون این برنامه‌ی وب هیچ request ای نداشت برنامه down شد و عملیات زمان بندی متوقف شد.

پس میشه گفت استفاده از سرویس‌های ویندوزی، راه حل متداولی هست؟

مطالب
رسم نمودار توسط 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)

مطالب
آموزش LINQ بخش ششم - عملگرهای پرس و جو قسمت اول
عملگرهای استاندارد پرس و جو

در یک طبقه بندی کلی، عملگرهای پرس و جو بر اساس ورودی و خروجی آنها به سه دسته تقسیم می‌شوند:
1- نتیجه‌ی توالی ورودی، بصورت یک توالی، به خروجی ارسال می‌شود.
2- نتیجه‌ی توالی ورودی، بصورت یک عنصر یکتا و واحد به خروجی ارسال می‌شود.
3- اثری از ورودی در توالی خروجی وجود ندارد (این عملگرها عناصر خودشان را تولید می‌کنند).

دسته‌ی آخر شاید کمی عجیب به نطر برسد. این عملگرها هیچ توالی ورودی را دریافت نمی‌کنند. مثلا می‌توان از طریق این عملگر‌ها، یک توالی از اعداد صحیح را تولید کرد.
تقسیم بندی عملگرهای پرس و جو بر اساس عملکرد به صورت زیر می‌باشد : 
  • محدود کننده (Restriction)
where
  • بازتابی (Projection)
Select,SelectMany 
  • جداکننده (Partitioning)
Take,Skip,TakeWhile,SkipWhile 
  • مرتب سازی (Ordering)
OrderBy,OrderByDescending,ThenBy,ThenByDescending,Reverse 
  • گروه بندی (Grouping)
GroupBy 
  • مجموعه (Set)
Concat,Union,Intersect,Except 
  • تبدیل (Conversion)
ToArray,ToList,ToDictionary,ToLookup,OfType,Cast 
  • عنصر(Element)
First,FirstOrDefault,Last,LastOrDefalt,Single,SingleOrDefault 
  • عنصر در (ElementAt)
ElementAtOrDefault,DefaultIfEmpty 
  • تولید (Generation)
Empty,Range,Report 
  • کمی (Quantifier)
Any,All,Contains,SequenceEqual 
  • مجموعه (Aggregate)
Count,LongCount,Sum,Min,Max,Average,Aggregate 
  • اتصال (Join)
Join,GroupJoin,Zip 

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

عملگرهای محدود کننده (Restriction Operators)
این عملگرها یک توالی ورودی را دریافت و یک توالی محدود شده یا به بیان دیگر فیلتر شده را تولید می‌کنند. عناصر توالی خروجی، عناصری هستند که با فیلتر اعمال شده مطابقت دارند.
Where
این عملگر، عناصری را به خروجی ارسال می‌کند که با گزاره‌ی (Predicate) تعریف شده مطابقت داشته باشند.
نکته : گزاره (Predicate) تابعی است که اگر شرط آن تامین شود، مقدار true و در غیر اینصورت مقدار false را باز می‌گرداند.
مثال : 
 Ingredient[] ingredients =
{
   new Ingredient{Name = "Sugar", Calories=500},
   new Ingredient{Name = "Egg", Calories=100},
   new Ingredient{Name = "Milk", Calories=150},
   new Ingredient{Name = "Flour", Calories=50},
   new Ingredient{Name = "Butter", Calories=200},
};

IEnumerable<Ingredient> query = ingredients.Where(x => x.Calories >= 200);
foreach (var ingredient in query)
{
   Console.WriteLine(ingredient.Name);
}
در کد فوق از عملگر where استفاده شده است. گزاره‌ی (x=>x.Calories>=200) به ازای هر غذایی که کالری آن مساوی یا بزرگتر از 200 باشد، مقدار true را باز می‌گرداند.
خروجی کد بالا:
 Sugar
Butter
عملگر where امضای دیگری دارد که اندیس عنصر ورودی توالی را نیز می‌پذیرد. در مثال قبل، اندیس Sugar برابر 0 و اندیس Butter برابر 4 است. پرس و جوی زیر خروجی مشابه مثال قبل را تولید می‌کند.
 IEnumerable<Ingredient> query = ingredients.Where((ingredient, index) => ingredient.Name == "Sugar" || index == 4);
گزاره نوشته شده در این پرس و جو  از نوع <Func<Ingredient,int,bool خواهد بود و پارامتر int، اندیس عنصر در توالی ورودی می‌باشد.

پیاده سازی توسط عبارت‌های پرس و جو
 در روش عبارت‌های پرس و جو، کلمه‌ی کلیدی where به‌همراه یک عبارت منطقی در پرس و جو ظاهر می‌شود:
 IEnumerable<Ingredient> gueryExpression =
from i in ingredients
where i.Calories >= 200
select i;


عملگرهای بازتاب (Projection Operators)

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

Select
عملگر پرس و جوی select هر عنصر توالی ورودی را به یک عنصر در توالی خروجی تبدیل می‌کند. تعداد عناصر ورودی و خروجی در این حالت یکسان می‌باشند.
پرس و جوی زیر عناصر توالی ورودی Ingredient را به عناصر رشته‌ای در توالی خروجی بازتاب می‌کند. عبارت Lambda تعریف شده، نحوه‌ی بازتاب عناصر را مشخص می‌کند (هر عنصر ingredient به یک عنصر رشته‌ای بازتاب می‌شود):
 IEnumerable<string> query = ingredients.Select(x => x.Name);
  می‌توان توالی خروجی با عناصر صحیح را نیز تولید کرد:  
 IEnumerable<int> query = ingredients.Select(x => x.Name.Length);

در عملیات بازتاب می‌توان یک شیء جدید را در توالی خروجی ایجاد کرد. در کد زیر عناصر Ingredient به یک عنصر جدید از نوع IngredientNameAndLenght بازتاب شده است.
class IngredientNameAndLength
{
    public string Name { get; set; }
    public int Length { get; set; }
    public override string ToString()
    {
      return Name + " " + Length;
    }
}

IEnumerable<IngredientNameAndLength> query = ingredients.Select(x =>
new IngredientNameAndLength
{
   Name = x.Name,
   Length = x.Name.Length
});
پرس و جوی بالا را می‌توان به شکل نوع‌های بی نام نیز بازنویسی کرد. باید دقت شود که نوع بازگشتی این پرس و جو باید از نوع var باشد.
var query = ingredients.Select(x =>
new
{
   Name = x.Name,
   Length = x.Name.Length
});
خروجی کد بالا به شکل زیر است :
{ Name = Sugar, Length = 5 }
{ Name = Egg, Length = 3 }
{ Name = Milk, Length = 4 }
{ Name = Flour, Length = 5 }
{ Name = Butter, Length = 6 }

پیاده سازی توسط عبارت‌های پرس و جو

کلمه‌ی کلیدی select در عبارت‌های پرس و جو، به شکل زیر استفاده می‌شود:
var query = from i in ingredients
select new
{
    Name=i.Name,
    Length=i.Name.Length
};

SelectMany 
برعکس دستور select که به ازای هر عنصر در توالی ورودی، یک عنصر را در توالی خروجی بازتاب می‌کرد، دستور SelectMany ممکن است تعداد عناصر کمتر و یا بیشتری را در توالی خروجی بازتاب کند (انتخاب مقادیر یک مجموعه از مجموعه‌ی دیگر).
عبارت Lambda نوشته شده در عملگر Select، یک مقدار را باز می‌گرداند. اما عبارت Lambda نوشته شده در عملگر SelectMany، یک توالی فرزند (Child Sequence) را ایجاد می‌کند. توالی فرزند ممکن است حاوی تعداد مختلفی از عناصر به ازای هر عنصر در توالی ورودی باشد.
در مثال زیر عبارت Lambda یک توالی فرزند از کاراکتر‌ها ایجاد می‌کند (یک کاراکتر به ازای هر حرف از هر عنصر توالی ورودی). به‌طور مثال عنصر ورودی Sugar، پس از پردازش توسط  عبارت Lambda، یک توالی فرزند با 5 عنصر 's','u','g','e','r' فراهم می‌کند. هر رشته‌ی در توالی Ingredient می‌تواند تعداد حروف متفاوتی داشته باشد. در نتیجه عبارت Lambda، توالی‌های فرزندی با طول‌های مختلف ایجاد می‌کند.
مثال:
string[] ingredients = {"Sugar","Egg","Milk","Flour","Butter"};
IEnumerable<char> query = ingredients.SelectMany(x => x.ToCharArray());
foreach (var item in query)
{
   Console.WriteLine(item);
}
خروجی مثال بالا :
 S
u
g
a
r
E
g
g
M
i
l
k
F
l
o
u
r
B
u
t
t
e
r

پیاده سازی توسط عبارت‌های پرس و جو

در روش عبارت‌های پرس و جو یک عبارت (clause) اضافی from برای تولید یک توالی فرزند به کار برده می‌شود. خروجی کد زیر مشابه کد قبلی است:
 string[] ingredients = {"Sugar","Egg","Milk","Flour","Butter"};
IEnumerable<char> query2 = from i in ingredients
from c in i.ToCharArray()
select c;

foreach (var item in query2)
{
   Console.WriteLine(item);
}

عملگرهای جداکننده (Partitioning Operators)
عملگر‌های جداکننده، یک توالی ورودی را دریافت و آنها را از هم جدا می‌کنند.

Take
عملگر Takeیک توالی ورودی را دریافت کرده و تعداد مشخصی از توالی را باز می‌گرداند.
مثال: عملگر Take، سه عضو اول توالی Ingredient را باز می‌گرداند:
 Ingredient[] ingredients =
{
   new Ingredient{Name = "Sugar", Calories=500},
   new Ingredient{Name = "Egg", Calories=100},
   new Ingredient{Name = "Milk", Calories=150},
   new Ingredient{Name = "Flour", Calories=50},
   new Ingredient{Name = "Butter", Calories=200},
};

IEnumerable<Ingredient> query = ingredients.Take(3);
foreach (var ingredient in query)
{
   Console.WriteLine(ingredient.Name);
}
خروجی کد بالا :
 Sugar
Egg
Milk
همچون سایر عملگر‌های پرس و جو، عملگر Take هم می‌تواند بصورت زنجیروار استفاده شود. در مثال زیر ابتدا عملگر Where برای محدود کردن عناصر با شرطی خاص و سپس عملگر Take برای جدا کردن عناصر حاصل از نتیجه‌ی مرحله قبل مورد استفاده قرار گرفته است:
Ingredient[] ingredients =
{
   new Ingredient{Name = "Sugar", Calories=500},
   new Ingredient{Name = "Egg", Calories=100},
   new Ingredient{Name = "Milk", Calories=150},
   new Ingredient{Name = "Flour", Calories=50},
   new Ingredient{Name = "Butter", Calories=200},
};

IEnumerable<Ingredient> query = ingredients.Where(x=>x.Calories>100).Take(2);
foreach (var ingredient in query)
{
   Console.WriteLine(ingredient.Name);
}
خروجی کد بالا :
Sugar
Milk

پیاده سازی توسط عبارت‌های پرس و جو

کلمه‌ی کلیدی (Key word) جایگزینی برای عملگر Take وجود ندارد، ولی می‌توان با ترکیب دو روش نوشتن پرس و جو، خروجی مورد نظر را تولید کرد:
 IEnumerable<Ingredient> query =
(from i in ingredients
  where i.Calories > 100
  select i).Take(2);
TakeWhile
عملگر TakeWhile بر عکس عملگر Take تعداد مشخصی را باز می‌گرداند . این عملگر تا زمانی که گزاره با عناصر مطابقت داشته باشد، اجرا می‌شود و در غیر اینصورت خاتمه پیدا می‌کند.
کد زیر تا زمانی که خصوصیت Calorie توالی ورودی بزرگتر و مساوی 100 باشد، عناصر را جدا می‌کند.
Ingredient[] ingredients =
{
   new Ingredient{Name = "Sugar", Calories=500},
   new Ingredient{Name = "Egg", Calories=100},
   new Ingredient{Name = "Milk", Calories=150},
   new Ingredient{Name = "Flour", Calories=50},
   new Ingredient{Name = "Butter", Calories=200},
};

IEnumerable<Ingredient> query = ingredients.TakeWhile(x => x.Calories >= 100);
foreach (var ingredient in query)
{
   Console.WriteLine(ingredient.Name);
}
خروجی کد بالا :
 Sugar
Egg
Milk
همانطور که مشاهده می‌کنید، وقتی عملگر TakeWhile به عنصری می‌رسد که گزاره‌ی آن را نقض می‌کند، متوقف می‌شود (در اینجا Flour). در حالی که ممکن است عناصری بعد از Flour وجود داشته باشند که با گزاره‌ی TakeWhile تطابق داشته باشند.

پیاده سازی توسط عبارت‌های پرس و جو
برای این عملگر هم کلمه‌ی کلیدی (Key word) جایگزینی وجود ندارد و با ترکیب دو روش نوشتن پرس و جو نتیجه‌ی دلخواه حاصل می‌شود.
 
Skip
این عملگر تعداد مشخصی از عناصر را از ابتدای توالی نادیده گرفته و باقی عناصر را باز می‌گرداند.
کد زیر سه عضو اول توالی را نادیده گرفته و مابقی را باز می‌گرداند:
Ingredient[] ingredients =
{
   new Ingredient{Name = "Sugar", Calories=500},
   new Ingredient{Name = "Egg", Calories=100},
   new Ingredient{Name = "Milk", Calories=150},
   new Ingredient{Name = "Flour", Calories=50},
   new Ingredient{Name = "Butter", Calories=200},
};

IEnumerable<Ingredient> query = ingredients.Skip(3);
foreach (var ingredient in query)
{
   Console.WriteLine(ingredient.Name);
}
خروجی کد بالا :
 Flour
Butter

پیاده سازی توسط عبارت‌های پرس و جو

برای این عملگر هم کلمه‌ی کلیدی (Key word) جایگزینی وجود ندارد و با ترکیب دو روش نوشتن پرس و جو، نتیجه‌ی دلخواه حاصل می‌شود.
با ترکیب عملگر Take و Skip می‌توان اطلاعات را به‌صورت صفحه بندی به کاربر ارائه کرد. مثال زیر این حالت را نشان می‌دهد.
IEnumerable<Ingredient> firstPage = ingredients.Take(2);
IEnumerable<Ingredient> secondPage = ingredients.Skip(2).Take(2);
IEnumerable<Ingredient> thirdPage = ingredients.Skip(4).Take(2);

Console.WriteLine("First Page : ");
foreach (var ingredient in firstPage)
{
   Console.WriteLine(" - " + ingredient.Name);
}

Console.WriteLine("Second Page : ");
foreach (var ingredient in secondPage)
{
   Console.WriteLine(" - " + ingredient.Name);
}

Console.WriteLine("Third Page : ");
foreach (var ingredient in thirdPage)
{
   Console.WriteLine(" - " + ingredient.Name);
}
خروجی کد بالا :
 First Page :
 - Sugar
 - Egg
Second Page :
 - Milk
 - Flour
Third Page :
 - Butter
SkipWhile
عملگر SkipWhile، مثل عملگر TakeWhile، از یک گزاره برای ارزیابی عناصر توالی استفاده می‌کند. این عملگر تا زمانیکه عناصر توالی، گزاره را نقض نکنند، عناصر را نادیده می‌گیرد.

مثال:
Ingredient[] ingredients =
{
   new Ingredient{Name = "Sugar", Calories=500},
   new Ingredient{Name = "Egg", Calories=100},
   new Ingredient{Name = "Milk", Calories=150},
   new Ingredient{Name = "Flour", Calories=50},
   new Ingredient{Name = "Butter", Calories=200},
};

IEnumerable<Ingredient> query = ingredients.SkipWhile(x => x.Name != "Milk");
foreach (var ingredient in query)
{
   Console.WriteLine(ingredient.Name);
}
خروجی کد بالا:
 Milk
Flour
Butter
مطالب
شروع به کار با EF Core 1.0 - قسمت 15 - نوشتن آزمون‌های واحد
یکی از مشخصات آزمون‌های واحد، عدم خروج از مرزهای IO سیستم، در حین اجرای آن‌ها است و چون درهنگام کار با بانک‌های اطلاعاتی حتما از مرزهای IO سیستم رد خواهیم شد (کار با شبکه، کار با فایل سیستم، برای به روز رسانی و درج اطلاعات)، نوشتن آزمون‌های واحد واقعی، برای برنامه‌هایی که از ORMها استفاده می‌کنند مشکل است. به همین جهت مباحث mocking، تقلید قسمت‌های مختلف ORMها و جایگزین کردن آن‌ها با نمونه‌های درون حافظه‌ای بسیار مرسوم است. برای رفع این مشکلات، تیم EF Core، یک تامین کننده‌ی بانک اطلاعاتی ویژه‌ی «درون حافظه‌ای» را به نام «Entity Framework Core InMemory provider» ارائه داده‌است. به این ترتیب، این محل ذخیره سازی اطلاعات درون حافظه‌ای، مشکل رد شدن از مرزهای IO سیستم را برطرف کرده و عملا نیاز به کار کردن با فریم ورک‌های mocking را منتفی می‌کند (حداقل برای تقلید قسمت‌های مختلف EF Core).
در این قسمت ابتدا نحوه‌ی فعال سازی فریم ورک آزمون‌های واحد مایکروسافت و سپس نحوه‌ی فعال سازی این تامین کننده‌ی بانک اطلاعاتی درون حافظه‌ای را بررسی خواهیم کرد. به علاوه برای سرویس بلاگ‌های قسمت قبل نیز آزمون واحد خواهیم نوشت.


نحوه‌ی فعالسازی فریم ورک MSTest در یک پروژه‌ی Class library از نوع NET Core.


تنها نکته‌ی مهم فعالسازی MSTest در یک پروژه‌ی Class library جدید که برای نوشتن آزمون‌های واحد مورد استفاده قرار خواهیم داد، تنظیمات فایل project.json آن است که در ذیل آمده است:
{
    "version": "1.0.0-*",
 
    "testRunner": "mstest",
    "dependencies": {
        "Microsoft.NETCore.App": {
            "type": "platform",
            "version": "1.0.0"
        },
        "dotnet-test-mstest": "1.1.1-preview",
        "MSTest.TestFramework": "1.0.1-preview",
        "NETStandard.Library": "1.6.0",
        "Microsoft.EntityFrameworkCore": "1.0.0",
        "Microsoft.EntityFrameworkCore.InMemory": "1.0.0",
        "Core1RtmEmptyTest.DataLayer": "1.0.0-*",
        "Core1RtmEmptyTest.Entities": "1.0.0-*",
        "Core1RtmEmptyTest.Services": "1.0.0-*",
        "Core1RtmEmptyTest.ViewModels": "1.0.0-*"
    },
 
    "frameworks": {
        "netcoreapp1.0": {
            "imports": [
                "dnxcore50",
                "portable-net45+win8"
            ]
        }
    }
}
- در اینجا قید testRunner الزامی است؛ در غیراینصورت آزمون‌های واحد شما شناسایی نخواهند شد. همچنین بسته‌های dotnet-test-mstest و MSTest.TestFramework نیز باید اضافه شوند.
- به علاوه در اینجا ارجاعاتی را به اسمبلی‌های موجودیت‌ها، Services و DataLayer که در قسمت «شروع به کار با EF Core 1.0 - قسمت 14 - لایه بندی و تزریق وابستگی‌ها» بررسی شدند نیز ملاحظه می‌کنید.
- همچنین وابستگی جدید Microsoft.EntityFrameworkCore.InMemory نیز در اینجا قابل ملاحظه است. این وابستگی را تنها به پروژه‌ی آزمون‌های واحد خود اضافه می‌کنیم. از این جهت که تنظیمات آن صرفا در این قسمت جدید قید می‌شوند و نه در سایر قسمت‌های برنامه.

 پس از آن، کار با این فریم ورک، همانند سایر نگارش‌های دات نت خواهد بود:
using Microsoft.VisualStudio.TestTools.UnitTesting;
 
namespace EFCore.MsTests
{
    [TestClass]
    public class CoreTests
    {
        [TestMethod]
        public void Test1()
        {
            Assert.IsTrue(true);
        }
    }
}
ابتدا کلاس مدنظر، با ویژگی TestClass مزین می‌شود. سپس متد آزمون واحد نوشته شده نیز باید به صورت public void و مزین شده‌ی با ویژگی TestMethod، ارائه شود.
پس از نوشتن اولین آزمون واحد، یکبار پروژه را build کرده و سپس از منوی Test، گزینه‌ی Windows را انتخاب کرده و در اینجا گزینه‌ی Test Explorer را انتخاب کنید. اندکی صبر کنید تا آزمون‌های واحد شما شناسایی شوند و سپس گزینه‌ی Run All را انتخاب کنید:



تغییرات Context برنامه جهت استفاده‌ی از تامین کننده‌ی داخل حافظه‌ای

در مورد نحوه‌ی تعریف و افزودن وابستگی‌های EF Core در مطلب «شروع به کار با EF Core 1.0 - قسمت 1 - برپایی تنظیمات اولیه» پیشتر بحث شد و همچنین در مطلب «شروع به کار با EF Core 1.0 - قسمت 3 - انتقال مهاجرت‌ها به یک اسمبلی دیگر»، اطلاعات Context برنامه را به اسمبلی دیگری منتقل کردیم.
اگر از روش بازنویسی متد OnConfiguring برای تنظیم تامین کننده‌ی بانک اطلاعاتی مورد نظر استفاده می‌کنید، متد OnConfiguring کلاس Context برنامه چنین شکلی را پیدا می‌کند:
public class ApplicationDbContext : DbContext, IUnitOfWork
{
    private readonly IConfigurationRoot _configuration;
 
    public ApplicationDbContext(IConfigurationRoot configuration)
    {
        _configuration = configuration;
    }
 
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
    {
    } 
 
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if (!optionsBuilder.IsConfigured)
        {
            optionsBuilder.UseSqlServer(
                _configuration["ConnectionStrings:ApplicationDbContextConnection"]
                , serverDbContextOptionsBuilder =>
                {
                    var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds;
                    serverDbContextOptionsBuilder.CommandTimeout(minutes);
                });
        }
    }
در اینجا دو تغییر جدید قابل ملاحظه هستند:
الف) اضافه شدن سازنده‌ی دومی که <DbContextOptions<ApplicationDbContext را دریافت می‌کند. از آن در سمت کدهای آزمون واحد برنامه جهت ثبت ()options.UseInMemoryDatabase استفاده می‌شود.
ب) به متد OnConfiguring، بررسی optionsBuilder.IsConfigured هم اضافه شده‌است. چون در سمت کدهای آزمون واحد، تامین کننده‌ی بانک اطلاعاتی درون حافظه‌ای اضافه می‌شود، مقدار optionsBuilder.IsConfigured به true تنظیم خواهد شد و دیگر از تامین کننده‌ی SQL Server استفاده نمی‌شود.

اگر از متد OnConfiguring به این شکل استفاده نمی‌کنید، تنها ذکر سازنده‌ی دوم ضروری است. از این جهت که در آزمون‌های واحد، از تنظیمات متد ConfigureServices کلاس آغازین برنامه استفاده نخواهد شد.


نوشتن آزمون‌های واحد مخصوص EF Core

پس از برپایی پیشنیازهای نوشتن آزمون‌ها واحد، شامل تنظیمات فریم ورک MSTest و همچنین افزودن وابستگی‌های مرتبط با فایل project.json ایی که در ابتدای بحث عنوان شد و اصلاح سازنده و متد OnConfiguring کلاس Context برنامه جهت آماده سازی آن‌ها برای پذیرش تامین کننده‌های دیگر، اکنون یک نمونه از آزمون‌های واحد درون حافظه‌ای EF Core، چنین شکلی را خواهد داشت:
using System;
using System.Linq;
using Core1RtmEmptyTest.DataLayer;
using Core1RtmEmptyTest.Entities;
using Core1RtmEmptyTest.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
 
namespace Core1RtmEmptyTest.MsTests
{
    [TestClass]
    public class CoreTests
    {
        private readonly IServiceProvider _serviceProvider;
 
        public CoreTests()
        {
            var services = new ServiceCollection();
            services.AddEntityFrameworkInMemoryDatabase()
                        .AddDbContext<ApplicationDbContext>(options => options.UseInMemoryDatabase());
 
            services.AddScoped<IUnitOfWork, ApplicationDbContext>();
            services.AddScoped<IBlogService, BlogService>();
 
            _serviceProvider = services.BuildServiceProvider();
        }
 
        [TestMethod]
        public void Find_searches_url()
        {
            // Insert seed data into the database using one instance of the context
            using (var serviceScope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope())
            {
                using (var context = serviceScope.ServiceProvider.GetRequiredService<IUnitOfWork>())
                {
                    context.Set<Blog>().Add(new Blog { Url = "http://sample.com/cats" });
                    context.Set<Blog>().Add(new Blog { Url = "http://sample.com/catfish" });
                    context.Set<Blog>().Add(new Blog { Url = "http://sample.com/dogs" });
                    context.SaveAllChanges();
                }
            }
 
            // Use a separate instance of the context to verify correct data was saved to database
            using (var serviceScope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope())
            {
                using (var context = serviceScope.ServiceProvider.GetRequiredService<IUnitOfWork>())
                {
                    Assert.AreEqual(3, context.Set<Blog>().Count());
                    Assert.AreEqual("http://sample.com/cats", context.Set<Blog>().First().Url);
                }
            }
 
            // Use a clean instance of the context to run the test
            using (var serviceScope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope())
            {
                var blogService = serviceScope.ServiceProvider.GetRequiredService<IBlogService>();
                var results = blogService.GetPagedBlogsAsNoTracking(pageNumber: 0, recordsPerPage: 10);
                Assert.AreEqual(3, results.Count);
            }
        }
    }
}
توضیحات:
همانطور که در قسمت «تغییرات Context برنامه جهت استفاده‌ی از تامین کننده‌ی داخل حافظه‌ای» فوق عنوان شد، در حین انجام آزمون‌های واحد، دیگر به کلاس آغازین برنامه و تنظیمات آن مراجعه نمی‌شود. بنابراین باید شبیه به عملکرد متد ConfigureServices آن‌را در اینجا پیاده سازی کرد. نمونه‌ای از انجام اینکار را در سازنده‌ی کلاس انجام آزمون‌های واحد مشاهده می‌کنید:
        private readonly IServiceProvider _serviceProvider;
 
        public CoreTests()
        {
            var services = new ServiceCollection();
            services.AddEntityFrameworkInMemoryDatabase()
                        .AddDbContext<ApplicationDbContext>(options => options.UseInMemoryDatabase());
 
            services.AddScoped<IUnitOfWork, ApplicationDbContext>();
            services.AddScoped<IBlogService, BlogService>();
 
            _serviceProvider = services.BuildServiceProvider();
        }
در اینجا است که توسط متد AddEntityFrameworkInMemoryDatabase، کار افزودن تامین کننده‌ی بانک اطلاعاتی درون حافظه‌ای انجام شده و سپس Context برنامه نیز از آن مطلع می‌شود (علت افزودن سازنده‌ی دومی که <DbContextOptions<ApplicationDbContext را دریافت می‌کند).
سپس همانند قبل، باید تمام سرویس‌های مدنظر تنظیم شوند تا بتوان از آن‌ها استفاده کرد.

نکته‌ی مهم دیگری را که باید به آن دقت داشت، ایجاد scope و سپس دسترسی به سرویس‌ها از طریق این Scope است. از این جهت که چون خارج از طول عمر یک درخواست وب قرار داریم، دیگر Scopeها برای ما به صورت خودکار ایجاد و تخریب نمی‌شوند و باید همان‌کاری را که ASP.NET Core در پشت صحنه انجام می‌دهد، به صورت دستی پیاده سازی کنیم:
            using (var serviceScope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope())
            {
                using (var context = serviceScope.ServiceProvider.GetRequiredService<IUnitOfWork>())
                {
اگر اینکار صورت نگیرد، چون Scope ایی ایجاد و تخریب نمی‌شود، کار کردن با متد serviceProvider.GetRequiredService_ اشتباه بوده و همیشه یک وهله از Context را باز می‌گرداند که مدنظر ما نیست. شبیه به این نکته را در قسمت «مقدار دهی اولیه‌ی جداول بانک‌های اطلاعاتی در EF Core» پیشتر ملاحظه کرده‌اید.


یک نکته‌ی تکمیلی

EF Core به همراه تامین کننده‌ی بانک اطلاعاتی SQLite نیز هست. یکی از نکات ویژه‌ی بانک اطلاعاتی SQLite، امکان تنظیم پارامتری است در رشته‌ی اتصالی آن، که آن‌را نیز تبدیل به یک «بانک اطلاعاتی درون حافظه‌ای» می‌کند. این روش سال‌ها است که جهت انجام آزمون‌های واحد ORMها مورد استفاده قرار می‌گیرد. بنابراین می‌توان آن‌را به عنوان جایگزینی برای مطلب جاری نیز درنظر گرفت.
 var connectionStringBuilder = new SqliteConnectionStringBuilder { DataSource = ":memory:" };
var connectionString = connectionStringBuilder.ToString();
var connection = new SqliteConnection(connectionString);
services.AddEntityFrameworkSqlite().AddDbContext<CmsDbContext>(options => options.UseSqlite(connection));
اهمیت آن در اینجا است که تامین کننده‌ی بانک اطلاعاتی درون حافظه‌ای EF، قیود را اعمال نمی‌کند ؛ اما بانک اطلاعاتی درون حافظه‌ای SQLite واقعا همانند یک بانک اطلاعاتی رابطه‌ای کامل عمل می‌کند.
مطالب
شروع به کار با DNTFrameworkCore - قسمت 3 - پیاده‌سازی سرویس‌های موجودیت‌ها
در قسمت قبل سناریوهای مختلف مرتبط با طراحی موجودیت‌های سیستم را بررسی کردیم. در این قسمت به طراحی DTO‌های متناظر با موجودیت‌ها به همراه اعتبارسنج‌های مرتبط و در نهایت به پیاده سازی سرویس‌های CRUD آنها خواهیم پرداخت. 
قراردادها، مفاهیم و نکات اولیه
  1. برخلاف بسیاری از طراحی‌های موجود، بر فراز هر موجودیت اصلی (منظور AggregateRoot) باید یک DTO که از این پس با عنوان Model از آنها یاد خواهیم کرد، تعریف شود. 
  2. هیچ تراکنشی برای موجودیت‌های فرعی یا همان Detailها نخواهیم داشت. این موجودیت‌ها در تراکنش موجودیت اصلی مرتبط به آن مدیریت خواهند شد.
  3. هر Commandای که قرار است مرتبط با یک موجودیت اصلی در سیستم انجام پذیرد، باید از منطق تجاری آن موجودیت عبور کند و نباید با دور زدن منطق تجاری، از طرق مختلف تغییراتی بر آن موجودیت اعمال شود. (موضوع مهمی که در ادامه مطلب جاری تشریح خواهد شد)
  4. ویوهای مختلفی از یک موجودیت می‌توان انتظار داشت که ویو پیش‌فرض آن در CrudService تدارک دیده شده است. برای سایر موارد نیاز است در سرویس مرتبط، متدهای Read مختلفی را پیاده‌سازی کنید.
  5. با اعمال اصل CQS، متدهای ثبت و ویرایش در کلاس سرویس پایه CRUD، بعد از انجام عملیات مربوطه، Id و RowVersion مدل ورودی و هچنین Id و TrackingState موجودیت‌های فرعی وابسته، مقداردهی خواهند شد و نیاز به انجام یک Query دیگر و بازگشت آن به عنوان خروجی متدها نبوده است. به همین دلیل خروجی این متدها صرفا Result ای می‌باشد که نشان از امکان Failure بودن انجام آنها می‌باشد که با اصل مذکور در تضاد نمی‌باشد.
  6. ورودی متدهای Read شما که در اکثر موارد نیاز به مهیا کردن خروجی صفحه‌بندی شده دارند، باید از نوع PagedQueryModel و یا اگر همچنین نیاز به جستجوی پویا براساس فیلدهایی موجود در ReadModel مرتبط دارید، باید از نوع FilteredPagedQueryModel باشد. متدهای الحاقی برای اعمال خودکار این صفحه‌‌بندی و جستجوی پویا در نظر گرفته شده است. همچنین خروجی آنها در اکثر موارد از نوع IPagedQueryResult خواهد بود. اگر نیاز است تا جستجوی خاصی داشته باشید که خصوصیتی متناظر با آن فیلد در مدل Read وجود ندارد، لازم است تا از این QueryModel‌های مطرح شده، ارث‌بری کرده و خصوصیت اضافی مدنظر خود را تعریف کنید. بدیهی است که اعمال جستجوی این موارد خاص به عهده توسعه دهنده می‌باشد.
  7. عملیات ثبت، ویرایش و حذف، برای کار بر روی لیستی از وهله‌های Model، طراحی شده‌اند. این موضوع در بسیاری از دومین‌ها قابلیت مورد توجهی می‌باشد. 
  8. رخداد متناظر با عملیات CUD مرتبط با هر موجودیت اصلی، به عنوان یکسری نقاط قابل گسترش (Extensibility Point) در اختیار سایر بخش‌های سیستم می‌باشد. این رخدادها درون تراکنش جاری Raise خواهند شد؛ از این جهت امکان اعمال یکسری Rule جدید از سمت سایر موءلفه‌های سیستم موجود می‌باشد.
  9. برخلاف بسیاری از طراحی‌های موجود، قصد ایجاد لایه انتزاعی برفراز EF Core  به منظور رسیدن به Persistence Ignorance را ندارم. بنابراین امروز بسته DNTFrameworkCore.EntityFramework آن آماده می‌باشد. اگر توسعه دهنده‌ای قصد یکپارچه کردن این زیرساخت را با سایر ORMها یا Micro ORMها داشته باشد، می‌تواند Pull Request خود را ارسال کند.
  10. خبر خوب اینکه هیچ وابستگی به AutoMapper به منظور نگاشت مابین موجودیت‌ها و مدل‌های متناظر آنها، در این زیرساخت وجود ندارد. با پیاده سازی متدهای MapToModel و MapToEntity می‌توانید از کتابخانه Mapper مورد نظر خودتان استفاده کنید؛ یا به صورت دستی این کار را انجام دهید. بعد از چند سال استفاده از AutoMapper، این روزها خیلی اعتقادی به استفاده از آن ندارم.
  11. هیچ وابستگی به FluentValidation به منظور اعتبارسنجی ورودی متدها یا پیاده‌سازی قواعد تجاری، در این زیرساخت وجود ندارد. شما امکان استفاده از Attributeهای اعتبارسنجی توکار، پیاده سازی IValidatableObject توسط مدل یا در موارد خاص به منظور پیاده سازی قواعد تجاری پیچیده، پیاده سازی IModelValidator را دارید. با این حال برای یکپارچگی با این کتابخانه محبوب، می‌توانید بسته نیوگت DNTFrameworkCore.FluentValidation را نصب کرده و استفاده کنید.
  12. با اعمال الگوی Template Method در پیاده سازی سرویس CRUD پایه، از طریق تعدادی متد با پیشوندهای Before و After متناظر با عملیات CUD می‌توانید در فرآیند انجام آنها نیز دخالت داشته باشید؛ به عنوان مثال: BeforeEditAsync یا AfterCreateAsync
  13. باتوجه به اینکه در فرآیند انجام متدهای CUD، یکسری Event هم Raise خواهند شد و همچنین در خیلی از موراد شاید نیاز به فراخوانی SaveChange مرتبط با UnitOfWork جاری باشد، لذا مطمئن‌ترین راه حل برای این قضیه و حفظ ثبات سیستم، همان استفاده از تراکنش محیطی می‌باشد. از این جهت متدهای مذکور با TransactionAttribute نیز تزئین شده‌اند که برای فعال سازی این مکانیزم نیاز است تا TransactionInterceptor مربوطه را به سیستم معرفی کنید.
  14. ValidationInterceptor موجود در زیرساخت، در صورتیکه خروجی متد از نوع Result باشد، خطاهای ممکن را در قالب یک شی Result بازگشت خواهد داد؛ در غیر این صورت یک استثنای ValidationException پرتاب می‌شود که این مورد هم توسط GlobalExceptionFilter مدیریت خواهند شد و در قالب یک BadRequest به کلاینت ارسال خواهد شد.
  15. در سناریوهای Master-Detail، قرارداد این است که Detailها به همراه Master متناظر واکشی خواهند شد و در زمان ثبت و یا ویرایش هم همه آنها به همراه Master متناظر خود به سرور ارسال خواهند شد. 
نکته مهم:  همانطور که اشاره شد، در سناریوهای Master-Detail باید تمامی Detailها به سمت سرور ارسال شوند. در این صورت سناریویی را در نظر بگیرید که قرار است کاربر در front-office سیستم امکان حذف یک قلم از اقلام فاکتور را داشته باشد؛ این درحالی است که در back-office و در منطق تجاری اصلی، ما جایی برای حذف یک تک قلم ندیده‌ایم و کلا منطق و قواعد تجاری حاکم بر فاکتور را زیر سوال می‌برد. چرا که ممکن است یکسری قواعد تجاری متناسب با دومین، بر روی لیست اقلام یک فاکتور در زمان ذخیره سازی وجود داشته باشند که با حذف یک تک قلم از یک مسیر فرعی، کل فاکتور را در حالت نامعتبری برای ذخیره سازی‌های بعدی قرار دهد. در این موارد باید API شما یک DTO سفارشی را دریافت کند که شامل شناسه قلم فاکتور و شناسه فاکتور می‌باشد. سپس با استفاده از شناسه فاکتور و سرویس متناظر، آن را واکشی کرده و از لیست قلم‌های InvoiceModel، آن قلم را با TrackingState.Deleted علامت‌گذاری کنید. همچنین باید توجه داشته باشید که برروی فیلدهای موجود در جداول مرتبط با موجودیت‌های Detail، محدودیت‌های دیتابیسی از جمله Unique Constraint و ... را اعمال نکنید؛ مگر اینکه میدانید و دقیقا مطمئن باشید عملیات حذف اقلام، قبل از عملیات ثبت اقلام جدید رخ می‎دهد (این موضوع نیاز به توضیح و شبیه سازی شرایط خاص آن را دارد که در صورت نیاز می‌توان در مطلب جدایی به آن پرداخت).
‌پیاده سازی و بررسی تعدادی سرویس فرضی
برای شروع لازم است بسته‌های نیوگت زیر را نصب کنید:
PM> Install-Package DNTFrameworkCore -Version 1.0.0
PM> Install-Package DNTFrameworkCore.EntityFramework -Version 1.0.0

مثال اول: پیاده‌سازی سرویس یک موجودیت ساده بدون نیاز به ReadModel 
گام اول: طراحی Model متناظر
[LocalizationResource(Name = "SharedResource", Location = "DNTFrameworkCore.TestAPI")]
public class BlogModel : MasterModel<int>, IValidatableObject
{
    public string Title { get; set; }

    [MaxLength(50, ErrorMessage = "Maximum length is 50")]
    public string Url { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (Title == "BlogTitle")
        {
            yield return new ValidationResult("IValidatableObject Message", new[] {nameof(Title)});
        }
    }
}
مدل متناظر با موجودیت‌های اصلی باید از کلاس جنریک MasterModel ارث‌بری کرده باشد. همانطور که ملاحظه می‌کنید، برای نشان دادن مکانیزم اعتبارسنجی، از DataAnnotationها و IValidatableObject استفاده شده‌است. LocalizationResource برای مشخص کردن نام و محل فایل Resource متناظر برای خواندن پیغام‌های اعتبارسنجی استفاده می‌شود. این مورد برای سناریوهای ماژولار و کامپوننت محور بیشتر می‌تواند مدنظر باشد. 
گام دوم: پیاده‌سازی اعتبارسنج مستقل
در صورت نیاز به اعتبارسنجی پیچیده برای مدل متناظر، می‌توانید با استفاده از دو روش زیر به این هدف برسید:
1- استفاده از کتابخانه DNTFrameworkCore.FluentValidation
public class BlogValidator : FluentModelValidator<BlogModel>
{
    public BlogValidator(IMessageLocalizer localizer)
    {
        RuleFor(b => b.Title).NotEmpty()
            .WithMessage(localizer["Blog.Fields.Title.Required"]);
    }
}
2- پیاده‌سازی IModelValidator یا ارث‌بری از کلاس ModelValidator پایه
public class BlogValidator : ModelValidator<BlogkModel>
{
    public override IEnumerable<ModelValidationResult> Validate(BlogModel model)
    {
        yield return new ModelValidationResult(nameof(BlogkModel.Title), "Validation from IModelValidator");
    }
}

گام سوم: پیاده‌سازی سرویس متناظر
public interface IBlogService : ICrudService<int, BlogModel>
{
}
پیاده سازی واسط بالا
public class BlogService : CrudService<Blog, int, BlogModel>, IBlogService
{
    public BlogService(CrudServiceDependency dependency) : base(dependency)
    {
    }

    protected override IQueryable<BlogModel> BuildReadQuery(FilteredPagedQueryModel model)
    {
        return EntitySet.AsNoTracking().Select(b => new BlogModel
            {Id = b.Id, RowVersion = b.RowVersion, Url = b.Url, Title = b.Title});
    }

    protected override Blog MapToEntity(BlogModel model)
    {
        return new Blog
        {
            Id = model.Id,
            RowVersion = model.RowVersion,
            Url = model.Url,
            Title = model.Title,
            NormalizedTitle = model.Title.ToUpperInvariant() //todo: normalize based on your requirement 
        };
    }

    protected override BlogModel MapToModel(Blog entity)
    {
        return new BlogModel
        {
            Id = entity.Id,
            RowVersion = entity.RowVersion,
            Url = entity.Url,
            Title = entity.Title
        };
    }
}
برای این چنین موجودیت‌هایی، بازنویسی همین 3 متد کفایت می‌کند؛ دو متد MapToModel و MapToEntity برای نگاشت مابین مدل و موجودیت مورد نظر و متد BuildReadQuery نیز برای تعیین نحوه ساخت کوئری ReadPagedListAsync پیش‌فرض موجود در CrudService به عنوان متد Read پیش‌فرض این موجودیت. باکمترین مقدار کدنویسی و با کیفیت قابل قبول، عملیات CRUD یک موجودیت ساده، تکمیل شد. 
مثال دوم: پیاده سازی سرویس یک موجودیت ساده با ReadModel و  FilteredPagedQueryModel متمایز
گام اول: طراحی Model متناظر
[LocalizationResource(Name = "SharedResource", Location = "DNTFrameworkCore.TestAPI")]
public class TaskModel : MasterModel<int>, IValidatableObject
{
    public string Title { get; set; }

    [MaxLength(50, ErrorMessage = "Validation from DataAnnotations")]
    public string Number { get; set; }

    public string Description { get; set; }
    public TaskState State { get; set; } = TaskState.Todo;

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (Title == "IValidatableObject")
        {
            yield return new ValidationResult("Validation from IValidatableObject");
        }
    }
}
public class TaskReadModel : MasterModel<int>
{
    public string Title { get; set; }
    public string Number { get; set; }
    public TaskState State { get; set; } = TaskState.Todo;
    public DateTimeOffset CreationDateTime { get; set; }
    public string CreatorUserDisplayName { get; set; }
}
به عنوان مثال خصوصیاتی برای نمایش داریم که در زمان ثبت و ویرایش، انتظار دریافت آنها را از کاربر نیز نداریم. 
گام دوم: پیاده‌سازی اعتبارسنج  مستقل 
public class TaskValidator : ModelValidator<TaskModel>
{
    public override IEnumerable<ModelValidationResult> Validate(TaskModel model)
    {
        if (!Enum.IsDefined(typeof(TaskState), model.State))
        {
            yield return new ModelValidationResult(nameof(TaskModel.State), "Validation from IModelValidator");
        }
    }
}
 گام سوم: پیاده‌سازی سرویس متناظر
public interface ITaskService : ICrudService<int, TaskReadModel, TaskModel, TaskFilteredPagedQueryModel>
{
}
همانطور که ملاحظه می‌کنید، از ICrudService استفاده شده است که امکان تعیین نوع پارامتر جنریک TReadModel و TFilteredPagedQueryModel را هم دارد.
مدل جستجو و صفحه‌بندی سفارشی 
public class TaskFilteredPagedQueryModel : FilteredPagedQueryModel
{
    public TaskState? State { get; set; }
}


پیاده سازی واسط ITaskService با استفاده از AutoMapper

public class TaskService : CrudService<Task, int, TaskReadModel, TaskModel, TaskFilteredPagedQueryModel>,
  ITaskService
{
    private readonly IMapper _mapper;

    public TaskService(CrudServiceDependency dependency, IMapper mapper) : base(dependency)
    {
        _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
    }

    protected override IQueryable<TaskReadModel> BuildReadQuery(TaskFilteredPagedQueryModel model)
    {
        return EntitySet.AsNoTracking()
                    .WhereIf(model.State.HasValue, t => t.State == model.State)
                    .ProjectTo<TaskReadModel>(_mapper.ConfigurationProvider);
    }

    protected override Task MapToEntity(TaskModel model)
    {
        return _mapper.Map<Task>(model);
    }

    protected override TaskModel MapToModel(Task entity)
    {
        return _mapper.Map<TaskModel>(entity);
    }
}

به عنوان مثال در کلاس بالا برای نگاشت مابین مدل و موجودیت، از واسط IMapper کتابخانه AutoMapper استفاده شده‌است و همچنین عملیات جستجوی سفارشی در همان متد BuildReadQuery برای تولید کوئری متد Read پیش‌فرض، قابل ملاحظه می‌باشد.

مثال سوم: پیاده‌سازی سرویس یک موجودیت اصلی به همراه تعدادی موجودیت فرعی وابسته (سناریوهای Master-Detail) 

گام اول: طراحی Modelهای متناظر

    public class UserModel : MasterModel
    {
        public string UserName { get; set; }
        public string DisplayName { get; set; }
        public string Password { get; set; }
        public bool IsActive { get; set; }
        public ICollection<UserRoleModel> Roles { get; set; } = new HashSet<UserRoleModel>();
        public ICollection<PermissionModel> Permissions { get; set; } = new HashSet<PermissionModel>();
        public ICollection<PermissionModel> IgnoredPermissions { get; set; } = new HashSet<PermissionModel>();
    }

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

public class PermissionModel : DetailModel<int>
{
    public string Name { get; set; }
}

به عنوان مثال PermissionModel بالا از DetailModel جنریک‌ای ارث‌بری کرده است که دارای Id و TrackingState نیز می‌باشد. 

public class UserRoleModel : DetailModel<int>
{
    public long RoleId { get; set; }
}

شاید در نگاه اول برای گروه‌های کاربری یک کاربر کافی بود تا یک لیست ساده از long را از کلاینت دریافت کنیم. در این صورت نیاز است تا برای تمام موجودیت‎های سیستم که چنین شرایط مشابهی را دارند، عملیات ثبت، ویرایش و حذف متناظر با تک تک Detailها را دستی مدیریت کنید. روش فعلی خصوصا برای سناریوهای منفصل به مانند پروژه‌های تحت وب، پیشنهاد می‌شود.

گام دوم: پیاده سازی اعتبارسنج مستقل

public class UserValidator : FluentModelValidator<UserModel>
{
    private readonly IUnitOfWork _uow;

    public UserValidator(IUnitOfWork uow, IMessageLocalizer localizer)
    {
        _uow = uow ?? throw new ArgumentNullException(nameof(uow));

        RuleFor(m => m.DisplayName).NotEmpty()
            .WithMessage(localizer["User.Fields.DisplayName.Required"])
            .MinimumLength(3)
            .WithMessage(localizer["User.Fields.DisplayName.MinimumLength"])
            .MaximumLength(User.MaxDisplayNameLength)
            .WithMessage(localizer["User.Fields.DisplayName.MaximumLength"])
            .Matches(@"^[\u0600-\u06FF,\u0590-\u05FF,0-9\s]*$")
            .WithMessage(localizer["User.Fields.DisplayName.RegularExpression"])
            .DependentRules(() =>
            {
                RuleFor(m => m).Must(model =>
                     !CheckDuplicateDisplayName(model.DisplayName, model.Id))
                    .WithMessage(localizer["User.Fields.DisplayName.Unique"])
                    .OverridePropertyName(nameof(UserModel.DisplayName));
            });

        RuleFor(m => m.UserName).NotEmpty()
            .WithMessage(localizer["User.Fields.UserName.Required"])
            .MinimumLength(3)
            .WithMessage(localizer["User.Fields.UserName.MinimumLength"])
            .MaximumLength(User.MaxUserNameLength)
            .WithMessage(localizer["User.Fields.UserName.MaximumLength"])
            .Matches("^[a-zA-Z0-9_]*$")
            .WithMessage(localizer["User.Fields.UserName.RegularExpression"])
            .DependentRules(() =>
            {
                RuleFor(m => m).Must(model =>
                     !CheckDuplicateUserName(model.UserName, model.Id))
                    .WithMessage(localizer["User.Fields.UserName.Unique"])
                    .OverridePropertyName(nameof(UserModel.UserName));
            });

        RuleFor(m => m.Password).NotEmpty()
            .WithMessage(localizer["User.Fields.Password.Required"])
            .When(m => m.IsNew, ApplyConditionTo.CurrentValidator)
            .MinimumLength(6)
            .WithMessage(localizer["User.Fields.Password.MinimumLength"])
            .MaximumLength(User.MaxPasswordLength)
            .WithMessage(localizer["User.Fields.Password.MaximumLength"]);

        RuleFor(m => m).Must(model => !CheckDuplicateRoles(model))
            .WithMessage(localizer["User.Fields.Roles.Unique"])
            .When(m => m.Roles != null && m.Roles.Any(r => !r.IsDeleted));
    }

    private bool CheckDuplicateUserName(string userName, long id)
    {
        var normalizedUserName = userName.ToUpperInvariant();
        return _uow.Set<User>().Any(u => u.NormalizedUserName == normalizedUserName && u.Id != id);
    }

    private bool CheckDuplicateDisplayName(string displayName, long id)
    {
        var normalizedDisplayName = displayName.NormalizePersianTitle();
        return _uow.Set<User>().Any(u => u.NormalizedDisplayName == normalizedDisplayName && u.Id != id);
    }

    private bool CheckDuplicateRoles(UserModel model)
    {
        var roles = model.Roles.Where(a => !a.IsDeleted);
        return roles.GroupBy(r => r.RoleId).Any(r => r.Count() > 1);
    }
}

به عنوان مثال در این اعتبارسنج بالا، قواعدی از جمله بررسی تکراری بودن نام‌کاربری و از این دست اعتبارسنجی‌ها نیز انجام شده است. نکته حائز اهمیت آن متد CheckDuplicateRoles می‌باشد:

private bool CheckDuplicateRoles(UserModel model)
{
    var roles = model.Roles.Where(a => !a.IsDeleted);
    return roles.GroupBy(r => r.RoleId).Any(r => r.Count() > 1);
}

با توجه به «نکته مهم» ابتدای بحث، model.Roles، شامل تمام گروه‌های کاربری متصل شده به کاربر می‌باشند که در این لیست برخی از آنها با TrackingState.Deleted، برخی دیگر با TrackingState.Added و ... علامت‌گذاری شده‌اند. لذا برای بررسی یکتایی و عدم تکرار در این سناریوها نیاز به اجری پرس‌و‌جویی بر روی دیتابیس نمی‌باشد. بدین منظور، با اعمال یک شرط، گروه‌های حذف شده را از بررسی خارج کرده‌ایم؛ چرا که آنها بعد از عبور از منطق تجاری، حذف خواهند شد. 


گام سوم: پیاده‌سازی سرویس متناظر

public interface IUserService : ICrudService<long, UserReadModel, UserModel>
{
}
public class UserService : CrudService<User, long, UserReadModel, UserModel>, IUserService
{
    private readonly IUserManager _manager;

    public UserService(CrudServiceDependency dependency, IUserManager manager) : base(dependency)
    {
        _manager = manager ?? throw new ArgumentNullException(nameof(manager));
    }

    protected override IQueryable<User> BuildFindQuery()
    {
        return base.BuildFindQuery()
            .Include(u => u.Roles)
            .Include(u => u.Permissions);
    }

    protected override IQueryable<UserReadModel> BuildReadQuery(FilteredPagedQueryModel model)
    {
        return EntitySet.AsNoTracking().Select(u => new UserReadModel
        {
            Id = u.Id,
            RowVersion = u.RowVersion,
            IsActive = u.IsActive,
            UserName = u.UserName,
            DisplayName = u.DisplayName,
            LastLoggedInDateTime = u.LastLoggedInDateTime
        });
    }

    protected override User MapToEntity(UserModel model)
    {
        return new User
        {
            Id = model.Id,
            RowVersion = model.RowVersion,
            IsActive = model.IsActive,
            DisplayName = model.DisplayName,
            UserName = model.UserName,
            NormalizedUserName = model.UserName.ToUpperInvariant(),
            NormalizedDisplayName = model.DisplayName.NormalizePersianTitle(),
            Roles = model.Roles.Select(r => new UserRole
                {Id = r.Id, RoleId = r.RoleId, TrackingState = r.TrackingState}).ToList(),
            Permissions = model.Permissions.Select(p => new UserPermission
            {
                Id = p.Id,
                TrackingState = p.TrackingState,
                IsGranted = true,
                Name = p.Name
            }).Union(model.IgnoredPermissions.Select(p => new UserPermission
            {
                Id = p.Id,
                TrackingState = p.TrackingState,
                IsGranted = false,
                Name = p.Name
            })).ToList()
        };
    }

    protected override UserModel MapToModel(User entity)
    {
        return new UserModel
        {
            Id = entity.Id,
            RowVersion = entity.RowVersion,
            IsActive = entity.IsActive,
            DisplayName = entity.DisplayName,
            UserName = entity.UserName,
            Roles = entity.Roles.Select(r => new UserRoleModel
                {Id = r.Id, RoleId = r.RoleId, TrackingState = r.TrackingState}).ToList(),
            Permissions = entity.Permissions.Where(p => p.IsGranted).Select(p => new PermissionModel
            {
                Id = p.Id,
                TrackingState = p.TrackingState,
                Name = p.Name
            }).ToList(),
            IgnoredPermissions = entity.Permissions.Where(p => !p.IsGranted).Select(p => new PermissionModel
            {
                Id = p.Id,
                TrackingState = p.TrackingState,
                Name = p.Name
            }).ToList()
        };
    }

    protected override Task BeforeSaveAsync(IReadOnlyList<User> entities, List<UserModel> models)
    {
        ApplyPasswordHash(entities, models);
        ApplySerialNumber(entities, models);
        return base.BeforeSaveAsync(entities, models);
    }

    private void ApplySerialNumber(IEnumerable<User> entities, IReadOnlyList<UserModel> models)
    {
        var i = 0;
        foreach (var entity in entities)
        {
            var model = models[i++];

            if (model.IsNew || !model.IsActive || !model.Password.IsEmpty() ||
                model.Roles.Any(a => a.IsNew || a.IsDeleted) ||
                model.IgnoredPermissions.Any(p => p.IsDeleted || p.IsNew) ||
                model.Permissions.Any(p => p.IsDeleted || p.IsNew))
            {
                entity.SerialNumber = _manager.NewSerialNumber();
            }
            else
            {
                //prevent include SerialNumber in update query
                UnitOfWork.Entry(entity).Property(a => a.SerialNumber).IsModified = false;
            }
        }
    }

    private void ApplyPasswordHash(IEnumerable<User> entities, IReadOnlyList<UserModel> models)
    {
        var i = 0;
        foreach (var entity in entities)
        {
            var model = models[i++];
            if (model.IsNew || !model.Password.IsEmpty())
            {
                entity.PasswordHash = _manager.HashPassword(model.Password);
            }
            else
            {
                //prevent include PasswordHash in update query
                UnitOfWork.Entry(entity).Property(a => a.PasswordHash).IsModified = false;
            }
        }
    }
}

در سناریوهای Master-Detail نیاز است متد دیگری تحت عنوان BuildFindQuery را نیز بازنویسی کنید. این متد برای بقیه حالات نیاز به بازنویسی نداشت؛ چرا که یک تک موجودیت واکشی می‌شد و خبری از موجودیت‌های Detail نبود. در اینجا لازم است تا روش تولید کوئری FindAsyn رو بازنویسی کنیم تا جزئیات دیگری را نیز واکشی کنیم. به عنوان مثال در اینجا Roles و Permissions کاربر نیز Include شده‌اند.

نکته: بازنویسی BuildFindQuery را شاید بتوان با روش‌های دیگری هم مانند تزئین موجودیت‌های وابسته با یک DetailOfAttribute و مشخص کردن نوع موجودیت اصلی، نیز جایگزین کرد.

متدهای MapToModel و MapToEntity هم به مانند قبل پیاده‌سازی شده‌اند. موضوع دیگری که در برخی از سناریوها پیش خواهد آمد، مربوط است به خصوصیتی که در زمان ثبت ضروری می‌باشد، ولی در زمان ویرایش اگر مقدار داشت باید با اطلاعات موجود در دیتابیس جایگزین شود؛ مانند Password و SerialNumber در موجودیت کاربر. برای این حالت می‌توان از متد BeforeSaveAsync بهره برد؛ به عنوان مثال برای SerialNumber:

private void ApplySerialNumber(IEnumerable<User> entities, IReadOnlyList<UserModel> models)
{
    var i = 0;
    foreach (var entity in entities)
    {
        var model = models[i++];

        if (model.IsNew || !model.IsActive || !model.Password.IsEmpty() ||
            model.Roles.Any(a => a.IsNew || a.IsDeleted) ||
            model.IgnoredPermissions.Any(p => p.IsDeleted || p.IsNew) ||
            model.Permissions.Any(p => p.IsDeleted || p.IsNew))
        {
            entity.SerialNumber = _manager.NewSerialNumber();
        }
        else
        {
            //prevent include SerialNumber in update query
            UnitOfWork.Entry(entity).Property(a => a.SerialNumber).IsModified = false;
        }
    }
}

در اینجا ابتدا بررسی شده‌است که اگر کاربر، جدید می‌باشد، غیرفعال شده است، کلمه عبور او تغییر داده شده است و یا تغییراتی در دسترسی‌ها و گروه‌های کاربری او وجود دارد، یک SerialNumber جدید ایجاد کند. در غیر این صورت با توجه به اینکه برای عملیات ویرایش، به صورت منفصل عمل می‌کنیم، نیاز است تا به شکل بالا، از قید این فیلد در کوئری ویرایش، جلوگیری کنیم. 

نکته: متد BeforeSaveAsync دقیقا بعد از ردیابی شدن وهله‌های موجودیت توسط Context برنامه و دقیقا قبل از UnitOfWork.SaveChange فراخوانی خواهد شد.


برای بررسی بیشتر، پیشنهاد می‌کنم پروژه DNTFrameworkCore.TestAPI موجود در مخزن این زیرساخت را بازبینی کنید.
مطالب
EF Code First #11

استفاده از الگوی Repository اضافی در EF Code first؛‌ آری یا خیر؟!

اگر در ویژوال استودیو، اشاره‌گر ماوس را بر روی تعریف DbContext قرار دهیم، راهنمای زیر ظاهر می‌شود:

A DbContext instance represents a combination of the Unit Of Work and Repository patterns such that 
it can be used to query from a database and group together changes that will then be written back to
the store as a unit. DbContext is conceptually similar to ObjectContext.

در اینجا تیم EF صراحتا عنوان می‌کند که DbContext در EF Code first همان الگوی Unit Of Work را پیاده سازی کرده و در داخل کلاس‌ مشتق شده از آن، DbSet‌ها همان Repositories هستند (فقط نام‌ها تغییر کرده‌اند؛ اصول یکی است).
به عبارت دیگر با نام بردن صریح از این الگوها، مقصود زیر را دنبال می‌کنند:
لطفا بر روی این لایه Abstraction ایی که ما تهیه دیده‌ایم، یک لایه Abstraction دیگر را ایجاد نکنید!
«لایه Abstraction دیگر» یعنی پیاده سازی الگوهای Unit Of Work و Repository جدید، برفراز الگوهای Unit Of Work و Repository توکار موجود!
کار اضافه‌ای که در بسیاری از سایت‌ها مشاهده می‌شود و ... متاسفانه اکثر آن‌ها هم اشتباه هستند! در ذیل روش‌های تشخیص پیاده سازی‌های نادرست الگوی Repository را بر خواهیم شمرد:
1) قرار دادن متد Save تغییرات نهایی انجام شده، در داخل کلاس Repository
متد Save باید داخل کلاس Unit of work تعریف شود نه داخل کلاس Repository. دقیقا همان کاری که در EF Code first به درستی انجام شده. متد SaveChanges توسط DbContext ارائه می‌شود. علت هم این است که در زمان Save ممکن است با چندین Entity و چندین جدول مشغول به کار باشیم. حاصل یک تراکنش، باید نهایتا ذخیره شود نه اینکه هر کدام از این‌ها، تراکنش خاص خودشان را داشته باشند.
2) نداشتن درکی از الگوی Unit of work
به Unit of work به شکل یک تراکنش نگاه کنید. در داخل آن با انواع و اقسام موجودیت‌ها از کلاس‌ها و جداول مختلف کار شده و حاصل عملیات، به بانک اطلاعاتی اعمال می‌گردد. پیاده سازی‌های اشتباه الگوی Repository، تمام امکانات را در داخل همان کلاس Repository قرار می‌دهند؛ که اشتباه است. این نوع کلاس‌ها فقط برای کار با یک Entity بهینه شده‌اند؛ در حالیکه در دنیای واقعی، اطلاعات ممکن است از دو Entity مختلف دریافت و نتیجه محاسبات مفروضی به Entity سوم اعمال شود. تمام این عملیات یک تراکنش را تشکیل می‌دهد، نه اینکه هر کدام، تراکنش مجزای خود را داشته باشند.
3) وهله سازی از DbContext به صورت مستقیم داخل کلاس Repository
4) Dispose اشیاء DbContext داخل کلاس Repository
هر بار وهله سازی DbContext مساوی است با باز شدن یک اتصال به بانک اطلاعاتی و همچنین از آنجائیکه راهنمای ذکر شده فوق را در مورد DbContext مطالعه نکرده‌اند، زمانیکه در یک متد با سه وهله از سه Repository موجودیت‌های مختلف کار می‌کنید، سه تراکنش و سه اتصال مختلف به بانک اطلاعاتی گشوده شده است. این مورد ذاتا اشتباه است و سربار بالایی را نیز به همراه دارد.
ضمن اینکه بستن DbContext در یک Repository، امکان اعمال کوئری‌های بعدی LINQ را غیرممکن می‌کند. به ظاهر یک شیء IQueryable در اختیار داریم که می‌توان بر روی آن انواع و اقسام کوئری‌های LINQ را تعریف کرد اما ... در اینجا با LINQ to Objects که بر روی اطلاعات موجود در حافظه کار می‌کند سر و کار نداریم. اتصال به بانک اطلاعاتی با بستن DbContext قطع شده، بنابراین کوئری LINQ بعدی شما کار نخواهد کرد.
همچنین در EF نمی‌توان یک Entity را از یک Context به Context‌ دیگری ارسال کرد. در پیاده سازی صحیح الگوی Repository (دقیقا همان چیزی که در EF Code first به صورت توکار وجود دارد)، Context باید بین Repositories که در اینجا فقط نامش DbSet تعریف شده، به اشتراک گذاشته شود. علت هم این است که EF از Context برای ردیابی تغییرات انجام شده بر روی موجودیت‌ها استفاده می‌کند (همان سطح اول کش که در قسمت‌های قبل به آن اشاره شد). اگر به ازای هر Repository یکبار وهله سازی DbContext انجام شود، هر کدام کش جداگانه خاص خود را خواهند داشت.
5) عدم امکان استفاده از تنها یک DbConetext به ازای یک Http Request
هنگامیکه وهله سازی DbContext به داخل یک Repository منتقل می‌شود و الگوی واحد کار رعایت نمی‌گردد، امکان به اشتراک گذاری آن بین Repositoryهای تعریف شده وجود نخواهد داشت. این مساله در برنامه‌های وب سبب کاهش کارآیی می‌گردد (باز و بسته شدن بیش از حد اتصال به بانک اطلاعاتی در حالیکه می‌شد تمام این عملیات را با یک DbContext انجام داد).

نمونه‌ای از این پیاده سازی اشتباه را در اینجا می‌توانید پیدا کنید. متاسفانه شبیه به همین پیاده سازی، در پروژه MVC Scaffolding نیز بکارگرفته شده است.


چرا تعریف لایه دیگری بر روی لایه Abstraction موجود در EF Code first اشتباه است؟

یکی از دلایلی که حین تعریف الگوی Repository دوم بر روی لایه موجود عنوان می‌شود، این است:
«به این ترتیب به سادگی می‌توان ORM مورد استفاده را تغییر داد» چون پیاده سازی استفاده از ORM، در پشت این لایه مخفی شده و ما هر زمان که بخواهیم به ORM دیگری کوچ کنیم، فقط کافی است این لایه را تغییر دهیم و نه کل برنامه‌ را.
ولی سؤال این است که هرچند این مساله از هزار فرسنگ بالاتر درست است، اما واقعا تابحال دیده‌اید که پروژه‌ای را با یک ORM شروع کنند و بعد سوئیچ کنند به ORM دیگری؟!
ضمنا برای اینکه واقعا لایه اضافی پیاده سازی شده انتقال پذیر باشد، شما باید کاملا دست و پای ORM موجود را بریده و توانایی‌های در دسترس آن را به سطح نازلی کاهش دهید تا پیاده سازی شما قابل انتقال باشد. برای مثال یک سری از قابلیت‌های پیشرفته و بسیار جالب در NH هست که در EF نیست و برعکس. آیا واقعا می‌توان به همین سادگی ORM مورد استفاده را تغییر داد؟ فقط در یک حالت این امر میسر است: از قابلیت‌های پیشرفته ابزار موجود استفاده نکنیم و از آن در سطحی بسیار ساده و ابتدایی کمک بگیریم تا از قابلیت‌های مشترک بین ORMهای موجود استفاده شود.
ضمن اینکه مباحث نگاشت کلاس‌ها به جداول را چکار خواهید کرد؟ EF راه و روش خاص خودش را دارد، NH چندین و چند روش خاص خودش را دارد! این‌ها به این سادگی قابل انتقال نیستند که شخصی عنوان کند: «هر زمان که علاقمند بودیم، ORM مورد استفاده را می‌شود عوض کرد!»

دلیل دومی که برای تهیه لایه اضافه‌تری بر روی DbContext عنوان می‌کنند این است:
«با استفاده از الگوی Repository نوشتن آزمون‌های واحد ساده‌تر می‌شود». زمانیکه برنامه بر اساس Interfaceها کار می‌کند می‌توان آن‌ها را بجای اشاره به بانک اطلاعاتی، به نمونه‌ای موجود در حافظه، در زمان آزمون تغییر داد.
این مورد در حالت کلی درست است اما .... نه در مورد بانک‌های اطلاعاتی!
زمانیکه در یک آزمون واحد، پیاده سازی جدیدی از الگوی Interface مخزن ما تهیه می‌شود و اینبار بجای بانک اطلاعاتی با یک سری شیء قرارگرفته در حافظه سروکار داریم، آیا موارد زیر را هم می‌توان به سادگی آزمایش کرد؟
ارتباطات بین جداول‌را، cascade delete، فیلدهای identity، فیلدهای unique، کلیدهای ترکیبی، نوع‌های خاص تعریف شده در بانک اطلاعاتی و مسایلی از این دست.
پاسخ: خیر! تغییر انجام شده، سبب کار برنامه با اطلاعات موجود در حافظه خواهد شد، یعنی LINQ to Objects.
شما در حالت استفاده از LINQ to Objects آزادی عمل فوق العاده‌ای دارید. می‌توانید از انواع و اقسام متدها حین تهیه کوئری‌های LINQ استفاده کنید که هیچکدام معادلی در بانک اطلاعاتی نداشته و ... به ظاهر آزمون واحد شما پاس می‌شود؛ اما در عمل بر روی یک بانک اطلاعاتی واقعی کار نخواهد کرد.
البته شاید شخصی عنوان که بله می‌شود تمام این‌ها نیازمندی‌ها را در حالت کار با اشیاء درون حافظه هم پیاده سازی کرد ولی ... در نهایت پیاده سازی آن بسیار پیچیده و در حد پیاده سازی یک بانک اطلاعاتی واقعی خواهد شد که واقعا ضرورتی ندارد.

و پاسخ صحیح در اینجا و این مساله خاص این است:
لطفا در حین کار با بانک‌های اطلاعاتی مباحث mocking را فراموش کنید. بجای SQL Server، رشته اتصالی و تنظیمات برنامه را به SQL Server CE تغییر داده و آزمایشات خود را انجام دهید. پس از پایان کار هم بانک اطلاعاتی را delete کنید. به این نوع آزمون‌ها اصطلاحا integration tests گفته می‌شود. لازم است برنامه با یک بانک اطلاعاتی واقعی تست شود و نه یک سری شیء ساده قرار گرفته در حافظه که هیچ قیدی همانند شرایط کار با یک بانک اطلاعاتی واقعی، بر روی آ‌ن‌ها اعمال نمی‌شود.
ضمنا باید درنظر داشت بانک‌های اطلاعاتی که تنها در حافظه کار کنند نیز وجود دارند. برای مثال SQLite حالت کار کردن صرفا در حافظه را پشتیبانی می‌کند. زمانیکه آزمون واحد شروع می‌شود، یک بانک اطلاعاتی واقعی را در حافظه تشکیل داده و پس از پایان کار هم ... اثری از این بانک اطلاعاتی باقی نخواهد ماند و برای این نوع کارها بسیار سریع است.


نتیجه گیری:
حین استفاده از EF code first، الگوی واحد کار، همان DbContext است و الگوی مخزن، همان DbSetها. ضرورتی به ایجاد یک لایه محافظ اضافی بر روی این‌ها وجود ندارد.
در اینجا بهتر است یک لایه اضافی را به نام مثلا Service ایجاد کرد و تمام اعمال کار با EF را به آن منتقل نمود. سپس در قسمت‌های مختلف برنامه می‌توان از متدهای این لایه استفاده کرد. به عبارتی در فایل‌های Code behind برنامه شما نباید کدهای EF مشاهده شوند. یا در کنترلرهای MVC نیز به همین ترتیب. این‌ها مصرف کننده نهایی لایه سرویس ایجاد شده خواهند بود.
همچنین بجای نوشتن آزمون‌های واحد، به Integration tests سوئیچ کنید تا بتوان برنامه را در شرایط کار با یک بانک اطلاعاتی واقعی تست کرد.


برای مطالعه بیشتر:
نظرات مطالب
Repository ها روی UnitOfWork ایده خوبی نیستند
آقای پاکدل لطفا راجع به این جمله بیشتر توضیح بدید:
»بهتر است متد Save را نیز در Repository قرار ندهید. متد Save باید توسط UnitOfWork به اشتراک گذاشته شده فراخوانی شود. 
در پروژه http://nopcommerce.codeplex.com/SourceControl/latest  اینگونه عمل شده
http://nopcommerce.codeplex.com/SourceControl/latest#src/Libraries/Nop.Data/EfRepository.cs
در نهایت این لایه سرویس است که باید اطمینان از انجام عملیات درخواستی و یا عدم انجام آنرا بدهد و برای عملیات‌های پیچیده‌تر نیز بایستی سیاست خودرا بسط دهد.منظور انجام عملیات Save و ادامه عملیات میباشد.
لایه UI وظیفه فراهم آوری اطلاعات را دارد و مابقی مسائل در لایه سرویس پوشش داده میشوند.
الزام این کار هم به وظیفه این لایه برمیگردد که یا این کار را میتوانم انجام دهم و یا خیر.
پیاده سازی ارائه شده نقضی بر جمله‌ی نقل قول شده میباشد؟
مطالب
مدیریت دانلود‌های همزمان از یک سایت و بحث تایم آوت
یک سرویس ویندوز ان تی با سی شارپ نوشته‌ام که کارش مراجعه به یک سری آدرس RSS و ذخیره سازی آنها به صورت آنالیز شده در یک دیتابیس SQL server است (این مورد ضعفی است که اکثر برنامه‌های فیدخوان دارند و پس از مدتی کار با آنها این احساس را دارید که اطلاعات گذشته را از دست داده‌اید).
در طی آزمایش اولیه این سرویس، به مشکل عجیب timeout پس از باز کردن برای مثال سومین یا چهارمین thread همزمان برای دانلود کردن اطلاعات بر خوردم. همه چیز درست بود، از کلاس‌ها، دریافت اطلاعات از وب و غیره، اما برنامه کار نمی‌کرد. این مشکل فقط هم با feedburner.com رخ می‌داد (همانطور که مطلع هستید feedburner.com سرویسی را جهت پیگیری آمار مشترکین فیدهای شما ارائه می‌دهد که بسیار جالب است. برای مثال چند نفر مشترک دارید، یا یک سری نمودار و غیره. به همین جهت رسم شده است که اکثر سایت‌ها فیدهای خودشان را در این سایت نیز ثبت می‌کنند).
پس از مدتی جستجو به نکته جالب زیر برخوردم که شاید برای شما هم در آینده مفید باشد:
مطابق RFC2068 - Hypertext Transfer Protocol -- HTTP/1.1 ، شما تنها مجازید 2 کانکشن فعال به یک سایت باز کنید. این علت تایم آوت در سومین thread ایجاد شده بود. برای مثال IE این مورد را محترم می‌شمارد. در دات نت نیز به صورت پیش فرض این محدودیت قرار داده شده است که به‌سادگی می‌توان آنرا تغییر داد. برای این منظور باید یک فایل app.config به پروژه اضافه کرد و سپس خطوط زیر را به آن افزود:

<configuration>
<system.net>
<connectionManagement>
<add address="*" maxconnection="100" />
</connectionManagement>
</system.net>
</configuration>


بعد از این تغییر مشکل timeout برنامه حل شد.

برای مدیریت چندین ترد همزمان دانلود کننده و در صف قرار دادن آنها در این پروژه، از کتابخانه سورس باز زیر استفاده کردم:
http://www.codeplex.com/smartthreadpool

مآخذ:
http://msdn.microsoft.com/en-us/library/fb6y0fyc.aspx
http://www.faqs.org/rfcs/rfc2068.html
http://vahidnasiri.blogspot.com
http://odetocode.com/Blogs/scott/archive/2004/06/08/272.aspx

پ.ن.
برای اینکه در بلاگر بتوانید متون حاوی xml را ارسال کنید باید از سرویس زیر استفاده کنید
http://www.elliotswan.com/postable/