در قسمت قبلی و در این لینک، به موارد زیر پرداختیم :
در این آموزش و در ادامه پروژه قبلی عملیات CRUD (ایجاد، خواندن، به روز رسانی و حذف) را بررسی خواهیم کرد.
از scaffolding engine در ویژوال استودیو برای اضافه کردن یک کنترلر و Viewهایی استفاده می کنیم که از EF برای پرس و جو و ذخیره داده ها استفاده می کند.
ایجاد خودکار متدها و Viewهای عملیات CRUD به عنوان scaffolding شناخته می شود.
در ادامه و بصورت خودکار scaffolding engine ویژوال استودیو یک فایل CustomersController.cs و مجموعه ای از Viewها (فایل های *.cshtml) ایجاد می کند که با کنترلر کار می کنند.
به کلاس CustomersController بروید و توجه کنید که کنترلر یک ApplicationDBContext را به عنوان پارامتر سازنده/Constractor می گیرد.
تزریق وابستگی ASP.NET Core با ارسال نمونه ای از ApplicationDBContext به کنترلر انجام می شود. شما آن را در کلاس Startup قبلا و در قسمت اول پیکربندی کرده اید.
بیشتر بخوانید : اصل Dependency Inversion Principle (DIP)
بیشتر بخوانید : معرفی و بررسي IoC, DIP, DI ,IoC Container
کنترلر حاوی یک Action Method با نام Index است که لیست همه Customerها را در پایگاه داده نمایش
می دهد. این متد با خواندن ویژگی Customers از context instance پایگاه داده، لیستی از مشتریان را از مجموعه Customers دریافت می کند:
و View در مسیر Views/Students/Index.cshtml این لیست را در یک جدول نمایش می دهد:
خوب همه چیز آماده است CTRL+F5 را برای اجرای پروژه فشار دهید یا از منوی Debug > Start Without Debugging را انتخاب کنید.
در ادامه این آموزش، کد CRUD (ایجاد، خواندن، به روز رسانی و حذف) را که scaffolding engine MVC به طور خودکار برای شما در کنترلرها و نماها ایجاد می کند، بررسی و سفارشی می کنیم.
اجرای الگوی Repository به منظور ایجاد یک لایه انتزاعی بین کنترلر و لایه دسترسی به داده، یک روش معمول است. برای ساده نگه داشتن این آموزش و تمرکز بر نحوه استفاده از Entity Framework، از Repository
Pattern استفاده نمی کنیم.
بیشتر بخوانید : پیاده سازی الگوی Repository در ASP.NET Core
خوب مراحل ایجاد ایجاد controller and views را همانطور که برای موجودیت Customer گفته شد برای Product و Order نیز انجام دهید.
هنگامی که Details page مربوط به موجودیت Order را مشاهده می کنید متوجه خواهید شد scaffolding engine ویژوال استودیو OrderItem Property را کنار گذاشته است، زیرا آن ویژگی یک collection را در خود دارد.بیایید نگاهی به کد آن بیاندازیم :
در Controllers/OrdersController.cs، اکشن متد برای نمایش جزئیات از متد FirstOrDefaultAsync برای بازیابی یک موجودیت Customer استفاده می کند. آن را بصورت زیر بروز کنید :
و برای نمایش آنچه از جزییات سفارش می خواهیم ببینیم می بایست view مرتبط به آن را نیز بروز کنید. که بعنوان تمرین به خود شما واگذار می شود.
نکته : متد AsNoTracking عملکرد را در سناریوهایی بهبود میبخشد که موجودیتهای بازگشتی در طول عمر شرایط فعلی بهروزرسانی نمیشوند.
هنگامی که یک database context داده ردیفهای جدول را بازیابی میکند و اشیاء موجودیتی را ایجاد میکند که آنها را نشان میدهد، به طور پیشفرض این موضوع را دنبال میکند که آیا موجودیتهای موجود در حافظه با آنچه در پایگاه داده موجودند همگام هستند یا خیر. داده های موجود در حافظه به عنوان یک کش عمل می کند و زمانی که یک موجودیت را به روز می کنید استفاده می شود. این حافظه پنهان اغلب در یک برنامه وب غیر ضروری است زیرا context instances معمولاً کوتاه مدت هستند (یک مورد جدید برای هر درخواست ایجاد و حذف می شود) و contextای که یک موجودیت را می خواند معمولاً قبل از استفاده مجدد از آن موجودیت حذف می شود.
با فراخوانی روش AsNoTracking می توانید ردیابی اشیاء موجود در حافظه را غیرفعال کنید.
به کلاس CustomersController.cs بروید و به کد آن نگاهی بیاندازید :
متد HttpPost Create را با افزودن یک بلوک try-catch و حذف ID/CustomerID از ویژگی Bind تغییر دهید.
این کد موجودیت Customer ایجاد شده توسط Binder مدل ASP.NET Core MVC را به مجموعه موجودیت Customers اضافه می کند و سپس تغییرات را در پایگاه داده ذخیره می کند.
ما ID را از ویژگی Bind حذف کردیم زیرا ID مقدار کلید اصلی است که SQL Server به صورت خودکار هنگام درج سطر جدید تنظیم می کند. ورودی کاربر مقدار ID را تعیین نمی کند.
به غیر از ویژگی Bind، بلوک try-catch تنها تغییری است که در کد scaffolding ایجاد کردهایم. اگر در حین ذخیره تغییرات، استثنایی که از DbUpdateException مشتق شده است، گرفته شود، یک پیام خطای عمومی نمایش داده می شود. برخی اوقات استثناهای DbUpdateException به جای یک خطای برنامه نویسی به دلیل چیزی خارجی برای برنامه ایجاد می شوند، بنابراین به کاربر توصیه می شود دوباره امتحان کند. اگرچه در این نمونه پیادهسازی نشده است.
یک برنامه کاربردی واقعی، Exceptionها را را ثبت و دخیره میکند.
نکته امنیتی در مورد overposting :
ویژگی Bind که کد scaffolding در متد Create درج میکند، یکی از راههای محافظت در برابر overposting در سناریوهای Create است. برای مثال، فرض کنید موجودیت Customer شامل یک پراپرتی Secret است که نمیخواهید در فرم یک صفحه وب تنظیم و مقداردهی شود.
public class Customer { public int CustomerID { get; set; } public string Name { get; set; } public string Family { get; set; } public DateTime DateOfBirth { get; set; } public string Secret{ get; set; } }
حتی اگر یک فیلد Secret در فرم صفحه وب خود ندارید، یک هکر می تواند از ابزاری مانند Fiddler یا نوشتن مقداری جاوا اسکریپت برای ارسال مقدار فرم Secret استفاده کند.
بدون مشخصه Bind فیلدهایی را که model Binder هنگام ایجاد یک نمونه Customer استفاده می کند، آن مقدار فرم Secret را انتخاب می کند و از آن برای ایجاد نمونه موجودیت Customer استفاده می کند. سپس هر مقداری که هکر برای فیلد فرم Secret مشخص کرده باشد در پایگاه داده شما به روز می شود.
سپس مقدار "OverPost" با موفقیت به ویژگی Secret ردیف درج شده اضافه می شود، اگرچه هرگز قصد نداشتید که در فرم صفحه وب کسی بخواهد یا بتواند آن ویژگی را تنظیم کند.
شما می توانید ابتدا با خواندن موجودیت از پایگاه داده و سپس فراخوانی TryUpdateModel و ارسال در لیست پراپرتی های مجاز ، از ارسال بیش از حد/overposting در سناریوهای ویرایش جلوگیری کنید.
یک راه جایگزین برای جلوگیری از overposting که توسط بسیاری از توسعه دهندگان ترجیح داده می شود، استفاده از view model یا DTO به جای استفاده مستقیم از کلاس های موجودیت است.
فقط ویژگی هایی را که می خواهید به روز کنید در view model لحاظ کنید. هنگامی که model Binder MVC به پایان رسید، property های view model را در نمونه موجودیت کپی کنید، به طور اختیاری با استفاده از ابزاری مانند AutoMapper.
از context.Entry_ در نمونه موجودیت استفاده کنید تا وضعیت آن را روی Unchanged تنظیم کنید و سپس در هر property موجودیتی که در view model موجود است Property("PropertyName").IsModified را به true تنظیم کنید. این روش در هر دو حالت سناریو Edit و Create کار می کند.
بیشتر بخوانید : پیاده سازی object to object mapping با AutoMapper
در CustomersController.cs، متد HttpGet Edit (متدی بدون ویژگی HttpPost) از متد FirstOrDefaultAsync برای بازیابی موجودیت Customer انتخاب شده استفاده می کند، همانطور که در متد Details مشاهده کردید. شما نیازی به تغییر این متد ندارید.
اما متد HttpPost Edit را ببینید :
کد زیر را جایگزین آن کنید :
این تغییرات بهترین روش امنیتی را برای جلوگیری از overposting اعمال می کند.در کد قبلی scaffolder یک ویژگی Bind ایجاد کرد. این کد برای بسیاری از سناریوها توصیه نمی شود زیرا ویژگی Bind هرگونه داده از قبل موجود را در فیلدهایی که در پارامتر Include فهرست نشده اند پاک می کند.
کد جدید موجودیت موجود را می خواند و TryUpdateModel را برای به روز رسانی فیلدهای موجودیت بازیابی شده بر اساس ورودی کاربر در داده های فرم ارسال شده فراخوانی می کند. ردیابی خودکار تغییرات Entity Framework یا همان change tracking پرچم Modified flag را روی فیلدهایی که با ورودی فرم تغییر میکنند، تنظیم میکند. هنگامی که متد SaveChanges فراخوانی می شود، Entity Framework دستورات SQL را برای به روز رسانی ردیف پایگاه داده ایجاد می کند. تداخل همزمان(Concurrency conflicts) نادیده گرفته می شود و تنها ستون های جدولی که توسط کاربر به روز شده اند در پایگاه داده به روز می شوند.
تداخل همزمانی(Concurrency conflicts) زمانی رخ میدهد که یک کاربر دادههای موجودیت را به منظور اصلاح آن بازیابی میکند و سپس کاربر دیگری دادههای همان موجودیت را قبل از نوشته شدن اولین تغییرات کاربر در پایگاه داده بهروزرسانی میکند.
به عنوان بهترین روش برای جلوگیری از overposting ، فیلدهایی که میخواهید توسط صفحه ویرایش بهروزرسانی شوند، در پارامترهای TryUpdateModel اعلام میشوند. (رشته خالی قبل از لیست فیلدها در لیست پارامترها برای پیشوندی(prefix) است که با نام فیلدهای فرم استفاده می شود.) در حال حاضر هیچ فیلد اضافی وجود ندارد که از آن محافظت کنید، اما فیلدهایی را لیست می کنید که می خواهید model Binder به آنها متصل شود. تضمین می کند که اگر در آینده فیلدهایی را به مدل داده اضافه کنید، تا زمانی که آنها را به صراحت اینجا اضافه نکنید، به طور خودکار محافظت می شوند.
خوب database context پیگیری میکند که آیا موجودیتهای موجود در حافظه با ردیفهای مربوطه خود در پایگاه داده همگام(Sync) هستند یا خیر، و این اطلاعات تعیین میکند که با فراخوانی متد SaveChanges چه اتفاقی میافتد. به عنوان مثال، هنگامی که یک موجودیت جدید را به متد Add ارسال می کنید، وضعیت آن موجودیت روی Added تنظیم می شود. سپس هنگامی که متد SaveChanges را فراخوانی می کنید، database context دستور SQL INSERT را صادر می کند.
یک موجودیت ممکن است در یکی از حالات زیر باشد:
در یک برنامه دسکتاپ، تغییرات حالت معمولاً به طور خودکار تنظیم می شود. شما یک موجودیت را می خوانید و در برخی از مقادیر ویژگی آن تغییراتی ایجاد می کنید. این باعث می شود که حالت موجودیت آن به طور خودکار به Modified تغییر یابد. سپس وقتی SaveChanges را فرا میخوانید، Entity Framework یک عبارت SQL UPDATE ایجاد میکند که فقط ویژگیهای واقعی را که تغییر دادهاید بهروزرسانی میکند.
در یک برنامه وب، DbContext در ابتدا یک موجودیت را میخواند و دادههای آن را برای ویرایش نمایش میدهد، پس از رندر شدن صفحه حذف میشود. هنگامی که متد HttpPost Edit فراخوانی می شود، یک درخواست وب جدید ایجاد می شود و شما یک نمونه جدید از DbContext دارید. اگر موجودیت را در آن database context جدید دوباره بخوانید، پردازش دسکتاپ را شبیه سازی می کنید.
اما اگر نمی خواهید عملیات خواندن اضافی را انجام دهید، باید از شی entity ایجاد شده توسط model binder استفاده کنید. ساده ترین راه برای انجام این کار این است که وضعیت موجودیت را روی Modified تنظیم کنید، همانطور که در کد HttpPost Edit جایگزین نشان داده شده است. سپس وقتی SaveChanges را فراخوانی میکنید، Entity Framework تمام ستونهای ردیف پایگاه داده را بهروزرسانی میکند، زیرا context راهی برای دانستن اینکه کدام ویژگی را تغییر دادهاید ندارد.
اگر میخواهید از رویکرد read-first اجتناب کنید، اما همچنین میخواهید دستور SQL UPDATE فقط فیلدهایی را که کاربر واقعاً تغییر دادهاند بهروزرسانی کند، کد پیچیدهتر است. شما باید مقادیر اصلی را به طریقی ذخیره کنید (مانند استفاده از فیلدهای مخفی) تا در هنگام فراخوانی متد ویرایش HttpPost در دسترس باشند. سپس می توانید با استفاده از مقادیر اصلی یک موجودیت Customer ایجاد کنید، متد Attach را با آن نسخه اصلی موجودیت فراخوانی کنید، مقادیر موجودیت را به مقادیر جدید به روز کنید و سپس SaveChanges را فراخوانی کنید.
در CustomerController.cs :
کد برای متد HttpGet Delete از متد FirstOrDefaultAsync برای بازیابی موجودیت customer انتخاب شده استفاده می کند، همانطور که در متدهای Details و Edit مشاهده کردید. با این حال، برای پیادهسازی یک پیام خطای سفارشی زمانی که تماس SaveChanges با شکست مواجه میشود، برخی از قابلیتها را به این متد و View مربوط به آن اضافه میکنیم.
عملیات حذف به دو Action method نیاز دارد. متدی که در پاسخ به درخواست GET فراخوانی می شود، Viewایی را نمایش می دهد که به کاربر فرصتی می دهد تا عملیات حذف را تأیید یا لغو کند. اگر کاربر آن را تایید کند، یک درخواست POST ایجاد می شود. هنگامی که این اتفاق می افتد، متد HttpPost Delete فراخوانی می شود و سپس آن متد در واقع عملیات حذف را انجام می دهد.
ما یک بلوک try-catch را به متد HttpPost Delete اضافه میکنیم تا خطاهایی را که ممکن است هنگام بهروزرسانی پایگاه داده رخ دهد، مدیریت کند. اگر خطایی رخ دهد، متد HttpPost Delete متد HttpGet Delete را فراخوانی میکند و پارامتری را به آن ارسال میکند که نشان میدهد خطا رخ داده است. سپس متد HttpGet Delete صفحه تایید را همراه با پیام خطا دوباره نمایش می دهد و به کاربر فرصت می دهد تا آن را لغو کند یا دوباره امتحان کند.
خوب Action method حذف HttpGet را با کد زیر جایگزین کنید، که گزارش خطا را مدیریت می کند.
بعد Action method حذف HttpPost (به نام DeleteConfirmed) را با کد زیر جایگزین کنید، که عملیات حذف واقعی را انجام می دهد و هر گونه خطای به روز رسانی پایگاه داده را می گیرد.
این کد موجودیت انتخاب شده را بازیابی می کند، سپس متد Remove را فراخوانی می کند تا وضعیت موجودیت را روی Deleted تنظیم کند. هنگامی که SaveChanges فراخوانی می شود، یک دستور SQL DELETE تولید می شود.
برای آزاد کردن منابعی که اتصال پایگاه داده در اختیار دارد، پس از اتمام کار، context instance باید در اسرع وقت حذف شود. تزریق وابستگی داخلی ASP.NET Core این کار را برای شما انجام می دهد.
در Program.cs، شما متد افزونه AddDbContext را برای ارائه کلاس DbContext در کانتینر ASP.NET Core DI فراخوانی می کنید.
builder.Services.AddDbContext<ApplicationDBContext>(options => { options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"), b => b.MigrationsAssembly(typeof(ApplicationDBContext).Assembly.FullName)); });
این متد طول عمر سرویس را به طور پیش فرض بر روی Scoped تنظیم می کند. Scoped به این معنی است که طول عمر شی Contextبا طول عمر درخواست وب منطبق است و متد Dispose به طور خودکار در پایان درخواست وب فراخوانی می شود.
بیشتر بخوانید : بررسی طول عمر سرویس Transient, Singleton , Scoped
به طور پیش فرض Entity Framework به طور ضمنی تراکنش ها را پیاده سازی می کند. در سناریوهایی که در چندین ردیف یا جدول تغییراتی ایجاد میکنید و سپس SaveChanges را فراخوانی میکنید، Entity Framework به طور خودکار مطمئن میشود که یا همه تغییرات شما موفقیتآمیز هستند یا همه آنها با شکست مواجه میشوند. اگر ابتدا برخی تغییرات انجام شود و سپس خطایی رخ دهد، آن تغییرات به طور خودکار برگردانده می شوند.
بیشتر بخوانید : دوره آموزشی Entity FrameWork Core - قسمت سوم
بیشتر بخوانید : دوره آموزشی Entity FrameWork Core
بیشتر بخوانید : نقشه راه توسعه دهندگان Asp.NET Core