مطالب دوره‌ها
کوئری نویسی مقدماتی در RavenDB
با شروع کوئری نویسی مقدماتی در RavenDB، در قسمت اول این مباحث، توسط فراخوانی متد Load یک سشن، آشنا شدید. در ادامه مباحث تکمیلی آن‌را مرور خواهیم کرد.

امکان استفاده از LINQ در RavenDB

RavenDB از LINQ جهت کوئری نویسی پشتیبانی می‌کند. برای استفاده از آن، در ادامه مطلب اول، ابتدا سرور RavenDB را اجرا نموده و سپس برنامه کنسول را به نحو ذیل تغییر دهید:
using System;
using System.Linq;
using Raven.Client.Document;
using RavenDBSample01.Models;

namespace RavenDBSample01
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var store = new DocumentStore
            {
                Url = "http://localhost:8080"
            }.Initialize())
            {
                using (var session = store.OpenSession())
                {
                    var questions = session.Query<Question>().Where(x => x.Title.StartsWith("Raven"));
                    foreach (var question in questions)
                    {
                        Console.WriteLine(question.Title);
                    }
                }
            }
        }
    }
}
در RavenDB برای دسترسی به امکانات LINQ، کار با متد Query یک سشن آغاز می‌شود و پس از آن، امکان استفاده از متدهای متداول LINQ مانند مثال فوق وجود خواهد داشت. البته بدیهی است مباحثی مانند JOIN و امثال آن در یک بانک اطلاعاتی NoSQL پشتیبانی نمی‌شود. ضمنا باید درنظر داشت که مبحث safe by default در اینجا نیز اعمال می‌شود. برای مثال اگر به کنسول سرور RavenDB که در حال اجرا است مراجعه کنید، یک چنین خروجی را حین اجرای مثال فوق می‌توان مشاهده کرد که در آن pageSize پیش فرضی اعمال شده است:
Available commands: cls, reset, gc, q
Request #   1: GET     -   179 ms - <system>   - 404 - /docs/Raven/Replication/Destinations
Request #   2: GET     - 3,818 ms - <system>   - 200 - /indexes/dynamic/Questions?&query=Title%3ARaven*&pageSize=128
        Query: Title:Raven*
        Time: 3,494 ms
        Index: Auto/Questions/ByTitle
        Results: 2 returned out of 2 total.
یعنی در عمل کوئری‌را که اجرا کرده است، شبیه به کوئری ذیل می‌باشد و یک Take پیش فرض بر روی آن اعمال شده است:
var questions = session.Query<Question>().Where(x => x.Title.StartsWith("Raven")).Take(128);
علت این مساله نیز به تصمیم نویسنده اصلی آن بر می‌گردد؛ ایشان پیش از شروع به تهیه RavenDB، کار تهیه انواع و اقسام پروفایلرهای مهم ORMهای معروف مانند NHibernate و Entity framework را انجام داده است و در این حین، یکی از مهم‌ترین مشکلاتی را که با آن‌ها در کدهای متداول برنامه نویس‌ها یافته است، unbounded queries است. کوئری‌هایی که حد و مرزی برای بازگشت اطلاعات قائل نمی‌شوند. داشتن این نوع کوئری‌ها با تعداد بالای کاربر، یعنی مصرف بیش از حد RAM بر روی سرور، به همراه بار پردازشی بیش از حد و غیر ضروری. چون عملا حتی اگر 10 هزار رکورد بازگشت داده شوند، عموم برنامه نویس‌ها حداکثر 100 رکورد آن‌را در یک صفحه نمایش می‌دهند و نه تمام رکوردها را.


ارتباط Lucene.NET و RavenDB

کل LINQ API تهیه شده در RavenDB یک محصور کننده امکانات Lucene.NET است. اگر پیشتر با Lucene.NET کار کرده باشید، در خروجی حالت دیباگ کنسول سرور فوق، سطر «Query: Title:Raven*» آشنا به نظر خواهد رسید. دقیقا کوئری LINQ نوشته شده به یک کوئری با Syntax مخصوص Lucene.NET ترجمه شده‌است. برای نمونه اگر علاقمند باشید که مستقیما کوئری‌های خاص لوسین را در RavenDB اجرا کنید، از Syntax ذیل می‌توان استفاده کرد:
var questions = session.Advanced.LuceneQuery<Question>().Where("Title:Raven*").ToList();
و یا اگر علاقمند به حفظ کردن Syntax خاص لوسین نیستید، یک سری متد الحاقی خاص نیز در اینجا برای LuceneQuery تدارک دیده شده است. برای مثال کوئری رشته‌ای فوق، معادل کوئری strongly typed ذیل است:
var questions = session.Advanced.LuceneQuery<Question>().WhereStartsWith(x => x.Title, "Raven").ToList();

استفاده مجدد از کوئری‌ها در RavenDB

در RavenDB، متد Query به صورت immutable تعریف شده است و متد LuceneQuery حالت mutable دارد (ترکیبات آن نیز یک وهله است).
یک مثال:
var query = session.Query<User>().Where(x => x.Name.StartsWith("A"));
var ageQuery = query.Where(x => x.Age > 21);
var eyeQuery = query.Where(x => x.EyeColor == "blue");
در اینجا از کوئری ابتدایی، در دو کوئری مجزا استفاده مجدد شده است. ترجمه خروجی سه کوئری فوق به نحو زیر است:
query - Name:A*
ageQuery - (Name:A*) AND (Age_Range:{Ix21 TO NULL})
eyeQuery - (Name:A*) AND (EyeColor:blue)
به این معنا که زمانیکه به eyeQuery رسیدیم، نتیجه ageQuery با آن ترکیب نمی‌شود؛ چون متد Query از نوع immutable است.
در ادامه اگر همین سه کوئری فوق را با فرمت LuceneQuery تهیه کنیم، به عبارات ذیل خواهیم رسید:
var luceneQuery = session.Advanced.LuceneQuery<User>().WhereStartsWith(x => x.Name, "A");
var ageLuceneQuery = luceneQuery.WhereGreaterThan(x => x.Age, 21);
var eyeLuceneQuery = luceneQuery.WhereEquals(x => x.EyeColor, "blue");
در خروجی‌های این سه کوئری، مورد سوم مهم است:
luceneQuery - Name:A* 
ageLuceneQuery - Name:A* Age_Range:{Ix21 TO NULL}
eyeLuceneQuery - Name:A* Age_Range:{Ix21 TO NULL} EyeColor:blue
همانطور که مشاهده می‌کنید، کوئری سوم، عبارت کوئری دوم را نیز به همراه دارد؛ این مورد دقیقا مفهوم اشیاء mutable یا تک وهله‌ای است مانند LuceneQuery در اینجا.


And و Or شدن کوئری‌های ترکیبی در RavenDB
در مثال استفاده مجدد از کوئری‌ها، زمانیکه از Where استفاده شد، بین عبارات حاصل AND قرار گرفته است. این مورد را به نحو ذیل می‌توان تنظیم کرد و مثلا به OR تغییر داد:
session.Advanced.LuceneQuery<User>().UsingDefaultOperator(QueryOperator.And);

صفحه بندی اطلاعات در RavenDB

در ابتدای بحث عنوان شد که کوئری LINQ اجرا شده در RavenDB، یک Take مخفی و پیش فرض تنظیم شده به 128 آیتم را دارد. اکنون سؤال این خواهد بود که چگونه می‌توان اطلاعات را به صورت صفحه بندی شده، بر اساس شماره صفحه خاصی نمایش داد.
using System;
using System.Linq;
using Raven.Client.Document;
using RavenDBSample01.Models;

namespace RavenDBSample01
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var store = new DocumentStore
            {
                Url = "http://localhost:8080"
            }.Initialize())
            {
                using (var session = store.OpenSession())
                {
                    int pageNumber = 0;
                    int resultsPerPage = 2;

                    var questions = session.Query<Question>()
                                           .Where(x => x.Title.StartsWith("Raven"))
                                           .Skip(pageNumber * resultsPerPage)
                                           .Take(resultsPerPage);
                    foreach (var question in questions)
                    {
                        Console.WriteLine(question.Title);
                    }
                }
            }
        }
    }
}
برای انجام صفحه بندی در RavenDB، کافی است از متدهای Skip و Take بر اساس محاسباتی که مشاهده می‌کنید، استفاده گردد.


دریافت اطلاعات آماری کوئری اجرا شده

در RavenDB امکان دریافت یک سری اطلاعات آماری از کوئری اجرا شده نیز وجود دارد؛ برای مثال یک کوئری چند ثانیه طول کشیده است، چه تعدادی رکورد را بازگشت داده است و امثال آن. برای پیاده سازی آن، نیاز است از متد الحاقی Statistics به نحو ذیل استفاده کرد:
using System;
using System.Linq;
using Raven.Client.Document;
using RavenDBSample01.Models;
using Raven.Client;

namespace RavenDBSample01
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var store = new DocumentStore
            {
                Url = "http://localhost:8080"
            }.Initialize())
            {
                using (var session = store.OpenSession())
                {
                    int pageNumber = 0;
                    int resultsPerPage = 2;
                    RavenQueryStatistics stats;
                    var questions = session.Query<Question>()
                                           .Statistics(out stats)
                                           .Where(x => x.Title.StartsWith("Raven"))
                                           .Skip(pageNumber * resultsPerPage)
                                           .Take(resultsPerPage);
                    foreach (var question in questions)
                    {
                        Console.WriteLine(question.Title);
                    }

                    Console.WriteLine("TotalResults: {0}", stats.TotalResults);
                }
            }
        }
    }
}
متد الحاقی Statistics پس از متد Query که نقطه آغازین نوشتن کوئری‌های LINQ است، فراخوانی شده و یک پارامتر out از نوع RavenQueryStatistics تعریف شده در فضای نام Raven.Client را دریافت می‌کند. پس از پایان کوئری می‌توان از این خروجی جهت نمایش اطلاعات آماری کوئری استفاده کرد.


امکانات ویژه فضای نام Raven.Client.Linq

یک سری متد الحاقی خاص جهت تهیه ساده‌تر کوئری‌های LINQ در فضای نام Raven.Client.Linq قرار دارند که پس از تعریف آن قابل دسترسی خواهند بود:
var list = session.Query<Question>().Where(q => q.By.In<string>(arrayOfUsers))).ToArray()
برای مثال در اینجا متد الحاقی جدید In را مشاهده می‌کنید که شبیه به کوئری SQL ذیل در دنیای بانک‌های اطلاعاتی رابطه‌ای عمل می‌کند:
 SELECT * FROM tbl WHERE data IN (1, 2, 3)

اتصال به RavenDB با استفاده از برنامه معروف LINQPad

اگر علاقمند باشید که کوئری‌های خود را در محیط برنامه معروف LINQPad نیز آزمایش کنید، درایور مخصوص RavenDB آن‌را از آدرس ذیل می‌توانید دریافت نمائید:
مطالب
پیاده سازی یک سیستم دسترسی Role Based در Web API و AngularJs - بخش دوم
در بخش پیشین مروری اجمالی را بر روی یک سیستم مبتنی بر نقش کاربر داشتیم. در این بخش تصمیم داریم تا به جزئیات بیشتری در مورد سیستم دسترسی ارائه شده بپردازیم.
همانطور که گفتیم ما به دو صورت قادر هستیم تا دسترسی‌های (Permissions) یک سیستم را تعریف کنیم. روش اول این بود که هر متد از یک کنترلر، دقیقا به عنوان یک آیتم در جدول Permissions قرار گیرد و در نهایت برای تعیین نقش جدید، مدیر باید جزء به جزء برای هر نقش، دسترسی به هر متد را مشخص کند. در روش دوم مجموعه‌ای از API Methodها به یک دسترسی تبدیل شده است.
مراحل توسعه این روش به صورت زیر خواهند بود:
  1. توسعه پایگاه داده سیستم دسترسی مبتنی بر نقش
  2. توسعه یک Customized Filter Attribute بر پایه Authorize Attribute
  3. توسعه سرویس‌های مورد استفاده در Authorize Attribute
  4. توسعه کنترلر Permissions: تمامی APIهایی که در جهت همگام سازی دسترسی‌ها بین کلاینت و سرور را بر عهده دارند در این کنترلر توسعه داده میشود.
  5. توسعه سرویس مدیریت دسترسی در کلاینت توسط AngularJS

توسعه پایگاه داده

در این مرحله پایگاه داده را به صورت Code First پیاده سازی مینماییم. مدل Permissions به صورت زیر میباشد:
    public class Permission
    {
        [Key]
        public string Id { get; set; }
        public string Title { get; set; }
        public string Description { get; set; }
        public string Area { get; set; }
        public string Control { get; set; }
        public virtual ICollection<Role> Roles { get; set; }
    }
در مدل فوق همانطور که مشاهده میکنید یک ارتباط چند به چند، به Roles وجود دارد که در EF به صورت توکار یک جدول اضافی Junction اضافه خواهد شد با نام RolesPermissions. Area و Control نیز طبق تعریف شامل محدوده مورد نظر و کنترل‌های روی ناحیه در نظر گرفته می‌شوند. به عنوان مثال برای یک سایت فروشگاهی، برای بخش محصولات می‌توان حوزه‌ها و کنترل‌ها را به صورت زیر تعریف نمود:
 Control Area 
 view  products
 add  products
 edit  products
 delete  products

با توجه به جدول فوق همانطور که مشاهده می‌کنید تمامی آنچه که برای دسترسی Products مورد نیاز است در یک حوزه و 4 کنترل گنجانده میشود. البته توجه داشته باشید سناریویی که مطرح کردیم برای روشن سازی مفهوم ناحیه یا حوزه و کنترل بود. همانطور که میدانیم در AngularJS تمامی اطلاعات توسط APIها فراخوانی می‌گردند. از این رو یک موهبت دیگر این روش، خوانایی مفهوم حوزه و کنترل نسبت به نام کنترلر و متد است.

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

    public class Role
    {
        [Key]
        public string Id { get; set; }
        public string Title { get; set; }
        public string Description { get; set; }
        public virtual ICollection<Permission> Permissions { get; set; }
        public virtual ICollection<User> Users { get; set; }
    }

در مدل فوق می‌بینید که دو رابطه چند به چند وجود دارد. رابطه اول که همان Permissions است و در مدل پیشین تشریح شد. رابطه‌ی دوم رابطه چند به چند بین کاربر و نقش است. چند کاربر قادرند یک نقش در سیستم داشته باشند و همینطور چندین نقش میتواند به یک کاربر انتساب داده شود.

ما در این سیستم از ASP.NET Identity 2.1 استفاده و کلاس IdentityUser را override کرده‌ایم. در مدل override شده، برخی اطلاعات جدید کاربر، به جدول کاربر اضافه شده‌اند. این اطلاعات شامل نام، نام خانوادگی، شماره تماس و ... می‌باشد.

public class ApplicationUser : IdentityUser
    {
        [MaxLength(100)]
        public string FirstName { get; set; }
        [MaxLength(100)]
        public string LastName { get; set; }
        public bool IsSysAdmin { get; set; }
        public DateTime JoinDate { get; set; }

        public virtual ICollection<Role> Roles { get; set; }
    }

در نهایت تمامی این مدل‌ها به وسیله EF Code First پایگاه داده سیستم ما را تشکیل خواهند داد.

توسعه یک Customized Filter Attribute بر پایه Authorize Attribute 

اگر در مورد Custom Filter Attributeها اطلاعات ندارید نگران نباشید! مقاله «فیلترها در MVC» تمامی آنچه را که باید در اینباره بدانید، به شما خواهد گفت. همچنین در  مقاله وب سایت  مایکروسافت به صورت عملی (ایجاد یک سیستم Logger) همه چیز را برای شما روشن خواهد کرد. حال بپردازیم به Filter Attribute نوشته شده که قرار است وظیفه پیش پردازش تمامی درخواست‌های کاربر را انجام دهد. در ابتدا کمی در اینباره بگوییم که این فیلتر قرار است چه کاری را دقیقا انجام دهد!
این فیلتر قرار است پیش از پردازش هر API Method، درخواست کاربر را با استفاده از نقشی که او در سیستم دارد، بررسی نماید که آیا کاربر به API اجازه دسترسی دارد یا خیر. برای این کار باید ما در ابتدا نقش‌های کاربر را بررسی نماییم. پس از اینکه نقش‌های کاربر واکشی شدند، باید بررسی کنیم آیا نقشی که کاربر دارد، شامل این حوزه و کنترل بوده است یا خیر؟ Area و Control دو پارامتری هستند که در سیستم پیش از هر متد، Hard Code شده‌اند و در ادامه نمونه‌ای از آن را نمایش خواهیم داد.
    public class RBACAttribute : AuthorizeAttribute
    {
        public string Area { get; set; }
        public string Control { get; set; }
        AccessControlService _AccessControl = new AccessControlService();
        public override void OnAuthorization(HttpActionContext actionContext)
        {
            var userId = HttpContext.Current.User.Identity.GetCurrentUserId();
            // If User Ticket is Not Expired
            if (userId == null || !_AccessControl.HasPermission(userId, this.Area, this.Control))
            {
                actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
            }
        }
    }
در خط پنجم، سرویس AccessControl را فراخوانی کرده‌ایم که در ادامه به پیاده سازی آن نیز خواهیم پرداخت. متد HasPermission از این سرویس دو پارامتر id کاربر و Area و Control را دریافت میکند و با استفاده از این سه پارامتر بررسی میکند که آیا کاربر جاری به این بخش دسترسی دارد یا خیر؟ در صورت منقضی شدن ticket کاربر و یا عدم دسترسی، سرور Unauthorized status code را به کاربر باز می‌گرداند.
بلوک زیر استفاده از این فیلتر را نمایش می‌دهد:
[HttpPost]
[Route("ChangeProductStatus")]
[RBAC(Area = "products", Control = "edit")]
public async Task<HttpResponseMessage> ChangeProductStatus(StatusCodeBindingModel model)
{
// Method Body
}
همانطور که مشاهده می‌کنید کافیست RBAC را با دو پارامتر، پیش از متد نوشت. به صورت خودکار پیش از فراخوانی این متد که وظیفه تغییر وضعیت کالا را بر عهده دارد، فیلتر نوشته شده فراخوانی خواهد شد.
در بخش بعدی به بیان ادامه جریان و توسعه سرویس Access Control خواهیم پرداخت.
نظرات مطالب
مقدمه ای بر AutoMapper
با سلام میخواستم ببینم آیا امکان استفاده از automapper بدین صورت هم وجود داره؟
 public void Edit_news_ajax(News news)
        {
            using (var Context = new ProCamContext())
            {
                var q=Context.News.Find(news.id);
                AutoMapper.Mapper.Map(news,q);
               //NewsRepository.EditNews(news.id, q);
                Context.SaveChanges();
            }//end news
        }
با تشکر
بازخوردهای پروژه‌ها
عدم نمایش سطر آخر رکورد در pdf
سلام..مشکلی که دارم اینه که خروجی pdf من سطر آخررکودهای خروجی  رو توی فایل pdf بر نمیگردونه.چک کردم dt من مشکلی نداره..میخواستم راهنمایی کنید..

کد‌های من :
public IPdfReportData CreatePdfReport(DataTable dt,string OrganName,string DateFrom,string DateEnd,string FilePath)
        {
            FileStream fo = new FileStream(FilePath, FileMode.Create);
            fo.Close();
            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.RightToLeft);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "AjansDaneshjo", Application = "PdfRpt", Keywords = "Report", Subject = "Test Rpt", Title = "Report" });
            })
            .DefaultFonts(fonts =>
            {
                fonts.Path(Application.StartupPath+"\\BNAZANIN.ttf", Application.StartupPath + "\\BNAZNNBD.ttf");
                fonts.Size(13);
            })
            .PagesFooter(footer =>
            {
                footer.DefaultFooter(PersianDate.ToPersianDateTime(DateTime.Now, "/", false, false));
            })
            .PagesHeader(header =>
            {
                header.CustomHeader(new MasterDetailsHeaders { PdfRptFont = header.PdfFont ,DateFrom=DateFrom,DateTo= DateEnd,ProjTitle=ProjTitle });
            })
            .MainTableTemplate(template =>
            {
                template.BasicTemplate(BasicTemplate.SilverTemplate);
            })
            .MainTablePreferences(table =>
            {
                table.ColumnsWidthsType(TableColumnWidthType.Relative);
                table.NumberOfDataRowsPerPage(50);
                table.GroupsPreferences(new GroupsPreferences
                {
                    GroupType = GroupType.HideGroupingColumns,
                    RepeatHeaderRowPerGroup = true,
                    ShowOneGroupPerPage = true,
                    SpacingBeforeAllGroupsSummary = 5f
                });
            })
            .MainTableDataSource(dataSource =>
            {
                dataSource.DataTable(dt);
            })
            .MainTableSummarySettings(summarySettings =>
            {
                summarySettings.OverallSummarySettings(" جمع کل ");
                summarySettings.PageSummarySettings("جمع صفحه");
            })
            .MainTableColumns(columns =>
            {
                columns.AddColumn(column =>
                {
                    column.PropertyName("rowNo");
                    column.IsRowNumber(true);
                    column.CellsHorizontalAlignment(PdfRpt.Core.Contracts.HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(0);
                    column.Width(1);
                    column.HeaderCell("ردیف");
                });
                columns.AddColumn(column =>
                {
                    column.PropertyName("Name_Family");
                    column.CellsHorizontalAlignment(PdfRpt.Core.Contracts.HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(1);
                    column.Width(2);
                    column.HeaderCell("در اختیار");
                    column.ColumnItemsTemplate(template =>
                    {
                        template.TextBlock();
                        template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });

                });
                columns.AddColumn(column =>
                {
                    column.PropertyName("DateAjans");
                    column.CellsHorizontalAlignment(PdfRpt.Core.Contracts.HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(2);
                    column.Width(2);
                    column.HeaderCell("تاریخ");
                    column.ColumnItemsTemplate(template =>
                    {
                        template.TextBlock();
                        template.DisplayFormatFormula(obj => obj == null ? string.Empty : PersianDate.ToPersianDateTime((DateTime)obj,"/",false,false) );
                    });
                });

                
                columns.AddColumn(column =>
                {
                    column.PropertyName("Receipt_number");
                    column.CellsHorizontalAlignment(PdfRpt.Core.Contracts.HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(3);
                    column.Width(2);
                    column.HeaderCell("شماره قبض");

                });
                columns.AddColumn(column =>
                {
                    column.PropertyName("Price");
                    column.CellsHorizontalAlignment(PdfRpt.Core.Contracts.HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(5);
                    column.Width(2);
                    column.HeaderCell("مبلغ");
                    column.ColumnItemsTemplate(template =>
                    {
                        template.TextBlock();
                        template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });
                    column.AggregateFunction(aggregateFunction =>
                    {
                        aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum);
                        aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0} ریال", obj));
                    });
                });
                columns.AddColumn(column =>
                {
                    column.PropertyName("Organ_Name");
                    column.CellsHorizontalAlignment(PdfRpt.Core.Contracts.HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(5);
                    column.Width(2);
                    column.HeaderCell("نام سازمان");
                    column.Group(true,
                    (val1, val2) =>
                    {
                        return val1.ToString() == val2.ToString();
                    });
                    
                });


                columns.AddColumn(column =>
                {
                    column.PropertyName("Destination");
                    column.CellsHorizontalAlignment(PdfRpt.Core.Contracts.HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(4);
                    column.Width(2);
                    column.HeaderCell("مقصد");

                });
            })
            .MainTableEvents(events =>
            {
                events.DataSourceIsEmpty(message: "There is no data available to display.");
            })
            .Export(export =>
            {
                export.ToExcel();
            })
            .Generate(data => data.AsPdfFile(fo.Name/*string.Format("{0}\\RptCalculatedFieldsSample-{1}.pdf", Application.StartupPath, Guid.NewGuid().ToString("N")))*/));
        }

کلاس MasterDetailsHeaders  :
public  class MasterDetailsHeaders : IPageHeader
    {
        public IPdfFont PdfRptFont { set; get; }
        public string DateFrom;
        public string DateTo;
        public string ProjTitle;
        public PdfGrid RenderingGroupHeader(Document pdfDoc, PdfWriter pdfWriter, IList<CellData> newGroupInfo, IList<SummaryCellData> summaryData)
        {
           
            var table = new PdfGrid(numColumns: 1) { WidthPercentage = 100 };
            var organName = newGroupInfo.GetSafeStringValueOf("Organ_Name");
            table.AddSimpleRow(
                (cellData, cellProperties) =>
                {
                    cellData.Value = " گزارش " + organName + " از تاریخ " + DateFrom + " الی " + DateTo;
                    cellProperties.PdfFont = PdfRptFont;
                    cellProperties.PdfFontStyle = DocumentFontStyle.Bold;
                    cellProperties.HorizontalAlignment = HorizontalAlignment.Center;
                    cellProperties.RunDirection = PdfRunDirection.RightToLeft;
                });
            return table.AddBorderToTable(borderColor: BaseColor.LIGHT_GRAY, spacingBefore: 5f);
        }
      public  PdfGrid RenderingReportHeader(Document pdfDoc, PdfWriter pdfWriter, IList<SummaryCellData> summaryData)
        {
            return null;
        }

بازخوردهای پروژه‌ها
خطای واردکردن(Import) فایل کلید
Error2Cannot import the following key file: key.pfx. The key file may be password protected. To correct this, try to import the certificate again or manually install the certificate to the Strong Name CSP with the following key container name: VS_KEY_2B6952079A0469C6PdfRpt
 
مطالب
آشنایی با WPF قسمت پنجم : DataContext بخش دوم
در ادامه قسمت قبلی قصد داریم دو کنترل دیگر را نیز بایند کنیم؛ ولی از آنجا که مقادیر آن‌ها رشته‌ای یا عددی نیست و مقداری متفاوت هست، از مبحثی به نام ValueConverter استفاده خواهیم کرد.
Value Converter چیست؟


موقعی که شما قصد بایند کردن دو نوع داده متفاوت را به هم دارید، نیاز به یک کد واسط پیدا می‌کنید تا این کد واسط مقادیر شما را از مبدا دریافت کرده و تبدیل به نوعی کند که مقصد بتواند از آن استفاده کند یا بلعکس. ValueConverter نام کلاسی است که از یک اینترفیس به نام IValueConverter ارث بری کرده است و شامل دو متد تبدیل نوع از مبدا به مقصد Convert و دیگری از مقصد به مبدا  ConvertBack می‌شود که خیلی کمتر پیاده سازی می‌شود.
پیاده سازی یک کلاس مبدل سه مرحله دارد:
  • مرحله اول :ساخت کلاس ValueConverter
  • مرحله دوم : تعریف آن به عنوان یک منبع یا ریسورس
  • مرحله سوم : استفاده از آن در عملیات بایند کردن Binding

در مرحله اول، نحوه پیاده سازی کلاس ValueConverter به شکل زیر است:
public class BoolToVisibilityConverter : IValueConverter
{
    public object Convert(object value, Type targetType, 
        object parameter, CultureInfo culture)
    {
        // Do the conversion from bool to visibility
    }
 
    public object ConvertBack(object value, Type targetType, 
        object parameter, CultureInfo culture)
    {
        // Do the conversion from visibility to bool
    }
}

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

در مرحله‌ی دوم نحوه تعریف مبدل ما در پنجره XAML به صورت زیر می‌باشد:
<Window x:Class="test.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:نامی دلخواه="clr-namespace:فضای نامی که کلاس مبدل در آن قرار دارد"
        >

    <Window.Resources>
        <نام دلخواهی که در بالا تعریف کرده اید: ClassNameنام کلاس  x:Key="کلید این آیتم در ریسورس"/>
    </Window.Resources>

کلمه‌های کلیدی xmlns که مخفف XML NameSpace هستند جهت تعریف دسترسی به فضاهای نام طراحی شده‌اند و طبق تعریف مایکروسافت همان مفهوم تگ‌های تعریف فضای نام در XML را دارند که توسط مایکروسافت توسعه یافته‌اند. این تعریف‌ها در تگ ریشه (در اینجا window) تعریف می‌شوند. دو فضای نام اولی که به طور پیش فرض در همه جای پروژه قرار دارند، اشاره به فریم ورک WPF دارند. کلمه‌ی کلیدی x در خط شماره سه، نام دلخواهی است که دسترسی ما را به خصوصیات یا تعاریف XAML موجود در sdk باز می‌کند؛ مثلا استفاده از خصوصیاتی چون x:key یا x:class را به همراه دارد.
پس الان باید خط چهارم برای ما روشن باشد؛ فضای نام جدیدی را در برنامه خودمان ایجاد کرده‌ایم که این تگ به آن اشاره می‌کند و نام دلخواهی هم برای اشاره به این فضای نام برایش در نظر گرفته‌ایم. هر موقع در برنامه این نام دلخواه تعیین شده قرار گیرد، یعنی اشاره به این فضای نام که در قسمت Window.resource خط هشتم تعریف شده است.
در خط هشتم، یک ریسورس (منبع) را به برنامه معرفی کرده‌ایم:
ریسورس‌ها برای ذخیره سازی داده‌ها در سطح یک کنترل، سطح محلی در یک پنجره، یا سطح عمومی در کل پروژه به کار می‌روند. محدودیتی در ذخیره داده‌ها وجود ندارد و هر چقدر که دوست دارید می‌توانید داده به آن پاس کنید. این داده‌ها می‌توانند یک سری اطلاعات ذخیره شده در یک ساختار ساده تا یک ساختار سلسه مراتبی از کنترل‌ها باشند. ریسورس‌ها به شما این اجازه را می‌دهند تا داده‌ها را در یک مکان ذخیره کرده و آن‌ها را در یک یا چندجا مورد استفاده قرار دهید.

از آن جا که مباحث ریسورس‌ها را در یک مقاله‌ی جداگانه بررسی می‌کنیم، فقط به ذکر نکات بالا جهت کد فعلی بسنده خواهیم کرد و ادامه‌ی آن را در یک مقاله دیگر مورد بررسی قرار می‌دهیم.
هر ریسورس دارای یک نام یا یک کلید است که با خصوصیت x:key تعریف می‌شود.
ریسورس بالا یک کلاس را که در فضای نام دلخواهی قرار دارد، تعریف می‌کند و یک کلید هم به آن انتساب می‌دهد.
مرحله‌ی سوم معرفی ریسورس به عملیات Binding است:
{Binding نام پراپرتی کلاس, Converter={StaticResource کلید آیتم مربوطه در ریسورس}, ConverterParameter=پارامتری که به کلاس مبدل پاس می‌شود}

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

حال که با اصول نوشتار آشنا شدیم کار را آغاز می‌کنیم.
قصد داریم یک مبدل برای فیلد جنیست درست کنیم. از آنجا که این فیلد Boolean است و خصوصیت IsChecked یک RadioButton هم Boolean است، می‌توان یک ارتباط مستقیم را ایجاد کرد. ولی مشکل در اینجا هست که True برای مذکر است و false برای مؤنث. در نتیجه تنها Radiobutton مربوطه به جنس مذکر به این حالت پاسخ می‌دهد و از آنجا که  برای جنس مونث  false در نظر گرفته شده است، انتخاب آن هم false خواهد بود. پس باید در مبدل، مقداری که کنترل می‌خواهد را شناسایی کرده و اگر مقدار با آن برابر بود، True را بازگردانیم. مقداری که هر کنترل درخواست می‌کند را از طریق پارامتر به تابع مبدل ارسال می‌کنیم. radiobutton مذکر، مقدار True را به عنوان پارامتر ارسال می‌کند و radiobutton مونث هم مقدار false را به عنوان پارامتر ارسال می‌کند. اگر تابع مبدل را ببینید، این مقدار‌ها با پارامترها همخوانی دارند، True در غیر این صورت false بر میگرداند.

مرحله اول تعریف کلاس ValueConveter:
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace test.ValueConverters
{
    public class GenderConverter: IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var ParameterString = parameter as string;
            if (ParameterString == null)
                return DependencyProperty.UnsetValue;

            bool bparam;

            bool test = bool.TryParse(parameter.ToString(), out bparam);
            if (test)
            {
                return ((bool)value).Equals(bparam);
            }
            return DependencyProperty.UnsetValue;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}
در کد بالا پارامتر ارسالی را دریافت می‌کنیم و اگر برابر با Null باشد، مقداری برگشتی را عدم ذکر شدن خصوصیت وابسته اعلام می‌کنیم. در نتیجه انگار این خصوصیت مقداردهی نشده است. اگر مخالف Null باشد، کار ادامه پیدا می‌کند. در نهایت مقایسه‌ای بین پارامتر و مقدار پراپرتی (value) صورت گرفته و نتیجه‌ی مقایسه را برگشت می‌دهیم.

برای تعریف این مبدل در محیط XAML به صورت زیر اقدام می‌کنیم:
<Window x:Class="test.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:valueConverters="clr-namespace:test.ValueConverters"
        Title="MainWindow" Height="511.851" Width="525">

    <Window.Resources>
        <valueConverters:GenderConverter x:Key="GenderConverter"/>
    </Window.Resources>
</window>
 کلمه valueConveter به فضای نام test.valueConverters اشاره می‌کند که در قسمت ریسورس، از کلاس GenderConverter آن استفاده می‌کنیم و کلیدی که برای این ریسورس در نظر گرفتیم را GenderConverter تعریف کردیم (هم نامی کلید ریسورس و نام کلاس مبدل اتفاقی است و ارتباطی با یکدیگر ندارند).

نکته: در صورتی که بعد از تعریف ریسورس با خطای زیر روبرو شدید و محیط طراحی Design را از دست دادید یکبار پروژه را بیلد کنید تا مشکل حل شود.
The name "GenderConverter" does not exist in the namespace "clr-namespace:test.ValueConverters".


اکنون در عملیات بایندینگ دو کنترل اینگونه می‌نویسیم:
     <RadioButton GroupName="Gender" IsChecked="{Binding Gender, Converter={StaticResource GenderConverter}, ConverterParameter=True}" Name="RdoMale"  >Male</RadioButton>
                <RadioButton GroupName="Gender" IsChecked="{Binding Gender, Converter={StaticResource GenderConverter}, ConverterParameter=False}" Name="RdoFemale" Margin="0 5 0 0"  >Female</RadioButton>
حال برنامه را اجرا کرده و نتیجه را ببینید. برای تست بهتر می‌توانید جنسیت فرد را در منبع داده تغییر دهید. همچنین از آنجا که این مقدار برای جنس مذکر نیازی به بررسی و تبدیل ندارد می‌توانید عملیات بایند را عادی بنویسید:
<RadioButton GroupName="Gender" IsChecked="{Binding Gender}" Name="RdoMale"  >Male</RadioButton>
این کد می‌تواند برای تمامی وضعیت‌های دو مقداری چون جنسیت و وضعیت تاهل و ... هم به کار برود. ولی حالا سناریویی را تصور کنید که که مقادیر از دو تا بیشتر شود؛ مثل وضعیت تحصیلی ، دسترسی‌ها و غیره که این‌ها نیاز به داده‌های شمارشی چون Enum‌ها دارند. روند کار دقیقا مانند بالاست:
هر کنترل مقداری از یک enum را میپذیرد که میتواند آن مقدار را با استفاده از پارامتر، به تابع مبدل ارسال کند و سپس تابع چک می‌کند که آیا چنین مقداری در enum مدنظر یافت می‌شود یا خیر؛ این کد الان کار می‌کندو واقعا هم کد درستی است و هیچ مشکلی هم ندارد. ولی برای یک لحظه تصور کنید پنجره شما شامل چهار خصوصیت enum دار است. یعنی الان باید 4 تابع مبدل بنویسید. پس باید کد را بازنویسی کنیم تا به هر enum که مدنظر است پاسخ دهد؛ به این ترتیب تنها یک تابع مبدل می‌نویسیم.

جهت یادآوری نگاهی به کلاس برنامه می‌اندازیم:
public enum FieldOfWork
    {
        Actor=0,
        Director=1,
        Producer=2
    }
    public class Person : INotifyPropertyChanged
    { 
        public bool Gender { get; set; }

        public string ImageName { get; set; }

        public string Country { get; set; }

        public DateTime Date { get; set; }

        public FieldOfWork FieldOfWork { get; set; }

        private string _name;
        public string Name
        {
            get { return _name; }
            set
            {
                _name = value;
                OnPropertyChanged();
            }
        }
فعلا IList را از پراپرتی FieldOfWork بر میداریم تا این سناریو باشد که تنها یک Enum قابل انتخاب است:
public FieldOfWork FieldOfWork { get; set; }
بعدا حالت قبلی را بررسی میکنیم.

کد کلاس مبدل را به صورت زیر می‌نویسیم:
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;

namespace test.ValueConverters
{
    public class EnumConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var ParameterString = parameter as string;
            if (ParameterString == null)
                return DependencyProperty.UnsetValue;

            if (Enum.IsDefined(value.GetType(), value) == false)
                return DependencyProperty.UnsetValue;

            object paramvalue = Enum.Parse(value.GetType(), ParameterString);
            return paramvalue.Equals(value);
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}
در مرحله اول مثل کد قبلی بررسی می‌شود که آیا پارامتری ارسال شده است یا خیر. در مرحله دوم بررسی می‌شود که نوع داده مقدار پراپرتی چیست یعنی چه Enum ایی مورد استفاده قرار گرفته است. اگر در Enum مقداری که در پارامتر به آن ذکر شده است وجود نداشته باشد، بهتر هست که کار در همین جا به پایان برسد؛ زیرا که یک پارامتر اشتباهی ارسال شده است و چنین مقداری در Enum وجود ندارد. در غیر اینصورت کار ادامه می‌یابد و پارامتر را به enum تبدیل کرده و با مقدار مقایسه می‌کنیم. اگر برابر باشند نتیجه true را باز میگردانیم.

کد قسمت ریسورس را با کلاس جدید به روز می‌کنیم:
    <Window.Resources>
        <valueConverters:GenderConverter x:Key="GenderConverter"/>
        <valueConverters:EnumConverter x:Key="EnumConverter"></valueConverters:EnumConverter>
    </Window.Resources>
کد چک باکس‌ها هم به شکل زیر تغییر می‌یابد:
    <CheckBox Name="ChkActor" IsChecked="{Binding FieldOfWork, Converter={StaticResource EnumConverter}, ConverterParameter=Actor}" >Actor/Actress</CheckBox>
                <CheckBox Name="ChkDirector" IsChecked="{Binding FieldOfWork, Converter={StaticResource EnumConverter}, ConverterParameter=Director}" >Director</CheckBox>
                <CheckBox Name="ChkProducer" IsChecked="{Binding FieldOfWork, Converter={StaticResource EnumConverter}, ConverterParameter=Producer}" >Producer</CheckBox>
کلاس GetPerson که منبع داده ما را فراهم می‌کند هم به شکل زیر است:
  public static Person GetPerson()
        {
            return new Person()
            {
                Name = "Leo",
                Gender =true,
                ImageName ="man.jpg",
                Country = "Italy",
                FieldOfWork = test.FieldOfWork.Actor,
                Date = DateTime.Now.AddDays(-3)
            };
        }

برنامه را اجرا کنید تا نتیجه کار را ببینید. باید چک باکس Actor تیک خورده باشد. میتوانید منبع داده را تغییر داده تا نتیجه کار را ببینید.

بگذارید فیلد FieldOfWork را به حالت قبلی یعنی IList برگردانیم. در بسیاری از اوقات ما چند گزینه از یک Enum را انتخاب می‌کنیم، مثل داشتن چند سطح دسترسی یا چند سمت کاری و ...

کلاس را به همراه کد GetPerson به شکل زیر تغییر می‌دهیم:
    public enum FieldOfWork
    {
        Actor=0,
        Director=1,
        Producer=2
    }
    public class Person : INotifyPropertyChanged
    { 
        public bool Gender { get; set; }

        public string ImageName { get; set; }

        public string Country { get; set; }

        public DateTime Date { get; set; }

        public IList<FieldOfWork> FieldOfWork { get; set; }

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

        public static Person GetPerson()
        {
            return new Person
            {
                Name = "Leo",
                Gender = true,
                ImageName = "man.jpg",
                Country = "Italy",
                FieldOfWork = new FieldOfWork[] { test.FieldOfWork.Actor, test.FieldOfWork.Producer },
                Date = DateTime.Now.AddDays(-3)
            };
        }
}

دو Enum بازیگر و تهیه کننده را انتخاب کرده‌ایم، پس در زمان اجرا باید این دو گزینه انتخاب شوند.
کد مبدل را به صورت زیر می‌نویسیم:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;

namespace test.ValueConverters
{
    public class EnumList : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var ParameterString = parameter as string;
            if (ParameterString == null)
                return DependencyProperty.UnsetValue;

         var enumlist= (IList) value;         

            if(enumlist==null && enumlist.Count<1)
                return DependencyProperty.UnsetValue;

            if (Enum.IsDefined(enumlist[0].GetType(), ParameterString) == false)
                return DependencyProperty.UnsetValue;


            /*
              foreach (var item  in enumlist)
            {
                object paramvalue = Enum.Parse(item.GetType(), ParameterString);
                bool result = item.Equals(paramvalue);
                if (result)
                    return true;
            }
            return false;
             */
            return (from object item in enumlist let paramvalue = Enum.Parse(item.GetType(), ParameterString) select item.Equals(paramvalue)).Any(result => result);
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}
مقدار پراپرتی را به نوع IList تبدیل می‌کنید و اگر لیست معتبر بود، برنامه را ادامه می‌دهیم؛ در غیر اینصورت تابع خاتمه می‌یابد. بعد از اینکه صحت وجود لیست اعلام شد، بررسی میکنیم آیا Enum چنین مقداری که پارامتر ذکر کرده است را دارد یا یک پارامتر اشتباهی است.
در حلقه‌ای که به شکل توضیح درآمده، همه آیتم‌های مربوطه در لیست را بررسی کرده و اگر آیتمی برابر پارامتر باشد، True بر میگرداند و در صورتی که حلقه به اتمام برسد و آیتم پیدا نشود، مقدار False را برمی‌گرداند. این حلقه از آن جهت به شکل توضیح درآمده است که کد Linq آن در زیر نوشته شده است.

تعریف کلاس بالا در ریسورس:
   <Window.Resources>
        <valueConverters:GenderConverter x:Key="GenderConverter"/>
        <valueConverters:EnumConverter x:Key="EnumConverter"></valueConverters:EnumConverter>
        <valueConverters:EnumList x:Key="EnumList"></valueConverters:EnumList>
    </Window.Resources>
و تغییر کلید ریسورس در خطوط چک باکس ها، باقی موارد همانند قبل ثابت هستند:
     <CheckBox Name="ChkActor" IsChecked="{Binding FieldOfWork, Converter={StaticResource EnumList}, ConverterParameter=Actor}" >Actor/Actress</CheckBox>
                <CheckBox Name="ChkDirector" IsChecked="{Binding FieldOfWork, Converter={StaticResource EnumList}, ConverterParameter=Director}" >Director</CheckBox>
                <CheckBox Name="ChkProducer" IsChecked="{Binding FieldOfWork, Converter={StaticResource EnumList}, ConverterParameter=Producer}" >Producer</CheckBox>

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

  بدیهی است خروجی‌های بالا برای کنترل هایی است که مقدار Boolean را می‌پذیرند و برای سایر کنترل‌ها باید با کمی تغییر در کد و نوع برگشتی که تحویل خروجی متد مبدل می‌شود، دهید.

دانلود فایل‌های این قسمت
مطالب دوره‌ها
استفاده از RavenDB در ASP.NET MVC به همراه تزریق وابستگی‌ها
جهت تکمیل مباحث این دوره می‌توان به نحوه مدیریت سشن‌ها و document store بانک اطلاعاتی RavenDB با استفاده از یک IoC Container مانند StructureMap در ASP.NET MVC پرداخت. اصول کلی آن به تمام فناوری‌های دات نتی دیگر مانند وب فرم‌ها، WPF و غیره نیز قابل بسط است. تنها پیشنیاز آن مطالعه «کامل» دوره «بررسی مفاهیم معکوس سازی وابستگی‌ها و ابزارهای مرتبط با آن » می‌باشد.


هدف از بحث
ارائه راه حلی جهت تزریق یک وهله از واحد کار تشکیل شده (همان شیء سشن در RavenDB) به کلیه کلاس‌های لایه سرویس برنامه و همچنین زنده نگه داشتن شیء document store آن در طول عمر برنامه است. ایجاد شیء document store که کار اتصال به بانک اطلاعاتی را مدیریت می‌کند، بسیار پرهزینه است. به همین جهت این شیء تنها یکبار باید در طول عمر برنامه ایجاد شود.


ابزارها و پیشنیازهای لازم
ابتدا یک برنامه جدید ASP.NET MVC را آغاز کنید. سپس ارجاعات لازم را به کلاینت RavenDB، سرور درون پروسه‌ای آن (RavenDB.Embedded) و همچنین StructureMap با استفاده از نیوگت، اضافه نمائید:
 PM> Install-Package RavenDB.Client
PM> Install-Package RavenDB.Embedded -Pre
PM> Install-Package structuremap

دریافت کدهای کامل این مثال

این مثال، به همراه فایل‌های باینری ارجاعات یاد شده، نیست (جهت کاهش حجم 100 مگابایتی آن). برای بازیابی آن‌ها می‌توانید به مطلبی در اینباره در سایت مراجعه کنید.
این پروژه از چهار قسمت مطابق شکل زیر تشکیل شده است:


الف) لایه سرویس‌های برنامه
using RavenDB25Mvc4Sample.Models;
using System.Collections.Generic;

namespace RavenDB25Mvc4Sample.Services.Contracts
{
    public interface IUsersService
    {
        User AddUser(User user);
        IList<User> GetUsers(int page, int count = 20);
    }
}

using System.Collections.Generic;
using System.Linq;
using Raven.Client;
using RavenDB25Mvc4Sample.Models;
using RavenDB25Mvc4Sample.Services.Contracts;

namespace RavenDB25Mvc4Sample.Services
{
    public class UsersService : IUsersService
    {
        private readonly IDocumentStore _documentStore;
        private readonly IDocumentSession _documentSession;
        public UsersService(IDocumentStore documentStore, IDocumentSession documentSession)
        {
            _documentStore = documentStore;
            _documentSession = documentSession;
        }

        public User AddUser(User user)
        {
            _documentSession.Store(user);
            return user;
        }

        public IList<User> GetUsers(int page, int count = 20)
        {
            return _documentSession.Query<User>()
                                   .Skip(page * count)
                                   .Take(count)
                                   .ToList();
        }

        //todo: سایر متدهای مورد نیاز در اینجا

    }
}
نکته مهمی که در اینجا وجود دارد، استفاده از اینترفیس‌های خود RavenDB است. به عبارتی IDocumentSession، تشکیل دهنده الگوی واحد کار در RavenDB است و نیازی به تعاریف اضافه‌تری در اینجا وجود ندارد.
هر کلاس لایه سرویس با یک اینترفیس مشخص شده و اعمال آن‌ها از طریق این اینترفیس‌ها در اختیار کنترلرهای برنامه قرار می‌گیرند.

ب) لایه Infrastructure برنامه
در این لایه کدهای اتصالات IoC Container مورد استفاده قرار می‌گیرند. کدهایی که به برنامه جاری وابسته‌اند، اما حالت عمومی و مشترکی ندارند تا در سایر پروژه‌های مشابه استفاده شوند.
using Raven.Client;
using Raven.Client.Embedded;
using RavenDB25Mvc4Sample.Services;
using RavenDB25Mvc4Sample.Services.Contracts;
using StructureMap;

namespace RavenDB25Mvc4Sample.Infrastructure
{
    public static class IoCConfig
    {
        public static void ApplicationStart()
        {
            ObjectFactory.Initialize(x =>
            {
                // داکیومنت استور سینگلتون تعریف شده چون باید در طول عمر برنامه زنده نگه داشته شود
                x.ForSingletonOf<IDocumentStore>().Use(() =>
                       {
                           return new EmbeddableDocumentStore
                           {
                               DataDirectory = "App_Data"
                           }.Initialize();
                       });

                // سشن در برنامه وب هیبرید تعریف شده تا در طول عمر یک درخواست زنده نگه داشته شود
                // در برنامه‌های ویندوزی حالت هیبرید را حذف کنید
                x.For<IDocumentSession>().HybridHttpOrThreadLocalScoped().Use(context =>
                    {
                        return context.GetInstance<IDocumentStore>().OpenSession();
                    });

                // اتصالات لایه سرویس در اینجا
                x.For<IUsersService>().Use<UsersService>();
                // ...
            });
        }

        public static void ApplicationEndRequest()
        {
            ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects();
        }
    }
}
تعاریف اتصالات StructureMap را در اینجا ملاحظه می‌کنید.
IDocumentStore و IDocumentSession، دو اینترفیس تعریف شده در کلاینت RavenDB هستند. اولی کار اتصال به بانک اطلاعاتی را مدیریت خواهد کرد و دومی کار مدیریت الگوی واحد کار را انجام می‌دهد. IDocumentStore به صورت Singleton تعریف شده است؛ چون باید در طول عمر برنامه زنده نگه داشته شود. اما IDocumentStore در ابتدای هر درخواست رسیده، وهله سازی شده و سپس در پایان هر درخواست در متد ApplicationEndRequest به صورت خودکار Dispose خواهد شد.
اگر به فایل Global.asax.cs پروژه وب برنامه مراجعه کنید، نحوه استفاده از این کلاس را مشاهده خواهید کرد:
using System;
using System.Globalization;
using System.Web.Mvc;
using System.Web.Routing;
using RavenDB25Mvc4Sample.Infrastructure;
using StructureMap;

namespace RavenDB25Mvc4Sample
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            IoCConfig.ApplicationStart();

            AreaRegistration.RegisterAllAreas();

            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);

            //Set current Controller factory as StructureMapControllerFactory  
            ControllerBuilder.Current.SetControllerFactory(new StructureMapControllerFactory());
        }

        protected void Application_EndRequest(object sender, EventArgs e)
        {
            IoCConfig.ApplicationEndRequest();
        }
    }

    public class StructureMapControllerFactory : DefaultControllerFactory
    {
        protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
        {
            if (controllerType == null)
                throw new InvalidOperationException(string.Format("Page not found: {0}", 
                    requestContext.HttpContext.Request.Url.AbsoluteUri.ToString(CultureInfo.InvariantCulture)));
            return ObjectFactory.GetInstance(controllerType) as Controller;
        }
    }
}
در ابتدای کار برنامه، متد IoCConfig.ApplicationStart جهت برقراری اتصالات، فراخوانی می‌شود. در پایان هر درخواست نیز شیء سشن جاری تخریب خواهد شد. همچنین کلاس StructureMapControllerFactory نیز جهت وهله سازی خودکار کنترلرهای برنامه به همراه تزریق وابستگی‌های مورد نیاز، تعریف گشته است.

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

using System.Web.Mvc;
using Raven.Client;
using RavenDB25Mvc4Sample.Models;
using RavenDB25Mvc4Sample.Services.Contracts;

namespace RavenDB25Mvc4Sample.Controllers
{
    public class HomeController : Controller
    {
        private readonly IDocumentSession _documentSession;
        private readonly IUsersService _usersService;
        public HomeController(IDocumentSession documentSession, IUsersService usersService)
        {
            _documentSession = documentSession;
            _usersService = usersService;
        }

        [HttpGet]
        public ActionResult Index()
        {
            return View(); //نمایش صفحه ثبت
        }

        [HttpPost]
        public ActionResult Index(User user)
        {
            if (this.ModelState.IsValid)
            {
                _usersService.AddUser(user);
                _documentSession.SaveChanges();

                return RedirectToAction("Index");
            }
            return View(user);
        }
    }
}
پس از این مقدمات و طراحی اولیه، استفاده از کلاس‌های لایه سرویس در کنترل‌ها، ساده خواهند بود. تنها کافی است اینترفیس‌های مورد نیاز را از طریق روش تزریق در سازنده کلاس‌ها تعریف کنیم. سایر مسایل وهله سازی آن خودکار خواهند بود.
مطالب
چگونه کد قابل تست بنویسیم - قسمت دوم و پایانی
گام 3 – از بین بردن ارتباط لایه‌ها (Loose Coupling)
بجای استفاده از اشیاء واقعی، براساس interface‌ها برنامه نویسی کنید. اگر شما کد خود را با استفاده از IShoppingCartService  به عنوان یک interface بجای استفاده از شیء واقعی ShoppingCartService نوشته باشید، زمانیکه تست را مینویسید، میتوانید یک سرویس کارت خرید جعلی (mocking) که IShoppingCartService را پیاده سازی کرده جایگزین شیء اصلی نمایید. در کد زیر، توجه کنید تنها تغییر این است که متغیر عضو اکنون از نوع IShoppingCartService  است بجای ShoppingCartService .
public interface IShoppingCartService
{
    ShoppingCart GetContents();
    ShoppingCart AddItemToCart(int itemId, int quantity);
}
public class ShoppingCartService : IShoppingCartService
{
    public ShoppingCart GetContents()
    {
        throw new NotImplementedException("Get cart from Persistence Layer");
    }
    public ShoppingCart AddItemToCart(int itemId, int quantity)
    {
        throw new NotImplementedException("Add Item to cart then return updated cart");
    }
}
public class ShoppingCart
{
    public List<product> Items { get; set; }
}
public class Product
{
    public int ItemId { get; set; }
    public string ItemName { get; set; }
}
public class ShoppingCartController : Controller
{
    //Concrete object below points to actual service
    //private ShoppingCartService _shoppingCartService;
    //loosely coupled code below uses the interface rather than the 
    //concrete object
    private IShoppingCartService _shoppingCartService;
    public ShoppingCartController()
    {
        _shoppingCartService = new ShoppingCartService();
    }
    public ActionResult GetCart()
    {
        //now using the shared instance of the shoppingCartService dependency
        ShoppingCart cart = _shoppingCartService.GetContents();
        return View("Cart", cart);
    }
    public ActionResult AddItemToCart(int itemId, int quantity)
    {
        //now using the shared instance of the shoppingCartService dependency
        ShoppingCart cart = _shoppingCartService.AddItemToCart(itemId, quantity);
        return View("Cart", cart);
    }
}

گام 4 – وابستگی‌ها را تزریق کنید
اکنون ما تمام وابستگی‌ها را در یک جا مرکزیت داده‌ایم و کد ما رابطه کمی با آن وابستگی‌ها دارد. همانند گذشته، چندین راه برای پیاده سازی این گام وجود دارد. بدون استفاده از ابزارهای کمکی برای این مفهوم، ساده‌ترین راه دوباره نویسی (Overload) متد ایجاد کننده است:
//loosely coupled code below uses the interface rather 
//than the concrete object
private IShoppingCartService _shoppingCartService;
//MVC uses this constructor 
public ShoppingCartController()
{
    _shoppingCartService = new ShoppingCartService();
}
//You can use this constructor when testing to inject the    
//ShoppingCartService dependency
public ShoppingCartController(IShoppingCartService shoppingCartService)
{
    _shoppingCartService = shoppingCartService;
}

گام 5 – تست را با استفاده از یک شیء جعلی (Mocking) انجام دهید
مثالی از یک سناریوی تست ممکن در زیر آمده است. توجه کنید که یک شیء جعلی از نوع کلاس ShoppingCartService ساخته‌ایم. این شیء جعلی فرستاده می‌شود به شیء Controller و متد GetContents پیاده سازی میشود تا بجای آنکه کد اصلی را که به منبع داده مراجعه می‌کند اجرا نماید، داده‌های جعلی و شبیه سازی شده را برگرداند. بدلیل آنکه تمام کد را نوشته ایم، بسیار سریعتر از اجرای کوئری بر روی دیتابیس اجرا خواهد شد و دیگر نگرانی بابت تهیه داده تستی و یا تصحیح داده بعد از اتمام تست (بازگرداندن داده به حالت قبل از تست) نخواهم داشت. توجه داشته باشید که بدلیل مرکزیت دادن به وابستگی‌ها در گام 2 ، تنها باید یکبار آنرا تزریق نماییم و بخاطر کاری که در گام 3 انجام شد، وابستگی ما به حدی پایین آمده که میتوانیم هر شیء‌ایی  را  (جعلی و یا حقیقی) ارسال کنیم و فقط کافیست شیء مورد نظر IShoppingCartService را پیاده سازی کرده باشد.
[TestClass]
public class ShoppingCartControllerTests
{
    [TestMethod]
    public void GetCartSmokeTest()
    {
        //arrange
        ShoppingCartController controller = 
           new ShoppingCartController(new ShoppingCartServiceStub());
        // Act
        ActionResult result = controller.GetCart();
        // Assert
        Assert.IsInstanceOfType(result, typeof(ViewResult));
    }
}
/// <summary>
/// This is is a stub of the ShoppingCartService
/// </summary>
public class ShoppingCartServiceStub : IShoppingCartService
{
    public ShoppingCart GetContents()
    {
        return new ShoppingCart
        {
            Items = new List<product> {
                new Product {ItemId = 1, ItemName = "Widget"}
            }
        };
    }
    public ShoppingCart AddItemToCart(int itemId, int quantity)
    {
        throw new NotImplementedException();
    }
}

مطالب تکمیلی
از یک ابزار کنترل وابستگی (IoC/DI) استفاده کنید:
از رایج‌ترین و عمومی‌ترین ابزارهای کنترل وابستگی برای .Net می‌توان به StructureMap و CastleWindsor اشاره کرد. در کد نویسی واقعی، شما وابستگی‌های بسیاری خواهید داشت، که این وابستگی‌ها هم وابستگی هایی دارند که به سرعت از مدیریت شما خارج خواهند شد. راه حل این مشکل استفاده از یک ابزار کنترل وابستگی خواهد بود.
از یک چارچوب تجزیه (Isolation Framework) استفاده نمایید:
برای ایجاد اشیاء جعلی ممکن است کار زیادی لازم باشد و  استفاده از یک Isolation Framework میتواند زمان و میزان کد نویسی شمارا کم کند. از رایج‌ترین این ابزارها میتوان Rhino Mocks  و Moq را نام برد.
مطالب
Globalization در ASP.NET MVC - قسمت چهارم
در قسمت قبل مقدمه ای راجع به انواع منابع موجود در ASP.NET و برخی مسائل پیرامون آن ارائه شد. در این قسمت راجع به نحوه رفتار ASP.NET در برخورد با انواع منابع بحث می‌شود.

مدیریت منابع در ASP.NET 
در مدل پرووایدر منابع در ASP.NET کار مدیریت منابع از کلاس ResourceProviderFactory شروع می‌شود. این کلاس که از نوع abstract تعریف شده است، دو متد برای فراهم کردن پرووایدرهای کلی و محلی دارد.
کلاس پیش‌فرض در ASP.NET برای پیاده‌سازی ResourceProviderFactory در اسمبلی System.Web قرار دارد. این کلاس که ResXResourceProviderFactory نام دارد نمونه‌هایی از کلاس‌های LocalResxResourceProvider و GlobalResxResourceProvider را برمی‌گرداند. درباره این کلاس‌ها در ادامه بیشتر بحث خواهد شد.

نکته: هر سه کلاس پیش‌فرض اشاره شده در بالا و نیز سایر کلاس‌های مربوط به عملیات مدیریت منابع در آن‌ها، همگی در فضای نام System.Web.Compilation قرار دارند و متاسفانه دارای سطح دسترسی internal هستند. بنابراین به صورت مستقیم در دسترس نیستند.

برای نمونه با توجه به تصویر فرضی نشان داده شده در قسمت قبل، در اولین بارگذاری صفحه SubDir1\Page1.aspx عبارات ضمنی بکاربرده شده در این صفحه برای منابع محلی (در قسمت قبل شرح داده شده است) باعث فراخوانی متد مربوط به Local Resources در کلاس ResXResourceProviderFactory می‌شود. این متد نمونه‌ای از کلاس LocalResXResourceProvider برمی‌گرداند. (در ادامه با نحوه سفارشی‌سازی این کلاس‌ها نیز آشنا خواهیم شد).
رفتار پیش‌فرض این پرووایدر این است که نمونه‌ای از کلاس ResourceManager با توجه به کلید درخواستی برای صفحه موردنظر (مثلا نوع Page1.aspx در اسمبلی App_LocalResources.subdir1.XXXXXX که در تصویر موجود در قسمت قبل نشان داده شده است) تولید می‌کند. حال این کلاس با استفاده از کالچر مربوط به درخواست موردنظر، ورودی موردنظر را از منبع مربوطه استخراج می‌کند. مثلا اگر کالچر موردبحث es (اسپانیایی) باشد، اسمبلی ستلایت موجود در مسیر نسبی \es\ انتخاب می‌شود.
برای روشن‌تر شدن بحث به تصویر زیر که عملیات مدیریت منابع پیش فرض در ASP.NET در درخواست صفحه Page1.aspx از پوشه SubDir1 را نشان می‌دهد، دقت کنید:

همانطور که در قسمت اول این سری مطالب عنوان شد، رفتار کلاس ResourceManager برای یافتن کلیدهای Resource، استخراج آن از نزدیکترین گزینه موجود است. یعنی مثلا برای یافتن کلیدی در کالچر es در مثال بالا، ابتدا اسمبلی‌های مربوط به این کالچر جستجو می‌شود و اگر ورودی موردنظر یافته نشد، جستجو در اسمبلی‌های ستلایت پیش‌فرض سیستم موجود در ریشه فولدر bin برنامه ادامه می‌یابد، تا درنهایت نزدیک‌ترین گزینه پیدا شود (فرایند fallback).

نکته: همانطور که در تصویر بالا نیز مشخص است، نحوه نامگذاری اسمبلی منابع محلی به صورت <App_LocalResources.<SubDirectory>.<A random code است.

نکته: پس از اولین بارگذاری هر اسمبلی، آن اسمبلی به همراه خود نمونه کلاس ResourceManager که مثلا توسط کلاس LocalResXResourceProvider تولید شده است در حافظه سرور کش می‌شوند تا در استفاده‌های بعدی به کار روند.

نکته: فرایند مشابه‌ای برای یافتن کلیدها در منابع کلی (Global Resources) به انجام می‌رسد. تنها تفاوت آن این است که کلاس ResXResourceProviderFactory نمونه‌ای از کلاس GlobalResXResourceProvider تولید می‌کند.

چرا پرووایدر سفارشی؟
تا اینجا بالا با کلیات عملیاتی که ASP.NET برای بارگذاری منابع محلی و کلی به انجام می‌رساند، آشنا شدیم. حالا باید به این پرسش پاسخ داد که چرا پرووایدری سفارشی نیاز است؟ علاوه بر دلایلی که در قسمت‌های قبلی به آنها اشاره شد، می‌توان دلایل زیر را نیز برشمرد:
 
- استفاده از منابع و یا اسمبلی‌های ستلایت موجود - اگر بخواهید در برنامه خود از اسمبلی‌هایی مشترک، بین برنامه‌های ویندوزی و وبی استفاده کنید، و یا بخواهید به هردلیلی از اسمبلی‌های جداگانه‌ای برای این منابع استفاده کنید، مدل پیش‌فرض موجود در ASP.NET جوابگو نخواهد بود.

- استفاده از منابع دیگری به غیر از فایلهای resx. مثل دیتابیس - برای برنامه‌های تحت وب که صفحات بسیار زیاد به همراه ورودی‌های بیشماری از Resourceها دارند، استفاده از مدل پرووایدر منابع پیش‌فرض در ASP.NET و ذخیره تمامی این ورودی‌ها درون فایل‌های resx. بار نسبتا زیادی روی حافظه سرور خواهد گذاشت. درصورت مدیریت بهینه فراخوانی‌های سمت دیتابیس می‌توان با بهره‌برداری از جداول یک دیتابیس به عنوان منبع، کمک زیادی به وب سرور کرد! هم‌چنین با استفاده از دیتابیس می‌توان مدیریت بهتری بر ورودی‌ها داشت و نیز امکان ذخیره‌سازی حجم بیشتری از داده‌ها در اختیار توسعه دهنده قرار خواهد گرفت.
البته به غیر از دیتابیس و فایل‌های resx. نیز گزینه‌های دیگری برای ذخیره‌سازی ورودی‌های این منابع وجود دارند. به عنوان مثال می‌توان مدیریت این منابع را کلا به سیستم دیگری سپرد و درخواست ورودی‌های موردنیاز را به یکسری وب‌سرویس سپرد. برای پیاده سازی چنین سیستمی نیاز است تا مدلی سفارشی تهیه و استفاده شود.

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

مدل پرووایدر منابع
همانطور که قبلا هم اشاره شد، وظیفه استخراج داده‌ها از Resourceها به صورت پیش‌فرض، درنهایت بر عهده نمونه‌ای از کلاس ResourceManager است. در واقع این کلاس کل فرایند انتخاب مناسب‌ترین کلید از منابع موجود را با توجه به کالچر رابط کاربری (UI Culture) در ثرد جاری کپسوله می‌کند. درباره این کلاس در ادامه بیشتر بحث خواهد شد.
هم‌چنین بازهم همانطور که قبلا توضیح داده شد، استفاده از ورودی‌های منابع موجود به دو روش انجام می‌شود. استفاده از عبارات بومی‌سازی و نیز با استفاده از برنامه‌نویسی که ازطریق دومتد GetLocalResourceObject و GetGlobalResourceObject انجام می‌شود. درضمن کلیه عبارات بومی‌سازی در زمان رندر صفحات وب درنهایت تبدیل به فراخوانی‌هایی از این دو متد در کلاس TemplateControl خواهند شد.
عملیات پس از فراخوانی این دو متد جایی است که مدل Resource Provider پیش‌فرض ASP.NET وارد کار می‌شود. این فرایند ابتدا با فراخوانی نمونه‌ای از کلاس ResourceProviderFactory آغاز می‌شود که پیاده‌سازی پیش‌فرض آن در کلاس ResXResourceProviderFactory قرار دارد.
این کلاس سپس با توجه به نوع منبع درخواستی (Global یا Local) نمونه‌ای از پرووایدر مربوطه (که باید اینترفیس IResourceProvider را پیاده‌سازی کرده باشند) را تولید می‌کند. پیاده‌سازی پیش‌فرض این پرووایدرها در ASP.NET در کلاس‌های GlobalResXResourceProvider و LocalResXResourceProvider قرار دارد.
این پروایدرها درنهایت باتوجه به محل ورودی درخواستی، نمونه مناسب از کلاس RsourceManager را تولید و استفاده می‌کنند.
هم‌چنین در پروایدرهای محلی، برای استفاده از عبارات بومی‌سازی ضمنی، نمونه‌ای از کلاس ResourceReader مورد استفاده قرار می‌گیرد. در زمان تجزیه و تحلیل صفحه وب درخواستی در سرور، با استفاده از این کلاس کلیدهای موردنظر یافته می‌شوند. این کلاس درواقع پیاده‌سازی اینترفیس IResourceReader بوده که حاوی یک Enumerator که جفت داده‌های Key-Value از کلیدهای Resource را برمی‌گرداند، است.
تصویر زیر نمایی کلی از فرایند پیش‌فرض موردبحث را نشان می‌دهد:

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

پیاده‌سازی‌ها
کلاس ResourceProviderFactory به صورت زیر تعریف شده است:
public abstract class ResourceProviderFactory
{
    public abstract IResourceProvider CreateGlobalResourceProvider(string classKey);
    public abstract IResourceProvider CreateLocalResourceProvider(string virtualPath);
}
همانطور که مشاهده می‌کنید دو متد برای تولید پرووایدرهای مخصوص منابع کلی و محلی در این کلاس وجود دارد. پرووایدر کلی تنها نیاز به نام کلید Resource برای یافتن داده موردنظر دارد. اما پرووایدر محلی به مسیر صفحه درخواستی برای اینکار نیاز دارد که با توجه به توضیحات ابتدای این مطلب کاملا بدیهی است.
پس از تولید پرووایدر موردنظر با استفاده از متد مناسب با توجه به شرایط شرح داده شده در بالا، نمونه تولیدشده از کلاس پرووایدر موردنظر وظیفه فراهم‌کردن کلیدهای Resource را برعهده دارد. پرووایدرهای موردبحث باید اینترفیس IResourceProvider را که به صورت زیر تعریف شده است، پیاده سازی کنند:
public interface IResourceProvider
{
    IResourceReader ResourceReader { get; }
    object GetObject(string resourceKey, CultureInfo culture);
}
همانطور که می‌بینید این پرووایدرها باید یک RsourceReader برای خواندن کلیدهای Resource فراهم کنند. همچنین یک متد با عنوان GetObject که کار اصلی برگرداندن داده ذخیره‌شده در ورودی موردنظر را برعهده دارد باید در این پرووایدرها پیاده‌سازی شود. همانطور که قبلا اشاره شد، پیاده‌سازی پیش‌فرض این کلاس‌ها درنهایت نمونه‌ای از کلاس ResourceManager را برای یافتن مناسب‌ترین گزینه از بین کلیدهای موجود تولید می‌کند. این نمونه مورد بحث در متد GetObject مورد استفاده قرار می‌گیرد. 

نکته: کدهای نشان‌داده‌شده در ادامه مطلب با استفاده از ابزار محبوب ReSharper استخراج شده‌اند. این ابزار برای دریافت این کدها معمولا از APIهای سایت SymbolSource.org استفاده می‌کند. البته منبع اصلی تمام کدهای دات نت فریمورک همان referencesource.microsoft.com است.
 
کلاس ResXResourceProviderFactory
پیاده‌سازی پیش‌فرض کلاس ResourceProviderFactory در ASP.NET که در کلاس ResXResourceProviderFactory قرار دارد، به صورت زیر است:
// Type: System.Web.Compilation.ResXResourceProviderFactory
// Assembly: System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
// Assembly location: C:\Windows\Microsoft.NET\assembly\GAC_32\System.Web\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Web.dll

using System.Runtime;
using System.Web;
namespace System.Web.Compilation
{
  internal class ResXResourceProviderFactory : ResourceProviderFactory
  {
    [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
    public ResXResourceProviderFactory()    {    }
    public override IResourceProvider CreateGlobalResourceProvider(string classKey)
    {
      return (IResourceProvider) new GlobalResXResourceProvider(classKey);
    }
    public override IResourceProvider CreateLocalResourceProvider(string virtualPath)
    {
      return (IResourceProvider) new LocalResXResourceProvider(VirtualPath.Create(virtualPath));
    }
  }
}
در این کلاس برای تولید پرووایدر منابع محلی از کلاس VirtualPath استفاده شده است که امکاناتی جهت استخراج مسیرهای موردنظر با توجه به مسیر نسبی و مجازی ارائه‌شده فراهم می‌کند. متاسفانه این کلاس نیز با سطح دسترسی internal تعریف شده است و امکان استفاده مستقیم از آن وجود ندارد.
 
کلاس GlobalResXResourceProvider
پیاده‌سازی پیش‌فرض اینترفیس IResourceProvider در ASP.NET برای منابع کلی که در کلاس GlobalResXResourceProvider قرار دارد، به صورت زیر است:
internal class GlobalResXResourceProvider : BaseResXResourceProvider
{
  private string _classKey;
  internal GlobalResXResourceProvider(string classKey)
  {
    _classKey = classKey;
  }
  protected override ResourceManager CreateResourceManager()
  {
    string fullClassName = BaseResourcesBuildProvider.DefaultResourcesNamespace + "." + _classKey;
    // If there is no app resource assembly, return null
    if (BuildManager.AppResourcesAssembly == null)
      return null;
    ResourceManager resourceManager = new ResourceManager(fullClassName, BuildManager.AppResourcesAssembly);
    resourceManager.IgnoreCase = true;
    return resourceManager;
  }
  public override IResourceReader ResourceReader
  {
    get
    {
      // App resources don't support implicit resources, so the IResourceReader should never be needed 
      throw new NotSupportedException();
    }
  }
}
در این کلاس عملیات تولید نمونه مناسب از کلاس ResourceManager انجام می‌شود. مقدار BaseResourcesBuildProvider.DefaultResourcesNamespace به صورت زیر تعریف شده است:
internal const string DefaultResourcesNamespace = "Resources";
که قبلا هم درباره این مقدار پیش فرض اشاره‌ای شده بود.
پارامتر classKey درواقع اشاره به نام فایل اصلی منبع کلی دارد. مثلا اگر این مقدار برابر Resource1 باشد، کلاس ResourceManager برای نوع داده Resources.Resource1 تولید خواهد شد.
هم‌چنین اسمبلی موردنظر برای یافتن ورودی‌های منابع کلی که از BuildManager.AppResourcesAssembly دریافت شده است، به صورت پیش فرض هم‌نام با مسیر منابع کلی و با عنوان App_GlobalResources تولید می‌شود.
کلاس BuildManager فرایندهای کامپایل کدها و  صفحات برای تولید اسمبلی‌ها و نگهداری از آن‌ها در حافظه را مدیریت می‌کند. این کلاس که محتوای نسبتا مفصلی دارد (نزدیک به 2000 خط کد) به صورت public و sealed تعریف شده است. بنابراین با ریفرنس دادن اسمبلی System.Web در فضای نام System.Web.Compilation در دسترس است، اما نمی‌توان کلاسی از آن مشتق کرد. BuildManager حاوی تعداد زیادی اعضای استاتیک برای دسترسی به اطلاعات اسمبلی‌هاست. اما متاسفانه بیشتر آن‌ها سطح دسترسی عمومی ندارند.

نکته: همانطور که در بالا نیز اشاره شد، ازآنجاکه کلاس ResourceReader در اینجا تنها برای عبارات بومی سازی ضمنی کاربرد دارد، و نیز عبارات بومی‌سازی ضمنی تنها برای منابع محلی کاربرد دارند، در این کلاس برای خاصیت مربوطه در پیاده سازی اینترفیس IResourceProvider یک خطای عدم پشتیبانی (NotSupportedException) صادر شده است.

کلاس LocalResXResourceProvider
پیاده‌سازی پیش‌فرض اینترفیس IResourceProvider در ASP.NET برای منابع محلی که در کلاس LocalResXResourceProvider قرار دارد، به صورت زیر است:
internal class LocalResXResourceProvider : BaseResXResourceProvider
{
  private VirtualPath _virtualPath;
  internal LocalResXResourceProvider(VirtualPath virtualPath)
  {
    _virtualPath = virtualPath;
  }
  protected override ResourceManager CreateResourceManager()
  {
    ResourceManager resourceManager = null;
    Assembly pageResAssembly = GetLocalResourceAssembly();
    if (pageResAssembly != null)
    {
      string fileName = _virtualPath.FileName;
      resourceManager = new ResourceManager(fileName, pageResAssembly);
      resourceManager.IgnoreCase = true;
    }
    else
    {
      throw new InvalidOperationException(SR.GetString(SR.ResourceExpresionBuilder_PageResourceNotFound));
    }
    return resourceManager;
  }
  public override IResourceReader ResourceReader
  {
    get
    {
      // Get the local resource assembly for this page 
      Assembly pageResAssembly = GetLocalResourceAssembly();
      if (pageResAssembly == null) return null;
      // Get the name of the embedded .resource file for this page 
      string resourceFileName = _virtualPath.FileName + ".resources";
      // Make it lower case, since GetManifestResourceStream is case sensitive 
      resourceFileName = resourceFileName.ToLower(CultureInfo.InvariantCulture);
      // Get the resource stream from the resource assembly
      Stream resourceStream = pageResAssembly.GetManifestResourceStream(resourceFileName);
      // If this page has no resources, return null 
      if (resourceStream == null) return null;
      return new ResourceReader(resourceStream);
    }
  }
  [PermissionSet(SecurityAction.Assert, Unrestricted = true)]
  private Assembly GetLocalResourceAssembly()
  {
    // Remove the page file name to get its directory
    VirtualPath virtualDir = _virtualPath.Parent;
    // Get the name of the local resource assembly
    string cacheKey = BuildManager.GetLocalResourcesAssemblyName(virtualDir);
    BuildResult result = BuildManager.GetBuildResultFromCache(cacheKey);
    if (result != null)
    {
      return ((BuildResultCompiledAssembly)result).ResultAssembly;
    }
    return null;
  }
}
عملیات موجود در این کلاس باتوجه به فرایندهای مربوط به یافتن اسمبلی مربوطه با استفاده از مسیر ارائه‌شده، کمی پیچیده‌تر از کلاس قبلی است.
در متد GetLocalResourceAssembly عملیات یافتن اسمبلی متناظر با درخواست جاری انجام می‌شود. اینکار باتوجه به نحوه نامگذاری اسمبلی منابع محلی که در ابتدای این مطلب اشاره شد انجام می‌شود. مثلا اگر صفحه درخواستی در مسیر SubDir1/Page1.aspx/~ باشد، در این متد با استفاده از ابزارهای موجود عنوان اسمبلی نهایی برای این مسیر که به صورت App_LocalResources.SubDir1.XXXXX است تولید و درنهایت اسمبلی مربوطه استخراج می‌شود.
درضمن در اینجا هم کلاس ResourceManager برای نوع داده متناظر با نام فایل اصلی منبع محلی تولید می‌شود. مثلا برای مسیر مجازی SubDir1/Page1.aspx/~ نوع داده‌ای با نام Page1.aspx درنظر گرفته خواهد شد (با توجه به نام فایل منبع محلی که باید به صورت Page1.aspx.resx باشد. در قسمت قبل در این باره شرح داده شده است).

نکته: کلاس SR (مخفف String Resources) که در فضای نام System.Web قرار دارد، حاوی عناوین کلیدهای Resourceهای مورداستفاده در اسمبلی System.Web است. این کلاس با سطح دسترسی internal و به صورت sealed تعریف شده است. عنوان تمامی کلیدها به صورت ثوابتی از نوع رشته تعریف شده‌‌اند.
SR درواقع یک Wrapper بر روی کلاس ResourceManager است تا از تکرار عناوین کلیدهای منابع که از نوع رشته هستند، در جاهای مختلف برنامه جلوگیری شود. کار این کلاس مشابه کاری است که کتابخانه T4MVC برای نگهداری عناوین کنترلرها و اکشن‌ها به صورت رشته‌های ثابت انجام می‌دهد. از این روش در جای جای دات نت فریمورک برای نگهداری رشته‌های ثابت استفاده شده است!
 
نکته: باتوجه به استفاده از عبارات بومی‌سازی ضمنی در استفاده از ورودی‌های منابع محلی، خاصیت ResourceReader در این کلاس نمونه‌ای متناظر برای درخواست جاری از کلاس ResourceReader با استفاده از Stream استخراج شده از اسمبلی یافته شده، تولید می‌کند.

کلاس پایه BaseResXResourceProvider
کلاس پایه BaseResXResourceProvider که در دو پیاده‌سازی نشان داده شده در بالا استفاده شده است (هر دو کلاس از این کلاس مشتق شده‌اند)، به صورت زیر است: 
internal abstract class BaseResXResourceProvider : IResourceProvider
{
  private ResourceManager _resourceManager;
  ///// IResourceProvider implementation
  public virtual object GetObject(string resourceKey, CultureInfo culture)
  {
    // Attempt to get the resource manager
    EnsureResourceManager();
    // If we couldn't get a resource manager, return null 
    if (_resourceManager == null) return null;
    if (culture == null) culture = CultureInfo.CurrentUICulture;
    return _resourceManager.GetObject(resourceKey, culture);
  }
  public virtual IResourceReader ResourceReader { get { return null; } }
  ///// End of IResourceProvider implementation 
  protected abstract ResourceManager CreateResourceManager();
  private void EnsureResourceManager()
  {
    if (_resourceManager != null)  return;
    _resourceManager = CreateResourceManager();
  }
}
در این کلاس پیاده‌سازی اصلی اینترفیس IResourceProvider انجام شده است. همانطور که می‌بینید کار نهایی استخراج ورودی‌های منابع در متد GetObject با استفاده از نمونه فراهم شده از کلاس ResourceManager انجام می‌شود.

نکته: دقت کنید که در کد بالا درصورت فراهم نکردن مقداری برای کالچر، از کالچر UI در ثرد جاری (CultureInfo.CurrentUICulture) به عنوان مقدار پیش‌فرض استفاده می‌شود.

کلاس ResourceManager
در زمان اجرا ASP.NET کلید مربوط به منبع موردنظر را با استفاده از کالچر جاری UI انتخاب می‌کند. در قسمت اول این سری مطالب شرح کوتاهی بابت انواع کالچرها داده شد، اما برای توضیحات کاملتر به اینجا مراجعه کنید.
در ASP.NET به صورت پیش‌فرض تمام منابع در زمان اجرا از طریق نمونه‌ای از کلاس ResourceManager در دسترس خواهند بود. به ازای هر نوع Resource که درخواستی برای یک کلید آن ارسال می‌شود یک نمونه از کلاس ResourceManager ساخته می‌شود. در این هنگام (یعنی پس از اولین درخواست به کلیدهای یک منبع) اسمبلی ستلایت مناسب آن پس از یافته شدن (یا تولیدشدن در زمان اجرا) به دامین ASP.NET جاری بارگذاری می‌شود و تا زمانیکه این دامین Unload نشود در حافظه سرور باقی خواهد ماند.

نکته: کلاس ResourceManager تنها توانایی استخراج کلیدهای Resource از اسمبلی‌های ستلایتی (فایل‌های resources. که در قسمت اول به آن‌ها اشاره شد) که در AppDomain جاری بارگذاری شده‌اند را دارد.

کلاس ResourceManager به صورت زیر نمونه سازی می‌شود:
System.Resources.ResourceManager(string baseName, Assembly assemblyName)
پارامتر baseName به نام کامل ریشه اسمبلی اصلی موردنظر(با فضای نام و ...) اما بدون پسوند اسمبلی مربوطه (resources.) اشاره دارد. این نام که برابر نام کلاس نهایی تولیدشده برای منبع موردنظر است همنام با فایل اصلی و پیش‌فرض منبع (فایلی که حاوی عنوان هیچ زبان و کالچری نیست) تولید می‌شود. مثلا برای اسمبلی ستلایت با عنوان MyApplication.MyResource.fa-IR.resources باید از عبارت MyApplication.MyResource استفاده شود.
پارامتر assemblyName نیز به اسمبلی حاوی اسمبلی ستلایت اصلی اشاره دارد. درواقع همان اسمبلی اصلی که نوع داده مربوط به فایل منبع اصلی درون آن embed شده است.
مثلا:
var manager = new System.Resources.ResourceManager("Resources.Resource1", typeof(Resource1).Assembly)
یا
var manager = new System.Resources.ResourceManager("Resources.Resource1", Assembly.LoadFile(@"c:\MyResources\MyGlobalResources.dll"))
 
روش دیگری نیز برای تولید نمونه‌ای از این کلاس وجود دارد که با استفاده از متد استاتیک زیر که در خود کلاس ResourceManager تعریف شده است انجام می‌شود:
public static ResourceManager CreateFileBasedResourceManager(string baseName, string resourceDir, Type usingResourceSet)
در این متد کار استخراج ورودی‌های منابع مستقیما از فایل‌های resources. انجام می‌شود. در اینجا baseName نام فایل اصلی منبع بدون پیشوند resources. است. resourceDir نیز مسیری است که فایل‌های resources. در آن قرار دارند. usingResourceSet نیز نوع کلاس سفارشی سازی شده از ResourceSet برای استفاده به جای کلاس پیش‌فرض است که معمولا مقدار null برای آن وارد می‌شود تا از همان کلاس پیش‌فرض استفاده شود (چون برای بیشتر نیازها همین کلاس پیش‌فرض کفایت می‌کند).
 
نکته: برای تولید فایل resources. از یک فایل resx. میتوان از ابزار resgen همانند زیر استفاده کرد:
resgen d:\MyResources\MyResource.fa.resx

نکته: عملیاتی که درون کلاس ResourceManager انجام می‌شود پیچیده‌تر از آن است که به نظر می‌آید. این عملیات شامل فرایندهای بسیاری شامل بارگذاری کلیدهای مختلف یافته شده و مدیریت ذخیره موقت آن‌ها در حافظه (کش)، کنترل و مدیریت انواع Resource Setها، و مهمتر از همه مدیریت عملیات Fallback و ... که در نهایت شامل هزاران خط کد است که با یک جستجوی ساده قابل مشاهده و بررسی است (^).

نمونه‌سازی مناسب از ResourceManager
در کدهای نشان داده شده در بالا برای پیاده‌سازی پیش‌فرض در ASP.NET، مهمترین نکته همان تولید نمونه مناسب از کلاس ResourceManager است. پس از آماده شدن این کلاس عملیات استخراج ورودی‌های منابع براحتی و با مدیریت کامل انجام می‌شود. اما ازآنجاکه تقریبا تمامی APIهای موردنیاز با سطح دسترسی internal تعریف شده‌اند، متاسفانه تهیه و تولید این نمونه مناسب خارج از اسمبلی System.Web به صورت مستقیم وجود ندارد.
درهرصورت، برای آشنایی بیشتر با فرایند نشان داده شده، تولید این نمونه مناسب و استفاده مستقیم از آن می‌تواند مفید و نیز جالب باشد. پس از کمی تحقیق و با استفاده از Reflection به کدهای زیر رسیدم:
private ResourceManager CreateGlobalResourceManager(string classKey)
{
  var baseName = "Resources." + classKey;
  var buildManagerType = typeof(BuildManager);
  var property = buildManagerType.GetProperty("AppResourcesAssembly", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.GetField);
  var appResourcesAssembly = (Assembly)property.GetValue(null, null);
  return new ResourceManager(baseName, appResourcesAssembly) { IgnoreCase = true };
}
تنها نکته کد فوق دسترسی به اسمبلی منابع کلی در خاصیت AppResourcesAssembly از کلاس BuildManager با استفاده از BindingFlagهای نشان داده شده است.
نحوه استفاده از این متد هم به صورت زیر است:
var manager = CreateGlobalResourceManager("Resource1");
Label1.Text = manager.GetString("String1");
اما برای منابع محلی کار کمی پیچیده‌تر است. کد مربوط به تولید نمونه مناسب از ResourceManager برای منابع محلی به صورت زیر خواهد بود:
private ResourceManager CreateLocalResourceManager(string virtualPath)
{
  var virtualPathType = typeof(BuildManager).Assembly.GetType("System.Web.VirtualPath", true);
  var virtualPathInstance = Activator.CreateInstance(virtualPathType, BindingFlags.NonPublic | BindingFlags.Instance, null, new object[] { virtualPath }, CultureInfo.InvariantCulture);
  var buildResultCompiledAssemblyType = typeof(BuildManager).Assembly.GetType("System.Web.Compilation.BuildResultCompiledAssembly", true);
  var propertyResultAssembly = buildResultCompiledAssemblyType.GetProperty("ResultAssembly", BindingFlags.NonPublic | BindingFlags.Instance);
  var methodGetLocalResourcesAssemblyName = typeof(BuildManager).GetMethod("GetLocalResourcesAssemblyName", BindingFlags.NonPublic | BindingFlags.Static);
  var methodGetBuildResultFromCache = typeof(BuildManager).GetMethod("GetBuildResultFromCache", BindingFlags.NonPublic | BindingFlags.Static, null, new Type[] { typeof(string) }, null);

  var fileNameProperty = virtualPathType.GetProperty("FileName");
  var virtualPathFileName = (string)fileNameProperty.GetValue(virtualPathInstance, null);

  var parentProperty = virtualPathType.GetProperty("Parent");
  var virtualPathParent = parentProperty.GetValue(virtualPathInstance, null);
      
  var localResourceAssemblyName = (string)methodGetLocalResourcesAssemblyName.Invoke(null, new object[] { virtualPathParent });
  var buildResultFromCache = methodGetBuildResultFromCache.Invoke(null, new object[] { localResourceAssemblyName });
  Assembly localResourceAssembly = null;
  if (buildResultFromCache != null)
    localResourceAssembly = (Assembly)propertyResultAssembly.GetValue(buildResultFromCache, null);

  if (localResourceAssembly == null)
    throw new InvalidOperationException("Unable to find the matching resource file.");

  return new ResourceManager(virtualPathFileName, localResourceAssembly) { IgnoreCase = true };
}
ازجمله نکات مهم این متد تولید یک نمونه از کلاس VirtualPath برای Parse کردن مسیر مجازی واردشده برای صفحه درخواستی است. از این کلاس برای بدست آوردن نام فایل منبع محلی به همراه مسیر فولدر مربوطه جهت استخراج اسمبلی متناظر استفاده میشود.
نکته مهم دیگر این کد دسترسی به متد GetLocalResourcesAssemblyName از کلاس BuildManager است که با استفاده از مسیر فولدر مربوط به صفحه درخواستی نام اسمبلی منبع محلی مربوطه را برمی‌گرداند.
درنهایت با استفاده از متد GetBuildResultFromCache از کلاس BuildManager اسمبلی موردنظر بدست می‌آید. همانطور که از نام این متد برمی‌آید این اسمبلی از کش خوانده می‌شود. البته مدیریت این اسمبلی‌ها کاملا توسط BuildManager و سایر ابزارهای موجود در ASP.NET انجام خواهد شد.

نحوه استفاده از متد فوق نیز به صورت زیر است: 
var manager = CreateLocalResourceManager("~/Default.aspx");
Label1.Text = manager.GetString("Label1.Text");
 
نکته: ارائه و شرح کدهای پیاده‌سازی‎‌های پیش‌فرض برای آشنایی با نحوه صحیح سفارشی سازی این کلاس‌ها آورده شده است. پس با دقت بیشتر بر روی این کدها سعی کنید نحوه پیاده‌سازی مناسب را برای سفارشی‌سازی موردنظر خود پیدا کنید.

تا اینجا با مقدمات فرایند تولید پرووایدرهای سفارشی برای استفاده در فرایند بارگذاری ورودی‌های Resourceها آشنا شدیم. در ادامه به بحث تولید پرووایدرهای سفارشی برای استفاده از دیگر انواع منابع (به غیر از فایل‌های resx.) خواهم پرداخت.

منابع:
مطالب
بررسی Source Generators در #C - قسمت پنجم - نوشتن آزمون‌های واحد
تا اینجا روش آزمایش تولید کننده‌های کد، صرفا بر اساس کامپایل برنامه و مشاهده‌ی خروجی نهایی آن بود و یا حتی با ترفندهایی امکان دیباگ آن‌ها نیز وجود دارد که البته هنوز در تمام IDEها پشتیبانی نمی‌شود. در این قسمت می‌خواهیم این وضعیت را بهبود بخشیده و برای تولید کننده‌های کد، آزمون واحد بنویسیم که یکی از مزایای آن، فراهم بودن امکان دیباگ یک چنین پروژه‌هایی در تمام IDEهای موجود است و برای انجام اینکار، نیاز به هیچ ترفند خاصی وجود ندارد و پروسه‌ی کاری آن یکدست و هماهنگ با سایر آزمون‌های واحد است.


آماده سازی مقدمات پروژه‌ی آزمون واحد

در ادامه‌ی مثال این سری، پروژه‌ی جدید NotifyPropertyChangedGenerator.Tests را از نوع class library با تنظیمات فایل csproj. زیر ایجاد می‌کنیم:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <IsPackable>false</IsPackable>
  </PropertyGroup>
    <ItemGroup>
        <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">
            <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
            <PrivateAssets>all</PrivateAssets>
        </PackageReference>
        <PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.2.0" />
        <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" PrivateAssets="all" />

        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
        <PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
        <PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
    </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\NotifyPropertyChangedGenerator\NotifyPropertyChangedGenerator.csproj" />
  </ItemGroup>
</Project>
در اینجا وابستگی‌های مورد نیاز برای دسترسی به امکانات Roslyn و همچنین برای نمونه MSTest را مشاهده می‌کنید. به علاوه مسیر پروژه‌ی Source Generator مورد استفاده به نحو متداولی تعریف شده‌است.


ایجاد یک کلاس کمکی برای اجرای Source Generators در پروژه‌های آزمون واحد

در اینجا می‌خواهیم همان کاری را که کامپایلر سی‌شارپ در پشت صحنه انجام می‌دهد، شبیه سازی کنیم تا بتوانیم یک تولید کننده‌ی کد را به مراحل کامپایل کد، معرفی و سپس آن‌را اجرا کنیم:
internal static class SourceGeneratorTestsExtensions
{
    public static (GeneratorDriver Driver, Compilation OutputCompilation, ImmutableArray<Diagnostic> Diagnostics)
        RunGenerators(this string source, params ISourceGenerator[] generators)
    {
        var references =
            AppDomain.CurrentDomain.GetAssemblies()
                .Where(assembly => !assembly.IsDynamic)
                .Select(assembly => MetadataReference.CreateFromFile(assembly.Location))
                .Cast<MetadataReference>();

        var inputCompilation = CSharpCompilation.Create("compilation",
            new[] { CSharpSyntaxTree.ParseText(source, new CSharpParseOptions(LanguageVersion.Latest)) },
            references,
            new CSharpCompilationOptions(OutputKind.ConsoleApplication));

        GeneratorDriver driver = CSharpGeneratorDriver.Create(generators);
        driver = driver.RunGeneratorsAndUpdateCompilation(
            inputCompilation,
            out var outputCompilation,
            out var diagnostics);

        return (driver, outputCompilation, diagnostics);
    }
}
این متد، یک قطعه کد ابتدایی را دریافت کرده و سپس آن‌را به همراه Source Generatorهای مدنظر، به کامپایلر سی‌شارپ معرفی می‌کند، تا کامپایلر تمام این موارد را در کنار هم پردازش کرده و اسمبلی درون حافظه‌ای را به نام compilation تولید کند. خروجی‌های این متد، اطلاعات غنی هستند از نحوه‌ی کامپایل داده‌های ارسالی به کامپایلر که در ادامه می‌توان از آن‌ها جهت نوشتن آزمون‌های واحد متکی به خودی استفاده کرد.


نوشتن اولین آزمون واحد مخصوص یک تولید کننده‌ی کد

پس از تهیه‌ی متدی که می‌تواند یک قطعه کد و تعدادی Source Generator را به کامپایلر سی‌شارپ، جهت پردازش معرفی کند، یک نمونه نحوه‌ی استفاده‌ی از آن جهت نوشتن آزمون‌های واحد کاملا مستقل و متکی به خود، به صورت زیر است:
using Microsoft.VisualStudio.TestTools.UnitTesting;
using PropertyChangedGenerator = NotifyPropertyChangedGenerator.NotifyPropertyChangedGenerator;

namespace NotifyPropertyChangedGenerator.Tests;

[TestClass]
public class GeneratorTest
{
    [TestMethod]
    public void SimpleGeneratorTest()
    {
        var userSource = @"
using System;
using System.ComponentModel;
namespace NotifyPropertyChangedDemo
{
  public class Test : INotifyPropertyChanged
  {
    private int regularField;
    private int IndexBackingField;
  }
}
";
        var (driver, outputCompilation, diagnostics) =
            userSource.RunGenerators(new PropertyChangedGenerator());

        var newFile = outputCompilation.SyntaxTrees
            .Single(x => Path.GetFileName(x.FilePath).EndsWith(".Test.cs"));

        Assert.IsNotNull(newFile);
        Assert.IsTrue(newFile.FilePath.EndsWith("Test.Notify.Test.cs"));

        var generatedSource = newFile.GetText().ToString();
        Assert.IsTrue(generatedSource.Contains("namespace NotifyPropertyChangedDemo"));

        // We can now assert things about the resulting compilation:
        Assert.IsTrue(diagnostics.IsEmpty); // there were no diagnostics created by the generators
        // we have two syntax trees, the original 'user' provided one, and the one added by the generator
        Assert.IsTrue(outputCompilation.SyntaxTrees.Count() == 2);
        // verify the compilation with the added source has no diagnostics
        Assert.IsTrue(outputCompilation.GetDiagnostics().IsEmpty);
    }
}
 - در این مثال ابتدا یک قطعه کد سی‌شارپ را که قرار است کدهای آن توسط تولید کننده‌ی کد توسعه داده شده تکمیل شوند، تعریف کرده‌ایم.
 - سپس این قطعه کد و نمونه‌ای از تولید کننده‌ی کد را به کامپایلر ارسال و اجرا کرده‌ایم.
 - اکنون بر اساس خروجی کامپایلر برای مثال می‌توان به فایل تولید شده و SyntaxTrees آن دسترسی پیدا کرد و یا با کمک متد GetText، به کل محتوای این فایل تولید شده دسترسی یافت و برای مثال آن‌را با مقداری که انتظار داریم مقایسه کرد تا به این ترتیب بتوان از صحت عملکرد تولید کننده‌ی کد، اطمینان حاصل نمود.
 - همانطور که عنوان شد، اکنون قرار دادن break-point در قسمت‌های مختلف آزمون واحد تهیه شده بسیار ساده‌است و به این ترتیب می‌توان یک چنین پروژه‌هایی را در تمام IDEها دیباگ کرد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: SourceGeneratorTests-part5.zip