اعمال کنترل دسترسی پویا در پروژههای ASP.NET Core با استفاده از AuthorizationPolicyProvider سفارشی
در مطلب «سفارشی سازی ASP.NET Core Identity - قسمت پنجم - سیاستهای دسترسی پویا» به طور مفصل به قضیه کنترل دسترسی پویا در ASP.NET Core Identity پرداخته شدهاست؛ در این مطلب روش دیگری را بررسی خواهیم کرد.
مشخص میباشد که بدون وابستگی به روش خاصی، خیلی ساده میتوان به شکل زیر عمل کرد:
services.AddAuthorization(options => { options.AddPolicy("View Projects", policy => policy.RequireClaim(CustomClaimTypes.Permission, "projects.view")); });
[Authorize("View Projects")] public IActionResult Index(int siteId) { return View(); }
Using a large range of policies (for different room numbers or ages, for example), so it doesn’t make sense to add each individual authorization policy with an AuthorizationOptions.AddPolicy call.
کار با پیاده سازی واسط IAuthorizationPolicyProvider شروع میشود؛ یا شاید ارث بری از DefaultAuthorizationPolicyProvider رجیستر شدهی در سیستم DI و توسعه آن هم کافی باشد.
public class AuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider { public AuthorizationPolicyProvider(IOptions<AuthorizationOptions> options) : base(options) { } public override Task<AuthorizationPolicy> GetPolicyAsync(string policyName) { if (!policyName.StartsWith(PermissionAuthorizeAttribute.PolicyPrefix, StringComparison.OrdinalIgnoreCase)) { return base.GetPolicyAsync(policyName); } var permissionNames = policyName.Substring(PermissionAuthorizeAttribute.PolicyPrefix.Length).Split(','); var policy = new AuthorizationPolicyBuilder() .RequireClaim(CustomClaimTypes.Permission, permissionNames) .Build(); return Task.FromResult(policy); } }
متد GetPolicyAsync موظف به یافتن و بازگشت یک Policy ثبت شده میباشد؛ با این حال میتوان با بازنویسی آن و با استفاده از وهلهای از AuthorizationPolicyBuilder، فرآیند تعریف سیاست درخواست شده را که احتمالا در تنظیمات آغازین پروژه تعریف نشده و پیشوند مدنظر را نیز دارد، خوکار کرد. در اینجا امکان ترکیب کردن چندین دسترسی را هم خواهیم داشت که برای این منظور میتوان دسترسیهای مختلف را به صورت comma separated به سیستم معرفی کرد.
نکتهی مهم در تکه کد بالا مربوط است به PolicyPrefix که با استفاده از آن مشخص کردهایم که برای هر سیاست درخواستی، این فرآیند را طی نکند و موجب اختلال در سیستم نشود.
پس از پیاده سازی واسط مطرح شده، لازم است این پیاده سازی جدید را به سیستم DI هم معرفی کنید:
services.AddSingleton<IAuthorizationPolicyProvider, AuthorizationPolicyProvider>();
خوب، تا اینجا فرآیند تعریف سیاستها به صورت خودکار انجام شد. در ادامه نیاز است با تعریف یک فیلتر Authorization، بتوان لیست دسترسیهای مورد نظر برای اکشنی خاص را نیز مشخص کرد تا در متد GetPolicyAsync فوق، کار ثبت خودکار سیاست دسترسی متناظر با آنرا توسط فراخوانی متد policyBuilder.RequireClaim، انجام دهد تا دیگر نیازی به تعریف دستی و جداگانهی آن، در کلاس آغازین برنامه نباشد. برای این منظور به شکل زیر عمل خواهیم کرد:
public class PermissionAuthorizeAttribute : AuthorizeAttribute { internal const string PolicyPrefix = "PERMISSION:"; /// <summary> /// Creates a new instance of <see cref="AuthorizeAttribute"/> class. /// </summary> /// <param name="permissions">A list of permissions to authorize</param> public PermissionAuthorizeAttribute(params string[] permissions) { Policy = $"{PolicyPrefix}{string.Join(",", permissions)}"; } }
همانطور که مشخص میباشد، رشته PERMISSION به عنوان پیشوند رشته تولیدی از لیست اسامی دسترسیها، استفاده شدهاست و در پراپرتی Policy قرار داده شدهاست. این بار برای کنترل دسترسی میتوان به شکل زیر عمل کرد:
[PermissionAuthorize(PermissionNames.Projects_View)] public IActionResult Get(FilteredQueryModel query) { //... } [PermissionAuthorize(PermissionNames.Projects_Create)] public IActionResult Post(ProjectModel model) { //... }
برای مثال در اولین فراخوانی فیلتر PermissionAuthorize فوق، مقدار ثابت PermissionNames.Projects_View به عنوان یک Policy جدید به متد GetPolicyAsync کلاس AuthorizationPolicyProvider سفارشی ما ارسال میشود. چون دارای پیشوند «:PERMISSION» است، مورد پردازش قرار گرفته و توسط متد policyBuilder.RequireClaim به صورت خودکار به سیستم معرفی و ثبت خواهد شد.
همچنین راه حل مطرح شده برای مدیریت دسترسیهای پویا، در gist به اشتراک گذاشته شده «موجودیتهای مرتبط با مدیریت دسترسیهای پویا» را نیز مد نظر قرار دهید.
public class CaptchaController : Controller { private static readonly Brush ForeColor = Brushes.Black; private const string FontName = "tahoma"; private const int FontSize = 14; private const int Width = 130; private const int Height = 35; [HttpGet] public ActionResult Image(string cc) { if (string.IsNullOrEmpty(cc) || string.IsNullOrWhiteSpace(cc)) return null; var captchaData = CustomHashing.DecryptTpl(cc); var rand = new Random((int)DateTime.Now.Ticks); // image stream FileContentResult img = null; using (var mem = new MemoryStream()) using (var bmp = new Bitmap(Width, Height)) using (var mtrx = new Matrix()) using (var gfx = Graphics.FromImage((Image)bmp)) { gfx.TextRenderingHint = TextRenderingHint.ClearTypeGridFit; gfx.SmoothingMode = SmoothingMode.AntiAlias; gfx.FillRectangle(Brushes.White, new Rectangle(0, 0, bmp.Width, bmp.Height)); //add noise int rn, xn, yn; var pen = new Pen(Color.Yellow); for (int i = 1; i < 10; i++) { pen.Color = Color.FromArgb((rand.Next(0, 255)), (rand.Next(0, 255)), (rand.Next(0, 255))); rn = rand.Next(0, (130 / 3)); xn = rand.Next(0, 130); yn = rand.Next(0, 30); gfx.DrawEllipse(pen, xn - rn, yn - rn, rn, rn); } //add chars #region draw pic float x = 1, y = 1; int degree = 10; for (int i = 0; i < captchaData.Length; i++) { mtrx.Reset(); x = (float)(Width * (0.19 * i)); y = (float)(Height * 0.19); degree = rand.Next(-25, 25); if (i == 0 && degree > 20) { x += (FontSize + 5); y -= 15; } mtrx.RotateAt(degree, new PointF(x, y)); gfx.Transform = mtrx; gfx.DrawString(captchaData[i].ToString(), new Font(FontName, FontSize), ForeColor, x, y); gfx.ResetTransform(); } #endregion //render as Jpeg bmp.Save(mem, System.Drawing.Imaging.ImageFormat.Jpeg); img = this.File(mem.GetBuffer(), "image/Jpeg"); } return img; }
@{ var r = new Web.Tools.CustomRandom(); string hash = Web.Tools.CustomHashing.EncryptTpl(r.CraeteCapchaNumericData(4)); } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>test Index</title> </head> <body> <div> <img src="@Url.Action("Image", "Captcha", new { cc = hash })" /> </div> </body> </html>
public class CustomRandom { /// <summary> /// ساخت یک عبارت عددی رندوم /// </summary> public string CraeteCapchaNumericData(int length) { var rnd = new Random((int) DateTime.Now.Ticks); var temp = new StringBuilder(); for (var i = 0; i < length; i++) temp.Append(Convert.ToChar(rnd.Next(49, 58))); return temp.ToString(); } /// <summary> /// ساخت یک عبارت رندوم /// </summary> public string CreateRandomName(int length) { var rnd = new Random((int) DateTime.Now.Ticks); var temp = new StringBuilder(); var flag = 1; for (var i = 0; i < length; i++) { flag = rnd.Next(0, 15); if (flag < 5) temp.Append(Convert.ToChar(rnd.Next(97, 123))); // lower else if (flag >= 5 && flag < 10) temp.Append(Convert.ToChar(rnd.Next(49, 58))); // numeric else temp.Append(Convert.ToChar(rnd.Next(65, 91))); // biger } return temp.ToString(); } }
دریافت آخرین نگارش Froala WYSIWYG Editor
برای دریافت فایلهای آخرین نگارش این ادیتور وب میتوانید به سایت آن، قسمت دریافت فایلها مراجعه نمائید.
http://editor.froala.com/download
و یا به این آدرس مراجعه کنید:
https://github.com/froala/wysiwyg-editor/releases
ساختار پروژه و نحوهی کپی فایلهای آن
در هر دو مثالی که فایلهای آنرا از انتهای بحث میتوانید دریافت کنید، این ساختار رعایت شده است:
فایلهای CSS و فونتهای آن، در پوشهی Content قرار گرفتهاند.
فایلهای اسکریپت و زبان آن (که دارای زبان فارسی هم هست) در پوشهی Scripts کپی شدهاند.
یک نکته
فایل font-awesome.css را نیاز است کمی اصلاح کنید. مسیر پوشهی فونتهای آن اکنون با fonts شروع میشود.
تنظیمات اولیه
تفاوتی نمیکند که از وب فرمها استفاده میکنید یا MVC، نحوهی تعریف و افزودن پیش نیازهای این ادیتور به نحو ذیل است:
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title></title> <link href="Content/font-awesome.css" rel="stylesheet" /> <link href="Content/froala_editor.css" rel="stylesheet" /> <script src="Scripts/jquery-1.10.2.min.js"></script> <script src="Scripts/froala_editor.min.js"></script> <script src="Scripts/langs/fa.js"></script> </head> <body> <form id="form1" runat="server"> </form> </body> </html>
استفاده از Froala WYSIWYG Editor در ASP.NET MVC
در ادامه نحوهی فعال سازی ادیتور وب Froala را در یک View برنامههای ASP.NET MVC ملاحظه میکنید:
@{ ViewBag.Title = "Index"; } <style type="text/css"> /*تنظیم فونت پیش فرض ادیتور*/ .froala-element { } </style> @using (Html.BeginForm(actionName: "Index", controllerName: "Home")) { @Html.TextArea(name: "Editor1") <input type="submit" value="ارسال" /> } @section Scripts { <script type="text/javascript"> $(function () { $('#Editor1').editable({ buttons: ["bold", "italic", "underline", "strikeThrough", "fontFamily", "fontSize", "color", "formatBlock", "align", "insertOrderedList", "insertUnorderedList", "outdent", "indent", "selectAll", "createLink", "insertImage", "insertVideo", "undo", "redo", "html", "save", "inserthorizontalrule"], inlineMode: false, inverseSkin: true, preloaderSrc: '@Url.Content("~/Content/img/preloader.gif")', allowedImageTypes: ["jpeg", "jpg", "png"], height: 300, language: "fa", direction: "rtl", fontList: ["Tahoma, Geneva", "Arial, Helvetica", "Impact, Charcoal"], autosave: true, autosaveInterval: 2500, saveURL: '@Url.Action("FroalaAutoSave", "Home")', saveParams: { postId: "123" }, spellcheck: true, plainPaste: true, imageButtons: ["removeImage", "replaceImage", "linkImage"], borderColor: '#00008b', imageUploadURL: '@Url.Action("FroalaUploadImage", "Home")', imageParams: { postId: "123" }, enableScript: false }); }); </script> }
سپس این ادیتور را بر روی المان TextArea قرار گرفته در صفحه، فعال میکنیم.
در قسمت مقادیر buttons، تمام حالات ممکن پیش بینی شدهاند. هر کدام را که نیاز ندارید، حذف کنید.
نحوهی تعریف زبان و راست به چپ بودن این ادیتور را با مقدار دهی پارامترهای language و direction ملاحظه میکنید.
پارامترهای autosave، saveURL و saveParams کار تنظیم ارسال خودکار محتوای ادیتور را جهت ذخیرهی آن در سرور به عهده دارند. بر اساس مقدار autosaveInterval میتوان مشخص کرد که هر چند میلی ثانیه یکبار اینکار باید انجام شود.
/// <summary> /// ذخیره سازی خودکار /// </summary> [HttpPost] [ValidateInput(false)] public ActionResult FroalaAutoSave(string body, int? postId) // نام پارامتر بادی را تغییر ندهید { //todo: save body ... return new EmptyResult(); }
چون قرار است تگهای HTML به سرور ارسال شوند، ویژگی ValidateInput به false تنظیم شدهاست.
saveParams آن، برای مقدار دهی پارامترهای اضافی است که نیاز میباشند تا به سرور ارسال شوند. مثلا شماره مطلب جاری نیز به سرور ارسال گردد.
در اینجا نام پارامتری که ارسال میگردد، دقیقا مساوی body است. بنابراین آنرا تغییر ندهید.
پارامترهای imageUploadURL و imageParams برای فعال سازی ذخیره تصاویر آن در سرور کاربرد دارند.
اکشن متد مدیریت کنندهی آن به نحو ذیل میتواند تعریف شود:
// todo: مسایل امنیتی آپلود را فراموش نکنید /// <summary> /// ذخیره سازی تصاویر ارسالی /// </summary> [HttpPost] public ActionResult FroalaUploadImage(HttpPostedFileBase file, int? postId) // نام پارامتر فایل را تغییر ندهید { var fileName = Path.GetFileName(file.FileName); var rootPath = Server.MapPath("~/images/"); file.SaveAs(Path.Combine(rootPath, fileName)); return Json(new { link = "images/" + fileName }, JsonRequestBehavior.AllowGet); }
خروجی آن برای مشخص سازی محل ذخیره سازی تصویر در سرور باید یک خروجی JSON دارای خاصیت و پارامتر link به نحو فوق باشد (این مسیر، یک مسیر نسبی است؛ نسبت به ریشه سایت).
imageParams آن برای مقدار دهی پارامترهای اضافی است که نیاز میباشند تا به سرور ارسال شوند. مثلا شماره مطلب جاری نیز به سرور ارسال گردد.
استفاده از Froala WYSIWYG Editor در ASP.NET Web forms
تمام نکاتی که در قسمت تنظیمات ASP.NET MVC در مورد ویژگیهای سمت کلاینت این ادیتور ذکر شد، در مورد وب فرمها نیز صادق است. فقط قسمت مدیریت سمت سرور آن اندکی تفاوت دارد.
<%@ Page Title="" Language="C#" MasterPageFile="~/Site.Master" ValidateRequest="false" EnableEventValidation="false" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="FroalaWebFormsTest.Default" %> <%--اعتبارسنجی ورودی غیرفعال شده چون باید تگ ارسال شود--%> <%--همچنین در وب کانفیگ هم تنظیم دیگری نیاز دارد--%> <asp:Content ID="Content1" ContentPlaceHolderID="head" runat="server"> </asp:Content> <asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="server"> <%--حالت کلاینت آی دی بهتر است تنظیم شود در اینجا--%> <asp:TextBox ID="txtEditor" ClientIDMode="Static" runat="server" Height="199px" TextMode="MultiLine" Width="447px"></asp:TextBox> <br /> <asp:Button ID="btnSave" runat="server" OnClick="btnSave_Click" Text="ارسال" /> <style type="text/css"> /*تنظیم فونت پیش فرض ادیتور*/ .froala-element { } </style> <script type="text/javascript"> $(function () { $('#txtEditor').editable({ buttons: ["bold", "italic", "underline", "strikeThrough", "fontFamily", "fontSize", "color", "formatBlock", "align", "insertOrderedList", "insertUnorderedList", "outdent", "indent", "selectAll", "createLink", "insertImage", "insertVideo", "undo", "redo", "html", "save", "inserthorizontalrule"], inlineMode: false, inverseSkin: true, preloaderSrc: 'Content/img/preloader.gif', allowedImageTypes: ["jpeg", "jpg", "png"], height: 300, language: "fa", direction: "rtl", fontList: ["Tahoma, Geneva", "Arial, Helvetica", "Impact, Charcoal"], autosave: true, autosaveInterval: 2500, saveURL: 'FroalaHandler.ashx', saveParams: { postId: "123" }, spellcheck: true, plainPaste: true, imageButtons: ["removeImage", "replaceImage", "linkImage"], borderColor: '#00008b', imageUploadURL: 'FroalaHandler.ashx', imageParams: { postId: "123" }, enableScript: false }); }); </script> </asp:Content>
به علاوه ClientIDMode=Static نیز تنظیم شدهاست، تا بتوان از ID تکست باکس قرار گرفته در صفحه، به سادگی در کدهای سمت کاربر جیکوئری استفاده کرد.
اگر دقت کرده باشید، save urlها اینبار به فایل FroalaHandler.ashx اشاره میکنند. محتوای این Genric handler را ذیل مشاهده میکنید:
using System.IO; using System.Web; using System.Web.Script.Serialization; namespace FroalaWebFormsTest { public class FroalaHandler : IHttpHandler { //todo: برای اینکارها بهتر است از وب ای پی آی استفاده شود //todo: یا دو هندلر مجزا یکی برای تصاویر و دیگری برای ذخیره سازی متن public void ProcessRequest(HttpContext context) { var body = context.Request.Form["body"]; var postId = context.Request.Form["postId"]; if (!string.IsNullOrWhiteSpace(body) && !string.IsNullOrWhiteSpace(postId)) { //todo: save changes context.Response.ContentType = "text/plain"; context.Response.Write(""); context.Response.End(); } var files = context.Request.Files; if (files.Keys.Count > 0) { foreach (string fileKey in files) { var file = context.Request.Files[fileKey]; if (file == null || file.ContentLength == 0) continue; //todo: در اینجا مسایل امنیتی آپلود فراموش نشود var fileName = Path.GetFileName(file.FileName); var rootPath = context.Server.MapPath("~/images/"); file.SaveAs(Path.Combine(rootPath, fileName)); var json = new JavaScriptSerializer().Serialize(new { link = "images/" + fileName }); // البته اینجا یک فایل بیشتر ارسال نمیشود context.Response.ContentType = "text/plain"; context.Response.Write(json); context.Response.End(); } } context.Response.ContentType = "text/plain"; context.Response.Write(""); context.Response.End(); } public bool IsReusable { get { return false; } } } }
یک نکتهی امنیتی مهم
<location path="upload"> <system.webServer> <handlers accessPolicy="Read" /> </system.webServer> </location>
کدهای کامل این مطلب را در ادامه میتوانید دریافت کنید
Froala-Sample
نام علمی لحظهی سال تحویل، Vernal Equinox است. Equinox به معنای نقطهای است که یک فصل، به فصلی دیگر تبدیل میشود:
Equinox واژهای است لاتین به معنای «شبهای مساوی» و به این نکته اشاره دارد که در Equinox، طول شب و روز یکی میشوند. هر سال دارای دو Equinox است: vernal equinox و autumnal equinox (بهاری و پائیزی). البته باید درنظر داشت که Equinox بهاری در نیم کرهی شمالی بیشتر معنا پیدا میکند؛ زیرا در نیم کرهی جنوبی در همین زمان، پائیز شروع میشود.
بنابراین میتوان enum زیر را برای تعریف این چهار ثابت رخدادهای خورشیدی تعریف کرد:
public enum SunEvent { /// <summary> /// march equinox /// </summary> VernalEquinox, /// <summary> /// june solstice /// </summary> SummerSolstice, /// <summary> /// september equinox /// </summary> AutumnalEquinox, /// <summary> /// december solstice /// </summary> WinterSolstice }
در ادامه برای محاسبهی زمان equinox از فصل 27 کتاب Astronomical Algorithms کمک گرفته شده و تمام اعداد و ارقام و جداولی را که ملاحظه میکنید از این کتاب استخراج شدهاند.
/// <summary> /// Based on Jean Meeus book _Astronomical Algorithms_ /// </summary> public static class EquinoxCalculator { /// <summary> /// Degrees to Radians conversion factor. /// </summary> public static readonly double Deg2Radian = Math.PI / 180.0; public static bool ApproxEquals(double d1, double d2) { const double epsilon = 2.2204460492503131E-16; if (d1 == d2) return true; var tolerance = ((Math.Abs(d1) + Math.Abs(d2)) + 10.0) * epsilon; var difference = d1 - d2; return (-tolerance < difference && tolerance > difference); } /// <summary> /// Calculates time of the Equinox and Solstice. /// </summary> /// <param name="year">Year to calculate for.</param> /// <param name="sunEvent">Event to calculate.</param> /// <returns>Date and time event occurs as a fractional Julian Day.</returns> public static DateTime GetSunEventUtc(this int year, SunEvent sunEvent) { double y; double julianEphemerisDay; if (year >= 1000) { y = (Math.Floor((double)year) - 2000) / 1000; switch (sunEvent) { case SunEvent.VernalEquinox: julianEphemerisDay = 2451623.80984 + 365242.37404 * y + 0.05169 * (y * y) - 0.00411 * (y * y * y) - 0.00057 * (y * y * y * y); break; case SunEvent.SummerSolstice: julianEphemerisDay = 2451716.56767 + 365241.62603 * y + 0.00325 * (y * y) - 0.00888 * (y * y * y) - 0.00030 * (y * y * y * y); break; case SunEvent.AutumnalEquinox: julianEphemerisDay = 2451810.21715 + 365242.01767 * y + 0.11575 * (y * y) - 0.00337 * (y * y * y) - 0.00078 * (y * y * y * y); break; case SunEvent.WinterSolstice: julianEphemerisDay = 2451900.05952 + 365242.74049 * y + 0.06223 * (y * y) - 0.00823 * (y * y * y) - 0.00032 * (y * y * y * y); break; default: throw new NotSupportedException(); } } else { y = Math.Floor((double)year) / 1000; switch (sunEvent) { case SunEvent.VernalEquinox: julianEphemerisDay = 1721139.29189 + 365242.13740 * y + 0.06134 * (y * y) - 0.00111 * (y * y * y) - 0.00071 * (y * y * y * y); break; case SunEvent.SummerSolstice: julianEphemerisDay = 1721233.25401 + 365241.72562 * y + 0.05323 * (y * y) - 0.00907 * (y * y * y) - 0.00025 * (y * y * y * y); break; case SunEvent.AutumnalEquinox: julianEphemerisDay = 1721325.70455 + 365242.49558 * y + 0.11677 * (y * y) - 0.00297 * (y * y * y) - 0.00074 * (y * y * y * y); break; case SunEvent.WinterSolstice: julianEphemerisDay = 1721414.39987 + 365242.88257 * y + 0.00769 * (y * y) - 0.00933 * (y * y * y) - 0.00006 * (y * y * y * y); break; default: throw new NotSupportedException(); } } var julianCenturies = (julianEphemerisDay - 2451545.0) / 36525; var w = 35999.373 * julianCenturies - 2.47; var lambda = 1 + 0.0334 * Math.Cos(w * Deg2Radian) + 0.0007 * Math.Cos(2 * w * Deg2Radian); var sumOfPeriodicTerms = getSumOfPeriodicTerms(julianCenturies); return JulianToUtcDate(julianEphemerisDay + (0.00001 * sumOfPeriodicTerms / lambda)); } /// <summary> /// Converts a fractional Julian Day to a .NET DateTime. /// </summary> /// <param name="julianDay">Fractional Julian Day to convert.</param> /// <returns>Date and Time in .NET DateTime format.</returns> public static DateTime JulianToUtcDate(double julianDay) { double a; int month, year; var j = julianDay + 0.5; var z = Math.Floor(j); var f = j - z; if (z >= 2299161) { var alpha = Math.Floor((z - 1867216.25) / 36524.25); a = z + 1 + alpha - Math.Floor(alpha / 4); } else a = z; var b = a + 1524; var c = Math.Floor((b - 122.1) / 365.25); var d = Math.Floor(365.25 * c); var e = Math.Floor((b - d) / 30.6001); var day = b - d - Math.Floor(30.6001 * e) + f; if (e < 14) month = (int)(e - 1.0); else if (ApproxEquals(e, 14) || ApproxEquals(e, 15)) month = (int)(e - 13.0); else throw new NotSupportedException("Illegal month calculated."); if (month > 2) year = (int)(c - 4716.0); else if (month == 1 || month == 2) year = (int)(c - 4715.0); else throw new NotSupportedException("Illegal year calculated."); var span = TimeSpan.FromDays(day); return new DateTime(year, month, (int)day, span.Hours, span.Minutes, span.Seconds, span.Milliseconds, new GregorianCalendar(), DateTimeKind.Utc); } /// <summary> /// These values are from Table 27.C /// </summary> private static double getSumOfPeriodicTerms(double julianCenturies) { return 485 * Math.Cos(Deg2Radian * 324.96 + Deg2Radian * (1934.136 * julianCenturies)) + 203 * Math.Cos(Deg2Radian * 337.23 + Deg2Radian * (32964.467 * julianCenturies)) + 199 * Math.Cos(Deg2Radian * 342.08 + Deg2Radian * (20.186 * julianCenturies)) + 182 * Math.Cos(Deg2Radian * 27.85 + Deg2Radian * (445267.112 * julianCenturies)) + 156 * Math.Cos(Deg2Radian * 73.14 + Deg2Radian * (45036.886 * julianCenturies)) + 136 * Math.Cos(Deg2Radian * 171.52 + Deg2Radian * (22518.443 * julianCenturies)) + 77 * Math.Cos(Deg2Radian * 222.54 + Deg2Radian * (65928.934 * julianCenturies)) + 74 * Math.Cos(Deg2Radian * 296.72 + Deg2Radian * (3034.906 * julianCenturies)) + 70 * Math.Cos(Deg2Radian * 243.58 + Deg2Radian * (9037.513 * julianCenturies)) + 58 * Math.Cos(Deg2Radian * 119.81 + Deg2Radian * (33718.147 * julianCenturies)) + 52 * Math.Cos(Deg2Radian * 297.17 + Deg2Radian * (150.678 * julianCenturies)) + 50 * Math.Cos(Deg2Radian * 21.02 + Deg2Radian * (2281.226 * julianCenturies)) + 45 * Math.Cos(Deg2Radian * 247.54 + Deg2Radian * (29929.562 * julianCenturies)) + 44 * Math.Cos(Deg2Radian * 325.15 + Deg2Radian * (31555.956 * julianCenturies)) + 29 * Math.Cos(Deg2Radian * 60.93 + Deg2Radian * (4443.417 * julianCenturies)) + 28 * Math.Cos(Deg2Radian * 155.12 + Deg2Radian * (67555.328 * julianCenturies)) + 17 * Math.Cos(Deg2Radian * 288.79 + Deg2Radian * (4562.452 * julianCenturies)) + 16 * Math.Cos(Deg2Radian * 198.04 + Deg2Radian * (62894.029 * julianCenturies)) + 14 * Math.Cos(Deg2Radian * 199.76 + Deg2Radian * (31436.921 * julianCenturies)) + 12 * Math.Cos(Deg2Radian * 95.39 + Deg2Radian * (14577.848 * julianCenturies)) + 12 * Math.Cos(Deg2Radian * 287.11 + Deg2Radian * (31931.756 * julianCenturies)) + 12 * Math.Cos(Deg2Radian * 320.81 + Deg2Radian * (34777.259 * julianCenturies)) + 9 * Math.Cos(Deg2Radian * 227.73 + Deg2Radian * (1222.114 * julianCenturies)) + 8 * Math.Cos(Deg2Radian * 15.45 + Deg2Radian * (16859.074 * julianCenturies)); } }
خروجی این الگوریتم را برای سالهای 2014 تا 2022 به صورت ذیل مشاهده میکنید:
2014 -> 1392/12/29 20:28:08 2015 -> 1394/01/01 02:16:29 2016 -> 1395/01/01 08:01:21 2017 -> 1395/12/30 14:00:00 2018 -> 1396/12/29 19:46:10 2019 -> 1398/01/01 01:29:29 2020 -> 1399/01/01 07:21:03 2021 -> 1399/12/30 13:08:41 2022 -> 1400/12/29 19:04:37
کدهای کامل این پروژه را از اینجا میتوانید دریافت کنید
Equinox.zip
مدلهای AuditLog (اصلاحیه)و ActivityLog
- استفاده از جداول جدا برای هر کدام از جداول به صورتیکه یک ارتباط یک به چند مابین آنها برقرار است. از این جداول تحت عنوان HistoryTable یاد میشود.
- استفاده از یک جدول برای نگهداری تاریخچهی تغییرات جداولی که نیازمند این امکان هستند.
/// <summary> /// Represent The Operation's log /// </summary> public class AuditLog { #region Ctor /// <summary> /// Create One Instance Of <see cref="AuditLog"/> /// </summary> public AuditLog() { Id = SequentialGuidGenerator.NewSequentialGuid(); OperatedOn = DateTime.Now; } #endregion #region Properties /// <summary> /// sets or gets identifier of AuditLog /// </summary> public virtual Guid Id { get; set; } /// <summary> /// gets or sets Type of Modification(create,softDelet,Delete,update) /// </summary> public virtual AuditAction Action { get; set; } /// <summary> /// sets or gets description of Log /// </summary> public virtual string Description { get; set; } /// <summary> /// sets or gets when log is operated /// </summary> public virtual DateTime OperatedOn { get; set; } /// <summary> /// sets or gets Type Of Entity /// </summary> public virtual string Entity { get; set; } /// <summary> /// gets or sets Old value of Properties before modification /// </summary> public virtual string XmlOldValue { get; set; } /// <summary> /// gets or sets XML Base OldValue of Properties (NotMapped) /// </summary> public virtual XElement XmlOldValueWrapper { get { return XElement.Parse(XmlOldValue); } set { XmlOldValue = value.ToString(); } } /// <summary> /// gets or sets new value of Properties after modification /// </summary> public virtual string XmlNewValue { get; set; } /// <summary> /// gets or sets XML Base NewValue of Properties (NotMapped) /// </summary> public virtual XElement XmlNewValueWrapper { get { return XElement.Parse(XmlNewValue); } set { XmlNewValue = value.ToString(); } } /// <summary> /// gets or sets Identifier Of Entity /// </summary> public virtual string EntityId { get; set; } /// <summary> /// gets or sets user agent information /// </summary> public virtual string Agent { get; set; } /// <summary> /// gets or sets user's ip address /// </summary> public virtual string OperantIp { get; set; } #endregion #region NavigationProperties /// <summary> /// sets or gets log's creator /// </summary> public virtual User Operant { get; set; } /// <summary> /// sets or gets identifier of log's creator /// </summary> public virtual long OperantId { get; set; } #endregion } public enum AuditAction { Create, Update, Delete, SoftDelete, }
- Action : از نوع AdutiAction است و برای مشخص کردن نوع عملیاتی که انجام شده است، میباشد.
- Description : اگر نیاز باشد توضیحاتی اضافی ثبت شوند، از این خصوصیت استفاده میشود.
- Entity : مشخص کنندهی نام مدل خواهد بود. شاید بهتر بود از یک Enum استفاده میشد. ولی این سیستم به احتمال زیاد قرار است افزونه پذیر باشد و استفاده از Enum، یعنی محدودیت و این امکان وجود نخواهد داشت که سایر افزونهها بتوانند از مدل بالا استفاده کنند. برا ی مثال BlogPost , NewsItem , ForumPost , ...
- EntitytId : آی دی رکوردی است که تاریخچهی آن ثبت شده است. از آنجائیکه بعضی از موجودیتها دارای آی دی از نوع long و برخی دیگر Guid ، لذا ذخیرهی رشتهای آن مفید خواهد بود.
- XmlOldValue : در برگیرندهی مقدار (قبل از اعمال تغییرات) خصوصیاتی است که لازم است از یک موجودیت مشخص، در قالب XML رشتهای ذخیره شوند.
- XmlNewValue : در برگیرندهی مقدار (بعد از تغییرات) خصوصیاتی است که لازم است از یک موجودیت مشخص، در قالب XML رشتهای ذخیره شوند.
- Operant, OperantId: برای برقراری ارتباط یک به چند مابین مدل کاربر و مدل بالا در نظر گرفته شدهاند که به عنوان انجام دهندهی این تغییرات بوده است.
/// <summary> /// Represents Activity Log record /// </summary> public class ActivityLog { #region Ctor /// <summary> /// Create one instance of <see cref="ActivityLog"/> /// </summary> public ActivityLog() { Id = SequentialGuidGenerator.NewSequentialGuid(); OperatedOn=DateTime.Now; } #endregion #region Properties /// <summary> /// gets or sets identifier /// </summary> public virtual Guid Id { get; set; } /// <summary> /// gets or sets the comment of this activity /// </summary> public virtual string Comment { get; set; } /// <summary> /// gets or sets the date that this activity was done /// </summary> public virtual DateTime OperatedOn { get; set; } /// <summary> /// gets or sets the page url . /// </summary> public virtual string Url { get; set; } /// <summary> /// gets or sets the title of page if Url is Not null /// </summary> public virtual string Title { get; set; } /// <summary> /// gets or sets user agent information /// </summary> public virtual string Agent { get; set; } /// <summary> /// gets or sets user's ip address /// </summary> public virtual string OperantIp { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets the type of this activity /// </summary> public virtual ActivityLogType Type{ get; set; } /// <summary> /// gets or sets the type's id of this activity /// </summary> public virtual Guid TypeId { get; set; } /// <summary> /// gets or sets User that done this activity /// </summary> public virtual User Operant { get; set; } /// <summary> /// gets or sets Id of User that done this activity /// </summary> public virtual long OperantId { get; set; } #endregion } /// <summary> /// Represents Activity Log Type Record /// </summary> public class ActivityLogType { #region Ctor /// <summary> /// Create one Instance of <see cref="ActivityLogType"/> /// </summary> public ActivityLogType() { Id = SequentialGuidGenerator.NewSequentialGuid(); } #endregion #region Properties /// <summary> /// gets or sets identifier /// </summary> public virtual Guid Id { get; set; } /// <summary> /// gets or sets the system name /// </summary> public virtual string Name{ get; set; } /// <summary> /// gets or sets the display name /// </summary> public virtual string DisplayName { get; set; } /// <summary> /// gets or sets the description /// </summary> public virtual string Description { get; set; } /// <summary> /// indicate this log type is enable for logging /// </summary> public virtual bool IsEnabled { get; set; } #endregion }
- Comment : توضیحات کوتاهی از اکشنی که کاربر انجام داده است.
- Url : آدرس صفحهای که این عملیات در آنجا انجام شده است. این خصوصیت نالپذیر میباشد.
- Title : عنوان صفحهای که این عملیات در آنجا انجام شده است؛ اگر Url نال نباشد.
- Operant , OperantId : برای برقراری ارتباط یک به چند بین کاربر و مدل فعالیتها در نظر گرفته شدهاند.
- Type : از نوع ActivityLogType پیاده سازی شده در بالا میباشد. با استفاده از مدل ActivityLogType میتوان مثلا لاگ فعالیت مربوط به بخش اخبار را غیر فعال کند یا بالعکس و از این موارد.
- Name : نام سیستمی آن است. برای مثال : Login ، NewsComment ، NewsItem و ...
- IsEnabled : نشان دهندهی این است که این نوع لاگ فعال است یا خیر.
کلاس پایه تمام مدلها (اصلاحیه)
/// <summary> /// Represents the entity /// </summary> /// <typeparam name="TForeignKey">type of user's Id that can be long or long? </typeparam> public abstract class Entity<TForeignKey> { #region Properties /// <summary> /// gets or sets date that this entity was created /// </summary> public virtual DateTime CreatedOn { get; set; } /// <summary> /// gets or sets Date that this entity was updated /// </summary> public virtual DateTime ModifiedOn { get; set; } /// <summary> /// gets or sets IP Address of Creator /// </summary> public virtual string CreatorIp { get; set; } /// <summary> /// gets or set IP Address of Modifier /// </summary> public virtual string ModifierIp { get; set; } /// <summary> /// indicate this entity is Locked for Modify /// </summary> public virtual bool ModifyLocked { get; set; } /// <summary> /// indicate this entity is deleted softly /// </summary> public virtual bool IsDeleted { get; set; } /// <summary> /// gets or sets information of user agent of modifier /// </summary> public virtual string ModifierAgent { get; set; } /// <summary> /// gets or sets information of user agent of Creator /// </summary> public virtual string CreatorAgent { get; set; } /// <summary> /// gets or sets date that this entity repoted last time /// </summary> public virtual DateTime? ReportedOn { get; set; } /// <summary> /// gets or sets counter for Content's report /// </summary> public virtual int ReportsCount { get; set; } /// <summary> /// gets or sets count of Modification Default is 1 /// </summary> public virtual int Version { get; set; } /// <summary> /// gets or sets action (create,update,softDelete) /// </summary> public virtual AuditAction Action { get; set; } /// <summary> /// gets or sets TimeStamp for prevent concurrency Problems /// </summary> public virtual byte[] RowVersion { get; set; } #endregion #region NavigationProperties /// <summary> /// gets ro sets User that Modify this entity /// </summary> public virtual User ModifiedBy { get; set; } /// <summary> /// gets ro sets Id of User that modify this entity /// </summary> public virtual TForeignKey ModifiedById { get; set; } /// <summary> /// gets ro sets User that Create this entity /// </summary> public virtual User CreatedBy { get; set; } /// <summary> /// gets ro sets User that Create this entity /// </summary> public virtual TForeignKey CreatedById { get; set; } #endregion } /// <summary> /// Represents the base Entity /// </summary> /// <typeparam name="TKey">type of Id</typeparam> /// <typeparam name="TForeignKey">type of User's Id that can be long or long?</typeparam> public abstract class BaseEntity<TKey,TForeignKey> : Entity<TForeignKey> { #region Properties /// <summary> /// gets or sets Identifier of this Entity /// </summary> public virtual TKey Id { get; set; } #endregion }
مدل سیستم آگاه سازی
/// <summary> /// Represents the Notification Record /// </summary> public class Notification { #region Ctor /// <summary> /// create one instance of <see cref="Notification"/> /// </summary> public Notification() { Id = SequentialGuidGenerator.NewSequentialGuid(); ReceivedOn = DateTime.Now; } #endregion #region Properties /// <summary> /// gets or sets identifier /// </summary> public virtual Guid Id { get; set; } /// <summary> /// indicate that this notification is read by owner /// </summary> public virtual bool IsRead { get; set; } /// <summary> /// gets or sets notification's text body /// </summary> public virtual string Message { get; set; } /// <summary> /// gets or sets page url that this notification is related with it /// </summary> public virtual string Url { get; set; } /// <summary> /// gets or sets date that this Notification Received /// </summary> public virtual DateTime ReceivedOn { get; set; } /// <summary> /// gets or sets the type of notification /// </summary> public virtual NotificationType Type { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets the id of user that is owner of this notification /// </summary> public virtual long OwnerId { get; set; } /// <summary> /// gets or sets the user that is owner of this notification /// </summary> public virtual User Owner { get; set; } #endregion } public enum NotificationType { NewConversation, NewConversationReply, ... }
- IsRead : مشخص کنندهی این است که یک اطلاع رسانی خوانده شده است یا خیر. در آخر هر روز اطلاع رسانیهایی که دارای خصوصیت IsRead با مقدار true هستند، حذف خواهند شد.
- Url : برای مواردی که لازم است کاربر با کلیک بر روی آن به صفحهی خاصی هدایت شود.
- OwnerId, Owner : برای برقراری ارتباط یک به چند بین کاربر و مدل Notification در نظر گرفته شدهاند.
- Type : از نوع NotificationType و مشخص کنندهی نوع اطلاع رسانی میباشد .
مدل Observation
public class Observation { #region Ctor /// <summary> /// create one instance of <see cref="Observation"/> /// </summary> public Observation() { LastObservedOn = DateTime.Now; Id = SequentialGuidGenerator.NewSequentialGuid(); } #endregion #region Properties public virtual Guid Id { get; set; } /// <summary> /// gets or sets datetime of last visit /// </summary> public virtual DateTime LastObservedOn { get; set; } /// <summary> /// gets or sets Id Of section That user is observing the entity /// </summary> public virtual string SectionId { get; set; } /// <summary> /// gets or sets section That user is observing in it /// </summary> public virtual string Section { get; set; } #endregion #region NavigationProperites /// <summary> /// gets or sets user that observed the entity /// </summary> public virtual User Observer { get; set; } /// <summary> /// gets or sets identifier of user that observed the entity /// </summary> public virtual long ObserverId { get; set; } #endregion }
مدل صفحات داینامیک
/// <summary> /// represents one custom page /// </summary> public class Page : BaseEntity<long, long> { #region Properties /// <summary> /// gets or sets the blog pot body /// </summary> public virtual string Body { get; set; } /// <summary> /// gets or sets the content title /// </summary> public virtual string Title { get; set; } /// <summary> /// gets or sets value indicating Custom Slug /// </summary> public virtual string SlugUrl { get; set; } /// <summary> /// gets or sets meta title for seo /// </summary> public virtual string MetaTitle { get; set; } /// <summary> /// gets or sets meta keywords for seo /// </summary> public virtual string MetaKeywords { get; set; } /// <summary> /// gets or sets meta description of the content /// </summary> public virtual string MetaDescription { get; set; } /// <summary> /// gets or sets /// </summary> public virtual string FocusKeyword { get; set; } /// <summary> /// gets or sets value indicating whether the content use CanonicalUrl /// </summary> public virtual bool UseCanonicalUrl { get; set; } /// <summary> /// gets or sets CanonicalUrl That the Post Point to it /// </summary> public virtual string CanonicalUrl { get; set; } /// <summary> /// gets or sets value indicating whether the content user no Follow for Seo /// </summary> public virtual bool UseNoFollow { get; set; } /// <summary> /// gets or sets value indicating whether the content user no Index for Seo /// </summary> public virtual bool UseNoIndex { get; set; } /// <summary> /// gets or sets value indicating whether the content in sitemap /// </summary> public virtual bool IsInSitemap { get; set; } /// <summary> /// gets or sets title for snippet /// </summary> public string SocialSnippetTitle { get; set; } /// <summary> /// gets or sets description for snippet /// </summary> public string SocialSnippetDescription { get; set; } /// <summary> /// gets or sets section's type that this page show on /// </summary> public virtual ShowPageSection Section { get; set; } /// <summary> /// indicate this page has not any body /// </summary> public virtual bool IsCategory { get; set; } /// <summary> /// gets or sets order for display forum /// </summary> public virtual int DisplayOrder { get; set; } #endregion #region NavigationProeprties /// <summary> /// gets or sets Parent of this page /// </summary> public virtual Page Parent { get; set; } /// <summary> /// gets or sets parent'id of this page /// </summary> public virtual long? ParentId { get; set; } /// <summary> /// get or set collection of page that they are children of this page /// </summary> public virtual ICollection<Page> Children { get; set; } #endregion } public enum ShowPageSection { Menu, Footer, SideBar }
نتیجهی تا این قسمت
معرفی مدل ارسالی برنامه سمت کلاینت
فرض کنید مطابق شکل فوق، قرار است اطلاعات یک کاربر، به همراه تعدادی تصویر از او، به سمت Web API ارسال شوند. برای نمونه، مدل اشتراکی کاربر را به صورت زیر تعریف کردهایم:
using System.ComponentModel.DataAnnotations; namespace BlazorWasmUpload.Shared { public class User { [Required] public string Name { get; set; } [Required] [Range(18, 90)] public int Age { get; set; } } }
ساختار کنترلر Web API دریافت کنندهی مدل برنامه
در این حالت امضای اکشن متد CreateUser واقع در کنترلر Files که قرار است این اطلاعات را دریافت کند، به صورت زیر است:
namespace BlazorWasmUpload.Server.Controllers { [ApiController] [Route("api/[controller]/[action]")] public class FilesController : ControllerBase { [HttpPost] public async Task<IActionResult> CreateUser( [FromForm] User userModel, [FromForm] IList<IFormFile> inputFiles = null)
ایجاد سرویسی در سمت کلاینت، برای آپلود اطلاعات یک مدل به همراه فایلهای انتخابی کاربر
کدهای کامل سرویسی که میتواند انتظارات یاد شده را در سمت کلاینت برآورده کند، به صورت زیر است:
using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Forms; namespace BlazorWasmUpload.Client.Services { public interface IFilesManagerService { Task<HttpResponseMessage> PostModelWithFilesAsync<T>(string requestUri, IEnumerable<IBrowserFile> browserFiles, string fileParameterName, T model, string modelParameterName); } public class FilesManagerService : IFilesManagerService { private readonly HttpClient _httpClient; public FilesManagerService(HttpClient httpClient) { _httpClient = httpClient; } public async Task<HttpResponseMessage> PostModelWithFilesAsync<T>( string requestUri, IEnumerable<IBrowserFile> browserFiles, string fileParameterName, T model, string modelParameterName) { var requestContent = new MultipartFormDataContent(); requestContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data"); if (browserFiles?.Any() == true) { foreach (var file in browserFiles) { var stream = file.OpenReadStream(maxAllowedSize: 512000 * 1000); requestContent.Add(content: new StreamContent(stream, (int)file.Size), name: fileParameterName, fileName: file.Name); } } requestContent.Add( content: new StringContent(JsonSerializer.Serialize(model), Encoding.UTF8, "application/json"), name: modelParameterName); var result = await _httpClient.PostAsync(requestUri, requestContent); result.EnsureSuccessStatusCode(); return result; } } }
- کامپوننت استاندارد InputFiles در Blazor Wasm، میتواند لیستی از IBrowserFileهای انتخابی توسط کاربر را در اختیار ما قرار دهد.
- fileParameterName، همان نام پارامتر "inputFiles" در اکشن متد سمت سرور مثال جاری است که به صورت متغیر قابل تنظیم شدهاست.
- model جنریک، برای نمونه وهلهای از شیء User است که به یک فرم Blazor متصل است.
- modelParameterName، همان نام پارامتر "userModel" در اکشن متد سمت سرور مثال جاری است که به صورت متغیر قابل تنظیم شدهاست.
- در ادامه یک MultipartFormDataContent را تشکیل دادهایم. توسط این ساختار میتوان فایلها و اطلاعات یک مدل را به صورت یکجا جمع آوری و به سمت سرور ارسال کرد. به این content ویژه، ابتدای لیستی از new StreamContentها را اضافه میکنیم. این streamها توسط متد OpenReadStream هر IBrowserFile دریافتی از کامپوننت InputFile، تشکیل میشوند. متد OpenReadStream به صورت پیشفرض فقط فایلهایی تا حجم 500 کیلوبایت را پردازش میکند و اگر فایلی حجیمتر را به آن معرفی کنیم، یک استثناء را صادر خواهد کرد. به همین جهت میتوان توسط پارامتر maxAllowedSize آن، این مقدار پیشفرض را تغییر داد.
- در اینجا مدل برنامه به صورت JSON به عنوان یک new StringContent اضافه شدهاست. مزیت کار کردن با JsonSerializer.Serialize استاندارد، ساده شدن برنامه و عدم درگیری با مباحث Reflection و خواندن پویای اطلاعات مدل جنریک است. اما در ادامه مشکلی را پدید خواهد آورد! این رشتهی ارسالی به سمت سرور، به صورت خودکار به یک مدل، Bind نخواهد شد و باید برای آن یک model-binder سفارشی را بنویسیم. یعنی این رشتهی new StringContent را در سمت سرور دقیقا به صورت یک رشته معمولی میتوان دریافت کرد و نه حالت دیگری و مهم نیست که اکنون به صورت JSON ارسال میشود؛ چون MultipartFormDataContent ویژهای را داریم، model-binder پیشفرض ASP.NET Core، انتظار یک شیء خاص را در این بین ندارد.
- تنظیم "form-data" را هم به عنوان Headers.ContentDisposition مشاهده میکنید. بدون وجود آن، ویژگی [FromForm] سمت Web API، از پردازش درخواست جلوگیری خواهد کرد.
- در آخر توسط متد PostAsync، این اطلاعات جمع آوری شده، به سمت سرور ارسال خواهند شد.
پس از تهیهی سرویس ویژهی فوق که میتواند اطلاعات فایلها و یک مدل را به صورت یکجا به سمت سرور ارسال کند، اکنون نوبت به ثبت و معرفی آن به سیستم تزریق وابستگیها در فایل Program.cs برنامهی کلاینت است:
namespace BlazorWasmUpload.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); // ... builder.Services.AddScoped<IFilesManagerService, FilesManagerService>(); // ... } } }
تکمیل فرم ارسال اطلاعات مدل و فایلهای همراه آن در برنامهی Blazor WASM
در ادامه پس از تشکیل IFilesManagerService، نوبت به استفادهی از آن است. به همین جهت همان کامپوننت Index برنامه را به صورت زیر تغییر میدهیم:
@code { IReadOnlyList<IBrowserFile> SelectedFiles; User UserModel = new User(); bool isProcessing; string UploadErrorMessage;
- SelectedFiles همان لیست فایلهای انتخابی توسط کاربر است.
- UserModel شیءای است که به EditForm جاری متصل خواهد شد.
- توسط isProcessing ابتدا و انتهای آپلود به سرور را مشخص میکنیم.
- UploadErrorMessage، خطای احتمالی انتخاب فایلها مانند «فقط تصاویر را انتخاب کنید» را تعریف میکند.
بر این اساس، فرمی را که در تصویر ابتدای بحث مشاهده کردید، به صورت زیر تشکیل میدهیم:
@page "/" @using System.IO @using BlazorWasmUpload.Shared @using BlazorWasmUpload.Client.Services @inject IFilesManagerService FilesManagerService <h3>Post a model with files</h3> <EditForm Model="UserModel" OnValidSubmit="CreateUserAsync"> <DataAnnotationsValidator /> <div> <label>Name</label> <InputText @bind-Value="UserModel.Name"></InputText> <ValidationMessage For="()=>UserModel.Name"></ValidationMessage> </div> <div> <label>Age</label> <InputNumber @bind-Value="UserModel.Age"></InputNumber> <ValidationMessage For="()=>UserModel.Age"></ValidationMessage> </div> <div> <label>Photos</label> <InputFile multiple disabled="@isProcessing" OnChange="OnInputFileChange" /> @if (!string.IsNullOrWhiteSpace(UploadErrorMessage)) { <div> @UploadErrorMessage </div> } @if (SelectedFiles?.Count > 0) { <table> <thead> <tr> <th>Name</th> <th>Size (bytes)</th> <th>Last Modified</th> <th>Type</th> </tr> </thead> <tbody> @foreach (var selectedFile in SelectedFiles) { <tr> <td>@selectedFile.Name</td> <td>@selectedFile.Size</td> <td>@selectedFile.LastModified</td> <td>@selectedFile.ContentType</td> </tr> } </tbody> </table> } </div> <div> <button disabled="@isProcessing">Create user</button> </div> </EditForm>
- UserModel که وهلهی از شیء اشتراکی User است، به EditForm متصل شدهاست.
- سپس توسط یک InputText و InputNumber، مقادیر خواص نام و سن کاربر را دریافت میکنیم.
- InputFile دارای ویژگی multiple هم امکان دریافت چندین فایل را توسط کاربر میسر میکند. پس از انتخاب فایلها، رویداد OnChange آن، توسط متد OnInputFileChange مدیریت خواهد شد:
private void OnInputFileChange(InputFileChangeEventArgs args) { var files = args.GetMultipleFiles(maximumFileCount: 15); if (args.FileCount == 0 || files.Count == 0) { UploadErrorMessage = "Please select a file."; return; } var allowedExtensions = new List<string> { ".jpg", ".png", ".jpeg" }; if(!files.Any(file => allowedExtensions.Contains(Path.GetExtension(file.Name), StringComparer.OrdinalIgnoreCase))) { UploadErrorMessage = "Please select .jpg/.jpeg/.png files only."; return; } SelectedFiles = files; UploadErrorMessage = string.Empty; }
- در ادامه اگر فایلی انتخاب نشده باشد، یا فایل انتخابی، تصویری نباشد، با مقدار دهی UploadErrorMessage، خطایی را به کاربر نمایش میدهیم.
- در پایان این متد، لیست فایلهای دریافتی را به فیلد SelectedFiles انتساب میدهیم تا در ذیل InputFile، به صورت یک جدول نمایش داده شوند.
مرحلهی آخر تکمیل این فرم، تدارک متد رویدادگردان OnValidSubmit فرم برنامه است:
private async Task CreateUserAsync() { try { isProcessing = true; await FilesManagerService.PostModelWithFilesAsync( requestUri: "api/Files/CreateUser", browserFiles: SelectedFiles, fileParameterName: "inputFiles", model: UserModel, modelParameterName: "userModel"); UserModel = new User(); } finally { isProcessing = false; SelectedFiles = null; } }
- سپس روش استفادهی از متد PostModelWithFilesAsync سرویس FilesManagerService را مشاهده میکنید که اطلاعات فایلها و مدل برنامه را به سمت اکشن متد api/Files/CreateUser ارسال میکند.
- در آخر با وهله سازی مجدد UserModel، به صورت خودکار فرم برنامه را پاک کرده و آمادهی دریافت اطلاعات بعدی میکنیم.
تکمیل کنترلر Web API دریافت کنندهی مدل برنامه
در ابتدای بحث، ساختار ابتدایی کنترلر Web API دریافت کنندهی اطلاعات FilesManagerService.PostModelWithFilesAsync فوق را معرفی کردیم. در ادامه کدهای کامل آنرا مشاهده میکنید:
using System.IO; using Microsoft.AspNetCore.Mvc; using BlazorWasmUpload.Shared; using Microsoft.AspNetCore.Hosting; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using System.Collections.Generic; using Microsoft.Extensions.Logging; using System.Text.Json; using BlazorWasmUpload.Server.Utils; using System.Linq; namespace BlazorWasmUpload.Server.Controllers { [ApiController] [Route("api/[controller]/[action]")] public class FilesController : ControllerBase { private const int MaxBufferSize = 0x10000; private readonly IWebHostEnvironment _webHostEnvironment; private readonly ILogger<FilesController> _logger; public FilesController( IWebHostEnvironment webHostEnvironment, ILogger<FilesController> logger) { _webHostEnvironment = webHostEnvironment; _logger = logger; } [HttpPost] public async Task<IActionResult> CreateUser( //[FromForm] string userModel, // <-- this is the actual form of the posted model [ModelBinder(BinderType = typeof(JsonModelBinder)), FromForm] User userModel, [FromForm] IList<IFormFile> inputFiles = null) { /*var user = JsonSerializer.Deserialize<User>(userModel); _logger.LogInformation($"userModel.Name: {user.Name}"); _logger.LogInformation($"userModel.Age: {user.Age}");*/ _logger.LogInformation($"userModel.Name: {userModel.Name}"); _logger.LogInformation($"userModel.Age: {userModel.Age}"); var uploadsRootFolder = Path.Combine(_webHostEnvironment.WebRootPath, "Files"); if (!Directory.Exists(uploadsRootFolder)) { Directory.CreateDirectory(uploadsRootFolder); } if (inputFiles?.Any() == true) { foreach (var file in inputFiles) { if (file == null || file.Length == 0) { continue; } var filePath = Path.Combine(uploadsRootFolder, file.FileName); using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, MaxBufferSize, useAsync: true); await file.CopyToAsync(fileStream); _logger.LogInformation($"Saved file: {filePath}"); } } return Ok(); } } }
[ModelBinder(BinderType = typeof(JsonModelBinder)), FromForm] User userModel,
using System; using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.ModelBinding; namespace BlazorWasmUpload.Server.Utils { public class JsonModelBinder : IModelBinder { public Task BindModelAsync(ModelBindingContext bindingContext) { if (bindingContext == null) { throw new ArgumentNullException(nameof(bindingContext)); } var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); if (valueProviderResult != ValueProviderResult.None) { bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult); var valueAsString = valueProviderResult.FirstValue; var result = JsonSerializer.Deserialize(valueAsString, bindingContext.ModelType); if (result != null) { bindingContext.Result = ModelBindingResult.Success(result); return Task.CompletedTask; } } return Task.CompletedTask; } } }
[HttpPost] public async Task<IActionResult> CreateUser( [FromForm] string userModel, // <-- this is the actual form of the posted model [FromForm] IList<IFormFile> inputFiles = null) { var user = JsonSerializer.Deserialize<User>(userModel);
یک نکته تکمیلی: در Blazor 5x، از نمایش درصد پیشرفت آپلود، پشتیبانی نمیشود؛ از این جهت که HttpClient طراحی شده، در اصل به fetch API استاندارد مرورگر ترجمه میشود و این API استاندارد، هنوز از streaming پشتیبانی نمیکند . حتی ممکن است با کمی جستجو به راهحلهایی که سعی کردهاند بر اساس HttpClient و نوشتن بایت به بایت اطلاعات در آن، درصد پیشرفت آپلود را محاسبه کرده باشند، برسید. این راهحلها تنها کاری را که انجام میدهند، بافر کردن اطلاعات، جهت fetch API و سپس ارسال تمام آن است. به همین جهت درصدی که نمایش داده میشود، درصد بافر شدن اطلاعات در خود مرورگر است (پیش از ارسال آن به سرور) و سپس تحویل آن به fetch API جهت ارسال نهایی به سمت سرور.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorWasmUpload.zip
قسمتی از یک پروژه به همراه کلاس SqlHelper آن در کامنتهای مطلب «اهمیت Code review» توسط یکی از خوانندگان بلاگ جهت Code review مطرح شده که بهتر است در یک مطلب جدید و مجزا به آن پرداخته شود. قسمت مهم آن کلاس SqlHelper است و مابقی در اینجا ندید گرفته میشوند:
//It's only for code review purpose!
using System.Data;
using System.Data.SqlClient;
using System.Web.Configuration;
public sealed class SqlHelper
{
private SqlHelper() { }
// Send Connection String
//---------------------------------------------------------------------------------------
public static string GetCntString()
{
return WebConfigurationManager.ConnectionStrings["db_ConnectionString"].ConnectionString;
}
// Connect to Data Base SqlServer
//---------------------------------------------------------------------------------------
public static SqlConnection Connect2Db(ref SqlConnection sqlCnt, string cntString)
{
try
{
if (sqlCnt == null) sqlCnt = new SqlConnection();
sqlCnt.ConnectionString = cntString;
if (sqlCnt.State != ConnectionState.Open) sqlCnt.Open();
return sqlCnt;
}
catch (SqlException)
{
return null;
}
}
// Run ExecuteScalar Command
//---------------------------------------------------------------------------------------
public static string RunExecuteScalarCmd(ref SqlConnection sqlCnt, string strCmd, bool blnClose)
{
Connect2Db(ref sqlCnt, GetCntString());
using (sqlCnt)
{
using(SqlCommand sqlCmd = sqlCnt.CreateCommand())
{
sqlCmd.CommandText = strCmd;
object objResult = sqlCmd.ExecuteScalar();
if (blnClose) CloseCnt(ref sqlCnt, true);
return (objResult == null) ? string.Empty : objResult.ToString();
}
}
}
// Close SqlServer Connection
//---------------------------------------------------------------------------------------
public static bool CloseCnt(ref SqlConnection sqlCnt, bool nullSqlCnt)
{
try
{
if (sqlCnt == null) return true;
if (sqlCnt.State == ConnectionState.Open)
{
sqlCnt.Close();
sqlCnt.Dispose();
}
if (nullSqlCnt) sqlCnt = null;
return true;
}
catch (SqlException)
{
return false;
}
}
}
مثالی از نحوه استفاده ارائه شده:
protected void BtnTest_Click(object sender, EventArgs e)
{
SqlConnection sqlCnt = new SqlConnection();
string strQuery = "SELECT COUNT(UnitPrice) AS PriceCount FROM [Order Details]";
// در این مرحله پارامتر سوم یعنی کانکشن باز نگه داشته شود
string strResult = SqlHelper.RunExecuteScalarCmd(ref sqlCnt, strQuery, false);
strQuery = "SELECT LastName + N'-' + FirstName AS FullName FROM Employees WHERE (EmployeeID = 9)";
// در این مرحله پارامتر سوم یعنی کانکشن بسته شود
strResult = SqlHelper.RunExecuteScalarCmd(ref sqlCnt, strQuery, true);
}
مروری بر این کد:
1) نحوه کامنت نوشتن
بین سی شارپ و زبان سی++ تفاوت وجود دارد. این نحوه کامنت نویسی بیشتر در سی++ متداول است. اگر از ویژوال استودیو استفاده میکنید، مکان نما را به سطر قبل از یک متد منتقل کرده و سه بار پشت سر هم forward slash را تایپ کنید. به صورت خودکار ساختار خالی زیر تشکیل خواهد شد:
/// <summary>
///
/// </summary>
/// <param name="sqlCnt"></param>
/// <param name="cntString"></param>
/// <returns></returns>
public static SqlConnection Connect2Db(ref SqlConnection sqlCnt, string cntString)
این روش مرسوم کامنت نویسی کدهای سی شارپ است. خصوصا اینکه ابزارهایی وجود دارند که به صورت خودکار از این نوع کامنتها، فایل CHM درست میکنند.
2) وجود سازنده private
احتمالا هدف این بوده که نه شخصی و نه حتی کامپایلر، وهلهای از این کلاس را ایجاد نکند. بنابراین بهتر است کلاسی را که تمام متدهای آن static است (که به این هم خواهیم رسید!) ، راسا static معرفی کنید. به این ترتیب نیازی به سازنده private نخواهد بود.
3) وجود try/catch
یک اصل کلی وجود دارد: اگر در حال طراحی یک کتابخانه پایهای هستید، try/catch را در هیچ متدی از آن لحاظ نکنید. بله؛ درست خوندید! لطفا try/catch ننویسید! کرش کردن برنامه خوب است! لایههای بالاتر برنامه که در حال استفاده از کدهای شما هستند متوجه خواهند شد که مشکلی رخ داده و این مشکل توسط کتابخانه مورد استفاده «خفه» نشده. برای مثال اگر هم اکنون SQL Server در دسترس نیست، لایههای بالاتر برنامه باید این مشکل را متوجه شوند. Exception اصلا چیز بدی نیست! کرش برنامه اصلا بد نیست!
فرض کنید که دچار بیماری شدهاید. اگر مثلا تبی رخ ندهد، از کجا باید متوجه شد که نیاز به مراقبت پزشکی وجود دارد؟ اگر هیچ علامتی بروز داده نشود که تا الان نسل بشر منقرض شده بود!
4) وجود ref و out
دوستان گرامی! این ref و out فقط جهت سازگاری با زبان C در سی شارپ وجود دارد. لطفا تا حد ممکن از آن استفاده نکنید! مثلا استفاده از توابع API ویندوز که با C نوشته شدهاند.
یکی از مهمترین کاربردهای pointers در زبان سی، دریافت بیش از یک خروجی از یک تابع است. برای مثال یک متد API ویندوز را فراخوانی میکنید؛ خروجی آن یک ساختار است که به کمک pointers به عنوان یکی از پارامترهای همان متد معرفی شده. این روش به وفور در طراحی ویندوز بکار رفته. ولی خوب در سی شارپ که از این نوع مشکلات وجود ندارد. یک کلاس ساده را طراحی کنید که چندین خاصیت دارد. هر کدام از این خاصیتها میتوانند نمایانگر یک خروجی باشند. خروجی متد را از نوع این کلاس تعریف کنید. یا برای مثال در دات نت 4، امکان دیگری به نام Tuples معرفی شده برای کسانی که سریع میخواهند چند خروجی از یک تابع دریافت کنند و نمیخواهند برای اینکار یک کلاس بنویسند.
ضمن اینکه برای مثال در متد Connect2Db، هم کانکشن یکبار به صورت ref معرفی شده و یکبار به صورت خروجی متد. اصلا نیازی به استفاده از ref در اینجا نبوده. حتی نیازی به خروجی کانکشن هم در این متد وجود نداشته. کلیه تغییرات شما در شیء کانکشنی که به عنوان پارامتر ارسال شده، در خارج از آن متد هم منعکس میشود (شبیه به همان بحث pointers در زبان سی). بنابراین وجود ref غیرضروری است؛ وجود خروجی متد هم به همین صورت.
5) استفاده از using در متد RunExecuteScalarCmd
استفاده از using خیلی خوب است؛ همیشه اینکار را انجام دهید!
اما اگر اینکار را انجام دادید، بدانید که شیء sqlCnt در پایان بدنه using ، توسط GC نابوده شده است. بنابراین اینجا bool blnClose دیگر چه کاربردی دارد؟! تصمیم شما دیگر اهمیتی نخواهد داشت؛ چون کار تخریبی پیشتر انجام شده.
6) متد CloseCnt
این متد زاید است؛ به دلیلی که در قسمت (5) عنوان شد. using های استفاده شده، کار را تمام کردهاند. بنابراین بستن اشیاء dispose شده معنا نخواهد داشت.
7) در مورد نحوه استفاده
اگر SqlHelper را در اینجا مثلا یک DAL ساده فرض کنیم (data access layer)، جای قسمت BLL (business logic layer) در اینجا خالی است. عموما هم چون توضیحات این موارد را خیلی بد ارائه دادهاند، افراد از شنیدن اسم آنها هم وحشت میکنند. BLL یعنی کمی دست به Refactoring بزنید و این پیاده سازی منطق تجاری ارائه شده در متد BtnTest_Click را به یک کلاس مجزا خارج از code behind پروژه منتقل کنید. Code behind فقط محل استفاده نهایی از آن باشد. همین! فعلا با همین مختصر شروع کنید.
مورد دیگری که در اینجا باز هم مشهود است، عدم استفاده از پارامتر در کوئریها است. چون از پارامتر استفاده نکردهاید، SQL Server مجبور است برای حالت EmployeeID = 9 یکبار execution plan را محاسبه کند، برای کوئری بعدی مثلا EmployeeID = 19، اینکار را تکرار کند و الی آخر. این یعنی مصرف حافظه بالا و همچنین سرعت پایین انجام کوئریها. بنابراین اینقدر در قید و بند باز نگه داشتن یک کانکشن نباشید؛ مشکل اصلی جای دیگری است!
8) برنامه وب و اطلاعات استاتیک!
این پروژه، یک پروژه ASP.NET است. دیدن تعاریف استاتیک در این نوع پروژهها یک علامت خطر است! در این مورد قبلا مطلب نوشتم:
متغیرهای استاتیک و برنامههای ASP.NET
یک درخواست عمومی!
لطف کنید در پروژهای «جدید» خودتون این نوع کلاسهای SqlHelper رو «دور بریزید». یاد گرفتن کار با یک ORM جدید اصلا سخت نیست. مثلا طراحی Entity framework مایکروسافت به حدی ساده است که هر شخصی با داشتن بهره هوشی در حد یک عنکبوت آبی یا حتی جلبک دریایی هم میتونه با اون کار کنه! فقط NHibernate هست که کمی مرد افکن است و گرنه مابقی به عمد ساده طراحی شدهاند.
مزایای کار کردن با ORM ها این است:
- کوئریهای حاصل از آنها «پارامتری» است؛ که این دو مزیت عمده را به همراه دارد:
امنیت: مقاومت در برابر SQL Injection
سرعت و همچنین مصرف حافظه کمتر: با کوئریهای پارامتری در SQL Server همانند رویههای ذخیره شده رفتار میشود.
- عدم نیاز به نوشتن DAL شخصی پر از باگ. چون ORM یعنی همان DAL که توسط یک سری حرفهای طراحی شده.
- یک دست شدن کدها در یک تیم. چون همه بر اساس یک اینترفیس مشخص کار خواهند کرد.
- امکان استفاده از امکانات جدید زبانهای دات نتی مانند LINQ و نوشتن کوئریهای strongly typed تحت کنترل کامپایلر.
- پایین آوردن هزینههای آموزشی افراد در یک تیم. مثلا EF را میشود به عنوان یک پیشنیاز در نظر گرفت؛ عمومی است و همه گیر. کسی هم از شنیدن نام آن تعجب نخواهد کرد. کتاب(های) آموزشی هم در مورد آن زیاد هست.
و ...
اگر به تصویر دقت کنید، در ستون Model آن، اطلاعات باینری ذخیره شدهاند. شاید در وهلهی اول اینطور به نظر برسد که این ستون حاوی هش نقل و انتقالات صورت گرفتهاست؛ اما ... خیر. اطلاعات این ستون، GZip شدهی یک رشتهی XML ایی یا همان EDMX معادل مدلها و نگاشتهای برنامه است.
در کدهای ذیل، نمونه مثالی را از نحوهی خواندن این اطلاعات، مشاهده میکنید:
using System; using System.Collections.Generic; using System.Data.SqlClient; using System.IO; using System.IO.Compression; using System.Xml.Linq; namespace EF_General { public static class InsideMigrations { public static void PrintFirstMigrationModel() { const string connectionString = "Data Source=(local);Initial Catalog=TestDbIdentity;Integrated Security = true"; const string sqlToExecute = "select top 1 model from __MigrationHistory"; using (var connection = new SqlConnection(connectionString)) { connection.Open(); using (var command = new SqlCommand(sqlToExecute, connection)) { using (var reader = command.ExecuteReader()) { if (!reader.HasRows) { throw new KeyNotFoundException("Nothing to display."); } while (reader.Read()) { var model = (byte[]) reader["model"]; var decompressed = decompressMigrationModel(model); Console.WriteLine(decompressed); } } } } } private static XDocument decompressMigrationModel(byte[] bytes) { using (var memoryStream = new MemoryStream(bytes)) { using (var gzipStream = new GZipStream(memoryStream, CompressionMode.Decompress)) { return XDocument.Load(gzipStream); } } } } }
بر اساس این اطلاعات، EF کاری به ساختار فعلی بانک اطلاعاتی شما ندارد. زمانیکه Add-Migration را اجرا میکنید، به جدول migrations مراجعه کرده، آخرین رکورد آنرا یافته و سپس اطلاعات آنرا از حالت فشرده خارج و XML نهایی آنرا استخراج میکند. در ادامه اطلاعات این فایل XML را با معادل مدلهای فعلی برنامه مقایسه میکند. اگر این دو یکی نبودند، اسکریپت اعمال تغییرات را تولید خواهد کرد.
مورد دیگری که در این جدول حائز اهمیت است، ستون ContextKey آن است: «رفع مشکل Migration با تغییر NameSpace در EF»