Dart کتابخانه ای است که توسط شرکت گوگل ارائه شده است و گفته میشود، قرار است جایگزین جاوا اسکریپت گردد و از آدرس https://www.dartlang.org قابل دسترسی میباشد. این کتابخانه، دارای انعطاف پذیری فوق العاده بالایی است و کد نویسی Java Script را راحتتر میکند. در حال حاضر هیچ مرورگری به غیر از Chromium از این تکنولوژی پشتیبانی نمیکند و جهت تسهیل در کدنویسی، باید از ویرایشگر Dart Editor استفاده کنید. این ویرایشگر کدهای نوشته شده را به دو صورت Native و JavaScript Compiled در اختیار مرورگر قرار میدهد. در ادامه با نحوهی کار و راه اندازی Dart آشنا خواهید شد.
ابتدا Dart و ویرایشگر مربوط به آن را توسط لینکهای زیر دانلود کنید:
دانلود نسخه 64 بیتی دارت + ویرایشگر
دانلود نسخه 32 بیتی دارت + ویرایشگر
بعد از اینکه فایلهای فوق را از حالت فشرده خارج کردید، پوشه ای با نام dart ایجاد مینماید. وارد پوشه dart شده و DartEditor را اجرا کنید.
توجه: جهت اجرای dart به JDK 6.0 یا بالاتر نیاز دارید
در مرحله بعد نمونه کدهای Dart را از لینک زیر دانلود نمایید و از حالت فشرده خارج کنید. پوشه ای با نام one-hour-codelab ایجاد میگردد.
از منوی File > Open Existing Folder… پوشه one-hour-codelab را باز کنید .
توضیحات
- پوشه packages و همچنین فایلهای pubspec.yaml و pubspec.lock شامل پیش نیازها و Package هایی هستند که جهت اجرای برنامههای تحت Dart مورد نیاز هستند. Dart Editor این نیازمندیها را به صورت خودکار نصب و تنظیم میکند.
توجه: اگر پوشه Packages را مشاهده نکردید و یا در سمت چپ فایلها علامت X قرمز رنگ وجود داشت، بدین معنی است که package ها به درستی نصب نشده اند. برای این منظور بر روی pubspec.yaml کلیک راست نموده و گزینه Get Pub را انتخاب کنید. توجه داشته باید که بدلیل تحریم ایران توسط گوگل باید از ابزارهای عبور از تحریم استفاده کنید.
- 6 پوشه را نیز در تصویر فوق مشاهده میکنید که نمونه کد piratebadge را بصورت مرحله به مرحله انجام داده و به پایان میرساند.
- Dart SDK شامل سورس کد مربوط به تمامی توابع، متغیرها و کلاس هایی است که توسط کیت توسعه نرم افزاری Dart ارائه شده است.
- Installed Packages شامل سورس کد مربوط به تمامی توابع، متغیرها و کلاسهای کتابخانههای اضافهتری است که Application به آنها وابسته است.
گام اول: اجرای یک برنامه کوچک
در این مرحله سورس کدهای آماده را مشاهده میکنید و با ساختار کدهای Dart و HTML آشنا میشوید و برنامه کوچکی را اجرا مینمایید.
در Dart Editor پوشه 1-blankbadge را باز کنید و فایلهای piratebadge.html و piratebadge.dart را مشاهده نمایید.
کد موجود در فایل piratebadge.html
<html> <head> <meta charset="utf-8"> <title>Pirate badge</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="piratebadge.css"> </head> <body> <h1>Pirate badge</h1> <div> TO DO: Put the UI widgets here. </div> <div> <div> Arrr! Me name is </div> <div> <span id="badgeName"> </span> </div> </div> <script type="application/dart" src="piratebadge.dart"></script> <script src="packages/browser/dart.js"></script> </body> </html>
توضیحات
- در کد HTML ، اولین تگ <script> ، فایل piratebadge.dart را جهت پیاده سازی دستورات dart به صفحه ضمیمه مینماید
- Dart Virtual Machine (Dart VM) کدهای Dart را بصورت Native یا بومی ماشین اجرا میکند. Dart VM کدهای خود را در Dartium که یک ویرایش ویژه از مرورگر Chromium میباشد اجرا میکند که میتواند برنامههای تحت Dart را بصورت Native اجرا کند.
- فایل packages/browser/dart.js پشتیبانی مرورگر از کد Native دارت را بررسی میکند و در صورت پشتیبانی، Dart VM را راه اندازی میکند و در غیر این صورت JavaScript کامپایل شده را بارگزاری مینماید.
کد موجود در piratebadge.dart
void main() { // Your app starts here. }
- این فایل شامل تابع main میباشد که تنها نقطه ورود به application است. تگ <script> موجود در piratebadge.html برنامه را با فراخوانی این تابع راه اندازی میکند.
- تابع main() یک تابع سطح بالا یا top-level میباشد.
- متغیرها و توابع top-level عناصری هستند که خارج از ساختار تعریف کلاس ایجاد میشوند.
جهت اجرای برنامه در Dart Editor بر روی piratebadge.html کلیک راست نمایید و گزینه Run in Dartium را اجرا کنید. این فایل توسط Dartium اجرا میشود و تابع main() را فراخوانی میکند و صفحه ای همانند شکل زیر را نمایش میدهد.
گام دوم: افزودن فیلد input
توجه داشته باشید که در این مرحله یا میتوانید تغییرات مورد نظر خود را در طی آموزش بر روی پوشهی 1-blankbadge اعمال کنید و یا به پوشههای تهیه شده در نمونه کد موجود در همین پروژه مراجعه نمایید.
در این مرحله یک تگ <input> به تگ <div class=”widgets”> اضافه کنید.
... <div> <div> <input type="text" id="inputName" maxlength="15"> </div> </div> ...
سپس کتابخانه dart:html را به ابتدای فایل piratebadge.dart اضافه کنید.
import 'dart:html';
توضیحات
- دستور فوق کلاسها و Resource های موجود در کتابخانه dart:html را اضافه میکند.
- از حجیم شدن کدهای خود نگران نباشید، زیرا فرایند کامپایل کدهای اضافی را حذف خواهد کرد.
- کتابخانه dart:html شامل کلاسهایی جهت کار با عناصر DOM و توابعی جهت دسترسی به این عناصر میباشد.
- در مباحث بعدی یاد میگیرید که با استفاده از کلمه کلیدی show فقط کلاسهایی را import کنید که به آن نیاز دارید.
- اگر کتابخانه ای در هیچ بخش کد استفاده نشود، خود Dart Editor به صورت warning اخطار میدهد و میتوانید آن را حذف کنید.
دستور زیر را در تابع main بنویسید تا رویداد مربوط به ورود اطلاعات در فیلد input را مدیریت نمایید.
void main() { querySelector('#inputName').onInput.listen(updateBadge); }
توضیحات
- تابع querySelector() در کتابخانه dart:html تعریف شده است و یک المنت DOM را جستجو مینماید. پارامتر ورودی آن یک selector میباشد که در اینجا فیلد input را توسط #inputName جستجو نمودیم که یک ID Selector میباشد.
- نوع خروجی این متد یک شی از نوع DOM میباشد.
- تابع onInput.Listen() رویدادی را برای پاسخگویی به ورود اطلاعات در فیلد input تعریف میکند. زمانی که کاربر اطلاعاتی را وارد نماید، تابع updateBadge فراخوانی میگردد.
- رویداد input زمانی رخ میدهد که کاربر کلیدی را از صفحه کلید فشار دهد.
- رشتهها همانند جاوا اسکریپت میتوانند در " یا ' قرار بگیرند.
تابع زیر را به صورت top-level یعنی خارج از تابع main تعریف کنید.
... void updateBadge(Event e) { querySelector('#badgeName').text = e.target.value; }
توضیحات
- این تابع محتوای المنت badgeName را به محتوای وارد شده در فیلد input تغییر میدهد.
- پارامتر ورودی این تابع شی e از نوع Event میباشد و به همین دلیل میتوانیم این تابع را یک Event Handler بنامیم.
- e.target به شی ای اشاره میکند که موجب رخداد رویداد شده است و در اینجا همان فیلد input میباشد
- با نوشتن کد فوق یک warning را مشاهده میکنید که بیان میکند ممکن است خصوصیت value برای e.target وجود نداشته باشد. برای حل این مسئله کد را بصورت زیر تغییر دهید.
... void updateBadge(Event e) { querySelector('#badgeName').text = (e.target as InputElement).value; }
توضیحات
- کلمه کلیدی as به منظور تبدیل نوع استفاده میشود که e.target را به یک InputElement تبدیل میکند.
همانند گام اول برنامه را اجرا کنید و نتیجه را مشاهده نمایید. با تایپ کردن در فیلد input به صورت همزمان در کادر قرمز رنگ نیز نتیجه تایپ را مشاهده مینمایید.
حالتهای عملکرد کتابخانهی Moq
کتابخانهی Moq، دو حالت عملکرد را دارد: Strict Mode و Loose mode. زمانیکه یک Mock object را نمونه سازی میکنیم، به صورت پیشفرض کتابخانهی Moq، یک Loose mock را ایجاد میکند. در این حالت این شیء، مقادیر پیشفرض خواص و اشیاء را بازگشت میدهد و استثنائی را صادر نمیکند. اگر این موارد مدنظر نیستند، میتوان به حالت Strict آن رجوع کرد که روش تنظیم آن به صورت زیر است:
var mockIdentityVerifier = new Mock<IIdentityVerifier>(MockBehavior.Strict);
Test method Loans.Tests.LoanApplicationProcessorShould.Accept threw exception: Moq.MockException: IIdentityVerifier.Initialize() invocation failed with mock behavior Strict. All invocations on the mock must have a corresponding setup.
mockIdentityVerifier.Setup(x => x.Initialize());
بنابراین هرچند کارکردن با حالت پیشفرض کتابخانهی Moq سادهاست، اما تنظیم حالت Strict سبب میشود تا تنظیمی را فراموش نکنیم و در نتیجه کیفیت آزمون واحد تهیه شده افزایش مییابد.
صدور استثناءها از طریق Mock objects
اگر در سیستم در حال آزمایش، قسمتی به بررسی خطاها اختصاص دارد، میتوان توسط Mock objects استثناءهایی را تولید و به این ترتیب منطق بررسی خطاها را آزمایش کرد.
برای نمونه در متد Process کلاس LoanApplicationProcessor، یک try/catch را به قسمت CalculateScore اضافه میکنیم:
try { _creditScorer.CalculateScore(application.Applicant.Name, application.Applicant.Address); } catch { return application.IsAccepted; }
mockCreditScorer.Setup(x => x.CalculateScore(It.IsAny<string>(), It.IsAny<string>())) .Throws(new InvalidOperationException("Test Exception"));
Assert.IsFalse(application.IsAccepted);
صدور رخدادها از طریق Mock objects
فرض کنید یک EventArgs سفارشی را به صورت زیر تعریف:
using System; namespace Loans.Models { public class CreditScoreResultArgs : EventArgs { public int Score { get; set; } } }
public interface ICreditScorer { event EventHandler<CreditScoreResultArgs> ResultAvailable;
mockCreditScorer.Raise(x => x.ResultAvailable += null, new CreditScoreResultArgs());
روش دیگر انجام اینکار به صورت زیر است:
mockCreditScorer.Setup(x => x.CalculateScore(It.IsAny<string>(), It.IsAny<string>())) .Raises(x => x.ResultAvailable += null, new CreditScoreResultArgs());
معرفی Partial Mocks
در اغلب آزمونهای واحدی که تا اینجا بررسی شدند، ابتدا یک Mock object را ایجاد و سپس وهلهای از سرویس مدنظر را توسط آن تهیه میکنیم. در ادامه تعدادی از متدهای این سرویس را مانند متد Process کلاس LoanApplicationProcessor، فراخوانی میکنیم. اینکار سبب اجرای فعالیتی در این سیستم شده و به همراه آن تعاملی با اشیاء Mock شده نیز صورت میگیرد. در نهایت حالت و یا نتیجهای را دریافت میکنیم و آنرا با حالت یا نتیجهای که انتظار داریم، مقایسه خواهیم کرد. در این روش پس از پایان اجرای سیستم در حال اجرا، حالت و نتیجهی نهایی حاصل از عملکرد آن، مورد بررسی قرار میگیرد. این بررسیها را نیز بر روی اینترفیسها انجام دادیم. اگر بجای اینترفیسها از یک class استفاده شود، به آن partial mock گفته میشود. عموما مواردی را که آزمایش آنها سخت است، با Partial mocks پیاده سازی میکنند؛ مانند کار با فایل سیستم، کار با قطعه کدهای نامعین مانند DateTime.Now، اعداد اتفاقی و یا Guidها.
در مثال زیر، شبیه به متد آزمون واحد Accept که تاکنون آنرا بررسی کردیم، از اشیاء Mock شده استفاده شدهاست؛ با یک تفاوت: بجای اینترفیس IIdentityVerifier، از کلاس پیاده سازی کنندهی آن که در اینجا IdentityVerifierServiceGateway است، استفاده شده:
namespace Loans.Tests { [TestClass] public class LoanApplicationProcessorShould { [TestMethod] public void AcceptUsingPartialMock() { var product = new LoanProduct {Id = 99, ProductName = "Loan", InterestRate = 5.25m}; var amount = new LoanAmount {CurrencyCode = "Rial", Principal = 2_000_000_0}; var applicant = new Applicant {Id = 1, Name = "User 1", Age = 25, Address = "This place", Salary = 1_500_000_0}; var application = new LoanApplication {Id = 42, Product = product, Amount = amount, Applicant = applicant}; var mockIdentityVerifier = new Mock<IdentityVerifierServiceGateway>(); mockIdentityVerifier.Setup(x => x.CallService(applicant.Name, applicant.Age, applicant.Address)) .Returns(true); var mockCreditScorer = new Mock<ICreditScorer>(); mockCreditScorer.Setup(x => x.ScoreResult.ScoreValue.Score).Returns(110_000); var sut = new LoanApplicationProcessor(mockIdentityVerifier.Object, mockCreditScorer.Object); sut.Process(application); Assert.IsTrue(application.IsAccepted); } } }
public virtual bool CallService(string applicantName, int applicantAge, string applicantAddress)
تبدیل DateTime.Now به یک مقدار ثابت قابل آزمایش توسط Partial Mocks
در کلاس IdentityVerifierServiceGateway، یک چنین کدی را داریم که از DateTime.Now نامشخص استفاده میکند و آزمون واحد نوشتن برای آن مشکل است؛ چون DateTime.Now در هربار که آزمایش اجرا میشود، تغییر میکند:
public bool Validate(string applicantName, int applicantAge, string applicantAddress) { Connect(); var isValidIdentity = CallService(applicantName, applicantAge, applicantAddress); LastCheckTime = DateTime.Now; Disconnect(); return isValidIdentity; }
public bool Validate(string applicantName, int applicantAge, string applicantAddress) { Connect(); var isValidIdentity = CallService(applicantName, applicantAge, applicantAddress); LastCheckTime = GetCurrentTime(); Disconnect(); return isValidIdentity; } public virtual DateTime GetCurrentTime() { return DateTime.Now; }
var expectedTime = new DateTime(2000, 1, 1); mockIdentityVerifier.Setup(x => x.GetCurrentTime()) .Returns(expectedTime); // ... Assert.AreEqual(expectedTime, mockIdentityVerifier.Object.LastCheckTime);
استفاده از متدهای protected بجای استفاده از متدهای public virtual در Partial Mocks
همانطور که مشاهده کردید، برای کار با Partial Mocks نیاز است متدهای معرفی شده، از نوع public virtual باشند. برای نمونه حتی مجبور شدیم یک متد private را نیز public کنیم. اگر علاقمند به این نوع تغییرات نیستید، میتوان بجای public کردن متدهای private، آنها را protected تعریف کرد. به همین جهت دو متدی را که تاکنون public virtual تعریف کردیم، تبدیل به protected virtual میکنیم.
پس از آن در کلاسی که آزمونهای واحد را تهیه کردیم، ابتدا using Moq.Protected را ذکر میکنیم تا بتوانیم به قابلیتهای ویژهی کار با متدهای Protected دسترسی پیدا کنیم.
سپس روش تنظیم این نوع متدهای protected، چون دسترسی مستقیمی به آنها وجود ندارد، به صورت زیر، با ذکر نام رشتهای آنها تغییر میکند:
mockIdentityVerifier.Protected().Setup<bool>( "CallService",applicant.Name, applicant.Age, applicant.Address) .Returns(true); var expectedTime = new DateTime(2000, 1, 1); mockIdentityVerifier.Protected().Setup<DateTime>("GetCurrentTime") .Returns(expectedTime);
روش دیگری نیز برای تعریف متدهای protected وجود دارد که اینبار strongly typed است. بالای متد آزمون واحد، اینترفیس private زیر را تعریف میکنیم:
interface IIdentityVerifierServiceGatewayProtectedMembers { DateTime GetCurrentTime(); bool CallService(string applicantName, int applicantAge, string applicantAddress); }
mockIdentityVerifier.Protected() .As<IIdentityVerifierServiceGatewayProtectedMembers>() .Setup(x => x.CallService(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<string>())) .Returns(true); var expectedTime = new DateTime(2000, 1, 1); mockIdentityVerifier.Protected() .As<IIdentityVerifierServiceGatewayProtectedMembers>() .Setup(x => x.GetCurrentTime()) .Returns(expectedTime);
معرفی روش دیگری بجای استفاده از متدهای protected
اگر در کدهای خود نیاز به استفادهی بیش از حد از متدهای protected را مشاهده کردید، این مورد میتوان نشانهی امکان Refactoring این قسمت از کدها به سرویسهایی مجزا باشند. برای مثال میتوان یک اینترفیس INowProvider را به صورت زیر تعریف کرد:
using System; namespace Loans.Services.Contracts { public interface INowProvider { DateTime GetNow(); } }
public class IdentityVerifierServiceGateway : IIdentityVerifier { private readonly INowProvider _nowProvider; public DateTime LastCheckTime { get; private set; } public IdentityVerifierServiceGateway(INowProvider nowProvider) { _nowProvider = nowProvider; }
public bool Validate(string applicantName, int applicantAge, string applicantAddress) { Connect(); var isValidIdentity = CallService(applicantName, applicantAge, applicantAddress); LastCheckTime = _nowProvider.GetNow(); // ...
var mockNowProvider = new Mock<INowProvider>(); mockNowProvider.Setup(x => x.GetNow()).Returns(expectedTime); var mockIdentityVerifier = new Mock<IdentityVerifierServiceGateway>(mockNowProvider.Object);
columns: [ { field: "IsAvailable", title: "موجود است", template: '<input type="checkbox" #= IsAvailable ? checked="checked" : "" # disabled="disabled" ></input>' } ]
همانطور که در این مثال نیز مشاهده میکنید، قالبهای Kendo UI از Hash (#) syntax استفاده میکنند. در اینجا قسمتهایی از قالب که با علامت # محصور میشوند، در حین اجرا، با اطلاعات فراهم شده جایگزین خواهند شد.
برای رندر مقادیر ساده میتوان از # =# استفاده کرد. از # :# برای رندر اطلاعات HTML-encoded کمک گرفته میشود و # # برای رندر کدهای جاوا اسکریپتی کاربرد دارد. از حالت HTML-encoded برای نمایش امن اطلاعات دریافتی از کاربران و جلوگیری از حملات XSS استفاده میشود.
اگر در این بین نیاز است # به صورت معمولی رندر شود، در حالت کدهای جاوا اسکریپتی به صورت #\\ و در HTML ساده به صورت #\ باید مشخص گردد.
مثالی از نحوهی تعریف یک قالب Kendo UI
<!--دریافت اطلاعات از منبع محلی--> <script id="javascriptTemplate" type="text/x-kendo-template"> <ul> # for (var i = 0; i < data.length; i++) { # <li>#= data[i] #</li> # } # </ul> </script> <div id="container1"></div> <script type="text/javascript"> $(function () { var data = ['User 1', 'User 2', 'User 3']; var template = kendo.template($("#javascriptTemplate").html()); var result = template(data); //Execute the template $("#container1").html(result); //Append the result }); </script>
در ادامه باید این قالب را رندر کرد. برای این منظور یک div با id مساوی container1 را جهت تعیین محل رندر نهایی اطلاعات مشخص میکنیم. سپس متد kendo.template بر اساس id قالب اسکریپتی تعریف شده، یک شیء قالب را تهیه کرده و سپس با ارسال آرایهای به آن، سبب اجرای آن میشود. خروجی نهایی، یک قطعه کد HTML است که در محل container1 درج خواهد شد.
همانطور که ملاحظه میکنید، متد kendo.template، نهایتا یک رشته را دریافت میکند. بنابراین همینجا و به صورت inline نیز میتوان یک قالب را تعریف کرد.
کار با منابع داده راه دور
فرض کنید مدل برنامه به صورت ذیل تعریف شدهاست:
namespace KendoUI04.Models { public class Product { public int Id { set; get; } public string Name { set; get; } public decimal Price { set; get; } public bool IsAvailable { set; get; } } }
using System.Collections.Generic; using System.Linq; using System.Web.Http; using KendoUI04.Models; namespace KendoUI04.Controllers { public class ProductsController : ApiController { public IEnumerable<Product> Get() { return ProductDataSource.LatestProducts.Take(10); } } }
<!--دریافت اطلاعات از سرور--> <div> <div id="container2"><ul></ul></div> </div> <script id="template1" type="text/x-kendo-template"> <li> #=Id# - #:Name# - #=kendo.toString(Price, "c")#</li> </script> <script type="text/javascript"> $(function () { var producatsTemplate1 = kendo.template($("#template1").html()); var productsDataSource = new kendo.data.DataSource({ transport: { read: { url: "api/products", dataType: "json", contentType: 'application/json; charset=utf-8', type: 'GET' } }, error: function (e) { alert(e.errorThrown); }, change: function () { $("#container2 > ul").html(kendo.render(producatsTemplate1, this.view())); } }); productsDataSource.read(); }); </script>
هرچند خروجی دریافتی از سرور نهایتا یک آرایه از اشیاء Product است، اما در template1 اثری از حلقهی جاوا اسکریپتی مشاهده نمیشود. در اینجا چون از متد kendo.render استفاده میشود، نیازی به ذکر حلقه نیست و به صورت خودکار، به تعداد عناصر آرایه دریافتی از سرور، قطعه HTML قالب را تکرار میکند.
در ادامه برای کار با سرور از یک Kendo UI DataSource استفاده شدهاست. قسمت transport/read آن، کار تعریف محل دریافت اطلاعات را از سرور مشخص میکند. رویدادگران change آن اطلاعات نهایی دریافتی را توسط متد view در اختیار متد kendo.render قرار میدهد. در نهایت، قطعهی HTML رندر شدهی نهایی حاصل از اجرای قالب، در بین تگهای ul مربوط به container2 درج خواهد شد.
رویدادگران change زمانیکه data source، از اطلاعات راه دور و یا یک آرایهی جاوا اسکریپتی پر میشود، فراخوانی خواهد شد. همچنین مباحث مرتب سازی اطلاعات، صفحه بندی و تغییر صفحه، افزودن، ویرایش و یا حذف اطلاعات نیز سبب فراخوانی آن میگردند. متد view ایی که در این مثال فراخوانی شد، صرفا در روال رویدادگردان change دارای اعتبار است و آخرین تغییرات اطلاعات و آیتمهای موجود در data source را باز میگرداند.
یک نکتهی تکمیلی: فعال سازی intellisense کدهای جاوا اسکریپتی Kendo UI
اگر به پوشهی اصلی مجموعهی Kendo UI مراجعه کنید، یکی از آنها vsdoc نام دارد که داخل آن فایلهای min.intellisense.js و vsdoc.js مشهود هستند.
اگر از ویژوال استودیوهای قبل از 2012 استفاده میکنید، نیاز است فایلهای vsdoc.js متناظری را به پروژه اضافه نمائید؛ دقیقا در کنار فایلهای اصلی js موجود. اگر از ویژوال استودیوی 2012 و یا بالاتر استفاده میکنید باید از فایلهای intellisense.js متناظر استفاده کنید. برای مثال اگر از kendo.all.min.js کمک میگیرید، فایل متناظر با آن kendo.all.min.intellisense.js خواهد بود.
بعد از اینکار نیاز است فایلی به نام references.js_ را به پوشهی اسکریپتهای خود با این محتوا اضافه کنید (برای VS 2012 به بعد):
/// <reference path="jquery.min.js" /> /// <reference path="kendo.all.min.js" />
Tools menu –> Options -> Text Editor –> JavaScript –> Intellisense –> References
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
KendoUI04.zip
زمانیکه پروسهی کامپایل برنامه شروع میشود، در این بین، به مرحلهی جدیدی به نام «تولید کدها» میرسد. در این حالت، کامپایلر تمام اطلاعاتی را که در مورد پروژهی جاری در اختیار دارد، به تولید کنندهی کد معرفی شدهی به آن ارائه میدهد. بر اساس این اطلاعات غنی ارائه شدهی توسط کامپایلر، تولید کنندهی کد، شروع به تولید کدهای جدیدی کرده و آنها را در اختیار ادامهی پروسهی کامپایل، قرار میدهد. پس از آن، کامپایلر با این کدهای جدید، همانند سایر کدهای موجود در پروژه رفتار کرده و عملکرد عادی خودش را ادامه میدهد.
یک برنامه میتواند از چندین Source Generators نیز استفاده کند که روش قرار گرفتن آنها را در پروسهی کامپایل، در شکل زیر مشاهده میکنید:
Source Generators از یکدیگر کاملا مستقل هستند و اطلاعات آنها Immutable است. یعنی نمیتوان اطلاعات تولیدی توسط یک Source Generator را در دیگری تغییر داد و تمام فایلهای تولیدی توسط انواع Source Generators موجود، به پروسهی کامپایل نهایی اضافه میشوند. هرچند زمانیکه فایلی توسط یک تولید کنندهی کد، به کامپایلر اضافه میشود، بلافاصله اطلاعات آن در کل برنامه و IDE و تمام Source Generators موجود دیگر، قابل مشاهده و استفاده است.
مقایسهای بین تولید کنندههای کد و فناوری IL Weaving
Source Generators، تنها راه و روش تولید کد، نیستند و پیش از آن روشهایی مانند استفاده از T4 templates ، Fody ، PostSharp و امثال آن نیز ارائه شدهاست. در ادامه مقایسهای را بین تولید کنندههای کد و فناوری IL Weaving را که پیشتر در سری AOP در این سایت مطالعه کردهاید، مشاهده میکنید:
تولید کنندههای کد:
- تنها میتوانند فایلهای جدید را اضافه کنند. یعنی «در حین» پروسهی کامپایل ظاهر میشوند و به عنوان یک مکمل، تاثیر گذارند. برای مثال نمیتوانند محتوای یک خاصیت یا متد از پیش موجود را تغییر دهند. اما میتوانند هر نوع کد partial ای را «تکمیل» کنند.
- محتوای اضافه شدهی توسط یک تولید کنندهی کد، بلافاصله توسط Compiler شناسایی شده و بررسی میشود و همچنین در Intellisense ظاهر شده و به سادگی قابل دسترسی است. همچنین، قابلیت دیباگ نیز دارد.
IL Weaving:
- میتوانند bytecode برنامه را تغییر دهند. یعنی «پس از» پروسهی کامپایل ظاهر شده و کدهایی را به اسمبلی نهایی تولید شده اضافه میکنند. در این حالت محدودیتی از لحاظ محل تغییر کدها وجود ندارد. برای مثال میتوان بدنهی یک متد یا خاصیت را بطور کامل بازنویسی کرد و کارکردهایی مانند تزریق کدهای caching و logging را دارند.
- کدهایی که توسط این پروسه اضافه میشوند، در حین کدنویسی متداول، قابلیت دسترسی ندارند؛ چون پس از پروسهی کامپایل، به فایل باینری نهایی تولیدی، اضافه میشوند. بنابراین قابلیت دیباگ به همراه سایر کدهای برنامه را نیز ندارند. به علاوه چون توسط کامپایلر در حین پروسهی کامپایل، بررسی نمیشوند، ممکن است به همراه قطعه کدهای غیرقابل اجرایی نیز باشند و دیباگ آنها بسیار مشکل است.
آیندهی Reflection به چه صورتی خواهد شد؟
هرچند Reflection کار تولید کدی را انجام نمیدهد، اما یکی از کارهای متداول با آن، یافتن و محاسبهی اطلاعات خواص و فیلدهای اشیاء، در زمان اجرا است و مزیت کار کردن با آن نیز این است که اگر خاصیتی یا فیلدی تغییر کند، نیازی به بازنویسی قسمتهای پیاده سازی شدهی با Reflection نیست. به همین جهت برای مثال تقریبا تمام کتابخانههای Serialization، از Reflection برای پیاده سازی اعمال خود استفاده میکنند.
امروز، تمام اینگونه عملیات را توسط Source Generators نیز میتوان انجام داد و این فناوری جدید، قابلیت به روز رسانی خودکار کدهای تولیدی را با کم و زیاد شدن خواص و فیلدهای کلاسها دارد و نمونهای از آن، Source Generator توکار مرتبط با کار با JSON در دات نت 6 است که به شدت سبب بهبود کارآیی برنامه، در مقایسه با استفادهی از Reflection میشود؛ چون اینبار تمام محاسبات دقیق مرتبط با Serialization به صورت خودکار در زمان کامپایل برنامه انجام میشود و جزئی از خروجی برنامهی نهایی خواهد شد و دیگر نیازی به محاسبهی هربارهی اطلاعات مورد نیاز، در زمان اجرای برنامه نیست.
نمونهای از روش دسترسی به اطلاعات کلاسها و خواص و فیلدهای آنها را در زمان کامپایل برنامه توسط Source Generators، در مثال قسمت بعد، مشاهده خواهید کرد.
وضعیت T4 templates چگونه خواهد شد؟
در سالهای آغازین ارائهی دات نت، استفاده از T4 templates جهت تولید کدها بسیار مرسوم بود؛ اما با ارائهی Source Generators، این ابزار نیز منسوخ شده در نظر گرفته میشود.
T4 Templates همانند Source Generators تنها کدها و فایلهای جدیدی را تولید میکنند و توانایی تغییر کدهای موجود را ندارند. اما مشکل مهم آن، داشتن Syntax ای خاص است که توسط اکثر IDEها پشتیبانی نمیشود. همچنین عموما اجرای آنها نیز دستی است و برخلاف Source Generators، با تغییرات کدها، به صورت خودکار به روز نمیشوند.
تغییرات زبان #C در جهت پشتیبانی از تولید کنندههای کد
از سالهای اول ارائهی زبان #C، واژهی کلیدی partial، جهت فراهم آوردن امکان تقسیم کدهای یک کلاس، به چندین فایل، میسر شد که از این قابلیت در فناوری T4 Templates زیاد استفاده میشد. اکنون با ارائهی تولید کنندههای کد، واژهی کلیدی partial را میتوان به متدها نیز افزود تا پیاده سازی اصلی آنها، در فایلی دیگر، توسط تولید کنندههای کد انجام شود. تا C# 8.0 امکان افزودن واژهی کلیدی partial به متدهای خصوصی یک کلاس و آن هم از نوع void وجود داشت و در C# 9.0 به متدهای عمومی کلاسها نیز اضافه شدهاست و اکنون این متدها میتوانند void هم نباشند:
partial class MyType { partial void OnModelCreating(string input); // C# 8.0 public partial bool IsPet(string input); // C# 9.0 } partial class MyType { public partial bool IsPet(string input) => input is "dog" or "cat" or "fish"; }
<html> <body> <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.27/vue.min.js"> new Vue({ }); </script> </body> </html>
<html> <body id="dotnettips"> <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.27/vue.min.js"> new Vue({ }); </script> </body> </html>
<html> <body id="dotnettips"> <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.27/vue.min.js"> new Vue({ el: '#dotnettips', data: { name: 'dotnettips' } }); </script> </body> </html>
استفاده از MVVM درون پروژه
//new model var SampleData =( name: 'dotnettips' )
- برای نمایش اطلاعات درون ویو جی اس باید از {{ }} استفاده کنید، تا ویژگی ساخته و فراخوانی شده را نمایش دهد.
<div id="dotnettips"> Hello {{ name }} </div>
new Vue({ el:'#dotnettips', data: SampleData })
<html> <body id="dotnettips"> <h3 id="dotnettips"> Hello {{ name }} </h3> <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.27/vue.min.js"> </script> <script type="text/javascript"> new Vue({ el: '#dotnettips', data:{ name: 'dotnettips' } }); </script> </body> </html>
<link href="@System.Web.Optimization.BundleTable.Bundles.ResolveBundleUrl("~/Content/css")"rel="stylesheet" type="text/css" /> <link href="@System.Web.Optimization.BundleTable. Bundles.ResolveBundleUrl("~/Content/themes/base/css")" rel="stylesheet" type="text/css" /> <script src="@System.Web.Optimization.BundleTable.Bundles.ResolveBundleUrl("~/Scripts/js")"></script>
<link href="/Content/css?v=ji3nO1pdg6VLv3CVUWntxgZNf1z" rel="stylesheet" type="text/css" /> <link href="/Content/themes/base/css?v=UM624qf1uFt8dYti" rel="stylesheet" type="text/css" /> <script src="/Scripts/js?v=4h5lPNUsLiFoa0vqrItjS2Jp"></script>
BundleTable.Bundles.RegisterTemplateBundles();
public static void EnableBootstrapBundle(this BundleCollection bundles) { var bootstrapCss = new Bundle("~/bootstrap/css", new CssMinify()); bootstrapCss.AddFile("~/css/bootstrap.css"); bootstrapCss.AddFile("~/css/bootstrap-responsive.css"); bootstrapCss.AddFile("~/css/application.css"); bootstrapCss.AddFile("~/css/prettify.css"); bundles.Add(bootstrapCss); var bootstrapJs = new Bundle("~/bootstrap/js", new JsMinify()); bootstrapJs.AddFile("~/js/jquery-1.7.1.js"); bootstrapJs.AddFile("~/js/bootstrap.js"); bootstrapJs.AddFile("~/js/prettify.js"); bundles.Add(bootstrapJs); }
BundleTable.Bundles.EnableBootstrapBundle();
<link href="@System.Web.Optimization.BundleTable.Bundles.ResolveBundleUrl("~/bootstrap/css")" rel="stylesheet"> <script src="@System.Web.Optimization.BundleTable.Bundles.ResolveBundleUrl("~/bootstrap/js")"></script>
<link href="/bootstrap/css?v=uBlJsuIAGAF93nUTTez8" rel="stylesheet"> <script src="/bootstrap/js?v=O6HaHC8QqtB"></script>
بررسی روش اجرای MS SQL Server Express درون یک Container
اگر مخازن Imageهای رسمی مایکروسافت را در داکرهاب بررسی کنیم، به مخازنی مانند mssql-server-windows-express ، mssql-server و یا mssql-server-linux نیز خواهیم رسید. در اینجا آخرین نگارش Image مربوط به SQL Server Express آن، حدود 7GB حجم دارد. برای دریافت آن ابتدا به Windows Containers سوئیچ کنید و سپس دستور زیر را صادر نمائید:
docker pull microsoft/mssql-server-windows-express
docker run -d -p 1433:1433 -e sa_password=<SA_PASSWORD> -e ACCEPT_EULA=Y microsoft/mssql-server-windows-express
- سوئیچ d سبب میشود تا پس از اجرای این دستور، بلافاصله به command prompt بازگشت داده شویم و SQL Server Express در background اجرا شود.
- سپس پورت 1433 میزبان به پورت 1433 کانتینر، نگاشت شدهاست که پورت استاندارد SQL Server است.
- سوئیچ e، امکان تنظیم متغیرهای محیطی را میسر میکند؛ برای مثال ورود کلمهی عبور کاربر SA و یا پذیرش مجوز آن. برای نمونه، این کلمهی عبور را مساوی password وارد کنید؛ هرچند کار نخواهد کرد، اما بررسی خطاهای به همراه آن مفید است.
- و در آخر نام image مرتبط ذکر شدهاست.
پس از اجرای این دستور، کانتینر SQL Server Express، در پس زمینه شروع به کار خواهد کرد و بلافاصله به خط فرمان بازگشت داده میشویم. در اینجا ممکن است آغاز SQL Server اندکی طول بکشد. برای اینکه دریابیم در این لحظه وضعیت پروسهی آن به چه صورتی است، دستور docker logs id را صادر کنید. پس از آن خطایی مانند password validation failed را مشاهده خواهیم کرد. عنوان میکند که پیچیدگی کلمهی عبور وارد شده کافی نیست.
یک نکته: زمانیکه دستور docker run را اجرا میکنیم، یک هش طولانی را نمایش میدهد و پس از آن به خط فرمان بازگشت داده میشویم. این هش طولانی، همان id کانتینر در حال اجرا است. برای مثال در دستور docker logs id میتوان 3 حرف ابتدای این هش را بجای id وارد کرد. البته این id را توسط دستور docker ps نیز میتوان بدست آورد.
بنابراین با توجه به اینکه دستور docker logs id، خطایی را گزارش کردهاست، توسط دستور docker stop id، این کانتینر را متوقف کرده و آنرا مجددا با کلمهی عبوری مانند pass!w0rd1 اجرا میکنیم:
docker run -d -p 1433:1433 -e sa_password=pass!w0rd1 -e ACCEPT_EULA=Y microsoft/mssql-server-windows-express
همانطور که در قسمت سوم نیز عنوان شد، اگر این کانتینر را بر روی ویندوز سرور، در حالت Windows Containers اجرا کنیم (و نه در حالت Hyper-V)، پروسههای اجرای شدهی داخل یک Container را میتوان با Job Object Idهای یکسانی که دارند، در Task Manager ویندوز، در کنار سایر پروسههای سیستم، شناسایی کرد.
اتصال به SQL Server Express اجرا شدهی داخل یک Container توسط SQL Server Management Studio
پس از اجرای SQL Server Express دخل کانتینر، مطابق تنظیمات آن، چه در سیستم میزبان و چه در داخل کانتینر، به پورت 1433 گوش فرا داده میشود. به همین منظور نیاز است IP این کانتینر را نیز بدست آوریم. برای اینکار دستور ipconfig را در سیستم میزبان صادر کنید تا بر اساس مشخصات کارت شبکهی مجازی آن، بتوان IP آنرا بدست آورد (دستور docker inspect id نیز چنین اطلاعاتی را به همراه دارد). اکنون میتوان از داخل سیستم راه دور دیگری که SQL Server Management Studio بر روی آن نصب است، توسط این IP و پورت، به SQL Server Express متصل شد.
البته در اینجا نیازی به ذکر پورت نیست؛ چون پورت 1433، شماره پورت پیشفرض است. بعد از اتصال، میتوان کارهای متداولی مانند ایجاد یک بانک اطلاعاتی جدید را انجام داد.
برای آزمایش، یکبار دستور docker ps را صادر کنید تا id این کانتینر مشخص شود. سپس دستور docker stop id را صادر کنید تا پروسه SQL Server Express خاتمه یابد. اکنون اگر در SQL Server Management Studio قصد کار با آنرا داشته باشیم، پیام عدم اتصال مشاهده میشود. اکنون برای اجرای مجدد کانتینر، دستور docker start id را صادر کنید.
بررسی روش اجرای MySQL داخل یک Container
برای اجرای MySQL نیاز است به Linux Containers سوئیچ کنیم. حجم tag ویژهی latest آن نیز حدود 138MB است که نسبت به SQL Server Express هفت گیگابایتی، بسیار کمتر است!
در همان صفحهی مستندات آن در داکرهاب، دستور اجرایی آن نیز ذکر شدهاست:
docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:tag
docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql
یک نکته: میتوان یک command prompt جدید را باز کرد و سپس دستور docker logs -f id را در آن صادر کرد. به این صورت لاگهای لحظهای یک کانتینر را نیز میتوان مشاهده کرد (f در اینجا به معنای follow است).
اکنون میخواهیم MySQL Client موجود در همین Container در حال اجرا را، اجرا کنیم (اجرای پروسهای درون یک کانتینر در حال اجرا). برای اینکار از دستور docker exec استفاده میشود:
docker ps docker exec -it id mysql --user=root --password=my-secret-pw
در اینجا توسط دستور docker exec ابتدا یک interactive shell را درخواست کردهایم (اجرای foreground یک برنامهی شل). سپس id این کانتینر باید ذکر شود. پس از آن نام فایل اجرایی MySQL Client قید شده و در پایان، نام کاربری و کلمهی عبور اتصال به آن که در دستور docker run تنظیم شدهاند، ذکر میشوند.
با اجرای این دستور، به خط فرمان MySQL Client داخل این کانتینر دسترسی پیدا میکنیم. در اینجا میتوان دستورات مختلفی را برای کار با پروسهی mysql اجرا کرد؛ مانند اجرای دستور show databases که لیست بانکهای اطلاعاتی موجود را نمایش میدهد:
mysql> show databases; use mysql; show tables; select * from user; exit;
روش مدیریت دادههای بانکهای اطلاعاتی توسط Docker
در قسمت قبل دریافتیم که لایهی رویی یک container، دارای قابلیت read/write است و برای مثال میتوان فایلهای یک وب سایت استاتیک را در آنجا کپی و سپس هاست کرد. اما این لایه، لایهی مناسبی برای ذخیره سازی دادههای یک بانک اطلاعاتی نیست. در اینجا برای مدیریت بهتر این نوع دادهها، از مفهومی به نام volume استفاده میشود.
برای درک روش مدیریت دادهها توسط داکر، دستور docker volume ls را اجرا کنید. مشاهده خواهید کرد که docker یک volume پیشفرض را نیز ایجاد کردهاست. البته با volumes پیشتر در قسمت چهارم، در بخش «روش به اشتراک گذاری فایل سیستم میزبان با کانتینرها» نیز آشنا شدهایم. این volume پیشفرض، کار ذخیره سازی اطلاعات را حتی اگر کانتینری در حال اجرا نباشد نیز انجام میدهد. وجود یک چنین قابلیتی جهت از دست نرفتن اطلاعات ارزشمند ذخیره شدهی در بانکهای اطلاعاتی بسیار ضروری است.
البته لازم به ذکر است، این volume ای را که در اینجا مشاهده میکنید، توسط Dockerfile خود mysql به صورت خودکار ایجاد میشود. برای مثال در داکرهاب، در قسمت full description این image، در ابتدای توضیحات قسمتی است به نام supported tags and respective dockerfile links. در اینجا هر tag نامبرده شده، در حقیقت لینکی است به یک Dockerfile. اگر یکی از آنها را باز کنید، چنین سطری را در آن مشاهده خواهید کرد:
VOLUME /var/lib/mysql
اینکار نه فقط برای بالابردن کارآیی اعمال read/write انجام شدهی توسط container انجام میشود، بلکه حتی اگر این کانتینر را توسط دستور docker rm id حذف کنیم، دستور docker volume ls، هنوز همان volume ای را که در حین نصب mysql به صورت خودکار ایجاد شده بود، نمایش میدهد. علت اینجا است که طول عمر این volume، وابستهی به طول عمر کانتینر آن نیست. به این ترتیب حذف تصادفی یک کانتینر، سبب از دست رفتن اطلاعات ارزشمند داخل بانک اطلاعاتی آن نمیشود.
روش تعیین صریح یک volume برای یک کانتینر بانک اطلاعاتی، توسط volumeهای نامدار
دستور docker run ای را که برای اجرای mysql صادر کردیم، یک volume خودکار را ایجاد کردهاست و اگر آنرا با دستور docker volume ls بررسی کنیم، دارای یک نام هش مانند است که به آن anonymous volume هم گفته میشود. در ادامه قصد داریم یک volume نامدار را ایجاد کنیم و سپس از آن جهت ذخیره سازی اطلاعات چندین وهله از کانتینر mysql استفاده نمائیم.
پیش از ادامه بحث، ابتدا توسط دستور docker rm id، کانتینر mysql ای را که پیشتر ایجاد کردیم حذف کنید؛ هرچند این دستور، volume متناظر با آنرا حذف نمیکند.
سپس برای اینکه یک کانتینر جدید mysql را با ذکر صریح volume آن ایجاد و اجرا کنیم، میتوان از دستور زیر استفاده کرد:
docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d -v db:/var/lib/mysql mysql
اکنون اگر دستور docker volume ls را صادر کنیم، در لیست خروجی آن، نام db قابل مشاهدهاست.
در ادامه پروسهی MySQL Client داخل این کانتینر را اجرا کرده:
docker exec -it some-mysql mysql --user=root --password=my-secret-pw
mysql> show databases; create database pets; show databases; exit;
اکنون در ابتدا این کانتینر را متوقف کرده و سپس آنرا حذف میکنیم:
docker ps docker stop id docker rm id
در ادامه مجددا همان دستور قبلی را که توسط آن volume نامداری، ایجاد کردیم، اجرا میکنیم:
docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d -v db:/var/lib/mysql mysql
docker exec -it some-mysql mysql --user=root --password=my-secret-pw
mysql> show databases;
روش حذف volumes اضافی
با توجه به اینکه volumeها، طول عمر متفاوتی را نسبت به کانتینرها دارند، ممکن است پس از مدتی فضای دیسک سخت شما را پر کنند. برای مثال به ازای هربار اجرای دستور docker run مربوط با MYSQL با نامی متفاوت، یک volume جدید نیز ایجاد میشود.
خروجی دستور docker inspect id به همراه قسمتی است به نام mounts که خاصیت name آن، دقیقا مساوی نام volume متناظر با کانتینر بررسی شدهاست. همچنین خاصیت source آن، محل دقیق ذخیره سازی این volume را بر روی فایل سیستم میزبان مشخص میکند.
برای حذف آنها، ابتدا نیاز است کانتینرها را متوقف کرد. دستور زیر تمام کانتینرهای در حال اجرا را متوقف میکند. در اینجا دستور docker ps -q، لیست id تمام کانتینرهای در حال اجرا را باز میگرداند (در این دستورات، افزودن پارامتر q، سبب بازگشت صرفا idها میشود):
docker stop $(docker ps -q)
docker rm $(docker ps -aq)
docker rm -f $(docker ps -aq)
docker volume rm $(docker volume ls -q)
docker rm -fv id
docker volume ls -f dangling=true
docker volume rm $(docker volume ls -qf dangling=true)
پیش فرضهای تعیین کلید اصلی در EF Core
به صورت پیش فرض هر خاصیتی که به نام Id و یا type name>Id> باشد، به عنوان primary key تفسیر خواهد شد؛ مانند:
public class Car { public string Id { get; set; }
public class Car { public string CarId { get; set; }
نحوهی تعیین کلید اصلی به صورت صریح
اگر یکی از دو حالت فوق برقرار نباشند، باید کلید اصلی را به نحو صریحی مشخص کرد.
الف) از طریق ویژگیها
public class Car { [Key] public string LicensePlate { get; set; }
ب) با استفاده از روش Fluent API
public class MyContext : DbContext { public DbSet<Car> Cars { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Car>() .HasKey(c => c.LicensePlate); } }
پیشنیاز کار با ویژگیها در EF Core
در اسمبلی که مدلهای موجودیتها شما قرار دارند، نیاز است وابستگی System.ComponentModel.Annotations به فایل project.json پروژه اضافه شود، تا ویژگیهایی مانند Key، شناسایی و قابل استفاده شوند:
{ "dependencies": { "System.ComponentModel.Annotations": "4.1.0" } }
تعیین کلید ترکیبی و یا Composite key
اگر نیاز است چندین خاصیت را به صورت کلید اصلی معرفی کرد که به آن composite key هم میگویند، تنها روش ممکن، استفاده از Fluent API و به صورت زیر است:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Car>() .HasKey(c => new { c.State, c.LicensePlate }); }
روشهای مختلف تولید خودکار مقادیر خواص
حالت پیش فرض تولید مقدار فیلدهای Id عددی، همان حالت خود افزایندهای است که توسط بانک اطلاعاتی کنترل میشود و یا کلید اصلی که از نوع Guid تعیین شود نیز به صورت خودکار توسط بانک اطلاعاتی در حین عملیات Add، مقدار دهی میشود (با استفاده از الگوریتم Guid سری در SQL Server).
اگر این حالات مطلوب شما نیست، حالتهای سه گانهی ذیل را میتوان استفاده کرد:
الف) هیچ دادهی خودکاری تولید نشود
برای اینکار میتوان با استفاده از ویژگی DatabaseGenerated و تنظیم مقدار آن به None، جلوی تولید خودکار کلید اصلی را گرفت. در این حالت باید هم در حین عملیات Add و هم در حین عملیات Update، مقادیر را خودتان مقدار دهی کنید:
public class Blog { [DatabaseGenerated(DatabaseGeneratedOption.None)] public int BlogId { get; set; } public string Url { get; set; } }
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>() .Property(b => b.BlogId) .ValueGeneratedNever(); }
ب) تولید دادههای خودکار فقط در حالت Add
حالت Add به این معنا است که دادههای خواص مشخصی، برای موجودیتهای «جدید»، به صورت خودکار تولید خواهند شد. اینکه آیا واقعا این مقادیر به صورت خودکار تولید میشوند یا خیر، صرفا وابستهاست به بانک اطلاعاتی در حال استفاده. برای مثال SQL Server برای نوعهای Guid، به صورت خودکار با کمک الگوریتم SQL Server sequential GUID، کار مقدار دهی یک چنین فیلدهایی را انجام میدهد.
این فیلدها باید توسط ویژگی DatabaseGenerated و با مقدار Identity مشخص شوند. در اینجا Identity به معنای فیلدهایی است که به صورت خودکار توسط بانک اطلاعاتی مقدار دهی میشوند و الزاما به کلید اصلی اشاره نمیکنند. برای مثال در موجودیت ذیل، خاصیت تاریخ ثبت رکورد، از نوع Identity مشخص شدهاست. به این معنا که در حین ثبت اولیهی رکورد آن، نیازی نیست تا خاصیت Inserted را مقدار دهی کرد. اما اینکه آیا SQL Server یک چنین کاری را به صورت خودکار انجام میدهد، پاسخ آن خیر است. SQL server فقط برای فیلدهای عددی و Guid ایی که با DatabaseGeneratedOption.Identity مزین شده باشند، مقادیر متناظری را به صورت خودکار تولید میکند. برای حالت DateTime نیاز است، مقدار پیش فرض فیلد را صریحا مشخص کرد که توسط ویژگیها میسر نیست و فقط fluent API از آن پشتیبانی میکند.
public class Blog { public int BlogId { get; set; } public string Url { get; set; } [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public DateTime Inserted { get; set; } }
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>() .Property(b => b.Inserted) .ValueGeneratedOnAdd(); }
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>() .Property(b => b.Inserted) .HasDefaultValueSql("getdate()"); }
یک نکته: در حالت DatabaseGeneratedOption.Identity و یا ValueGeneratedOnAdd فوق، اگر مقداری به این نوع فیلدها انتساب داده شده باشد که با مقدار پیش فرض آنها (property.ClrType.GetDefaultValue) متفاوت باشد، از این مقدار جدید، بجای تولید مقداری خودکار، استفاده خواهد شد. برای مثال مقدار پیش فرض رشتهها، نال، مقادیر عددی، صفر و برای Guid مقدار Guid.Empty است. اگر هر مقدار دیگری بجای اینها به فیلدهای فوق انتساب داده شوند، از آنها استفاده میشود.
ج) تولید دادههای خودکار در هر دو حالت Add و Update
تولید دادهها در حالتهای Add و Update به این معنا است که یک چنین خواصی، همواره با فراخوانی متد SaveChanges، دارای مقدار خودکار جدیدی خواهند شد و نیازی نیست در کدها مقدار دهی شوند. برای مشخص سازی این نوع خواص، از ویژگی DatabaseGenerated با مقدار Computed و یا متد ValueGeneratedOnAddOrUpdate در حالت Fluent API میتوان استفاده کرد:
public class Blog { public int BlogId { get; set; } public string Url { get; set; } [DatabaseGenerated(DatabaseGeneratedOption.Computed)] public DateTime LastUpdated { get; set; } }
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>() .Property(b => b.LastUpdated) .ValueGeneratedOnAddOrUpdate(); }
تذکر: در اینجا نیز همانند حالت ValueGeneratedOnAdd، اگر این خواص مشخص شده، دارای مقدار متفاوتی با مقدار پیش فرض آنها باشند، از این مقادیر جدید بجای تولید خودکار مقادیر استفاده خواهد شد.
خواص محاسباتی (Computed Columns) و تفاوت آنها با DatabaseGeneratedOption.Computed
خواص محاسباتی (Computed Columns)، خواصی هستند که مقادیر آنها در بانک اطلاعاتی محاسبه میشوند و کاملا متفاوت هستند با DatabaseGeneratedOption.Computed که مفهوم دیگری دارد. DatabaseGeneratedOption.Computed به این معنا است که این فیلد خاص، با هر بار فراخوانی SaveChanges باید مقدار محاسبه شدهی جدیدی را داشته باشد و روش تولید این مقدار خودکار، یا بر اساس Guidهای سری است، یا توسط فیلدهای خود افزایندهی عددی و یا از طریق مقادیر پیش فرضی مانند getdate در حین ثبت یا به روز رسانی، مقدار دهی میشوند. اما خواص محاسباتی، یکی از امکانات «گزارشگیری سریع» SQL Server هستند و به نحو ذیل، تنها توسط Fluent API قابل تنظیم میباشند:
public class Person { public int PersonId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string DisplayName { get; set; } } public class MyContext : DbContext { public DbSet<Person> People { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Person>() .Property(p => p.DisplayName) .HasComputedColumnSql("[LastName] + ', ' + [FirstName]"); } }
امکان تعریف Sequence در EF Core 1.0
Sequence قابلیتی است که به SQL Server 2012 اضافه شدهاست و توضیحات بیشتر آنرا در مطلب «نحوه ایجاد Sequence و استفاده آن در Sql Server 2012» میتوانید مطالعه کنید.
در EF Core، امکان مدلسازی Sequence نیز پیش بینی شدهاست. آنها به صورت پیش فرض در مدلها ذکر نمیشوند و همچنین وابستگی به جدول خاصی ندارند. به همین جهت امکان تعریف آنها صرفا توسط Fluent API وجود دارد:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.HasSequence<int>("OrderNumbers", schema: "shared") .StartsAt(1000).IncrementsBy(5); modelBuilder.Entity<Order>() .Property(o => o.OrderNo) .HasDefaultValueSql("NEXT VALUE FOR shared.OrderNumbers"); }
در مثال فوق، ابتدا یک Sequence نمونه به نام OrderNumbers تعریف شدهاست که از عدد 1000 شروع شده و واحد افزایش آن 5 است. سپس از این نام در قسمت مقدار پیش فرض ستون OrderNo استفاده شدهاست.
و یا از Sequence ها میتوان برای تعیین مقدار پیش فرض Primary key بجای حالت identity خود افزایش یابنده استفاده کرد:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.HasSequence<int>("PrimaryKeyWithSequenceSequence"); modelBuilder.Entity<PrimaryKeyWithSequence>(entity => { entity.Property(e => e.PrimaryKeyWithSequenceId).HasDefaultValueSql("NEXT VALUE FOR [PrimaryKeyWithSequenceSequence]"); }); }
[PrimaryKeyWithSequenceSequence] دریافت و سپس بجای فیلد id درج میشود.
به این روش الگوریتم Hi-Low هم میگویند که یکی از مهمترین اهداف آن داشتن یک سری Id منحصربفرد، جهت بالابردن سرعت insertها در یک batch است. در حالت عادی insertها، ابتدا یک insert انجام میشود، سپس کوئری گرفته شده و آخرین Id درج شده به کلاینت بازگشت داده میشود. این روش، برای انجام تنها یک insert، سریع است. اما برای batch insert، به شدت کارآیی پایینی دارد. به همین جهت دسترسی به بازهای از اعداد منحصربفرد، پیش از شروع به insert تعداد زیادی رکورد، سرعت نهایی کار را بالا میبرد.
نحوهی تعریف ایندکسها در EF Core 1.0
برای افزودن ایندکسها به EF Core 1.0، تنها روش میسر، استفاده از Fluent API است (و برخلاف EF 6.x از روش data annotations فعلا پشتیبانی نمیکند؛ هرچند API جدید آن نسبت به EF 6.x بسیار واضحتر است و با ابهامات کمتر).
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>() .HasIndex(b => b.Url) .HasName("Index_Url");
modelBuilder.Entity<Blog>().HasIndex(b => b.Url).HasName("Index_Url").IsUnique();
modelBuilder.Entity<Person>() .HasIndex(idx => new { idx.FirstName, idx.LastName }) .IsUnique();
یک نکته: اگر از پروایدر SQL Server استفاده میکنید، میتوان متد الحاقی ویژهای را به نام ForSqlServerIsClustered نیز برای تعریف clustered indexes، در این زنجیره ذکر کرد.
امکان تعریف Alternate Keys در EF Core 1.0
به Unique Constraints در EF Core، نام Alternate Keys را دادهاند و این مورد نیز تنها از طریق Fluent API قابل تنظیم است:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Car>() .HasAlternateKey(c => c.LicensePlate) .HasName("AlteranteKey_LicensePlate"); }
اگر متد HasName در اینجا ذکر نشود، نام پیش فرض آن <type name>_<property name> خواهد بود و اگر همانند composite keys و یا ایندکسهای ترکیبی، چند خاصیت ذکر شوند، قسمت property name به جمع نام تمام خواص ذکر شده و جدا شدهی با _ تنظیم میشود.
برای نمونه اگر یک Alternate Key ترکیبی را به صورت ذیل تعریف کنیم:
modelBuilder.Entity<Person>() .HasAlternateKey(x => new { x.FirstName, x.LastName });
table.UniqueConstraint("AK_Persons_FirstName_LastName", x => new { x.FirstName, x.LastName });
سؤال: یک Unique Constraint با Unique Index چه تفاوتی دارد؟
در پشت صحنه، پیاده سازی یک Unique Constraint با Unique Index تفاوتی ندارند. فقط از دیدگاه روشنتر شدن مقصود، استفادهی از Unique Constraint ترجیح داده میشود.
البته از دیدگاه بانک اطلاعاتی پیاده سازی کننده نیز برای نمونه SQL Server، این تفاوتها وجود دارند:
الف) یک Unique Constraint را نمیتوان غیرفعال کرد؛ برخلاف Unique Indexها.
ب) Unique Constraintها موارد اضافهتری را مانند FILLFACTOR و IGNORE_DUP_KEY نیز میتوانند تنظیم کنند.
ج) امکان تعریف فیلترها برای Unique Indexها وجود دارد؛ برخلاف Unique Constraint ها.
که البته از دیدگاه EF، این سه مورد اهمیتی ندارند و بیشتر روشنتر شدن مقصود، هدف اصلی آنها است.