let alphas = ["1","2","3","4"] ; // method 1 - using splice const index = alphas.findIndex(item=>item == "3"); // or use indexOf("3") if (index !== -1) { alphas.splice(index, 1); // array.splice(start, deleteCount) } // method 2 - using filter let newAlphas = alphas.filter(item => item !=="3");
کدهای سمت سرور دریافت فایل PDF
در اینجا کدهای سمت سرور برنامه، نکتهی خاصی را به همراه نداشته و صرفا یک فایل PDF ساده (محتوای باینری) را بازگشت میدهد:
using Microsoft.AspNetCore.Mvc; namespace AngularTemplateDrivenFormsLab.Controllers { [Route("api/[controller]")] public class ReportsController : Controller { [HttpGet("[action]")] public IActionResult GetPdfReport() { return File(virtualPath: "~/assets/sample.pdf", contentType: "application/pdf", fileDownloadName: "sample.pdf"); } } }
سرویس دریافت محتوای باینری در برنامههای Angular
برای اینکه HttpClient برنامههای Angular بتواند محتوای باینری را بجای محتوای JSON پیشفرض آن دریافت کند، نیاز است نوع خروجی سمت سرور آنرا به blob تنظیم کرد:
import { Injectable } from "@angular/core"; import { Observable } from "rxjs/Observable"; import { HttpClient } from "@angular/common/http"; @Injectable() export class DownloadPdfDataService { constructor(private httpClient: HttpClient) { } public getReport(): Observable<Blob> { return this.httpClient.get("/api/Reports/GetPdfReport", { responseType: "blob" }); } }
اصلاح Content Security Policy سمت سرور جهت ارائهی محتوای blob
پس از دریافت فایل PDF به صورت یک blob، با استفاده از متد URL.createObjectURL میتوان آدرس موقت محلی را برای دسترسی به آن تولید کرد و یک چنین آدرسهایی به صورت blob:http تولید میشوند. در این حالت در Content Security Policy سمت سرور، نیاز است امکان دسترسی به تصاویر و همچنین اشیاء از نوع blob را نیز آزاد معرفی کنید:
img-src 'self' data: blob: default-src 'self' blob: object-src 'self' blob:
دریافت فایلهای PDF از سرور و نمایش آنها در یک برنامهی Angular
پس از این مقدمات، کامپوننتی که یک فایل PDF را از سمت سرور دریافت کرده و نمایش میدهد، چنین کدی را خواهد داشت:
import { DownloadPdfDataService } from "./../download-pdf-data.service"; import { WindowRefService } from "./../../core/window.service"; import { Component, OnInit } from "@angular/core"; import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser"; @Component({ templateUrl: "./view-pdf.component.html", styleUrls: ["./view-pdf.component.css"] }) export class ViewPdfComponent implements OnInit { private nativeWindow: Window; private pdfBlobUrl: string; sanitizedPdfBlobResourceUrl: SafeResourceUrl; constructor(private downloadService: DownloadPdfDataService, private windowRefService: WindowRefService, private sanitizer: DomSanitizer) { } ngOnInit() { this.nativeWindow = this.windowRefService.nativeWindow; this.downloadService.getReport().subscribe(pdfDataBlob => { console.log("pdfDataBlob", pdfDataBlob); const urlCreator = this.nativeWindow.URL; this.pdfBlobUrl = urlCreator.createObjectURL(pdfDataBlob); console.log("pdfBlobUrl", this.pdfBlobUrl); this.sanitizedPdfBlobResourceUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.pdfBlobUrl); }); } }
<h1>Display PDF Files</h1> <div *ngIf="sanitizedPdfBlobResourceUrl"> <h4>using iframe</h4> <iframe width="100%" height="600" [attr.src]="sanitizedPdfBlobResourceUrl" type="application/pdf"></iframe> <h4>using object</h4> <object [attr.data]="sanitizedPdfBlobResourceUrl" type="application/pdf" width="100%" height="100%"></object> <h4>usin embed</h4> <embed [attr.src]="sanitizedPdfBlobResourceUrl" type="application/pdf" width="100%" height="100%"> </div>
- سپس مشترک متد getReport دریافت فایل PDF شده و اطلاعات نهایی آنرا به صورت pdfDataBlob دریافت میکنیم.
- این اطلاعات باینری را به متد createObjectURL ارسال کرده و آدرس موقتی این تصویر را در مرورگر بدست میآوریم.
- چون در این حالت Angular این URL را امن سازی میکند، یک چنین خروجی unsafe:blob بجای blob تولید خواهد شد که نمایش این مورد نیز توسط مرورگر سد میشود. برای رفع این مشکل، میتوان از سرویس DomSanitizer آن که به سازندهی کلاس تزریق شدهاست استفاده کرد:
this.sanitizedPdfBlobResourceUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.pdfBlobUrl);
اینبار یک چنین انتسابی به صورت مستقیم کار میکند که سه نمونهی این انتساب را به اشیاء iframe ،object و embed، در قالب فوق مشاهده میکنید.
افزودن دکمهی چاپ PDF به برنامه
پس از اینکه به this.pdfBlobUrl دسترسی یافتیم، اکنون میتوان یک iframe مخفی را ایجاد کرد، سپس src آنرا به این آدرس ویژه تنظیم نمود و در آخر متد print آنرا فراخوانی کرد که سبب نمایش خودکار دیالوگ چاپ مرورگر میشود:
printPdf() { const iframe = document.createElement("iframe"); iframe.style.display = "none"; iframe.src = this.pdfBlobUrl; document.body.appendChild(iframe); iframe.contentWindow.print(); }
نمایش فایل PDF در یک برگهی جدید
اگر علاقمند بودید تا این فایل PDF را به صورت تمام صفحه و در برگهای جدید نمایش دهید، میتوان از متد window.open استفاده کرد:
showPdf() { this.nativeWindow.open(this.pdfBlobUrl); }
دریافت فایل PDF
بجای نمایش فایل PDF میتوان دکمهای را بر روی صفحه قرار داد که با کلیک بر روی آن، این فایل توسط مرورگر به صورت متداولی جهت دریافت به کاربر ارائه شود:
downloadPdf() { const fileName = "test.pdf"; const anchor = document.createElement("a"); anchor.style.display = "none"; anchor.href = this.pdfBlobUrl; anchor.download = fileName; document.body.appendChild(anchor); anchor.click(); }
این روش در مورد تدارک دکمهی دریافت تمام blobهای دریافتی از سرور کاربرد دارد و منحصر به فایلهای PDF نیست.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید.
Bootstrap 4 در واقع یک اقدام بزرگ بود که پس از یک سال توسعه، بزرگی این اقدام در خط به خط کدها احساس میگردد. تصمیم گرفتیم تا نسخهی اولیهی آن را به اشتراک بگذاریم و انتقادات و پیشنهادات شما را بشنویم. برای بهبود و پیشرفت در این زمینه، بسیاری از اخبار مرتبط را در اختیار شما قرار میدهیم. امیدواریم که ما را در بهتر شدن یاری کنید.
امکانات جدید Bootstrap
انتقال از Less به Sass
بهبود grid system مبتنی بر "rems"
h1 { /* 16 * 2.5 = 40px */ }
تغییر panel و wells به cards
Reset Component جایگزینی برای normalize.css
با انتخاب ok باز نیز با قالب جدیدی به شکل زیر برای انتخاب پروژه مواجه میشوید.
اینجا همه چیز تکراری است به غیر از گزینه Configure Authentication.
همهی گزینهها تکراری اند به غیر از گزینه Individual User Accounts. البته این همان FormsAuthentication قبلی است. نکته قابل توجه، یکپارچی آن با سرویسهای اجتماعی و شبکههای سرویس دهنده است. البته در نسخهی قبلی نیز این سیستم وجود داشت، ولی این دفعه با ASP.NET Identity یک پارچه است که در ادامه بیشتر آن را خواهید دید.
البته گویا حالت دیگری به نام Organizational Accounts نیز وجود دارد که گویا برای فعال سازی،باید یک بستهی به روز رسانی دریافت میکردم، که من نکردم.(اینترنت حجمی و شبانه دانلود کردن...)
این حالت که در شکل زیر مشخص است، امکان یکپارچگی احراز هویت با Active Directory در windows server و azure را دارد.
با توجه به پوشهی مدل این را متوجه میشویم که مایکروسافت هم به لزوم ViewModel اعتقاد پیدا کرده است.
با اجرا کردن پروژه bootstrap و responsive بودن آن، خودنمایی میکنند.
MVC Scaffolding #1
کل سری ASP.NET MVC
به همراه کل سری EF Code First
MVC Scaffolding چیست؟
MVC Scaffolding ابزاری است برای تولید خودکار کدهای «اولیه» برنامه، جهت بالا بردن سرعت تولید برنامههای ASP.NET MVC مبتنی بر EF Code First.
بررسی مقدماتی MVC Scaffolding
امکان اجرای ابزار MVC Scaffolding از دو طریق دستورات خط فرمان Powershell و یا صفحه دیالوگ افزودن یک کنترلر در پروژههای ASP.NET MVC وجود دارد. در ابتدا حالت ساده و ابتدایی استفاده از صفحه دیالوگ افزودن یک کنترلر را بررسی خواهیم کرد تا با کلیات این فرآیند آشنا شویم. سپس در ادامه به خط فرمان Powershell که اصل توانمندیها و قابلیتهای سفارشی MVC Scaffolding در آن قرار دارد، خواهیم پرداخت.
برای این منظور یک پروژه جدید MVC را آغاز کنید؛ ابزارهای مقدماتی MVC Scaffolding از اولین به روز رسانی ASP.NET MVC3 به بعد با VS.NET یکپارچه هستند.
ابتدا کلاس زیر را به پوشه مدلهای برنامه اضافه کنید:
using System; using System.ComponentModel; using System.ComponentModel.DataAnnotations; namespace MvcApplication1.Models { public class Task { public int Id { set; get; } [Required] public string Name { set; get; } [DisplayName("Due Date")] public DateTime? DueDate { set; get; } [DisplayName("Is Complete")] public bool IsComplete { set; get; } [StringLength(450)] public string Description { set; get; } } }
همانطور که ملاحظه میکنید در قسمت قالبها، تولید کنترلرهایی با اکشن متدهای ثبت و نمایش اطلاعات مبتنی بر EF Code First انتخاب شده است. کلاس مدل نیز به کلاس Task فوق تنظیم گردیده و در زمان انتخاب DbContext مرتبط، گزینه new data context را انتخاب کرده و نام پیش فرض آنرا پذیرفتهایم. زمانیکه بر روی دکمه Add کلیک کنیم، اتفاقات ذیل رخ خواهند داد:
الف) کنترلر جدید TasksController.cs به همراه تمام کدهای Insert/Update/Delete/Display مرتبط تولید خواهد شد.
ب) کلاس DbContext خودکاری به نام MvcApplication1Context.cs در پوشه مدلهای برنامه ایجاد میگردد تا کلاس Task را در معرض دید EF Code first قرار دهد. (همانطور که عنوان شد یکی از پیشنیازهای بحث Scaffolding آشنایی با EF Code first است)
ج) در پوشه Views\Tasks، پنج View جدید را جهت مدیریت فرآیندهای نمایش صفحات Insert، حذف، ویرایش، نمایش و غیره تهیه میکند.
د) فایل وب کانفیگ برنامه جهت درج رشته اتصالی به بانک اطلاعاتی تغییر کرده است. حالت پیش فرض آن استفاده از SQL CE است و برای استفاده از آن نیاز است قسمت 15 سری EF سایت جاری را پیشتر مطالعه کرده باشید (به چه اسمبلیهای دیگری مانند System.Data.SqlServerCe.dll برای اجرا نیاز است و چطور باید اتصال به بانک اطلاعاتی را تنظیم کرد)
معایب:
کیفیت کد تولیدی پیش فرض قابل قبول نیست:
- DbContext در سطح یک کنترلر وهله سازی شده و الگوی Context Per Request در اینجا بکارگرفته نشده است. واقعیت یک برنامه ASP.NET MVC کامل، داشتن چندین Partial View تغدیه شونده از کنترلرهای مختلف در یک صفحه واحد است. اگر قرار باشد به ازای هر کدام یکبار DbContext وهله سازی شود یعنی به ازای هر صفحه چندین بار اتصال به بانک اطلاعاتی باید برقرار شود که سربار زیادی را به همراه دارد. (قسمت 12 سری EF سایت جاری)
- اکشن متدها حاوی منطق پیاده سازی اعمال CRUD یا همان Create/Update/Delete هستند. به عبارتی از یک لایه سرویس برای خلوت کردن اکشن متدها استفاده نشده است.
- از ViewModel تعریف شدهای به نام Task هم به عنوان Domain model و هم ViewModel استفاده شده است. یک کلاس متناظر با جداول بانک اطلاعاتی میتواند شامل فیلدهای بیشتری باشد و نباید آنرا مستقیما در معرض دید یک View قرار داد (خصوصا از لحاظ مسایل امنیتی).
مزیتها:
قسمت عمدهای از کارهای «اولیه» تهیه یک کنترلر و همچنین Viewهای مرتبط به صورت خودکار انجام شدهاند. کارهای اولیهای که با هر روش و الگوی شناخته شدهای قصد پیاده سازی آنها را داشته باشید، وقت زیادی را به خود اختصاص داده و نهایتا آنچنان تفاوت عمدهای هم با کدهای تولیدی در اینجا نخواهند داشت. حداکثر فرمهای آنرا بخواهید با jQuery Ajax پیاده سازی کنید یا کنترلهای پیش فرض را با افزونههای jQuery غنی سازی نمائید. اما شروع کار و کدهای اولیه چیزی بیشتر از این نیست.
نصب بسته اصلی MVC Scaffolding توسط NuGet
بسته اصلی MVC Scaffolding را با استفاده از دستور خط فرمان Powershell ذیل، از طریق منوی Tools، گزینه Library package manager و انتخاب Package manager console میتوان به پروژه خود اضافه کرد:
Install-Package MvcScaffolding
Attempting to resolve dependency 'T4Scaffolding'. Attempting to resolve dependency 'T4Scaffolding.Core'. Attempting to resolve dependency 'EntityFramework'. Successfully installed 'T4Scaffolding.Core 1.0.0'. Successfully installed 'T4Scaffolding 1.0.8'. Successfully installed 'MvcScaffolding 1.0.9'. Successfully added 'T4Scaffolding.Core 1.0.0' to MvcApplication1. Successfully added 'T4Scaffolding 1.0.8' to MvcApplication1. Successfully added 'MvcScaffolding 1.0.9' to MvcApplication1.
پس از اینکه بسته MvcScaffolding به پروژه جاری اضافه شد، همان مراحل قبل را که توسط صفحه دیالوگ افزودن یک کنترلر انجام دادیم، اینبار به کمک دستور ذیل نیز میتوان پیاده سازی کرد:
Scaffold Controller Task
نکته و مزیت مهم دیگری که در اینجا در دسترس میباشد، سوئیچهای خط فرمانی است که به همراه صفحه دیالوگ افزودن یک کنترلر وجود ندارند. برای مثال دستور Scaffold Controller را تایپ کرده و سپس یک خط تیره را اضافه کنید. اکنون دکمه tab را مجددا بفشارید. منویی ظاهر خواهد شد که بیانگر سوئیچهای قابل استفاده است.
برای مثال اگر بخواهیم دستور Scaffold Controller Task را با جزئیات اولیه کاملتری ذکر کنیم، مانند تعیین نام دقیق کلاس مدل و کنترلر تولیدی به همراه نام دیگری برای DbContext مرتبط، خواهیم داشت:
Scaffold Controller -ModelType Task -ControllerName TasksController -DbContextType TasksDbContext
بهبود مقدماتی کیفیت کد تولیدی MVC Scaffolding
در همان کنسول پاروشل NuGet، کلید up arrow را فشار دهید تا مجددا دستور قبلی اجرا شده ظاهر شود. اینبار دستور قبلی را با سوئیچ جدید Repository (استفاده از الگوی مخزن) اجرا کنید:
Scaffold Controller -ModelType Task -ControllerName TasksController -DbContextType TasksDbContext -Repository
Scaffold Controller -ModelType Task -ControllerName TasksController -DbContextType TasksDbContext -Repository -Force
public class TasksController : Controller { private readonly ITaskRepository taskRepository; // If you are using Dependency Injection, you can delete the following constructor public TasksController() : this(new TaskRepository()) { } public TasksController(ITaskRepository taskRepository) { this.taskRepository = taskRepository; }
public interface ITaskRepository : IDisposable { IQueryable<Task> All { get; } IQueryable<Task> AllIncluding(params Expression<Func<Task, object>>[] includeProperties); Task Find(int id); void InsertOrUpdate(Task task); void Delete(int id); void Save(); }
پیاده سازی این اینترفیس در حالت متد Save آن شامل فراخوانی context.SaveChanges است. این مورد باید به الگوی واحد کار (که در اینجا تعریف نشده) منتقل شود. زیرا در یک دنیای واقعی حاصل کار بر روی چندین موجودیت باید در یک تراکنش ذخیره شوند و قرارگیری متد Save داخل کلاس مخزن یا سرویس برنامه، مخزنهای تعریف شده را تک موجودیتی میکند.
اما در کل با توجه به اینکه پیاده سازی منطق کار با موجودیتها به کلاسهای مخزن واگذار شدهاند و کنترلرها به این نحو خلوتتر گردیدهاند، یک مرحله پیشرفت محسوب میشود.
string startPath = @"c:\example\start"; string zipPath = @"c:\example\result.zip"; ZipFile.CreateFromDirectory(startPath, zipPath);
string extractPath = @"c:\example\extract"; ZipFile.ExtractToDirectory(zipPath, extractPath);
using (ZipArchive zipFile = ZipFile.Open(zipName, ZipArchiveMode.Create)) { zipFile.CreateEntryFromFile(@"C:\Temp\File1.txt", "File1.txt"); zipFile.CreateEntryFromFile(@"C:\Temp\File2.txt", "File2.txt", CompressionLevel.Fastest); }
using (MemoryStream zipStream = new MemoryStream()) { using (ZipArchive zipFile = new ZipArchive(zipStream, ZipArchiveMode.Create)) { zipFile.CreateEntryFromFile(filepath, filename); } }
using (ZipArchive archive = ZipFile.OpenRead(zipName)) { foreach (ZipArchiveEntry file in archive.Entries) { Console.WriteLine("File Name: {0}", file.Name); Console.WriteLine("File Size: {0} bytes", file.Length); Console.WriteLine("Compression Ratio: {0}", ((double)file.CompressedLength / file.Length).ToString("0.0%")); file.ExtractToFile(directorypath); } }
روشهای مختلف اطلاع رسانی به سیستم ردیابی تغییرات
متد DbSet.Add کار اطلاع رسانی تبدیل وهلههای ثبت شده را به کوئریهای Insert رکوردهای جدید، انجام میدهد:
using (var db = new BloggingContext()) { var blog = new Blog { Url = "http://sample.com" }; db.Blogs.Add(blog); db.SaveChanges(); }
سیستم ردیابی اطلاعات، اگر تغییراتی را در خواص اشیاء تحت نظر خود مشاهده کند، سبب تولید کوئریهای Update میگردد. یک چنین اشیایی تحت نظر Context هستند:
الف) اشیایی که در طول عمر Context از دیتابیس کوئری گرفته شدهاند.
ب) اشیایی که در طول عمر Context به آن اضافه شدهاند (حالت قبل).
using (var db = new BloggingContext()) { var blog = db.Blogs.First(); blog.Url = "http://sample.com/blog"; db.SaveChanges(); }
و متد DbSet.Remove کار اطلاع رسانی تبدیل وهلههای حذف شده را به کوئریهای Delete معادل، انجام میدهد:
using (var db = new BloggingContext()) { var blog = db.Blogs.First(); db.Blogs.Remove(blog); db.SaveChanges(); }
به علاوه امکان ترکیب متدهای Add، Remove و همچنین به روز رسانی اشیاء در طی یک Context و با فراخوانی یک SaveChanges در انتهای کار نیز وجود دارد. از این جهت که یک Context، الگوی واحد کار را پیاده سازی میکند و بیانگر یک تراکنش است. در این حالت ترکیبی، یا کل تراکنش با موفقیت به پایان میرسد و یا در صورت بروز مشکلی، هیچکدام از تغییرات درخواستی، اعمال نخواهند شد.
عملیات ردیابی، بر روی هر نوع Projections صورت نمیگیرد
اگر توسط LINQ Projections، نتیجهی نهایی کوئری را تغییر دادید، فقط در زمانی سیستم ردیابی بر روی آن فعال خواهد بود که projection نهایی حاوی اصل موجودیت مدنظر باشد. برای مثال در کوئری ذیل چون در Projection صورت گرفتهی در متد Select، هنوز در خاصیت Blog، به اصل موجودیت Blog اشاره میشود، نتیجهی این کوئری نیز تحت نظر سیستم ردیابی خواهد بود:
using (var context = new BloggingContext()) { var blog = context.Blogs .Select(b => new { Blog = b, Posts = b.Posts.Count() }); }
using (var context = new BloggingContext()) { var blog = context.Blogs .Select(b => new { Id = b.BlogId, Url = b.Url }); }
لغو سیستم ردیابی تغییرات، در زمانیکه به آن نیازی نیست
سیستم ردیابی تغییرات بر اساس مفاهیم AOP و تولید پروکسیهای آن کار میکند. این پروکسیها، اشیایی شفاف هستند که اشیاء شما را احاطه میکنند و هر تغییری را که اعمال میکنید، ابتدا از این غشاء رد شده و در سیستم ردیابی EF ثبت میشوند. سپس به وهلهی اصلی شیء موجود اعمال خواهند شد.
بدیهی است تولید این پروکسیها، دارای سربار است و اگر هدف شما صرفا کوئری گرفتن از اطلاعات، جهت نمایش آنها است، نیازی به تولید خودکار این پروکسیها را ندارید و این مساله سبب کاهش مصرف حافظهی برنامه و بالا رفتن سرعت آن میشود.
در قسمت قبل عنوان شد که «یک چنین اشیایی تحت نظر Context هستند: الف) اشیایی که در طول عمر Context از دیتابیس کوئری گرفته شدهاند.»
اگر میخواهید این حالت پیش فرض را لغو کنید، از متد AsNoTracking استفاده نمائید:
using (var context = new BloggingContext()) { var blogs = context.Blogs.AsNoTracking().ToList(); }
اگر میخواهید متد AsNoTracking را به صورت خودکار به تمام کوئریهای یک context خاص اعمال کنید، روش کار و تنظیم آن به صورت زیر است:
using (var context = new BloggingContext()) { context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
نکات به روز رسانی ارجاعات موجودیتها
دو حالت زیر را درنظر بگیرید که در اولی، blog از بانک اطلاعاتی واکشی شدهاست و post به صورت مستقیم وهله سازی شدهاست:
using (var context = new BloggingContext()) { var blog = context.Blogs.First(); var post = new Post { Title = "Intro to EF Core" }; blog.Posts.Add(post); context.SaveChanges(); }
using (var context = new BloggingContext()) { var blog = new Blog { Url = "http://blogs.msdn.com/visualstudio" }; var post = context.Posts.First(); blog.Posts.Add(post); context.SaveChanges(); }
در حالت دوم، ابتدا blog در بانک اطلاعاتی ثبت میشود (چون برخلاف حالت اول، تحت نظر context نیست) و سپس این post (که تحت نظر context است) به مجموعه مطالب آن اضافه میشود (بلاگ جدیدی اضافه شده و ارجاع مطلب موجودی به آن اضافه میشود).
وارد کردن یک موجودیت به سیستم ردیابی اطلاعات
در مثال قبل مشاهده کردیم که اگر موجودیتی تحت نظر context نباشد (برای مثال توسط یک کوئری به context وارد نشده باشد)، در حین ذخیره سازی ارجاعات، با آن به صورت یک وهلهی جدید رفتار شده و حتما در بانک اطلاعاتی به صورت یک رکورد جدید ذخیره میشود؛ حتی اگر Id آنرا دستی تنظیم کرده باشید که ندید گرفته خواهد شد.
اگر Id و سایر اطلاعات شیءایی را دارید، نیازی نیست تا حتما توسط یک کوئری ابتدا آنرا از بانک اطلاعاتی دریافت و سپس به صورت خودکار وارد سیستم ردیابی کنید؛ متد Attach نیز یک چنین کاری را انجام میدهد:
var blog = new Blog { Id = 2, Url = "https://www.dntips.ir" }; context.Blog.Attach(blog); context.SaveChanges();
علاوه بر متد Attach، متد AttachRange نیز برای افزودن لیستی از موجودیتها در حالت EntityState.Unchanged، پیش بینی شدهاست.
روش دیگر انجام اینکار به صورت ذیل است:
در اینجا ابتدا یک وهلهی جدید از Blog ایجاد شدهاست و سپس توسط متد Entry به Context وارد شده و همچنین حالت آن به صورت صریح، به تغییر یافته، مشخص گردیدهاست:
var blog = new Blog { Id = 2, Url = "https://www.dntips.ir" }; context.Entry(blog).State = EntityState.Modified ; context.SaveChanges();
var blog = new Blog { Id = 2, Url = "https://www.dntips.ir" }; context.Update(blog); context.SaveChanges();
به علاوه متد UpdateRange نیز برای افزودن لیستی از موجودیتها در حالت EntityState.Modified، پیش بینی شدهاست.
یک نکته: متدهای Attach و Update، هم بر روی یک DbSet و هم بر روی Context، قابل اجرا هستند. اگر بر روی Context اجرا شدند، نوع موجودیت دریافتی به نوع DbSet متناظر به صورت خودکار نگاشت شده و استفاده میشود (context.Set<T>().Attach(entity)). یعنی در حقیقت بین این دو حالت تفاوتی نیست و امکان فراخوانی این متدها بر روی Context، صرفا جهت سهولت کار درنظر گرفته شدهاست.
تفاوت رفتار context.Entry در EF Core با EF 6.x
متد context.Entry در EF 6.x هم وجود دارد. اما در EF core سبب تغییر وضعیت گراف متصل به یک شیء نمیشود و ضعیت روابط آنرا به روز رسانی نمیکند (برخلاف EF 6.x). اگر در EF Core نیاز به یک چنین به روز رسانی گراف مانندی را داشتید، باید از متد جدید context.ChangeTracker.TrackGraph به نحو ذیل استفاده نمائید:
context.ChangeTracker.TrackGraph(blog, e => e.Entry.State = EntityState.Added);
کوئری گرفتن از سیستم ردیابی اطلاعات
این سناریوها را درنظر بگیرید:
- میخواهم سیستمی شبیه به تریگرهای اس کیوال سرور را با EF داشته باشم.
- میخواهم اطلاعات تمام رکوردهای ثبت شده، حذف شده و به روز رسانی شده را لاگ کنم.
- میخواهم پس از ثبت رکوردی در هر جای برنامه، شبیه به مباحث SQL Server Service Broker و SqlDependency بلافاصله مطلع شده و توسط SignalR اطلاع رسانی کنم.
و در حالت کلی میخواهم پیش و یا پس از ثبت اطلاعات، بتوانم به تغییرات صورت گرفته دسترسی داشته باشم و عملیاتی را بر روی آنها انجام دهم. تمام این موارد و سناریوها را با کوئری گرفتن از سیستم ردیابی اطلاعات EF میتوان پیاده سازی کرد.
برای نمونه در مطلب قبل و قسمت «طراحی یک کلاس پایه، بدون تنظیمات ارث بری روابط»، یک کلاس پایه را که مقادیر پیش فرض خود را از SQL Server دریافت میکند، طراحی کردیم. در اینجا میخواهیم با استفاده از سیستم ردیابی EF، طراحی این کلاس پایه را عمومی کرده و سازگار با تمام بانکهای اطلاعاتی موجود کنیم.
جهت یادآوری، کلاس پایه موجودیتها، یک چنین شکلی را داشته:
public class BaseEntity { public int Id { set; get; } public DateTime? DateAdded { set; get; } public DateTime? DateUpdated { set; get; } }
public class Person : BaseEntity { public string FirstName { get; set; } public string LastName { get; set; } }
public class ApplicationDbContext : DbContext { // same as before public override int SaveChanges() { this.ChangeTracker.DetectChanges(); var modifiedEntries = this.ChangeTracker .Entries<BaseEntity>() .Where(x => x.State == EntityState.Modified); foreach (var modifiedEntry in modifiedEntries) { modifiedEntry.Entity.DateUpdated = DateTime.UtcNow; } var addedEntries = this.ChangeTracker .Entries<BaseEntity>() .Where(x => x.State == EntityState.Added); foreach (var addedEntry in addedEntries) { addedEntry.Entity.DateAdded = DateTime.UtcNow; } return base.SaveChanges(); } }
در اینجا کار با کوئری گرفتن از خاصیت ChangeTracker شروع میشود. سپس باید مشخص کنیم چه نوع موجودیتهایی را مدنظر داریم. چون تمام موجودیتهای ما از کلاس پایهی BaseEntity مشتق میشوند، بنابراین کوئری گرفتن بر روی این نوع، به معنای دسترسی به تمام موجودیتهای برنامه نیز هست. سپس در اینجا اگر حالتی EntityState.Modified بود، فقط مقدار خاصیت DateUpdated را به صورت خودکار مقدار دهی میکنیم و اگر حالتی EntityState.Added بود، تنها مقدار خاصیت DateAdded را به روز رسانی خواهیم کرد.
در یک چنین حالتی دیگر نیازی نیست تا مقادیر این خواص را در حین ثبت اطلاعات برنامه به صورت دستی مشخص کنیم.
یک نکته: اگر به ابتدای متد بازنویسی شده دقت کنید، فراخوانی متد this.ChangeTracker.DetectChanges در آن انجام شدهاست. علت اینجا است که این فراخوانی به صورت خودکار توسط متد base.SaveChanges انجام میشود، اما چون این مرحله را تا انتهای متد بازنویسی شده، به تاخیر انداختهایم، نیاز است خودمان به صورت دستی سبب محاسبهی مجدد تغییرات صورت گرفته شویم.
نکتهای در مورد بهبود کیفیت کدهای متد SaveChanges: استفادهی Change Tracker به این صورت با بازنویسی متد SaveChanges بسیار مرسوم است. اما پس از مدتی به متد SaveChanges ایی خواهید رسید که کنترل آن از دست خارج میشود. به همین جهت برای EF 6.x پروژههایی مانند EFHooks طراحی شدهاند تا کپسوله سازی بهتری را بتوان ارائه داد. انتقال کدهای آن به EF Core کار مشکلی نیست و اصل آن، بازنویسی HookedDbContext آن است که نحوهی مدیریت شکیلتر کوئری گرفتن از ChangeTracker را بیان میکند.
خواص سایهای یا Shadow properties
EF Core به همراه مفهوم کاملا جدیدی است به نام خواص سایهای. این نوع خواص در سمت کدهای ما و در کلاسهای موجودیتهای برنامه وجود خارجی نداشته، اما در سمت جداول بانک اطلاعاتی وجود دارند و اکنون امکان کوئری گرفتن و کار کردن با آنها در EF Core میسر شدهاست.
برای تعریف آنها، بجای افزودن خاصیتی به کلاسهای برنامه، کار از متد OnModelCreating به نحو ذیل شروع میشود:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>().Property<DateTime>("DateAdded");
سپس برای کار کردن و کوئری گرفتن از آن میتوان از متد جدید EF.Property، به نحو ذیل استفاده کرد:
var blogs = context.Blogs.OrderBy(b => EF.Property<DateTime>(b, "DateAdded"));
context.Entry(myBlog).Property("DateAdded").CurrentValue = DateTime.Now;
foreach (var addedEntry in addedEntries) { addedEntry.Property("DateAdded").CurrentValue = DateTime.UtcNow; }
پیاده سازی ساده Google Recaptcha در ASP.NET MVC
- یک چنین سطری در برنامههای ASP.NET نباید وجود داشته باشد:
if (!Task.Run(() => client.IsAcceptAsync(response, secretkey)).Result)
اگر در نگارش 5x از Task.Run استفاده کنید، سبب بروز این مشکلات خواهید شد:
- تداخل با thread pool خود ASP.NET که سبب کاهش کارآیی برنامه با مصرف تردی اضافی میشود.
- سبب اجرای قطعه کد قرار گرفتهی درون آن، خارج از request context جاری میشود.
یعنی در اینجا یک ترد جدید را ایجاد کردید و همچنین با استفاده از Result اجرای غیرهمزمان کدهای داخل آنرا سد کردید. این یک سطر باز هم غیرهمزمان اجرا میشود. بنابراین نیازی به Task.Run ندارد؛ چون با مصرف کردن یک ترد اضافی، جلوی پاسخدهی برنامه را به یک درخواست ممکن میگیرد.
همچنین بجای استفادهی از Result هم به این صورت عمل کنید:
if (!client.IsAcceptAsync(response, secretkey).GetAwaiter().GetResult())
- از قطعه کد زیر استفاده نکنید. اطلاعات بیشتر
using (var client = new HttpClient())
در قسمت مدل ابتدا یک کلاس پایه برای مدل ایجاد خواهیم کرد:
public abstract class Entity { public Guid Id { get; set; } }
public class Book : EntityBase { public string Name { get; set; } public decimal Author { get; set; } }
public class BookRepository { private readonly ConcurrentDictionary<Guid, Book> result = new ConcurrentDictionary<Guid, Book>(); public IQueryable<Book> GetAll() { return result.Values.AsQueryable(); } public Book Add(Book entity) { if (entity.Id == Guid.Empty) entity.Id = Guid.NewGuid(); if (result.ContainsKey(entity.Id)) return null; if (!result.TryAdd(entity.Id, entity)) return null; return entity; } }
نوبت به کلاس کنترلر میرسد. یک کنترلر Api به نام BooksController ایجاد کنید و سپس کدهای زیر را در آن کپی نمایید:
public class BooksController : ApiController { public static BookRepository repository = new BookRepository(); public BooksController() { repository.Add(new Book { Id=Guid.NewGuid(), Name="C#", Author="Masoud Pakdel" }); repository.Add(new Book { Id = Guid.NewGuid(), Name = "F#", Author = "Masoud Pakdel" }); repository.Add(new Book { Id = Guid.NewGuid(), Name = "TypeScript", Author = "Masoud Pakdel" }); } public IEnumerable<Book> Get() { return repository.GetAll().ToArray(); } }
در این کنترلر، اکشنی به نام Get داریم که در آن اطلاعات کتابها از Repository مربوطه برگشت داده خواهد شد. در سازنده این کنترلر ابتدا سه کتاب به صورت پیش فرض اضافه میشود و انتظار داریم که بعد از اجرای برنامه، لیست مورد نظر را مشاهده نماییم.
module Model { export class Book{ Id: string; Name: string; Author: string; } }
<div ng-controller="Books.Controller"> <table class="table table-striped table-hover" style="width: 500px;"> <thead> <tr> <th>Name</th> <th>Author</th> </tr> </thead> <tbody> <tr ng-repeat="book in books"> <td>{{book.Name}}</td> <td>{{book.Author}}</td> </tr> </tbody> </table> </div>
ابتدا یک کنترلری که به نام Controller که در ماژولی به نام Book تعریف شده است باید ایجاد شود. اطلاعات تمام کتب ثبت شده باید از سرویس مورد نظر دریافت و با یک ng-repeat در جدول نمایش داده خواهند شود.
در پوشه app یک فایل TypeScript دیگر برای تعریف برخی نیازمندیها به نام AngularModule ایجاد میکنیم که کد آن به صورت زیر خواهد بود:
declare module AngularModule { export interface HttpPromise { success(callback: Function) : HttpPromise; } export interface Http { get(url: string): HttpPromise; } }
در اینترفیس Http نیز تابعی به نام get تعریف شده است که برای دریافت اطلاعات از سرویس api، مورد استفاده قرار خواهد گرفت. از آن جا که تعریف توابع در اینترفیس فاقد بدنه است در نتیجه این جا فقط امضای توابع مشخص خواهد شد. پیاده سازی توابع به عهده کنترلرها خواهد بود:
مرحله بعد مربوط است به تعریف کنترلری به نام BookController تا اینترفیس بالا را پیاده سازی نماید. کدهای آن به صورت زیر خواهد بود:
/// <reference path='AngularModule.ts' /> /// <reference path='BookModel.ts' /> module Books { export interface Scope { books: Model.Book[]; } export class Controller { private httpService: any; constructor($scope: Scope, $http: any) { this.httpService = $http; this.getAllBooks(function (data) { $scope.books = data; }); var controller = this; } getAllBooks(successCallback: Function): void { this.httpService.get('/api/books').success(function (data, status) { successCallback(data); }); } } }
توضیح کدهای بالا:
برای دسترسی به تعاریف انجام شده در سایر ماژولها باید ارجاعی به فایل تعاریف ماژولهای مورد نظر داشته باشیم. در غیر این صورت هنگام استفاده از این ماژولها با خطای کامپایلری روبرو خواهیم شد. عملیات ارجاع به صورت زیر است:
/// <reference path='AngularModule.ts' /> /// <reference path='BookModel.ts' />
export interface Scope { books: Model.Book[]; }
در نهایت خروجی به صورت زیر خواهد بود:
سورس پیاده سازی مثال بالا در Visual Studio 2013
الف) مقیاس پذیری سمت سرور
در اعمال سمت سرور متداول، تردهای متعددی جهت پردازش درخواستهای کلاینتها تدارک دیده میشوند. هر زمانیکه یکی از این تردها، یک عملیات blocking را انجام میدهد (مانند دسترسی به شبکه یا اعمال I/O)، ترد مرتبط با آن تا پایان کار این عملیات معطل خواهد شد. با بالا رفتن تعداد کاربران یک برنامه و در نتیجه بیشتر شدن تعداد درخواستهایی که سرور باید پردازش کند، تعداد تردهای معطل مانده نیز به همین ترتیب بیشتر خواهند شد. مشکل اصلی اینجا است که نمونه سازی تردها بسیار هزینه بر است (با اختصاص 1MB of virtual memory space) و منابع سرور محدود. با زیاد شدن تعداد تردهای معطل اعمال I/O یا شبکه، سرور مجبور خواهد شد بجای استفاده مجدد از تردهای موجود، تردهای جدیدی را ایجاد کند. همین مساله سبب بالا رفتن بیش از حد مصرف منابع و حافظه برنامه میگردد. یکی از روشهای رفع این مشکل بدون نیاز به بهبودهای سخت افزاری، تبدیل اعمال blocking نامبرده شده به نمونههای non-blocking است. به این ترتیب ترد پردازش کنندهی این اعمال Async بلافاصله آزاد شده و سرور میتواند از آن جهت پردازش درخواست دیگری استفاده کند؛ بجای اینکه ترد جدیدی را وهله سازی نماید.
ب) بالا بردن پاسخ دهی کلاینتها
کلاینتها نیز اگر مدام درخواستهای blocking را به سرور جهت دریافت پاسخی ارسال کنند، به زودی به یک رابط کاربری غیرپاسخگو خواهند رسید. برای رفع این مشکل نیز میتوان از توانمندیهای Async دات نت 4.5 جهت آزاد سازی ترد اصلی برنامه یا همان ترد UI استفاده کرد.
و ... تمام اینها یک شرط را دارند. نیاز است یک چنین API خاصی که اعمال Async واقعی را پشتیبانی میکنند، فراهم شده باشد. بنابراین صرفا وجود متد Task.Run، به معنای اجرای واقعی Async یک متد خاص نیست. برای این منظور ADO.NET 4.5 به همراه متدهای Async ویژه کار با بانکهای اطلاعاتی است و پس از آن Entity framework 6 از این زیر ساخت استفاده کردهاست که در ادامه جزئیات آنرا بررسی خواهیم کرد.
پیشنیازها
برای کار با امکانات جدید Async موجود در EF 6 نیاز است از VS 2012 به بعد که به همراه کامپایلری است که واژههای کلیدی async و await را پشتیبانی میکند و همچنین دات نت 4.5 استفاده کرد. چون ADO.NET 4.5 اعمال async واقعی را پشتیبانی میکند، دات نت 4 در اینجا قابل استفاده نخواهد بود.
متدهای الحاقی جدید Async در EF 6.x
جهت متدهای الحاقی متداول EF مانند ToList، Max، Min و غیره، نمونههای Async آنها نیز اضافه شدهاند:
QueryableExtensions: AllAsync AnyAsync AverageAsync ContainsAsync CountAsync FirstAsync FirstOrDefaultAsync ForEachAsync LoadAsync LongCountAsync MaxAsync MinAsync SingleAsync SingleOrDefaultAsync SumAsync ToArrayAsync ToDictionaryAsync ToListAsync DbSet: FindAsync DbContext: SaveChangesAsync Database: ExecuteSqlCommandAsync
چند مثال
فرض کنید، مدلهای برنامه، رابطهی one-to-many ذیل را بین یک کاربر و مقالات او دارند:
public class User { public int Id { get; set; } public string Name { get; set; } public virtual ICollection<BlogPost> BlogPosts { get; set; } } public class BlogPost { public int Id { get; set; } public string Title { get; set; } public string Content { get; set; } [ForeignKey("UserId")] public virtual User User { get; set; } public int UserId { get; set; } }
public class MyContext : DbContext { public DbSet<User> Users { get; set; } public DbSet<BlogPost> BlogPosts { get; set; } public MyContext() : base("Connection1") { this.Database.Log = sql => Console.Write(sql); } }
private async Task<User> addUserAsync(CancellationToken cancellationToken = default(CancellationToken)) { using (var context = new MyContext()) { var user = context.Users.Add(new User { Name = "Vahid" }); context.BlogPosts.Add(new BlogPost { Content = "Test", Title = "Test", User = user }); await context.SaveChangesAsync(cancellationToken); return user; } }
چند نکته جهت یادآوری مباحث Async
- به امضای متد واژهی کلیدی async اضافه شدهاست، زیرا در بدنهی آن از کلمهی کلیدی await استفاده کردهایم (لازم و ملزوم هستند).
- به انتهای نام متد، کلمهی Async اضافه شدهاست. این مورد ضروری نیست؛ اما به یک استاندارد و قرارداد تبدیل شدهاست.
- مدل Async دات نت 4.5 مبتنی بر Taskها است. به همین جهت اینبار خروجیهای توابع نیاز است از نوع Task باشند و آرگومان جنریک آنها، بیانگر نوع مقداری که باز میگردانند.
- تمام متدهای الحاقی جدیدی که نامبرده شدند، دارای پارامتر اختیاری لغو عملیات نیز هستند. این مورد را با مقدار دهی cancellationToken در کدهای فوق ملاحظه میکنید.
نمونهای از نحوهی مقدار دهی این پارامتر در ASP.NET MVC به صورت زیر میتواند باشد:
[AsyncTimeout(8000)] public async Task<ActionResult> Index(CancellationToken cancellationToken)
- برای اجرا و دریافت نتیجهی متدهای Async دار EF، نیاز است از واژهی کلیدی await استفاده گردد.
استفاده کننده نیز میتواند متد addUserAsync را به صورت زیر فراخوانی کند:
var user = await addUserAsync(); Console.WriteLine("user id: {0}", user.Id);
شبیه به همین اعمال را نیز جهت به روز رسانی و یا حذف اطلاعات خواهیم داشت:
private async Task<User> updateAsync(CancellationToken cancellationToken = default(CancellationToken)) { using (var context = new MyContext()) { var user1 = await context.Users.FindAsync(cancellationToken, 1); if (user1 != null) user1.Name = "Vahid N."; await context.SaveChangesAsync(cancellationToken); return user1; } } private async Task<int> deleteAsync(CancellationToken cancellationToken = default(CancellationToken)) { using (var context = new MyContext()) { var user1 = await context.Users.FindAsync(cancellationToken, 1); if (user1 != null) context.Users.Remove(user1); return await context.SaveChangesAsync(cancellationToken); } }
کدهای Async تقلبی!
به قطعه کد ذیل دقت کنید:
public async Task<List<TEntity>> GetAllAsync() { return await Task.Run(() => _tEntities.ToList()); }
به این نوع متدها که از Task.Run برای فراخوانی متدهای همزمان قدیمی مانند ToList جهت Async جلوه دادن آنها استفاده میشود، کدهای Async تقلبی میگویند! این عملیات هر چند در یک ترد دیگر انجام میشود اما هم سربار ایجاد یک ترد جدید را به همراه دارد و هم عملیات ToList آن کاملا blocking است.
معادل صحیح Async واقعی این عملیات را در ذیل مشاهده میکنید:
private async Task<List<User>> getUsersAsync(CancellationToken cancellationToken = default(CancellationToken)) { using (var context = new MyContext()) { return await context.Users.ToListAsync(cancellationToken); } }
برای مثال پشت صحنهی متد الحاقی SaveChangesAsync به یک چنین متدی ختم میشود:
internal override async Task<long> ExecuteAsync( //... rowsAffected = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false); //...
و یا برای شبیه سازی ToListAsync با ADO.NET 4.5 و استفاده از متدهای Async واقعی آن، به یک چنین کدهایی نیاز است:
var connectionString = "........"; var sql = @"......""; var users = new List<User>(); using (var cnx = new SqlConnection(connectionString)) { using (var cmd = new SqlCommand(sql, cnx)) { await cnx.OpenAsync(); using (var reader = await cmd.ExecuteReaderAsync(CommandBehavior.CloseConnection)) { while (await reader.ReadAsync()) { var user = new User { Id = reader.GetInt32(0), Name = reader.GetString(1), }; users.Add(user); } } } }
محدودیت پردازش موازی اعمال در EF
در متد ذیل، دو Task غیرهمزمان تعریف شدهاند و سپس با await Task.WhenAll درخواست اجرای همزمان و موازی آنها را کردهایم:
// multiple operations private static async Task loadAllAsync(CancellationToken cancellationToken = default(CancellationToken)) { using (var context = new MyContext()) { var task1 = context.Users.ToListAsync(cancellationToken); var task2 = context.BlogPosts.ToListAsync(cancellationToken); await Task.WhenAll(task1, task2); // use task1.Result } }
An unhandled exception of type 'System.NotSupportedException' occurred in mscorlib.dll Additional information: A second operation started on this context before a previous asynchronous operation completed. Use 'await' to ensure that any asynchronous operations have completed before calling another method on this context. Any instance members are not guaranteed to be thread safe.