Learn how you can save time by creating your own reusable .NET Core templates in just a few steps.
Do you ever develop prototypes, or starter projects/accelerators, that you’d like to use again in the future? A good way to do that is by creating custom templates for dotnet. Once completed, anytime you want to create a new project of that type in the future, you can key in “dotnet new ” and you’re off, complete with correct namespaces. You can even do conditional checks, or variable replacements.
اشتراکها
۳۰ روز با TDD
Cayley is an open-source graph inspired by the graph database behind Freebase and Google's Knowledge Graph.
Features
- Community driven
- Written in Go
- can be used as a Go library
- Easy to get running (3 or 4 commands)
- RESTful API
- or a REPL if you prefer
- Built-in query editor and visualizer
- Multiple query languages:
- Plays well with multiple backend stores:
- KVs: Bolt, LevelDB
- NoSQL: MongoDB, ElasticSearch, CouchDB/PouchDB
- SQL: PostgreSQL, CockroachDB, MySQL
- In-memory, ephemeral
- Modular design; easy to extend with new languages and backends
- Good test coverage
- Speed, where possible.
فرض کنید میخواهیم بارکد این قبض را یافته و سپس عدد متناظر با آنرا در برنامه بخوانیم.
مراحل کار به این صورت هستند:
بارگذاری تصویر و چرخش آن در صورت نیاز
ابتدا تصویر بارکد دار را بارگذاری کرده و آنرا تبدیل به یک تصویر سیاه و سفید میکنیم:
در این بین ممکن است بارکد موجود در تصویر، دقیقا در زاویهای که در تصویر ابتدای بحث قرار گرفتهاست، وجود نداشته باشد؛ مثلا منهای 90 درجه، چرخیده باشد. به همین جهت میتوان از متد چرخش تصویر مطلب «تغییر اندازه، و چرخش تصاویر» ارائه شده در قسمت نهم این سری استفاده کرد.
تشخیص گرادیانهای افقی و عمودی
یکی از روشهای تشخیص بارکد، استفاده از روشی است که در تشخیص خودرو قسمت 16 بیان شد. تعداد زیادی تصویر بارکد را تهیه و سپس آنها را به الگوریتمهای machine learning جهت تشخیص و یافتن محدودهی بارکد موجود در یک تصویر، ارسال کنیم. هرچند این روش جواب خواهد داد، اما در این مورد خاص، قسمت بارکد، شبیه به گرادیانی از رنگها است. کتابخانهی OpenCV برای یافتن این نوع گرادیانها دارای متدی است به نام Sobel :
ابتدا درجهی شدت گرادیانها در جهتهای x و y محاسبه میشوند. سپس این شدتها از هم کم خواهند شد تا بیشترین شدت گرادیان موجود در محور x حاصل شود. این بیشترین شدتها، بیانگر نواحی خواهند بود که احتمال وجود بارکدهای افقی در آنها بیشتر است.
کاهش نویز و یکی کردن نواحی تشخیص داده شده
در ادامه میخواهیم با استفاده از متدهای تشخیص کانتور (قسمت 12)، نواحی با بیشترین شدت گرادیان افقی را پیدا کنیم. اما تصویر حاصل از قسمت قبل برای اینکار مناسب نیست. به همین جهت با استفاده از متدهای کار با مورفولوژی تصاویر، این نواحی گرادیانی را یکی میکنیم (قسمت 8).
این سه مرحله را در تصاویر ذیل مشاهده میکنید:
ابتدا با استفاده از متد Threshold، تصویر را به یک تصویر باینری تبدیل خواهیم کرد. در این تصویر تمام نقاط دارای شدت رنگ کمتر از مقدار thresh، به مقدار حداکثر 255 تنظیم میشوند.
سپس با استفاده از متدهای تغییر مورفولوژی تصویر، قسمتهای مجاور به هم را میبندیم و یکی میکنیم. این مورد در یافتن اشیاء احتمالی که ممکن است بارکد باشند، بسیار مفید است.
متدهای Erode و Dilate در اینجا کار حذف نویزهای اضافی را انجام میدهند؛ تا بهتر بتوان بر روی نواحی بزرگتر یافت شده، تمرکز کرد.
یافتن بزرگترین ناحیهی به هم پیوستهی موجود در یک تصویر
تمام این مراحل را انجام دادیم تا بتوانیم بزرگترین ناحیهی به هم پیوستهای را که احتمال میرود بارکد باشد، در تصویر تشخیص دهیم. پس از این آماده سازیها، اکنون با استفاده از متد یافتن کانتورها، تمام نواحی یکی شده را یافته و بزرگترین مساحت ممکن را به عنوان بارکد انتخاب میکنیم:
حاصل این عملیات یافتن بزرگترین ناحیهی گرادیانی به هم پیوستهی موجود در تصویر است:
خواندن مقدار متناظر با بارکد یافت شده
خوب، تا اینجا موفق شدیم، محل قرارگیری بارکد را تصویر پیدا کنیم. مرحلهی بعد خواندن مقدار متناظر با این تصویر است. برای این منظور از کتابخانهی سورس بازی به نام http://zxingnet.codeplex.com استفاده خواهیم کرد. این کتابخانه قادر است بارکد بسازد و همچنین تصاویر بارکدها را خوانده و مقادیر متناظر با آنها را استخراج کند. برای نصب آن میتوان از دستور ذیل استفاده کرد:
پس از نصب این کتابخانهی بارکدساز و بارکد خوان، اکنون تنها کاری که باید صورت گیرد، ارسال تصویر بارکد جدا شدهی توسط OpenCV به آن است:
چند نکته را باید در مورد کار با ZXing.Net بخاطر داشت؛ وگرنه جواب نمیگیرید:
الف) این کتابخانه حتما نیاز دارد تا تصویر بارکد، در یک حاشیهی سفید در اختیار او قرار گیرد. به همین جهت در متد getBarcodeText، ابتدا تصویر بارکد یافت شده، به میانهی یک مستطیل سفید رنگ بزرگتر کپی میشود.
ب) برای تبدیل Mat به Bitmap مورد نیاز این کتابخانه میتوان از متد الحاقی ToBitmap استفاده کرد (قسمت 7).
ج) پس از آن وهلهای از کلاس BarcodeReader آماده شده و در آن پارامترهایی مانند بیشتر سعی کن (TryHarder) و اصلاح درجهی چرخش تصویر (AutoRotate) تنظیم شدهاند.
د) بارکدهای موجود در قبضهای ایران عموما بر اساس فرمت CODE_128 ساخته میشوند. بنابراین برای خواندن سریعتر آنها میتوان PossibleFormats را مقدار دهی کرد. اگر این مقدار دهی صورت نگیرد، تمام حالتهای ممکن بررسی میشوند.
در آخر کار این متد، از متد Writer آن نیز برای تولید بارکد مشابهی استفاده شدهاست تا بتوان بررسی کرد این دو تا چه اندازه به هم شبیه هستند.
همانطور که مشاهده میکنید، عدد تشخیص داده شده، با عدد شناسهی قبض و شناسهی پرداخت تصویر ابتدای بحث یکی است.
بهبود تصویر، پیش از ارسال آن به متد Decode کتابخانهی ZXing.Net
در تصویر قبلی، سطر decode failed را هم ملاحظه میکنید. علت اینجا است که اولین سعی انجام شده، موفق نبوده است؛ چون تصویر تشخیص داده شده، بیش از اندازه نویز و حاشیهی خاکستری دارد. میتوان این حاشیهی خاکستری را با دوبار اعمال متد Threshold از بین برد:
اعداد یافت شده، دقیقا از روی تصویر بهبود یافتهی توسط متدهای Threshold خوانده شدهاند و نه تصویر ابتدایی یافت شده. بنابراین به این موضوع نیز باید دقت داشت.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید.
مراحل کار به این صورت هستند:
بارگذاری تصویر و چرخش آن در صورت نیاز
ابتدا تصویر بارکد دار را بارگذاری کرده و آنرا تبدیل به یک تصویر سیاه و سفید میکنیم:
// load the image and convert it to grayscale var image = new Mat(fileName); if (rotation != 0) { rotateImage(image, image, rotation, 1); } if (debug) { Cv2.ImShow("Source", image); Cv2.WaitKey(1); // do events } var gray = new Mat(); var channels = image.Channels(); if (channels > 1) { Cv2.CvtColor(image, gray, ColorConversion.BgrToGray); } else { image.CopyTo(gray); }
تشخیص گرادیانهای افقی و عمودی
یکی از روشهای تشخیص بارکد، استفاده از روشی است که در تشخیص خودرو قسمت 16 بیان شد. تعداد زیادی تصویر بارکد را تهیه و سپس آنها را به الگوریتمهای machine learning جهت تشخیص و یافتن محدودهی بارکد موجود در یک تصویر، ارسال کنیم. هرچند این روش جواب خواهد داد، اما در این مورد خاص، قسمت بارکد، شبیه به گرادیانی از رنگها است. کتابخانهی OpenCV برای یافتن این نوع گرادیانها دارای متدی است به نام Sobel :
// compute the Scharr gradient magnitude representation of the images // in both the x and y direction var gradX = new Mat(); Cv2.Sobel(gray, gradX, MatType.CV_32F, xorder: 1, yorder: 0, ksize: -1); //Cv2.Scharr(gray, gradX, MatType.CV_32F, xorder: 1, yorder: 0); var gradY = new Mat(); Cv2.Sobel(gray, gradY, MatType.CV_32F, xorder: 0, yorder: 1, ksize: -1); //Cv2.Scharr(gray, gradY, MatType.CV_32F, xorder: 0, yorder: 1); // subtract the y-gradient from the x-gradient var gradient = new Mat(); Cv2.Subtract(gradX, gradY, gradient); Cv2.ConvertScaleAbs(gradient, gradient); if (debug) { Cv2.ImShow("Gradient", gradient); Cv2.WaitKey(1); // do events }
ابتدا درجهی شدت گرادیانها در جهتهای x و y محاسبه میشوند. سپس این شدتها از هم کم خواهند شد تا بیشترین شدت گرادیان موجود در محور x حاصل شود. این بیشترین شدتها، بیانگر نواحی خواهند بود که احتمال وجود بارکدهای افقی در آنها بیشتر است.
کاهش نویز و یکی کردن نواحی تشخیص داده شده
در ادامه میخواهیم با استفاده از متدهای تشخیص کانتور (قسمت 12)، نواحی با بیشترین شدت گرادیان افقی را پیدا کنیم. اما تصویر حاصل از قسمت قبل برای اینکار مناسب نیست. به همین جهت با استفاده از متدهای کار با مورفولوژی تصاویر، این نواحی گرادیانی را یکی میکنیم (قسمت 8).
// blur and threshold the image var blurred = new Mat(); Cv2.Blur(gradient, blurred, new Size(9, 9)); var threshImage = new Mat(); Cv2.Threshold(blurred, threshImage, thresh, 255, ThresholdType.Binary); if (debug) { Cv2.ImShow("Thresh", threshImage); Cv2.WaitKey(1); // do events } // construct a closing kernel and apply it to the thresholded image var kernel = Cv2.GetStructuringElement(StructuringElementShape.Rect, new Size(21, 7)); var closed = new Mat(); Cv2.MorphologyEx(threshImage, closed, MorphologyOperation.Close, kernel); if (debug) { Cv2.ImShow("Closed", closed); Cv2.WaitKey(1); // do events } // perform a series of erosions and dilations Cv2.Erode(closed, closed, null, iterations: 4); Cv2.Dilate(closed, closed, null, iterations: 4); if (debug) { Cv2.ImShow("Erode & Dilate", closed); Cv2.WaitKey(1); // do events }
ابتدا با استفاده از متد Threshold، تصویر را به یک تصویر باینری تبدیل خواهیم کرد. در این تصویر تمام نقاط دارای شدت رنگ کمتر از مقدار thresh، به مقدار حداکثر 255 تنظیم میشوند.
سپس با استفاده از متدهای تغییر مورفولوژی تصویر، قسمتهای مجاور به هم را میبندیم و یکی میکنیم. این مورد در یافتن اشیاء احتمالی که ممکن است بارکد باشند، بسیار مفید است.
متدهای Erode و Dilate در اینجا کار حذف نویزهای اضافی را انجام میدهند؛ تا بهتر بتوان بر روی نواحی بزرگتر یافت شده، تمرکز کرد.
یافتن بزرگترین ناحیهی به هم پیوستهی موجود در یک تصویر
تمام این مراحل را انجام دادیم تا بتوانیم بزرگترین ناحیهی به هم پیوستهای را که احتمال میرود بارکد باشد، در تصویر تشخیص دهیم. پس از این آماده سازیها، اکنون با استفاده از متد یافتن کانتورها، تمام نواحی یکی شده را یافته و بزرگترین مساحت ممکن را به عنوان بارکد انتخاب میکنیم:
//find the contours in the thresholded image, then sort the contours //by their area, keeping only the largest one Point[][] contours; HiearchyIndex[] hierarchyIndexes; Cv2.FindContours( closed, out contours, out hierarchyIndexes, mode: ContourRetrieval.CComp, method: ContourChain.ApproxSimple); if (contours.Length == 0) { throw new NotSupportedException("Couldn't find any object in the image."); } var contourIndex = 0; var previousArea = 0; var biggestContourRect = Cv2.BoundingRect(contours[0]); while ((contourIndex >= 0)) { var contour = contours[contourIndex]; var boundingRect = Cv2.BoundingRect(contour); //Find bounding rect for each contour var boundingRectArea = boundingRect.Width * boundingRect.Height; if (boundingRectArea > previousArea) { biggestContourRect = boundingRect; previousArea = boundingRectArea; } contourIndex = hierarchyIndexes[contourIndex].Next; } var barcode = new Mat(image, biggestContourRect); //Crop the image Cv2.CvtColor(barcode, barcode, ColorConversion.BgrToGray); Cv2.ImShow("Barcode", barcode); Cv2.WaitKey(1); // do events
خواندن مقدار متناظر با بارکد یافت شده
خوب، تا اینجا موفق شدیم، محل قرارگیری بارکد را تصویر پیدا کنیم. مرحلهی بعد خواندن مقدار متناظر با این تصویر است. برای این منظور از کتابخانهی سورس بازی به نام http://zxingnet.codeplex.com استفاده خواهیم کرد. این کتابخانه قادر است بارکد بسازد و همچنین تصاویر بارکدها را خوانده و مقادیر متناظر با آنها را استخراج کند. برای نصب آن میتوان از دستور ذیل استفاده کرد:
PM> Install-Package ZXing.Net
private static string getBarcodeText(Mat barcode) { // `ZXing.Net` needs a white space around the barcode var barcodeWithWhiteSpace = new Mat(new Size(barcode.Width + 30, barcode.Height + 30), MatType.CV_8U, Scalar.White); var drawingRect = new Rect(new Point(15, 15), new Size(barcode.Width, barcode.Height)); var roi = barcodeWithWhiteSpace[drawingRect]; barcode.CopyTo(roi); Cv2.ImShow("Enhanced Barcode", barcodeWithWhiteSpace); Cv2.WaitKey(1); // do events return decodeBarcodeText(barcodeWithWhiteSpace.ToBitmap()); } private static string decodeBarcodeText(System.Drawing.Bitmap barcodeBitmap) { var source = new BitmapLuminanceSource(barcodeBitmap); // using http://zxingnet.codeplex.com/ // PM> Install-Package ZXing.Net var reader = new BarcodeReader(null, null, ls => new GlobalHistogramBinarizer(ls)) { AutoRotate = true, TryInverted = true, Options = new DecodingOptions { TryHarder = true, //PureBarcode = true, /*PossibleFormats = new List<BarcodeFormat> { BarcodeFormat.CODE_128 //BarcodeFormat.EAN_8, //BarcodeFormat.CODE_39, //BarcodeFormat.UPC_A }*/ } }; //var newhint = new KeyValuePair<DecodeHintType, object>(DecodeHintType.ALLOWED_EAN_EXTENSIONS, new Object()); //reader.Options.Hints.Add(newhint); var result = reader.Decode(source); if (result == null) { Console.WriteLine("Decode failed."); return string.Empty; } Console.WriteLine("BarcodeFormat: {0}", result.BarcodeFormat); Console.WriteLine("Result: {0}", result.Text); var writer = new BarcodeWriter { Format = result.BarcodeFormat, Options = { Width = 200, Height = 50, Margin = 4}, Renderer = new ZXing.Rendering.BitmapRenderer() }; var barcodeImage = writer.Write(result.Text); Cv2.ImShow("BarcodeWriter", barcodeImage.ToMat()); return result.Text; }
الف) این کتابخانه حتما نیاز دارد تا تصویر بارکد، در یک حاشیهی سفید در اختیار او قرار گیرد. به همین جهت در متد getBarcodeText، ابتدا تصویر بارکد یافت شده، به میانهی یک مستطیل سفید رنگ بزرگتر کپی میشود.
ب) برای تبدیل Mat به Bitmap مورد نیاز این کتابخانه میتوان از متد الحاقی ToBitmap استفاده کرد (قسمت 7).
ج) پس از آن وهلهای از کلاس BarcodeReader آماده شده و در آن پارامترهایی مانند بیشتر سعی کن (TryHarder) و اصلاح درجهی چرخش تصویر (AutoRotate) تنظیم شدهاند.
د) بارکدهای موجود در قبضهای ایران عموما بر اساس فرمت CODE_128 ساخته میشوند. بنابراین برای خواندن سریعتر آنها میتوان PossibleFormats را مقدار دهی کرد. اگر این مقدار دهی صورت نگیرد، تمام حالتهای ممکن بررسی میشوند.
در آخر کار این متد، از متد Writer آن نیز برای تولید بارکد مشابهی استفاده شدهاست تا بتوان بررسی کرد این دو تا چه اندازه به هم شبیه هستند.
همانطور که مشاهده میکنید، عدد تشخیص داده شده، با عدد شناسهی قبض و شناسهی پرداخت تصویر ابتدای بحث یکی است.
بهبود تصویر، پیش از ارسال آن به متد Decode کتابخانهی ZXing.Net
در تصویر قبلی، سطر decode failed را هم ملاحظه میکنید. علت اینجا است که اولین سعی انجام شده، موفق نبوده است؛ چون تصویر تشخیص داده شده، بیش از اندازه نویز و حاشیهی خاکستری دارد. میتوان این حاشیهی خاکستری را با دوبار اعمال متد Threshold از بین برد:
var barcodeClone = barcode.Clone(); var barcodeText = getBarcodeText(barcodeClone); if (string.IsNullOrWhiteSpace(barcodeText)) { Console.WriteLine("Enhancing the barcode..."); //Cv2.AdaptiveThreshold(barcode, barcode, 255, //AdaptiveThresholdType.GaussianC, ThresholdType.Binary, 9, 1); //var th = 119; var th = 100; Cv2.Threshold(barcode, barcode, th, 255, ThresholdType.ToZero); Cv2.Threshold(barcode, barcode, th, 255, ThresholdType.Binary); barcodeText = getBarcodeText(barcode); } Cv2.Rectangle(image, new Point(biggestContourRect.X, biggestContourRect.Y), new Point(biggestContourRect.X + biggestContourRect.Width, biggestContourRect.Y + biggestContourRect.Height), new Scalar(0, 255, 0), 2); if (debug) { Cv2.ImShow("Segmented Source", image); Cv2.WaitKey(1); // do events } Cv2.WaitKey(0); Cv2.DestroyAllWindows();
اعداد یافت شده، دقیقا از روی تصویر بهبود یافتهی توسط متدهای Threshold خوانده شدهاند و نه تصویر ابتدایی یافت شده. بنابراین به این موضوع نیز باید دقت داشت.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید.
در این قسمت میخواهیم اطلاعات اتاقهای ثبت شده را به همراه تصاویر مرتبط با آنها، حذف کنیم و همچنین به یک خطای مهم در حین کار با EF-Core برسیم و متوجه شویم که روش کار با DbContext در برنامههای مبتنی بر Blazor Server .... با روش کار متداول با آن در برنامههای Web API، یکی نیست!
مشکل حذف تصاویر آپلود شده
در قسمت قبل، این امکان را مهیا کردیم که کاربران بتوانند پیش از ثبت اطلاعات یک اتاق، تصاویر آنرا به سرور آپلود کنند. یعنی تصاویری که در ابتدای کار آپلود میشوند، هنوز در بانک اطلاعاتی ثبت نشدهاند و هیچ رکوردی از آنها موجود نیست. در این حالت اگر کاربری تصاویری را آپلود کرده و سپس بر روی دکمهی back کلیک کند، با تعدادی تصویر آپلود شدهی غیرمنتسب به اتاقهای موجود، مواجه خواهیم شد. همچنین اگر شخصی به قسمت ویرایش تصاویر مراجعه کند و با کلیک بر روی دکمهی حذف یک تصویر، آنرا حذف کند، این حذف باید در بانک اطلاعاتی هم منعکس شود؛ در غیر اینصورت باز هم کاربر میتواند تصویری را حذف کند، اما در آخر بر روی دکمهی به روز رسانی اطلاعات رکورد کلیک نکند. در این حالت در دفعات بعدی مراجعهی به اطلاعات یک چنین اتاقی، با نقص اطلاعات تصاویری مواجه میشویم که در لیست تصاویر منتسب به یک اتاق وجود دارند، اما اصل فایل تصویری متناظر با آنها از سرور حذف شدهاست.
حذف اطلاعات تصاویر، در حالت ثبت اطلاعات
زمانیکه قرار است اطلاعات اتاقی برای اولین بار ثبت شود، حذف تصاویر آپلود شدهی مرتبط با آن سادهاست؛ چون هنوز اصل رکورد اتاق ثبت نشدهاست و این تصاویر در این لحظه، به رکوردی تعلق ندارند. بنابراین ابتدا متد رویدادگردان DeletePhoto را به دکمهی حذف اطلاعات هر تصویر نمایش داده شده، انتساب میدهیم:
و سپس آنرا به صورت زیر تکمیل میکنیم:
- با هر بار کلیک بر روی دکمهی Delete، شیء HotelRoomImageDTO متناظری به متد DeletePhoto ارسال میشود.
- در این شیء، مقدار خاصیت RoomImageUrl، همواره با نام پوشهای که فایلهای تصویری در آن آپلود شدهاند، شروع میشود. به همین جهت نام پوشه را از آن حذف کرده و بر این اساس، متد FileUploadService.DeleteFile را فراخوانی میکنیم تا تصویر جاری را از سرور حذف کند.
- سپس با فراخوانی متد Remove بر روی لیست تصاویر موجود، سبب به روز رسانی UI نیز خواهیم شد و به این ترتیب، تصویری که فایل آن از سرور حذف شده، از UI نیز حذف خواهد شد.
حذف تصاویر، در زمان ویرایش اطلاعات یک اتاق تعریف شده
همانطور که در ابتدای بحث نیز عنوان شد، نمیخواهیم در حالت ویرایش یک رکورد، با کلیک بر روی حذف یک تصویر، بلافاصله آنرا از سرور نیز حذف کنیم. چون ممکن است کاربری تصویری را حذف کند، اما بجای ذخیره سازی اطلاعات رکورد، بر روی دکمهی back کلیک کند. بنابراین در اینجا حذف تصاویر را صرفا به حذف آنها از UI محدود میکنیم و حذف نهایی را به زمان کلیک بر روی دکمهی ذخیره سازی اطلاعات در حال ویرایش، موکول خواهیم کرد.
به همین جهت در ابتدا با کلیک بر روی دکمهی حذف، ابتدا با حذف آن تصویر از HotelRoomImages، سبب به روز رسانی UI خواهیم شد، اما این تصویر را واقعا حذف نمیکنیم. در اینجا فقط نام آنرا در یک لیست، برای حذف نهایی، ذخیره سازی خواهیم کرد:
به این ترتیب اگر کاربر بر روی دکمهی back کلیک کند، اتفاق خاصی رخ نمیدهد؛ نه رکوردی از بانک اطلاعاتی و نه فایل تصویری از سرور حذف میشود.
سپس در جائیکه کار مدیریت ثبت اطلاعات صورت میگیرد، پس از به روز رسانی رکورد متناظر با یک اتاق، بر اساس لیست DeletedImageFileNames، فایلهای علامتگذاری شدهی برای حذف را نیز واقعا از سرور حذف میکنیم:
در اینجا باز هم نیازی نیست تا یک حلقه را تشکیل دهیم و اطلاعات را مستقیما از جدول تصاویر حذف کنیم. HotelRoomModel ارسال شدهی به متد UpdateHotelRoomAsync، چون به همراه لیست جدید HotelRoomImages است (که توسط فراخوانی HotelRoomModel.HotelRoomImages.Remove به روز شدهاست)، در حین Update، تصاویری که در این لیست وجود نداشته باشند، به صورت خودکار توسط EF-Core از سر دیگر رابطه حذف میشوند.
نمایش «لطفا منتظر بمانید» در حین آپلود تصاویر
در ادامه میخواهیم تا پایان نمایش آپلود تصاویر، پیام «لطفا منتظر بمانید» را به همراه یک spinner نمایش دهیم. بنابراین در ابتدا کلاسهای جدید زیر را به فایل wwwroot\css\site.css اضافه میکنیم:
سپس برای مدیریت نمایش spinner فوق، در ابتدای کار آپلود، فیلدIsImageUploadProcessStarted را به true تنظیم کرده و در پایان کار، آنرا false میکنیم. به همین جهت نیاز به یک try/finally خواهد بود:
پس از آن فقط کافی است بر اساس مقدار جاری این فیلد، ذیل فیلد InputFile، پیامی را نمایش دهیم:
دریافت تائیدیهی حذف، پس از کلیک بر روی دکمههای حذف تصاویر
در قسمت 12 این سری، کامپوننت Confirmation.razor را توسعه دادیم. در اینجا میخواهیم با کلیک بر روی دکمههای حذف تصاویر، ابتدا توسط این کامپوننت، تائیدیهای دریافت شود و در صورت تائید، آن تصویر انتخابی را حذف کنیم.
به همین جهت در ابتدا فایل Confirmation.razor را به پوشهی جدید Pages\Components کپی میکنیم. سپس فضای نام آنرا به فایل BlazorServer\BlazorServer.App\_Imports.razor اضافه میکنیم تا در تمام کامپوننتهای برنامه قابل استفاده شود:
سپس در ابتدا کامپوننت Confirmation را به صورت زیر اضافه میکنیم:
- ref تعریف شده سبب میشود تا بتوان متدهای عمومی تعریف شدهی در این کامپوننت، مانند Show و Hide را فراخوانی کرد.
- سپس روالهای رویدادگردان OnCancel و OnConfirm به متدهایی در کامپوننت جاری متصل شدهاند.
- در آخر پیامی تعریف شدهاست.
برای اینکه کامپوننت فوق عمل کند، نیاز است تغییرات زیر را به قسمت کدها اعمال کنیم:
- توسط وهلهی Confirmation1، میتوان متد Show را زمانیکه بر روی دکمهی Delete هر تصویر کلیک میشود، فراخوانی کنیم. قبل از آن مشخصات شیء تصویر درخواستی را در فیلد ImageToBeDeleted ذخیره میکنیم تا پس از تائید کاربر، دقیقا بر اساس اطلاعات آن بتوانیم متد OnConfirmDeleteImageClicked را پردازش کنیم.
- در اینجا محتوای متد DeletePhoto اصلی را (متدی را که تا پیش از این مرحله تکمیل کردیم) به متد جدید OnConfirmDeleteImageClicked منتقل کردهایم. یعنی در ابتدا فقط یک modal نمایش داده میشود. پس از اینکه کاربر عملیات حذف را تائید کرد، رویداد OnConfirm، سبب فراخوانی متد OnConfirmDeleteImageClicked خواهد شد (که همان DeletePhoto قبل از این تغییرات است).
حذف کامل یک اتاق به همراه تمام تصاویر منتسب به آن
مرحلهی آخر این قسمت، اضافه کردن دکمهی حذف، به ردیفهای کامپوننت نمایش لیست اتاقها است که این مورد نیز باید به همراه دریافت تائیدیهی حذف و همچنین حذف تمام وابستگیهای اتاق ثبت شده باشد:
در کامپوننت BlazorServer\BlazorServer.App\Pages\HotelRoom\HotelRoomList.razor، دکمهی Delete را به نحو فوق اضافه کردهایم که با کلیک بر روی آن، روال رویدادگردان HandleDeleteRoom اجرا شده و room متناظری را دریافت میکند.
اکنون برای مدیریت دریافت تائیدیهی حذف از کاربر، کامپوننت Confirmation را اضافه کرده:
و به نحو زیر تکمیل میکنیم:
با کلیک بر روی دکمهی حذف، متد HandleDeleteRoom اجرا شده و فیلد RoomToBeDeleted را مقدار دهی میکند. از این فیلد پس از دریافت تائید، در متد OnConfirmDeleteRoomClicked برای حذف اتاق انتخابی استفاده شدهاست.
مشکل! این روش استفادهی از DbContext کار نمیکند!
اگر برنامه را اجرا کرده و سعی در حذف یک ردیف کنیم، به خطای زیر میرسیم:
عنوان میکند که متد OnConfirmDeleteRoomClicked، بر روی ترد دیگری نسبت به ترد اولیهای که DbContext بر روی آن ایجاد شده، در حال اجرا است و چون DbContext برای یک چنین سناریوهایی، thread-safe نیست، اجازهی استفادهی از آنرا نمیدهد. در مورد روش حل این مشکل ویژه، در قسمت بعد بحث خواهیم کرد.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-18.zip
مشکل حذف تصاویر آپلود شده
در قسمت قبل، این امکان را مهیا کردیم که کاربران بتوانند پیش از ثبت اطلاعات یک اتاق، تصاویر آنرا به سرور آپلود کنند. یعنی تصاویری که در ابتدای کار آپلود میشوند، هنوز در بانک اطلاعاتی ثبت نشدهاند و هیچ رکوردی از آنها موجود نیست. در این حالت اگر کاربری تصاویری را آپلود کرده و سپس بر روی دکمهی back کلیک کند، با تعدادی تصویر آپلود شدهی غیرمنتسب به اتاقهای موجود، مواجه خواهیم شد. همچنین اگر شخصی به قسمت ویرایش تصاویر مراجعه کند و با کلیک بر روی دکمهی حذف یک تصویر، آنرا حذف کند، این حذف باید در بانک اطلاعاتی هم منعکس شود؛ در غیر اینصورت باز هم کاربر میتواند تصویری را حذف کند، اما در آخر بر روی دکمهی به روز رسانی اطلاعات رکورد کلیک نکند. در این حالت در دفعات بعدی مراجعهی به اطلاعات یک چنین اتاقی، با نقص اطلاعات تصاویری مواجه میشویم که در لیست تصاویر منتسب به یک اتاق وجود دارند، اما اصل فایل تصویری متناظر با آنها از سرور حذف شدهاست.
حذف اطلاعات تصاویر، در حالت ثبت اطلاعات
زمانیکه قرار است اطلاعات اتاقی برای اولین بار ثبت شود، حذف تصاویر آپلود شدهی مرتبط با آن سادهاست؛ چون هنوز اصل رکورد اتاق ثبت نشدهاست و این تصاویر در این لحظه، به رکوردی تعلق ندارند. بنابراین ابتدا متد رویدادگردان DeletePhoto را به دکمهی حذف اطلاعات هر تصویر نمایش داده شده، انتساب میدهیم:
@if (HotelRoomModel.HotelRoomImages.Count > 0) { var serial = 1; foreach (var roomImage in HotelRoomModel.HotelRoomImages) { <div class="col-md-2 mt-3"> <div class="room-image" style="background: url('@roomImage.RoomImageUrl') 50% 50%; "> <span class="room-image-title">@serial</span> </div> <button type="button" @onclick="()=>DeletePhoto(roomImage)" class="btn btn-outline-danger btn-block mt-4">Delete</button> </div> serial++; } }
@code { private const string UploadFolder = "Uploads"; private void DeletePhoto(HotelRoomImageDTO imageDto) { var imageFileName = imageDto.RoomImageUrl.Replace($"{UploadFolder}/", "", StringComparison.OrdinalIgnoreCase); if (HotelRoomModel.Id == 0 && Title == "Create") { FileUploadService.DeleteFile(imageFileName, WebHostEnvironment.WebRootPath, UploadFolder); HotelRoomModel.HotelRoomImages.Remove(imageDto); } } }
- در این شیء، مقدار خاصیت RoomImageUrl، همواره با نام پوشهای که فایلهای تصویری در آن آپلود شدهاند، شروع میشود. به همین جهت نام پوشه را از آن حذف کرده و بر این اساس، متد FileUploadService.DeleteFile را فراخوانی میکنیم تا تصویر جاری را از سرور حذف کند.
- سپس با فراخوانی متد Remove بر روی لیست تصاویر موجود، سبب به روز رسانی UI نیز خواهیم شد و به این ترتیب، تصویری که فایل آن از سرور حذف شده، از UI نیز حذف خواهد شد.
حذف تصاویر، در زمان ویرایش اطلاعات یک اتاق تعریف شده
همانطور که در ابتدای بحث نیز عنوان شد، نمیخواهیم در حالت ویرایش یک رکورد، با کلیک بر روی حذف یک تصویر، بلافاصله آنرا از سرور نیز حذف کنیم. چون ممکن است کاربری تصویری را حذف کند، اما بجای ذخیره سازی اطلاعات رکورد، بر روی دکمهی back کلیک کند. بنابراین در اینجا حذف تصاویر را صرفا به حذف آنها از UI محدود میکنیم و حذف نهایی را به زمان کلیک بر روی دکمهی ذخیره سازی اطلاعات در حال ویرایش، موکول خواهیم کرد.
به همین جهت در ابتدا با کلیک بر روی دکمهی حذف، ابتدا با حذف آن تصویر از HotelRoomImages، سبب به روز رسانی UI خواهیم شد، اما این تصویر را واقعا حذف نمیکنیم. در اینجا فقط نام آنرا در یک لیست، برای حذف نهایی، ذخیره سازی خواهیم کرد:
@code { private List<string> DeletedImageFileNames = new List<string>(); private void DeletePhoto(HotelRoomImageDTO imageDto) { var imageFileName = imageDto.RoomImageUrl.Replace($"{UploadFolder}/", "", StringComparison.OrdinalIgnoreCase); if (HotelRoomModel.Id == 0 && Title == "Create") { // ... } else { // Edit Mode DeletedImageFileNames.Add(imageFileName); HotelRoomModel.HotelRoomImages.Remove(imageDto); // Update UI } }
سپس در جائیکه کار مدیریت ثبت اطلاعات صورت میگیرد، پس از به روز رسانی رکورد متناظر با یک اتاق، بر اساس لیست DeletedImageFileNames، فایلهای علامتگذاری شدهی برای حذف را نیز واقعا از سرور حذف میکنیم:
private async Task HandleHotelRoomUpsert() { // ... if (HotelRoomModel.Id != 0 && Title == "Update") { // Update Mode var updatedRoomDto = await HotelRoomService.UpdateHotelRoomAsync(HotelRoomModel.Id, HotelRoomModel); foreach(var imageFileName in DeletedImageFileNames) { FileUploadService.DeleteFile(imageFileName, WebHostEnvironment.WebRootPath, UploadFolder); } // await AddHotelRoomImageAsync(updatedRoomDto); await JsRuntime.ToastrSuccess($"The `{HotelRoomModel.Name}` updated successfully."); } else { // ... } } }
نمایش «لطفا منتظر بمانید» در حین آپلود تصاویر
در ادامه میخواهیم تا پایان نمایش آپلود تصاویر، پیام «لطفا منتظر بمانید» را به همراه یک spinner نمایش دهیم. بنابراین در ابتدا کلاسهای جدید زیر را به فایل wwwroot\css\site.css اضافه میکنیم:
.spinner { border: 16px solid silver !important; border-top: 16px solid #337ab7 !important; border-radius: 50% !important; width: 80px !important; height: 80px !important; animation: spin 700ms linear infinite !important; top: 50% !important; left: 50% !important; transform: translate(-50%, -50%); position: absolute !important; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
@code { private bool IsImageUploadProcessStarted; private async Task HandleImageUpload(InputFileChangeEventArgs args) { try { IsImageUploadProcessStarted = true; // ... } finally { IsImageUploadProcessStarted = false; } } }
<InputFile OnChange="HandleImageUpload" multiple></InputFile> <div class="row"> @if (IsImageUploadProcessStarted) { <div class="col-md-12"> <span><i class="spinner"></i> Please wait.. Images are uploading...</span> </div> }
دریافت تائیدیهی حذف، پس از کلیک بر روی دکمههای حذف تصاویر
در قسمت 12 این سری، کامپوننت Confirmation.razor را توسعه دادیم. در اینجا میخواهیم با کلیک بر روی دکمههای حذف تصاویر، ابتدا توسط این کامپوننت، تائیدیهای دریافت شود و در صورت تائید، آن تصویر انتخابی را حذف کنیم.
به همین جهت در ابتدا فایل Confirmation.razor را به پوشهی جدید Pages\Components کپی میکنیم. سپس فضای نام آنرا به فایل BlazorServer\BlazorServer.App\_Imports.razor اضافه میکنیم تا در تمام کامپوننتهای برنامه قابل استفاده شود:
@using BlazorServer.App.Pages.Components
<Confirmation @ref="Confirmation1" OnCancel="OnCancelDeleteImageClicked" OnConfirm="@(()=>OnConfirmDeleteImageClicked(ImageToBeDeleted))"> <div> Do you want to delete @ImageToBeDeleted?.RoomImageUrl image? </div> </Confirmation>
- سپس روالهای رویدادگردان OnCancel و OnConfirm به متدهایی در کامپوننت جاری متصل شدهاند.
- در آخر پیامی تعریف شدهاست.
برای اینکه کامپوننت فوق عمل کند، نیاز است تغییرات زیر را به قسمت کدها اعمال کنیم:
private Confirmation Confirmation1; private HotelRoomImageDTO ImageToBeDeleted; private void OnCancelDeleteImageClicked() { // Confirmation1.Hide(); } private void DeletePhoto(HotelRoomImageDTO imageDto) { ImageToBeDeleted = imageDto; Confirmation1.Show(); } private void OnConfirmDeleteImageClicked(HotelRoomImageDTO imageDto) {
- در اینجا محتوای متد DeletePhoto اصلی را (متدی را که تا پیش از این مرحله تکمیل کردیم) به متد جدید OnConfirmDeleteImageClicked منتقل کردهایم. یعنی در ابتدا فقط یک modal نمایش داده میشود. پس از اینکه کاربر عملیات حذف را تائید کرد، رویداد OnConfirm، سبب فراخوانی متد OnConfirmDeleteImageClicked خواهد شد (که همان DeletePhoto قبل از این تغییرات است).
حذف کامل یک اتاق به همراه تمام تصاویر منتسب به آن
مرحلهی آخر این قسمت، اضافه کردن دکمهی حذف، به ردیفهای کامپوننت نمایش لیست اتاقها است که این مورد نیز باید به همراه دریافت تائیدیهی حذف و همچنین حذف تمام وابستگیهای اتاق ثبت شده باشد:
<td> <NavLink href="@($"hotel-room/edit/{room.Id}")" class="btn btn-primary">Edit</NavLink> <button class="btn btn-danger" @onclick="()=>HandleDeleteRoom(room)">Delete</button> </td>
اکنون برای مدیریت دریافت تائیدیهی حذف از کاربر، کامپوننت Confirmation را اضافه کرده:
<Confirmation @ref="Confirmation1" OnCancel="OnCancelDeleteRoomClicked" OnConfirm="OnConfirmDeleteRoomClicked"> <div> Do you want to delete @RoomToBeDeleted?.Name? </div> </Confirmation>
@code { private List<HotelRoomDTO> HotelRooms = new List<HotelRoomDTO>(); private HotelRoomDTO RoomToBeDeleted; private Confirmation Confirmation1; private void OnCancelDeleteRoomClicked() { // Confirmation1.Hide(); } private void HandleDeleteRoom(HotelRoomDTO roomDto) { RoomToBeDeleted = roomDto; Confirmation1.Show(); } private async Task OnConfirmDeleteRoomClicked() { if(RoomToBeDeleted is null) { return; } await HotelRoomService.DeleteHotelRoomAsync(RoomToBeDeleted.Id); HotelRooms.Remove(RoomToBeDeleted); // Update UI }
مشکل! این روش استفادهی از DbContext کار نمیکند!
اگر برنامه را اجرا کرده و سعی در حذف یک ردیف کنیم، به خطای زیر میرسیم:
An exception occurred while iterating over the results of a query for context type 'BlazorServer.DataAccess.ApplicationDbContext'. System.InvalidOperationException: A second operation was started on this context before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-18.zip
اشتراکها
فرمت آنلاین کدهای SQL
در دنیای NET. همواره دو نوع LINQ وجود داشته داشته است: LINQ to Objects و ... مابقی. در حالت اول با <IEnumerable<Tها کار میکنیم که تمام عملیات در حافظه انجام میشود و در مابقی حالات یک <IQueryable<T وجود دارد که عبارت حاصل از آن جهت کاربردهای مختلفی به زبانهای متفاوتی مانند SQL ترجمه میشوند. در هر دو حالت کلی، Syntax نهایی یکی است و تنها اگر به منبع دادهی آنها دقت کنیم، میتوانیم نوع آنها را تشخیص دهیم. برای نمونه کوئری ذیل بر اساس منبع Blogs است که میتواند LINQ to Objects باشد و یا حالت <Queryable<Blog که قرار است به زبانی مشخص ترجمه شود:
اکنون فرض کنید که این عبارت قرار است به SQL ترجمه شده و سپس بر روی یک بانک اطلاعاتی اجرا شود. در این حالت مفسر LINQ باید بداند که متد Contains را چگونه به معادل SQL آن ترجمه کند و این ترجمه میتواند بر اساس بانکهای اطلاعاتی مختلف، متفاوت نیز باشد. اما در حالت LINQ to Objects یک چنین مشکلی وجود ندارد و این ترجمه مستقیما بر روی متد Contains کلاس string انجام میشود.
اما اکنون چطور؟
فرض کنید یک متد الحاقی را به نام ComputeHash به کلاس string اضافه کردهایم. یک چنین کوئری را اگر بر روی EF 6.x اجرا کنیم، برنامه با یک استثناء متوقف خواهد شد؛ چون امکان ترجمهی متد ComputeHash را به معادل SQL آن ندارد؛ اما EF Core برای انجام یک چنین کوئریهایی بهبود یافتهاست که به آن، محاسبات سمت کلاینت گفته میشود.
یک مثال: بررسی تاثیر ارزیابیهای سمت کلاینت در EF Core
فرض کنید ساختار جدول بلاگها به صورت زیر است:
همچنین یک متد الحاقی را به نام ComputeHash به صورت ذیل تعریف کردهایم:
اکنون میخواهیم بلاگهایی را پیدا کنیم که Hash مربوط به Url آنها بیشتر از 10 است (صرفا جهت نمایش این قابلیت جدید):
اگر این کوئری را اجرا کنیم، یک چنین خروجی SQL ایی تولید خواهد شد و همچنین برنامه کرش هم نمیکند:
به این معنا که در ارزیابیهای سمت کلاینت:
الف) مفسر LINQ در EF Core، شروع به ارزیابی کوئری نوشته شده میکند و هرجائیکه متدی را یافت و از درک آن عاجز بود (معادل SQL ایی را برای آن نیافت)، آنرا از کوئری حذف میکند.
ب) کوئری SQL نهایی بدون متد ComputeHash بر روی بانک اطلاعاتی اجرا شده و نتیجه به سمت کلاینت بازگشت داده میشود. به همین جهت است که در خروجی SQL فوق خبری از متد ComputeHash نیست.
ج) اکنون که EF Core اطلاعات لازم را از سمت سرور دریافت کردهاست، متد ComputeHash را در سمت کلاینت بر روی این نتیجهی دریافتی اعمال میکند. یعنی مرحلهی آخر همان LINQ to Objects متداول خواهد بود.
به این ترتیب است که EF Core قابلیت اجرای هر نوع متدی را که معادل SQL ایی برای آن وجود ندارد، خواهد یافت.
چگونه متوجه شویم که ارزیابی سمت کلاینت رخ دادهاست؟
EF Core این قابلیت را دارد تا گزارش کاملی را از ارزیابیهای سمت کلاینت صورت گرفته ارائه دهد. هرچند در مثال فوق متد الحاقی ComputeHash بسیار واضح است، اما برای نمونه متد string.Join نیز معادل SQL ایی ندارد:
این مثال بدون مشکل توسط EF Core و قابلیت جدید ارزیابی سمت کلاینت آن اجرا میشود، اما بهتر است از وقوع یک چنین رخدادهایی مطلع شویم:
برای این منظور تنها کافی است درخواست فعالسازی لاگ کردن QueryClientEvaluationWarning را در قسمت ConfigureWarnings آن ارائه دهیم. در این حالت اگر برنامه را مجددا اجرا کنیم، ابتدا یک چنین خروجی لاگ میشود:
عنوان کردهاست که قابلیت ترجمهی ComputeHash را به SQL نداشته و آنرا در نهایت به صورت محلی و در سمت کلاینت محاسبه میکند.
اگر میخواهید ارزیابی سمت کلاینت را ممنوع کنید، در تنظیمات فوق warnings.Log را به warnings.Throw تغییر دهید. این مورد سبب خواهد شد تا اگر برنامه به این نوع ارزیابیها رسید، با یک استثناء متوقف شود (شبیه به حالت EF 6.x).
تاثیر ارزیابیهای سمت کلاینت بر روی کارآیی برنامه
هرچند قابلیت ارزیابیهای سمت کلاینت بسیار مفید است اما باید دقت داشت:
الف) در این حالت چون ابتدا متدهایی که قابلیت ارزیابی در سمت سرور را دارا نیستند، حذف خواهند شد، ممکن است تمام رکوردها به سمت کلاینت بازگشت داده شده و سپس فیلترینگ نهایی در سمت کلاینت صورت گیرد. مانند مثال محاسبهی hash که در SQL تولیدی آن، خبری از قسمت where نیست و این شرط در انتهای کار، در سمت کلاینت و به صورت LINQ to Objects اعمال میشود.
ب) این قابلیت ممکن است برنامه نویسها را از تفکر در مورد یافتن روشهای محاسباتی سمت سرور دور کند. برای مثال هر چند مثال string.Join نوشته شده در سمت کلاینت محاسبه خواهد شد و این کوئری بدون مشکل اجرا میشود، اما اگر آنرا به صورت ذیل جایگزین کنیم:
اینبار به یک خروجی SQL قابل محاسبهی در سمت سرور، خواهیم رسید:
به همین جهت حداقل لاگ کردن ارزیابیهای سمت کلاینت را روشن کنید تا از وقوع یک چنین مسایلی مطلع گردید.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید: ClientSideEvaluation.zip
var blogs = from blog in Blogs where blog.Name.Contains("Development") select blog;
اما اکنون چطور؟
var blogs = from blog in Blogs where blog.Name.ComputeHash() == 0 select blog;
یک مثال: بررسی تاثیر ارزیابیهای سمت کلاینت در EF Core
فرض کنید ساختار جدول بلاگها به صورت زیر است:
public class Blog { public int BlogId { get; set; } public string Url { get; set; } }
public static class StringExtensions { public static int ComputeHash(this string str) { var hash = 0; foreach (var ch in str) { hash += (int)ch; } return hash; } }
using (var context = new BloggingContext()) { var blogs = context.Blogs .Where(blog => blog.Url.ComputeHash() >= 10) .ToList(); Console.WriteLine(blogs.First().Url); }
SELECT [blog].[BlogId], [blog].[Url] FROM [Blogs] AS [blog]
الف) مفسر LINQ در EF Core، شروع به ارزیابی کوئری نوشته شده میکند و هرجائیکه متدی را یافت و از درک آن عاجز بود (معادل SQL ایی را برای آن نیافت)، آنرا از کوئری حذف میکند.
ب) کوئری SQL نهایی بدون متد ComputeHash بر روی بانک اطلاعاتی اجرا شده و نتیجه به سمت کلاینت بازگشت داده میشود. به همین جهت است که در خروجی SQL فوق خبری از متد ComputeHash نیست.
ج) اکنون که EF Core اطلاعات لازم را از سمت سرور دریافت کردهاست، متد ComputeHash را در سمت کلاینت بر روی این نتیجهی دریافتی اعمال میکند. یعنی مرحلهی آخر همان LINQ to Objects متداول خواهد بود.
به این ترتیب است که EF Core قابلیت اجرای هر نوع متدی را که معادل SQL ایی برای آن وجود ندارد، خواهد یافت.
چگونه متوجه شویم که ارزیابی سمت کلاینت رخ دادهاست؟
EF Core این قابلیت را دارد تا گزارش کاملی را از ارزیابیهای سمت کلاینت صورت گرفته ارائه دهد. هرچند در مثال فوق متد الحاقی ComputeHash بسیار واضح است، اما برای نمونه متد string.Join نیز معادل SQL ایی ندارد:
var idUrls = context.Blogs .Select(b => new { IdUrlString = string.Join(", ", b.BlogId, b.Url), }).ToList();
public class BloggingContext : DbContext { public BloggingContext() { } public BloggingContext(DbContextOptions options) : base(options) { } public DbSet<Blog> Blogs { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (!optionsBuilder.IsConfigured) { optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Demo.ClientSideEvaluation;Trusted_Connection=True;"); optionsBuilder.ConfigureWarnings(warnings => { warnings.Log(CoreEventId.IncludeIgnoredWarning); warnings.Log(RelationalEventId.QueryClientEvaluationWarning); }); } } }
warn: Microsoft.EntityFrameworkCore.Query[200500] The LINQ expression 'where ([blog].Url.ComputeHash() >= 10)' could not be translated and will be evaluated locally.
اگر میخواهید ارزیابی سمت کلاینت را ممنوع کنید، در تنظیمات فوق warnings.Log را به warnings.Throw تغییر دهید. این مورد سبب خواهد شد تا اگر برنامه به این نوع ارزیابیها رسید، با یک استثناء متوقف شود (شبیه به حالت EF 6.x).
تاثیر ارزیابیهای سمت کلاینت بر روی کارآیی برنامه
هرچند قابلیت ارزیابیهای سمت کلاینت بسیار مفید است اما باید دقت داشت:
الف) در این حالت چون ابتدا متدهایی که قابلیت ارزیابی در سمت سرور را دارا نیستند، حذف خواهند شد، ممکن است تمام رکوردها به سمت کلاینت بازگشت داده شده و سپس فیلترینگ نهایی در سمت کلاینت صورت گیرد. مانند مثال محاسبهی hash که در SQL تولیدی آن، خبری از قسمت where نیست و این شرط در انتهای کار، در سمت کلاینت و به صورت LINQ to Objects اعمال میشود.
ب) این قابلیت ممکن است برنامه نویسها را از تفکر در مورد یافتن روشهای محاسباتی سمت سرور دور کند. برای مثال هر چند مثال string.Join نوشته شده در سمت کلاینت محاسبه خواهد شد و این کوئری بدون مشکل اجرا میشود، اما اگر آنرا به صورت ذیل جایگزین کنیم:
var idUrls2 = context.Blogs .Select(b => new { IdUrlString = b.BlogId + "," + b.Url }).ToList();
SELECT (CAST([b].[BlogId] AS nvarchar(max)) + N',') + [b].[Url] AS [IdUrlString] FROM [Blogs] AS [b]
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید: ClientSideEvaluation.zip
اشتراکها
بررسی تازههای C# 12
یک نکتهی تکمیلی
تعیین شماره نگارش IE مورد استفادهی توسط برنامه به صورت خودکار:
در اینجا شماره نگارش IE از کنترل WebBrowser دریافت میشود و همیشه به آخرین نگارش تنظیم خواهد شد و همچنین چون از Registry.CurrentUser استفاده میکند، نیازی به دسترسی مدیریتی برای اعمال ندارد.
تعیین شماره نگارش IE مورد استفادهی توسط برنامه به صورت خودکار:
using System; using System.Diagnostics; using Microsoft.Win32; using System.Windows.Forms; namespace Core { public static class UseLatestVersionOfIE { /// <summary> /// Use the latest version of IE in WebBrowser control /// </summary> public static void SetWebBrowserVersion() { RegistryKey regkey = null; try { regkey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_BROWSER_EMULATION", writable: true); if (regkey == null) { return; } var regVal = getInstalledIEVersion(); var appName = string.Format("{0}.exe", Process.GetCurrentProcess().ProcessName); regkey.SetValue(appName, regVal, RegistryValueKind.DWord); } catch (Exception ex) { // todo: log ... } finally { if (regkey != null) { regkey.Close(); } } } private static int getInstalledIEVersion() { int browserVer; using (var wb = new WebBrowser()) { browserVer = wb.Version.Major; } int regVal; if (browserVer >= 11) regVal = 11001; else switch (browserVer) { case 10: regVal = 10001; break; case 9: regVal = 9999; break; case 8: regVal = 8888; break; default: regVal = 7000; break; } return regVal; } } }
در بخش پیشین به بررسی جزئیتر ایجاد پایگاه داده و همچنین توسعه Custom Filter Attribute پرداختیم که وظیفه تایید صلاحیت کاربر جاری و بررسی دسترسی وی به API Method مورد نظر را بررسی میکرد. در این مقاله به این بحث میپردازیم که در Filter Attribute توسعه داده شده، قصد داریم یک سرویس Access Control ایجاد نماییم.
همانطور که ملاحظه میکنید، ما سه متد GetUserPermissions، GetUserRoles و HasPermission را توسعه دادهایم. حال اینکه بر حسب نیاز، میتوانید متدهای بیشتری را نیز به این سرویس اضافه نمایید. متد اول، وظیفهی واکشی تمامی permissionهای کاربر را عهده دار میباشد. متد GetUserRoles نیز تمامی نقشهای کاربر را در سیستم، بازمیگرداند و در نهایت متد سوم، همان متدی است که ما در Filterattribute از آن استفاده کردهایم. این متد با دریافت پارامترها و بازگردانی یک مقدار درست یا نادرست، تعیین میکند که کاربر جاری به آن محدوده دسترسی دارد یا خیر.
در متد فوق ما از متد سرویس Access Control که لیست تمامی permissionهای کاربر را باز میگرداند، کمک گرفتیم. متد GetUserPermissions پس از ورود کاربر توسط کلاینت فراخوانی میگردد و لیست تمامی دسترسیها در سمت کلاینت، در rootScope انگیولار ذخیره سازی میگردد. حال نوبت به آن رسیده که به بررسی عملیات سمت کلاینت بپردازیم.
اگر بخواهیم مختصری دربارهی این سرویس صحبت کنیم، متد اول که یک دستور GET ساده است و لیست دسترسیها را از PermissionController دریافت میکند. متد بعدی که در آینده بیشتر با آن آشنا میشویم، عملیات تایید صلاحیت کاربر را به ناحیه مورد نظر، انجام میدهد (همان مثال دسترسی به دکمه ویرایش مطلب در یک صفحه). در این متد برای جستجوی لیست permissions از کتابخانهای با نام Lodash کمک گرفتهایم. این کتابخانه کاری شبیه به دستورات Linq را در کالکشنها و آرایههای جاوااسکریپتی، انجام میدهد. متد some یک مقدار درست یا نادرست را بازمیگرداند. بازگردانی مقدار درست، به این معنی است که کاربر به ناحیهی مورد نظر اجازهی دسترسی را خواهد داشت.
حال تمامی اطلاعات دسترسی، در سمت کلاینت نیز قابل دسترسی میباشد. تنها کاری که نیاز است، broadCast کردن متد isAuthorize است که آن هم باید در rootScope قرار بگیرد. ما برای این انتساب یک راهکار را ارائه کردهایم. معماری سیستم کلاینت به این صورت است که تمامی کنترلرها درون یک parentController قرار گرفتهاند. از این رو میتوان در parentController این انتساب (ایجاد دسترسی عمومی برایisAuthorize) صورت گیرد. برای این کار در parentController تغییرات زیر صورت میگیرد:
در کد فوق ما isAuthorize را درون scope قرار دادهایم. دلیل آن هم این است که هر چه که در scope قرار بگیرد، تمامی کنترلرهای child نیز به آن دسترسی خواهند داشت. البته ممکن است که این بهترین نوع پیاده سازی برای به اشتراک گذاری یک منبع نباشد.
همانطور که مشاهده میکنید، تمامی المانها را میتوان با دستور ساده ng-if، از دید کاربران بدون صلاحیت، پنهان نمود. البته توجه داشته باشید که شما نمیتوانید تنها به پنهان کردن این اطلاعات اکتفا کنید. بلکه باید تمامی متدهای کنترلرهای سمت سرور را هم با همین روش (فیلتر کردن با Filter Attribute) بررسی نمایید. به ازای هر درخواست کاربر باید بررسی شود که او به منبع مورد نظر دسترسی دارد یا خیر.
این سرویس وظیفه تمامی اعمال مربوط به نقشها و دسترسیهای کاربر را بر عهده خواهد داشت. این سرویس به صورت زیر تعریف میگردد:
public class AccessControlService { private DbContext db; public AccessControlService() { db = new DbContext(); } public IEnumerable<Permission> GetUserPermissions(string userId) { var userRoles = this.GetUserRoles(userId); var userPermissions = new List<Permission>(); foreach (var userRole in userRoles) { foreach (var permission in userRole.Permissions) { // prevent duplicates if (!userPermissions.Contains(permission)) userPermissions.Add(permission); } } return userPermissions; } public IEnumerable<Role> GetUserRoles(string userId) { return db.Users.FirstOrDefault(x => x.UserId == userId).Roles.ToList(); } public bool HasPermission(string userId, string area, string control) { var found = false; var userPermissions = this.GetUserPermissions(userId); var permission = userPermissions.FirstOrDefault(x => x.Area == area && x.Control == control); if (permission != null) found = true; return found; } {
تمامی حداقلهایی که برای نگارش سمت سرور نیاز بود، به پایان رسید. حال به بررسی این موضوع خواهیم پرداخت که چگونه این سطوح دسترسی را با سمت کاربر همگام نماییم. به طوری که به عنوان مثال اگر کاربری حق دسترسی به ویرایش مطالب یک سایت را ندارد، دکمه مورد نظر که او را به آن صفحه هدایت میکند نیز نباید به وی نشان داده شود. سناریویی که ما برای این کار در نظر گرفتهایم، به این گونه میباشد که در هنگام ورود کاربر، لیستی از تمامی دسترسیهای او به صورت JSON به سمت کلاینت ارسال میگردد. حال وظیفه مدیریت نمایش یا عدم نمایش المانهای صفحه، بر عهده زبان سمت کلاینت، یعنی AngularJs خواهد بود. بنابراین در ابتداییترین حالت، ما نیاز به یک کنترلر و متد Web API داریم تا لیست دسترسیهای کاربر را بازگرداند. این کنترلر در حال حاضر شامل یک متد است. اما بر حسب نیاز، میتوانید متدهای بیشتری را به کنترلر اضافه نمایید.
[RoutePrefix("َAuth/permissions")] public class PermissionsController : ApiController { private AccessControlService _AccessControlService = null; public PermissionsController() { _AccessControlService = new AccessControlService(); } [Route("GetUserPermissions")] public async Task<IHttpActionResult> GetUserPermissions() { if (!User.Identity.IsAuthenticated) { return Unauthorized(); } return Ok(_AccessControlService.GetPermissions(User.Identity.GetCurrentUserId())); } }
توسعه سرویسها و فرآیندهای سمت وب کلاینت AngularJS
در ابتدا در سمت کلاینت نیاز به سرویسی داریم که دسترسیهای سمت سرور را دریافت نماید. از این رو ما نام این سرویس را permissionService مینامیم.
'use strict'; angular.module('App').factory('permissionService', ['$http', '$q', function ($http, $q) { var _getUserPermissions = function () { return $http.get(serviceBaseUrl + '/api/permissions/GetUserPermissions/'); } var _isAuthorize = function (area, control) { return _.some($scope.permissions, { 'area': area, 'control': control }); } return { getUserPermissions: _getUserPermissions, isAuthorize: _isAuthorize }; }]);
حال باید متدهای این سرویس را در کنترلر لاگین فراخوانی نماییم. در این مرحله ما از rootScope dependency استفاده میکنیم. برای نحوهی عملکرد rootScope میتوانید به مقالهای در این زمینه در وب سایت toddomotto مراجعه کنید. در این مقاله ویژگیها و اختلافهای scope و rootScope به تفصیل بیان شده است. مقالهای دیگر در همین زمینه نوشته شده است که در انتهای مقاله به بررسی چند نکته در مورد کدهای مشترک پرداخته شدهاست. تکه کد زیر، متد login را نمایش میدهد که در loginController قرار گرفته است و ما در آن از نوشتن کل بلاک loginController چشم پوشی کردهایم. متد savePermissions تنها یک کار را انجام میدهد و آن هم این است که در ابتدا، به سرویس permissionService متصل شده و تمامی دسترسیها را واکشی مینماید و پس از آن، آنها را درون rootScope قرار میدهد تا در تمامی کنترلرها قابل دسترسی باشد.
$scope.login = function () { authService.login($scope.loginData).then(function (response) { savePermissions(); $location.path('/userPanel'); }, function (err) { $scope.message = err.error_description; }); }; var savePermissions = function () { permissionService.getUserPermissions().then(function (response) { $rootScope.permissions = response.data; }, function (err) { }); } }
App.controller('parentController', ['$rootScope', '$scope', 'authService', 'permissionService', function ($rootScope, $scope, authService, permissionService) { $scope.authentication = authService.authentication; // isAuthorize Method $scope.isAuthorize = permissionService.isAuthorize(); // rest of codes }]);
در گام بعدی کافیست المانهای صفحه را بر اساس همین دسترسیها فعال یا غیر فعال کنیم. برای این کار از دستور ng-if میتوان استفاده کرد. برای این کار به مثال زیر توجه کنید:
<div ng-controller="childController"> <div ng-if="isAuthorize('articles', 'edit')" > <!-- the block that we want to not see unauthorize person --> </div> </div>
تنها نکتهای که باقی میماند این است که طول عمر scope و rootScope چقدر است؟! برای پاسخ به این سوال باید بگوییم هر بار که صفحه refresh میشود، تمامی مقادیر scope و rootScope خالی میشوند. برای این کار هم یک راهکار خیلی ساده (و شاید کمی ناشیانه) در نظر گرفته شدهاست. میتوان بلاک مربوط به پر کردن rootScope.permissions را که در loginController نوشته شده بود، به درون parentController انتقال داد و آن را با استفاده از emit اجرا کرد و در حالت عادی، در هنگام refresh شدن صفحه نیز چون parentController در اولین لحظه اجرا میشود، میتوان تمامی مقادیر rootScope.permissions را دوباره از سمت سرور دریافت کرد.