روش متداول ایجاد کارهای پس زمینه
سادهترین روش انجام کارهای پس زمینه در برنامههای دات نتی، استفاده از متدهایی هستند که یک ترد جدید را ایجاد میکنند مانند Task.Run, Task.Factory.StartNew, Delegate.BeginInvoke, ThreadPool.QueueUserWorkItem و امثال آن. اما ... این روشها در برنامههای ASP.NET ایدهی خوبی نیستند!
از این جهت که موتور ASP.NET در این حالات اصلا نمیداند که شما کار پس زمینهای را ایجاد کردهاید. به همین جهت اگر پروسهی برنامه پس از مدتی recycle شود، تمام کارهای پس زمینهی موجود نیز از بین خواهند رفت.
معرفی HostingEnvironment.QueueBackgroundWorkItem
متد HostingEnvironment.QueueBackgroundWorkItem به دات نت 4.5.2 اضافه شدهاست تا بتوان توسط آن یک کار پس زمینه را توسط موتور ASP.NET شروع کرد و نه مانند قبل، بدون اطلاع آن. البته باید دقت داشت که این کارهای پس زمینه مستقل از هر نوع درخواستی اجرا میشوند.
در این حالت چون موتور ASP.NET از وجود کار پس زمینهی آغاز شده مطلع است، در صورت فرا رسیدن زمان recycle شدن برنامه، کل AppDomain را به یکباره نابود نخواهد کرد. البته این مورد فقط به این معنا است که در صورت فرا رسیدن زمان recycle شدن پروسه، با تنظیم یک CancellationToken، اطلاع رسانی خواهد کرد. در این حالت حداکثر 30 ثانیه فرصت خواهید داشت تا کارهای پس زمینه را بدون مشکل خاتمه دهید. اگر کار پس زمینه در این مدت به پایان نرسد، همانند قبل، کل AppDomain نابود خواهد شد.
این متد دو overload دارد و در هر دو حالت، تنظیم خودکار پارامتر CancellationToken توسط ASP.NET، بیانگر آغاز زمان خاتمهی کل برنامه است:
public static void QueueBackgroundWorkItem(Action<CancellationToken> workItem); public static void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);
علت استفاده از Action و Func در اینجا، امکان تعریف خلاصه و inline یک متد و ارسال پارامتری به آن از طرف برنامه است، بجای تعریف یک اینترفیس جدید، نیاز به پیاده سازی آن اینترفیس و بعد برای مثال ارسال یک مقدار از طرف برنامه به متد Stop آن (بجای تعریف یک اینترفیس تک متدی، از Action و یا Func نیز میتوان استفاده کرد).
نمونهای از نحوهی فراخوانی این دو overload را در ذیل مشاهده میکنید:
HostingEnvironment.QueueBackgroundWorkItem(cancellationToken => { //todo: ... }); HostingEnvironment.QueueBackgroundWorkItem(async cancellationToken => { //todo: ... await Task.Delay(20000, cancellationToken); });
پشت صحنهی HostingEnvironment.QueueBackgroundWorkItem
روش استاندارد ثبت و معرفی یک کار پس زمینه در ASP.NET، توسط پیاده سازی اینترفیسی به نام IRegisteredObject انجام میشود. سپس توسط متد HostingEnvironment.RegisterObject میتوان این کلاس را به موتور ASP.NET معرفی کرد. در این حالت زمانیکه AppDomain قرار است خاتمه یابد، متد Stop اینترفیس IRegisteredObject کار اطلاع رسانی را انجام میدهد. توسط QueueBackgroundWorkItem دقیقا از همین روش به همراه فراخوانی ThreadPool.QueueUserWorkItemجهت اجرای متد معرفی شدهی به آن استفاده میشود.
از مکانیزم IRegisteredObject در DNT Scheduler نیز استفاده شدهاست.
پیشنیازها
ابتدا نیاز است به خواص پروژه مراجعه کرده و Target framework را بر روی 5.4.2 قرار داد. اگر به روز رسانی دوم VS 2013 را نصب کرده باشید، این نگارش هم اکنون بر روی سیستم شما فعال است. اگر خیر، امکان دریافت و نصب آن، به صورت جداگانه نیز وجود دارد:
.NET Framework 4.5.2
Developer pack
محدودیتهای QueueBackgroundWorkItem
- از آن در خارج از یک برنامهی وب ASP.NET نمیتوان استفاده کرد.
- توسط آن، خاتمهی یک AppDomain تنها به مدت 30 ثانیه به تاخیر میافتد؛ تا فرصت داشته باشید کارهای در حال اجرا را با حداقل خسارت به پایان برسانید.
- یک work item، اطلاعاتی را از فراخوان خود دریافت نمیکند. به این معنا که مستقل از زمینهی یک درخواست اجرا میشود.
- استفادهی از آن الزاما به این معنا نیست که کار درخواستی شما حتما اجرا خواهد شد. زمانیکه که کار خاتمهی AppDomain آغاز میشود، فراخوانیهای QueueBackgroundWorkItem دیگر پردازش نخواهند شد.
- اگر برنامه به مقدار CancellationToken تنظیم شده توسط ASP.NET دقت نکند، جهت پایان یافتن کار در حال اجرا، صبر نخواهد شد.
تغییرات API دات نت 5 از دیدگاه افزونههای HttpClient
در اینجا لیست کامل متدهای الحاقی اضافه شدهی به فضای نام جدید و استاندارد System.Net.Http.Json را مشاهده میکنید:
namespace System.Net.Http.Json { public static class HttpClientJsonExtensions { public static Task<object> GetFromJsonAsync(this HttpClient client, string requestUri, Type type, JsonSerializerOptions options, CancellationToken cancellationToken = default(CancellationToken)); public static Task<object> GetFromJsonAsync(this HttpClient client, string requestUri, Type type, CancellationToken cancellationToken = default(CancellationToken)); public static Task<object> GetFromJsonAsync(this HttpClient client, Uri requestUri, Type type, JsonSerializerOptions options, CancellationToken cancellationToken = default(CancellationToken)); public static Task<object> GetFromJsonAsync(this HttpClient client, Uri requestUri, Type type, CancellationToken cancellationToken = default(CancellationToken)); public static Task<TValue> GetFromJsonAsync<TValue>(this HttpClient client, string requestUri, JsonSerializerOptions options, CancellationToken cancellationToken = default(CancellationToken)); public static Task<TValue> GetFromJsonAsync<TValue>(this HttpClient client, string requestUri, CancellationToken cancellationToken = default(CancellationToken)); public static Task<TValue> GetFromJsonAsync<TValue>(this HttpClient client, Uri requestUri, JsonSerializerOptions options, CancellationToken cancellationToken = default(CancellationToken)); public static Task<TValue> GetFromJsonAsync<TValue>(this HttpClient client, Uri requestUri, CancellationToken cancellationToken = default(CancellationToken)); public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(this HttpClient client, string requestUri, TValue value, JsonSerializerOptions options = null, CancellationToken cancellationToken = default(CancellationToken)); public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(this HttpClient client, string requestUri, TValue value, CancellationToken cancellationToken); public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(this HttpClient client, Uri requestUri, TValue value, JsonSerializerOptions options = null, CancellationToken cancellationToken = default(CancellationToken)); public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(this HttpClient client, Uri requestUri, TValue value, CancellationToken cancellationToken); public static Task<HttpResponseMessage> PutAsJsonAsync<TValue>(this HttpClient client, string requestUri, TValue value, JsonSerializerOptions options = null, CancellationToken cancellationToken = default(CancellationToken)); public static Task<HttpResponseMessage> PutAsJsonAsync<TValue>(this HttpClient client, string requestUri, TValue value, CancellationToken cancellationToken); public static Task<HttpResponseMessage> PutAsJsonAsync<TValue>(this HttpClient client, Uri requestUri, TValue value, JsonSerializerOptions options = null, CancellationToken cancellationToken = default(CancellationToken)); public static Task<HttpResponseMessage> PutAsJsonAsync<TValue>(this HttpClient client, Uri requestUri, TValue value, CancellationToken cancellationToken); } public static class HttpContentJsonExtensions { public static Task<object> ReadFromJsonAsync(this HttpContent content, Type type, JsonSerializerOptions options = null, CancellationToken cancellationToken = default(CancellationToken)); public static Task<T> ReadFromJsonAsync<T>(this HttpContent content, JsonSerializerOptions options = null, CancellationToken cancellationToken = default(CancellationToken)); } public sealed class JsonContent : HttpContent { public Type ObjectType { get; } public object Value { get; } public static JsonContent Create(object inputValue, Type inputType, MediaTypeHeaderValue mediaType = null, JsonSerializerOptions options = null); public static JsonContent Create<T>(T inputValue, MediaTypeHeaderValue mediaType = null, JsonSerializerOptions options = null); protected override void SerializeToStream(Stream stream, TransportContext context, CancellationToken cancellationToken); protected override Task SerializeToStreamAsync(Stream stream, TransportContext context); protected override Task SerializeToStreamAsync(Stream stream, TransportContext context, CancellationToken cancellationToken); protected override bool TryComputeLength(out long length); } }
متدهای الحاقی جدید کلاس HttpClientJsonExtensions
این متدها به صورت خلاصه شامل سه متد زیر میشوند:
- GetFromJsonAsync : یک درخواست Get را به آدرسی خاص ارسال کرده و خروجی JSON دریافتی را به کمک امکانات توکار System.Text.Json، پردازش و deserialize میکند.
- PostAsJsonAsync : یک درخواست POST را به آدرسی خاص، ارسال میکند. شیء ارسالی به آن به صورت خودکار به JSON تبدیل شده و سپس به سمت سرور ارسال میگردد.
- PutAsJsonAsync : یک درخواست PUT را به آدرسی خاص، ارسال میکند. شیء ارسالی به آن به صورت خودکار به JSON تبدیل شده و سپس به سمت سرور ارسال میگردد.
در ذیل چند مثال را در مورد نحوهی کار با این متدهای الحاقی جدید فضای نام استاندارد System.Net.Http.Json، مشاهده میکنید:
var httpClient = new HttpClient(); httpClient.BaseAddress = new Uri("https://localhost:5000"); var profiles = await httpClient.GetFromJsonAsync<Profile[]>("api/users/profiles"); var profile = new Profile { FirstName = "User 1", LastName = "Name 1", Age = 25 }; using var response1 = await httpClient.PostAsJsonAsync("api/users/profiles", profile); response1.EnsureSuccessStatusCode(); var updatedProfile = new Profile { FirstName = "User 2", LastName = "Name 2", Age = 40 }; using var response2 = await httpClient.PutAsJsonAsync("api/users/profiles", profile); response2.EnsureSuccessStatusCode();
اگر میخواستیم یک چنین کارهایی را پیش از دات نت 5 انجام دهیم، میبایستی قسمت Serialize کردن و همچنین تنظیم content-type را دستی انجام میدادیم:
var profile = new Profile { FirstName = "User 1", LastName = "Name 1", Age = 25 }; var json = JsonSerializer.Serialize(profile); var stringContent = new StringContent(json, Encoding.UTF8, "application/json"); using var response4 = await httpClient.PostAsync("api/users/profiles", stringContent); response4.EnsureSuccessStatusCode();
متدهای الحاقی جدید کلاس HttpContentJsonExtensions
این کلاس، متد الحاقی جدید ReadFromJsonAsync را ارائه میدهد که کار آن، خواندن یک محتوای HTTP از نوع HttpContent و deserialize آن به صورت JSON است. یک مثال:
var httpClient = new HttpClient(); httpClient.BaseAddress = new Uri("https://localhost:5000"); var request = new HttpRequestMessage(HttpMethod.Get, "api/users/profiles"); using var response1 = await httpClient.SendAsync(request); if (response1.IsSuccessStatusCode) { var profiles = await response1.Content.ReadFromJsonAsync<Profile[]>(); }
انجام اینکار در نگارشهای پیشین دات نت، نیاز به فراخوانی دستی JsonSerializer.DeserializeAsync را دارد:
var request = new HttpRequestMessage(HttpMethod.Get, "api/users/profiles"); using var response2 = await httpClient.SendAsync(request); if (response2.IsSuccessStatusCode) { using var streamResult = await response2.Content.ReadAsStreamAsync(); var profiles = JsonSerializer.DeserializeAsync<Profile[]>(streamResult); }
روشهای زیادی برای کار با HttpClient وجود دارند. یک روش آن، ساخت دستی HttpRequestMessage و سپس ارسال آن توسط متد SendAsync است؛ بجای استفاده از متد PostAsJsonAsync که بررسی شد. در این حالت با استفاده از متد جدید JsonContent.Create، میتوان کار تبدیل یک شیء را به JSON و همچنین تنظیم content-type را به صورت خودکار انجام داد:
var httpClient = new HttpClient(); var uri = "https://localhost:5000"; httpClient.BaseAddress = new Uri(uri); var requestMessage = new HttpRequestMessage(HttpMethod.Post, "https://localhost:5000") { Content = JsonContent.Create(new Profile { FirstName = "User 1", LastName = "Name 1", Age = 25 }) }; using var reponse1 = await httpClient.SendAsync(requestMessage); reponse1.EnsureSuccessStatusCode();
کتابخانه PdfReport به عمد جهت دات نت 3.5 تهیه شده است تا بازه وسیعی از سیستم عاملها را پوشش دهد.
این کتابخانه علاوه بر تبدیل اطلاعات شما به گزارشات مبتنی بر PDF، امکان تهیه خروجی خودکار اکسل (2007 به بعد) را نیز دارد. فایل خروجی آن، به صورت پیوست درون فایل PDF تهیه شده قرار میگیرد و جزئی از آن میشود.
بدیهی است اینبار با کتابخانه گزارش سازی روبرو هستید که با راست به چپ مشکلی ندارد!
کتابخانه PdfReport بر پایه کتابخانههای معروف سورس باز iTextSharp و EPPlus تهیه شده است. حداقل مزیت استفاده از آن، صرفه جویی در وقت شما جهت آموختن ریزه کاریهای مرتبط با هر کدام از کتابخانههای یاده شده است. برای نمونه جهت فراگیری کار با iTextSharp نیاز است یک کتاب 600 صفحهای به نام iText in action را مطالعه و تمرین کنید. این مورد منهای مسایل و نکات متعدد مرتبط با زبان فارسی است که در این کتاب به آنها اشارهای نشده است.
برای تهیه آن، مشکلات متداولی که کاربران مدام در انجمنهای برنامه نویسی مطرح میکنند و ابزارهای موجود عاجز از ارائه راه حلی ساده برای حل آنها هستند، مد نظر بوده و امید است نگارش یک این کتابخانه بتواند بسیاری از این دردها را کاهش دهد.
کار با این کتابخانه صرفا با کدنویسی میسر است (code first) و همین مساله انعطاف پذیری قابل توجهی را ایجاد کرده که در طی روزهای آینده با جزئیات بیشتر آن آشنا خواهید شد.
اما چرا PDF؟
استفاده از قالب PDF برای تهیه گزارشات، این مزایا را به همراه دارد:
- دقیقا همان چیزی که مشاهده میشود، در هر مکانی قابل چاپ است. با همان کیفیت، همان اندازه صفحه، همان فونت و غیره. به این ترتیب به صفحه بندی بسیار مناسب و بهینهای میتوان رسید و مشکلات گزارشات HTML ایی وب را ندارد.
- امکان استفاد از فونتهای شکیلتر در آن بدون مشکل و بدون نیاز به نصب بر روی کامپیوتری میسر است؛ چون فونت را میتوان در فایل PDF نیز قرار داد.
- این فایل در تمام سیستم عاملها پشتیبانی میشود. خصوصا اینکه فایل نهایی در تمام کامپیوترها و در انواع و اقسام سیستم عاملها به یک شکل و اندازه نمایش داده خواهد شد. برای مثال اینطور نیست که در ویندوز XP ،اعداد آن فارسی نمایش داده شوند و در ویندوز 7 با تنظیمات محلی خاصی، دیگر اینطور نباشد. حتی تحت لینوکس هم اعداد آن فارسی نمایش داده خواهد شد چون فونت مخصوص بکار رفته، در خود فایل PDF قابل قرار دادن است.
- برنامه معروف و رایگان Adobe reader برای خواندن و مشاهده آن کفایت میکند و البته کلاینت یکبار باید این برنامه را نصب کند. همچنین از این نوع برنامههای رایگان برای مشاهده فایلهای PDF زیاد است.
- تمام صفحات گزارش را در یک فایل میتوان داشت و به یکباره تمام آن نیز به سادگی قابل چاپ است. این مشکلی است که با گزارشات تحت وب وجود دارد که نمیشود مثلا یک گزارش 100 صفحهای را به یکباره به چاپگر ارسال کرد. به همین جهت عموما کاربران درخواست میدهند تا کل گزارش را در یک صفحه HTML نمایش دهید تا ما راحت آنرا چاپ کنیم یا راحت از آن خروجی بگیریم. اما زمانیکه فایل PDF تهیه میشود این مشکلات وجود نخواهد داشت و جهت Print بسیار بهینه سازی شده است. تا حدی که الان فرمت برگزیده تهیه کتابهای الکترونیکی نیز PDF است. مثلا سایت معروف آمازون امکان فروش نسخه PDF کتابها را هم پیش بینی کرده است.
-امکان صفحه بندی دقیق به همراه مشخص سازی landscape یا portrait بودن صفحه نهایی میسر است. چیزی که در گزارشات صفحات وب آنچنان معنایی ندارد.
- امکان رمزنگاری اطلاعات در آن پیش بینی شده است. همچنین میتوان به فایلهای PDF امضای دیجیتال نیز اضافه کرد. به این ترتیب هرگونه تغییری در محتوای فایل توسط برنامههای PDF خوان معتبر گزارش داده شده و میتوان از صحت اطلاعات ارائه شده توسط آن اطمینان حاصل کرد.
- از فشرده سازی مطالب، فایلها و تصاویر قرار داده شده در آن پشتیبانی میکند.
- از گرافیک برداری پشتیبانی میکند.
مجوز استفاده از این کتابخانه:
کار من مبتنی بر LGPL است. به این معنا که به صورت باینری (فایل dll) در هر نوع پروژهای قابل استفاده است.
اما ... PdfReport از دو کتابخانه دیگر نیز استفاده میکند:
- کتابخانه iTextSharp که دارای مجوز AGPL است. این مجوز رایگان نیست.
- کتابخانه EPPlus برای تولید فایلهای اکسل با کیفیت. مجوز استفاده از این کتابخانه LGPL است و تا زمانیکه به صورت باینری از آن استفاده میکنید، محدودیتی را برای شما ایجاد نخواهد کرد.
کتابخانه PdfReport به صورت سورس باز در CodePlex قرار گرفته ؛ اما جهت پرسیدن سؤالات، پیشنهادات، ارائه بهبود و غیره میتوانید (و بهتر است) از قسمت مدیریت پروژه مرتبط در سایت جاری نیز استفاده کنید.
نحوه تهیه اولین گزارش، با کتابخانه PdfReport
الف) یک پروژه Class library جدید را شروع کنید. از این جهت که گزارشات PdfReport در انواع و اقسام پروژههای VS.NET قابل استفاده است، میتوان از این پروژه Class library به عنوان کلاسهای پایه قابل استفاده در انواع و اقسام پروژههای مختلف، بدون نیاز به تغییری در کدهای آن استفاده کرد.
ب) آخرین نگارش فایلهای مرتبط با PdfReport را از اینجا دریافت کنید و سپس ارجاعاتی را به اسمبلیهای موجود در بسته آن به پروژه خود اضافه نمائید (ارجاعاتی به PdfReport، iTextSharp و EPPlus). فایل XML راهنمای کتابخانه نیز به همراه بسته آن میباشد که در حین استفاده از متدها و خواص PdfReport کمک بزرگی خواهد بود.
ج) کلاسهای زیر را به آن اضافه کنید:
using System.Web; using System.Windows.Forms; namespace PdfReportSamples { public static class AppPath { public static string ApplicationPath { get { if (isInWeb) return HttpRuntime.AppDomainAppPath; return Application.StartupPath; } } private static bool isInWeb { get { return HttpContext.Current != null; } } } }
همانطور که مشاهده میکنید ارجاعاتی را به System.Windows.Forms.dll و System.Web.dll نیاز دارد.
در ادامه کلاس User را جهت ساخت یک منبع داده درون حافظهای تعریف خواهیم کرد:
using System; namespace PdfReportSamples.IList { public class User { public int Id { set; get; } public string Name { set; get; } public string LastName { set; get; } public long Balance { set; get; } public DateTime RegisterDate { set; get; } } }
using System; using System.Collections.Generic; using PdfRpt.Core.Contracts; using PdfRpt.FluentInterface; namespace PdfReportSamples.IList { public class IListPdfReport { public IPdfReportData CreatePdfReport() { return new PdfReport().DocumentPreferences(doc => { doc.RunDirection(PdfRunDirection.RightToLeft); doc.Orientation(PageOrientation.Portrait); doc.PageSize(PdfPageSize.A4); doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" }); }) .DefaultFonts(fonts => { fonts.Path(Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf", Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf"); }) .PagesFooter(footer => { footer.DefaultFooter(DateTime.Now.ToString("MM/dd/yyyy")); }) .PagesHeader(header => { header.DefaultHeader(defaultHeader => { defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png"); defaultHeader.Message("گزارش جدید ما"); }); }) .MainTableTemplate(template => { template.BasicTemplate(BasicTemplate.ClassicTemplate); }) .MainTablePreferences(table => { table.ColumnsWidthsType(TableColumnWidthType.Relative); table.NumberOfDataRowsPerPage(5); }) .MainTableDataSource(dataSource => { var listOfRows = new List<User>(); for (int i = 0; i < 200; i++) { listOfRows.Add(new User { Id = i, LastName = "نام خانوادگی " + i, Name = "نام " + i, Balance = i + 1000 }); } dataSource.StronglyTypedList<User>(listOfRows); }) .MainTableSummarySettings(summarySettings => { summarySettings.OverallSummarySettings("جمع کل"); summarySettings.PerviousPageSummarySettings("نقل از صفحه قبل"); summarySettings.PageSummarySettings("جمع صفحه"); }) .MainTableColumns(columns => { columns.AddColumn(column => { column.PropertyName("rowNo"); column.IsRowNumber(true); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(0); column.Width(1); column.HeaderCell("#"); }); columns.AddColumn(column => { column.PropertyName<User>(x => x.Id); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(1); column.Width(2); column.HeaderCell("شماره"); }); columns.AddColumn(column => { column.PropertyName<User>(x => x.Name); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(2); column.Width(3); column.HeaderCell("نام"); }); columns.AddColumn(column => { column.PropertyName<User>(x => x.LastName); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(3); column.Width(3); column.HeaderCell("نام خانوادگی"); }); columns.AddColumn(column => { column.PropertyName<User>(x => x.Balance); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(4); 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)); }); }); }) .MainTableEvents(events => { events.DataSourceIsEmpty(message: "رکوردی یافت نشد."); }) .Export(export => { export.ToExcel(); export.ToCsv(); export.ToXml(); }) .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\RptIListSample.pdf")); } } }
var rpt = new IListPdfReport().CreatePdfReport(); // rpt.FileName
برای نمونه، جهت مشاهده نمایش این خروجی در یک برنامه ویندوزی، به مثالهای همراه سورس پروژه در مخزن کد آن مراجعه نمائید.
توضیحات بیشتر:
- در قسمت DocumentPreferences، جهت راست به چپ (PdfRunDirection)، اندازه صفحه (PdfPageSize)، جهت صفحه (PageOrientation) و امثال آن تنظیم میشوند.
- سپس نیاز است قلمهای مورد استفاده در گزارش مشخص شوند. در متد DefaultFonts باید دو قلم را معرفی کنید. قلم اول، قلم پیش فرض خواهد بود و قلم دوم برای رفع نواقص قلم اول مورد استفاده قرار میگیرد. برای مثال اگر قلم اول فاقد حروف انگلیسی است، به صورت خودکار به قلم دوم رجوع خواهد شد.
- در ادامه در متد PagesFooter، تاریخ درج شده در پایین تمام صفحات مشخص میشود. در مورد ساخت Footer سفارشی در قسمتهای بعدی بحث خواهد شد.
- در متد PagesHeader، متن و تصویر قرار گرفته در Header تمام صفحات گزارش قابل تنظیم است. این مورد نیز قابل سفارشی سازی است که در قسمتهای بعد به آن خواهیم پرداخت.
- توسط MainTableTemplate، قالب ظاهری ردیفهای گزارش مشخص میشود. یک سری قالب پیش فرض در کتابخانه PdfReport موجود است که توسط متد BasicTemplate آن قابل دسترسی است. در مورد نحوه تعریف قالبهای سفارشی به مرور در قسمتهای بعد، بحث خواهد شد.
- در قسمت MainTablePreferences تنظیمات جدول اصلی گزارش تعیین میشود. برای مثال چه تعداد ردیف در صفحه نمایش داده شود. اگر این مورد را تنظیم نکنید، به صورت خودکار محاسبه خواهد شد. نحوه تعیین عرض ستونهای گزارش به کمک متد ColumnsWidthsType مشخص میشود که در اینجا حالت نسبی درنظر گرفته شده است.
- منبع داده مورد استفاده توسط متد MainTableDataSource مشخص میشود که در اینجا یک لیست جنریک تعیین شده و سپس توسط متد StronglyTypedList در اختیار گزارش ساز جاری قرار میگیرد. تعدادی منبع داده پیش فرض در PdfReport وجود دارند که هر کدام را در قسمتهای بعدی بررسی خواهیم کرد. همچنین امکان تعریف منابع داده سفارشی نیز وجود دارد.
- با کمک متد MainTableSummarySettings، برچسبهای جمعهای پایین صفحات مشخص میشود.
- در قسمت MainTableColumns، ستونهایی را که علاقمندیم در گزارش ظاهر شوند، قید میکنیم. هر ستون باید با یک فیلد یا خاصیت منبع داده متناظر باشد. همچنین همانطور که مشاهده میکنید امکان تعیین Visibility، عرض و غیره آن نیز مهیا است (قابلیت ساخت گزارشاتی که به انتخاب کاربر، ستونهای آن ظاهر یا مخفی شوند). در اینجا توسط callbackهایی که در متد ColumnItemsTemplate قابل دسترسی هستند، میتوان اطلاعات را پیش از نمایش فرمت کرد. برای مثال سه رقم جدا کننده به اعداد اضافه کرد (برای نمونه در خاصیت موجودی فوق) و یا توسط متد AggregateFunction، میتوان متد تجمعی مناسبی را جهت ستون جاری مشخص کرد.
- توسط متد MainTableEvents به بسیاری از رخدادهای داخلی PdfReport دسترسی خواهیم یافت. برای مثال اگر در اینجا رکوردی موجود نباشد، رخداد DataSourceIsEmpty صادر خواهد شد.
- به کمک متد Export، خروجیهای دلخواه مورد نظر را میتوان مشخص کرد. تعدادی خروجی، مانند اکسل، XML و CSV در این کتابخانه موجود است. امکان سفارشی سازی آنها نیز پیش بینی شده است.
- و نهایتا توسط متد Generate مشخص خواهیم کرد که فایل گزارش کجا ذخیره شود.
لطفا برای طرح مشکلات و سؤالات خود در رابطه با کتابخانه PdfReport از این قسمت سایت استفاده کنید.
PM> Install-Package Postal
شروع به کار با Postal
using Postal; public class HomeController : Controller { public ActionResult Index() { dynamic email = new Email("Example"); email.To = "webninja@example.com"; email.FunnyLink = DB.GetRandomLolcatLink(); email.Send(); return View(); } }
To: @ViewBag.To From: lolcats@website.com Subject: Important Message Hello, You wanted important web links right? Check out this: @ViewBag.FunnyLink <3
پیکربندی SMTP
<configuration> ... <system.net> <mailSettings> <smtp deliveryMethod="network"> <network host="example.org" port="25" defaultCredentials="true"/> </smtp> </mailSettings> </system.net> ... </configuration>
ایمیلهای Strongly-typed
namespace App.Models { public class ExampleEmail : Email { public string To { get; set; } public string Message { get; set; } } }
public void Send() { var email = new ExampleEmail { To = "hello@world.com", Message = "Strong typed message" }; email.Send(); }
@model App.Models.ExampleEmail To: @Model.To From: postal@example.com Subject: Example Hello, @Model.Message Thanks!
آزمونهای واحد (Unit Testing)
public class ExampleController : Controller { public ExampleController(IEmailService emailService) { this.emailService = emailService; } readonly IEmailService emailService; public ActionResult Index() { dynamic email = new Email("Example"); // ... emailService.Send(email); return View(); } }
[Test] public void ItSendsEmail() { var emailService = A.Fake<IEmailService>(); var controller = new ExampleController(emailService); controller.Index(); A.CallTo(() => emailService.Send(A<Email>._)) .MustHaveHappened(); }
ایمیلهای ساده و HTML
To: test@test.com From: example@test.com Subject: Fancy email Views: Text, Html
Content-Type: text/plain; charset=utf-8 Hello @ViewBag.PersonName, This is a message
Content-Type: text/html; charset=utf-8 <html> <body> <p>Hello @ViewBag.PersonName,</p> <p>This is a message</p> </body> </html>
ضمیمه ها
dynamic email = new Email("Example"); email.Attach(new Attachment("c:\\attachment.txt")); email.Send();
جاسازی تصاویر در ایمیل ها
<configuration> <system.web.webPages.razor> <pages pageBaseType="System.Web.Mvc.WebViewPage"> <namespaces> <add namespace="Postal" /> </namespaces> </pages> </system.web.webPages.razor> </configuration>
To: john@example.org From: app@example.org Subject: Image @Html.EmbedImage("~/content/postal.jpg")
Postal بیرون از ASP.NET
using Postal; class Program { static void Main(string[] args) { // Get the path to the directory containing views var viewsPath = Path.GetFullPath(@"..\..\Views"); var engines = new ViewEngineCollection(); engines.Add(new FileSystemRazorViewEngine(viewsPath)); var service = new EmailService(engines); dynamic email = new Email("Test"); // Will look for Test.cshtml or Test.vbhtml in Views directory. email.Message = "Hello, non-asp.net world!"; service.Send(email); } }
Email Headers: برای در بر داشتن نام، در آدرس ایمیل از فرمت زیر استفاده کنید.
To: John Smith <john@example.org>
Bcc: john@smith.com, harry@green.com Subject: Example etc
Bcc: john@smith.com Bcc: harry@green.com Subject: Example etc
ساختن ایمیل بدون ارسال آن
public class ExampleController : Controller { public ExampleController(IEmailService emailService) { this.emailService = emailService; } readonly IEmailService emailService; public ActionResult Index() { dynamic email = new Email("Example"); // ... var message = emailService.CreateMailMessage(email); CustomProcessMailMessage(message); return View(); } }
شما در ASP.NET Core امکان کار با هر دو فریم ورک یاد شده را دارید و این دو به هم وابستگی ندارند. به عبارتی چندین target را دراینجا میتوانید معرفی و استفاده کنید. اگر دات نت 4.6 را هم استفاده کردید، برنامه فقط قابلیت چندسکویی خودش را از دست خواهد داد. برای مثال شما هم اکنون میتوانید EF 6.x را با ASP.NET Core 1.0 استفاده کنید (اگر نمیخواهید تا زمان تکمیل نهایی EF Core صبر کنید). فقط در این حالت باید دقت داشته باشید که کدهای شما بر روی لینوکس اجرا نخواهند شد (چون EF 6.x مبتنی بر دات نت 4x است).
facilities.Should().BeEquivalentTo(expectedResult);
متدهای توکاری مانند Assert.Equal کار بررسی reference equality را انجام میدهند که برای اینکار مناسب نیستند و یا برای متدهای خاصی مانند CollectionAssert.AreEqual باید حتما پیاده سازی IComparer وجود داشته باشد:
CollectionAssert.AreEqual(list1, resultList, new CustomComparer())
endpoints.MapGet("/api/authors", async (MinimalBlogDbContext ctx) => { var authors = await ctx.Authors.ToListAsync(); return authors; }); endpoints.MapPost("/api/authors", async (MinimalBlogDbContext ctx, AuthorDto authorDto) => { var author = new Author(); author.FirstName = authorDto.FirstName; author.LastName = authorDto.LastName; author.Bio = authorDto.Bio; author.DateOfBirth = authorDto.DateOfBirth; ctx.Authors.Add(author); await ctx.SaveChangesAsync(); return author; });
ایجاد Command و هندلر مخصوص ایجاد یک نویسندهی جدید
در الگوی CQRS، یک دستور، کاری را بر روی بانک اطلاعاتی انجام میدهد. برای مثال در اینجا قرار است نویسندهای را ثبت کند. در ادامه میخواهیم بدنهی endpoints.MapPost فوق را با الگوی CQRS انطباق دهیم. به همین جهت به یک Command نیاز داریم:
using MediatR; using MinimalBlog.Domain.Model; namespace MinimalBlog.Api.Features.Authors; public class CreateAuthorCommand : IRequest<Author> { public AuthorDto AuthorDto { get; set; } = default!; }
public interface IRequest<out TResponse> : IBaseRequest
سپس نیاز به یک هندلر است تا دستور رسیده را پردازش کند:
namespace MinimalBlog.Api.Features.Authors; public class CreateAuthorCommandHandler : IRequestHandler<CreateAuthorCommand, Author> { private readonly MinimalBlogDbContext _context; private readonly IMapper _mapper; public CreateAuthorCommandHandler(MinimalBlogDbContext context, IMapper mapper) { _context = context ?? throw new ArgumentNullException(nameof(context)); _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); } public async Task<Author> Handle(CreateAuthorCommand request, CancellationToken cancellationToken) { if (request == null) { throw new ArgumentNullException(nameof(request)); } var toAdd = _mapper.Map<Author>(request.AuthorDto); _context.Authors.Add(toAdd); await _context.SaveChangesAsync(cancellationToken); return toAdd; } }
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse>
پس از این تغییرات، بدنهی lambda expression مربوط به endpoints.MapPost به صورت زیر تغییر کرده و ساده میشود:
endpoints.MapPost("/api/authors", async (IMediator mediator, AuthorDto authorDto) => { var command = new CreateAuthorCommand { AuthorDto = authorDto }; var author = await mediator.Send(command); return author; });
نکته 1: await داخل بدنهی lambda expression مربوط به endpoints را فراموش نکنید. تمام متدهای IMediator از نوع aysnc هستند؛ هرچند روش نامگذاری SendAsync را رعایت نکردهاند و اگر این await فراموش شود، مشاهده خواهید کرد که برنامه در حین فراخوانی endpoints در مرورگر، در حالت هنگ و صبر کردن نامحدود قرار میگیرد، بدون اینکه کاری را انجام دهد و یا حتی استثنایی را صادر کند.
نکته 2: در پیاده سازی هندلر، استفاده از cancellationToken را نیز مشاهده میکنید. تقریبا تمام متدهای async مربوط به EF-Core به همراه پارامتری جهت دریافت cancellationToken هم هستند. اگر کاربری قصد لغو یک درخواست طولانی را داشته باشد و بر روی دکمهی stop مرورگر کلیک کند و یا حتی صفحه را چندین بار ریفرش کند، این به معنای abort درخواست(های) رسیدهاست. وجود این cancellationTokenها، بار سرور را کاهش داده و عملیات در حال اجرای سمت سرور را در یک چنین حالتهایی متوقف میکند.
البته هندلری که در اینجا تعریف شده، این cancellationToken را باید از mediator دریافت کند که در کدهای endpoint فوق، چنین نیست. برای رفع این مشکل باید به صورت زیر عمل کرد:
endpoints.MapGet("/api/authors", async (IMediator mediator, CancellationToken ct) => { var request = new GetAllAuthorsQuery(); var authors = await mediator.Send(request, ct); return authors; });
نکته 3: هندلرها عموما چیزی را بازگشت نمیدهند؛ صرف نظر از هندلر فوق که نیاز بوده تا Id شیء ذخیره شده را بازگشت دهد، عموما به همراه هیچ خروجی نیستند. به همین جهت در حین تعریف آنها فقط کافی است در آرگومانهای جنریک آنها، نوع خروجی را ذکر نکنیم:
public class Handler : IRequestHandler<Command>
public interface IRequestHandler<in TRequest> : IRequestHandler<TRequest, Unit> where TRequest : IRequest<Unit>
public async Task<Unit> Handle(Command request, CancellationToken cancellationToken) { // ... return Unit.Value; }
public class Command : IRequest
ایجاد Query و هندلر مخصوص بازگشت لیست نویسندهها
در الگوی CQRS، یک کوئری قرار است اطلاعاتی را بازگشت دهد و ... وضعیت بانک اطلاعاتی را تغییر نمیدهد. بنابراین در اینجا یک IRequest که قرار است لیستی از نویسندگان را بازگشت دهد، تعریف میکنیم. بدنهی آن هم میتواند خالی باشد و یا به همراه خواصی مانند اطلاعات صفحه بندی و یا مرتب سازی گزارشگیری رسیدهی از درخواست:
using MediatR; using MinimalBlog.Domain.Model; namespace MinimalBlog.Api.Features.Authors; public class GetAllAuthorsQuery : IRequest<List<Author>> { }
namespace MinimalBlog.Api.Features.Authors; public class GetAllAuthorsHandler : IRequestHandler<GetAllAuthorsQuery, List<Author>> { private readonly MinimalBlogDbContext _context; public GetAllAuthorsHandler(MinimalBlogDbContext context) { _context = context ?? throw new ArgumentNullException(nameof(context)); } public Task<List<Author>> Handle(GetAllAuthorsQuery request, CancellationToken cancellationToken) { return _context.Authors.ToListAsync(cancellationToken); } }
endpoints.MapGet("/api/authors", async (IMediator mediator) => { var request = new GetAllAuthorsQuery(); var authors = await mediator.Send(request); return authors; });
و یا حتی معماری CQRS با معماری Event store نیز قابل ترکیب است:
در اینجا بجای استفاده از بانک اطلاعاتی Write، از یک Event store استفاده میشود. کار event store، دریافت رویدادهای write است و سپس باز پخش آنها به بانک اطلاعاتی Read؛ تا کار همگام سازی به این نحو صورت گیرد.
روشی برای نظم دادن به نحوهی تعریف کلاسهای الگوی CQRS
تا اینجا برای مثال کلاسCreateAuthorCommand را در یک فایل مجزا و سپس هندلر آنرا به نام CreateAuthorCommandHandler در یک فایل دیگر تعریف کردیم. میتوان جهت بالابردن خوانایی برنامه، کاهش رفت و برگشتها برای یافتن کلاسهای مرتبط و همچنین سهولت یافتن هندلرهای مرتبط با هر متد mediator.Send، از روش زیر نیز استفاده کرد:
public static class CreateAuthor { public class Command : IRequest<AuthorGetDto> { // ... } public class Handler : IRequestHandler<Command, AuthorGetDto> { // ... } }
var command = new CreateAuthor.Command { AuthorDto = authorDto }; var author = await mediator.Send(command, ct);
در مورد کوئریها هم میتوان به قالب مشابهی رسید که در اینجا هم کوئری و هندلر آن، ذیل نام اصلی مدنظر قرار میگیرند:
public static class GetAllAuthors { public class Query : IRequest<List<AuthorGetDto>> { //... } public class Handler : IRequestHandler<Query, List<AuthorGetDto>> { //... } }
var container = new UnityContainer();
container.RegisterType<ISomeService, SomeService>(new PerRequestLifetimeManager()); container.RegisterType<ISomeBusiness, SomeBusiness>(new PerRequestLifetimeManager()); container.RegisterType<ISomeController, SomeController>(new PerRequestLifetimeManager());
DependencyResolver.SetResolver(new UnityDependencyResolver(container));
var someService=(ISomeService)DependencyResolver.Current.GetService(typeof(ISomeService)); var data=someService.GetData();
در لایه سرویس یک کلاس Service Factory داریم که قرار است همه سرویسها، برای برقراری ارتباط با یکدیگر از آن استفاده کنند.این کلاس معمولا در لایه سرویس به اشکال گوناگونی پیاده سازی میشود که کارش وهه سازی از Interfaceهای درخواستی است. اما برای یکپارچه کردن آن با Unity من آنرا به شکل زیر پیاده سازی کرده ام
public class ServiceFactory : MarshalByRefObject { static IUnityContainer uContainer = new UnityContainer(); public static Type DataContextType { get; set; } public static void Initialise(IUnityContainer unityContainer, Type dbContextType) { uContainer = unityContainer; DataContextType = dbContextType; uContainer.RegisterType(typeof(BaseDataContext), DataContextType, new HierarchicalLifetimeManager()); } public static T Create<T>() { return (T)Activator.CreateInstance<T>(); } public static T Create<T>(string fullTypeName) { return (T)System.Reflection.Assembly.GetExecutingAssembly().CreateInstance(fullTypeName); } public static T Create<T>(Type entityType) { return (T)Activator.CreateInstance(entityType); } public static dynamic Create(Type entityType) { return Activator.CreateInstance(entityType); } public static T Get<T>() { return uContainer.Resolve<T>(); } public static object Get(Type type) { return uContainer.Resolve(type); } }
در این کلاس ما بجای ایجاد داینامیک آبجکتها، از Unity استفاده کردهایم. در همان ابتدا که برنامهی وب ما برای اولین بار اجرا میشود و بعد از Register کردن کلاسها، میتوانیم container را به صورت پارامتر سازنده به کلاس Service Factory ارسال کنیم. به این ترتیب برای استفاده از سرویسها در لایه Business از Unity بهره میبریم.
البته استفاده از Unity برای DataContext خیلی منطقی نیست و بهتر است نوع DataContext را در ابتدا بگیریم و هرجا نیاز داشتیم با استفاده از متد Create از آن وهله سازی بکنیم.
کوئری هایی با قابلیت استفاده ی مجدد
اگر سوال جناب سیف اللهی رو درست متوجه شده باشم ، ایشون میخوان که فیلدهایی رو که از یک تابع برگشت داده میشه خودشون انتخاب کنن و محدود به مقدار بازگشتی از نوع Student برای مثال نباشن .
ایده ای که به ذهن من رسید ( بر اساس برداشتی که از سوال داشتم ) استفاده از قابلیت بسیار کاربردی Func هستش . یک Func با ورودی از نوع Entity و مقدار بازگشتی از نوع anonymous Type . در هنگام فراخوانی هم میشه از نوع dynamic برای دریافت نتیجه استفاده کرد . یک نمونه از پیاده سازی همچین چیزی رو اینجا قرار دادم .