- روش دوم: مثال AcroFormTemplatePdfReport از کتابخانهی PdfReport
- روش سوم: مثال CustomCellTemplatePdfReport از کتابخانهی PdfReport
- روش چهارم: مثال HtmlCellTemplatePdfReport از کتابخانهی PdfReport
- روش پنجم: مثال InlineProvidersPdfReport از کتابخانهی PdfReport
جدول AppUserClaims
جدول AppUserClaims، جزو جداول اصلی ASP.NET Core Identity است و هدف آن ذخیرهی اطلاعات ویژهی کاربران و بازیابی سادهتر آنها از طریق کوکیهای آنها است (همانند User.Identity.Name). زمانیکه کاربری به سیستم وارد میشود، بر اساس UserId او، تمام رکوردهای User Claims متعلق به او از این جدول واکشی شده و به صورت خودکار به کوکی او اضافه میشوند.
در پروژهی DNT Identity از این جدول استفاده نمیشود. چون اطلاعات User Claims مورد نیاز آن، هم اکنون در جدول AppUsers موجود هستند. به همین جهت افزودن این نوع User Claimها به جدول AppUserClaims، به ازای هر کاربر، کاری بیهوده است. سناریویی که استفادهی از این جدول را با مفهوم میکند، ذخیره سازی تنظیمات ویژهی هرکاربر است (خارج از فیلدهای جدول کاربران). برای مثال اگر سایتی را چندزبانه طراحی کردید، میتوانید یک User Claim سفارشی جدید را برای این منظور ایجاد و زبان انتخابی کاربر را به عنوان یک رکورد جدید مخصوص آن در این جدول ویژه ثبت کنید. مزیت آن این است که واکشی و افزوده شدن اطلاعات آن به کوکی شخص، به صورت خودکار توسط فریم ورک صورت گرفته و در حین مرور صفحات توسط کاربر، دیگر نیازی نیست تا اطلاعات زبان انتخابی او را از بانک اطلاعاتی واکشی کرد.
بنابراین برای ذخیره سازی تنظیمات با کارآیی بالای ویژهی هرکاربر، جدول جدیدی را ایجاد نکنید. جدول User Claim برای همین منظور درنظر گرفته شدهاست و پردازش اطلاعات آن توسط فریم ورک صورت میگیرد.
ASP.NET Core Identity چگونه اطلاعات جدول AppUserClaims را پردازش میکند؟
ASP.NET Identity Core در حین لاگین کاربر به سیستم، از سرویس SignInManager خودش استفاده میکند که با نحوهی سفارشی سازی آن پیشتر در قسمت دوم این سری آشنا شدیم. سرویس SignInManager پس از لاگین شخص، از یک سرویس توکار دیگر این فریم ورک به نام UserClaimsPrincipalFactory جهت واکشی اطلاعات User Claims و همچنین Role Claims و افزودن آنها به کوکی رمزنگاری شدهی شخص، استفاده میکند.
بنابراین اگر قصد افزودن User Claim سفارشی دیگری را داشته باشیم، میتوان همین سرویس توکار UserClaimsPrincipalFactory را سفارشی سازی کرد (بجای اینکه الزاما رکوردی را به جدول AppUserClaims اضافه کنیم).
اطلاعات جالبی را هم میتوان از پیاده سازی متد CreateAsync آن استخراج کرد:
public virtual async Task<ClaimsPrincipal> CreateAsync(TUser user)
2) userName شخص پس از لاگین از طریق User Claims ایی با نوع Options.ClaimsIdentity.UserNameClaimType به کوکی او اضافه میشود.
3) security stamp او (آخرین بار تغییر اطلاعات اکانت کاربر) نیز یک Claim پیشفرض است.
4) اگر نقشهایی به کاربر انتساب داده شده باشند، تمام این نقشها واکشی شده و به عنوان یک Claim جدید به کوکی او اضافه میشوند.
5) اگر یک نقش منتسب به کاربر دارای Role Claim باشد، این موارد نیز واکشی شده و به کوکی او به عنوان یک Claim جدید اضافه میشوند. در ASP.NET Identity Core نقشها نیز میتوانند Claim داشته باشند (امکان پیاده سازی سطوح دسترسی پویا).
بنابراین حداقل مدیریت Claims این 5 مورد خودکار است و اگر برای مثال نیاز به Id کاربر لاگین شده را داشتید، نیازی نیست تا آنرا از بانک اطلاعاتی واکشی کنید. چون این اطلاعات هم اکنون در کوکی او موجود هستند.
سفارشی سازی کلاس UserClaimsPrincipalFactory جهت افزودن User Claims سفارشی
تا اینجا دریافتیم که کلاس UserClaimsPrincipalFactory کار مدیریت Claims پیشفرض این فریم ورک را برعهده دارد. در ادامه از این کلاس ارث بری کرده و متد CreateAsync آنرا جهت افزودن Claims سفارشی خود بازنویسی میکنیم. این پیاده سازی سفارشی را در کلاس ApplicationClaimsPrincipalFactory میتوانید مشاهده کنید:
public override async Task<ClaimsPrincipal> CreateAsync(User user) { var principal = await base.CreateAsync(user).ConfigureAwait(false); addCustomClaims(user, principal); return principal; } private static void addCustomClaims(User user, IPrincipal principal) { ((ClaimsIdentity) principal.Identity).AddClaims(new[] { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString(), ClaimValueTypes.Integer), new Claim(ClaimTypes.GivenName, user.FirstName ?? string.Empty), new Claim(ClaimTypes.Surname, user.LastName ?? string.Empty), new Claim(PhotoFileName, user.PhotoFileName ?? string.Empty, ClaimValueTypes.String), }); }
برای مثال نام، نام خانوادگی و نام تصویر شخص به صورت Claimهایی جدید به کوکی او اضافه میشوند. در این حالت دیگر نیازی نیست تا به ازای هر کاربر، جدول AppUserClaims را ویرایش کرد و اطلاعات جدیدی را افزود و یا تغییر داد. همینقدر که کاربر به سیستم لاگین کند، شیء User او به متد Create کلاس UserClaimsPrincipalFactory ارسال میشود. به این ترتیب میتوان به تمام خواص این کاربر دسترسی یافت و در صورت نیاز آنها را به صورت Claimهایی به کوکی او افزود.
پس از تدارک کلاس ApplicationClaimsPrincipalFactory، تنها کاری را که باید در جهت معرفی و جایگرینی آن انجام داد، تغییر ذیل در کلاس IdentityServicesRegistry است:
services.AddScoped<IUserClaimsPrincipalFactory<User>, ApplicationClaimsPrincipalFactory>(); services.AddScoped<UserClaimsPrincipalFactory<User, Role>, ApplicationClaimsPrincipalFactory>();
چگونه به اطلاعات User Claims در سرویسهای برنامه دسترسی پیدا کنیم؟
برای دسترسی به اطلاعات User Claims نیاز به دسترسی به HttpContext جاری را داریم. در این مورد و تزریق سرویس IHttpContextAccessor جهت تامین آن، در مطلب «بررسی روش دسترسی به HttpContext در ASP.NET Core» پیشتر بحث شدهاست.
به علاوه در کلاس IdentityServicesRegistry، تزریق وابستگیهای سفارشیتری نیز صورت گرفتهاست:
services.AddScoped<IPrincipal>(provider => provider.GetService<IHttpContextAccessor>()?.HttpContext?.User ?? ClaimsPrincipal.Current);
چگونه اطلاعات User Claims سفارشی را دریافت کنیم؟
برای کار سادهتر با Claims یک کلاس کمکی به نام IdentityExtensions به پروژه اضافه شدهاست و متدهایی مانند دو متد ذیل را میتوانید در آن مشاهده کنید:
public static string FindFirstValue(this ClaimsIdentity identity, string claimType) { return identity?.FindFirst(claimType)?.Value; } public static string GetUserClaimValue(this IIdentity identity, string claimType) { var identity1 = identity as ClaimsIdentity; return identity1?.FindFirstValue(claimType); }
برای نمونه متد GetUserDisplayName این کلاس کمکی، از همان Claims سفارشی که در کلاس ApplicationClaimsPrincipalFactory تعریف کردیم، اطلاعات خود را استخراج میکند و اگر در View ایی خواستید این اطلاعات را نمایش دهید، میتوانید بنویسید:
@User.Identity.GetUserDisplayName()
چگونه پس از ویرایش اطلاعات کاربر، اطلاعات کوکی او را نیز به روز کنیم؟
در پروژه قسمتی وجود دارد جهت ویرایش اطلاعات کاربران (UserProfileController). اگر کاربری برای مثال نام و نام خانوادگی خود را ویرایش کرد، میخواهیم بلافاصله متد GetUserDisplayName اطلاعات صحیح و به روزی را از کوکی او دریافت کند. برای اینکار یا میتوان او را وادار به لاگین مجدد کرد (تا پروسهی رسیدن به متد CreateAsync کلاس ApplicationClaimsPrincipalFactory طی شود) و یا روش بهتری نیز وجود دارد:
// reflect the changes, in the current user's Identity cookie await _signInManager.RefreshSignInAsync(user).ConfigureAwait(false);
کدهای کامل این سری را در مخزن کد DNT Identity میتوانید ملاحظه کنید.
ساختار درختی
اصطلاحات درخت
در شکل بالا دایرههایی برای هر بخش از اطلاعت کشیده شده و ارتباط هر کدام از آنها از طریق یک خط برقرار شده است. اعداد داخل هر دایره تکراری نیست و همه منحصر به فرد هستند. پس وقتی از اعداد اسم ببریم متوجه میشویم که در مورد چه چیزی صحبت میکنیم.
در شکل بالا به هر یک از دایرهها یک گره Node میگویند و به هر خط ارتباط دهنده بین گرهها لبه Edge گفته میشود. گرههای 19 و 21 و 14 زیر گرههای گره 7 محسوب میشوند. گرههایی که به صورت مستقیم به زیر گرههای خودشان اشاره میکنند را گرههای والد Parent میگویند و زیرگرههای 7 را گرههای فرزند ChildNodes. پس با این حساب میتوانیم بگوییم گرههای 1 و 12 و 31 را هم فرزند گره 19 هستند و گره 19 والد آن هاست. همچنین گرههای یک والد را مثل 19 و 21 و 14 که والد مشترک دارند، گرههای خواهر و برادر یا حتی همنژاد Sibling میگوییم. همچنین ارتباط بین گره 7 و گرههای سطح دوم و الی آخر یعنی 1 و 12 و 31 و 23 و 6 را که والد بودن آن به صورت غیر مستقیم است را جد یا ancestor مینامیم و نوهها و نتیجههای آنها را نسل descendants.
ریشه Root: به گرهای میگوییم که هیچ والدی ندارد و خودش در واقع اولین والد محسوب میشود؛ مثل گره 7.
برگ Leaf: به گرههایی که هیچ فرزندی ندارند، برگ میگوییم. مثال گرههای 1 و12 و 31 و 23 و 6
گرههای داخلی Internal Nodes: گره هایی که نه برگ هستند و نه ریشه. یعنی حداقل یک فرزند دارند و خودشان یک گره فرزند محسوب میشوند؛ مثل گرههای 19 و 14.
مسیر Path: راه رسیدن از یک گره به گره دیگر را مسیر میگویند. مثلا گرههای 1 و 19 و 7 و 21 به ترتیب یک مسیر را تشکیل میدهند ولی گرههای 1 و 19 و 23 از آن جا که هیچ جور اتصالی بین آنها نیست، مسیری را تشکیل نمیدهند.
طول مسیر Length of Path: به تعداد لبههای یک مسیر، طول مسیر میگویند که میتوان از تعداد گرهها -1 نیز آن را به دست آورد. برای نمونه : مسیر 1 و19 و 7 و 21 طول مسیرشان 3 هست.
عمق Depth: طول مسیر یک گره از ریشه تا آن گره را عمق درخت میگویند. عمق یک ریشه همیشه صفر است و برای مثال در درخت بالا، گره 19 در عمق یک است و برای گره 23 عمق آن 2 خواهد بود.
تعریف خود درخت Tree: درخت یک ساختار داده برگشتی recursive است که شامل گرهها و لبهها، برای اتصال گرهها به یکدیگر است.
جملات زیر در مورد درخت صدق میکند:
- هر گره میتواند فرزند نداشته باشد یا به هر تعداد که میخواهد فرزند داشته باشد.
- هر گره یک والد دارد و تنها گرهای که والد ندارد، گره ریشه است (البته اگر درخت خالی باشد هیچ گره ای وجود ندارد).
- همه گرهها از ریشه قابل دسترسی هستند و برای دسترسی به گره مورد نظر باید از ریشه تا آن گره، مسیری را طی کرد.
برای پیاده سازی یک درخت، از دو کلاس یکی جهت ساخت گره که حاوی اطلاعات است <TreeNode<T و دیگری جهت ایجاد درخت اصلی به همراه کلیه متدها و خاصیت هایش <Tree<T کمک میگیریم.
public class TreeNode<T> { // شامل مقدار گره است private T value; // مشخص میکند که آیا گره والد دارد یا خیر private bool hasParent; // در صورت داشتن فرزند ، لیست فرزندان را شامل میشود private List<TreeNode<T>> children; /// <summary>سازنده کلاس </summary> /// <param name="value">مقدار گره</param> public TreeNode(T value) { if (value == null) { throw new ArgumentNullException( "Cannot insert null value!"); } this.value = value; this.children = new List<TreeNode<T>>(); } /// <summary>خاصیتی جهت مقداردهی گره</summary> public T Value { get { return this.value; } set { this.value = value; } } /// <summary>تعداد گرههای فرزند را بر میگرداند</summary> public int ChildrenCount { get { return this.children.Count; } } /// <summary>به گره یک فرزند اضافه میکند</summary> /// <param name="child">آرگومان این متد یک گره است که قرار است به فرزندی گره فعلی در آید</param> public void AddChild(TreeNode<T> child) { if (child == null) { throw new ArgumentNullException( "Cannot insert null value!"); } if (child.hasParent) { throw new ArgumentException( "The node already has a parent!"); } child.hasParent = true; this.children.Add(child); } /// <summary> /// گره ای که اندیس آن داده شده است بازگردانده میشود /// </summary> /// <param name="index">اندیس گره</param> /// <returns>گره بازگشتی</returns> public TreeNode<T> GetChild(int index) { return this.children[index]; } } /// <summary>این کلاس ساختار درخت را به کمک کلاس گرهها که در بالا تعریف کردیم میسازد</summary> /// <typeparam name="T">نوع مقادیری که قرار است داخل درخت ذخیره شوند</typeparam> public class Tree<T> { // گره ریشه private TreeNode<T> root; /// <summary>سازنده کلاس</summary> /// <param name="value">مقدار گره اول که همان ریشه میشود</param> public Tree(T value) { if (value == null) { throw new ArgumentNullException( "Cannot insert null value!"); } this.root = new TreeNode<T>(value); } /// <summary>سازنده دیگر برای کلاس درخت</summary> /// <param name="value">مقدار گره ریشه مثل سازنده اول</param> /// <param name="children">آرایه ای از گرهها که فرزند گره ریشه میشوند</param> public Tree(T value, params Tree<T>[] children) : this(value) { foreach (Tree<T> child in children) { this.root.AddChild(child.root); } } /// <summary> /// ریشه را بر میگرداند ، اگر ریشه ای نباشد نال بر میگرداند /// </summary> public TreeNode<T> Root { get { return this.root; } } /// <summary>پیمودن عرضی و نمایش درخت با الگوریتم دی اف اس </summary> /// <param name="root">ریشه (گره ابتدایی) درختی که قرار است پیمایش از آن شروع شود</param> /// <param name="spaces">یک کاراکتر جهت جداسازی مقادیر هر گره</param> private void PrintDFS(TreeNode<T> root, string spaces) { if (this.root == null) { return; } Console.WriteLine(spaces + root.Value); TreeNode<T> child = null; for (int i = 0; i < root.ChildrenCount; i++) { child = root.GetChild(i); PrintDFS(child, spaces + " "); } } /// <summary>متد پیمایش درخت به صورت عمومی که تابع خصوصی که در بالا توضیح دادیم را صدا میزند</summary> public void TraverseDFS() { this.PrintDFS(this.root, string.Empty); } } /// <summary> /// کد استفاده از ساختار درخت /// </summary> public static class TreeExample { static void Main() { // Create the tree from the sample Tree<int> tree = new Tree<int>(7, new Tree<int>(19, new Tree<int>(1), new Tree<int>(12), new Tree<int>(31)), new Tree<int>(21), new Tree<int>(14, new Tree<int>(23), new Tree<int>(6)) ); // پیمایش درخت با الگوریتم دی اف اس یا عمقی tree.TraverseDFS(); // خروجی // 7 // 19 // 1 // 12 // 31 // 21 // 14 // 23 // 6 } }
پیمایش درخت به روش عمقی (DFS (Depth First Search
هدف از پیمایش درخت ملاقات یا بازبینی (تهیه لیستی از همه گرههای یک درخت) تنها یکبار هر گره در درخت است. برای این کار الگوریتمهای زیادی وجود دارند که ما در این مقاله تنها دو روش DFS و BFS را بررسی میکنیم.
روش DFS: هر گرهای که به تابع بالا بدهید، آن گره برای پیمایش، گره ریشه حساب خواهد شد و پیمایش از آن آغاز میگردد. در الگوریتم DFS روش پیمایش بدین گونه است که ما از گره ریشه آغاز کرده و گره ریشه را ملاقات میکنیم. سپس گرههای فرزندش را به دست میآوریم و یکی از گرهها را انتخاب کرده و دوباره همین مورد را رویش انجام میدهیم تا نهایتا به یک برگ برسیم. وقتی که به برگی میرسیم یک مرحله به بالا برگشته و این کار را آنقدر تکرار میکنیم تا همهی گرههای آن ریشه یا درخت پیمایش شده باشند.
همین درخت را در نظر بگیرید:
پیمایش درخت را از گره 7 آغاز میکنیم و آن را به عنوان ریشه در نظر میگیریم. حتی میتوانیم پیمایش را از گره مثلا 19 آغاز کنیم و آن را برای پیمایش ریشه در نظر بگیریم ولی ما از همان 7 پیمایش را آغاز میکنیم:
ابتدا گره 7 ملاقات شده و آن را مینویسیم. سپس فرزندانش را بررسی میکنیم که سه فرزند دارد. یکی از فرزندان مثل گره 19 را انتخاب کرده و آن را ملاقات میکنیم (با هر بار ملاقات آن را چاپ میکنیم) سپس فرزندان آن را بررسی میکنیم و یکی از گرهها را انتخاب میکنیم و ملاقاتش میکنیم؛ برای مثال گره 1. از آن جا که گره یک، برگ است و فرزندی ندارد یک مرحله به سمت بالا برمیگردیم و برگهای 12 و 31 را هم ملاقات میکنیم. حالا همهی فرزندان گره 19 را بررسی کردیم، بر میگردیم یک مرحله به سمت بالا و گره 21 را ملاقات میکنیم و از آنجا که گره 21 برگ است و فرزندی ندارد به بالا باز میگردیم و بعد گره 14 و فرزندانش 23 و 6 هم بررسی میشوند. پس ترتیب چاپ ما اینگونه میشود:
7-19-1-12-31-21-14-23-6
پیمایش درخت به روش (BFS (Breadth First Search
در این روش (پیمایش سطحی) گره والد ملاقات شده و سپس همه گرههای فرزندش ملاقات میشوند. بعد از آن یک گره انتخاب شده و همین پیمایش مجددا روی آن انجام میشود تا آن سطح کاملا پیمایش شده باشد. سپس به همین مرحله برگشته و فرزند بعدی را پیمایش میکنیم و الی آخر. نمونهی این پیمایش روی درخت بالا به صورت زیر نمایش داده میشود:
7-19-21-14-1-12-31-23-6
اگر خوب دقت کنید میبینید که پیمایش سطحی است و هر سطح به ترتیب ملاقات میشود. به این الگوریتم، پیمایش موجی هم میگویند. دلیل آن هم این است که مثل سنگی میماند که شما برای ایجاد موج روی دریاچه پرتاب میکنید.
برای این پیمایش از صف کمک گرفته میشود که مراحل زیر روی صف صورت میگیرد:
- ریشه وارد صف Q میشود.
- دو مرحله زیر مرتبا تکرار میشوند:
- اولین گره صف به نام V را از Q در یافت میکنیم و آن را چاپ میکنیم.
- فرزندان گره V را به صف اضافه میکنیم.
"We no longer believe the git.php.net server has been compromised. However, it is possible that the master.php.net user database leaked," Nikita Popov said in a message posted on its mailing list on April 6.
using System; namespace CS6NewFeatures { class Person { public string FirstName { set; get; } public string LastName { set; get; } public int Age { set; get; } } class Program { static void Main(string[] args) { var person = new Person { FirstName = "User 1", LastName = "Last Name 1", Age = 50 }; var message = string.Format("Hello! My name is {0} {1} and I am {2} years old.", person.FirstName, person.LastName, person.Age); Console.Write(message); } } }
در C# 6 جهت رفع این مشکلات، راه حلی به نام String interpolation ارائه شدهاست و اگر افزونهی ReSharper یا یکی از افزونههای Roslyn را نصب کرده باشید، به سادگی امکان تبدیل کدهای قدیمی را به فرمت جدید آن خواهید یافت:
در این حالت کد قدیمی فوق، به کد ذیل تبدیل خواهد شد:
static void Main(string[] args) { var person = new Person { FirstName = "User 1", LastName = "Last Name 1", Age = 50 }; var message = $"Hello! My name is {person.FirstName} {person.LastName} and I am {person.Age} years old."; Console.Write(message); }
عملیاتی که در اینجا توسط کامپایلر صورت خواهد گرفت، تبدیل این کدهای جدید مبتنی بر String interpolation به همان string.Format قدیمی در پشت صحنهاست. بنابراین این قابلیت جدید C# 6 را به کدهای قدیمی خود نیز میتوانید اعمال کنید. فقط کافی است VS 2015 را نصب کرده باشید و دیگر شمارهی دات نت فریم ورک مورد استفاده مهم نیست.
امکان انجام محاسبات با String interpolation
زمانیکه $ در ابتدای رشته قرار گرفت، عبارات داخل {}ها توسط کامپایلر محاسبه و جایگزین میشوند. بنابراین میتوان چنین محاسباتی را نیز انجام داد:
var message2 = $"{Environment.NewLine}Test {DateTime.Now}, {3*2}"; Console.Write(message2);
تغییر فرمت عبارات نمایش داده شده توسط String interpolation
همانطور که با string.Format میتوان نمایش سه رقم جدا کنندهی هزارها را فعال کرد و یا تاریخی را به نحوی خاص نمایش داد، در اینجا نیز همان قابلیتها برقرار هستند و باید پس از ذکر یک : عنوان شوند:
var message3 = $"{Environment.NewLine}{1000000:n0} {DateTime.Now:dd-MM-yyyy}"; Console.Write(message3);
سفارشی سازی String interpolation
اگر متغیر رشتهای معرفی شدهی توسط $ را با یک var مشخص کنیم، نوع آن به صورت پیش فرض، از نوع string خواهد بود. برای نمونه در مثالهای فوق، message و message2 از نوع string تعریف میشوند. اما این رشتههای ویژه را میتوان از نوع IFormattable و یا FormattableString نیز تعریف کرد.
در حقیقت رشتههای آغاز شدهی با $ از نوع IFormattable هستند و اگر نوع متغیر آنها ذکر نشود، به صورت خودکار به نوع FormattableString که اینترفیس IFormattable را پیاده سازی میکند، تبدیل میشوند. بنابراین پیاده سازی این اینترفیس، امکان سفارشی سازی خروجی string interpolation را میسر میکند. برای نمونه میخواهیم در مثال message2، نحوهی نمایش تاریخ را سفارشی سازی کنیم.
class MyDateFormatProvider : IFormatProvider { readonly MyDateFormatter _formatter = new MyDateFormatter(); public object GetFormat(Type formatType) { return formatType == typeof(ICustomFormatter) ? _formatter : null; } class MyDateFormatter : ICustomFormatter { public string Format(string format, object arg, IFormatProvider formatProvider) { if (arg is DateTime) return ((DateTime)arg).ToString("MM/dd/yyyy"); return arg.ToString(); } } }
پس از پیاده سازی این سفارشی کنندهی تاریخ، نحوهی استفادهی از آن به صورت ذیل است:
static string formatMyDate(FormattableString formattable) { return formattable.ToString(new MyDateFormatProvider()); }
در ادامه برای اعمال این سفارشی سازی، فقط کافی است متد formatMyDate را به رشتهی مدنظر اعمال کنیم:
var message2 = formatMyDate($"{Environment.NewLine}Test {DateTime.Now}, {3*2}"); Console.Write(message2);
و اگر تنها میخواهید فرهنگ جاری را عوض کنید، از روش سادهی زیر استفاده نمائید:
public static string faIr(IFormattable formattable) { return formattable.ToString(null, new CultureInfo("fa-Ir")); }
نمونهی کاربردیتر آن اعمال InvariantCulture به String interpolation است:
static string invariant(FormattableString formattable) { return formattable.ToString(CultureInfo.InvariantCulture); }
یک نکته: همانطور که عنوان شد این قابلیت جدید با نگارشهای قبلی دات نت نیز سازگار است؛ اما این کلاسهای جدید را در این نگارشها نخواهید یافت. برای رفع این مشکل تنها کافی است این کلاسهای یاد شده را به صورت دستی در فضای نام اصلی آنها تعریف و اضافه کنید. یک مثال
غیرفعال سازی String interpolation
اگر میخواهید در رشتهای که با $ شروع شده، بجای محاسبهی عبارتی، دقیقا خود آنرا نمایش دهید (و { را escape کنید)، از {{}} استفاده کنید:
var message0 = $"Hello! My name is {person.FirstName} {{person.FirstName}}";
پردازش عبارات شرطی توسط String interpolation
همانطور که عنوان شد، امکان ذکر یک عبارت کامل هم در بین {} وجود دارد (محاسبات، ذکر یک عبارت LINQ، ذکر یک متد و امثال آن). اما در این میان اگر یک عبارت شرطی مدنظر بود، باید بین () قرار گیرد:
Console.Write($"{(person.Age>50 ? "old": "young")}");
سؤال: مرورگر چه زمانی از کش محلی خودش استفاده خواهد کرد (بدون ارسال درخواستی به سرور) و چه زمانی مجددا از سرور درخواست دریافت مجدد این عنصر کش شده را میکند؟
برای پاسخ دادن به این سؤال نیاز است با مفهومی به نام Conditional Requests (درخواستهای شرطی) آشنا شد که در ادامه به بررسی آن خواهیم پرداخت.
درخواستهای شرطی
مرورگرهای وب دو نوع درخواست شرطی و غیر شرطی را توسط پروتکل HTTP و HTTPS ارسال میکنند. دراینجا، زمانی یک درخواست غیرشرطی ارسال میشود که نسخهی ذخیره شدهی محلی منبع مورد نظر، مهیا نباشد. در این حالت، اگر منبع درخواستی در سرور موجود باشد، در پاسخ ارسالی خود وضعیت 200 یا HTTP/200 OK را باز میگرداند. اگر هدرهای دیگری نیز مانند کش کردن منبع در اینجا تنظیم شده باشند، مرورگر نتیجهی دریافتی را برای استفادهی بعدی ذخیره خواهد کرد.
در بار دومی که منبع مفروضی درخواست میگردد، مرورگر ابتدا به کش محلی خود نگاه خواهد کرد. همچنین در این حالت نیاز دارد که بداند این کش معتبر است یا خیر؟ برای بررسی این مورد ابتدا هدرهای ذخیره شده به همراه منبع، بررسی میشوند. پس از این بررسی اگر مرورگر به این نتیجه برسد که کش محلی معتبر است، دیگر درخواستی را به سرور ارسال نخواهد کرد.
اما در آینده اگر مدت زمان کش شدن تنظیم شده توسط هدرهای مرتبط، منقضی شده باشد (برای مثال با توجه به max-age هدر کش شدن منبع)، مرورگر هنوز هم درخواست کاملی را برای دریافت نسخهی جدید منبع مورد نیاز، به سرور ارسال نمیکند. در اینجا ابتدا یک conditional request را به وب سرور ارسال میکند (یک درخواست شرطی). این درخواست شرطی تنها دارای هدرهای If-Modified-Since و یا If-None-Match است و هدف از آن سؤال پرسیدن از وب سرور است که آیا این منبع خاص، در سمت سرور اخیرا تغییر کردهاست یا خیر؟ اگر پاسخ سرور خیر باشد، باز هم از همان کش محلی استفاده خواهد شد و مجددا درخواست کاملی برای دریافت نمونهی جدیدتر منبع مورد نیاز، به سرور ارسال نمیگردد.
پاسخی که سرور جهت مشخص سازی عدم تغییر منابع خود ارسال میکند، با هدر HTTP/304 Not Modified مشخص میگردد (این پاسخ هیچ body خاصی نداشته و فقط یک سری هدر است). اما اگر منبع درخواستی اخیرا تغییر کرده باشد، پاسخ HTTP/200 OK را در هدر بازگشت داده شده، به مرورگر بازخواهد گرداند (یعنی محتوا را مجددا دریافت کن).
چه زمانی مرورگر درخواستهای شرطی If-Modified-Since را به سرور ارسال میکند؟
اگر یکی از شرایط ذیل برقرار باشد، مرورگر حتی اگر تاریخ کش شدن منبع ویژهای به 10 سال بعد تنظیم شده باشد، مجددا یک درخواست شرطی را برای بررسی اعتبار کش محلی خود به سرور ارسال میکند:
الف) کش شدن بر اساس هدر خاصی به نام vary صورت گرفتهاست (برای مثال بر اساس id یا نام یک فایل).
ب) اگر نحوهی هدایت به صفحهی جاری از طریق META REFRESH باشد.
ج) اگر از طریق کدهای جاوا اسکریپتی، دستور reload صفحه صادر شود.
د) اگر کاربر دکمهی refresh را فشار دهد.
ه) اگر قسمتی از صفحه توسط پروتکل HTTP و قسمتی دیگر از آن توسط پروتکل HTTPS ارائه شود.
و ... اگر بر اساس هدر تاریخ مدت زمان کش شدن منبع، زمان منقضی شدن آن فرا رسیده باشد.
مدیریت درخواستهای شرطی در ASP.NET MVC
تا اینجا به این نکته رسیدیم که قرار دادن ویژگی Output cache بر روی یک اکشن متد، الزاما به معنای کش شدن آن تا مدت زمان تعیین شده نخواهد بود و مرورگر ممکن است (در یکی از 6 حالت ذکر شده فوق) توسط ارسال هدر If-Modified-Since ، سعی در تعیین اعتبار کش محلی خود کند و اگر پاسخ 304 را از سرور دریافت نکند، حتما نسبت به دریافت مجدد و کامل آن منبع اقدام خواهد کرد.
سؤال: چگونه میتوان هدر If-Modified-Since را در ASP.NET MVC مدیریت کرد؟
پاسخ: اگر از فیلتر OutputCache استفاده میکنید، به صورت خودکار هدر Last-Modified را اضافه میکند؛ اما این مورد کافی نیست.
در ادامه یک کنترلر و اکشن متد GetImage آنرا ملاحظه میکنید که تصویری را از مسیر app_data/images خوانده و بازگشت میدهد. همچنین این تصویر بازگشت داده شده را نیز با توجه به OutputCache آن به مدت یک ماه کش میکند.
using System.IO; using System.Web.Mvc; namespace MVC4Basic.Controllers { public class HomeController : Controller { public ActionResult Index() { return View(); } const int AMonth = 30 * 86400; [OutputCache(Duration = AMonth, VaryByParam = "name")] public ActionResult GetImage(string name) { name = Path.GetFileName(name); var path = Server.MapPath(string.Format("~/app_data/images/{0}", name)); var content = System.IO.File.ReadAllBytes(path); return File(content, "image/png", name); } } }
<img src="@Url.Action("GetImage","Home", new { name = "test.png"})"/>
بار اول که صفحهی اول برنامه درخواست میشود، یک چنین هدرهایی رد و بدل خواهند شد (توسط ابزارهای توکار مرورگر وب کروم تهیه شدهاست؛ همان دکمهی F12 معروف):
Remote Address:127.0.0.1:5656 Request URL:http://localhost:10419/Home/GetImage?name=test.png Request Method:GET Status Code:200 OK Response Headers Cache-Control:public, max-age=2591916 Expires:Sat, 31 May 2014 12:45:55 GMT Last-Modified:Thu, 01 May 2014 12:45:55 GMT
در همین حال اگر صفحه را ریفرش کنیم (فشردن دکمهی F5)، اینبار هدرهای حاصل چنین شکلی را پیدا میکنند:
Remote Address:127.0.0.1:5656 Request URL:http://localhost:10419/Home/GetImage?name=test.png Request Method:GET Status Code:304 Not Modified Request Headers If-Modified-Since:Thu, 01 May 2014 12:45:55 GMT
در ادامه بجای اینکه صفحه را ریفرش کنیم، یکبار دیگر در نوار آدرس آن، دکمهی Enter را فشار خواهیم داد تا آدرس موجود در آن (ریشه سایت) مجددا در حالت معمولی دریافت شود.
Remote Address:127.0.0.1:5656 Request URL:http://localhost:10419/Home/GetImage?name=test.png Request Method:GET Status Code:200 OK (from cache)
مشکل! مرورگر را ببندید، تا کار دیباگ برنامه خاتمه یابد. مجددا برنامه را اجرا کنید. مشاهده خواهید کرد که ... اجرای برنامه در Break point قرار گرفته در سطر اول متد GetImage متوقف میشود. چرا؟! مگر قرار نبود تا یک ماه دیگر کش شود؟! هدر رد و بدل شده نیز Status Code:200 OK کامل است (که سبب دریافت کامل فایل میشود).
Remote Address:127.0.0.1:5656 Request URL:http://localhost:10419/Home/GetImage?name=test.png Request Method:GET Status Code:200 OK Request Headers If-Modified-Since:Thu, 01 May 2014 12:45:55 GMT
using System; using System.IO; using System.Net; using System.Web.Mvc; namespace MVC4Basic.Controllers { public class HomeController : Controller { public ActionResult Index() { return View(); } const int AMonth = 30 * 86400; [OutputCache(Duration = AMonth, VaryByParam = "name")] public ActionResult GetImage(string name) { name = Path.GetFileName(name); var path = Server.MapPath(string.Format("~/app_data/images/{0}", name)); var lastWriteTime = System.IO.File.GetLastWriteTime(path); this.Response.Cache.SetLastModified(lastWriteTime.ToUniversalTime()); var header = this.Request.Headers["If-Modified-Since"]; if (!string.IsNullOrWhiteSpace(header)) { DateTime isModifiedSince; if (DateTime.TryParse(header, out isModifiedSince) && isModifiedSince > lastWriteTime) { return new HttpStatusCodeResult(HttpStatusCode.NotModified); } } var content = System.IO.File.ReadAllBytes(path); return File(content, "image/png", name); } } }
برای امتحان آن همانطور که عنوان شد فقط کافی است یکبار مرورگر خود را کاملا بسته و مجددا برنامه را اجرا کنید.
Remote Address:127.0.0.1:5656 Request URL:http://localhost:10419/Home/GetImage?name=test.png Request Method:GET Status Code:304 Not Modified Request Headers If-Modified-Since:Thu, 01 May 2014 13:43:32 GMT
موارد کاربرد
اکثر فید خوانهای معروف نیز ابتدا هدر If-Modified-Since را ارسال میکنند و سپس (اگر چیزی تغییر کرده بود) محتوای فید شما را دریافت خواهند کرد. بنابراین برای کاهش بار برنامه و هچنین کاهش میزان انتقال دیتای سایت، مدیریت آن در حین ارائه محتوای پویای فیدها نیز بهتر است صورت گیرد. همچنین هر جایی که قرار است فایلی به صورت پویا به کاربران ارائه شود؛ مانند مثال فوق.
تبدیل این کدها به روش سازگار با ASP.NET MVC
ما در اینجا رسیدیم به یک سری کد تکراری if و else که باید در هر اکشن متدی که OutputCache دارد، تکرار شود. روش AOP وار آن در ASP.NET MVC، تبدیل این کدها به یک فیلتر با قابلیت استفادهی مجدد است:
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] public sealed class SetIfModifiedSinceAttribute : ActionFilterAttribute { public string Parameter { set; get; } public string BasePath { set; get; } public override void OnActionExecuting(ActionExecutingContext filterContext) { var response = filterContext.RequestContext.HttpContext.Response; var request = filterContext.RequestContext.HttpContext.Request; var path = getPath(filterContext); if (string.IsNullOrWhiteSpace(path)) { response.StatusCode = (int)HttpStatusCode.NotFound; filterContext.Result = new EmptyResult(); return; } var lastWriteTime = File.GetLastWriteTime(path); response.Cache.SetLastModified(lastWriteTime.ToUniversalTime()); var header = request.Headers["If-Modified-Since"]; if (string.IsNullOrWhiteSpace(header)) return; DateTime isModifiedSince; if (DateTime.TryParse(header, out isModifiedSince) && isModifiedSince > lastWriteTime) { response.StatusCode = (int)HttpStatusCode.NotModified; response.SuppressContent = true; filterContext.Result = new EmptyResult(); } } string getPath(ActionExecutingContext filterContext) { if (!filterContext.ActionParameters.ContainsKey(Parameter)) return string.Empty; var name = filterContext.ActionParameters[Parameter] as string; if (string.IsNullOrWhiteSpace(name)) return string.Empty; var path = Path.GetFileName(name); path = filterContext.HttpContext.Server.MapPath(string.Format("{0}/{1}", BasePath, path)); return !File.Exists(path) ? string.Empty : path; } }
و برای استفاده از آن خواهیم داشت:
[SetIfModifiedSince(Parameter = "name", BasePath = "~/app_data/images/")] [OutputCache(Duration = AMonth, VaryByParam = "name")] public ActionResult GetImage(string name) { name = Path.GetFileName(name); var path = Server.MapPath(string.Format("~/app_data/images/{0}", name)); var content = System.IO.File.ReadAllBytes(path); return File(content, "image/png", name); }
خلاصهی بحث
چون فیلتر OutputCache در ASP.NET MVC، هدر If-Modified-Since را پردازش نمیکند (از این جهت که پردازش آن برای نمونه در مثال فوق وابسته به منطق خاصی است و عمومی نیست)، اگر با هر بار گشودن سایت خود مشاهده کردید، تصاویر پویایی که قرار بوده یک ماه کش شوند، دوباره از سرور درخواست میشوند (البته به ازای هرباری که مرورگر از نو اجرا میشود و نه در دفعات بعدی که صفحات سایت با همان وهلهی ابتدایی مرور خواهند شد)، نیاز است خودتان دسترسی کار پردازش هدر If-Modified-Since را انجام داده و سپس status code 304 را در صورت نیاز، ارسال کنید.
و در حالت عمومی، طراحی سیستم caching محتوای پویای شما بدون پردازش هدر If-Modified-Since ناقص است (تفاوتی نمیکند که از کدام فناوری سمت سرور استفاده میکنید).
برای مطالعه بیشتر
Understanding Conditional Requests and Refresh
Use If-Modified-Since header in ASP.NET
Make your browser cache the output of an HttpHandler
304 Your images from a database
Conditional GET
Website Performance with ASP.NET - Part4 - Use Cache Headers
ASP.NET MVC 304 Not Modified Filter for Syndication Content
namespace MyNewsWCFLibrary { using System; using System.Collections.Generic; public partial class tblNews { public int tblNewsId { get; set; } public int tblCategoryId { get; set; } public string Title { get; set; } public string Description { get; set; } public System.DateTime RegDate { get; set; } public Nullable<bool> IsDeleted { get; set; } public virtual tblCategory tblCategory { get; set; } } }
using System.Runtime.Serialization; namespace MyNewsWCFLibrary { using System; using System.Collections.Generic; [DataContract] public partial class tblNews { [DataMember] public int tblNewsId { get; set; } [DataMember] public int tblCategoryId { get; set; } [DataMember] public string Title { get; set; } [DataMember] public string Description { get; set; } [DataMember] public System.DateTime RegDate { get; set; } [DataMember] public Nullable<bool> IsDeleted { get; set; } public virtual tblCategory tblCategory { get; set; } } }
namespace MyNewsWCFLibrary { using System; using System.Collections.Generic; using System.Runtime.Serialization; [DataContract] public partial class tblCategory { public tblCategory() { this.tblNews = new HashSet<tblNews>(); } [DataMember] public int tblCategoryId { get; set; } [DataMember] public string CatName { get; set; } [DataMember] public bool IsDeleted { get; set; } public virtual ICollection<tblNews> tblNews { get; set; } } }
BEGIN TRANSACTION GO ALTER TABLE dbo.tblNews DROP CONSTRAINT FK_tblNews_tblCategory GO ALTER TABLE dbo.tblCategory SET (LOCK_ESCALATION = TABLE) GO COMMIT BEGIN TRANSACTION GO CREATE TABLE dbo.Tmp_tblNews ( tblNewsId int NOT NULL IDENTITY (1, 1), tblCategoryId int NOT NULL, Title nvarchar(50) NOT NULL, Description nvarchar(MAX) NOT NULL, RegDate datetime NOT NULL, IsDeleted bit NOT NULL ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] GO ALTER TABLE dbo.Tmp_tblNews SET (LOCK_ESCALATION = TABLE) GO ALTER TABLE dbo.Tmp_tblNews ADD CONSTRAINT DF_tblNews_IsDeleted DEFAULT 0 FOR IsDeleted GO SET IDENTITY_INSERT dbo.Tmp_tblNews ON GO IF EXISTS(SELECT * FROM dbo.tblNews) EXEC('INSERT INTO dbo.Tmp_tblNews (tblNewsId, tblCategoryId, Title, Description, RegDate, IsDeleted) SELECT tblNewsId, tblCategoryId, Title, Description, RegDate, IsDeleted FROM dbo.tblNews WITH (HOLDLOCK TABLOCKX)') GO SET IDENTITY_INSERT dbo.Tmp_tblNews OFF GO DROP TABLE dbo.tblNews GO EXECUTE sp_rename N'dbo.Tmp_tblNews', N'tblNews', 'OBJECT' GO ALTER TABLE dbo.tblNews ADD CONSTRAINT PK_tblNews PRIMARY KEY CLUSTERED ( tblNewsId ) WITH( STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] GO ALTER TABLE dbo.tblNews ADD CONSTRAINT FK_tblNews_tblCategory FOREIGN KEY ( tblCategoryId ) REFERENCES dbo.tblCategory ( tblCategoryId ) ON UPDATE NO ACTION ON DELETE NO ACTION GO COMMIT
schema = { username: Joi.string() .alphanum() .min(5) .max(15) .messages({ "string.empty": "نام کاربری وارد نشده است", "string.min": "حداقل تعداد حروف 5 کاراکتر میباشد", "string.max": "حداکثر تعداد حروف مجاز 15 کاراکتر میباشد", "string.alpha": "از حروف و عدد استفاده نمایید" }), password: Joi.string() .min(5) .max(12) .alphanum() .required() .messages({ "string.empty": "رمز عبور وارد نشده است", "string.min": "حداقل تعداد حروف 5 کاراکتر میباشد", "string.max": "حداکثر تعداد حروف مجاز 15 کاراکتر میباشد", "string.alpha": "از حروف و عدد استفاده نمایید" }), confirmation: Joi.any().valid(Joi.ref('password')) .messages({ "any.only": "رمز عبور مطابقت ندارد" }) };
schema = schema = Joi.object().keys({ username: Joi.string().regex(/[a-zA-Z0-9]{3,30}/).min(3).max(30).required(), password: Joi.string().regex(/[a-zA-Z0-9]{3,30}/), confirmation: Joi.ref('password') }) .with('password', 'confirmation');
سؤالات متداول
- میخواهم برنامهام را بر روی کامپیوتر مشتری بدون دردسر نصب کنم؛ با حداقل حجم و دردسر توزیع. به علاوه بدون نیاز به وصله پینه کردن اسمبلیهای تجاری دریافت شده.
پاسخ: PdfReport از چند اسمبلی دات نتی تشکیل شده است که نیاز به نصب خاصی ندارد. همچنین حجم فشرده شده آن نیز زیر 2 مگابایت است. بنابراین از جهت توزیع مشکل خاصی نخواهید داشت. همچنین کل این مجموعه سورس باز است و مشکلات متداول همراه با گزارش سازهای تجاری را به همراه ندارد.
- میخواهم فایل گزارش، به همراه برنامه و فایل exe آن و نه جدای از آن توزیع شود.
پاسخ: روش کار با PdfReport اصطلاحا code-first است. یعنی یک یا چند کلاس تهیه شده توسط شما، کار تهیه گزارش را انجام میدهند و تمام اینها کامپایل شده و به همراه فایل اجرایی یا اسمبلیهای خاصی که برای آن درنظر میگیرید، کامپایل و توزیع خواهند شد. همچنین با توجه به code-first بودن آن و عدم وابستگی به فناوری خاصی، این گزارشات در برنامههای وب و ویندوز نیز میتوانند بدون نیاز به تغییری در کدهای شما مورد استفاده قرار گیرند.
- برای کار با PdfReport از کجا باید شروع کرد؟
پاسخ: لطفا برچسب PdfReport را در سایت جاری دنبال نمائید. نحوه استفاده از قابلیتهای آن قدم به قدم توضیح داده شدهاند.
- میخواهم برای صرفه جویی در کاغذ چاپی، گزارش چند ستونهای را تهیه کنم.
پاسخ: لطفا مراجعه کنید به مثال ایجاد قالبهای سفارشی ستونها.
- میخواهم گزارشی را تولید کنم که حجم متن فیلدهای آن مشخص نیست. یکی ممکن است نصف صفحه باشد دیگری دو صفحه و یا بعضی تنها یک سطر.
پاسخ: در PdfReport ارتفاع هر سطر به صورت خودکار بر مبنای حجم وارد شده محاسبه و تنظیم میگردد. به عبارتی این تنظیم ثابت نیست و سبب حذف محتوای ارائه شده در آنها یا محو شدن ردیفهای دیگر نمیگردد.
- نیاز به گزارش سازی دارم که بدون مشکل با ORMها کار کند. برای مثال در حین کار با Entity framework مستقیما بتواند با اشیاء و لیستهای حاصل از آن کار کند.
پاسخ: یکی از انواع منابع داده تعریف شده در PdfReport جهت کار با ORMها طراحی شده است که مثالی از نحوه استفاده از آنرا در مثال EF Code first همراه با مجموعه مثالهای این کتابخانه میتوانید ملاحظه نمائید.
- آیا این کتابخانه میتواند از فایل سیستم (و نه صرفا بانک اطلاعاتی) هم تصاویر را دریافت و در گزارشات قرار دهد؟
پاسخ: بلی. لطفا به مثال ImageFilePath مراجعه نمائید.
- میخواهم یک گزارش ساز پویا در برنامه داشته باشم. فقط کوئری غیر مشخصی را به آن بدهم و حاصل آن یک گزارش باشد.
پاسخ: امکان تهیه گزارشهای پویا نیز در PdfReport پیش بینی شده است. توضیحات بیشتر همچنین این امکان در حین کار با ORMها نیز وجود دارد.
- میخواهم بدون استفاده از بانک اطلاعاتی نیز بتوانم گزارشی را تهیه کنم. برای مثال یک لیست جنریک تشکیل شده در حافظه دارم.
پاسخ: برای این منظور تنها کافی است از منبع داده صحیحی استفاده نمائید. برای اطلاعات بیشتر به مثال IList مراجعه کنید.
- میخواهم از بانکهای اطلاعاتی دیگری بجز SQL Server استفاده کنم.
پاسخ: میتوانید یک نمونه مثال استفاده از PdfReport را با بانک اطلاعاتی SQLite، در اینجا مشاهده کنید.
var list = context.BlogComment.Where(p => p.UserId == 100) .Select(p => p.Body, p.Id, p.Children) .ToList();