SeoTags Create all SEO tags you need such as meta, link, twitter card (twitter:), open graph (og:), and JSON-LD schema (structred data).
Sample output:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin /> <link rel="preconnect" href="https://www.google-analytics.com" crossorigin /> <link rel="dns-prefetch" href="https://fonts.gstatic.com/" /> <link rel="dns-prefetch" href="https://www.google-analytics.com" /> <link rel="preload" as="style" href="https://site.com/site.css" /> <link rel="preload" as="script" href="https://site.com/app.js" /> <link rel="preload" as="font" type="font/woff2" href="https://site.com/fonts/Font.woff2" crossorigin /> <link rel="preload" as="font" type="font/woff2" href="https://site.com/fonts/Font_Light.woff2" crossorigin /> <link rel="preload" as="font" type="font/woff2" href="https://site.com/fonts/Font_Medium.woff2" crossorigin /> <link rel="preload" as="font" type="font/woff2" href="https://site.com/fonts/Font_Bold.woff2" crossorigin /> <link rel="preload" as="image" type="image/jpeg" href="https://site.com/uploads/image.jpg" /> <title>SEO Tags for ASP.NET Core - My Site Title</title> <meta name="title" content="SEO Tags for ASP.NET Core - My Site Title" /> <meta name="description" content="Create all SEO tags you need such as meta, link, twitter card (twitter:), open graph (og:), and ..." /> <meta name="keywords" content="SEO, AspNetCore, MVC, RazorPages" /> <meta name="author" content="Author Name" /> <link rel="author" href="https://github.com/author-profile" /> <link rel="canonical" href="https://site.com/url/" /> <link rel="application/opensearchdescription+xml" title="My Site Title" href="https://site.com/open-search.xml" /> <link rel="alternate" type="application/rss+xml" title="Post Feeds" href="https://site.com/rss/" /> <link rel="alternate" type="application/rss+xml" title="Post Comments" href="https://site.com/post/comment/rss" /> <meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:title" content="SEO Tags for ASP.NET Core" /> <meta name="twitter:description" content="Create all SEO tags you need such as meta, link, twitter card (twitter:), open graph (og:), and ..." /> <meta name="twitter:site" content="@MySiteTwitter" /> <meta name="twitter:creator" content="@MyTwitterId" /> <meta name="twitter:image" content="https://site.com/uploads/image.jpg" /> <meta name="twitter:image:width" content="1280" /> <meta name="twitter:image:height" content="720" /> <meta name="twitter:image:alt" content="Image alt" /> <meta property="og:type" content="article" /> <meta property="og:title" content="SEO Tags for ASP.NET Core" /> <meta property="og:description" content="Create all SEO tags you need such as meta, link, twitter card (twitter:), open graph (og:), and ..." /> <meta property="og:url" content="https://site.com/url/" /> <meta property="og:site_name" content="My Site Title" /> <meta property="og:locale" content="en_US" /> <meta property="og:image" content="https://site.com/uploads/image.jpg" /> <meta property="og:image:secure_url" content="https://site.com/uploads/image.jpg" /> <meta property="og:image:type" content="image/jpeg" /> <meta property="og:image:width" content="1280" /> <meta property="og:image:height" content="720" /> <meta property="og:image:alt" content="Image alt" /> <meta property="article:publisher" content="https://facebook.com/MySite" /> <meta property="article:author" content="https://facebook.com/MyUserId" /> <meta property="article:published_time" content="2021-07-03T13:34:41+00:00" /> <meta property="article:modified_time" content="2021-07-03T13:34:41+00:00" /> <meta property="article:section" content="Article Topic" /> <meta property="article:tag" content="SEO" /> <meta property="article:tag" content="AspNetCore" /> <meta property="article:tag" content="MVC" /> <meta property="article:tag" content="RazorPages" /> <meta property="og:see_also" content="https://site.com/see-also-1" /> <meta property="og:see_also" content="https://site.com/see-also-2" /> ...
طراحی و پیاده سازی زیرساختی برای مدیریت خطاهای حاصل از Business Rule Validationها در ServiceLayer
- Exceptions for flow control: why not?
- Exception handling for flow control is EVIL!
- Replacing Throwing Exceptions with Notification in Validations
- نکات کار با استثناءها در دات نت
- Defensive Programming - بازگشت نتایج قابل پیش بینی توسط متدها
- تفاوت اعتبارسنجی ورودیها با اعتبارسنجی مرتبط با قواعد تجاری
- صدور Exception یا بازگشت Result به عنوان خروجی متد، برای رسیدگی به خطاها
- صدور استثناءها چگونه بر روی کارآیی برنامه تاثیر میگذارند؟
- Fail Fast principle
- نحوهی صحیح استفادهی از Exceptions در برنامههای NET.
- Functional C#: Handling failures, input errors
- بررسی تاثیر صدور استثناءها بر روی کارآیی برنامه
- Code review: User Controller and error handling
- Improving Managed Code Performance (Exception Management)
- پشت صحنهی استثناءها
- Domain Command Patterns - Validation
استفاده از Exception برای نمایش پیغام برای کاربر نهایی
با صدور یک استثناء و مدیریت سراسری آن در بالاترین (خارجی ترین) لایه و نمایش پیغام مرتبط با آن به کاربر نهایی، میتوان از آن به عنوان ابزاری برای ارسال هر نوع پیغامی به کاربر نهایی استفاده کرد. اگر قوانین تجاری با موفقیت برآورده نشدهاند یا لازم است به هر دلیلی یک پیغام مرتبط با یک اعتبارسنجی تجاری را برای کاربر نمایش دهید، این روش بسیار کارساز میباشد و با یکبار وقت گذاشتن برای توسعه زیرساخت برای این موضوع، به عنوان یک Cross Cutting Concern تحت عنوان Exception Management، آزادی عمل زیادی در ادامه توسعه سیستم خود خواهید داشت.
اگر مطالب پیش نیاز را مطالعه کنید، قطعا روش مطرح شده را انتخاب نخواهید کرد؛ به همین دلیل به دنبال راه حل صحیح برخورد با این سناریوها بودم که نتیجه آن را در ادامه خواهیم دید.
راه حل صحیح برای برخورد با این سناریوها بازگشت یک Result میباشد که در مطلب قبلی هم تحت عنوان OperationResult مطرح شد.
public class Result { private static readonly Result SuccessResult = new Result(true, null); protected Result(bool succeeded, string message) { if (succeeded) { if (message != null) throw new ArgumentException("There should be no error message for success.", nameof(message)); } else { if (message == null) throw new ArgumentNullException(nameof(message), "There must be error message for failure."); } Succeeded = succeeded; Error = message; } public bool Succeeded { get; } public string Error { get; } [DebuggerStepThrough] public static Result Success() { return SuccessResult; } [DebuggerStepThrough] public static Result Failed(string message) { return new Result(false, message); } [DebuggerStepThrough] public static Result<T> Failed<T>(string message) { return new Result<T>(default, false, message); } [DebuggerStepThrough] public static Result<T> Success<T>(T value) { return new Result<T>(value, true, string.Empty); } [DebuggerStepThrough] public static Result Combine(string seperator, params Result[] results) { var failedResults = results.Where(x => !x.Succeeded).ToList(); if (!failedResults.Any()) return Success(); var error = string.Join(seperator, failedResults.Select(x => x.Error).ToArray()); return Failed(error); } [DebuggerStepThrough] public static Result Combine(params Result[] results) { return Combine(", ", results); } [DebuggerStepThrough] public static Result Combine<T>(params Result<T>[] results) { return Combine(", ", results); } [DebuggerStepThrough] public static Result Combine<T>(string seperator, params Result<T>[] results) { var untyped = results.Select(result => (Result) result).ToArray(); return Combine(seperator, untyped); } public override string ToString() { return Succeeded ? "Succeeded" : $"Failed : {Error}"; } }
مشابه کلاس بالا، در فریمورک ASP.NET Identity کلاسی تحت عنوان IdentityResult برای همین منظور در نظر گرفته شدهاست.
پراپرتی Succeeded نشان دهنده موفقت آمیز بودن یا عدم موفقیت عملیات (به عنوان مثال یک متد ApplicationService) میباشد. پراپرتی Error دربرگیرنده پیغام خطایی میباشد که قبلا از طریق Message مربوط به یک استثناء صادر شده، در اختیار بالاترین لایه قرار میگرفت. با استفاده از متد Combine، امکان ترکیب چندین Result حاصل از عملیات مختلف را خواهید داشت. متدهای استاتیک Failed و Success هم برای درگیر نشدن برای وهله سازی از کلاس Result در نظر گرفته شدهاند.
متد GetForEdit مربوط به MeetingService را در نظر بگیرید. به عنوان مثال وظیفه این متد بازگشت یک MeetingEditModel میباشد؛ اما با توجه به یکسری قواعد تجاری، بهعنوان مثال «امکان ویرایش جلسهای که پابلیش نهایی شدهاست، وجود ندارد و ...» لازم است خروجی این متد نیز در صورت Fail شدن، دلیل آن را به مصرف کننده ارائه دهد. از این رو کلاس جنریک Result را به شکل زیر خواهیم داشت:
public class Result<T> : Result { private readonly T _value; protected internal Result(T value, bool succeeded, string error) : base(succeeded, error) { _value = value; } public T Value { get { if (!Succeeded) throw new InvalidOperationException("There is no value for failure."); return _value; } } }
public static class ResultExtensions { public static Result<TK> OnSuccess<T, TK>(this Result<T> result, Func<T, TK> func) { return !result.Succeeded ? Result.Failed<TK>(result.Error) : Result.Success(func(result.Value)); } public static Result<T> Ensure<T>(this Result<T> result, Func<T, bool> predicate, string message) { if (!result.Succeeded) return Result.Failed<T>(result.Error); return !predicate(result.Value) ? Result.Failed<T>(message) : Result.Success(result.Value); } public static Result<TK> Map<T, TK>(this Result<T> result, Func<T, TK> func) { return !result.Succeeded ? Result.Failed<TK>(result.Error) : Result.Success(func(result.Value)); } public static Result<T> OnSuccess<T>(this Result<T> result, Action<T> action) { if (result.Succeeded) action(result.Value); return result; } public static T OnBoth<T>(this Result result, Func<Result, T> func) { return func(result); } public static Result OnSuccess(this Result result, Action action) { if (result.Succeeded) action(); return result; } public static Result<T> OnSuccess<T>(this Result result, Func<T> func) { return !result.Succeeded ? Result.Failed<T>(result.Error) : Result.Success(func()); } public static Result<TK> OnSuccess<T, TK>(this Result<T> result, Func<T, Result<TK>> func) { return !result.Succeeded ? Result.Failed<TK>(result.Error) : func(result.Value); } public static Result<T> OnSuccess<T>(this Result result, Func<Result<T>> func) { return !result.Succeeded ? Result.Failed<T>(result.Error) : func(); } public static Result<TK> OnSuccess<T, TK>(this Result<T> result, Func<Result<TK>> func) { return !result.Succeeded ? Result.Failed<TK>(result.Error) : func(); } public static Result OnSuccess<T>(this Result<T> result, Func<T, Result> func) { return !result.Succeeded ? Result.Failed(result.Error) : func(result.Value); } public static Result OnSuccess(this Result result, Func<Result> func) { return !result.Succeeded ? result : func(); } public static Result Ensure(this Result result, Func<bool> predicate, string message) { if (!result.Succeeded) return Result.Failed(result.Error); return !predicate() ? Result.Failed(message) : Result.Success(); } public static Result<T> Map<T>(this Result result, Func<T> func) { return !result.Succeeded ? Result.Failed<T>(result.Error) : Result.Success(func()); } public static TK OnBoth<T, TK>(this Result<T> result, Func<Result<T>, TK> func) { return func(result); } public static Result<T> OnFailure<T>(this Result<T> result, Action action) { if (!result.Succeeded) action(); return result; } public static Result OnFailure(this Result result, Action action) { if (!result.Succeeded) action(); return result; } public static Result<T> OnFailure<T>(this Result<T> result, Action<string> action) { if (!result.Succeeded) action(result.Error); return result; } public static Result OnFailure(this Result result, Action<string> action) { if (!result.Succeeded) action(result.Error); return result; } }
[HttpPost, AjaxOnly, ValidateAntiForgeryToken, ValidateModelState] public virtual async Task<ActionResult> Create([Bind(Prefix = "Model")]MeetingCreateModel model) { var result = await _service.CreateAsync(model); return result.OnSuccess(() => { }) .OnFailure(() => { }) .OnBoth(r => r.Succeeded ? InformationNotification("Messages.Save.Success") : ErrorMessage(r.Error)); }
یا در حالتهای پیچیده تر:
var result = await _service.CreateAsync(new TenantAwareEntityCreateModel()); return Result.Combine(result, Result.Success(), Result.Failed("نتیجه یک متد دیگر به عنوان مثال")) .OnSuccess(() => { }) .OnFailure(() => { }) .OnBoth(r => r.Succeeded ? Json("OK") : Json(r.Error));
ترکیب با الگوی Maybe یا Option
public struct Maybe<T> : IEquatable<Maybe<T>> where T : class { private readonly T _value; private Maybe(T value) { _value = value; } public bool HasValue => _value != null; public T Value => _value ?? throw new InvalidOperationException(); public static Maybe<T> None => new Maybe<T>(); public static implicit operator Maybe<T>(T value) { return new Maybe<T>(value); } public static bool operator ==(Maybe<T> maybe, T value) { return maybe.HasValue && maybe.Value.Equals(value); } public static bool operator !=(Maybe<T> maybe, T value) { return !(maybe == value); } public static bool operator ==(Maybe<T> left, Maybe<T> right) { return left.Equals(right); } public static bool operator !=(Maybe<T> left, Maybe<T> right) { return !(left == right); } /// <inheritdoc /> /// <summary> /// Avoid boxing and Give type safety /// </summary> /// <param name="other"></param> /// <returns></returns> public bool Equals(Maybe<T> other) { if (!HasValue && !other.HasValue) return true; if (!HasValue || !other.HasValue) return false; return _value.Equals(other.Value); } /// <summary> /// Avoid reflection /// </summary> /// <param name="obj"></param> /// <returns></returns> public override bool Equals(object obj) { if (obj is T typed) { obj = new Maybe<T>(typed); } if (!(obj is Maybe<T> other)) return false; return Equals(other); } /// <summary> /// Good practice when overriding Equals method. /// If x.Equals(y) then we must have x.GetHashCode()==y.GetHashCode() /// </summary> /// <returns></returns> public override int GetHashCode() { return HasValue ? _value.GetHashCode() : 0; } public override string ToString() { return HasValue ? _value.ToString() : "NO VALUE"; } }
public static Result<T> ToResult<T>(this Maybe<T> maybe, string message) where T : class { return !maybe.HasValue ? Result.Failed<T>(message) : Result.Success(maybe.Value); }
Result<Customer> customerResult = _customerRepository.GetById(model.Id) .ToResult("Customer with such Id is not found: " + model.Id);
همچنین متدهای الحاقی زیر را نیز برای ساختار داده Maybe میتوان در نظر گرفت:
public static T GetValueOrDefault<T>(this Maybe<T> maybe, T defaultValue = default) where T : class { return maybe.GetValueOrDefault(x => x, defaultValue); } public static TK GetValueOrDefault<T, TK>(this Maybe<T> maybe, Func<T, TK> selector, TK defaultValue = default) where T : class { return maybe.HasValue ? selector(maybe.Value) : defaultValue; } public static Maybe<T> Where<T>(this Maybe<T> maybe, Func<T, bool> predicate) where T : class { if (!maybe.HasValue) return default(T); return predicate(maybe.Value) ? maybe : default(T); } public static Maybe<TK> Select<T, TK>(this Maybe<T> maybe, Func<T, TK> selector) where T : class where TK : class { return !maybe.HasValue ? default : selector(maybe.Value); } public static Maybe<TK> Select<T, TK>(this Maybe<T> maybe, Func<T, Maybe<TK>> selector) where T : class where TK : class { return !maybe.HasValue ? default(TK) : selector(maybe.Value); } public static void Execute<T>(this Maybe<T> maybe, Action<T> action) where T : class { if (!maybe.HasValue) return; action(maybe.Value); } }
- استفاده از الگوی Specification برای زمانیکه منطقی قرار است هم برای اعتبارسنجی درون حافظهای استفاده شود و همچنین برای اعمال فیلتر برای واکشی دادهها؛ در واقع دو Use-case استفاده از این الگو حداقل یکجا وجود داشته باشد. استفاده از این مورد برای Domain Validation در سناریوهای پیچیده بسیار پیشنهاد میشود.
- استفاده از Domain Eventها برای اعمال اعتبارسنجیهای مرتبط با قواعد تجاری تنها در شرایط inter-application communication و در شرایط inner-application communication به صورت صریح، اعتبارسنجیهای مرتبط با قواعد تجاری را در جریان اصلی برنامه پیاده سازی کنید.
@Html.ActionLink("text", "Index", "Home")
پارامترهای دوم و سوم آن که به نامهای یک اکشن متد و کنترلر آن اشاره میکنند، توسط رشتهها تعریف شدهاند. مشکلاتی هم که با رشتهها در حالت کلی وجود دارند به شرح زیر است:
الف) میتوان نام کنترلر یا نام متد را در برنامه تغییر داد. به این ترتیب تمام ActionLink هایی که در برنامه به این کنترلر اشاره میکردند از کار میافتند (تکرار رشتهها به علاوه refactoring friendly نبودن آنها).
ب) برای نوشتن رشتهها intellisense کارآیی ندارد.
ج) امکان بروز اشتباهات تایپی در این بین بسیار زیاد است.
راه حل متداولی که برای حل این نوع مشکلات وجود دارد، تعریف یک کلاس عمومی و معرفی رشتهها به صورت فیلدهایی ثابت در آنها میباشند و سپس استفاده از این فیلدها بجای استفاده مستقیم از رشتهها.
و ... چقدر خوب میشد اگر ابزاری وجود میداشت که کلاسهای کنترلرهای ما را آنالیز میکرد و خودش این ثوابت رشتهای را از آنها استخراج و کلاسهای عمومی یاد شده را تشکیل میداد!
خوشبختانه نیازی به اختراع مجدد چرخ نیست و اینکار توسط پروژهی سورس بازی به نام T4MVC انجام شده است. برای دریافت آن به سایت زیر مراجعه نمائید:
این پروژه توسط David Ebbo از اعضای تیم ASP.NET MVC تهیه شده است.
پس از دریافت پروژه، تنها به دو فایل زیر از آن نیاز داریم:
T4MVC.tt و T4MVC.tt.settings.t4
دو فایل فوق را به درون پوشهای از پروژه جاری MVC خود کپی کنید (مثلا یک پوشه T4MVC را ایجاد و این دو فایل را به آن اضافه کنید). بلافاصله این فایلهای t4 وارد عمل شده و کلاسهای کنترلرها، Viewها، تصاویر و غیره را آنالیز و ... ثوابت رشتهای معادل آنها را تولید میکنند.
اکنون برای استفاده از این کلاسهای تولید شده میتوان به صورت زیر عمل کرد:
اصلاح ActionLinkها و حذف رشتههای موجود در آنها
اینبار بجای اینکه بنویسیم:
@Html.ActionLink("text", "Index", "Home")
@Html.ActionLink("text", result: MVC.Home.Index())
@Html.ActionLink("text", MVC.Home.ActionNames.Index, MVC.Home.Name)
برای دسترسی به امکانات آن با نام کلاس MVC شروع میکنیم و سپس برای مثال به نام کنترلر Home رسیده و توسط ActionNames آن به تمام اکشن متدهای موجود در آن میتوان دسترسی داشت.
البته این پروژه بسیار فراتر از تولید فیلدهای strongly typed معادل رشتهها است. همانطور که ملاحظه میکنید، یک سری overload را هم به متدهای پیش فرض ASP.NET MVC اضافه کرده است و حتی بجای معرفی رشته معادل اکشن متد Index، خود این اکشن متد را میتوان معرفی کرد (آرگومانی از نوع ActionResult را هم اضافه کرده است). نمونه دیگر آن به نحو زیر میتواند باشد:
@Url.Action(result: MVC.Article.Delete())
@Url.Action("Delete", "Article")
امکان معرفی بهتر نام Partial Viewها
برای مثال اگر پیشتر یک Partial View را به این شکل تعریف میکردید:
@{ Html.RenderPartial("_ViewPage1"); }
@{ Html.RenderPartial(MVC.Home.Views._ViewPage1); }
return PartialView(Views._ViewPage1);
و یا بجای:
return RedirectToAction(actionName: "Index", controllerName: "Menu");
return RedirectToAction(actionName: MVC.Menu.ActionNames.Index, controllerName: MVC.Menu.Name);
return RedirectToAction(result: MVC.Menu.Index());
و یا بجای مسیر دهی به شکل زیر:
return PartialView("~/Views/CommentsArchive/_LatestCommentsInfo.cshtml", data);
return PartialView(MVC.CommentsArchive.Views._LatestCommentsInfo, data);
امکان معرفی بهتر عناصر استاتیک سایت
این مورد نیز بسیار جالب توجه است. توسط کلاس Links آن میتوان به محتویات استاتیک (تصاویر، فایلهای css و غیره) پوشههای Content و Scripts هم دسترسی یافت و حتی این موارد را نیز refactor کرد:
<img src="@Links.Content.Images.arrow_right_png" alt="arrow" /> <script src="@Links.Scripts.jquery_1_5_1_min_js" type="text/javascript"></script>
امکان تعریف بهتر پارامترها و مقادیر route
بجای اینکه routeValues را همانند سابق با anonymously typed objects مقدار دهی کنیم:
Html.ActionLink(linkText: "عنوان", actionName: "Index", controllerName: "Comments", routeValues: new { userName = @Model.FriendlyName }, htmlAttributes: null))
Html.ActionLink(linkText: "عنوان", result: MVC.Comments.Index(userName: @Model.FriendlyName) htmlAttributes: null))
چند نکته جانبی
-این ابزار بر اساس Reflection کار میکند (البته فقط در حین تشکیل خودکار کلاسهای مورد نیاز؛ وگرنه ثوابتی را که ایجاد میکند کامپایل شده و در زمان اجرا سرباری را به برنامه اضافه نمیکنند). بنابراین اگر کلاسی به پروژه اضافه شده است، کامپایل کردن آنرا فراموش نکنید.
-اگر تغییری در فایلهای View، در تعداد و نام آنها صورت گرفت، روی فایل T4MVC.tt کلیک راست کرده و گزینهی اجرای آنرا انتخاب کنید. پس از اینکار، مجددا کامپایل پروژه را فراموش نکنید.
-در فایل T4MVC.tt.settings.t4 یک سری تنظیمات پیش فرض قرار دارند. برای مثال اگر علاقمندید که به این فایلهای تولید شده خودکار، فضای نام سفارشی خاصی را اضافه کنید میشود آرایه ReferencedNamespaces آنرا مقدار دهی کرد.
- overloadهای جدید ActionResult دار آن نسبت به نمونههای استاندارد موجود، بسیار منطقیتر به نظر میرسند.
- توضیحات کامل امکانات T4MVC را در مستندات رسمی آن میتوانید مطالعه کنید.
و ... اگر یک مدت با آن کار کنید خواهید گفت: «من قبلا چطور با ASP.NET MVC کار میکردم؟!»
در اینجا مدل User، کنترلری به نام Home و View متناظر با آن را ملاحظه میکنید:
namespace ModelStateTest.Models { public class User { public string Email { set; get; } } }
using System.Web.Mvc; using ModelStateTest.Models; namespace ModelStateTest.Controllers { public class HomeController : Controller { public ActionResult Index() { return View(); } [HttpPost] public ActionResult Index(User model) { model.Email = "?"; return View(model); } } }
@model ModelStateTest.Models.User @{ ViewBag.Title = "Index"; } <h2>Index</h2> @using (Html.BeginForm()) { @Html.ValidationSummary(true) <fieldset> <legend>User</legend> <div class="editor-label"> @Html.LabelFor(model => model.Email) </div> <div class="editor-field"> @Html.EditorFor(model => model.Email) @Html.ValidationMessageFor(model => model.Email) </div> <p> <input type="submit" value="Create" /> </p> </fieldset> }
model.Email = "?";
احتمالا عنوان میکنید که خوب ... همان مقدار علامت سؤال انتساب داده شده. اما ... اینچنین نیست! دقیقا همان مقداری که در حین Postback به سرور ارسال شده، نمایش داده میشود.
این مورد نکتهای است که عدم آشنایی با آن ممکن است چندین ساعت را به دیباگ یک برنامه اختصاص دهد، بدون اینکه نتیجه مفیدی حاصل شود.
مطابق نظر طراحان اصلی ASP.NET MVC، اینکار و این رفتار، دقیقا به همین نحو صحیح است و باگ نیست.
«فرض کنید در فیلدی عددی، کاربر عبارت «تست» را وارد کرده است. نیاز است در خطای اعتبار سنجی پس از Postback به او عنوان کنیم، لطفا بجای «تست»، عدد وارد کنید. چون خاصیت متناظر قید شده در مدل، عددی است، مقدار «تست» وارد شده را از دست خواهیم داد. به همین جهت همان مقدار اولیه وارد شده را در HTML Helpers پس از Postback حفظ میکنیم.»
راه حلهای ممکن، برای به روز رسانی وضعیت مدل پس از Postback
الف) استفاده از متد ModelState.Clear
این متد کلیه دادههای موجود در ModelState را منجمله خطاهای حاصل از اعتبارسنجی، حذف میکند. در این حالت مطابق مثال فوق پس از Postback، مقدار علامت سؤال نسبت داده شده به خاصیت ایمیل، نمایش داده خواهد شد.
ب) استفاده از متد ModelState.Remove
this.ModelState.Remove("Email");
ج) عدم استفاده از HTML Helpers
این مورد را فقط با متدهای کمکی For دار، مانند Html.EditorFor مشاهده خواهید کرد. اگر نحوه تعریف را به شکل زیر تغییر دهیم، نیازی به استفاده از متد ModelState.Remove نخواهد بود. البته، مزیتهای استفاده از HTML Helpers دارای متدهای For دار را که Strongly typed هستند، از دست میدهیم.
<input type="text" name="Email" id="Email" value="@Model.Email" />
حذف فضاهای خالی در خروجی صفحات ASP.NET MVC
public class RemoveWhitespacesAttribute : ActionFilterAttribute { public override void OnActionExecuted(ActionExecutedContext filterContext) { var response = filterContext.HttpContext.Response; if (filterContext.HttpContext.Request.RawUrl != "/sitemap.xml") { if (response.ContentType == "text/html" && response.Filter != null) { response.Filter = new HelperClass(response.Filter); } } } private class HelperClass : Stream { private System.IO.Stream Base; public HelperClass(System.IO.Stream ResponseStream) { if (ResponseStream == null) throw new ArgumentNullException("ResponseStream"); this.Base = ResponseStream; } StringBuilder s = new StringBuilder(); public override void Write(byte[] buffer, int offset, int count) { string HTML = Encoding.UTF8.GetString(buffer, offset, count); Regex reg = new Regex(@"(?<=\s)\s+(?![^<>]*</pre>)"); HTML = reg.Replace(HTML, string.Empty); buffer = System.Text.Encoding.UTF8.GetBytes(HTML); this.Base.Write(buffer, 0, buffer.Length); } #region Other Members public override int Read(byte[] buffer, int offset, int count) { throw new NotSupportedException(); } public override bool CanRead { get { return false; } } public override bool CanSeek { get { return false; } } public override bool CanWrite { get { return true; } } public override long Length { get { throw new NotSupportedException(); } } public override long Position { get { throw new NotSupportedException(); } set { throw new NotSupportedException(); } } public override void Flush() { Base.Flush(); } public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); } public override void SetLength(long value) { throw new NotSupportedException(); } #endregion } }
protected void Application_Start() { try { GlobalFilters.Filters.Add(new App_Start.RemoveWhitespacesAttribute()); } catch { HttpRuntime.UnloadAppDomain(); // سبب ری استارت برنامه و آغاز مجدد آن با درخواست بعدی میشود throw; } }
برای Gzip هم اکثر در این حالت که هردو مورد با هم قرار داده شده است در برخی از موارد فایلهای جاواسکریپ را با مشکل روبرو میکند .به نظر من از Gzip توکار IIS استفاده شود بهتر است. البته باید ماژول آن در ISS فعال شده باشد.
برای اینکار هم داخل Web.config کدهای زیر را داخل configuration قرار بدید.
<httpCompression directory="%SystemDrive%\inetpub\temp\IIS Temporary Compressed Files"> <scheme name="gzip" dll="%Windir%\system32\inetsrv\gzip.dll" staticCompressionLevel="9" /> <dynamicTypes> <add mimeType="text/*" enabled="true" /> <add mimeType="message/*" enabled="true" /> <add mimeType="application/x-javascript" enabled="true" /> <add mimeType="application/javascript" enabled="true" /> <add mimeType="application/json" enabled="true" /> <add mimeType="application/json; charset=utf-8" enabled="true" /> <add mimeType="application/atom+xml" enabled="true" /> <add mimeType="application/xaml+xml" enabled="true" /> <add mimeType="*/*" enabled="false" /> </dynamicTypes> <staticTypes> <add mimeType="text/*" enabled="true" /> <add mimeType="message/*" enabled="true" /> <add mimeType="application/x-javascript" enabled="true" /> <add mimeType="application/javascript" enabled="true" /> <add mimeType="application/json" enabled="true" /> <add mimeType="application/json; charset=utf-8" enabled="true" /> <add mimeType="application/atom+xml" enabled="true" /> <add mimeType="application/xaml+xml" enabled="true" /> <add mimeType="*/*" enabled="false" /> </staticTypes> </httpCompression> <urlCompression doStaticCompression="true" doDynamicCompression="true" /> </system.webServer> <location path="Default Web Site"> <system.webServer> <serverRuntime enabled="true" frequentHitThreshold="1" frequentHitTimePeriod="10:00:00" /> </system.webServer> </location>
PowerShell 7.x - قسمت سیزدهم - ساخت یک Static Site Generator ساده توسط PowerShell و GitHub Actions
├── _layout │ ├── _footer.html │ ├── _header.html │ ├── _nav.html │ └── main.html ├── build ├── img ├── posts └── set-posts.ps1
- دایرکتوری layout_: درون این دایرکتوری، ساختار اصلی بلاگ را قرار دادهایم. در ادامه محتویات هر فایل را مشاهده خواهید کرد:
<!--main.html--> <!DOCTYPE html> <html dir="rtl"> {{header}} <body> {{nav}} <main> {{content}} </main> {{footer}} </body> <!--_header.html--> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{{title}}</title> <link href="https://cdn.jsdelivr.net/gh/rastikerdar/samim-font@v4.0.5/dist/font-face.css" rel="stylesheet" type="text/css" /> <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.16/dist/tailwind.min.css" rel="stylesheet"> <style> * { } </style> </head> <!--_nav.html--> <header> <nav> <div> <div> <a href="#">بلاگ من</a> </div> <div> <ul> {{nav}} </ul> </div> </div> </nav> </header> <!--_footer.html--> <footer> <div> <div> <p> تمامی حقوق محفوظ است </p> </div> </div> </footer>
- دایرکتوری build: درون این دایرکتوری، خروجیهای HTML که قرار است توسط اسکریپت PowerShell جنریت شوند، قرار خواهند گرفت. این پوشه در واقع قرار است توسط GitHub Pages میزبانی شود.
- دایرکتوری img: درون این دایرکتوری، تصاویر مربوط به هر بلاگپست را قرار خواهیم داد.
- دایرکتوری posts: درون این دایرکتوری، مطالبمان را با فرمت Markdown، قرار خواهیم داد. به عنوان مثال در ادامه یک نمونه از آن را مشاهده خواهید کرد (در کد زیر از Front Matter برای اضافه کردن یکسری متادیتای موردنیاز که حین بیلد شدن ضروری هستند استفاده شدهاست)
--- title: اولین پست من slug: hello date: 2023-04-26 author: سیروان عفیفی tags: [tag1, tag2, tag3] excerpt: این یک پست تستی است در مورد اینکه چطور میتوانیم از این قالب استفاده کنیم --- # اولین پست من ## اولین پست من ### اولین پست من #### اولین پست من ##### اولین پست من ###### اولین پست من لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است. چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است و برای شرایط فعلی تکنولوژی مورد نیاز و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی میباشد. کتابهای زیادی در شصت و سه درصد گذشته، حال و آینده شناخت فراوان جامعه و متخصصان را میطلبد تا با نرم افزارها شناخت بیشتری را برای طراحان رایانه ای علی الخصوص طراحان خلاقی و فرهنگ پیشرو در زبان فارسی ایجاد کرد. در این صورت میتوان امید داشت که تمام و دشواری موجود در ارائه راهکارها و شرایط سخت تایپ به پایان رسد وزمان مورد نیاز شامل حروفچینی دستاوردهای اصلی و جوابگوی سوالات پیوسته اهل دنیای موجود طراحی اساسا مورد استفاده قرار گیرد. <img src="/img/graphql.jpg"/>
- فایل set_post.ps1: موتور اصلی جنریت کردن صفحات HTML این فایل میباشد. در ادامه محتویات آن را مشاهده خواهید کرد. سپس هر کدام از توابع استفاده شده را یکییکی توضیح خواهیم داد:
Function Get-Layouts { $headerLayout = Get-Content -Path ./_layout/_header.html -Raw $homeLayout = Get-Content -Path ./_layout/main.html -Raw $footerLayout = Get-Content -Path ./_layout/_footer.html -Raw Return @{ Header = $headerLayout Home = $homeLayout Footer = $footerLayout } } Function Get-PostFrontMatter($postContent) { $frontMatter = [regex]::Match($postContent, "(---(?:\r?\n(?!--|\s*$).*)*)\s*((?:\r?\n(?!---).*)*\r?\n---)") Return $frontMatter } Function Set-Headings($postHtml) { Return $postHtml -Replace '<h(\d) id="(.*)">', { $level = $_.Groups[1].Value $id = $_.Groups[2].Value $class = Switch ($level) { '1' { 'text-4xl font-bold mb-2' } '2' { 'text-3xl font-bold mb-2' } '3' { 'text-2xl font-bold mb-2' } '4' { 'text-xl font-bold mb-2' } '5' { 'text-lg font-bold mb-2' } '6' { 'text-base font-bold mb-2' } } "<h$level class='$class' id='$id'>" } } Function ConvertTo-Slug { [CmdletBinding()] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [string]$String ) process { $slug = $String -replace '[^\w\s-]', '' # remove non-word characters except hyphens $slug = $slug -replace '\s+', '-' # replace whitespace with a single hyphen $slug = $slug -replace '^-+|-+$', '' # remove leading/trailing hyphens $slug = $slug.ToLower() # convert to lowercase Write-Output $slug } } Function Get-Posts { $markdownPosts = Get-ChildItem -Path ./posts -Filter *.md $posts = @() Foreach ($post in $markdownPosts) { $postContent = Get-Content -Path $post.FullName -Raw $frontMatter = Get-PostFrontMatter $postContent $frontMatterObject = $frontMatter | ConvertFrom-Yaml $slug = $frontMatterObject.slug ?? (ConvertTo-Slug "$($frontMatterObject.date)-$($frontMatterObject.title)") $body = $postContent.Replace($frontMatter.Value, "") | ConvertFrom-Markdown $postHtml = $layouts.Home -replace '{{header}}', $layouts.Header ` -replace '{{title}}', $frontMatterObject.title ` -replace '{{nav}}', (Set-Navs) ` -replace '{{content}}', $body.Html ` -replace '{{footer}}', $layouts.Footer $postHtml = Set-Headings $postHtml $postHtml | Out-File -FilePath ./build/$slug.html $posts += @{ title = $frontMatterObject.title slug = $slug excerpt = $frontMatterObject.excerpt date = $frontMatterObject.date author = $frontMatterObject.author body = $body.Html } } Return $posts } Function Set-Archive { $posts = Get-Posts $archive = @() $archive = @" <ul> $($posts | ForEach-Object { "<li><a href='$($_.slug).html'>$($_.title)</a></li>" }) </ul> "@ Return $archive -join "`r`n" } Function Copy-ToBuild { $layouts = Get-Layouts $latestPosts = Get-Posts | ForEach-Object { @" <div> <img src="https://via.placeholder.com/300x200" alt="$($_.title)"> <h2>$($_.title)</h2> <p>$($_.excerpt)</p> <a href="$($_.slug).html">ادامه مطلب</a> </div> "@ } $homeLayout = $layouts.Home -replace '{{header}}', $layouts.Header ` -replace '{{nav}}', (Set-Navs) ` -replace '{{title}}', 'بلاگ من' ` -replace '{{content}}', ('<div>' + $latestPosts + '</div>') ` -replace '{{footer}}', $layouts.Footer $homeLayout | Out-File -FilePath ./build/index.html Copy-Item -Path ./img -Destination ./build -Recurse -Force } Function Set-Navs { $navs = @( @{ title = "صفحه اصلی" url = "/sample" }, @{ title = "درباره ما" url = "/sample/about.html" }, @{ title = "تماس با ما" url = "/sample/contact.html" } ) $navLayout = Get-Content -Path ./_layout/_nav.html -Raw $navLayout -replace '{{nav}}', ($navs | ForEach-Object { "<li><a href=""$($_.url)""text-gray-700 hover:text-gray-800 m-2"">$($_.title)</a></li>" }) } Copy-ToBuild
name: Deploy static content to Pages on: push: branches: - main workflow_dispatch: permissions: contents: read pages: write id-token: write concurrency: group: "pages" cancel-in-progress: false jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: Checkout Repository uses: actions/checkout@v2 - name: Install powershell-yaml module shell: pwsh run: | Set-PSRepository PSGallery -InstallationPolicy Trusted Install-Module powershell-yaml -ErrorAction Stop - name: Setup Pages uses: actions/configure-pages@v3 - name: Build Static Site shell: pwsh run: | . ./set-posts.ps1 - name: Upload Static Site Artifact uses: actions/upload-pages-artifact@v1 with: path: build - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v2
- درون دایرکتوری posts، مطلب موردنظر را به همراه Front Matter زیر ایجاد کرده و سپس محتویات مطلب را بعد از آن وارد کنید:
--- title: اولین پست من slug: hello date: 2023-04-26 author: سیروان عفیفی tags: [tag1, tag2, tag3] excerpt: این یک پست تستی است در مورد اینکه چطور میتوانیم از این قالب استفاده کنیم --- content
- تغییرات ایجاد شده را کامیت و سپس پوش کنید. به محض پوش کردن تغییرات، GitHub Actions پروسه بیلد را انجام خواهد داد و بلافاصله میتوانید تغییرات را مشاهده نمائید.
کدهای این مطلب را میتوانید از اینجا دریافت کنید.
در ادامه، تعاریف سایر موجودیتهای سیستم ثبت سفارشات و نگاشت آنها را بررسی خواهیم کرد.
کلاس Product تعریف شده در فایل جدید Product.cs در پوشه domain برنامه:
namespace NHSample1.Domain
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal UnitPrice { get; set; }
public bool Discontinued { get; set; }
}
}
using FluentNHibernate.Mapping;
using NHSample1.Domain;
namespace NHSample1.Mappings
{
public class ProductMapping : ClassMap<Product>
{
public ProductMapping()
{
Not.LazyLoad();
Id(p => p.Id).GeneratedBy.HiLo("1000");
Map(p => p.Name).Length(50).Not.Nullable();
Map(p => p.UnitPrice).Not.Nullable();
Map(p => p.Discontinued).Not.Nullable();
}
}
}
آزمون واحد بررسی این نگاشت نیز همانند مثال قبلی است.
کلاس ProductMapping_Fixture را در فایل جدید ProductMapping_Fixture.cs به پروژه UnitTests خود (که ارجاعات آنرا در قسمت قبل مشخص کردیم) خواهیم افزود:
using NUnit.Framework;
using FluentNHibernate.Testing;
using NHSample1.Domain;
namespace UnitTests
{
[TestFixture]
public class ProductMapping_Fixture : FixtureBase
{
[Test]
public void can_correctly_map_product()
{
new PersistenceSpecification<Product>(Session)
.CheckProperty(p => p.Id, 1001)
.CheckProperty(p => p.Name, "Apples")
.CheckProperty(p => p.UnitPrice, 10.45m)
.CheckProperty(p => p.Discontinued, true)
.VerifyTheMappings();
}
}
}
ProductMapping_Fixture.can_correctly_map_product : Passed
NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 2, @p1 = 1
NHibernate: INSERT INTO "Product" (Name, UnitPrice, Discontinued, Id) VALUES (@p0, @p1, @p2, @p3);@p0 = 'Apples', @p1 = 10.45, @p2 = True, @p3 = 1001
NHibernate: SELECT product0_.Id as Id1_0_, product0_.Name as Name1_0_, product0_.UnitPrice as UnitPrice1_0_, product0_.Discontinued as Disconti4_1_0_ FROM "Product" product0_ WHERE product0_.Id=@p0;@p0 = 1001
در ادامه تعریف کلاس کارمند، نگاشت و آزمون واحد آن به صورت زیر خواهند بود:
using System;
namespace NHSample1.Domain
{
public class Employee
{
public int Id { set; get; }
public string LastName { get; set; }
public string FirstName { get; set; }
}
}
using NHSample1.Domain;
using FluentNHibernate.Mapping;
namespace NHSample1.Mappings
{
public class EmployeeMapping : ClassMap<Employee>
{
public EmployeeMapping()
{
Not.LazyLoad();
Id(e => e.Id).GeneratedBy.Assigned();
Map(e => e.LastName).Length(50);
Map(e => e.FirstName).Length(50);
}
}
}
using NUnit.Framework;
using NHSample1.Domain;
using FluentNHibernate.Testing;
namespace UnitTests
{
[TestFixture]
public class EmployeeMapping_Fixture : FixtureBase
{
[Test]
public void can_correctly_map_employee()
{
new PersistenceSpecification<Employee>(Session)
.CheckProperty(p => p.Id, 1001)
.CheckProperty(p => p.FirstName, "name1")
.CheckProperty(p => p.LastName, "lname1")
.VerifyTheMappings();
}
}
}
NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 2, @p1 = 1
NHibernate: INSERT INTO "Employee" (LastName, FirstName, Id) VALUES (@p0, @p1, @p2);@p0 = 'lname1', @p1 = 'name1', @p2 = 1001
NHibernate: SELECT employee0_.Id as Id4_0_, employee0_.LastName as LastName4_0_, employee0_.FirstName as FirstName4_0_ FROM "Employee" employee0_ WHERE employee0_.Id=@p0;@p0 = 1001
همانطور که ملاحظه میکنید، این آزمونهای واحد 4 مرحله را در یک سطر انجام میدهند:
الف) ایجاد یک وهله از کلاس Employee
ب) ثبت اطلاعات کارمند در دیتابیس
ج) دریافت اطلاعات کارمند در وهلهای جدید از شیء Employee
د) و در پایان بررسی میکند که آیا شیء جدید ایجاد شده با شیء اولیه مطابقت دارد یا خیر
اکنون در ادامه پیاده سازی سیستم ثبت سفارشات، به قسمت جالب این مدل میرسیم. قسمتی که در آن ارتباطات اشیاء و روابط one-to-many تعریف خواهند شد. تعاریف کلاسهای OrderItem و OrderItemMapping را به صورت زیر در نظر بگیرید:
کلاس OrderItem تعریف شده در فایل جدید OrderItem.cs واقع شده در پوشه domain پروژه:
که در آن هر سفارش (order) دقیقا از یک محصول (product) تشکیل میشود و هر محصول میتواند در سفارشات متعدد و مختلفی درخواست شود.
namespace NHSample1.Domain
{
public class OrderItem
{
public int Id { get; set; }
public int Quantity { get; set; }
public Product Product { get; set; }
}
}
using FluentNHibernate.Mapping;
using NHSample1.Domain;
namespace NHSample1.Mappings
{
public class OrderItemMapping : ClassMap<OrderItem>
{
public OrderItemMapping()
{
Not.LazyLoad();
Id(oi => oi.Id).GeneratedBy.Assigned();
Map(oi => oi.Quantity).Not.Nullable();
References(oi => oi.Product).Not.Nullable();
}
}
}
نکتهی دیگر مهم آن این مورد است که Id در اینجا به صورت یک کلید تعریف نشده است. یک آیتم سفارش داده شده، موجودیت به حساب نیامده و فقط یک شیء مقداری (value object) است و به خودی خود امکان وجود ندارد. هر وهله از آن تنها توسط یک سفارش قابل تعریف است. بنابراین id در اینجا فقط به عنوان یک index میتواند مورد استفاده قرار گیرد و فقط توسط شیء Order زمانیکه یک OrderItem به آن اضافه میشود، مقدار دهی خواهد شد.
اگر برای این نگاشت نیز آزمون واحد تهیه کنیم، به صورت زیر خواهد بود:
using NUnit.Framework;
using NHSample1.Domain;
using FluentNHibernate.Testing;
namespace UnitTests
{
[TestFixture]
public class OrderItemMapping_Fixture : FixtureBase
{
[Test]
public void can_correctly_map_order_item()
{
var product = new Product
{
Name = "Apples",
UnitPrice = 4.5m,
Discontinued = true
};
new PersistenceSpecification<OrderItem>(Session)
.CheckProperty(p => p.Id, 1)
.CheckProperty(p => p.Quantity, 5)
.CheckReference(p => p.Product, product)
.VerifyTheMappings();
}
}
}
مشکل! این آزمون واحد با شکست مواجه خواهد شد، زیرا هنوز مشخص نکردهایم که دو شیء Product را که در قسمت CheckReference فوق برای این منظور معرفی کردهایم، چگونه باید با هم مقایسه کرد. در مورد مقایسه نوعهای اولیه و اصلی مانند int و string و امثال آن مشکلی نیست، اما باید منطق مقایسه سایر اشیاء سفارشی خود را با پیاده سازی اینترفیس IEqualityComparer دقیقا مشخص سازیم:
using System.Collections;
using NHSample1.Domain;
namespace UnitTests
{
public class CustomEqualityComparer : IEqualityComparer
{
public bool Equals(object x, object y)
{
if (ReferenceEquals(x, y)) return true;
if (x == null || y == null) return false;
if (x is Product && y is Product)
return (x as Product).Id == (y as Product).Id;
if (x is Customer && y is Customer)
return (x as Customer).Id == (y as Customer).Id;
if (x is Employee && y is Employee)
return (x as Employee).Id == (y as Employee).Id;
if (x is OrderItem && y is OrderItem)
return (x as OrderItem).Id == (y as OrderItem).Id;
return x.Equals(y);
}
public int GetHashCode(object obj)
{
//شاید وقتی دیگر
return obj.GetHashCode();
}
}
}
سپس برای بکار گیری این کلاس جدید، سطر مربوط به استفاده از PersistenceSpecification به صورت زیر تغییر خواهد کرد:
new PersistenceSpecification<OrderItem>(Session, new CustomEqualityComparer())
پس از این تغییرات و مشخص سازی نحوهی مقایسه دو شیء سفارشی، آزمون واحد ما پاس شده و خروجی SQL تولید شده آن به صورت زیر میباشد:
NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 2, @p1 = 1
NHibernate: INSERT INTO "Product" (Name, UnitPrice, Discontinued, Id) VALUES (@p0, @p1, @p2, @p3);@p0 = 'Apples', @p1 = 4.5, @p2 = True, @p3 = 1001
NHibernate: INSERT INTO "OrderItem" (Quantity, Product_id, Id) VALUES (@p0, @p1, @p2);@p0 = 5, @p1 = 1001, @p2 = 1
NHibernate: SELECT orderitem0_.Id as Id0_1_, orderitem0_.Quantity as Quantity0_1_, orderitem0_.Product_id as Product3_0_1_, product1_.Id as Id3_0_, product1_.Name as Name3_0_, product1_.UnitPrice as UnitPrice3_0_, product1_.Discontinued as Disconti4_3_0_ FROM "OrderItem" orderitem0_ inner join "Product" product1_ on orderitem0_.Product_id=product1_.Id WHERE orderitem0_.Id=@p0;@p0 = 1
قسمت پایانی کار تعاریف کلاسهای نگاشت، مربوط به کلاس Order است که در ادامه بررسی خواهد شد.
using System;
using System.Collections.Generic;
namespace NHSample1.Domain
{
public class Order
{
public int Id { set; get; }
public DateTime OrderDate { get; set; }
public Employee Employee { get; set; }
public Customer Customer { get; set; }
public IList<OrderItem> OrderItems { get; set; }
}
}
using NHSample1.Domain;
using FluentNHibernate.Mapping;
namespace NHSample1.Mappings
{
public class OrderMapping : ClassMap<Order>
{
public OrderMapping()
{
Not.LazyLoad();
Id(o => o.Id).GeneratedBy.GuidComb();
Map(o => o.OrderDate).Not.Nullable();
References(o => o.Employee).Not.Nullable();
References(o => o.Customer).Not.Nullable();
HasMany(o => o.OrderItems)
.AsList(index => index.Column("ListIndex").Type<int>());
}
}
}
قسمت جدید آن HasMany است که جهت تعریف رابطه one-to-many بکار گرفته شده است. یک سفارش رابطه many-to-one با یک مشتری و همچنین کارمندی که این رکورد را ثبت میکند، دارد. در اینجا مجموعه آیتمهای یک سفارش به صورت یک لیست بازگشت داده میشود و ایندکس آن به ستونی به نام ListIndex در یک جدول دیتابیس نگاشت خواهد شد. نوع این ستون، int میباشد.
using System;
using System.Collections.Generic;
using NUnit.Framework;
using NHSample1.Domain;
using FluentNHibernate.Testing;
namespace UnitTests
{
[TestFixture]
public class OrderMapping_Fixture : FixtureBase
{
[Test]
public void can_correctly_map_an_order()
{
{
var product1 =
new Product
{
Name = "Apples",
UnitPrice = 4.5m,
Discontinued = true
};
var product2 =
new Product
{
Name = "Pears",
UnitPrice = 3.5m,
Discontinued = false
};
Session.Save(product1);
Session.Save(product2);
var items = new List<OrderItem>
{
new OrderItem
{
Id = 1,
Quantity = 100,
Product = product1
},
new OrderItem
{
Id = 2,
Quantity = 200,
Product = product2
}
};
var customer = new Customer
{
FirstName = "Vahid",
LastName = "Nasiri",
AddressLine1 = "Addr1",
AddressLine2 = "Addr2",
PostalCode = "1234",
City = "Tehran",
CountryCode = "IR"
};
var employee =
new Employee
{
FirstName = "name1",
LastName = "lname1"
};
var order = new Order
{
Customer = customer,
Employee = employee,
OrderDate = DateTime.Today,
OrderItems = items
};
new PersistenceSpecification<Order>(Session, new CustomEqualityComparer())
.CheckProperty(o => o.OrderDate, order.OrderDate)
.CheckReference(o => o.Customer, order.Customer)
.CheckReference(o => o.Employee, order.Employee)
.CheckList(o => o.OrderItems, order.OrderItems)
.VerifyTheMappings();
}
}
}
}
متد آزمون واحد فوق کمی طولانی است؛ زیرا در آن باید تعاریف انواع و اقسام اشیاء مورد استفاده را مشخص نمود (و ارزش کار نیز دقیقا در همینجا مشخص میشود که بجای SQL نوشتن، با اشیایی که توسط کامپایلر تحت نظر هستند سر و کار داریم).
تنها نکته جدید آن استفاده از CheckList برای بررسی IList تعریف شده در قسمت قبل است.
خروجی SQL این آزمون واحد پس از اجرا و موفقیت آن به صورت زیر است:
OrderMapping_Fixture.can_correctly_map_an_order : Passed
NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 2, @p1 = 1
NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 3, @p1 = 2
NHibernate: INSERT INTO "Product" (Name, UnitPrice, Discontinued, Id) VALUES (@p0, @p1, @p2, @p3);@p0 = 'Apples', @p1 = 4.5, @p2 = True, @p3 = 1001
NHibernate: INSERT INTO "Product" (Name, UnitPrice, Discontinued, Id) VALUES (@p0, @p1, @p2, @p3);@p0 = 'Pears', @p1 = 3.5, @p2 = False, @p3 = 1002
NHibernate: INSERT INTO "Customer" (FirstName, LastName, AddressLine1, AddressLine2, PostalCode, City, CountryCode, Id) VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7);@p0 = 'Vahid', @p1 = 'Nasiri', @p2 = 'Addr1', @p3 = 'Addr2', @p4 = '1234', @p5 = 'Tehran', @p6 = 'IR', @p7 = 2002
NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 4, @p1 = 3
NHibernate: INSERT INTO "Employee" (LastName, FirstName, Id) VALUES (@p0, @p1, @p2);@p0 = 'lname1', @p1 = 'name1', @p2 = 3003
NHibernate: INSERT INTO "OrderItem" (Quantity, Product_id, Id) VALUES (@p0, @p1, @p2);@p0 = 100, @p1 = 1001, @p2 = 1
NHibernate: INSERT INTO "OrderItem" (Quantity, Product_id, Id) VALUES (@p0, @p1, @p2);@p0 = 200, @p1 = 1002, @p2 = 2
NHibernate: INSERT INTO "Order" (OrderDate, Employee_id, Customer_id, Id) VALUES (@p0, @p1, @p2, @p3);@p0 = 2009/10/10 12:00:00 ق.ظ, @p1 = 3003, @p2 = 2002, @p3 = 0
NHibernate: UPDATE "OrderItem" SET Order_id = @p0, ListIndex = @p1 WHERE Id = @p2;@p0 = 0, @p1 = 0, @p2 = 1
NHibernate: UPDATE "OrderItem" SET Order_id = @p0, ListIndex = @p1 WHERE Id = @p2;@p0 = 0, @p1 = 1, @p2 = 2
NHibernate: SELECT order0_.Id as Id1_2_, order0_.OrderDate as OrderDate1_2_, order0_.Employee_id as Employee3_1_2_, order0_.Customer_id as Customer4_1_2_, employee1_.Id as Id4_0_, employee1_.LastName as LastName4_0_, employee1_.FirstName as FirstName4_0_, customer2_.Id as Id2_1_, customer2_.FirstName as FirstName2_1_, customer2_.LastName as LastName2_1_, customer2_.AddressLine1 as AddressL4_2_1_, customer2_.AddressLine2 as AddressL5_2_1_, customer2_.PostalCode as PostalCode2_1_, customer2_.City as City2_1_, customer2_.CountryCode as CountryC8_2_1_ FROM "Order" order0_ inner join "Employee" employee1_ on order0_.Employee_id=employee1_.Id inner join "Customer" customer2_ on order0_.Customer_id=customer2_.Id WHERE order0_.Id=@p0;@p0 = 0
NHibernate: SELECT orderitems0_.Order_id as Order4_2_, orderitems0_.Id as Id2_, orderitems0_.ListIndex as ListIndex2_, orderitems0_.Id as Id0_1_, orderitems0_.Quantity as Quantity0_1_, orderitems0_.Product_id as Product3_0_1_, product1_.Id as Id3_0_, product1_.Name as Name3_0_, product1_.UnitPrice as UnitPrice3_0_, product1_.Discontinued as Disconti4_3_0_ FROM "OrderItem" orderitems0_ inner join "Product" product1_ on orderitems0_.Product_id=product1_.Id WHERE orderitems0_.Order_id=@p0;@p0 = 0
تا اینجای کار تعاریف اشیاء ، نگاشت آنها و همچنین بررسی صحت این نگاشتها به پایان میرسد.
نکته:
دیتابیس برنامه را جهت آزمونهای واحد برنامه، از نوع SQLite ساخته شده در حافظه مشخص کردیم. اگر علاقمند باشید که database schema تولید شده توسط NHibernate را مشاهده نمائید، در متد SetupContext کلاس FixtureBase که در قسمت قبل معرفی شد، سطر آخر را به صورت زیر تغییر دهید، تا اسکریپت دیتابیس نیز به صورت خودکار در خروجی اس کیوال آزمون واحد لحاظ شود (پارامتر دوم آن مشخص میکند که schema ساخته شده، نمایش داده شود یا خیر):
SessionSource.BuildSchema(Session, true);
drop table if exists "OrderItem"
drop table if exists "Order"
drop table if exists "Customer"
drop table if exists "Product"
drop table if exists "Employee"
drop table if exists hibernate_unique_key
create table "OrderItem" (
Id INTEGER not null,
Quantity INTEGER not null,
Product_id INTEGER not null,
Order_id INTEGER,
ListIndex INTEGER,
primary key (Id)
)
create table "Order" (
Id INTEGER not null,
OrderDate DATETIME not null,
Employee_id INTEGER not null,
Customer_id INTEGER not null,
primary key (Id)
)
create table "Customer" (
Id INTEGER not null,
FirstName TEXT not null,
LastName TEXT not null,
AddressLine1 TEXT not null,
AddressLine2 TEXT,
PostalCode TEXT not null,
City TEXT not null,
CountryCode TEXT not null,
primary key (Id)
)
create table "Product" (
Id INTEGER not null,
Name TEXT not null,
UnitPrice NUMERIC not null,
Discontinued INTEGER not null,
primary key (Id)
)
create table "Employee" (
Id INTEGER not null,
LastName TEXT,
FirstName TEXT,
primary key (Id)
)
create table hibernate_unique_key (
next_hi INTEGER
)
برای اینکه از دیتابیس اس کیوال سرور استفاده کنیم، در همان متد SetupContext کلاس مذکور، سطر اول را به صورت زیر تغییر دهید (نوع دیتابیس اس کیوال سرور 2008 مشخص شده و سپس رشته اتصالی به دیتابیس ذکر گردیده است):
var cfg = Fluently.Configure().Database(
// SQLiteConfiguration.Standard.ShowSql().InMemory
MsSqlConfiguration
.MsSql2008
.ShowSql()
.ConnectionString("Data Source=(local);Initial Catalog=testdb2009;Integrated Security = true")
);
if exists (select 1 from sys.objects where object_id = OBJECT_ID(N'[FK3EF88858466CFBF7]') AND parent_object_id = OBJECT_ID('[OrderItem]'))
alter table [OrderItem] drop constraint FK3EF88858466CFBF7
if exists (select 1 from sys.objects where object_id = OBJECT_ID(N'[FK3EF888589F32DE52]') AND parent_object_id = OBJECT_ID('[OrderItem]'))
alter table [OrderItem] drop constraint FK3EF888589F32DE52
if exists (select 1 from sys.objects where object_id = OBJECT_ID(N'[FK3117099B1EBA72BC]') AND parent_object_id = OBJECT_ID('[Order]'))
alter table [Order] drop constraint FK3117099B1EBA72BC
if exists (select 1 from sys.objects where object_id = OBJECT_ID(N'[FK3117099BB2F9593A]') AND parent_object_id = OBJECT_ID('[Order]'))
alter table [Order] drop constraint FK3117099BB2F9593A
if exists (select * from dbo.sysobjects where id = object_id(N'[OrderItem]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [OrderItem]
if exists (select * from dbo.sysobjects where id = object_id(N'[Order]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [Order]
if exists (select * from dbo.sysobjects where id = object_id(N'[Customer]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [Customer]
if exists (select * from dbo.sysobjects where id = object_id(N'[Product]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [Product]
if exists (select * from dbo.sysobjects where id = object_id(N'[Employee]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [Employee]
if exists (select * from dbo.sysobjects where id = object_id(N'hibernate_unique_key') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table hibernate_unique_key
create table [OrderItem] (
Id INT not null,
Quantity INT not null,
Product_id INT not null,
Order_id INT null,
ListIndex INT null,
primary key (Id)
)
create table [Order] (
Id INT not null,
OrderDate DATETIME not null,
Employee_id INT not null,
Customer_id INT not null,
primary key (Id)
)
create table [Customer] (
Id INT not null,
FirstName NVARCHAR(50) not null,
LastName NVARCHAR(50) not null,
AddressLine1 NVARCHAR(50) not null,
AddressLine2 NVARCHAR(50) null,
PostalCode NVARCHAR(10) not null,
City NVARCHAR(50) not null,
CountryCode NVARCHAR(2) not null,
primary key (Id)
)
create table [Product] (
Id INT not null,
Name NVARCHAR(50) not null,
UnitPrice DECIMAL(19,5) not null,
Discontinued BIT not null,
primary key (Id)
)
create table [Employee] (
Id INT not null,
LastName NVARCHAR(50) null,
FirstName NVARCHAR(50) null,
primary key (Id)
)
alter table [OrderItem]
add constraint FK3EF88858466CFBF7
foreign key (Product_id)
references [Product]
alter table [OrderItem]
add constraint FK3EF888589F32DE52
foreign key (Order_id)
references [Order]
alter table [Order]
add constraint FK3117099B1EBA72BC
foreign key (Employee_id)
references [Employee]
alter table [Order]
add constraint FK3117099BB2F9593A
foreign key (Customer_id)
references [Customer]
create table hibernate_unique_key (
next_hi INT
)
الف) جداول مطابق نام کلاسهای ما تولید شدهاند.
ب) نام فیلدها دقیقا مطابق نام خواص کلاسهای ما تشکیل شدهاند.
ج) Id ها به صورت primary key تعریف شدهاند (از آنجائیکه ما در هنگام تعریف نگاشتها، آنها را از نوع identity مشخص کرده بودیم).
د) رشتهها به نوع nvarchar با اندازه 50 نگاشت شدهاند.
ه) کلیدهای خارجی بر اساس نام جدول با پسوند _id تشکیل شدهاند.
ادامه دارد ...
استفاده از افزونهی jsTree در ASP.NET MVC
استفاده از آن هم سادهاست. تنها کاری که باید انجام دهید، تعریف آخرین نگارش jQuery و سپس افزودن jQuery migrate است:
<script src="jquery.js"></script> <script src="jquery-migrate-1.2.1.js"></script>
خلاصه مهاجرت داده پروفایل ها
- کلاس جدیدی بسازید که دارای خواصی برای ذخیره اطلاعات پروفایل است.
- کلاس جدیدی بسازید که از 'ProfileBase' ارث بری میکند و متدهای لازم برای دریافت پروفایل کاربران را پیاده سازی میکند.
- استفاده از تامین کنندههای پیش فرض را، در فایل web.config فعال کنید. و کلاسی که در مرحله 2 ساختید را بعنوان کلاس پیش فرض برای خواندن اطلاعات پروفایل معرفی کنید.
شروع به کار
پوشه جدیدی با نام 'Models' بسازید تا اطلاعات پروفایل را در آن قرار دهیم.
بعنوان یک مثال، بگذارید تا تاریخ تولد کاربر، شهر سکونت، قد و وزن او را در پروفایلش ذخیره کنیم. قد و وزن بصورت یک کلاس سفارشی (custom class) بنام 'PersonalStats' ذخیره میشوند. برای ذخیره و بازیابی پروفایل ها، به کلاسی احتیاج داریم که 'ProfileBase' را ارث بری میکند. پس کلاس جدیدی با نام 'AppProfile' بسازید.
public class ProfileInfo { public ProfileInfo() { UserStats = new PersonalStats(); } public DateTime? DateOfBirth { get; set; } public PersonalStats UserStats { get; set; } public string City { get; set; } } public class PersonalStats { public int? Weight { get; set; } public int? Height { get; set; } } public class AppProfile : ProfileBase { public ProfileInfo ProfileInfo { get { return (ProfileInfo)GetPropertyValue("ProfileInfo"); } } public static AppProfile GetProfile() { return (AppProfile)HttpContext.Current.Profile; } public static AppProfile GetProfile(string userName) { return (AppProfile)Create(userName); } }
پروفایل را در فایل web.config خود فعال کنید. نام کلاسی را که در مرحله قبل ساختید، بعنوان کلاس پیش فرض برای ذخیره و بازیابی پروفایلها معرفی کنید.
<profile defaultProvider="DefaultProfileProvider" enabled="true" inherits="UniversalProviders_ProfileMigrations.Models.AppProfile"> <providers> ..... </providers> </profile>
برای دریافت اطلاعات پروفایل از کاربر، فرم وب جدیدی در پوشه Account بسازید و آنرا 'AddProfileData.aspx' نامگذاری کنید.
<h2> Add Profile Data for <%# User.Identity.Name %></h2> <asp:Label Text="" ID="Result" runat="server" /> <div> Date of Birth: <asp:TextBox runat="server" ID="DateOfBirth"/> </div> <div> Weight: <asp:TextBox runat="server" ID="Weight"/> </div> <div> Height: <asp:TextBox runat="server" ID="Height"/> </div> <div> City: <asp:TextBox runat="server" ID="City"/> </div> <div> <asp:Button Text="Add Profile" ID="Add" OnClick="Add_Click" runat="server" /> </div>
کد زیر را هم به فایل code-behind اضافه کنید.
protected void Add_Click(object sender, EventArgs e) { AppProfile profile = AppProfile.GetProfile(User.Identity.Name); profile.ProfileInfo.DateOfBirth = DateTime.Parse(DateOfBirth.Text); profile.ProfileInfo.UserStats.Weight = Int32.Parse(Weight.Text); profile.ProfileInfo.UserStats.Height = Int32.Parse(Height.Text); profile.ProfileInfo.City = City.Text; profile.Save(); }
دقت کنید که فضای نامی که کلاس AppProfile در آن قرار دارد را وارد کرده باشید.
اپلیکیشن را اجرا کنید و کاربر جدیدی با نام 'olduser' بسازید. به صفحه جدید 'AddProfileData' بروید و اطلاعات پروفایل کاربر را وارد کنید.
با استفاده از پنجره Server Explorer میتوانید تایید کنید که اطلاعات پروفایل با فرمت xml در جدول 'Profiles' ذخیره میشوند.
مهاجرت الگوی دیتابیس
اسکریپت مورد نیاز را از آدرس https://raw.github.com/suhasj/UniversalProviders-Identity-Migrations/master/Migration.txt دریافت کرده و آن را اجرا کنید. اگر اتصال خود به دیتابیس را تازه کنید خواهید دید که جداول جدیدی اضافه شده اند. میتوانید دادههای این جداول را بررسی کنید تا ببینید چگونه اطلاعات منتقل شده اند.
مهاجرت اپلیکیشن برای استفاده از ASP.NET Identity
- Microsoft.AspNet.Identity.EntityFramework
- Microsoft.AspNet.Identity.Owin
- Microsoft.Owin.Host.SystemWeb
- Microsoft.Owin.Security.Facebook
- Microsoft.Owin.Security.Google
- Microsoft.Owin.Security.MicrosoftAccount
- Microsoft.Owin.Security.Twitter
using Microsoft.AspNet.Identity.EntityFramework; using System; using System.Collections.Generic; using System.Linq; using System.Web; using UniversalProviders_ProfileMigrations.Models; namespace UniversalProviders_Identity_Migrations { public class User : IdentityUser { public User() { CreateDate = DateTime.UtcNow; IsApproved = false; LastLoginDate = DateTime.UtcNow; LastActivityDate = DateTime.UtcNow; LastPasswordChangedDate = DateTime.UtcNow; Profile = new ProfileInfo(); } public System.Guid ApplicationId { get; set; } public bool IsAnonymous { get; set; } public System.DateTime? LastActivityDate { get; set; } public string Email { get; set; } public string PasswordQuestion { get; set; } public string PasswordAnswer { get; set; } public bool IsApproved { get; set; } public bool IsLockedOut { get; set; } public System.DateTime? CreateDate { get; set; } public System.DateTime? LastLoginDate { get; set; } public System.DateTime? LastPasswordChangedDate { get; set; } public System.DateTime? LastLockoutDate { get; set; } public int FailedPasswordAttemptCount { get; set; } public System.DateTime? FailedPasswordAttemptWindowStart { get; set; } public int FailedPasswordAnswerAttemptCount { get; set; } public System.DateTime? FailedPasswordAnswerAttemptWindowStart { get; set; } public string Comment { get; set; } public ProfileInfo Profile { get; set; } } }
انتقال داده پروفایلها به جداول جدید
آخرین نسخه پکیج Entity Framework را نصب کنید. همچنین یک رفرنس به اپلیکیشن وب پروژه بدهید (کلیک راست روی پروژه و گزینه 'Add Reference').
کد زیر را در کلاس Program.cs وارد کنید. این قطعه کد پروفایل تک تک کاربران را میخواند و در قالب 'ProfileInfo' آنها را serialize میکند و در دیتابیس ذخیره میکند.
public class Program { var dbContext = new ApplicationDbContext(); foreach (var profile in dbContext.Profiles) { var stringId = profile.UserId.ToString(); var user = dbContext.Users.Where(x => x.Id == stringId).FirstOrDefault(); Console.WriteLine("Adding Profile for user:" + user.UserName); var serializer = new XmlSerializer(typeof(ProfileInfo)); var stringReader = new StringReader(profile.PropertyValueStrings); var profileData = serializer.Deserialize(stringReader) as ProfileInfo; if (profileData == null) { Console.WriteLine("Profile data deserialization error for user:" + user.UserName); } else { user.Profile = profileData; } } dbContext.SaveChanges(); }
برخی از مدلهای استفاده شده در پوشه 'IdentityModels' تعریف شده اند که در پروژه اپلیکیشن وبمان قرار دارند، بنابراین افزودن فضاهای نام مورد نیاز فراموش نشود.
کد بالا روی دیتابیسی که در پوشه App_Data وجود دارد کار میکند، این دیتابیس در مراحل قبلی در اپلیکیشن وب پروژه ایجاد شد. برای اینکه این دیتابیس را رفرنس کنیم باید رشته اتصال فایل app.config اپلیکیشن کنسول را بروز رسانی کنید. از همان رشته اتصال web.config در اپلیکیشن وب پروژه استفاده کنید. همچنین آدرس فیزیکی کامل را در خاصیت 'AttachDbFilename' وارد کنید.
یک Command Prompt باز کنید و به پوشه bin اپلیکیشن کنسول بالا بروید. فایل اجرایی را اجرا کنید و نتیجه را مانند تصویر زیر بررسی کنید.
در پنجره Server Explorer جدول 'AspNetUsers' را باز کنید. حال ستونهای این جدول باید خواص کلاس مدل را منعکس کنند.
کارایی سیستم را تایید کنید