SPList list = mWeb.GetList(strUrl); if (list != null) { for (int i = list.ItemCount - 1; i >= 0; i--) { list.Items[i].Delete(); } list.Update(); }
public static void DeleteAllItems(string site, string list) { using (SPSite spSite = new SPSite(site)) { using (SPWeb spWeb = spSite.OpenWeb()) { StringBuilder deletebuilder = BatchCommand(spWeb.Lists[list]); spSite.RootWeb.ProcessBatchData(deletebuilder.ToString()); } } } private static StringBuilder BatchCommand(SPList spList) { StringBuilder deletebuilder= new StringBuilder(); deletebuilder.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?><Batch>"); string command = "<Method><SetList Scope=\"Request\">" + spList.ID + "</SetList><SetVar Name=\"ID\">{0}</SetVar><SetVar Name=\"Cmd\">Delete</SetVar></Method>"; foreach (SPListItem item in spList.Items) { deletebuilder.Append(string.Format(command, item.ID.ToString())); } deletebuilder.Append("</Batch>"); return deletebuilder; }
// We prepare a String.Format with a String.Format, this is why we have a {{0}} string command = String.Format("<Method><SetList Scope=\"Request\">{0}</SetList><SetVar Name=\"ID\">{{0}}</SetVar><SetVar Name=\"Cmd\">Delete</SetVar><SetVar Name=\"owsfileref\">{{1}}</SetVar></Method>", list.ID); // We get everything but we limit the result to 100 rows SPQuery q = new SPQuery(); q.RowLimit = 100; // While there's something left while (list.ItemCount > 0) { // We get the results SPListItemCollection coll = list.GetItems(q); StringBuilder sbDelete = new StringBuilder(); sbDelete.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?><Batch>"); Guid[] ids = new Guid[coll.Count]; for (int i=0;i<coll.Count;i++) { SPListItem item = coll[i]; sbDelete.Append(string.Format(command, item.ID.ToString(), item.File.ServerRelativeUrl)); ids[i] = item.UniqueId; } sbDelete.Append("</Batch>"); // We execute it web.ProcessBatchData(sbDelete.ToString()); //We remove items from recyclebin web.RecycleBin.Delete(ids); list.Update(); } }
- ایجاد متغیرها به سادگی با شروع نوشتن نام متغیر با $ و بدون تعریف نوع آنها انجام میشود
- write-host حکم write را دارد و واضح است که نوشتن تنهای آن برای ایجاد یک line break میباشد.
- کامنت کردن با #
- عدم وجود semi colon برای اتمام فرامین
[System.Reflection.Assembly]::Load("Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c") [System.Reflection.Assembly]::Load("Microsoft.SharePoint.Portal, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c") [System.Reflection.Assembly]::Load("Microsoft.SharePoint.Publishing, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c") [System.Reflection.Assembly]::Load("System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a") write-host # Enter your configuration here $siteUrl = "http://mysharepointsite.example.com/" $listName = "Name of my list" $batchSize = 1000 write-host "Opening web at $siteUrl..." $site = new-object Microsoft.SharePoint.SPSite($siteUrl) $web = $site.OpenWeb() write-host "Web is: $($web.Title)" $list = $web.Lists[$listName]; write-host "List is: $($list.Title)" while ($list.ItemCount -gt 0) { write-host "Item count: $($list.ItemCount)" $batch = "<?xml version=`"1.0`" encoding=`"UTF-8`"?><Batch>" $i = 0 foreach ($item in $list.Items) { $i++ write-host "`rProcessing ID: $($item.ID) ($i of $batchSize)" -nonewline $batch += "<Method><SetList Scope=`"Request`">$($list.ID)</SetList><SetVar Name=`"ID`">$($item.ID)</SetVar><SetVar Name=`"Cmd`">Delete</SetVar><SetVar Name=`"owsfileref`">$($item.File.ServerRelativeUrl)</SetVar></Method>" if ($i -ge $batchSize) { break } } $batch += "</Batch>" write-host write-host "Sending batch..." # We execute it $result = $web.ProcessBatchData($batch) write-host "Emptying Recycle Bin..." # We remove items from recyclebin $web.RecycleBin.DeleteAll() write-host $list.Update() } write-host "Done."
Kendo UI MVVM
- «استفاده از Kendo UI templates »
- «اعتبار سنجی ورودیهای کاربر در Kendo UI»
- «فعال سازی عملیات CRUD در Kendo UI Grid» جهت آشنایی با نحوهی تعریف DataSource ایی که میتواند اطلاعات را ثبت، حذف و یا ویرایش کند.
در این مطلب قصد داریم به یک چنین صفحهای برسیم که در آن در ابتدای نمایش، لیست ثبت نامهای موجود، از سرور دریافت و توسط یک Kendo UI template نمایش داده میشود. سپس امکان ویرایش و حذف هر ردیف، وجود خواهد داشت، به همراه امکان افزودن ردیفهای جدید. در این بین مدیریت نمایش لیست ثبت نامها توسط امکانات binding توکار فریم ورک MVVM مخصوص Kendo UI صورت خواهد گرفت. همچنین کلیه اعمال مرتبط با هر ردیف نیز توسط data binding دو طرفه مدیریت خواهد شد.
Kendo UI MVVM
الگوی MVVM یا Model-View-ViewModel که برای اولین بار جهت کاربردهای WPF و Silverlight معرفی شد، برای ساده سازی اتصال تغییرات کنترلهای برنامه به خواص ViewModel یک View کاربرد دارد. برای مثال با تغییر عنصر انتخابی یک DropDownList در یک View، بلافاصله خاصیت متصل به آن که در ViewModel برنامه تعریف شدهاست، مقدار دهی و به روز خواهد شد. هدف نهایی آن نیز جدا سازی منطق کدهای UI، از کدهای جاوا اسکریپتی سمت کاربر است. برای این منظور کتابخانههایی مانند Knockout.js به صورت اختصاصی برای این کار تهیه شدهاند؛ اما Kendo UI نیز جهت یکپارچگی هرچه تمامتر اجزای آن، دارای یک فریم ورک MVVM توکار نیز میباشد. طراحی آن نیز بسیار شبیه به Knockout.js است؛ اما با سازگاری 100 درصد با کل مجموعه.
پیاده سازی الگوی MVVM از 4 قسمت تشکیل میشود:
- Model که بیانگر خواص متناظر با اشیاء رابط کاربری است.
- View همان رابط کاربری است که به کاربر نمایش داده میشود.
- ViewModel واسطی است بین Model و View. کار آن انتقال دادهها و رویدادها از View به مدل است و در حالت binding دوطرفه، عکس آن نیز صحیح میباشد.
- Declarative data binding جهت رهایی برنامه نویسها از نوشتن کدهای هماهنگ سازی اطلاعات المانهای View و خواص ViewModel کاربرد دارد.
در ادامه این اجزا را با پیاده سازی مثالی که در ابتدای بحث مطرح شد، دنبال میکنیم.
تعریف Model و ViewModel
در سمت سرور، مدل ثبت نام برنامه چنین شکلی را دارد:
namespace KendoUI07.Models { public class Registration { public int Id { set; get; } public string UserName { set; get; } public string CourseName { set; get; } public int Credit { set; get; } public string Email { set; get; } public string Tel { set; get; } } }
<script type="text/javascript"> $(function () { var model = kendo.data.Model.define({ id: "Id", fields: { Id: { type: 'number' }, // leave this set to 0 or undefined, so Kendo knows it is new. UserName: { type: 'string' }, CourseName: { type: 'string' }, Credit: { type: 'number' }, Email: { type: 'string' }, Tel: { type: 'string' } } }); }); </script>
<script type="text/javascript"> $(function () { var viewModel = kendo.observable({ accepted: false, course: new model() }); }); </script>
اتصال ViewModel به View برنامه
تعریف فرم ثبت نام را در اینجا ملاحظه میکنید. فیلدهای مختلف آن بر اساس نکات اعتبارسنجی HTML 5 با ویژگیهای خاص آن، مزین شدهاند. جزئیات آنرا در مطلب «اعتبار سنجی ورودیهای کاربر در Kendo UI» پیشتر بررسی کردهایم.
اگر به تعریف هر فیلد دقت کنید، ویژگی data-bind جدیدی را هم ملاحظه خواهید کرد:
<div id="coursesSection" class="k-rtl k-header"> <div class="box-col"> <form id="myForm" data-role="validator" novalidate="novalidate"> <h3>ثبت نام</h3> <ul> <li> <label for="Id">Id</label> <span id="Id" data-bind="text:course.Id"></span> </li> <li> <label for="UserName">نام</label> <input type="text" id="UserName" name="UserName" class="k-textbox" data-bind="value:course.UserName" required /> </li> <li> <label for="CourseName">دوره</label> <input type="text" dir="ltr" id="CourseName" name="CourseName" required data-bind="value:course.CourseName" /> <span class="k-invalid-msg" data-for="CourseName"></span> </li> <li> <label for="Credit">مبلغ پرداختی</label> <input id="Credit" name="Credit" type="number" min="1000" max="6000" required data-max-msg="عددی بین 1000 و 6000" dir="ltr" data-bind="value:course.Credit" class="k-textbox k-input" /> <span class="k-invalid-msg" data-for="Credit"></span> </li> <li> <label for="Email">پست الکترونیک</label> <input type="email" id="Email" dir="ltr" name="Email" data-bind="value:course.Email" required class="k-textbox" /> </li> <li> <label for="Tel">تلفن</label> <input type="tel" id="Tel" name="Tel" dir="ltr" pattern="\d{8}" required class="k-textbox" data-bind="value:course.Tel" data-pattern-msg="8 رقم" /> </li> <li> <input type="checkbox" name="Accept" data-bind="checked:accepted" required /> شرایط دوره را قبول دارم. <span class="k-invalid-msg" data-for="Accept"></span> </li> <li> <button class="k-button" data-bind="enabled: accepted, click: doSave" type="submit"> ارسال </button> <button class="k-button" data-bind="click: resetModel">از نو</button> </li> </ul> <span id="doneMsg"></span> </form> </div>
<script type="text/javascript"> $(function () { var model = kendo.data.Model.define({ // ... }); var viewModel = kendo.observable({ // ... }); kendo.bind($("#coursesSection"), viewModel); }); </script>
<input type="text" id="UserName" name="UserName" class="k-textbox" data-bind="value:course.UserName" required />
بنابراین تا اینجا به صورت خلاصه، مدلی را توسط متد kendo.data.Model.define، معادل مدل سمت سرور خود ایجاد کردیم. سپس وهلهای از این مدل را به صورت یک خاصیت جدید دلخواهی در ViewModel تعریف شده توسط متد kendo.observable در معرض دید View برنامه قرار دادیم. در ادامه اتصال ViewModel و View، با فراخوانی متد kendo.bind انجام شد. اکنون برای دریافت تغییرات کنترلهای برنامه، تنها کافی است ویژگیهای data-bind ایی را به آنها اضافه کنیم.
در ناحیهی تعریف شده توسط متد kendo.bind، کلیه خواص ViewModel در دسترس هستند. برای مثال اگر به تعریف ViewModel دقت کنید، یک خاصیت دیگر به نام accepted با مقدار false نیز در آن تعریف شدهاست (این خاصیت چون صرفا کاربرد UI داشت، در model برنامه قرار نگرفت). از آن برای اتصال checkbox تعریف شده، به button ارسال اطلاعات، استفاده کردهایم:
<input type="checkbox" name="Accept" data-bind="checked:accepted" required /> <button class="k-button" data-bind="enabled: accepted, click: doSave" type="submit"> ارسال </button>
ارسال دادههای تغییر کردهی ViewModel به سرور
تا اینجا 4 جزء اصلی الگوی MVVM که در ابتدای بحث عنوان شد، تکمیل شدهاند. مدل اطلاعات فرم تعریف گردید. ViewModel ایی که این خواص را به المانهای فرم متصل میکند نیز در ادامه اضافه شدهاست. توسط ویژگیهای data-bind کار Declarative data binding انجام میشود.
در ادامه نیاز است تغییرات ViewModel را به سرور، جهت ثبت، به روز رسانی و حذف نهایی منتقل کرد.
<script type="text/javascript"> $(function () { var model = kendo.data.Model.define({ //... }); var dataSource = new kendo.data.DataSource({ type: 'json', transport: { read: { url: "api/registrations", dataType: "json", contentType: 'application/json; charset=utf-8', type: 'GET' }, create: { url: "api/registrations", contentType: 'application/json; charset=utf-8', type: "POST" }, update: { url: function (course) { return "api/registrations/" + course.Id; }, contentType: 'application/json; charset=utf-8', type: "PUT" }, destroy: { url: function (course) { return "api/registrations/" + course.Id; }, contentType: 'application/json; charset=utf-8', type: "DELETE" }, parameterMap: function (data, type) { // Convert to a JSON string. Without this step your content will be form encoded. return JSON.stringify(data); } }, schema: { model: model }, error: function (e) { alert(e.errorThrown); }, change: function (e) { // فراخوانی در زمان دریافت اطلاعات از سرور و یا تغییرات محلی viewModel.set("coursesDataSourceRows", new kendo.data.ObservableArray(this.view())); } }); var viewModel = kendo.observable({ //... }); kendo.bind($("#coursesSection"), viewModel); dataSource.read(); // دریافت لیست موجود از سرور در آغاز کار }); </script>
متصل کردن DataSource به ViewModel
تا اینجا DataSource ایی جهت کار با سرور تعریف شدهاست؛ اما مشخص نیست که اگر رکوردی اضافه شد، چگونه باید اطلاعات خودش را به روز کند. برای این منظور خواهیم داشت:
<script type="text/javascript"> $(function () { $("#coursesSection").kendoValidator({ // ... }); var model = kendo.data.Model.define({ // ... }); var dataSource = new kendo.data.DataSource({ // ... }); var viewModel = kendo.observable({ accepted: false, course: new model(), doSave: function (e) { e.preventDefault(); console.log("this", this.course); var validator = $("#coursesSection").data("kendoValidator"); if (validator.validate()) { if (this.course.Id == 0) { dataSource.add(this.course); } dataSource.sync(); // push to the server this.set("course", new model()); // reset controls } }, resetModel: function (e) { e.preventDefault(); this.set("course", new model()); } }); kendo.bind($("#coursesSection"), viewModel); dataSource.read(); // دریافت لیست موجود از سرور در آغاز کار }); </script>
در متد doSave، ابتدا بررسی میکنیم آیا اعتبارسنجی فرم با موفقیت انجام شدهاست یا خیر. اگر بله، توسط متد add منبع داده، اطلاعات فرم جاری را توسط شیء course که هم اکنون به تمامی فیلدهای آن متصل است، اضافه میکنیم. در اینجا بررسی شدهاست که آیا Id این اطلاعات صفر است یا خیر. از آنجائیکه از همین متد برای به روز رسانی نیز در ادامه استفاده خواهد شد، در حالت به روز رسانی، Id شیء ثبت شده، از طرف سرور دریافت میگردد. بنابراین غیر صفر بودن این Id به معنای عملیات به روز رسانی است و در این حالت نیازی نیست کار بیشتری را انجام داد؛ زیرا شیء متناظر با آن پیشتر به منبع داده اضافه شدهاست.
استفاده از متد add صرفا به معنای مطلع کردن منبع داده محلی از وجود رکوردی جدید است. برای ارسال این تغییرات به سرور، از متد sync آن میتوان استفاده کرد. متد sync بر اساس متد add یک درخواست POST، بر اساس شیءایی که Id غیر صفر دارد، یک درخواست PUT و با فراخوانی متد remove بر روی منبع داده، یک درخواست DELETE را به سمت سرور ارسال میکند.
متد دلخواه resetModel سبب مقدار دهی مجدد شیء course با یک وهلهی جدید از شیء model میشود. همینقدر برای پاک کردن تمامی کنترلهای صفحه کافی است.
تا اینجا دو متد جدید را در ViewModel برنامه تعریف کردهایم. در مورد نحوهی اتصال آنها به View، به کدهای دو دکمهی موجود در فرم دقت کنید:
<button class="k-button" data-bind="enabled: accepted, click: doSave" type="submit"> ارسال </button> <button class="k-button" data-bind="click: resetModel">از نو</button>
مدیریت سمت سرور ثبت، ویرایش و حذف اطلاعات
در حالت ثبت، متد Post توسط آدرس مشخص شده در قسمت create منبع داده، فراخوانی میگردد. نکتهی مهمی که در اینجا باید به آن دقت داشت، نحوهی بازگشت Id رکورد جدید ثبت شدهاست. اگر این تنظیم صورت نگیرد، Id رکورد جدید را در لیست، مساوی صفر مشاهده خواهید کرد و منبع داده این رکورد را همواره به عنوان یک رکورد جدید، مجددا به سرور ارسال میکند.
using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Web.Http; using KendoUI07.Models; namespace KendoUI07.Controllers { public class RegistrationsController : ApiController { public HttpResponseMessage Delete(int id) { var item = RegistrationsDataSource.LatestRegistrations.FirstOrDefault(x => x.Id == id); if (item == null) return Request.CreateResponse(HttpStatusCode.NotFound); RegistrationsDataSource.LatestRegistrations.Remove(item); return Request.CreateResponse(HttpStatusCode.OK, item); } public IEnumerable<Registration> Get() { return RegistrationsDataSource.LatestRegistrations; } public HttpResponseMessage Post(Registration registration) { if (!ModelState.IsValid) return Request.CreateResponse(HttpStatusCode.BadRequest); var id = 1; var lastItem = RegistrationsDataSource.LatestRegistrations.LastOrDefault(); if (lastItem != null) { id = lastItem.Id + 1; } registration.Id = id; RegistrationsDataSource.LatestRegistrations.Add(registration); // ارسال آی دی مهم است تا از ارسال رکوردهای تکراری جلوگیری شود return Request.CreateResponse(HttpStatusCode.Created, registration); } [HttpPut] // Add it to fix this error: The requested resource does not support http method 'PUT' public HttpResponseMessage Update(int id, Registration registration) { var item = RegistrationsDataSource.LatestRegistrations .Select( (prod, index) => new { Item = prod, Index = index }) .FirstOrDefault(x => x.Item.Id == id); if (item == null) return Request.CreateResponse(HttpStatusCode.NotFound); if (!ModelState.IsValid || id != registration.Id) return Request.CreateResponse(HttpStatusCode.BadRequest); RegistrationsDataSource.LatestRegistrations[item.Index] = registration; return Request.CreateResponse(HttpStatusCode.OK); } } }
نمایش آنی اطلاعات ثبت شده در یک لیست
ردیفهای اضافه شده به منبع داده را میتوان بلافاصله در همان سمت کلاینت توسط Kendo UI Template که قابلیت کار با ViewModelها را دارد، نمایش داد:
<div id="coursesSection" class="k-rtl k-header"> <div class="box-col"> <form id="myForm" data-role="validator" novalidate="novalidate"> <!--فرم بحث شده در ابتدای مطلب--> </form> </div> <div id="results"> <table class="metrotable"> <thead> <tr> <th>Id</th> <th>نام</th> <th>دوره</th> <th>هزینه</th> <th>ایمیل</th> <th>تلفن</th> <th></th> <th></th> </tr> </thead> <tbody data-template="row-template" data-bind="source: coursesDataSourceRows"></tbody> <tfoot data-template="footer-template" data-bind="source: this"></tfoot> </table> <script id="row-template" type="text/x-kendo-template"> <tr> <td data-bind="text: Id"></td> <td data-bind="text: UserName"></td> <td dir="ltr" data-bind="text: CourseName"></td> <td> #: kendo.toString(get("Credit"), "c0") # </td> <td data-bind="text: Email"></td> <td data-bind="text: Tel"></td> <td><button class="k-button" data-bind="click: deleteCourse">حذف</button></td> <td><button class="k-button" data-bind="click: editCourse">ویرایش</button></td> </tr> </script> <script id="footer-template" type="text/x-kendo-template"> <tr> <td colspan="3"></td> <td> جمع کل: #: kendo.toString(totalPrice(), "c0") # </td> <td colspan="2"></td> <td></td> <td></td> </tr> </script> </div> </div>
<script type="text/javascript"> $(function () { // ... var viewModel = kendo.observable({ accepted: false, course: new model(), coursesDataSourceRows: new kendo.data.ObservableArray([]), doSave: function (e) { // ... }, resetModel: function (e) { // ... }, totalPrice: function () { var sum = 0; $.each(this.get("coursesDataSourceRows"), function (index, item) { sum += item.Credit; }); return sum; }, deleteCourse: function (e) { // the current data item is passed as the "data" field of the event argument var course = e.data; dataSource.remove(course); dataSource.sync(); // push to the server }, editCourse: function(e) { // the current data item is passed as the "data" field of the event argument var course = e.data; this.set("course", course); } }); kendo.bind($("#coursesSection"), viewModel); dataSource.read(); // دریافت لیست موجود از سرور در آغاز کار }); </script>
- ابتدا خاصیت دلخواه coursesDataSourceRows به viewModel اضافه میشود تا در ناحیهی coursesSection در دسترس قرار گیرد.
- سپس اگر به انتهای تعریف DataSource دقت کنید، داریم:
<script type="text/javascript"> $(function () { var dataSource = new kendo.data.DataSource({ //... change: function (e) { // فراخوانی در زمان دریافت اطلاعات از سرور و یا تغییرات محلی viewModel.set("coursesDataSourceRows", new kendo.data.ObservableArray(this.view())); } }); }); </script>
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید:
KendoUI07.zip
ASP.NET MVC #8
public static MvcHtmlString MyImage2(this HtmlHelper helper, string src, string alt, int width, int height) { var img=new TagBuilder("img"); img.MergeAttribute("src", VirtualPathUtility.ToAbsolute(src)); img.MergeAttribute("alt", alt); img.MergeAttribute("width", width.ToString()); img.MergeAttribute("height", height.ToString()); return MvcHtmlString.Create(img.ToString()); }
آیا استفاده از VirtualPathUtility.ToAbsolute(src) برای بدست آوردن آدرس تصویر که در پوشه content قرار دارد روش درستی است ؟
من جواب گرفتم .ولی میخواستم بدون آیا میشه بهتر از این عمل کرد در کلاس helper
متدهای کمکی مفید در پروژه های asp.net mvc
public static string RemoveAllHtmlTags(this string text) { var withoutHtml = String.IsNullOrEmpty(text) ? String.Empty : Regex.Replace(text, @"<(.|\n)*?>", String.Empty); withoutHtml = withoutHtml.Replace(" ", " "); withoutHtml = withoutHtml.Replace("‌", " "); withoutHtml = withoutHtml.Replace(""", " "); withoutHtml = withoutHtml.Replace("amp;", ""); withoutHtml = withoutHtml.Replace("«", "«"); withoutHtml = withoutHtml.Replace("»", "»"); return withoutHtml; }
MVC Scaffolding #3
آشنایی با ساختار اصلی MVC Scaffolding
پس از نصب MVC Scaffolding از طریق NuGet به پوشه Packages مراجعه نمائید. در اینجا پوشههای MvcScaffolding، T4Scaffolding و T4Scaffolding.Core ساختار اصلی این بسته را تشکیل میدهند. برای نمونه اگر پوشه T4Scaffolding\tools را باز کنیم، شاهد تعدادی فایل ps1 خواهیم بود که همان فایلهای پاورشل هستند. مطابق طراحی NuGet، همواره فایلی با نام init.ps1 در ابتدا اجرا خواهد شد. همچنین در اینجا پوشههای T4Scaffolding\tools\EFRepository و T4Scaffolding\tools\EFDbContext نیز قرار دارند که حاوی قالبهای اولیه کدهای مرتبط با الگوی مخزن و DbContext تولیدی میباشند.
در پوشه MvcScaffolding\tools، ساختار قالبهای پیش فرض تولید Viewها و کنترلرهای تولیدی قرار دارند. در اینجا به ازای هر مورد، دو نگارش vb و cs قابل مشاهده است.
سفارشی سازی قالبهای پیش فرض Viewهای MVC Scaffolding
برای سفارشی سازی قالبهای پیش فرض از دستور کلی زیر استفاده میشود:
Scaffold CustomTemplate Name Template
Scaffold CustomTemplate View Index
اگر دستور فوق را اجرا کنیم، فایل جدیدی به نام CodeTemplates\Scaffolders\MvcScaffolding.RazorView\Index.cs.t4 به پروژه جاری اضافه میشود. از این پس کلیه فرامین اجرایی، از نسخه محلی فوق بجای نمونههای پیش فرض استفاده خواهند کرد.
در ادامه قصد داریم اندکی این قالب پیش فرض را جهت اعمال ویژگی DisplayName به هدر جدول تولیدی نمایش اطلاعات Tasks تغییر دهیم. در کلاس Task، خاصیت زمان موعود با ویژگی DisplayName مزین شده است. این نام نمایشی حین تولید فرمهای ثبت و ویرایش اطلاعات بکار گرفته میشود، اما در زمان تولید جدول اطلاعات ثبت شده، به هدر جدول اعمال نمیگردد.
[DisplayName("Due Date")] public DateTime? DueDate { set; get; }
// Describes the information about a property on the model class ModelProperty { public string Name { get; set; } public string DisplayName { get; set; } public string ValueExpression { get; set; } public EnvDTE.CodeTypeRef Type { get; set; } public bool IsPrimaryKey { get; set; } public bool IsForeignKey { get; set; } public bool IsReadOnly { get; set; } }
static string GetDisplayName(EnvDTE.CodeProperty prop) { var displayAttr = prop.Attributes.OfType<EnvDTE80.CodeAttribute2>().Where(x => x.FullName == typeof(System.ComponentModel.DisplayNameAttribute).FullName).FirstOrDefault(); if(displayAttr == null) { return prop.Name; } return displayAttr.Value.Replace("\"",""); }
اکنون برای اعمال متد GetDisplayName، متد GetEligibleProperties را یافته و به نحو زیر تغییر دهید:
results.Add(new ModelProperty { Name = prop.Name, DisplayName = GetDisplayName(prop), ValueExpression = "Model." + prop.Name, Type = prop.Type, IsPrimaryKey = Model.PrimaryKeyName == prop.Name, IsForeignKey = ParentRelations.Any(x => x.RelationProperty == prop), IsReadOnly = !prop.IsWriteable() });
اکنون قسمت هدر جدول تولیدی را در ابتدای فایل t4 یافته و به نحو زیر تغییر میدهیم تا از DisplayName استفاده کند:
<# List<ModelProperty> properties = GetModelProperties(Model.ViewDataType, true); foreach (ModelProperty property in properties) { if (!property.IsPrimaryKey && !property.IsForeignKey) { #> <th> <#= property.DisplayName #> </th> <# } } #>
PM> Scaffold Controller -ModelType Task -ControllerName TasksController -DbContextType TasksDbContext -Repository -Force
سفارشی سازی قالبهای پیش فرض کنترلرهای MVC Scaffolding
در ادامه قصد داریم کدهای الگوی مخزن تهیه شده را اندکی تغییر دهیم. برای مثال با توجه به اینکه از تزریق وابستگیها استفاده خواهیم کرد، نیازی به سازنده اولیه پیش فرض کنترلر که در بالای آن ذکر شده «در صورت استفاده از یک DI این مورد را حذف کنید»، نداریم. برای این منظور دستور زیر را اجرا کنید:
PM> Scaffold CustomTemplate Controller ControllerWithRepository
به این ترتیب فایل جدید CodeTemplates\Scaffolders\MvcScaffolding.Controller\ControllerWithRepository.cs.t4 به پروژه جاری اضافه خواهد شد. در این فایل چند سطر ذیل را یافته و سپس حذف کنید:
// If you are using Dependency Injection, you can delete the following constructor public <#= Model.ControllerName #>() : this(<#= String.Join(", ", Repositories.Values.Select(x => "new " + x.RepositoryTypeName + "()")) #>) { }
PM> Scaffold Controller -ModelType Task -ControllerName TasksController -DbContextType TasksDbContext -Repository -Force -ForceMode ControllerOnly
یا اگر نیاز به تغییر کدهای الگوی مخزن مورد استفاده است میتوان از دستور ذیل استفاده کرد:
Scaffold CustomScaffolder EFRepository
لیست Scaffolderهای مهیا با دستور Get-Scaffolder قابل مشاهده است.
C# 12.0 - Primary Constructors
private string <FirstName>k__BackingField;
public class AbcController : Controller { private readonly IMyService _myService; public AbcController(IMyService myService) { _myService = myService; } }
public class MyController(IMyService _myService) : ControllerBase { [HttpGet] public IActionResult RandomNumber() { var number = _myService.RandomNumber(); _myService = null; var number2 = MyRandom(); return Ok(number + number2); } public int MyRandom() { return _myService.RandomNumber(); } }
سرویس ما متدهای زیر را در دسترس قرار میدهد.
Relative URl | HTTP method | Action |
api/products/ | GET | گرفتن لیست تمام محصولات |
api/products/id/ | GET | گرفتن یک محصول بر اساس شناسه |
api/products?category=category/ | GET | گرفتن یک محصول بر اساس طبقه بندی |
api/products/ | POST | ایجاد یک محصول جدید |
api/products/id/ | PUT | بروز رسانی یک محصول |
api/products/id/ | DELETE | حذف یک محصول |
همانطور که مشاهده میکنید برخی از آدرس ها، شامل شناسه محصول هم میشوند. بعنوان مثال برای گرفتن محصولی با شناسه 28، کلاینت یک درخواست GET را به آدرس زیر ارسال میکند:
http://hostname/api/products/28
منابع
سرویس ما آدرس هایی برای دستیابی به دو نوع منبع (resource) را تعریف میکند:
URI | Resource |
api/products/ | لیست تمام محصولات |
api/products/id/ | یک محصول مشخص |
متد ها
چهار متد اصلی HTTP یعنی همان GET, PUT, POST, DELETE میتوانند بصورت زیر به عملیات CRUD نگاشت شوند:
- متد GET یک منبع (resource) را از آدرس تعریف شده دریافت میکند. متدهای GET هیچگونه تاثیری روی سرور نباید داشته باشند. مثلا حذف رکوردها با متد اکیدا اشتباه است.
- متد PUT یک منبع را در آدرس تعریف شده بروز رسانی میکند. این متد برای ساختن منابع جدید هم میتواند استفاده شود، البته در صورتی که سرور به کلاینتها اجازه مشخص کردن آدرسهای جدید را بدهد. در مثال جاری پشتیبانی از ایجاد منابع توسط متد PUT را بررسی نخواهیم کرد.
- متد POST منبع جدیدی میسازد. سرور آدرس آبجکت جدید را تعیین میکند و آن را بعنوان بخشی از پیام Response بر میگرداند.
- متد DELETE منبعی را در آدرس تعریف شده حذف میکند.
نکته: متد PUT موجودیت محصول (product entity) را کاملا جایگزین میکند. به بیان دیگر، از کلاینت انتظار میرود که آبجکت کامل محصول را برای بروز رسانی ارسال کند. اگر میخواهید از بروز رسانیهای جزئی/پاره ای (partial) پشتیبانی کنید متد PATCH توصیه میشود. مثال جاری متد PATCH را پیاده سازی نمیکند.
یک پروژه Web API جدید بسازید
ویژوال استودیو را باز کنید و پروژه جدیدی از نوع ASP.NET MVC Web Application بسازید. نام پروژه را به "ProductStore" تغییر دهید و OK کنید.
در دیالوگ New ASP.NET Project قالب Web API را انتخاب کرده و تایید کنید.
افزودن یک مدل
یک مدل، آبجکتی است که داده اپلیکیشن شما را نمایندگی میکند. در ASP.NET Web API میتوانید از آبجکتهای Strongly-typed بعنوان مدل هایتان استفاده کنید که بصورت خودکار برای کلاینت به فرمتهای JSON, XML مرتب (Serialize) میشوند. در مثال جاری، دادههای ما محصولات هستند. پس کلاس جدیدی بنام Product میسازیم.
در پوشه Models کلاس جدیدی با نام Product بسازید.
حال خواص زیر را به این کلاس اضافه کنید.
namespace ProductStore.Models { public class Product { public int Id { get; set; } public string Name { get; set; } public string Category { get; set; } public decimal Price { get; set; } } }
افزودن یک مخزن
ما نیاز به ذخیره کردن کلکسیونی از محصولات داریم، و بهتر است این کلکسیون از پیاده سازی سرویس تفکیک شود. در این صورت بدون نیاز به بازنویسی کلاس سرویس میتوانیم منبع دادهها را تغییر دهیم. این نوع طراحی با نام الگوی مخزن یا Repository Pattern شناخته میشود. برای شروع نیاز به یک قرارداد جنریک برای مخزنها داریم.
روی پوشه Models کلیک راست کنید و گزینه Add, New Item را انتخاب نمایید.
نوع آیتم جدید را Interface انتخاب کنید و نام آن را به IProductRepository تغییر دهید.
حال کد زیر را به این اینترفیس اضافه کنید.
namespace ProductStore.Models { public interface IProductRepository { IEnumerable<Product> GetAll(); Product Get(int id); Product Add(Product item); void Remove(int id); bool Update(Product item); } }
namespace ProductStore.Models { public class ProductRepository : IProductRepository { private List<Product> products = new List<Product>(); private int _nextId = 1; public ProductRepository() { Add(new Product { Name = "Tomato soup", Category = "Groceries", Price = 1.39M }); Add(new Product { Name = "Yo-yo", Category = "Toys", Price = 3.75M }); Add(new Product { Name = "Hammer", Category = "Hardware", Price = 16.99M }); } public IEnumerable<Product> GetAll() { return products; } public Product Get(int id) { return products.Find(p => p.Id == id); } public Product Add(Product item) { if (item == null) { throw new ArgumentNullException("item"); } item.Id = _nextId++; products.Add(item); return item; } public void Remove(int id) { products.RemoveAll(p => p.Id == id); } public bool Update(Product item) { if (item == null) { throw new ArgumentNullException("item"); } int index = products.FindIndex(p => p.Id == item.Id); if (index == -1) { return false; } products.RemoveAt(index); products.Add(item); return true; } } }
مخزن ما لیست محصولات را در حافظه محلی نگهداری میکند. برای مثال جاری این طراحی کافی است، اما در یک اپلیکیشن واقعی دادههای شما در یک دیتابیس یا منبع داده ابری ذخیره خواهند شد. همچنین استفاده از الگوی مخزن، تغییر منبع دادهها در آینده را راحتتر میکند.
افزودن یک کنترلر Web API
اگر قبلا با ASP.NET MVC کار کرده باشید، با مفهوم کنترلرها آشنایی دارید. در ASP.NET Web API کنترلرها کلاس هایی هستند که درخواستهای HTTP دریافتی از کلاینت را به اکشن متدها نگاشت میکنند. ویژوال استودیو هنگام ساختن پروژه شما دو کنترلر به آن اضافه کرده است. برای مشاهد آنها پوشه Controllers را باز کنید.
- HomeController یک کنترلر مرسوم در ASP.NET MVC است. این کنترلر مسئول بکار گرفتن صفحات وب است و مستقیما ربطی به Web API ما ندارد.
- ValuesController یک کنترلر نمونه WebAPI است.
کنترلر ValuesController را حذف کنید، نیازی به این آیتم نخواهیم داشت. حال برای اضافه کردن کنترلری جدید مراحل زیر را دنبال کنید.
در پنجره Solution Explorer روی پوشه Controllers کلیک راست کرده و گزینه Add, Controller را انتخاب کنید.
در دیالوگ Add Controller نام کنترلر را به ProductsController تغییر داده و در قسمت Scaffolding Options گزینه Empty API Controller را انتخاب کنید.
حال فایل کنترلر جدید را باز کنید و عبارت زیر را به بالای آن اضافه نمایید.
using ProductStore.Models;
public class ProductsController : ApiController { static readonly IProductRepository repository = new ProductRepository(); }
فراخوانی ()new ProductRepository طراحی جالبی نیست، چرا که کنترلر را به پیاده سازی بخصوصی از این اینترفیس گره میزند. بهتر است از تزریق وابستگی (Dependency Injection) استفاده کنید. برای اطلاعات بیشتر درباره تکنیک DI در Web API به این لینک مراجعه کنید.
گرفتن منابع
ProductStore API اکشنهای متعددی در قالب متدهای HTTP GET در دسترس قرار میدهد. هر اکشن به متدی در کلاس ProductsController مرتبط است.
Relative URl | HTTP Method | Action |
api/products/ | GET | دریافت لیست تمام محصولات |
api/products/id/ | GET | دریافت محصولی مشخص بر اساس شناسه |
api/products?category=category/ | GET | دریافت محصولات بر اساس طبقه بندی |
برای دریافت لیست تمام محصولات متد زیر را به کلاس ProductsController اضافه کنید.
public class ProductsController : ApiController { public IEnumerable<Product> GetAllProducts() { return repository.GetAll(); } // .... }
برای دریافت محصولی مشخص بر اساس شناسه آن متد زیر را اضافه کنید.
public Product GetProduct(int id) { Product item = repository.Get(id); if (item == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } return item; }
نام این متد هم با "Get" شروع میشود اما پارامتری با نام id دارد. این پارامتر به قسمت id مسیر درخواست شده (request URl) نگاشت میشود. تبدیل پارامتر به نوع داده مناسب (در اینجا int) هم بصورت خودکار توسط فریم ورک ASP.NET Web API انجام میشود.
متد GetProduct در صورت نامعتبر بودن پارامتر id استثنایی از نوع HttpResponseException تولید میکند. این استثنا بصورت خودکار توسط فریم ورک Web API به خطای 404 (Not Found) ترجمه میشود.
در آخر متدی برای دریافت محصولات بر اساس طبقه بندی اضافه کنید.
public IEnumerable<Product> GetProductsByCategory(string category) { return repository.GetAll().Where( p => string.Equals(p.Category, category, StringComparison.OrdinalIgnoreCase)); }
اگر آدرس درخواستی پارامترهای query string داشته باشد، Web API سعی میکند پارامترها را با پارامترهای متد کنترلر تطبیق دهد. بنابراین درخواستی به آدرس "api/products?category=category" به این متد نگاشت میشود.
ایجاد منبع جدید
قدم بعدی افزودن متدی به ProductsController برای ایجاد یک محصول جدید است. لیست زیر پیاده سازی ساده ای از این متد را نشان میدهد.
// Not the final implementation! public Product PostProduct(Product item) { item = repository.Add(item); return item; }
- نام این متد با "Post" شروع میشود. برای ساختن محصولی جدید کلاینت یک درخواست HTTP POST ارسال میکند.
- این متد پارامتری از نوع Product میپذیرد. در Web API پارامترهای پیچیده (complex types) بصورت خودکار با deserialize کردن بدنه درخواست بدست میآیند. بنابراین در اینجا از کلاینت انتظار داریم که آبجکتی از نوع Product را با فرمت XML یا JSON ارسال کند.
پیاده سازی فعلی این متد کار میکند، اما هنوز کامل نیست. در حالت ایده آل ما میخواهیم پیام HTTP Response موارد زیر را هم در بر گیرد:
- Response code: بصورت پیش فرض فریم ورک Web API کد وضعیت را به 200 (OK) تنظیم میکند. اما طبق پروتکل HTTP/1.1 هنگامی که یک درخواست POST منجر به ساخته شدن منبعی جدید میشود، سرور باید با کد وضعیت 201 (Created) پاسخ دهد.
- Location: هنگامی که سرور منبع جدیدی میسازد، باید آدرس منبع جدید را در قسمت Location header پاسخ درج کند.
ASP.NET Web API دستکاری پیام HTTP response را آسان میکند. لیست زیر پیاده سازی بهتری از این متد را نشان میدهد.
public HttpResponseMessage PostProduct(Product item) { item = repository.Add(item); var response = Request.CreateResponse<Product>(HttpStatusCode.Created, item); string uri = Url.Link("DefaultApi", new { id = item.Id }); response.Headers.Location = new Uri(uri); return response; }
متد CreateResponse آبجکتی از نوع HttpResponseMessage میسازد و بصورت خودکار آبجکت Product را مرتب (serialize) کرده و در بدنه پاسخ مینویسد. نکته دیگر آنکه مثال جاری، مدل را اعتبارسنجی نمیکند. برای اطلاعات بیشتر درباره اعتبارسنجی مدلها در Web API به این لینک مراجعه کنید.
بروز رسانی یک منبع
بروز رسانی یک محصول با PUT ساده است.
public void PutProduct(int id, Product product) { product.Id = id; if (!repository.Update(product)) { throw new HttpResponseException(HttpStatusCode.NotFound); } }
حذف یک منبع
برای حذف یک محصول متد زیر را به کلاس ProductsController اضافه کنید.
public void DeleteProduct(int id) { Product item = repository.Get(id); if (item == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } repository.Remove(id); }
در مثال جاری متد DeleteProduct نوع void را بر میگرداند، که فریم ورک Web API آن را بصورت خودکار به کد وضعیت 204 (No Content) ترجمه میکند.
EF Code First #10
حین کار با ORMهای پیشرفته، ویژگیهای جالب توجهی در اختیار برنامه نویسها قرار میگیرد که در زمان استفاده از کلاسهای متداول SQLHelper از آنها خبری نیست؛ مانند:
الف) Deferred execution
ب) Lazy loading
ج) Eager loading
نحوه بررسی SQL نهایی تولیدی توسط EF
برای توضیح موارد فوق، نیاز به مشاهده خروجی SQL نهایی حاصل از ORM است و همچنین شمارش تعداد بار رفت و برگشت به بانک اطلاعاتی. بهترین ابزاری را که برای این منظور میتوان پیشنهاد داد، برنامه EF Profiler است. برای دریافت آن میتوانید به این آدرس مراجعه کنید: (^) و (^)
پس از وارد کردن نام و آدرس ایمیل، یک مجوز یک ماهه آزمایشی، به آدرس ایمیل شما ارسال خواهد شد.
زمانیکه این فایل را در ابتدای اجرای برنامه به آن معرفی میکنید، محل ذخیره سازی نهایی آن جهت بازبینی بعدی، مسیر MyUserName\Local Settings\Application Data\EntityFramework Profiler خواهد بود.
استفاده از این برنامه هم بسیار ساده است:
الف) در برنامه خود، ارجاعی را به اسمبلی HibernatingRhinos.Profiler.Appender.dll که در پوشه برنامه EFProf موجود است، اضافه کنید.
ب) در نقطه آغاز برنامه، متد زیر را فراخوانی نمائید:
HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize();
نقطه آغاز برنامه میتواند متد Application_Start برنامههای وب، در متد Program.Main برنامههای ویندوزی کنسول و WinForms و در سازنده کلاس App برنامههای WPF باشد.
ج) برنامه EFProf را اجرا کنید.
مزایای استفاده از این برنامه
1) وابسته به بانک اطلاعاتی مورد استفاده نیست. (برخلاف برای مثال برنامه معروف SQL Server Profiler که فقط به همراه SQL Server ارائه میشود)
2) خروجی SQL نمایش داده شده را فرمت کرده و به همراه Syntax highlighting نیز هست.
3) کار این برنامه صرفا به لاگ کردن SQL تولیدی خلاصه نمیشود. یک سری از Best practices را نیز به شما گوشزد میکند. بنابراین اگر نیاز دارید سیستم خود را بر اساس دیدگاه یک متخصص بررسی کنید (یک Code review ارزشمند)، این ابزار میتواند بسیار مفید باشد.
4) میتواند کوئریهای سنگین و سبک را به خوبی تشخیص داده و گزارشات آماری جالبی را به شما ارائه دهد.
5) میتواند دقیقا مشخص کند، کوئری را که مشاهده میکنید از طریق کدام متد در کدام کلاس صادر شده است و دقیقا از چه سطری.
6) امکان گروه بندی خودکار کوئریهای صادر شده را بر اساس DbContext مورد استفاده به همراه دارد.
و ...
استفاده از این برنامه حین کار با EF «الزامی» است! (البته نسخههای NH و سایر ORMهای دیگر آن نیز موجود است و این مباحث در مورد تمام ORMهای پیشرفته صادق است)
مدام باید بررسی کرد که صفحه جاری چه تعداد کوئری را به بانک اطلاعاتی ارسال کرده و به چه نحوی. همچنین آیا میتوان با اعمال اصلاحاتی، این وضع را بهبود بخشید. بنابراین عدم استفاده از این برنامه حین کار با ORMs، همانند راه رفتن در خواب است! ممکن است تصور کنید برنامه دارد به خوبی کار میکند اما ... در پشت صحنه فقط صفحه جاری برنامه، 100 کوئری را به بانک اطلاعاتی ارسال کرده، در حالیکه شما تنها نیاز به یک کوئری داشتهاید.
کلاسهای مدل مثال جاری
کلاسهای مدل مثال جاری از یک دپارتمان که دارای تعدادی کارمند میباشد، تشکیل شده است. ضمنا هر کارمند تنها در یک دپارتمان میتواند مشغول به کار باشد و رابطه many-to-many نیست :
using System.Collections.Generic;
namespace EF_Sample06.Models
{
public class Department
{
public int DepartmentId { get; set; }
public string Name { get; set; }
//Creates Employee navigation property for Lazy Loading (1:many)
public virtual ICollection<Employee> Employees { get; set; }
}
}
namespace EF_Sample06.Models
{
public class Employee
{
public int EmployeeId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
//Creates Department navigation property for Lazy Loading
public virtual Department Department { get; set; }
}
}
نگاشت دستی این کلاسها هم ضرورتی ندارد، زیرا قراردادهای توکار EF Code first را رعایت کرده و EF در اینجا به سادگی میتواند primary key و روابط one-to-many را بر اساس navigation properties تعریف شده، تشخیص دهد.
در اینجا کلاس Context برنامه به شرح زیر است:
using System.Data.Entity;
using EF_Sample06.Models;
namespace EF_Sample06.DataLayer
{
public class Sample06Context : DbContext
{
public DbSet<Department> Departments { set; get; }
public DbSet<Employee> Employees { set; get; }
}
}
و تنظیمات ابتدایی نحوه به روز رسانی و آغاز بانک اطلاعاتی نیز مطابق کدهای زیر میباشد:
using System.Collections.Generic;
using System.Data.Entity.Migrations;
using EF_Sample06.Models;
namespace EF_Sample06.DataLayer
{
public class Configuration : DbMigrationsConfiguration<Sample06Context>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
AutomaticMigrationDataLossAllowed = true;
}
protected override void Seed(Sample06Context context)
{
var employee1 = new Employee { FirstName = "f name1", LastName = "l name1" };
var employee2 = new Employee { FirstName = "f name2", LastName = "l name2" };
var employee3 = new Employee { FirstName = "f name3", LastName = "l name3" };
var employee4 = new Employee { FirstName = "f name4", LastName = "l name4" };
var dept1 = new Department { Name = "dept 1", Employees = new List<Employee> { employee1, employee2 } };
var dept2 = new Department { Name = "dept 2", Employees = new List<Employee> { employee3 } };
var dept3 = new Department { Name = "dept 3", Employees = new List<Employee> { employee4 } };
context.Departments.Add(dept1);
context.Departments.Add(dept2);
context.Departments.Add(dept3);
base.Seed(context);
}
}
}
نکته: تهیه خروجی XML از نگاشتهای خودکار تهیه شده
اگر علاقمند باشید که پشت صحنه نگاشتهای خودکار EF Code first را در یک فایل XML جهت بررسی بیشتر ذخیره کنید، میتوان از متد کمکی زیر استفاده کرد:
void ExportMappings(DbContext context, string edmxFile)
{
var settings = new XmlWriterSettings { Indent = true };
using (XmlWriter writer = XmlWriter.Create(edmxFile, settings))
{
System.Data.Entity.Infrastructure.EdmxWriter.WriteEdmx(context, writer);
}
}
بهتر است پسوند فایل XML تولیدی را edmx قید کنید تا بتوان آنرا با دوبار کلیک بر روی فایل، در ویژوال استودیو نیز مشاهده کرد:
using (var db = new Sample06Context())
{
ExportMappings(db, "mappings.edmx");
}
الف) بررسی Deferred execution یا بارگذاری به تاخیر افتاده
برای توضیح مفهوم Deferred loading/execution بهترین مثالی را که میتوان ارائه داد، صفحات جستجوی ترکیبی در برنامهها است. برای مثال یک صفحه جستجو را طراحی کردهاید که حاوی دو تکست باکس دریافت FirstName و LastName کاربر است. کنار هر کدام از این تکست باکسها نیز یک چکباکس قرار دارد. به عبارتی کاربر میتواند جستجویی ترکیبی را در اینجا انجام دهد. نحوه پیاده سازی صحیح این نوع مثالها در EF Code first به چه نحوی است؟
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using EF_Sample06.DataLayer;
using EF_Sample06.Models;
namespace EF_Sample06
{
class Program
{
static IList<Employee> FindEmployees(string fName, string lName, bool byName, bool byLName)
{
using (var db = new Sample06Context())
{
IQueryable<Employee> query = db.Employees.AsQueryable();
if (byLName)
{
query = query.Where(x => x.LastName == lName);
}
if (byName)
{
query = query.Where(x => x.FirstName == fName);
}
return query.ToList();
}
}
static void Main(string[] args)
{
// note: remove this line if you received : create database is not supported by this provider.
HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize();
Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample06Context, Configuration>());
var list = FindEmployees("f name1", "l name1", true, true);
foreach (var item in list)
{
Console.WriteLine(item.FirstName);
}
}
}
}
نحوه صحیح این نوع پیاده سازی ترکیبی را در متد FindEmployees مشاهده میکنید. نکته مهم آن، استفاده از نوع IQueryable و متد AsQueryable است و امکان ترکیب کوئریها با هم.
به نظر شما با فراخوانی متد FindEmployees به نحو زیر که هر دو شرط آن توسط کاربر انتخاب شده است، چه تعداد کوئری به بانک اطلاعاتی ارسال میشود؟
var list = FindEmployees("f name1", "l name1", true, true);
شاید پاسخ دهید که سه بار : یکبار در متد db.Employees.AsQueryable و دوبار هم در حین ورود به بدنه شرطهای یاد شده و اینجا است که کسانی که قبلا با رویههای ذخیره شده کار کرده باشند، شروع به فریاد و فغان میکنند که ما قبلا این مسایل رو با یک SP در یک رفت و برگشت مدیریت میکردیم!
پاسخ صحیح: «فقط یکبار»! آنهم تنها در زمان فراخوانی متد ToList و نه قبل از آن.
برای اثبات این مدعا نیاز است به خروجی SQL لاگ شده توسط EF Profiler مراجعه کرد:
SELECT [Extent1].[EmployeeId] AS [EmployeeId],
[Extent1].[FirstName] AS [FirstName],
[Extent1].[LastName] AS [LastName],
[Extent1].[Department_DepartmentId] AS [Department_DepartmentId]
FROM [dbo].[Employees] AS [Extent1]
WHERE ([Extent1].[LastName] = 'l name1' /* @p__linq__0 */)
AND ([Extent1].[FirstName] = 'f name1' /* @p__linq__1 */)
IQueryable قلب LINQ است و تنها بیانگر یک عبارت (expression) از رکوردهایی میباشد که مد نظر شما است و نه بیشتر. برای مثال زمانیکه یک IQueryable را همانند مثال فوق فیلتر میکنید، هنوز چیزی از بانک اطلاعاتی یا منبع دادهای دریافت نشده است. هنوز هیچ اتفاقی رخ نداده است و هنوز رفت و برگشتی به منبع دادهای صورت نگرفته است. به آن باید به شکل یک expression builder نگاه کرد و نه لیستی از اشیاء فیلتر شدهی ما. به این مفهوم، deferred execution (اجرای به تاخیر افتاده) نیز گفته میشود.
کوئری LINQ شما تنها زمانی بر روی بانک اطلاعاتی اجرا میشود که کاری بر روی آن صورت گیرد مانند فراخوانی متد ToList، فراخوانی متد First یا FirstOrDefault و امثال آن. تا پیش از این فقط به شکل یک عبارت در برنامه وجود دارد و نه بیشتر.
اطلاعات بیشتر: «تفاوت بین IQueryable و IEnumerable در حین کار با ORMs»
ب) بررسی Lazy Loading یا واکشی در صورت نیاز
در مطلب جاری اگر به کلاسهای مدل برنامه دقت کنید، تعدادی از خواص به صورت virtual تعریف شدهاند. چرا؟
تعریف یک خاصیت به صورت virtual، پایه و اساس lazy loading است و به کمک آن، تا به اطلاعات شیءایی نیاز نباشد، وهله سازی نخواهد شد. به این ترتیب میتوان به کارآیی بیشتری در حین کار با ORMs رسید. برای مثال در کلاسهای فوق، اگر تنها نیاز به دریافت نام یک دپارتمان هست، نباید حین وهله سازی از شیء دپارتمان، شیء لیست کارمندان مرتبط با آن نیز وهله سازی شده و از بانک اطلاعاتی دریافت شوند. به این وهله سازی با تاخیر، lazy loading گفته میشود.
Lazy loading پیاده سازی سادهای نداشته و مبتنی است بر بکارگیری AOP frameworks یا کتابخانههایی که امکان تشکیل اشیاء Proxy پویا را در پشت صحنه فراهم میکنند. علت virtual تعریف کردن خواص رابط نیز به همین مساله بر میگردد، تا این نوع کتابخانهها بتوانند در نحوه تعریف اینگونه خواص virtual در زمان اجرا، در پشت صحنه دخل و تصرف کنند. البته حین استفاده از EF یا انواع و اقسام ORMs دیگر با این نوع پیچیدگیها روبرو نخواهیم شد و تشکیل اشیاء Proxy در پشت صحنه انجام میشوند.
یک مثال: قصد داریم اولین دپارتمان ثبت شده در حین آغاز برنامه را یافته و سپس لیست کارمندان آنرا نمایش دهیم:
using (var db = new Sample06Context())
{
var dept1 = db.Departments.Find(1);
if (dept1 != null)
{
Console.WriteLine(dept1.Name);
foreach (var item in dept1.Employees)
{
Console.WriteLine(item.FirstName);
}
}
}
رفتار یک ORM جهت تعیین اینکه آیا نیاز است برای دریافت اطلاعات بین جداول Join صورت گیرد یا خیر، واکشی حریصانه و غیرحریصانه را مشخص میسازد.
در حالت واکشی حریصانه به ORM خواهیم گفت که لطفا جهت دریافت اطلاعات فیلدهای جداول مختلف، از همان ابتدای کار در پشت صحنه، Join های لازم را تدارک ببین. در حالت واکشی غیرحریصانه به ORM خواهیم گفت به هیچ عنوان حق نداری Join ایی را تشکیل دهی. هر زمانی که نیاز به اطلاعات فیلدی از جدولی دیگر بود باید به صورت مستقیم به آن مراجعه کرده و آن مقدار را دریافت کنی.
به صورت خلاصه برنامه نویس در حین کار با ORM های پیشرفته نیازی نیست Join بنویسد. تنها باید ORM را طوری تنظیم کند که آیا اینکار را حتما خودش در پشت صحنه انجام دهد (واکشی حریصانه)، یا اینکه خیر، به هیچ عنوان SQL های تولیدی در پشت صحنه نباید حاوی Join باشند (lazy loading).
در مثال فوق به صورت خودکار دو کوئری به بانک اطلاعاتی ارسال میگردد:
SELECT [Limit1].[DepartmentId] AS [DepartmentId],
[Limit1].[Name] AS [Name]
FROM (SELECT TOP (2) [Extent1].[DepartmentId] AS [DepartmentId],
[Extent1].[Name] AS [Name]
FROM [dbo].[Departments] AS [Extent1]
WHERE [Extent1].[DepartmentId] = 1 /* @p0 */) AS [Limit1]
SELECT [Extent1].[EmployeeId] AS [EmployeeId],
[Extent1].[FirstName] AS [FirstName],
[Extent1].[LastName] AS [LastName],
[Extent1].[Department_DepartmentId] AS [Department_DepartmentId]
FROM [dbo].[Employees] AS [Extent1]
WHERE ([Extent1].[Department_DepartmentId] IS NOT NULL)
AND ([Extent1].[Department_DepartmentId] = 1 /* @EntityKeyValue1 */)
یکبار زمانیکه قرار است اطلاعات دپارتمان یک (db.Departments.Find) دریافت شود. تا این لحظه خبری از جدول Employees نیست. چون lazy loading فعال است و فقط اطلاعاتی را که نیاز داشتهایم فراهم کرده است.
زمانیکه برنامه به حلقه میرسد، نیاز است اطلاعات dept1.Employees را دریافت کند. در اینجا است که کوئری دوم، به بانک اطلاعاتی صادر خواهد شد (بارگذاری در صورت نیاز).
ج) بررسی Eager Loading یا واکشی حریصانه
حالت lazy loading بسیار جذاب به نظر میرسد؛ برای مثال میتوان خواص حجیم یک جدول را به جدول مرتبط دیگری منتقل کرد. مثلا فیلدهای متنی طولانی یا اطلاعات باینری فایلهای ذخیره شده، تصاویر و امثال آن. به این ترتیب تا زمانیکه نیازی به اینگونه اطلاعات نباشد، lazy loading از بارگذاری آنها جلوگیری کرده و سبب افزایش کارآیی برنامه میشود.
اما ... همین lazy loading در صورت استفاده نا آگاهانه میتواند سرور بانک اطلاعاتی را در یک برنامه چندکاربره از پا درآورد! نیازی هم نیست تا شخصی به سایت شما حمله کند. مهاجم اصلی همان برنامه نویس کم اطلاع است!
اینبار مثال زیر را درنظر بگیرید که بجای دریافت اطلاعات یک شخص، مثلا قصد داریم، اطلاعات کلیه دپارتمانها را توسط یک Grid نمایش دهیم (فرقی نمیکند برنامه وب یا ویندوز باشد؛ اصول یکی است):
using (var db = new Sample06Context())
{
foreach (var dept in db.Departments)
{
Console.WriteLine(dept.Name);
foreach (var item in dept.Employees)
{
Console.WriteLine(item.FirstName);
}
}
}
There is already an open DataReader associated with this Command which must be closed first
برای رفع این مشکل نیاز است گزینه MultipleActiveResultSets=True را به کانکشن استرینگ اضافه کرد:
<connectionStrings>
<clear/>
<add
name="Sample06Context"
connectionString="Data Source=(local);Initial Catalog=testdb2012;Integrated Security = true;MultipleActiveResultSets=True;"
providerName="System.Data.SqlClient"
/>
</connectionStrings>
سؤال: به نظر شما در دو حلقه تو در توی فوق چندبار رفت و برگشت به بانک اطلاعاتی صورت میگیرد؟ با توجه به اینکه در متد Seed ذکر شده در ابتدای مطلب، تعداد رکوردها مشخص است.
پاسخ: 7 بار!
و اینجا است که عنوان شد استفاده از EF Profiler در حین توسعه برنامههای مبتنی بر ORM «الزامی» است! اگر از این نکته اطلاعی نداشتید، بهتر است یکبار تمام صفحات گزارشگیری برنامههای خود را که حاوی یک Grid هستند، توسط EF Profiler بررسی کنید. اگر در این برنامه پیغام خطای n+1 select را دریافت کردید، یعنی در حال استفاده ناصحیح از امکانات lazy loading میباشید.
آیا میتوان این وضعیت را بهبود بخشید؟ زمانیکه کار ما گزارشگیری از اطلاعات با تعداد رکوردهای بالا است، استفاده ناصحیح از ویژگی Lazy loading میتواند به شدت کارآیی بانک اطلاعاتی را پایین بیاورد. برای حل این مساله در زمانهای قدیم (!) بین جداول join مینوشتند؛ الان چطور؟
در EF متدی به نام Include جهت Eager loading اطلاعات موجودیتهای مرتبط به هم درنظر گرفته شده است که در پشت صحنه همینکار را انجام میدهد:
using (var db = new Sample06Context())
{
foreach (var dept in db.Departments.Include(x => x.Employees))
{
Console.WriteLine(dept.Name);
foreach (var item in dept.Employees)
{
Console.WriteLine(item.FirstName);
}
}
}
همانطور که ملاحظه میکنید اینبار به کمک متد Include، نسبت به واکشی حریصانه Employees اقدام کردهایم. اکنون اگر برنامه را اجرا کنیم، فقط یک رفت و برگشت به بانک اطلاعاتی انجام خواهد شد و کار Join نویسی به صورت خودکار توسط EF مدیریت میگردد:
SELECT [Project1].[DepartmentId] AS [DepartmentId],
[Project1].[Name] AS [Name],
[Project1].[C1] AS [C1],
[Project1].[EmployeeId] AS [EmployeeId],
[Project1].[FirstName] AS [FirstName],
[Project1].[LastName] AS [LastName],
[Project1].[Department_DepartmentId] AS [Department_DepartmentId]
FROM (SELECT [Extent1].[DepartmentId] AS [DepartmentId],
[Extent1].[Name] AS [Name],
[Extent2].[EmployeeId] AS [EmployeeId],
[Extent2].[FirstName] AS [FirstName],
[Extent2].[LastName] AS [LastName],
[Extent2].[Department_DepartmentId] AS [Department_DepartmentId],
CASE
WHEN ([Extent2].[EmployeeId] IS NULL) THEN CAST(NULL AS int)
ELSE 1
END AS [C1]
FROM [dbo].[Departments] AS [Extent1]
LEFT OUTER JOIN [dbo].[Employees] AS [Extent2]
ON [Extent1].[DepartmentId] = [Extent2].[Department_DepartmentId]) AS [Project1]
ORDER BY [Project1].[DepartmentId] ASC,
[Project1].[C1] ASC
متد Include در نگارشهای اخیر EF پیشرفت کرده است و همانند مثال فوق، امکان کار با lambda expressions را جهت تعریف خواص مورد نظر به صورت strongly typed ارائه میدهد. در نگارشهای قبلی این متد، تنها امکان استفاده از رشتهها برای معرفی خواص وجود داشت.
همچنین توسط متد Include امکان eager loading چندین سطح با هم نیز وجود دارد؛ مثلا x.Employees.Kids و همانند آن.
چند نکته در مورد نحوه خاموش کردن Lazy loading
امکان خاموش کردن Lazy loading در تمام کلاسهای برنامه با تنظیم خاصیت Configuration.LazyLoadingEnabled کلاس Context برنامه به نحو زیر میسر است:
public class Sample06Context : DbContext
{
public Sample06Context()
{
this.Configuration.LazyLoadingEnabled = false;
}
یا اگر تنها در مورد یک کلاس نیاز است این خاموش سازی صورت گیرد، کلمه کلیدی virtual را حذف کنید. برای مثال با نوشتن public ICollection<Employee> Employees بجای public virtual ICollection<Employee> Employees در اولین بار وهله سازی کلاس دپارتمان، لیست کارمندان آن به نال تنظیم میشود. البته در این حالت null object pattern را نیز فراموش نکنید (وهله سازی پیش فرض Employees در سازنده کلاس):
public class Department
{
public int DepartmentId { get; set; }
public string Name { get; set; }
public ICollection<Employee> Employees { get; set; }
public Department()
{
Employees = new HashSet<Employee>();
}
}
به این ترتیب به خطای null reference object بر نخواهیم خورد. همچنین وهله سازی، با مقدار دهی لیست دریافتی از بانک اطلاعاتی متفاوت است. در اینجا نیز باید از متد Include استفاده کرد.
بنابراین در صورت خاموش کردن lazy loading، حتما نیاز است از متد Include استفاده شود. اگرlazy loading فعال است، جهت تبدیل آن به eager loading از متد Include استفاده کنید (اما اجباری نیست).