آیا به این نتیجه رسیدید که اصل DRY را نقض کردهایم؟ بله همین طور است. تکرار کلاسهای css مربوط به بوت استرپ، تکرار هلپرهای توکار ASP.NET MVC بارها و بارها، خوانایی کد را پایین میارود و در برخی موارد هم خسته کننده خواهد بود. اگر با مباحث مربوط به EditorTemplateها قبلا آشنا شده باشید، خیلی سریع عنوان خواهید کرد که بهتر است از این امکان بهره برد؛ بله درست است. برای این منظور در مسیر Views/Shared/EditorTemplates، فایل cshtml. همنام با نوع داده مد نظر را ایجاد میکنیم.
String.cshtml
@model string @Html.TextBox("",ViewData.TemplateInfo.FormattedModelValue, new { @class="form-control",placeholder=ViewData.ModelMetadata.Watermark})
Enum.cshtml
@model Enum @Html.EnumDropDownListFor(m => Model, new { @class = "form-control" })
حال دوباره به نتیجه حاصل از تغییرات اعمال شده توجه کنید:
این نتیجه امیدوار کننده است ولی بازهم یکسری از کدها بی دلیل تکرار شدهاند. هلپرهای زیر نیز میتوانند در کاهش کدها به کمک ما برسند :
public static class BootstrapHelpers { public static IHtmlString BootstrapLabelFor<TModel,TProp>( this HtmlHelper<TModel> helper, Expression<Func<TModel,TProp>> property) { return helper.LabelFor(property, new { @class = "col-md-2 control-label" }); } public static IHtmlString BootstrapLabel( this HtmlHelper helper, string propertyName) { return helper.Label(propertyName, new { @class = "col-md-2 control-label" }); } }
از کلاس بالا برای عدم تکرار کلاسهای بوت استرپ مربوط به Label، استفاده میشود .
حال دوباره نتیجه را مشاهده کنید:
خیلی عالی؛ توانستیم از تکرار یکسری از کلاسهای بوت استرپ خلاص شویم. اما در ادامه با استفاده از یک Object Template به عنوان EditorTemplate برای نوع دادههای Complex، کار را تمام خواهیم کرد.
EditorTemplateهای تعریف شده در بالا، صرفا برای نوع دادههای خاصی مورد استفاده قرار خواهند گرفت؛ ولی پیاده سازی یک EditorTemplate جنریک که حتی از ویومدلهای موجود در پروژه نیز پشتیابی کند، به شکل زیر خواهد بود.
Object.cshtml
@model dynamic @foreach (var prop in ViewData.ModelMetadata.Properties .Where(p => p.ShowForEdit)) { if (prop.TemplateHint == "HiddenInput") { @Html.Hidden(prop.PropertyName) } else { <div class="form-group"> @Html.BootstrapLabel(prop.PropertyName) <div class="col-md-10"> @Html.Editor(prop.PropertyName) @Html.ValidationMessage(prop.PropertyName) </div> </div> } }
با استفاده از ViewData.ModelMetadata میتوان به خصوصیات مدل مربوط به ویو دسترسی پیدا کرد که در بالا با استفاده از همین خصوصیت به تمام پراپرتیهای مدل دسترسی پیدا کرده و مقداری کد تکراری باقی مانده را هم در اینجا کپسوله کردیم.
حال کافی است به شکل زیر عمل کنیم:
میخواهیم از یک لیست در گزارش خود استفاده کنیم؛ بطور مثال وقتی در LINQ از دستور ToList استفاده میکنیم و میخواهیم آنرا بصورت مستقیم به Stimul بفرستیم. فرض بر این است که شما DLLهای Stimul را به پروژه اضافه کرده اید و آماده گزارشگیری هستید.
مثلا مدلی در Entity FrameWork با نام base_CenterType
public class base_CenterType { public int ID { get; set; } public string Title { get; set; } public string Dsc { get; set; } }
و متدی بصورت ذیل:
public IList<base_CenterType> GetAll() { return _base_CenterType.ToList(); }
طراحی گزارش برای این لیست به این صورت است:
1- اضافه کردن StiWebReport به فرم به نام StiWebReport1
2- با کلیک بر روی فلش سمت راست و بالای StiWebReport1 و انتخاب Design Report، وارد قسمت طراحی میشویم:
3- با راست کلیک بر روی Business Object و انتخاب New Business Object پنجره مربوطه باز میشود:
4- بعد از زدن OK پنجره زیر باز خواهد شد که باید در کادر Name نام Business Object را انتخاب کنیم که برای خوانایی بهتر است همان نام کلاس را برای آن انتخاب کنیم. چون Category نداریم پس باید کادر آن خالی بماند.
در قسمت Columns باید ستونهای هم نام و هم نوع با خواص کلاس base_CenterType را ایجاد کنیم.
و نهایتا Business Objectی به نام base_CenterType با سه ستون ایجاد خواهد شد.
حال میتوانید ستونهای مورد نظر را در گزارش بکار ببرید.
با فرض اینکه گزارش را طراحی کرده و آنرا در ریشه درایو C ذخیره کردهاید، از قطعه کد زیر برای ارسال لیست به گزارش و نمایش آن استفاده میکنیم.
StiReport mainreport = new StiReport(); mainreport.RegBusinessObject("base_CenterType", base_CenterTypeService.GetAll()); mainreport.Load("C:\\StiWebReport2.mrt"); mainreport.Show();
روش تعیین نوع useState Hook
برای این منظور در ابتدا فایل جدید src\components\Input.tsx را ایجاد کرده و به صورت زیر تکمیل میکنیم:
import React, { useState } from "react"; export const Input = () => { const [name, setName] = useState(""); return <input value={name} onChange={(e) => setName(e.target.value)} />; };
پس از این تعاریف ... برنامه بدون مشکل کار میکند و کامپایل میشود. اکنون سؤال اینجا است که آیا تایپاسکریپت در ایجا اصلا کاری را هم انجام میدهد؟ برای درک این موضوع، سطر useState را به صورت زیر تغییر میدهیم:
const [name, setName] = useState(0);
عنوان میکند که مقدار رشتهای e.target.value، به مقدار عددی name قابل انتساب نیست. به عبارتی TypeScript از قابلیت Type Inference خود در اینجا استفاده میکند. درست است که به ظاهر نوعی را برای useState و خروجی منتسب به آن تعیین نکردهایم، اما بر اساس نوع مقدار پیشفرض آن، نوع name و setName به صورت خودکار مشخص میشوند و نیازی به ذکر صریح این نوع، نیست. برای مثال در حالت اول چون مقدار پیشفرض useState را یک رشتهی خالی معرفی کرده بودیم، نوع name نیز string درنظر گرفته شده بود. در حالت دوم بر اساس مقدار پیشفرض عددی useState، اینبار نوع name نیز یک number خواهد بود و دیگر نمیتوان یک مقدار رشتهای را مانند e.target.value به آن انتساب داد. مزیت کار کردن با TypeScript در اینجا، مشاهدهی آنی خطای یک چنین استفادهها و انتسابهای نادرستی است.
مفهوم Type Inference را در تصاویر زیر بهتر میتوان مشاهده کرد. اشارهگر ماوس را به تعریف useState نزدیک کنید. در توضیحاتی که ظاهر میشود، بر اساس نوع مقدار پیشفرض آن، نوع آرگومان جنریک متد useState نیز به صورت خودکار تغییر میکند:
و نکتهی مهم اینجا است که نیازی به ذکر صریح این نوع جنریک، مانند مثال زیر نیست:
const [name, setName] = useState<string>("");
const [name, setName] = useState("");
سؤال: اگر مقدار اولیهی useState را null تعیین کردیم و یا اصلا تعریف نکردیم و undefined بود، چطور؟
در یک چنین حالتی که نوع دقیق state، از طریق مقدار اولیهی آن قابل استنتاج نیست، نیاز هست همانند تصاویر فوق، تعریف جنریک useState را به نحو صریحی ذکر کرده و آنرا با union types تکمیل کنیم:
const [name, setName] = useState<string | null>(null);
روش تعیین نوع useRef Hook
در ادامه میخواهیم نحوهی تعیین نوع DOM Elements را در React بررسی کنیم. با استفاده از useRef میتوان به ارجاعی از یک DOM Element دسترسی یافت.
import React, { useRef, useState } from "react"; export const Input = () => { const [name, setName] = useState(""); const inputRef = useRef(null); if (inputRef && inputRef.current) { console.log("ref", inputRef.current.value); } return ( <input ref={inputRef} value={name} onChange={(e) => setName(e.target.value)} /> ); };
عنوان میکند نوعی که inputRef.current دارد، نال است و نال به همراه خاصیت value نیست. برای اینکه نوع inputRef را بهتر بتوانیم بررسی کنیم، دقیقا بر روی آن کلیک راست کرده و گزینهی Go to Type Definition را انتخاب کنید. بلافاصله به تعریف زیر هدایت خواهیم شد:
interface MutableRefObject<T> { current: T; }
برای رفع این مشکل فقط کافی است نوع المان مدنظر را صریحا به عنوان آرگومان جنریک useRef معرفی کنیم:
const inputRef = useRef<HTMLInputElement>(null);
فعال بودن Strict Null Checking در پروژههای TypeScript ای React
نکات مطلب «نوعهای نال نپذیر در TypeScript» به صورت پیشفرض در پروژههای تایپ اسکریپتی React هم فعال هستند؛ چون پرچم strict واقع در فایل tsconfig.json پروژه، به صورت پیشفرض به true تنظیم شدهاست و این پرچم، Strict Null Checking را نیز شامل میشود. برای آزمایش فعال بودن آن، کدهای فوق را به صورت زیر تغییر دهید تا صرفا if آن حذف شود:
//if (inputRef && inputRef.current) { console.log("ref", inputRef.current.value); //}
که برای رفع آن، همانند قبل باید ذکر if بررسی نال بودن inputRef و خاصیت current آنرا اضافه کرد؛ تا دیگر در زمان اجرای برنامه، با شانس نال بودن یکی از ایندو مواجه نشویم و به کیفیت بالاتری در برنامهی خود برسیم.
روش بررسی if (inputRef && inputRef.current) معادل سادهتری را نیز در TypeScript 3.7 به بعد دارد که به Optional Chaining معروف است؛ به صورت زیر:
console.log("ref", inputRef?.current?.value);
در این بین راه حل دیگری نیز وجود دارد که با تمام مرورگرها سازگار است؛ اما تنها گزارش درصد آپلود را توسط آن نخواهیم داشت. در اینجا به صورت پویا یک IFrame مخفی در صفحه تشکیل میشود، مقادیر معمولی فرم (تمام المانها، منهای file) به صورت Ajax ایی به سرور ارسال خواهند شد. المان file آن در این IFrame مخفی، به صورت معمولی به سرور Postback میشود. البته کاربر در این بین چیزی را مشاهده یا احساس نخواهد کرد و تمام عملیات از دیدگاه او Ajax ایی به نظر میرسد. برای انجام اینکار تنها کافی است از افزونهی AjaxFileUpload استفاده کنیم که در ادامه نحوهی استفاده از آنرا بررسی خواهیم کرد.
پیشنیازها
در ادامه فرض بر این است که افزونهی AjaxFileUpload را دریافت کرده و به فایل Layout برنامه افزودهاید:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>@ViewBag.Title - My ASP.NET Application</title> <link href="~/Content/Site.css" rel="stylesheet" type="text/css" /> </head> <body> <div> @RenderBody() </div> <script src="~/Scripts/jquery-1.11.1.min.js"></script> <script src="~/Scripts/jquery.unobtrusive-ajax.js"></script> <script src="~/Scripts/ajaxfileupload.js"></script> @RenderSection("Scripts", required: false) </body> </html>
مدل، کنترلر و View برنامه
مدل برنامه مشخصات یک محصول است:
namespace MVCAjaxFormUpload.Models { public class Product { public int Id { set; get; } public string Name { set; get; } } }
کنترلر آن از سه متد تشکیل شدهاست:
using System.Threading; using System.Web; using System.Web.Mvc; using MVCAjaxFormUpload.Models; namespace MVCAjaxFormUpload.Controllers { public class HomeController : Controller { public ActionResult Index() { return View(); } [HttpPost] public ActionResult Index(Product product) { var isAjax = this.Request.IsAjaxRequest(); return Json(new { result = "ok" }, JsonRequestBehavior.AllowGet); } [HttpPost] public ActionResult UploadFiles(HttpPostedFileBase image1, int id) { var isAjax = this.Request.IsAjaxRequest(); Thread.Sleep(3000); //شبیه سازی عملیات طولانی return Json(new { FileName = "/Uploads/filename.ext" }, "text/html", JsonRequestBehavior.AllowGet); } } }
Index دوم کار پردازش Ajax ایی اطلاعات ارسالی به سرور را به عهده دارد. HttpPost آن Ajax ایی است.
متد UploadFiles، کار پردازش اطلاعات ارسالی از طرف IFrame مخفی را انجام میدهد. HttpPost آن معمولی است.
و کدهای View این مثال نیز به شرح زیر است:
@model MVCAjaxFormUpload.Models.Product @{ ViewBag.Title = "Index"; } <h2>Ajax Form Upload</h2> @using (Ajax.BeginForm(actionName: "Index", controllerName: "Home", ajaxOptions: new AjaxOptions { HttpMethod = "POST" }, routeValues: null, htmlAttributes: new { id = "uploadForm" })) { <label>Name:</label> @Html.TextBoxFor(model => model.Name) <br /> <label>Image:</label> <br /> <input type="file" name="Image1" id="Image1" /> <br /> <input type="submit" value="Submit" /> <img id="loading" src="~/Content/Images/loading.gif" style="display:none;"> } @section Scripts { <script type="text/javascript"> $(function () { $('#uploadForm').submit(function () { $("#loading").show(); $.ajaxFileUpload({ url: "@Url.Action("UploadFiles", "Home")", // مسیری که باید فایل به آن ارسال شود secureuri: false, fileElementId: 'Image1', // آی دی المان ورودی فایل dataType: 'json', data: { id: 1, data: 'test' }, // اطلاعات اضافی در صورت نیاز success: function (data, status) { $("#loading").hide(); if (typeof (data.FileName) != 'undefined') { alert(data.FileName); } }, error: function (data, status, e) { $("#loading").hide(); alert(e); } }); }); }); </script> }
در ادامه نحوهی فعال سازی ajaxFileUpload را دقیقا در زمان submit فرم، مشاهده میکنید. در اینجا url آن به اکشن متدی که اطلاعات المان file را باید دریافت کند، اشاره میکند. fileElementId آن مساوی Id المان فایل فرم Ajax ایی صفحهاست. از قسمت data جهت ارسال اطلاعات اضافهتری به اکشن متد UploadFiles استفاده میشود. سایر قسمتهای آن نیز مشخص هستند. اگر عملیات موفقیت آمیز بود، success آن و اگر خیر، error آن اجرا میشوند.
فقط باید دقت داشت که content type دریافتی توسط آن باید text/html باشد، که این مورد در اکشن متدهای کنترلر مشخص هستند.
به این ترتیب دیگر کاربر نیازی ندارد ابتدا یکبار بر روی دکمهی دومی کلیک کرده و فایل را ارسال کند و سپس بار دیگر بر روی دکمهی submit فرم کلیک نماید. هر دو کار توسط یک دکمه انجام میشوند.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید
MVCAjaxFormUpload.zip
یک نکتهی تکمیلی: روش نمایش خودکار آرگومانهای نامدار در Rider
اگر از Rider استفاده میکنید و علاقمندید تا خودش کار تکمیل و نمایش آرگومانهای نامدار را انجام دهد، روش کار به صورت زیر است:
الف) ویژگی فرمت کردن کدها را در حالت ذخیره سازی تغییرات، فعال کنید:
با اینکار، هربار که تغییرات را ذخیره میکنید، تنظیمات کدنویسی، به صورت خودکار به فایلهای ذخیره نشده، اعمال میشوند.
ب) به قسمت Settings -> Editor -> Cody Style -> C# -> Syntax Style مراجعه کرده و در قسمت تنظیمات آرگومانها، حداقل گزینههای Literal values و String literal values را بر روی named argumets قرار دهید تا نکات مطلب جاری، به صورت خودکار اعمال شوند:
همانطور که در مثال before/after تصویر فوق هم مشخص است، مزیت اینکار، مفهوم پیدا کردن اعداد و رشتههای وارد شده به عنوان آرگومانهای متدها هستند.
- پس از خداحافظی با شرکتی که در آن کار میکردی، شخصی با پوزخند به شما میگوید که «میدونستی در برنامهی حق و دستمزد شما، بچههای ادمین شبکه، دیتابیس برنامه رو مستقیما دستکاری میکردند و تعداد ساعات کاری بیشتری رو وارد میکردند»؟!
- مسئول فروشی/مسئول پذیرشی که یاد گرفته چطور به صورت مستقیم به بانک اطلاعاتی دسترسی پیدا کند و آمار فروش/پذیرش روز خودش را در بانک اطلاعاتی، با دستکاری مستقیم و خارج از برنامه، کمتر از مقدار واقعی نمایش دهد.
- باز هم مدیر سیستمی/شبکهای که دسترسی مستقیم به بانک اطلاعاتی دارد، در ساعاتی مشخص، کلمهی عبور هش شدهی خودش را مستقیما، بجای کلمهی عبور ادمین برنامه در بانک اطلاعاتی وارد کرده و پس از آن ...
این موارد متاسفانه واقعی هستند! اکنون سؤال اینجا است که آیا برنامهی شما قادر است تشخیص دهد رکوردهایی که هم اکنون در بانک اطلاعاتی ثبت شدهاند، واقعا توسط برنامه و تمام سطوح دسترسی که برای آن طراحی کردهاید، به این شکل درآمدهاند، یا اینکه توسط اشخاصی به صورت مستقیم و با دور زدن کامل برنامه، از طریق management studioهای مختلف، در سیستم وارد و دستکاری شدهاند؟! در ادامه راه حلی را برای بررسی این مشکل مهم، مرور خواهیم کرد.
چگونه تغییرات رکوردها را در بانکهای اطلاعاتی ردیابی کنیم؟
روش متداولی که برای بررسی تغییرات رکوردها مورد استفاده قرار میگیرد، هش کردن تمام اطلاعات یک ردیف از جدول است و سپس مقایسهی این هشها با هم. علت استفادهی از الگوریتمهای هش نیز، حداقل به دو علت است:
- با تغییر حتی یک بیت از اطلاعات، مقدار هش تولید شده تغییر میکند.
- طول نهایی مقدار هش شدهی اطلاعاتی حجیم، بسیار کم است و به راحتی توسط بانکهای اطلاعاتی، قابل مدیریت و جستجو است.
اگر از SQL Server استفاده میکنید، یک چنین قابلیتی را به صورت توکار به همراه دارد:
SELECT [Id], (SELECT top 1 * FROM [AppUsers] FOR XML auto), HASHBYTES ('SHA2_256', (SELECT top 1 * FROM [AppUsers] FOR XML auto)) AS [hash] -- varbinary(n), since 2012 FROM [AppUsers]
کاری که این کوئری انجام میدهد شامل دو مرحله است:
الف) کوئری "SELECT top 1 * FROM [AppUsers] FOR XML auto" کاری شبیه به serialization را انجام میدهد. همانطور که مشاهده میکنید، نام و مقادیر تمام فیلدهای یک ردیف را به صورت یک خروجی XML در میآورد. بنابراین دیگر نیازی نیست تا کار تبدیل مقادیر تمام ستونهای یک ردیف را به عبارتی قابل هش، به صورت دستی انجام دهیم؛ رشتهی XML ای آن هم اکنون آمادهاست.
ب) متد HASHBYTES، این خروجی serialized را با الگوریتم SHA2_256، هش میکند. الگوریتمهای SHA2_256 و همچنین SHA2_512، از سال 2012 به بعد به SQL Server اضافه شدهاند.
اکنون اگر این هش را به نحوی ذخیره کنیم (برنامه باید این هش را ذخیره و یا به روز رسانی کند) و سپس شخصی به صورت مستقیم ردیف فوق را در بانک اطلاعاتی تغییر دهد، هش جدید این ردیف، با هش قبلی ذخیره شدهی توسط برنامه، یکی نخواهد بود که بیانگر دستکاری مستقیم این ردیف، خارج از برنامه و با دور زدن کامل تمام سطوح دسترسی آن است.
چگونه تغییرات رکوردها را در بانکهای اطلاعاتی، توسط EF Core ردیابی کنیم؟
مزیت روش فوق، توکار بودن آن است که کارآیی فوق العادهای را نیز به همراه دارد. اما چون در ادامه قصد داریم از یک ORM استفاده کنیم و ORMها نیز قرار است توانایی کار کردن با انواع و اقسام بانکهای اطلاعاتی را داشته باشند، دو مرحلهی serialization و هش کردن را در کدهای برنامه و با مدیریت EF Core، مستقل از بانک اطلاعاتی خاصی، انجام خواهیم داد.
معرفی موجودیتهای برنامه
در مثالی که بررسی خواهیم کرد، دو موجودیت Blog و Post تعریف شدهاند:
using System.Collections.Generic; namespace EFCoreRowIntegrity { public interface IAuditableEntity { string Hash { set; get; } } public static class AuditableShadowProperties { public static readonly string CreatedDateTime = nameof(CreatedDateTime); public static readonly string ModifiedDateTime = nameof(ModifiedDateTime); } public class Blog : IAuditableEntity { public int BlogId { get; set; } public string Url { get; set; } public List<Post> Posts { get; set; } public string Hash { get; set; } } public class Post : IAuditableEntity { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } public int BlogId { get; set; } public Blog Blog { get; set; } public string Hash { get; set; } } }
- به علاوه جهت تکمیل بحث، دو خاصیت سایهای نیز تعریف شدهاند تا بررسی کنیم که آیا هش اینها نیز درست محاسبه میشود یا خیر.
- علت اینکه خاصیت Hash، سایهای تعریف نشد، سهولت دسترسی و بالا بردن کارآیی آن بود.
معرفی ظرفی برای نگهداری نام خواص و مقادیر متناظر با یک موجودیت
در ادامه دو کلاس AuditEntry و AuditProperty را مشاهده میکنید:
using System.Collections.Generic; using Microsoft.EntityFrameworkCore.ChangeTracking; namespace EFCoreRowIntegrity { public class AuditEntry { public EntityEntry EntityEntry { set; get; } public IList<AuditProperty> AuditProperties { set; get; } = new List<AuditProperty>(); public AuditEntry() { } public AuditEntry(EntityEntry entry) { EntityEntry = entry; } } public class AuditProperty { public string Name { set; get; } public object Value { set; get; } public bool IsTemporary { set; get; } public PropertyEntry PropertyEntry { set; get; } public AuditProperty() { } public AuditProperty(string name, object value, bool isTemporary, PropertyEntry property) { Name = name; Value = value; IsTemporary = isTemporary; PropertyEntry = property; } } }
معرفی روشی برای هش کردن مقادیر یک شیء
زمانیکه توسط سیستم Tracking، در حال کاربر بر روی موجودیتهای اضافه شده و یا ویرایش شده هستیم، میخواهیم فیلد هش آنها را نیز به صورت خودکار ویرایش و مقدار دهی کنیم. کلاس زیر، منطق ارائه دهندهی این مقدار هش را بیان میکند:
using System; using System.Collections.Generic; using System.Security.Cryptography; using System.Text; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using Newtonsoft.Json; namespace EFCoreRowIntegrity { public static class HashingExtensions { public static string GenerateObjectHash(this object @object) { if (@object == null) { return string.Empty; } var jsonData = JsonConvert.SerializeObject(@object, Formatting.Indented); using (var hashAlgorithm = new SHA256CryptoServiceProvider()) { var byteValue = Encoding.UTF8.GetBytes(jsonData); var byteHash = hashAlgorithm.ComputeHash(byteValue); return Convert.ToBase64String(byteHash); } } public static string GenerateEntityEntryHash(this EntityEntry entry, string propertyToIgnore) { var auditEntry = new Dictionary<string, object>(); foreach (var property in entry.Properties) { var propertyName = property.Metadata.Name; if (propertyName == propertyToIgnore) { continue; } auditEntry[propertyName] = property.CurrentValue; } return auditEntry.GenerateObjectHash(); } public static string GenerateEntityHash<TEntity>(this DbContext context, TEntity entity, string propertyToIgnore) { return context.Entry(entity).GenerateEntityEntryHash(propertyToIgnore); } } }
- نکتهی مهم: ما نمیخواهیم تمام خواص یک موجودیت را هش کنیم. برای مثال اگر موجودیتی دارای چندین رابطه با جداول دیگری بود، ما مقادیر اینها را هش نمیکنیم (چون رکوردهای متناظر با آنها در جداول خودشان میتوانند دارای فیلد هش مخصوصی باشند). بنابراین یک Dictionary را از خواص و مقادیر متناظر با آنها تشکیل داده و این Dictionary را تبدیل به JSON میکنیم.
- همچنین در این بین، مقدار خود فیلد Hash یک شیء نیز نباید در هش محاسبه شده، حضور داشته باشد. به همین جهت پارامتر propertyToIgnore را مشاهده میکنید.
معرفی Context برنامه که کار هش کردن خودکار موجودیتها را انجام میدهد
اکنون نوبت استفاده از تنظیمات انجام شدهی تا این مرحلهاست:
using System; using System.Collections.Generic; using System.IO; using System.Linq; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.Extensions.Logging; namespace EFCoreRowIntegrity { public class BloggingContext : DbContext { public BloggingContext() { } public BloggingContext(DbContextOptions options) : base(options) { } public DbSet<Blog> Blogs { get; set; } public DbSet<Post> Posts { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (!optionsBuilder.IsConfigured) { optionsBuilder.EnableSensitiveDataLogging(); var path = Path.Combine(Directory.GetCurrentDirectory(), "app_data", "EFCore.RowIntegrity.mdf"); optionsBuilder.UseSqlServer($"Server=(localdb)\\mssqllocaldb;Database=EFCore.RowIntegrity;AttachDbFilename={path};Trusted_Connection=True;"); optionsBuilder.UseLoggerFactory(new LoggerFactory().AddConsole((message, logLevel) => logLevel == LogLevel.Debug && message.StartsWith("Microsoft.EntityFrameworkCore.Database.Command"))); } } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); foreach (var entityType in modelBuilder.Model .GetEntityTypes() .Where(e => typeof(IAuditableEntity) .IsAssignableFrom(e.ClrType))) { modelBuilder.Entity(entityType.ClrType) .Property<DateTimeOffset?>(AuditableShadowProperties.CreatedDateTime); modelBuilder.Entity(entityType.ClrType) .Property<DateTimeOffset?>(AuditableShadowProperties.ModifiedDateTime); } } public override int SaveChanges() { var auditEntries = OnBeforeSaveChanges(); var result = base.SaveChanges(); OnAfterSaveChanges(auditEntries); return result; } private IList<AuditEntry> OnBeforeSaveChanges() { var auditEntries = new List<AuditEntry>(); foreach (var entry in ChangeTracker.Entries<IAuditableEntity>()) { if (entry.State == EntityState.Detached || entry.State == EntityState.Unchanged) { continue; } var auditEntry = new AuditEntry(entry); auditEntries.Add(auditEntry); var now = DateTimeOffset.UtcNow; foreach (var property in entry.Properties) { var propertyName = property.Metadata.Name; if (propertyName == nameof(IAuditableEntity.Hash)) { continue; } if (property.IsTemporary) { // It's an auto-generated value and should be retrieved from the DB after calling the base.SaveChanges(). auditEntry.AuditProperties.Add(new AuditProperty(propertyName, null, true, property)); continue; } switch (entry.State) { case EntityState.Added: entry.Property(AuditableShadowProperties.CreatedDateTime).CurrentValue = now; auditEntry.AuditProperties.Add(new AuditProperty(propertyName, property.CurrentValue, false, property)); break; case EntityState.Modified: auditEntry.AuditProperties.Add(new AuditProperty(propertyName, property.CurrentValue, false, property)); entry.Property(AuditableShadowProperties.ModifiedDateTime).CurrentValue = now; break; } } } return auditEntries; } private void OnAfterSaveChanges(IList<AuditEntry> auditEntries) { foreach (var auditEntry in auditEntries) { foreach (var auditProperty in auditEntry.AuditProperties.Where(x => x.IsTemporary)) { // Now we have the auto-generated value from the DB. auditProperty.Value = auditProperty.PropertyEntry.CurrentValue; auditProperty.IsTemporary = false; } auditEntry.EntityEntry.Property(nameof(IAuditableEntity.Hash)).CurrentValue = auditEntry.AuditProperties.ToDictionary(x => x.Name, x => x.Value).GenerateObjectHash(); } base.SaveChanges(); } } }
public override int SaveChanges() { var auditEntries = OnBeforeSaveChanges(); var result = base.SaveChanges(); OnAfterSaveChanges(auditEntries); return result; }
if (property.IsTemporary) { // It's an auto-generated value and should be retrieved from the DB after calling the base.SaveChanges(). auditEntry.AuditProperties.Add(new AuditProperty(propertyName, null, true, property)); continue; }
همین مقدار تنظیم، برای محاسبه و به روز رسانی خودکار فیلد هش، کفایت میکند.
روش بررسی اصالت یک موجودیت
در متد زیر، روش محاسبهی هش واقعی یک موجودیت دریافت شدهی از بانک اطلاعاتی را توسط متد الحاقی GenerateEntityHash مشاهده میکنید. اگر این هش واقعی (بر اساس مقادیر فعلی این ردیف که حتی ممکن است به صورت دستی و خارج از برنامه تغییر کرده باشد)، با مقدار Hash ثبت شدهی پیشین در آن ردیف یکی بود، اصالت این ردیف تائید خواهد شد:
private static void CheckRow1IsAuthentic() { using (var context = new BloggingContext()) { var blog1 = context.Blogs.Single(x => x.BlogId == 1); var entityHash = context.GenerateEntityHash(blog1, propertyToIgnore: nameof(IAuditableEntity.Hash)); var dbRowHash = blog1.Hash; Console.WriteLine($"entityHash: {entityHash}\ndbRowHash: {dbRowHash}"); if (entityHash == dbRowHash) { Console.WriteLine("This row is authentic!"); } else { Console.WriteLine("This row is tampered outside of the application!"); } } }
entityHash: P110cYquWpoaZuTpCWaqBn6HPSGdoQdmaAN05s1zYqo= dbRowHash: P110cYquWpoaZuTpCWaqBn6HPSGdoQdmaAN05s1zYqo= This row is authentic!
اکنون بانک اطلاعاتی را خارج از برنامه، مستقیما دستکاری میکنیم و برای مثال Url اولین ردیف را تغییر میدهیم:
در ادامه یکبار دیگر برنامه را اجرا خواهیم کرد:
entityHash: tdiZhKMJRnROGLLam1WpldA0fy/CbjJaR2Y2jNU9izk= dbRowHash: P110cYquWpoaZuTpCWaqBn6HPSGdoQdmaAN05s1zYqo= This row is tampered outside of the application!
به علاوه باید درنظر داشت، محاسبهی این هش بدون خود برنامه، کار سادهای نیست. به همین جهت به روز رسانی دستی آن تقریبا غیرممکن است؛ خصوصا اگر متد GenerateObjectHash، کمی با پیچ و تاب بیشتری نیز تهیه شود.
چگونه وضعیت اصالت تعدادی ردیف را بررسی کنیم؟
مثال قبل، در مورد روش بررسی اصالت یک تک ردیف بود. کوئری زیر روش محاسبهی فیلد جدید IsAuthentic را در بین لیستی از ردیفها نمایش میدهد:
var blogs = (from blog in context.Blogs.ToList() // Note: this `ToList()` is necessary here for having Shadow properties values, otherwise they will considered `null`. let computedHash = context.GenerateEntityHash(blog, nameof(IAuditableEntity.Hash)) select new { blog.BlogId, blog.Url, RowHash = blog.Hash, ComputedHash = computedHash, IsAuthentic = blog.Hash == computedHash }).ToList();
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید: EFCoreRowIntegrity.zip
درون دیتابیس، یک Table دارم که درون این Table نام تمام موجودیتهای سیستم خودم رو نگه میدارم و در یک Table دیگر تمام فیلدهای موجودیتها را همراه با نوع داده آنها ذخیره میکنم
برای یک سری شرایط خاص میخواهم کار زیر را انجام دهم:
یک فرم طراحی کردم که برای تمام موجودیتهای تعریف شده درون جدول Entities کاربرد داره ، میخواهم زمانی که این فرم اجرا شده با توجه به اینکه این فرم برای کدام موجودیت فراخوانی شده است یک کلاس برای آن موجودیت ایجاد کنم و پس از آن یک لیست از کلاسی که ایجاد شده ، ایجاد بکنم و درون آن لیست مقادیری را قرار دهم (مقادیر را از دیتابیس خوانده میشود) و در آخر مقادیر لیست را در یک کنترل مثل gridview نمایش دهم
حال من برای انجام این کار به چند مشکل برخوردم . کدی که نوشتم به صورت زیر است
var ctx = new Entities(); var Fields = ctx.ENTITIES_FEILDS.ToList(); var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly( name: new AssemblyName("Demo"), access: AssemblyBuilderAccess.Run); var moduleBuilder = assemblyBuilder.DefineDynamicModule(name: "Module"); var typeBuilder = moduleBuilder.DefineType(name: Fields.First(c=>c.FEILD_ID==1).ENTITIES.ENTITY_NAME, attr: TypeAttributes.Public); foreach (var item in Fields) { }
میانافزار چندسکویی فشرده سازی صفحات در ASP.NET Core
پیشتر مطلب «استفاده از GZip توکار IISهای جدید و تنظیمات مرتبط با آنها» را در سایت جاری مطالعه کردهاید. این قابلیت صرفا وابستهاست به IIS و همچنین در صورت نصب بودن ماژول httpCompression آن کار میکند. بنابراین قابلیت انتقال به سایر سیستم عاملها را نخواهد داشت و هرچند تنظیمات فایل web.config آن هنوز هم در برنامههای ASP.NET Core معتبر هستند، اما چندسکویی نیستند. برای رفع این مشکل، تیم ASP.NET Core، میانافزار توکاری را برای فشرده سازی صفحات ارائه دادهاست که جزئی از تازههای ASP.NET Core 1.1 نیز بهشمار میرود.
برای نصب آن دستور ذیل را در کنسول پاورشل نیوگت، اجرا کنید:
PM> Install-Package Microsoft.AspNetCore.ResponseCompression
{ "dependencies": { "Microsoft.AspNetCore.ResponseCompression": "1.0.0" } }
مرحلهی بعد، افزودن سرویسهای و میان افزار مرتبط، به کلاس آغازین برنامه هستند. همیشه متدهای Add کار ثبت سرویسهای میانافزار را انجام میدهند و متدهای Use کار افزودن خود میانافزار را به مجموعهی موجود تکمیل میکنند.
public void ConfigureServices(IServiceCollection services) { services.AddResponseCompression(options => { options.MimeTypes = Microsoft.AspNetCore.ResponseCompression.ResponseCompressionDefaults.MimeTypes; }); }
namespace Microsoft.AspNetCore.ResponseCompression { /// <summary> /// Defaults for the ResponseCompressionMiddleware /// </summary> public class ResponseCompressionDefaults { /// <summary> /// Default MIME types to compress responses for. /// </summary> // This list is not intended to be exhaustive, it's a baseline for the 90% case. public static readonly IEnumerable<string> MimeTypes = new[] { // General "text/plain", // Static files "text/css", "application/javascript", // MVC "text/html", "application/xml", "text/xml", "application/json", "text/json", }; } }
services.AddResponseCompression(options => { options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[] { "image/svg+xml", "application/font-woff2" }); });
به علاوه options ذکر شدهی در اینجا دارای خاصیت options.Providers نیز میباشد که نوع و الگوریتم فشرده سازی را مشخص میکند. در صورتیکه مقدار دهی نشود، مقدار پیش فرض آن Gzip خواهد بود:
services.AddResponseCompression(options => { //If no compression providers are specified then GZip is used by default. //options.Providers.Add<GzipCompressionProvider>();
همچنین اگر علاقمند بودید تا میزان فشرده سازی تامین کنندهی Gzip را تغییر دهید، نحوهی تنظیمات آن به صورت ذیل است:
services.Configure<GzipCompressionProviderOptions>(options => { options.Level = System.IO.Compression.CompressionLevel.Optimal; });
به صورت پیشفرض، فشرده سازی صفحات Https انجام نمیشود. برای فعال سازی آن تنظیم ذیل را نیز باید قید کرد:
options.EnableForHttps = true;
مرحلهی آخر این تنظیمات، افزودن میان افزار فشرده سازی خروجی به لیست میان افزارهای موجود است:
public void Configure(IApplicationBuilder app) { app.UseResponseCompression() // Adds the response compression to the request pipeline .UseStaticFiles(); // Adds the static middleware to the request pipeline }
تنظیمات کش کردن چندسکویی فایلهای ایستا در ASP.NET Core
تنظیمات کش کردن فایلهای ایستا در web.config مخصوص IIS به صورت ذیل است :
<staticContent> <clientCache httpExpires="Sun, 29 Mar 2020 00:00:00 GMT" cacheControlMode="UseExpires" /> </staticContent>
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { app.UseResponseCompression() .UseStaticFiles( new StaticFileOptions { OnPrepareResponse = _ => _.Context.Response.Headers[HeaderNames.CacheControl] = "public,max-age=604800" // A week in seconds }) .UseMvc(routes => routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}")); }
معادل چندسکویی ماژول URL Rewrite در ASP.NET Core
مثالهایی از ماژول URL Rewrite را در مباحث بهینه سازی سایت برای بهبود SEO پیشتر بررسی کردهایم (^ و ^ و ^). این ماژول نیز همچنان در ASP.NET Core هاست شدهی در ویندوز و IIS قابل استفاده است (البته به شرطی که ماژول مخصوص آن در IIS نصب و فعال شده باشد). معادل چندسکویی این ماژول به صورت یک میانافزار توکار به ASP.NET Core 1.1 اضافه شدهاست.
برای استفادهی از آن، ابتدا نیاز است بستهی نیوگت آنرا به نحو ذیل نصب کرد:
PM> Install-Package Microsoft.AspNetCore.Rewrite
{ "dependencies": { "Microsoft.AspNetCore.Rewrite": "1.0.0" } }
پس از نصب آن، نمونهای از نحوهی تعریف و استفادهی آن در کلاس آغازین برنامه به صورت ذیل خواهد بود:
public void Configure(IApplicationBuilder app) { app.UseRewriter(new RewriteOptions() .AddRedirectToHttps() .AddRewrite(@"app/(\d+)", "app?id=$1", skipRemainingRules: false) // Rewrite based on a Regular expression //.AddRedirectToHttps(302, 5001) // Redirect to a different port and use HTTPS .AddRedirect("(.*)/$", "$1") // remove trailing slash, Redirect using a regular expression .AddRedirect(@"^section1/(.*)", "new/$1", (int)HttpStatusCode.Redirect) .AddRedirect(@"^section2/(\\d+)/(.*)", "new/$1/$2", (int)HttpStatusCode.MovedPermanently) .AddRewrite("^feed$", "/?format=rss", skipRemainingRules: false));
در اینجا مثالهایی را از اجبار به استفادهی از HTTPS، تا حذف / از انتهای مسیرهای وب سایت و یا هدایت آدرس قدیمی فید سایت، به آدرسی جدید واقع در مسیر format=rss، توسط عبارات باقاعده مشاهده میکنید.
در این تنظیمات اگر پارامتر skipRemainingRules به true تنظیم شود، به محض برآورده شدن شرط انطباق مسیر (پارامتر اول ذکر شده)، بازنویسی مسیر بر اساس پارامتر دوم، صورت گرفته و دیگر شرطهای ذکر شده، پردازش نخواهند شد.
این میانافزار قابلیت دریافت تعاریف خود را از فایلهای web.config و یا htaccess (لینوکسی) نیز دارد:
app.UseRewriter(new RewriteOptions() .AddIISUrlRewrite(env.ContentRootFileProvider, "web.config") .AddApacheModRewrite(env.ContentRootFileProvider, ".htaccess"));
و یا اگر خواستید منطق پیچیدهتری را نسبت به عبارات باقاعده اعمال کنید، میتوان یک IRule سفارشی را نیز به نحو ذیل تدارک دید:
public class RedirectWwwRule : Microsoft.AspNetCore.Rewrite.IRule { public int StatusCode { get; } = (int)HttpStatusCode.MovedPermanently; public bool ExcludeLocalhost { get; set; } = true; public void ApplyRule(RewriteContext context) { var request = context.HttpContext.Request; var host = request.Host; if (host.Host.StartsWith("www", StringComparison.OrdinalIgnoreCase)) { context.Result = RuleResult.ContinueRules; return; } if (ExcludeLocalhost && string.Equals(host.Host, "localhost", StringComparison.OrdinalIgnoreCase)) { context.Result = RuleResult.ContinueRules; return; } string newPath = request.Scheme + "://www." + host.Value + request.PathBase + request.Path + request.QueryString; var response = context.HttpContext.Response; response.StatusCode = StatusCode; response.Headers[HeaderNames.Location] = newPath; context.Result = RuleResult.EndResponse; // Do not continue processing the request } }
و سپس میتوان آنرا به عنوان یک گزینهی جدید Rewriter معرفی نمود:
app.UseRewriter(new RewriteOptions().Add(new RedirectWwwRule()));
یک نکته: در اینجا در صورت نیاز میتوان از تزریق وابستگیهای در سازندهی کلاس Rule جدید تعریف شده نیز استفاده کرد. برای اینکار باید RedirectWwwRule را به لیست سرویسهای متد ConfigureServices معرفی کرد و سپس نحوهی دریافت وهلهای از آن جهت معرفی به میانافزار بازنویسی مسیرهای وب به صورت ذیل درخواهد آمد:
var options = new RewriteOptions().Add(app.ApplicationServices.GetService<RedirectWwwRule>());
git://github.com/[username]/[repositoryname].git
git@github.com:[username]/[repositoryname].git
origin/master
git clone https://github.com/jquery/jquery.git
git clone [URL][directory name]
git remote add [alias][URL]
git remote add origin https://github.com/jquery/jquery.git
git remote
git remote [alias] -rm
git branch -r
git branch -a
git fetch [alias][alias/branch name]
pull [alias][remote branch name ]
git branch --set-upstream [local brnach][alias/branch name]
git push -u [alias][branch name ]
git tag [tag name]
git tag