T attachedEntity = set.Find(entity.Id); var attachedEntry = dbContext.Entry(attachedEntity); attachedEntry.CurrentValues.SetValues(entity);
مستند سازی ASP.NET Core 2x API توسط OpenAPI Swagger - قسمت چهارم - تکمیل مستندات نوعهای خروجی API
در برنامههای 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)
در اینجا 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)
[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>
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'.
/// <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)
کار آن مدیریت تمام حالتهای دیگری است که هنوز توسط 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 در WPF را میتوان نوعی لیست جنریک ویژه تعریف کرد که زمانیکه به کنترلی بایند شد، کنترل را از تغییرات خودش آگاه میکند. برای مثال اگر آیتمی به این لیست اضافه شد بلافاصله آن آیتم را در کنترل مقید به آن نیز خواهید دید، به همین ترتیب در مورد ویرایش و یا حذف یک آیتم، بدون نیاز به کوچکترین تماسی با کنترل مورد نظر. برای مثال اگر مقدار یک خاصیت را تغییر دادید، بلافاصله بدون اینکه به کنترل مقید به آن اعلام کنیم که لطفا این مورد ویژه را برای من تغییر بده، شاهد نتیجهی نهایی خواهیم بود.
اما استفادهی پیشرفته از این لیست جنریک ویژه به همینجا ختم نشده و حین اضافه کردن کمی پیچیدگی به برنامه مشکلات عدیدهای بروز میکنند که آنها را جهت دسترسی سادهی بعدی در زیر لیست میکنم:
الف) اصلا Observable collection چیست؟ چکار میکند؟
List vs ObservableCollection vs INotifyPropertyChanged in Silverlight
ب) نمیتوانم از این مجموعهی اشیای خودآگاه سازنده در یک ترد استفاده کنم. مشکل کجاست؟
این روزها نمیتوان یک برنامهی دسکتاپ خوب را بدون استفاده از تردها متصور شد. اما به محض سعی در به روز رسانی این لیست جنریک در یک ترد دیگر (ترد دیگر منظور هر تردی بجز ترد اصلی برنامه است که کار مدیریت رابط کاربر را به عهده دارد) خطای زیر ظاهر میشود:
راه حل:
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 آن کنترل، نسبت به خالی کردن نمای ظاهری آن اقدام کنم. چکار باید کرد؟
خطای مشاهده شده:
راه حل:
همان Observable collection اصلی را تخلیه کنید، UI به صورت خودکار به روز خواهد شد.
و) اضافه کردن رنجی از اطلاعات به آن به صورتی یکباره ممکن است کند باشد. چه باید کرد؟
راه حل:
AddRange for ObservableCollection in Silverlight 3
روش متداول کار با کتابخانهی 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; } }
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; }
کار با ایجاد یک وهله از شیء 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(); } }
ذکر 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(); } }
محل فراخوانی این متدها هم میتواند در کنار متدهای به روز رسانی اطلاعات اصلی در بانک اطلاعاتی برنامه باشند. اگر رکوردی اضافه یا حذف شده، ایندکس متناظر نیز باید به روز شود.
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، ضخیم نمایش داده خواهند شد.
این قسمت از مقاله به ایده اصلی برنامه نویسی تابعی و دلیل وجودی آن خواهد پرداخت. هیچ شکی نیست که بزرگترین چالش در توسعه نرم افزارهای بزرگ، پیچیدگی آن است. تغییرات همیش اجتناب ناپذیر هستند. به خصوص زمانی که صحبت از پیاده سازی امکان جدیدی باشد، پیچیدگی اضافه خواهد شد. در نتیجه منجر به سخت شدن فهمیدن کد میشود، زمان توسعه را بالاتر میبرد و باگهای ناخواسته را به وجود خواهد آورد. همچنین تغییر هر چیزی در دنیای نرم افزار بدون به وجود آوردن رفتارهای ناخواسته و یا اثرات جانبی، تقریبا غیر ممکن است. در نهایت همه این موارد میتوانند سرعت توسعه را پایین برده و حتی باعث شکست پروژههای نرم افزاری شوند. سبکهای کد نویسی دستوری (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 هم نوعی تابع هستند.
به طور ساده با نگاه کردن به ورودیهای تابع و نام آنها باید بتوانیم کاری را که انجام میدهد، حدس بزنیم. یعنی یک تابع باید بر اساس ورودیهای آن کاری را انجام دهد و نباید یک پارامتر Global آن را تحت تاثیر قرار دهد. پارامترهای Global میتوانند یک Property در سطح یک کلاس باشند، یا یک شیء که وضعیت آن تحت کنترل تابع نیست؛ مانند شی DateTime. به مثال زیر توجه کنید:
public int CalculateElapsedDays(DateTime from) { DateTime now = DateTime.Now; return (now - from).Days; }
آیا میتوانید این تابع را شفاف کنیم؟ بله!
چطور؟ به سادگی! با تغییر پارامترهای ورودی:
public static int CalculateElapsedDays(DateTime from, DateTime now) => (now - from).Days;
صداقت یک تابع یعنی یک تابع باید همه اطلاعات مربوط به ورودیها و خروجیها را پوشش دهد. به این مثال دقت کنید:
public int Divide(int numerator, int denominator) { return numerator / denominator; }
آیا این همه مواردی را که از آن انتظار داریم پوشش میدهد؟ احتمالا خیر!
اگر دو عدد صحیح را به این تابع بفرستیم، احتمالا مشکلی پیش نخواهد آمد. اما همانطور که حدس میزنید اگر پارامتر دوم 0 باشد چه اتفاقی خواهد افتاد؟
var result = Divide(1,0);
چگونه مشکل را حل کنیم؟ تایپ ورودی را به شکل زیر تغییر دهیم:
public static int Divide(int numerator, NonZeroInt denominator) { return numerator / denominator.Value; }
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);
توابع مرتبه بالا! یک یا چند تابع را به عنوان ورودی میگیرند و یک تابع را به عنوان نتیجه بر میگرداند. در مثال بالا 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; }
2. ملاک تشخیص اینکه چه آیتمهایی در لیست باید وجود داشته باشند، به عهده متدی میباشد که آن را فراخوانی میکند.
در این مثال، تابع Where، تابع ورودی را به ازای هر المان، در لیست فراخوانی میکند. این تابع میتواند طوری طراحی شود که تابع ورودی را به صورت شرطی اعمال کند. آزمایش این حالت به عهده شما میباشد. اما به صورت کلی انتظار میرود که قدرت توابع High-Order را درک کرده باشید.
تزریق وابستگیهای AutoMapper در لایه سرویس برنامه
تنظیمات 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()); }); } }
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); } } }
تغییرات لایه سرویس برنامه جهت استفاده از 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; } }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
AM_Sample03.zip
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); });
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'); }
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 ); }); }
رابطه ای از جدول 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 دوم (گروهها ) قرار میدهم
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); }
$('#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); }); }); });
- ابتدا یک تگ option میسازیم
- مقادیر مربوطه شامل Id که باید در attribute مورد نظر value قرار گیرد و متن آن که باید به عنوان text باشد را مقدار دهی میکنیم
- option آماده شده را به DropDownList دومی (Category ) اضافه میکنیم.
$('#rdbCategory').empty();
بعد از انتخاب :