جهت تکمیل مطلب، برای کنترل نسخه تغییرات بانک اطلاعاتی میشه از Fluent Migrator استفاده کرد.
EF Code First #3
چند متد الحاقی پیشنهادی
اگر این متد رو به صورت fluent بنویسی کار میکنه یعنی str.append.append.append الی آخر. البته بهینه نیست و استفاده از StringBulder زیر 600 بار جمع زدن رشتهها کارآیی کمتری از استفاده از + معمولی داره.
Garbage Collection
فرض کنید متغیری را ایجاد کرده و به آن مقدار دادهاید:
string message = "Hello World!";
آیا تابحال به این موضوع فکر کردهاید که طول عمر متغیر message تا چه زمانی است و چه زمانی باید از بین برود؟
چه زمانی باید توسط کامپایلر ( یا بهتر بگوییم ، Runtime ) طول عمر این متغیر به پایان برسد و از حافظه حذف شود؟
- Unmanaged: در این دسته از زبان ها، وظیفهی ایجاد اشیاء، تشخیص زمان درست برای از بین بردن و از بین بردن آنها، برعهدهی شماست. زبانهای C و ++C جز این نوع زبانها میباشند.
- Managed: در این دسته از زبانها، ایجاد اشیاء مانند قبل بر عهدهی شماست، اما وظیفه تشخیص و از بین بردن آنها در زمان درست را Runtime برعهده میگیرد.
در این نوع زبانها ما دغدغهی حذف متغیری را که در چندین خط بالاتر، آن را ایجاد کرده و به آن مقدار داده، به چندین متد آن را پاس داده و انواع تغییرات را روی آن انجام دادهایم، نداریم. زبانهای #C و Java جزو این نوع زبانها میباشند.
ایده کلی ایجاد زبانهای Managed بر این است که شما درگیر مباحث مربوط به Memory Management نشوید و تمرکز اصلیتان روی Business باشد.
اکثر پروژهها به اندازه کافی پیچیدگیهای Business ای دارند و ترکیب کردن این نوع پیچیدگیها با پیچیدگیهای Low Level و Technical ای همچون مباحث Memory Management، در اکثر اوقات باعث میشود که نگهداری از پروژه کاری بسیار دشوار شده و به تدریج مانند بسیاری از پروژههای دیگر، نام Legacy را با خود به یدک بکشد.
این بدان معنا نیست که پروژههایی که با زبان هایی همچون C و ++C نوشته میشوند، از همان روز اول Legacy بوده و قابل نگهداری نیستند، بلکه بدین معناست که نگهداری کد چنین زبانهایی نسبت به زبانهای Managed، به مراتب مشکلتر است و همچنین قابل ذکر است که قابلیت نگهداری یک پروژه به مباحث بسیار زیاد دیگری نیز بستگی دارد.
کد این برنامه در زبان C :
#include <stdio.h> #include <stdlib.h> void printReport(int* data) { printf("Report: %d", *data); } int main(void) { int *myNumber; myNumber = (int*)malloc(sizeof(int)); if (myNumber == 0) { printf("ERROR: Out of memory"); return 1; } *myNumber = 25; printReport(myNumber); free(myNumber); myNumber = NULL; return 0; }
"هدف" و Business اصلی این برنامه، چاپ و یک گزارش ساده بود، اما مسائل بسیار بیشتری در این مثال دخیل شده اند:
چند مرحله از این مراحل ذکر شده، واقعا نیاز Business ای برنامه ما بود؟ این مثال، بسیار ساده و غیر واقعی بود؛ اما تصور کنید با این روش، یک برنامه بزرگ با Business Ruleها و پیچیدگیهای خودش، چه حجمی از کد و پیچیدگی را خواهد داشت.
کد این برنامه در زبان #C :
using System; public class Program { public static void Main() { int myNumber = 25; PrintReport(myNumber); } private static void PrintReport(int number) { Console.WriteLine($"Report: {number}"); } }
همانطور که میبینید در اینجا تمرکزمان روی هدف اصلی و Business است و درگیر پیچیدگی مباحث جانبی نظیر Manual Memory Management نشدهایم و Runtime زبان #C یعنی CoreCLR وظیفه Memory Management را در پشت صحنه برعهده گرفته است.
تفاوت بین زبانهای Managed و Unmanaged را میتوانیم به رانندگی با ماشین دندهای و اتومات تشبیه کنیم.
اکثر اوقات هدف اصلی رانندگی، رفتن از یک مبدا به یک مقصد است. با استفاده از یک ماشین دندهای، علاوه بر هدف اصلی یعنی رسیدن به یک مقصد، ذهن ما درگیر تعویض دنده در سرعت مناسب در طول مسیر میشود و اینکار را ممکن است بیش از صدها یا هزاران بار انجام دهیم. در این روش طبیعتا ما کنترل بیشتری داریم و در بعضی مواقع بسیار بهتر به نسبت یک سیستم خودکار عمل میکنیم، اما از هدف اصلی خود یعنی رفتن از نقطه A به B دور شدهایم.
در زبانهای Managed و Unmanaged هم دقیقا چنین تفاوت هایی وجود دارد.
در زبانهای Unmanaged، شما کنترل کاملی را روی طول عمر اشیا و مدیریت حافظه دارید و همه چیز برعهده شماست؛ اما علاوه بر هدف اصلیتان، درگیر مباحث جانبی دیگری نیز شدهاید. اکثر اوقات قدرت زبانهای Unmanaged را در Game Engineها و Real-Time Processing Systemها میتوانید ببینید که در آنها مدیریت حافظه بصورت دستی انجام میشود و برنامه نویسهای آن سیستم، تعیین کننده اصلی این هستند که طول عمر اشیاء تا چه زمانی باشد و چه زمانی از بین بروند که باعث اختلال یا کندی یک سیستم حتی برای چند میلی ثانیه نشوند.
در زبانهای Managed، اکثر اوقات حتی نیازی نیست که شما درگیر مباحث جانبی مدیریت حافظه شوید و تمام کار را Runtime شما بصورت خودکار انجام میدهد. اما گاهی اوقات لازم است که قسمتهای حساس برنامه ( اصطلاحا Hot-Pathها ) خود را پیدا کنید، از این قسمتها Benchmark گرفته و مطمئن شوید که با حجم تعداد بالای درخواست و بار، به خوبی عمل میکند و همچنین قسمتی از برنامه، نشستی حافظه ( اصطلاحا Memory Leak ) ندارد. همانطور که گفتیم، گاهی اوقات یک انسان ( سیستم دستی ) بهتر از یک سیستم خودکار میتواند تصمیم بگیرد که در یک لحظه چه اتفاقی داخل برنامه رخ دهد.
در این سری مقالات قصد داریم وارد مبحث Memory Management در #C شده، با Garbage Collector آشنا و دیدی کلی از نحوهی انجام کار آن داشته باشیم.
ویژگی های پیشرفته ی AutoMapper - قسمت دوم
public class ProductType:BaseEntity { #region Field private string persianTitle; private string englishTitle; private IList<Product> products; #endregion #region Memebr public string PersianTitle { get { return (persianTitle); } set { persianTitle = Microsoft.Security.Application.Encoder.HtmlEncode(value); } } public string EnglishTitle { get { return (englishTitle); } set { englishTitle = Microsoft.Security.Application.Encoder.HtmlEncode(value); } } public virtual IList<Product> Products { get { return (products); } set { products = value; } } #endregion }
[HttpPost] public ActionResult Update(ProductTypeViewModel productTypeViewModel) { if (productTypeViewModel.ExaminId()) { ProductType productType; productType = _productType.Find(x => x.Id == productTypeViewModel.Id); AutoMapper.Mapper.Map(productTypeViewModel, productType); _uow.SaveChanges(); } return RedirectToAction("Index"); }
issing type map configuration or unsupported mapping. Mapping types: ProductTypeViewModel -> ProductType_5334DF7BAFBE780DF5328E2D6DF2A0DC3350F23340BE2EE2FC506AE9EDEC38DA MvcUserInterface.Areas.Management.Models.ProductTypeViewModel -> System.Data.Entity.DynamicProxies.ProductType_5334DF7BAFBE780DF5328E2D6DF2A0DC3350F23340BE2EE2FC506AE9EDEC38DA Destination path: ProductType_5334DF7BAFBE780DF5328E2D6DF2A0DC3350F23340BE2EE2FC506AE9EDEC38DA Source value: MvcUserInterface.Areas.Management.Models.ProductTypeViewModel
عموما برای درج فایلهای ثابت اسکریپتها و شیوهنامههای سایت، از روش متداول زیر استفاده میشود:
<link rel="stylesheet" href="/css/site.css" /> <script src="/js/site.js"></script>
مشکلی که به همراه این روش وجود دارد، مطلع سازی کاربران و مرورگر، از تغییرات آنهاست؛ چون این فایلهای ثابت، توسط مرورگرها کش شده و با فشردن دکمههایی مانند Ctrl+F5 و بهروز شدن کش مرورگر، به نگارش جدید، ارتقاء پیدا میکنند. برای رفع این مشکل حداقل دو روش وجود دارد:
الف) هربار نام این فایلها را تغییر دهیم. برای مثال بجای نام قدیمی site.css، از نام جدید site.v.1.1.css استفاده کنیم.
ب) یک کوئری استرینگ متغیر را به نام ثابت این فایلها، اضافه کنیم.
که در این بین، روش دوم متداولتر و معقولتر است. برای این منظور، ASP.NET Core به همراه ویژگی توکاری است به نام asp-append-version که اگر آنرا به تگهای اسکریپت و link اضافه کنیم:
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" /> <script src="~/js/site.js" asp-append-version="true"></script>
این کوئری استرینگ را به صورت خودکار محاسبه کرده و به آدرس فایل درج شده اضافه میکند؛ با خروجیهایی شبیه به مثال زیر:
<link rel="stylesheet" href="/css/site.css?v=AAs5qCYR2ja7e8QIduN1jQ8eMcls-cPxNYUozN3TJE0" /> <script src="/js/site.js?v=NO2z9yI9csNxHrDHIeTBBfyARw3PX_xnFa0bz3RgnE4"></script>
ASP.NET Core در اینجا هش فایلهای یافت شده را با استفاده از الگوریتم SHA256 محاسبه و url encode کرده و به صورت یک کوئری استرینگ، به انتهای آدرس فایلها اضافه میکند. به این ترتیب با تغییر محتوای این فایلها، این هش نیز تغییر میکند و مرورگر بر این اساس، همواره آخرین نگارش ارائه شده را از سرور دریافت خواهد کرد. نتیجهی این محاسبات نیز به صورت خودکار کش میشود و همچنین با استفاده از یک File Watcher در پشت صحنه، تغییرات این فایلها هم بررسی میشوند. یعنی اگر فایلی تغییر کرد، نیازی به ریاستارت برنامه نیست و محاسبات جدید و کش شدن مجدد آنها، به صورت خودکار انجام میشود.
البته این ویژگی هنوز به Blazor اضافه نشدهاست؛ اما امکان استفادهی از زیر ساخت ویژگی asp-append-version با کدنویسی مهیا است که در ادامه با استفاده از آن، کامپوننتی را مخصوص Blazor SSR، تهیه میکنیم.
دسترسی به زیر ساخت محاسباتی ویژگی asp-append-version با کدنویسی
زیرساخت محاسباتی ویژگی asp-append-version، با استفاده از سرویس توکار IFileVersionProvider به صورت زیر قابل دسترسی است:
public static class FileVersionHashProvider { private static readonly string ProcessExecutableModuleVersionId = Assembly.GetEntryAssembly()!.ManifestModule.ModuleVersionId.ToString("N"); public static string GetFileVersionedPath(this HttpContext httpContext, string filePath, string? defaultHash = null) { ArgumentNullException.ThrowIfNull(httpContext); var fileVersionedPath = httpContext.RequestServices.GetRequiredService<IFileVersionProvider>() .AddFileVersionToPath(httpContext.Request.PathBase, filePath); return IsEmbeddedOrNotFound(fileVersionedPath, filePath) ? QueryHelpers.AddQueryString(filePath, new Dictionary<string, string?>(StringComparer.Ordinal) { { "v", defaultHash ?? ProcessExecutableModuleVersionId } }) : fileVersionedPath; } private static bool IsEmbeddedOrNotFound(string fileVersionedPath, string filePath) => string.Equals(fileVersionedPath, filePath, StringComparison.Ordinal); }
در برنامههای Blazor SSR، دسترسی کاملی به HttpContext وجود دارد و همانطور که مشاهده میکنید، این سرویس نیز به اطلاعات آن جهت محاسبهی هش فایل معرفی شدهی به آن، نیاز دارد. در اینجا اگر هش قابل محاسبه نبود، از هش فایل اسمبلی جاری استفاده خواهد شد.
ساخت کامپوننتهایی برای درج خودکار هش فایلهای اسکریپتها
یک نمونه روش استفادهی از متد الحاقی GetFileVersionedPath فوق را در کامپوننت DntFileVersionedJavaScriptSource.razor زیر میتوانید مشاهده کنید:
@if (!string.IsNullOrWhiteSpace(JsFilePath)) { <script src="@HttpContext.GetFileVersionedPath(JsFilePath)" type="text/javascript"></script> } @code{ [CascadingParameter] public HttpContext HttpContext { set; get; } = null!; [Parameter] [EditorRequired] public required string JsFilePath { set; get; } }
با استفاده از HttpContext مهیای در برنامههای Blazor SSR، متد الحاقی GetFileVersionedPath به همراه مسیر فایل js. مدنظر، در صفحه درج میشود.
برای مثال یک نمونه از استفادهی آن، به صورت زیر است:
<DntFileVersionedJavaScriptSource JsFilePath="/lib/quill/dist/quill.js"/>
در نهایت با اینکار، یک چنین خروجی در صفحه درج خواهد شد که با تغییر محتوای فایل quill.js، هش متناظر با آن به صورت خودکار بهروز خواهد شد:
<scriptsrc="/lib/quill/dist/quill.js?v=5q7uUOOlr88Io5YhQk3lgYcoB_P3-5Awq1lf0rRa7-Y" type="text/javascript"></script>
شبیه به همین کار را برای شیوهنامهها هم میتوان تکرار کرد و کدهای آن، تفاوت آنچنانی با کامپوننت فوق ندارند.
منبعی که با کادر قرمز مشخص کردم، به درستی کار میکنن و مشکلی نداره و از کلاسهای لایه domain میتونن ازش استفاده کنن. اما خب منطقی نیست برای هر کلاس یک منبع بسازم.
مساله ام، نحوه نامگذاری منبع اشتراکی هست. SharedResource رو هر گونه نامگذاری کردم، domain نتونست اونو بشناسه. صرفا نمیدونم که منبع اشتراکی رو چه نامی بهش بدم. نه نامگذاری تصویر بالا و نه تصویر پایین، هیچکدوم کار نکرد.
متشکرم.
روش تعیین نوع 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);
مشکل مهم این روش عدم سازگاری کامل آن با EF است. برای مثال در آن تفاوتی بین (Include(x=>x.Tags و (Include(x=>x.Users وجود ندارد. به همین جهت در این نوع موارد، قادر به تولید کلید منحصربفردی جهت کش کردن اطلاعات یک کوئری مشخص نیست. در اینجا یک کوئری LINQ، به معادل رشتهای آن تبدیل میشود و سپس Hash آن محاسبه میگردد. این هش، کلید ذخیره سازی اطلاعات حاصل از کوئری، در سیستم کش خواهد بود. زمانیکه دو کوئری Include دار متفاوت EF، هشهای یکسانی را تولید کنند، عملا این سیستم کش، کارآیی خودش را از دست میدهد. برای رفع این مشکل پروژهی دیگری به نام EF cache ارائه شدهاست. این پروژه بسیار عالی طراحی شده و میتواند جهت ایده دادن به تیم EF نیز بکار رود. اما در آن فرض بر این است که شما میخواهید کل سیستم را در یک کش قرار دهید. وارد مکانیزم DBCommand و DataReader میشود و در آنجا کار کش کردن تمام کوئریها را انجام میدهد؛ مگر آنکه به آن اعلام کنید از کوئریهای خاصی صرفنظر کند.
با توجه به این مشکلات، روش بهتری برای تولید هش یک کوئری LINQ to Entities بر اساس کوئری واقعی SQL تولید شده توسط EF، پیش از ارسال آن به بانک اطلاعاتی به صورت زیر وجود دارد:
private static ObjectQuery TryGetObjectQuery<T>(IQueryable<T> source) { var dbQuery = source as DbQuery<T>; if (dbQuery != null) { const BindingFlags privateFieldFlags = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public; var internalQuery = source.GetType().GetProperty("InternalQuery", privateFieldFlags) .GetValue(source); return (ObjectQuery)internalQuery.GetType().GetProperty("ObjectQuery", privateFieldFlags) .GetValue(internalQuery); } return null; }
این اطلاعات، پایهی تهیهی کتابخانهی جدیدی به نام EFSecondLevelCache گردید. برای نصب آن کافی است دستور ذیل را در کنسول پاورشل نیوگت صادر کنید:
PM> Install-Package EFSecondLevelCache
var products = context.Products.Include(x => x.Tags).FirstOrDefault();
var products = context.Products.Include(x => x.Tags).Cacheable().FirstOrDefault(); // Async methods are supported too.
پس از آن نیاز است کدهای کلاس Context خود را نیز به نحو ذیل ویرایش کنید (به روز رسانی شدهی آن در اینجا):
namespace EFSecondLevelCache.TestDataLayer.DataLayer { public class SampleContext : DbContext { // public DbSet<Product> Products { get; set; } public SampleContext() : base("connectionString1") { } public override int SaveChanges() { return SaveAllChanges(invalidateCacheDependencies: true); } public int SaveAllChanges(bool invalidateCacheDependencies = true) { var changedEntityNames = getChangedEntityNames(); var result = base.SaveChanges(); if (invalidateCacheDependencies) { new EFCacheServiceProvider().InvalidateCacheDependencies(changedEntityNames); } return result; } private string[] getChangedEntityNames() { return this.ChangeTracker.Entries() .Where(x => x.State == EntityState.Added || x.State == EntityState.Modified || x.State == EntityState.Deleted) .Select(x => ObjectContext.GetObjectType(x.Entity.GetType()).FullName) .Distinct() .ToArray(); } } }
کدهای کامل این پروژه را از مخزن کد ذیل میتوانید دریافت کنید:
EFSecondLevelCache
پ.ن.
این کتابخانه هم اکنون در سایت جاری در حال استفاده است.