نظرات مطالب
چند نکته کاربردی درباره Entity Framework
در کد آپدیت بالا هدف نشان دادن نحوه بروز رسانی یک شی اتچ شده بود که به اشتباه متد آپدیت رو قراردادم. (اصلاح شد)
T attachedEntity = set.Find(entity.Id);
var attachedEntry = dbContext.Entry(attachedEntity);
attachedEntry.CurrentValues.SetValues(entity);

مطالب
مستند سازی ASP.NET Core 2x API توسط OpenAPI Swagger - قسمت چهارم - تکمیل مستندات نوع‌های خروجی API
Swashbuckle.AspNetCore چگونه اطلاعات خود را فراهم می‌کند؟

در برنامه‌های ASP.NET Core، اطلاعات OpenAPI بر اساس سرویس توکاری به نام ApiExplorer تولید می‌شود که کار آن فراهم آوردن متادیتای مرتبط با یک برنامه‌ی وب است. برای مثال توسط این سرویس می‌توان به لیست کنترلرها، متدها و پارامترهای آن‌ها دسترسی یافت. Swashbuckle.AspNetCore به کمک ApiExplorer کار تولید OpenAPI Specification را انجام می‌دهد. برای فعالسازی این سرویس نیازی نیست کار خاصی انجام شود و زمانیکه ()services.AddMvc را فراخوانی می‌کنیم، ثبت و معرفی این سرویس نیز جزئی از آن است.


اهمیت تولید Response Types صحیح

در قسمت‌های قبل مشاهده کردیم که اگر متدی برای مثال در قسمتی از آن return NotFound یا 404 را داشته باشد، این نوع از خروجی، در OpenAPI Specification تولیدی لحاظ نمی‌شود و ناقص است و یا حتی ممکن است Response Type پیش‌فرض تولیدی که 200 است، ارتباطی به هیچکدام از نوع‌های خروجی یک اکشن متد نداشته باشد و نیاز به اصلاح آن است. این مورد برای تکمیل مستندات یک API ضروری است و استفاده کنندگان از یک API باید بدانند چون نوع خروجی‌هایی را ممکن است در شرایط مختلف، دریافت کنند.


روش تغییر و اصلاح Response Type پیش‌فرض OpenAPI Specification

اکشن متد GetBook کنترلر کتاب‌ها، دارای دو نوع return Ok و return NotFound است؛ اما OpenAPI Specification تولیدی پیش‌فرض، تنها حالت return Ok یا 200 را مستند می‌کند. برای تکمیل مستندات این اکشن متد، می‌توان به صورت زیر عمل کرد:
/// <summary>
/// Get a book by id for a specific author
/// </summary>
/// <param name="authorId">The id of the book author</param>
/// <param name="bookId">The id of the book</param>
/// <returns>An ActionResult of type Book</returns>
/// <response code="200">Returns the requested book</response>
[HttpGet("{bookId}")]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<Book>> GetBook(Guid authorId, Guid bookId)
در اینجا با استفاده از ویژگی ProducesResponseType، می‌توان StatusCodes بازگشتی از متد را به صورت صریحی مشخص کرد. وجود این متادیتاها سبب خواهد شد تاOpenAPI Specification تولیدی، به نحو صحیحی حالت 404 را نیز لحاظ کند.
در اینجا StatusCodes.Status400BadRequest را نیز مشاهده می‌کنید. هرچند حالت return BadRequest در کدهای این اکشن متد وجود خارجی ندارد، اما در صورت بروز مشکلی در فراخوانی و یا پردازش آن، به صورت خودکار توسط فریم‌ورک بازگشت داده می‌شود. بنابراین مستندسازی آن نیز ضروری است.

برای آزمایش آن، برنامه را اجرا کنید. در قسمت مستندات متد فوق، اکنون سه حالت 404، 400 و 200 قابل مشاهده هستند. برای نمونه بر روی دکمه‌ی try it out آن کلیک کرده و زمانیکه authorId و bookId را درخواست می‌کند، دو Guid اتفاقی و کاملا بی‌ربط را وارد کنید. همچنین Controls Accept header را نیز بر روی application/json قرار دهید. سپس بر روی دکمه‌ی execute در ذیل آن کلیک نمائید. یک چنین خروجی 404 کاملی را مشاهده خواهید کرد:


در این تصویر، response body بر اساس rfc 7807 تولید می‌شود و استاندارد گزارش مشکلات یک API است. این مورد اکنون به صورت یک اسکیمای جدید در انتهای مستندات تولیدی نیز قابل مشاهده‌است:



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

مورد دیگری که در اینجا جالب توجه است، تشخیص نوع خروجی، در حالت return Ok است:


در اینجا اگر بر روی لینک Schema، بجای Example value پیش‌فرض کلیک کنیم، تصویر فوق حاصل می‌شود. تشخیص این نوع، به علت استفاده‌ی از ActionResult از نوع Book، به صورت زیر است که در ASP.NET Core 2.1 برای همین منظور (تکمیل مستندات Swagger) معرفی شده‌است:
public async Task<ActionResult<Book>> GetBook(Guid authorId, Guid bookId)
بنابراین از ASP.NET Core 2.1 به بعد، بهتر است در APIها خود از IActionResult استفاده نکنید و شروع به کار با <ActionResult<T نمائید تا بتوان مستندات بهتری را تولید کرد. اگر از IActionResult استفاده کنید، دیگر خبری از Example value و Schema تصویر فوق نخواهد بود و از روی متادیتای این اکشن متد نمی‌توان نوع خروجی آن‌را تشخیص داد. البته در این حالت می‌توان این مشکل را به صورت زیر نیز حل کرد؛ اما باز هم بهتر است از <ActionResult<T استفاده کنید:
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Book))]

یک نکته: در این تصویر، در قسمت توضیحات حالت 200، عبارت "Returns the requested book" مشاهده می‌شود. اما در حالت‌های دیگر response types تعریف شده، عبارات پیش‌فرض bad request و یا not found نمایش داده شده‌اند. نحوه‌ی بازنویسی این پیش‌فرض‌ها، با تکمیل مستندات XMLای اکشن متد و ذکر response code دلخواه، به صورت زیر است:
/// <response code="200">Returns the requested book</response>


استفاده از API Analyzers برای بهبود OpenAPI Specification تولیدی

اکنون این سؤال مطرح می‌شود که پس از این تغییرات، هنوز چه مواردی در OpenAPI Specification تولیدی ما وجود خارجی ندارند و بهتر است اضافه شوند. برای پاسخ به این سؤال، از زمان ارائه‌ی ASP.NET Core 2.2، بسته‌ی جدید Microsoft.AspNetCore.Mvc.Api.Analyzers نیز ارائه شده‌است که کار آن دقیقا بررسی همین نقایص و کمبودها است. بنابراین ابتدا آن‌را به فایل OpenAPISwaggerDoc.Web.csproj اضافه کرده و سپس دستور dotnet restore را صادر می‌کنیم:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Api.Analyzers" Version="2.2.0" />
  </ItemGroup>
پس از نصب این ابزار جدید که به صورت افزونه‌ای برای کامپایلر #‍C کار می‌کند، بلافاصله اخطارهایی توسط کامپایلر ظاهر خواهند شد؛ مانند:
Controllers\BooksController.cs(40,17): warning API1000: Action method returns undeclared status code '404'.
Controllers\BooksController.cs(89,13): warning API1000: Action method returns undeclared status code '201'.
همانطور که ملاحظه می‌کنید، هنوز هم نیاز به تعریف تعدادی ProducesResponseType فراموش شده وجود دارد که آن‌ها را به صورت زیر اضافه خواهیم کرد:
/// <summary>
/// Get the books for a specific author
/// </summary>
/// <param name="authorId">The id of the book author</param>
/// <returns>An ActionResult of type IEnumerable of Book</returns>
[HttpGet()]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<IEnumerable<Book>>> GetBooks(Guid authorId)
در ابتدا نوع‌های خروجی اکشن متد GetBooks را تکمیل می‌کنیم که آن نیز دارای return NotFound و همچنین return Ok است. به علاوه در اینجا ویژگی جدید ProducesDefaultResponseType را نیز ملاحظه می‌کنید که یک چنین خروجی را تولید می‌کند:


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


ساده سازی کدهای تکراری تعریف ProducesResponseTypeها

مواردی مانند StatusCodes.Status400BadRequest و یا 406 را در حالت عدم قبول درخواست (مثلا با انتخاب یک accept header اشتباه) و یا 500 را در صورت وجود استثنائی در سمت سرور، باید به تمام اکشن متدها نیز اضافه کرد؛ چون می‌توانند تحت شرایطی، نوع‌های خروجی معتبری باشند. برای خلاصه سازی این عملیات، یا می‌توان این ویژگی‌ها را بجای قراردادن آن‌ها در بالای تعریف امضای اکشن متدها، به بالای تعریف کلاس کنترلر انتقال داد. با اینکار ویژگی که به یک کنترلر اعمال شده باشد به تمام اکشن متدهای آن نیز اعمال خواهد شد و یا حتی برای عدم تعریف این ویژگی‌های تکراری به ازای هر کنترلر موجود، می‌توان آن‌ها را به صورت سراسری تعریف کرد:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc(setupAction =>
            {
                setupAction.Filters.Add(
                    new ProducesResponseTypeAttribute(StatusCodes.Status400BadRequest));
                setupAction.Filters.Add(
                    new ProducesResponseTypeAttribute(StatusCodes.Status406NotAcceptable));
                setupAction.Filters.Add(
                    new ProducesResponseTypeAttribute(StatusCodes.Status500InternalServerError));
                setupAction.Filters.Add(
                    new ProducesDefaultResponseTypeAttribute());

                setupAction.ReturnHttpNotAcceptable = true;
// ...
به این ترتیب یکسری از نوع‌های خروجی که ممکن است توسط خود فریم‌ورک به صورت خودکار بازگشت داده شوند، به صورت سراسری به تمام اکشن متدهای برنامه اعمال می‌شوند و دیگر نیازی به تعریف دستی آن‌ها نخواهد بود.



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

در قسمت بعد، روش‌های دیگری را برای تکمیل مستندات خروجی API بررسی می‌کنیم.
مطالب
خلاصه‌ای کاربردی در مورد Observable collection

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



اما استفاده‌ی پیشرفته از این لیست جنریک ویژه به همینجا ختم نشده و حین اضافه کردن کمی پیچیدگی به برنامه مشکلات عدیده‌ای بروز می‌کنند که آن‌ها را جهت دسترسی ساده‌ی بعدی در زیر لیست می‌کنم:

الف) اصلا Observable collection چیست؟ چکار می‌کند؟
List vs ObservableCollection vs INotifyPropertyChanged in Silverlight

ب) نمی‌توانم از این مجموعه‌ی اشیای خودآگاه سازنده در یک ترد استفاده کنم. مشکل کجاست؟
این روزها نمی‌توان یک برنامه‌ی دسکتاپ خوب را بدون استفاده از تردها متصور شد. اما به محض سعی در به روز رسانی این لیست جنریک در یک ترد دیگر (ترد دیگر منظور هر تردی بجز ترد اصلی برنامه است که کار مدیریت رابط کاربر را به عهده دارد) خطای زیر ظاهر می‌شود:
This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread

راه حل:
Adding to an ObservableCollection from a background thread

ج) یکی از خاصیت‌های یک شیء این لیست جنریک ویژه را تغییر داده‌ام. اما هیچ تغییری در کنترل بایند شده به آن مشاهده نمی‌کنم. مشکل در کجاست؟
راه حل: پیاده سازی اینترفیس INotifyPropertyChanged را فراموش کرده‌اید:
Data Binding in WPF with the Monostate Pattern

د) خوب، این که خیلی دردسر دارد! راه ساده‌تری برای تعریف این موارد نیست؟!
هوشمندانه‌ترین روشی که برای حل این مساله تابحال دیده‌ام:
An easier way to manage INotifyPropertyChanged

ه) زمانیکه این یک لیست جنریک خودآگاه سازنده را به یک مثلا listview بایند می‌کنم، دیگر نمی‌توانم با استفاده از متد clear items آن کنترل، نسبت به خالی کردن نمای ظاهری آن اقدام کنم. چکار باید کرد؟
خطای مشاهده شده:
Operation is not valid while ItemsSource is in use. Access and modify elements with ItemsControl.ItemsSource instead

راه حل:
همان Observable collection اصلی را تخلیه کنید، UI به صورت خودکار به روز خواهد شد.

و) اضافه کردن رنجی از اطلاعات به آن به صورتی یکباره ممکن است کند باشد. چه باید کرد؟
راه حل:
AddRange for ObservableCollection in Silverlight 3


مطالب
تبدیل HTML به PDF با استفاده از کتابخانه‌ی iTextSharp

روش متداول کار با کتابخانه‌ی iTextSharp ، ایجاد شیء Document ، سپس ایجاد PdfWriter برای نوشتن در آن، گشودن سند و ... افزودن اشیایی مانند Paragraph ، PdfPTable ، PdfPCell و غیره به آن است و در نهایت بستن سند. راه میانبری هم برای کار با این کتابخانه وجود دارد و آن هم استفاده از امکانات فضای نام iTextSharp.text.html.simpleparser آن می‌باشد. به این ترتیب می‌توان به صورت خودکار، یک محتوای HTML را تبدیل به فایل PDF کرد.

مثال : نمایش یک متن HTML ساده انگلیسی
using System.Diagnostics;

using System.IO;
using iTextSharp.text;
using iTextSharp.text.html.simpleparser;
using iTextSharp.text.pdf;

namespace HeadersAndFooters
{
class Program
{
static void Main(string[] args)
{
using (var pdfDoc = new Document(PageSize.A4))
{
PdfWriter.GetInstance(pdfDoc, new FileStream("Test.pdf", FileMode.Create));
pdfDoc.Open();

var html = @"<span style='color:blue'><b>Testing</b></span>
<i>iTextSharp's</i> <u>HTML to PDF capabilities</u>";
var parsedHtmlElements = HTMLWorker.ParseToList(new StringReader(html), null);

foreach (var htmlElement in parsedHtmlElements)
{
pdfDoc.Add(htmlElement);
}
}

//open the final file with adobe reader for instance.
Process.Start("Test.pdf");
}
}
}


نکته‌ی جدید کد فوق، استفاده از متد HTMLWorker.ParseToList است. به این ترتیب parser کتابخانه‌ی iTextSharp وارد عمل شده و html تعریف شده را به معادل المان‌های بومی خودش تبدیل می‌کند؛ مثلا تبدیل به chunk یا pdfptable و امثال آن. در نهایت در طی یک حلقه، این عناصر به صفحه اضافه می‌شوند.
البته باید دقت داشت که HTMLWorker امکان تبدیل عناصر پیچیده، تودرتو و چندلایه HTML را ندارد؛ اما بهتر از هیچی است!

همه‌ی این‌ها خوب! اما به درد ما فارسی زبان‌ها نمی‌خورد. همین متغیر html فوق را با یک متن فارسی جایگزین کنید، چیزی نمایش داده نخواهد شد. البته این هم نکته دارد که در ادامه ذکر خواهد شد.
جهت نمایش متون فارسی نیاز است تا نکات ذکر شده در مطلب «فارسی نویسی و iTextSharp» رعایت شوند که شامل:
- تعیین صریح قلم
- تعیین encoding
- استفاده از عناصر دربرگیرنده‌ای است که خاصیت RunDirection را پشتیبانی می‌کنند؛ مانند PdfPCell و غیره


به این ترتیب خواهیم داشت:
using System.Diagnostics;

using System.IO;
using iTextSharp.text;
using iTextSharp.text.html.simpleparser;
using iTextSharp.text.pdf;
using iTextSharp.text.html;

namespace HeadersAndFooters
{
class Program
{
static void Main(string[] args)
{
using (var pdfDoc = new Document(PageSize.A4))
{
PdfWriter.GetInstance(pdfDoc, new FileStream("Test.pdf", FileMode.Create));
pdfDoc.Open();

//روش صحیح تعریف فونت
FontFactory.Register("c:\\windows\\fonts\\tahoma.ttf");

StyleSheet styles = new StyleSheet();
styles.LoadTagStyle(HtmlTags.BODY, HtmlTags.FONTFAMILY, "tahoma");
styles.LoadTagStyle(HtmlTags.BODY, HtmlTags.ENCODING, "Identity-H");

var html = @"<span style='color:blue'><b>آزمایش</b></span>
کتابخانه <i>iTextSharp</i> <u>جهت بررسی فارسی نویسی</u>";
var parsedHtmlElements = HTMLWorker.ParseToList(new StringReader(html), styles);

PdfPCell pdfCell = new PdfPCell { Border = 0 };
pdfCell.RunDirection = PdfWriter.RUN_DIRECTION_RTL;

foreach (var htmlElement in parsedHtmlElements)
{
pdfCell.AddElement(htmlElement);
}

var table1 = new PdfPTable(1);
table1.AddCell(pdfCell);
pdfDoc.Add(table1);
}

//open the final file with adobe reader for instance.
Process.Start("Test.pdf");
}
}
}

همانطور که ملاحظه می‌کنید ابتدا قلمی در cache قلم‌های این کتابخانه ثبت می‌شود (FontFactory.Register). سپس نوع قلم و encoding آن توسط یک StyleSheet تعریف شده و به HTMLWorker.ParseToList ارسال می‌گردد و در نهایت به کمک یک المان دارای RunDirection، در صفحه نمایش داده می‌شود.



نکته:
ممکن است که به متغیر html ، یک table ساده html را نسبت دهید. در این حالت پس از تنظیم style یاد شده، در هر سلول این html table ، متون فارسی به صورت معکوس نمایش داده خواهند شد که این هم یک نکته‌ی کوچک دیگر دارد:

foreach (var htmlElement in parsedHtmlElements)

{
if (htmlElement is PdfPTable)
{
var table = (PdfPTable)htmlElement;
table.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
foreach (var row in table.Rows)
{
foreach (var cell in row.GetCells())
{
cell.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
}
}
}

pdfCell.AddElement(htmlElement);
}

در قسمتی که قرار است المان‌های معادل به pdfCell اضافه شوند، آن‌ها را بررسی کرده و RunDirection آن‌ها را RTL خواهیم کرد.


کاربردها:
بدیهی است این حالت برای تهیه گزارشات پیشرفته‌تر برای مثال تهیه قالب‌هایی که در حین تهیه PDF ، قسمت‌هایی از آن‌ها توسط برنامه نویس Replace می‌شوند، بسیار مناسب است.
همچنین مطلب «بارگذاری یک یوزرکنترل با استفاده از جی‌کوئری» و متد RenderUserControl مطرح شده در آن که در نهایت یک قطعه کد HTML را به صورت رشته به ما تحویل می‌دهد، می‌تواند جهت تهیه گزارش‌های پویایی که برای مثال قسمتی از آن یک GridView بایند شده حاصل از یک یوزر کنترل است،‌ مورد استفاده قرار گیرد.


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

1) دریافت کتابخانه‌های لازم
نیاز به کتابخانه‌های Lucene.NET و همچنین Lucene.Net Contrib است که هر دو مورد را به سادگی توسط NuGet می‌توانید دریافت و نصب کنید.
Highlighter استفاده شده، در کتابخانه Lucene.Net Contrib قرار دارد. به همین جهت این مورد را نیز باید جداگانه دریافت کرد.


2) تهیه منبع داده
در اینجا جهت سادگی کار فرض کنید که لیستی از مطالب را به فرمت زیر دراختیار داریم:
public class Post
{
    public int Id { set; get; }
    public string Title { set; get; }
    public string Body { set; get; }
}
تفاوتی نمی‌کند که از چه منبع داده‌ای استفاده می‌کنید. آیا قرار است یک سری فایل متنی ساده موجود در یک پوشه را ایندکس کنید یا تعدادی رکورد بانک اطلاعاتی؛ از NHibernate استفاده می‌کنید یا از Entity framework و یا از ADO.NET. کتابخانه Lucene مستقل است از منبع داده مورد استفاده و تنها اطلاعاتی با فرمت شیء Document معرفی شده به آن‌را می‌شناسد.


3) تبدیل اطلاعات به فرمت Lucene.NET
همانطور که عنوان شد نیاز است هر رکورد از اطلاعات خود را به شیء Document نگاشت کنیم. نمونه‌ای از اینکار را در متد ذیل مشاهده می‌نمائید:
static Document MapPostToDocument(Post post)
{
    var postDocument = new Document();
    postDocument.Add(new Field("Id", post.Id.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED));
    postDocument.Add(new Field("Title", post.Title, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS));
    postDocument.Add(new Field("Body", post.Body, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS));
    return postDocument;
}
این متد وهله‌ای از شیء Post را دریافت کرده و آن‌را تبدیل به یک سند Lucene می‌کند.
کار با ایجاد یک وهله از شیء Document شروع شده و سپس اطلاعات به صوت فیلدهایی به این سند اضافه می‌شوند.

توضیحات آرگومان‌های مختلف سازنده کلاس Field:
- در ابتدا نام فیلد مورد نظر ذکر می‌گردد.
- سپس مقدار متناظر با آن فیلد، به صورت رشته باید معرفی شود.
- آرگومان سوم آن مشخص می‌کند که اصل اطلاعات نیز علاوه بر ایندکس شدن باید در فایل‌های Lucene ذخیره شوند یا خیر. توسط Field.Store.YES مشخص می‌کنیم که بله؛ علاقمندیم تا اصل اطلاعات نیز از طریق Lucene قابل بازیابی باشند. این مورد جهت نمایش سریع نتایج جستجوها می‌تواند مفید باشد. اگر قرار نیست اطلاعاتی را از این فیلد خاص به کاربر نمایش دهید می‌توانید از گزینه Field.Store.NO استفاده کنید. همچنین امکان فشرده سازی اطلاعات ذخیره شده با انتخاب گزینه Field.Store.COMPRESS نیز میسر است.
- توسط آرگومان چهارم آن تعیین خواهیم کرد که اطلاعات فیلد مورد نظر ایندکس شوند یا خیر. مقدار Field.Index.NOT_ANALYZED سبب عدم ایندکس شدن فیلد Id می‌شوند (چون قرار نیست روی id در قسمت جستجوی عمومی سایت، جستجویی صورت گیرد). به کمک مقدار Field.Index.ANALYZED، مقدار معرفی شده، ایندکس خواهد شد.
- پارامتر پنجم آن‌را جهت سرعت عمل در نمایان سازی/برجسته کردن و highlighting عبارات جستجو شده در متن‌های یافت شده معرفی کرده‌ایم. الگوریتم‌های متناظر با این روش در فایل‌های Lucene.Net Contrib قرار دارند.


یک نکته
اگر اطلاعاتی را که قرار است ایندکس کنید از نوع HTML می‌باشند، بهتر است تمام تگ‌های آن‌را پیش از افزودن به لوسین حذف کنید. به این ترتیب نتایج جستجوی دقیق‌تری را می‌توان شاهد بود. برای این منظور می‌توان از متد ذیل کمک گرفت:
public static string RemoveHtmlTags(string text)
{
    return string.IsNullOrEmpty(text) ? string.Empty : Regex.Replace(text, @"<(.|\n)*?>", string.Empty);
}


4) تهیه Full text index به کمک Lucene.NET
تا اینجا توانستیم اطلاعات خود را به فرمت اسناد لوسین تبدیل کنیم. اکنون ثبت و تبدیل آن‌ها به فایل‌های Full text search لوسین به سادگی زیر است:
static readonly Lucene.Net.Util.Version _version = Lucene.Net.Util.Version.LUCENE_29;
public static void CreateIdx(IEnumerable<Post> dataList)
{
    var directory = FSDirectory.Open(new DirectoryInfo(Environment.CurrentDirectory + "\\LuceneIndex"));
    var analyzer = new StandardAnalyzer(_version);
    using (var writer = new IndexWriter(directory, analyzer, create: true, mfl: IndexWriter.MaxFieldLength.UNLIMITED))
    {
         foreach (var post in dataList)
        {
            writer.AddDocument(MapPostToDocument(post));
        }

        writer.Optimize();
        writer.Commit();
        writer.Close();
        directory.Close();
    }
}
ابتدا محل ذخیره سازی فایل‌های full text search مشخص می‌شوند. سپس آنالیز کننده اطلاعات باید معرفی شود. در ادامه به کمک این اطلاعات، شیء IndexWriter ایجاد و مستندات لوسین به آن اضافه می‌شوند. در آخر، این اطلاعات بهینه سازی شده و ثبت نهایی صورت خواهد گرفت.
ذکر version در اینجا ضروری است؛ از این جهت که اگر ایندکسی با فرمت مثلا LUCENE_29 تهیه شود ممکن است با نگارش بعدی این کتابخانه سازگار نباشد و در صورت ارتقاء، نتایج جستجوی انجام شده، کاملا بی‌ربط نمایش داده شوند. با ذکر صریح نگارش، دیگر این اتفاق رخ نخواهد داد.


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


5) به روز رسانی ایندکس‌ها
به کمک سه متد ذیل می‌توان اطلاعات ایندکس‌های موجود را به روز یا حذف کرد:
public static void UpdateIndex(Post post)
{
        var directory = FSDirectory.Open(new DirectoryInfo(Environment.CurrentDirectory + "\\LuceneIndex"));
        var analyzer = new StandardAnalyzer(_version);
        using (var indexWriter = new IndexWriter(directory, analyzer, create: false, mfl: IndexWriter.MaxFieldLength.UNLIMITED))
        {
            var newDoc = MapPostToDocument(post);

             indexWriter.UpdateDocument(new Term("Id", post.Id.ToString()), newDoc);
             indexWriter.Commit();
             indexWriter.Close();
             directory.Close();
         }
}

public static void DeleteIndex(Post post)
{
         var directory = FSDirectory.Open(new DirectoryInfo(Environment.CurrentDirectory + "\\LuceneIndex"));
         var analyzer = new StandardAnalyzer(_version);
         using (var indexWriter = new IndexWriter(directory, analyzer, create: false, mfl: IndexWriter.MaxFieldLength.UNLIMITED))
         {
             indexWriter.DeleteDocuments(new Term("Id", post.Id.ToString()));
             indexWriter.Commit();
             indexWriter.Close();
             directory.Close();
          }
}

public static void AddIndex(Post post)
{
      var directory = FSDirectory.Open(new DirectoryInfo(Environment.CurrentDirectory + "\\LuceneIndex"));
      var analyzer = new StandardAnalyzer(_version, getStopWords());
      using (var indexWriter = new IndexWriter(directory, analyzer, create: false, mfl: IndexWriter.MaxFieldLength.UNLIMITED))
      {
           var searchQuery = new TermQuery(new Term("Id", post.Id.ToString()));
           indexWriter.DeleteDocuments(searchQuery);

            var newDoc = MapPostToDocument(post);
            indexWriter.AddDocument(newDoc);
            indexWriter.Commit();
            indexWriter.Close();
            directory.Close();
        }
}
تنها نکته مهم این متدها، استفاده از متد IndexWriter با پارامتر create مساوی false است. به این ترتیب فایل‌های موجود بجای از نو ساخته شدن، به روز خواهند شد.
محل فراخوانی این متدها هم می‌تواند در کنار متدهای به روز رسانی اطلاعات اصلی در بانک اطلاعاتی برنامه باشند. اگر رکوردی اضافه یا حذف شده، ایندکس متناظر نیز باید به روز شود.


6) جستجو در اطلاعات ایندکس شده و نمایش آن‌ها به همراه نمایان/برجسته سازی عبارات جستجو شده
قسمت نهایی کار با لوسین و اطلاعات ایندکس‌های تهیه شده، کوئری گرفتن از آن‌ها است. متدهای کامل مورد نیاز را در ذیل مشاهده می‌کنید:

public static void Query(string term)
{
     var directory = FSDirectory.Open(new DirectoryInfo(Environment.CurrentDirectory + "\\LuceneIndex"));
     using (var searcher = new IndexSearcher(directory, readOnly: true))
     {
          var analyzer = new StandardAnalyzer(_version);
          var parser = new MultiFieldQueryParser(_version, new[] { "Body", "Title" }, analyzer);
          var query = parseQuery(term, parser);
          var hits = searcher.Search(query, 10).ScoreDocs;

          if (hits.Length == 0)
          {
               term = searchByPartialWords(term);
               query = parseQuery(term, parser);
               hits = searcher.Search(query, 10).ScoreDocs;
           }

           FastVectorHighlighter fvHighlighter = new FastVectorHighlighter(true, true);
           foreach (var scoreDoc in hits)
           {
               var doc = searcher.Doc(scoreDoc.doc);
               string bestfragment = fvHighlighter.GetBestFragment(
                                fvHighlighter.GetFieldQuery(query),
                                searcher.GetIndexReader(),
                                docId: scoreDoc.doc,
                                fieldName: "Body",
                                fragCharSize: 400);
                var id = doc.Get("Id");
                var title = doc.Get("Title");
                var score = scoreDoc.score;
                Console.WriteLine(bestfragment);
            }

            searcher.Close();
            directory.Close();
      }
   }

   private static Query parseQuery(string searchQuery, QueryParser parser)
   {
       Query query;
        try
        {
            query = parser.Parse(searchQuery.Trim());
        }
        catch (ParseException)
        {
            query = parser.Parse(QueryParser.Escape(searchQuery.Trim()));
        }
        return query;
   }

   private static string searchByPartialWords(string bodyTerm)
   {
       bodyTerm = bodyTerm.Replace("*", "").Replace("?", "");
       var terms = bodyTerm.Trim().Replace("-", " ").Split(' ')
                                .Where(x => !string.IsNullOrEmpty(x))
                                .Select(x => x.Trim() + "*");
       bodyTerm = string.Join(" ", terms);
       return bodyTerm;
   }
توضیحات:
اکثر سایت‌ها را که بررسی کنید، جستجوی بر روی یک فیلد را توضیح داده‌اند. در اینجا نحوه جستجو بر روی چند فیلد را به کمک MultiFieldQueryParser ملاحظه می‌کنید.
نکته‌ی مهمی را هم که در اینجا باید به آن دقت داشت، حساس بودن لوسین به کوچکی و بزرگی نام فیلدهای معرفی شده است و در صورت عدم رعایت این مساله، جستجوی شما نتیجه‌ای را دربر نخواهد داشت.
در ادامه برای parse اطلاعات، از متد کمکی parseQuery استفاده شده است. ممکن است به ParseException بخاطر یک سری حروف خاص بکارگرفته شده در عبارات مورد جستجو برسیم. در اینجا می‌توان توسط متد QueryParser.Escape، اطلاعات دریافتی را اصلاح کرد.
سپس نحوه استفاده از کوئری تهیه شده و متد Search را ملاحظه می‌کنید. در اینجا بهتر است تعداد رکوردهای بازگشت داده شده را تعیین کرد (به کمک آرگومان دوم متد جستجو) تا بی‌جهت سرعت عملیات را پایین نیاورده و همچنین مصرف حافظه سیستم را نیز بالا نبریم.
ممکن است تعداد hits یا نتایج حاصل صفر باشد؛ بنابراین بد نیست خودمان دست به کار شده و به کمک متد searchByPartialWords، ورودی کاربر را بر اساس زبان جستجوی ویژه لوسین اندکی بهینه کنیم تا بتوان به نتایج بهتری دست یافت.
در آخر نحوه کار با  ScoreDocs یافت شده را ملاحظه می‌کنید. اگر محتوای فیلد را در حین ایندکس سازی ذخیره کرده باشیم، به کمک متد doc.Get می‌توان به اطلاعات کامل آن نیز دست یافت.
همچنین نکته دیگری را که در اینجا می‌توان ملاحظه کرد استفاده از FastVectorHighlighter می‌باشد. به کمک این Highlighter ویژه می‌توان نتایج جستجو را شبیه به نتایج نمایش داده شده توسط موتور جستجوی گوگل درآورد. برای مثال اگر شخصی ef code first را جستجو کرد، توسط متد GetBestFragment، بهترین جزئی که شامل بیشترین تعداد حروف جستجو شده است، یافت گردیده و همچنین به کمک تگ‌های B، ضخیم نمایش داده خواهند شد.

 
مطالب
Functional Programming یا برنامه نویسی تابعی - قسمت اول
 آشنایی

این قسمت از مقاله به ایده اصلی برنامه نویسی تابعی و دلیل وجودی آن خواهد پرداخت. هیچ شکی نیست که بزرگترین چالش در توسعه نرم افزار‌های بزرگ، پیچیدگی آن است. تغییرات همیش اجتناب ناپذیر هستند. به خصوص زمانی که صحبت از پیاده سازی امکان جدیدی باشد، پیچیدگی اضافه خواهد شد. در نتیجه منجر به سخت شدن فهمیدن کد می‌شود، زمان توسعه را بالاتر می‌برد و باگ‌های ناخواسته را به وجود خواهد آورد. همچنین تغییر هر چیزی در دنیای نرم افزار بدون به وجود آوردن رفتار‌های ناخواسته و یا اثرات جانبی، تقریبا غیر ممکن است. در نهایت همه این موارد می‌توانند سرعت توسعه را پایین برده و حتی باعث شکست پروژه‌های نرم افزاری شوند. سبک‌های کد نویسی دستوری (Imperative) مانند برنامه نویسی شیء گرا، میتوانند به کاهش این پیچیدگی‌ها تا حد خوبی کمک کنند. البته در صورتیکه به طور صحیحی پیاده شوند. در واقع با ایجاد Abstraction در این مدل برنامه نویسی، پیچیدگی‌ها را مخفی میکنیم.


سیر تکاملی الگو‌های برنامه نویسی


برنامه نویسی شیء گرا در خون برنامه نویس‌های سی شارپ جاری است؛ ما معمولا ساعت‌ها درباره اینکه چگونه میتوانیم با استفاده از ارث بری و ترتیب پیاده کلاس‌ها، یک هدف خاص برسیم، بر روی کپسوله سازی تمرکز میکنیم و انتزاع (Abstraction) و چند ریختی ( Polymorphism ) را برای تغییر وضعیت برنامه استفاده میکنیم. در این مدل همیشه احتمال این وجود دارد که چند ترد به صورت همزمان به یک ناحیه از حافظه دسترسی داشته باشند و تغییری در آن به وجود بیاورند و باعث به وجود آمدن شرایط Race Condition شوند. البته همگی به خوبی میدانیم که میتوانیم یک برنامه‌ی کاملا Thread-Safe هم داشته باشیم که به خوبی مباحث همزمانی و همروندی را مدیریت کند؛ اما یک مساله اساسی در مورد کارآیی باقی می‌ماند. گرچه Parallelism به ما کمک میکند که کارآیی برنامه خود را افزایش دهیم، اما refactor کردن کد‌های موجود، به حالت موازی، کاری سخت و پردردسر خواهد بود.


راهکار چیست؟

برنامه نویسی تابعی، یک الگوی برنامه نویسی است که از یک ایده قدیمی (قبل از اولین کامپیوتر‌ها !) برگرفته شده‌است؛ زمانیکه دو ریاضیدان، یک تئوری به نام  lambda calculus را معرفی کردند، که یک چارچوب محاسباتی می‌باشد؛ عملیاتی ریاضی را انجام می‌دهد و نتیجه را محاسبه میکند، بدون اینکه تغییری را در وضعیت داده‌ها و وضعیت، به وجود بیاورد. با این کار، فهمیدن کد‌ها آسانتر خواهد بود و اثرات جانبی را کمتر خواهد کرد، همچین نوشتن تست‌ها ساده‌تر خواهند شد.


زبان‌های تابعی

جالب است اگر زبان‌های برنامه نویسی را که از برنامه نویسی تابعی پشتیبانی میکنند، بررسی کنیم، مانند Lisp , Clojure, Erlang, Haskel، هر کدام از این زبان‌ها جنبه‌های مختلفی از برنامه نویسی تابعی را پوشش میدهند. #F یک عضو از خانواده ML می‌باشد که بر روی دات نت فریمورک در سال 2002 پیاده سازی شده. ولی جالب است بدانید بیشتر زبان‌های همه کاره مانند #C به اندازه کافی انعطاف پذیر هستند تا بتوان الگوهای مختلفی را توسط آن‌ها پیاده کرد. از آنجایی که اکثرا ما از #C برای توسعه نرم افزارهایمان استفاده میکنیم، ترکیب ایده‌های برنامه نویسی تابعی میتواند راهکار جالبی برای حل مشکلات ما باشد.


مفاهیم پایه ای

قبلا درباره توابع ریاضی صحت کردیم. در زبان‌های برنامه نویسی هم ایده همان است؛ ورودی‌های مشخص و خروجی مورد انتظار، بدون تغییری در حالت برنامه. به این مفاهیم شفافیت و صداقت توابع میگوییم که در ادامه با آن بیشتر آشنا میشویم. به این نکته توجه داشته باشید که منظور از تابع در #C فقط Method نیست؛ Func , Action , Delegate هم نوعی تابع هستند.


شفافیت توابع (Referential Transparency)

به طور ساده با نگاه کردن به ورودی‌های تابع و نام آن‌ها باید بتوانیم کاری را که انجام میدهد، حدس بزنیم. یعنی یک تابع باید بر اساس ورودی‌های آن کاری را انجام دهد و نباید یک پارامتر Global آن را تحت تاثیر قرار دهد. پارامتر‌های Global میتوانند یک Property در سطح یک کلاس باشند، یا یک شیء که وضعیت آن تحت کنترل تابع نیست؛ مانند شی DateTime. به مثال زیر توجه کنید:
public int CalculateElapsedDays(DateTime from)
{
   DateTime now = DateTime.Now;
   return (now - from).Days;
}
این تابع شفاف نیست. چرا؟ چون امروز، یک خروجی را میدهد و فردا یک خروجی دیگر را! به بیان دیگر وابسته به یک شیء سراسری DateTime.Now است.
آیا میتوانید این تابع را شفاف کنیم؟ بله!
چطور؟ به سادگی! با تغییر پارامتر‌های ورودی:
 public static int CalculateElapsedDays(DateTime from, DateTime now) => (now - from).Days;
در مثال بالا، ما وابستگی به یک شیء سراسری را از بین بردیم.


صداقت توابع (Function Honesty)

صداقت یک تابع یعنی یک تابع باید همه اطلاعات مربوط به ورودی‌ها و خروجی‌ها را پوشش دهد. به این مثال دقت کنید:
public int Divide(int numerator, int denominator)
{
   return numerator / denominator;
}
آیا این تابع شفاف است؟ بله.
آیا این همه مواردی را که از آن انتظار داریم پوشش میدهد؟ احتمالا خیر!

اگر دو عدد صحیح را به این تابع بفرستیم، احتمالا مشکلی پیش نخواهد آمد. اما همانطور که حدس میزنید اگر پارامتر دوم 0 باشد چه اتفاقی خواهد افتاد؟
var result = Divide(1,0);
قطعا خطای Divide By Zero را خواهیم گرفت. امضای این تابع به ما اطلاعاتی درباره خطاهای احتمالی نمی‌دهد.

چگونه مشکل را حل کنیم؟ تایپ ورودی را به شکل زیر تغییر دهیم:
public static int Divide(int numerator, NonZeroInt denominator)
{
   return numerator / denominator.Value;
}
NonZeroInt یک نوع ورودی اختصاصی است که خودمان طراحی کرده‌ایم که تمام مقادیر را به جز صفر، قبول میکند.

به طور کلی تمرین زیادی لازم داریم تا بتوانیم با این مفاهیم به طور عمیق آشنا شویم. در این مقاله قصد دارم جنبه‌های ابتدایی برنامه نویسی تابعی مانند  Functions as first class values ، High Order Functions و Pure Functions را مورد بررسی قرار دهم.

Functions as first-class values

ترجمه فارسی این کلمه ما را از معنی اصلی آن خیلی دور می‌کند؛ احتمالا یک ترجمه ساد‌ه‌ی آم میتواند «تابع، ارزش اولیه کلاس» باشد!
وقتی توابع first-class values باشند، یعنی می‌توانند به عنوان ورودی سایر توابع استفاده شوند، می‌توانند به یک متغیر انتساب داده شوند، دقیقا مثل یک مقدار. برای مثال:
Func<int, bool> isMod2 = x => x % 2 == 0;
var list = Enumerable.Range(1, 10);
var evenNumbers = list.Where(isMod2);
در این مثال، تابع، First-class value می‌باشد؛ چون شما می‌توانید آن را به یک متغیر نسبت دهید و به عنوان ورودی به تابع بعدی بدهید. در مدل برنامه نویسی تابعی، تلقی شدن توابع به عنوان مقدار، ضروری است. چون به ما امکان تعریف توابع High-Order را میدهد.


High-Order Functions (HOF)

توابع مرتبه بالا! یک یا چند تابع را به عنوان ورودی می‌گیرند و یک تابع را به عنوان نتیجه بر میگرداند. در مثال بالا Extension Method ، Where یک تابع High-Order می‌باشد.
پیاده سازی Where احتمالا به شکل زیر می‌باشد:
public static IEnumerable<T> Where<T>(this IEnumerable<T> ts, Func<T, bool> predicate)
{
   foreach (T t in ts)
      if (predicate(t))
         yield return t;
}
1. وظیفه چرخیدن روی آیتم‌های لیست، مربوط به Where می‌باشد.
2. ملاک تشخیص اینکه چه آیتم‌هایی در لیست باید وجود داشته باشند، به عهده متدی می‌باشد که آن را فراخوانی میکند.

در این مثال، تابع Where، تابع ورودی را به ازای هر المان، در لیست فراخوانی میکند. این تابع می‌تواند طوری طراحی شود که تابع ورودی را به صورت شرطی اعمال کند. آزمایش این حالت به عهده شما می‌باشد. اما به صورت کلی انتظار می‌رود که قدرت توابع High-Order را درک کرده باشید.


Pure Functions

توابع خالص در واقع توابع ریاضی هستند که دو مفهوم ابتدایی که قبلا درباره آن‌ها صحبت کردیم را دنبال می‌کنند؛ شفافیت و صداقت توابع. توابع خالص نباید هیچوقت اثر جانبی (side effect) ای داشته باشند. این یعنی نباید یک global state را تغییر دهند و یا از آن‌ها به عنوان پارامتر ورودی استفاده کنند. توابع خالص به راحتی قابل تست شدن هستند. چون به ازای یک ورودی، یک خروجی ثابت را بر میگردانند. ترتیب محاسبات اهمیتی ندارد! این‌ها بازیگران اصلی یک برنامه تابعی می‌باشد که می‌توانند برای اجرای موازی، محاسبه متاخر ( Lazy Evaluation ) و کش کردن ( memoization ) استفاده شوند.

در ادامه این سری مقالات، به پیاده سازی‌ها و الگوهای رایج برنامه نویسی تابعی با #C بیشتر خواهیم پرداخت.
مطالب دوره‌ها
تزریق وابستگی‌های AutoMapper در لایه سرویس برنامه
اگر مطلب «Refactoring به تزریق وابستگی‌ها» را به خاطر داشته باشید، جهت تشخیص وابستگی‌های یک کلاس، کار از بررسی کلمات new و همچنین فراخوانی‌های استاتیک، شروع می‌شود و ... متد استاتیک Mapper.Map کتابخانه‌ی AutoMapper نیز از همین دست است. در ادامه قصد داریم بجای فراخوانی مستقیم Mapper.Map از اینترفیس IMappingEngine به عنوان تامین کننده‌ی متد Map استفاده کنیم. همچنین کلاس‌های Profile نوشته شده را نیز به صورت خودکار به برنامه اضافه نمائیم.


تنظیمات IoC Container مختص به AutoMapper

در ذیل یک کلاس Registry مخصوص StructureMap را مشاهده می‌کنید که جهت کپسوله کردن اطلاعات خاص AutoMapper تهیه شده‌است. می‌توان این اطلاعات را در داخل تنظیمات new Container خود قرار داد و یا می‌توان آن‌ها را جهت شلوغ نشدن سایر تنظیمات IoC Container، به یک کلاس Registry منتقل کرد:
public class AutomapperRegistry : Registry
{
    public AutomapperRegistry()
    {
        var platformSpecificRegistry = PlatformAdapter.Resolve<IPlatformSpecificMapperRegistry>();
        platformSpecificRegistry.Initialize();
 
        For<ConfigurationStore>().Singleton().Use<ConfigurationStore>()
            .Ctor<IEnumerable<IObjectMapper>>().Is(MapperRegistry.Mappers);
 
        For<IConfigurationProvider>().Use(ctx => ctx.GetInstance<ConfigurationStore>());
 
        For<IConfiguration>().Use(ctx => ctx.GetInstance<ConfigurationStore>());
 
        For<ITypeMapFactory>().Use<TypeMapFactory>();
 
        For<IMappingEngine>().Singleton().Use<MappingEngine>()
                             .SelectConstructor(() => new MappingEngine(null));
 
        this.Scan(scanner =>
        {
            scanner.AssembliesFromApplicationBaseDirectory();
 
            scanner.ConnectImplementationsToTypesClosing(typeof(ITypeConverter<,>))
                   .OnAddedPluginTypes(t => t.HybridHttpOrThreadLocalScoped());
 
            scanner.ConnectImplementationsToTypesClosing(typeof(ValueResolver<,>))
                .OnAddedPluginTypes(t => t.HybridHttpOrThreadLocalScoped());
        });
    }
}
هدف اصلی، وهله سازی خودکار IMappingEngine است و برای رسیدن به آن، باید تمام وابستگی‌های کلاس MappingEngine را مانند IConfigurationProvider و سایر مواردی که مشاهده می‌کنید، مشخص کرد. پس از این تنظیمات، کلاس ObjectFactory سفارشی برنامه به شکل ذیل جهت معرفی AutomapperRegistry تغییر خواهد کرد:
public static class SmObjectFactory
{
    private static readonly Lazy<Container> _containerBuilder =
        new Lazy<Container>(defaultContainer, LazyThreadSafetyMode.ExecutionAndPublication);
 
    public static IContainer Container
    {
        get { return _containerBuilder.Value; }
    }
 
    private static Container defaultContainer()
    {
        var container = new Container(cfg =>
        {
            cfg.AddRegistry<AutomapperRegistry>();
            cfg.Scan(scan =>
            {
                scan.TheCallingAssembly();
                scan.WithDefaultConventions();
                scan.AddAllTypesOf<Profile>().NameBy(item => item.FullName);
            });
        });
 
        configureAutoMapper(container);
 
        return container;
    }
 
    private static void configureAutoMapper(IContainer container)
    {
        var configuration = container.TryGetInstance<IConfiguration>();
        if (configuration == null) return;
        //saying AutoMapper how to resolve services
        configuration.ConstructServicesUsing(container.GetInstance);
        foreach (var profile in container.GetAllInstances<Profile>())
        {
            configuration.AddProfile(profile);
        }
    }
}
در اینجا علاوه بر معرفی AutomapperRegistry، یک مورد دیگر نیز اضافه شده‌است: یافتن خودکار کلاس‌هایی از نوع Profile و همچنین فراخوانی متد AddProfile کتابخانه‌ی AutoMapper به صورت خودکار. به این ترتیب دیگر نیازی نخواهد بود تا در ابتدای کار برنامه، متد Mapper.Initialize را جهت معرفی کلاس‌های Profile فراخوانی کرد و این‌کار به صورت خودکار توسط متد configureAutoMapper انجام می‌شود.


تغییرات لایه سرویس برنامه جهت استفاده از IoC Container

اکنون که IoC Container ما با نحوه‌ی یافتن وابستگی‌های IMappingEngine آشنا شده‌است، تنها کافی است این اینترفیس را در سازنده‌ی کلاس سرویس خود تزریق کنیم:
public class UsersService : IUsersService
{
    private readonly IMappingEngine _mappingEngine;
 
    public UsersService(IMappingEngine mappingEngine)
    {
        _mappingEngine = mappingEngine;
    }
 
    public UserViewModel GetName(int id)
    {
        var dbUser1 = new User
        {
            Id = 1,
            Name = "Test",
            RegistrationDate = DateTime.Now.AddDays(-10)
        };
 
        var uiUser = new UserViewModel();
        _mappingEngine.Map(source: dbUser1, destination: uiUser);
        return uiUser;
    }
}
و پس از آن از متد Map این اینترفیس بجای فراخوانی مستقیم Mapper.Map می‌توان استفاده کرد. به این ترتیب وابستگی مورد نیاز این کلاس، از طریق سازنده‌ی آن به آن تزریق شده‌است و دیگر فراخوانی‌های استاتیک را در اینجا مشاهده نمی‌کنیم.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:
AM_Sample03.zip
نظرات مطالب
شروع به کار با EF Core 1.0 - قسمت 15 - نوشتن آزمون‌های واحد
قسمت using‌ها را می‌توانیم درون یک متد جنریک به اینصورت نیز کپسوله کنیم:
private void ScopeContext<T>(Action<IUnitOfWork, T> callback)
        {
            using (var serviceScope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope())
            {
                using (var context = serviceScope.ServiceProvider.GetRequiredService<IUnitOfWork>())
                {
                    callback(context, serviceScope.ServiceProvider.GetRequiredService<T>());
                }
            }
        }

در اینحالت خواهیم داشت:
ScopeContext<IBlogService>(async (context, scope) =>
{
                Assert.AreEqual(3, context.Set<Blog>().Count());
                Assert.AreEqual("http://sample.com/cats", context.Set<Blog>().First().Url);
});

نظرات مطالب
طراحی یک گرید با Angular و ASP.NET Core - قسمت دوم - پیاده سازی سمت کلاینت
به جای متد toQueryString از کلاس HttpParams برای ارسال مقادیر در کوئری استرینگ نیز میتوان استفاده کرد به شکل زیر :

let params = new HttpParams();
if(sortBy != null && isAscending != null && page != null && pageSize != null) {
   params = params.append('sortBy');
   params = params.append('isAscending');
   params = params.append('page');
   params = params.append('pageSize');
}
و سپس این مقادیر رو به به سمت کلاینت فرستاد درون سرویس ProductsListService به شکل زیر :

getPagedProductsList(sortBy?,isAscending?,
            page?,pageSize?):Observable<PagedQueryResult<AppProduct>> {
       let params = new HttpParams();
       if(sortBy != null && isAscending != null && page != null && pageSize != null) 
       {
         params = params.append('sortBy');
         params = params.append('isAscending');
         params = params.append('page');
         params = params.append('pageSize');
       }

      return this.http.get(`${this.baseUrl}/GetPagedProducts , {params})
      .map(res => {
        const result = res.json();
        return new PagedQueryResult<AppProduct>(
          result.totalItems,
          result.items
        );
      });
  }

مطالب
ساخت DropDownList های مرتبط به کمک jQuery Ajax در MVC
در پروژه‌های زیادی با مواردی مواجه می‌شویم که دو انتخاب داریم ، و یک انتخاب زیر مجموعه انتخاب دیگر است ، مثلا رابطه بین استان‌ها و شهر‌ها ، در انتخاب اول استان انتخاب می‌شود و مقادیر DropDownList  شهر‌ها حاوی ، شهر‌های آن استان می‌شود.
در پروژه‌های WebForm  این کار به کمک Update Panel  انجام می‌شد ، ولی در پروژه‌های MVC  ما این امکان را نداریم و خودمان باید به کمک موارد دیگر این درخواست را پیاده سازی کنیم.
در پروژه ای که فعلا مشغول آن هستم ، با این مورد مواجه شدم ، و دیدم شاید بد نباشد راهکار خودم را با شما در میان بگذارم.
صورت مسئله :
کاربری نیاز به وارد کردن اطلاعات یک شیرالات بهداشتی  (Tap) را دارد ، از DropDownList   اول نوع آن شیر (Type)  را انتخاب می‌کند ، و از DropDownList  دوم ، که شامل گروه‌های آن نوع است ، گروه(Category) مورد نظر را انتخاب می‌کند. (در انتهای همین مطلب ببینید )
ساختار Table‌های دیتابیس به این صورت است: 

رابطه ای از جدول Type به جدول Category.

به کمک Scaffolding یک کنترلر برای کلاس Tap (شیر آب) می‌سازیم ، به طور عادی در فایل Create.chtml مقدار گروه را به صورت DropDown  نمایش می‌دهد، حال ما نیاز داریم که خودمان  DropDown را برای Type  ایجاد کنیم و بعد ارتباط این‌ها را بر قرار کنیم.

تابع اولی Create را این طوری ویرایش می‌کنیم :

        public ActionResult Create()
        {
            ViewBag.Type = new SelectList(db.Types, "Id", "Title");
            ViewBag.Category = new SelectList(db.Categories, "Id", "Title");
            return View();
        }

همان طور که مشخص است ، علاوه بر مقادیر Category  که خودش ارسال می‌کند ، ما نیز مقادیر نوع‌ها را به View  مورد نظر ارسال می‌کنیم.

برای نمایش دادن هر دو DropDownList ویو مورد نظر را به این صورت ویرایش می‌کنیم :

        <div>
            نوع
        </div>
        <div>
            @Html.DropDownList("Type", (SelectList)ViewBag.Type, "-- انتخاب ---", new { id = "rdbTyoe" })
            @Html.ValidationMessageFor(model => model.Category)
        </div>

        <div>
            دسته بندی
        </div>
        <div>
            @Html.DropDownList("Category", (SelectList)ViewBag.Category, "-- انتخاب ---", new { id = "rdbCategory"})
            @Html.ValidationMessageFor(model => model.Category)
        </div>

همان طور که مشاهده می‌کنید ، در اینجا  DropDownList  مربوط به Type که خودمان سمت سرور ،مقادیر آن را پر کرده بودیم نمایش می‌دهیم.

خب شاید تا اینجای کار ، ساده بود ولی میرسیم به اصل مطلب و ارتباط بین این دو DropDownList. (قبل از این قسمت حتما نگاهی به ساختار DropDownList یا همان تگ select بیندازید ، اطلاعات جی کوئری شما در این قسمت خیلی کمک حال شما است)

برای این کار ما از jQuery  استفادی می‌کنیم ، کار به این صورت است که هنگامی که مقدار DropDownList  اول تغییر کرد :

  •  ما Id  آن را به سرور ارسال می‌کنیم.
  •  در آنجا Category  هایی که دارای Type با Id  مورد نظر هستند را جدا می‌کنیم
  • فیلد‌های مورد نیاز یعنی Id  و Title را می‌گیریم
  • و بعد به کمک Json مقادیر را بر می‌گردانیم 
  • و مقادیر ارسالی از سرور را در option‌های DropDownList  دوم (گروه‌ها ) قرار می‌دهم 
در ابتدا متدی در سرور می‌نویسیم ، کار این متد (همان طور که گفته شد) گرفتن Id از نوع Type استو برگرداندن Category‌های مورد نظر. به این صورت :
        public ActionResult SelectCategory(int id)
        {
            var categoris = db.Categories.Where(m => m.Type1.Id == id).Select(c => new { c.Id, c.Title });
            return Json(categoris, JsonRequestBehavior.AllowGet);
        }
ابتدا به کمک Where مقادیر مورد نظر را پیدا می‌کنیم و بعد به کمک Select فیلد هایی مورد استفاده را جدا می‌کنیم و به کمک Json اطلاعات را بر می‌گردانیم. توجه داشته باشید که مقادیری که در categoris  قرار می‌گیرد ، شبیه به مقادیر json  هستند و ما می‌تانیم مستیما از این مقادیر در javascript استفاده کنیم. 
در کلایت (همان طور که گفته شد ) به این نیاز داریم که هنگاهی که مقدار در لیست اول تغییر کرد ، ای دی آن را به تابع بالا بفرستیم و مقادیر بازگشتی را در  DropDownList  دوم قرار دهیم : به این صورت :
            $('#rdbTyoe').change(function () {
                jQuery.getJSON('@Url.Action("SelectCategory")', { id: $(this).attr('value') }, function (data) {
                    $('#rdbCategory').empty();
                    jQuery.each(data, function (i) {
                        var option = $('<option></option>').attr("value", data[i].Id).text(data[i].Title);
                        $("#rdbCategory").append(option);
                    });
                });
            });
توضیح کد :
با کمک تابع .change() از تغییر مقدار آگاه می‌شویم ، و حالا می‌توانیم مراحل کار را انجام دهیم. حالا وقت ارسال اطلاعات ( Id مورد گزینه انتخاب شده در تغییر مقدار ) به سرور است که ما این کار را به کمک jQuery.getJSON انجام می‌دهیم ، در تابع callback باید کار مقدار دهی انجام شود.
به ازای رکورد هایی که برگشت شده است که حالا در data قرار دارد به کمک تابع each لوپ می‌رنیم و هربار این کار را تکرار می‌کنیم.
  • ابتدا یک تگ option می‌سازیم 
  • مقادیر مربوطه شامل Id که باید در attribute مورد نظر value قرار گیرد و متن آن که باید به عنوان text باشد را مقدار دهی می‌کنیم
  • option آماده شده را به DropDownList دومی (Category ) اضافه می‌کنیم.
توجه داشته باشید که هنگامی که کار دریافت اطلاعات با موفقیت انجام شد ، مقادیر داخل DropDownList  دومی ، مربوط به Category را به منظور بازیابی دوباره در  
$('#rdbCategory').empty(); 
خالی می‌کنیم.
نتیجه کار برای من می‌شود این :
قبل از انتخاب :

بعد از انتخاب :

موفق و پیروز باشید