tag:blogger.com,1999:blog-78158219156510485782024-03-13T06:07:07.171+03:30.NET Tipsوحيد نصيريhttp://www.blogger.com/profile/04454130327051686471noreply@blogger.comBlogger846125tag:blogger.com,1999:blog-7815821915651048578.post-45905603221107921532012-05-23T12:08:00.000+04:302012-05-23T12:08:00.608+04:30در مورد ادامه ...<div dir="rtl" style="text-align: right;" trbidi="on"><br />
در حال تهيه يك سيستم blogging هستم كه بشود آنرا چند كاربره يا چند نويسندهاي كرد. اين سيستم جديد تا يك ماه ديگر جايگزين سيستم وصله پينهاي جاري خواهد شد (متن يكجا، css لينك شده از يك سايت ديگر، syntax highlighting از يك سايت ديگر، عكسها در يك سرور مجزا، كامنتها كلا در يك سرور ديگر (ديسكاس) بجز هاست بلاگر، لينكهاي روزانه در يك سايت ثالث، فايلهاي مثالها در سروري ديگر! هاست اصلي بلاگ (بلاگر) هم كلا در ايران فيلتر است!). البته همين سيستم وصله پينهاي 4 سال است كه داره كار ميكنه!<br />
به همين دليل براي پيدا كردن وقت جهت بازنويسي سيستم، اين سايت تا يك ماه ديگر در حالت تعليق خواهد بود.<br />
در اين بين اگر علاقمند به مشاركت در بلاگ جديد (با همين آدرس با همين عنوان) به عنوان نويسنده هستيد، يك ايميل به من ارسال كنيد (vahid_nasiri در ياهو). موضوع سايت مشخص است. اهداف آن هم به همين ترتيب. فقط فني است. تنها مطالب فني قرار است ارسال شود. پس از آماده شدن سايت جديد، يك اكانت نويسنده مطلب براي شما ارسال خواهد شد. با تشكر از همكاري شما.<br />
<br />
</div>وحيد نصيريhttp://www.blogger.com/profile/04454130327051686471noreply@blogger.comtag:blogger.com,1999:blog-7815821915651048578.post-76268284537490388362012-05-19T12:20:00.000+04:302012-05-19T12:20:07.439+04:30EF Code First #15<div dir="rtl" style="text-align: right;" trbidi="on"><br />
<b>EF Code first و بانكهاي اطلاعاتي متفاوت</b><br />
<br />
در آخرين قسمت از سري EF Code first بد نيست نحوه استفاده از بانكهاي اطلاعاتي ديگري را بجز SQL Server نيز بررسي كنيم. در اينجا كلاسهاي مدل و كدهاي مورد استفاده نيز همانند قسمت 14 است و تنها به ذكر تفاوتها و نكات مرتبط اكتفاء خواهد شد.<br />
<br />
<br />
<b>حالت كلي پشتيباني از بانكهاي اطلاعاتي مختلف توسط EF Code first</b><br />
<br />
EF Code first با كليه پروايدرهاي تهيه شده براي ADO.NET 3.5 كه پشتيباني از EF را لحاظ كرده باشند، به خوبي كار ميكند. پروايدرهاي مخصوص ADO.NET 4.0، تنها سه گزينه DeleteDatabase/CreateDatabase/DatabaseExists را نسبت به نگارش قبلي بيشتر دارند و EF Code first ويژگيهاي بيشتري را طلب نميكند.<br />
بنابراين اگر حين استفاده از پروايدر ADO.NET مخصوص بانك اطلاعاتي خاصي با پيغام «CreateDatabase is not supported by the provider» مواجه شديد، به اين معنا است كه اين پروايدر براي دات نت 4 به روز نشده است. اما به اين معنا نيست كه با EF Code first كار نميكند. فقط بايد يك ديتابيس خالي از پيش تهيه شده را به برنامه معرفي كنيد تا مباحث Database Migrations به خوبي كار كنند؛ يا اينكه كلا ميتوانيد Database Migrations را خاموش كرده (متد Database.SetInitializer را با پارامتر نال فراخواني كنيد) و فيلدها و جداول را دستي ايجاد كنيد.<br />
<br />
<br />
<b>استفاده از EF Code first با SQLite</b><br />
<br />
براي استفاده از SQLite در دات نت ابتدا نياز به پروايدر ADO.NET آن است: «<a href="http://www.dotnettips.info/2011/06/sqlite.html">مكان دريافت درايورهاي جديد SQLite مخصوص دات نت</a>»<br />
ضمن اينكه به نكته «<a href="http://www.dotnettips.info/2010/11/2-4.html">استفاده از اسمبليهاي دات نت 2 در يك پروژه دات نت 4</a>» نيز بايد دقت داشت. <br />
و يكي از بهترين management studio هايي كه براي آن تهيه شده: «<a href="https://addons.mozilla.org/en-US/firefox/addon/sqlite-manager/">SQLite Manager</a>»<br />
پس از دريافت پروايدر آن، ارجاعي را به اسمبلي System.Data.SQLite.dll به برنامه اضافه كنيد.<br />
سپس فايل كانفيگ برنامه را به نحو زير تغيير دهيد:<br />
<div align="left" dir="ltr"><pre language="XML" name="code"><?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=4.3.1.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
</configSections>
<startup useLegacyV2RuntimeActivationPolicy="true">
<supportedRuntime version="v4.0"/>
</startup>
<connectionStrings>
<clear/>
<add name="Sample09Context"
connectionString="Data Source=CodeFirst.db"
providerName="System.Data.SQLite"/>
</connectionStrings>
</configuration>
</pre></div><br />
همانطور كه ملاحظه ميكنيد، تفاوت آن با قبل، تغيير connectionString و providerName است.<br />
اكنون اگر همان برنامه قسمت قبل را اجرا كنيم به خطاي زير برخواهيم خورد:<br />
«The given key was not present in the dictionary»<br />
در اين مورد هم توضيح داده شد. سه گزينه DeleteDatabase/CreateDatabase/DatabaseExists در پروايدر جاري SQLite براي دات نت وجود ندارد. به همين جهت نياز است فايل «CodeFirst.db» ذكر شده در كانكشن استرينگ را ابتدا دستي درست كرد. <br />
براي مثال از افزونه SQLite Manager استفاده كنيد. ابتدا يك بانك اطلاعاتي خالي را درست كرده و سپس دستورات زير را بر روي بانك اطلاعاتي اجرا كنيد تا دو جدول خالي را ايجاد كند (در برگه Execute sql افزونه SQLite Manager):<br />
<br />
<div align="left" dir="ltr"><pre language="Sql" name="code">CREATE TABLE [Payees](
[Id] [integer] PRIMARY KEY AUTOINCREMENT NOT NULL,
[Name] [text] NULL,
[CreatedOn] [datetime] NOT NULL,
[CreatedBy] [text] NULL,
[ModifiedOn] [datetime] NOT NULL,
[ModifiedBy] [text] NULL
);
CREATE TABLE [Bills](
[Id] [integer] PRIMARY KEY AUTOINCREMENT NOT NULL,
[Amount] [float](18, 2) NOT NULL,
[Description] [text] NULL,
[CreatedOn] [datetime] NOT NULL,
[CreatedBy] [text] NULL,
[ModifiedOn] [datetime] NOT NULL,
[ModifiedBy] [text] NULL,
[Payee_Id] [integer] NULL
);
</pre></div><br />
سپس سطر زير را نيز به ابتداي برنامه اضافه كنيد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">Database.SetInitializer<Sample09Context>(null);
</pre></div><br />
به اين ترتيب database migrations خاموش ميشود و اكنون برنامه بدون مشكل كار خواهد كرد.<br />
فقط بايد به يك سري نكات مانند نوع دادهها در بانكهاي اطلاعاتي مختلف دقت داشت. براي مثال integer در اينجا از نوع Int64 است؛ بنابراين در برنامه نيز بايد به همين ترتيب تعريف شود تا نگاشتها به درستي انجام شوند.<br />
<br />
در كل تنها مشكل پروايدر فعلي SQLite عدم پشتيباني از مباحث database migrations است. اين مورد را خاموش كرده و تغييرات ساختار بانك اطلاعاتي را به صورت دستي به بانك اطلاعاتي اعمال كنيد. بدون مشكل كار خواهد كرد.<br />
<br />
البته اگر به دنبال پروايدري تجاري با پشتيباني از آخرين نگارش EF Code first هستيد، گزينه زير نيز مهيا است:<br />
<a href="http://devart.com/dotconnect/sqlite/">http://devart.com/dotconnect/sqlite/</a><br />
براي مثال اگر علاقمند به استفاده از حالت تشكيل بانك اطلاعاتي SQLite در حافظه هستيد (با رشته اتصالي ويژه Data Source=:memory:;Version=3;New=True;)، فعلا تنها گزينه مهيا استفاده از پروايدر تجاري فوق است؛ زيرا مبحث Database Migrations را به خوبي پشتيباني ميكند.<br />
<br />
<br />
<br />
<b>استفاده از EF Code first با SQL Server CE</b><br />
<br />
قبلا در مورد «<a href="http://www.dotnettips.info/2012/03/sql-ce-nhibernate.html">استفاده از SQL-CE به كمك NHibernate</a>» مطلبي را در اين سايت مطالعه كردهايد. سه مورد اول آن با EF Code first يكي است و تفاوتي نميكند (يك سري بحث عمومي مشترك است). البته با يك تفاوت؛ در اينجا EF Code first قادر است يك بانك اطلاعاتي خالي SQL Server CE را به صورت خودكار ايجاد كند و نيازي نيست تا آنرا دستي ايجاد كرد. مباحث database migrations و به روز رساني خودكار ساختار بانك اطلاعاتي نيز در اينجا پشتيباني ميشود.<br />
براي استفاده از آن ابتدا ارجاعي را به اسمبلي System.Data.SqlServerCe.dll قرار گرفته در مسير Program Files\Microsoft SQL Server Compact Edition\v4.0\Desktop اضافه كنيد.<br />
سپس رشته اتصالي به بانك اطلاعاتي و providerName را به نحو زير تغيير دهيد:<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code"><connectionStrings>
<clear/>
<add name="Sample09Context"
connectionString="Data Source=mydb.sdf;Password=1234;Encrypt Database=True"
providerName="System.Data.SqlServerCE.4.0"/>
</connectionStrings>
</pre></div><br />
<br />
بدون نياز به هيچگونه تغييري در كدهاي برنامه، همين مقدار تغيير در تنظيمات ابتدايي برنامه براي كار با SQL Server CE كافي است.<br />
ضمنا مشكلي هم با فيلد Identity در آخرين نگارش EF Code first وجود ندارد؛ برخلاف حالت database first آن كه پيشتر اين اجازه را نميداد و خطاي «Server-generated keys and server-generated values are not supported by SQL Server Compact» را ظاهر ميكرد.<br />
<br />
<br />
<br />
<b>استفاده از EF Code first با MySQL</b><br />
<br />
براي استفاده از EF Code first با MySQL (نگارش 5 به بعد البته) ابتدا نياز است پروايدر مخصوص ADO.NET آنرا دريافت كرد: (<a href="http://dev.mysql.com/downloads/connector/net/">^</a>)<br />
كه از EF نيز پشتيباني ميكند. پس از نصب آن، ارجاعي را به اسمبلي MySql.Data.dll قرار گرفته در مسير Program Files\MySQL\MySQL Connector Net 6.5.4\Assemblies\v4.0 به پروژه اضافه نمائيد. <br />
سپس رشته اتصالي و providerName را به نحو زير تغيير دهيد:<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code"><connectionStrings>
<clear/>
<add name="Sample09Context"
connectionString="Datasource=localhost; Database=testdb2; Uid=root; Pwd=123;"
providerName="MySql.Data.MySqlClient"/>
</connectionStrings>
<system.data>
<DbProviderFactories>
<remove invariant="MySql.Data.MySqlClient"/>
<add name="MySQL Data Provider"
invariant="MySql.Data.MySqlClient"
description=".Net Framework Data Provider for MySQL"
type="MySql.Data.MySqlClient.MySqlClientFactory, MySql.Data, Version=6.5.4.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d" />
</DbProviderFactories>
</system.data>
</pre></div><br />
همانطور كه مشاهده ميكنيد در اينجا شماره نگارش دقيق پروايدر مورد استفاده نيز ذكر شده است. براي مثال اگر چندين پروايدر روي سيستم نصب است، با مقدار دهي DbProviderFactories ميتوان از نگارش مخصوصي استفاده كرد.<br />
<br />
با اين تغييرات پس از اجراي برنامه قسمت قبل، به خطاي زير برخواهيم خورد:<br />
The given key was not present in the dictionary<br />
<br />
توضيحات اين مورد با قسمت SQLite يكي است؛ به عبارتي نياز است بانك اطلاعاتي testdb را دستي درست كرد. همچنين جداول و فيلدها را نيز بايد دستي ايجاد كرد و database migrations را نيز بايد خاموش كرد (پارامتر Database.SetInitializer را به نال مقدار دهي كنيد).<br />
براي اين منظور يك ديتابيس خالي را ايجاد كرده و سپس دو جدول زير را به آن اضافه كنيد:<br />
<br />
<div align="left" dir="ltr"><pre language="Sql" name="code">CREATE TABLE IF NOT EXISTS `bills` (
`Id` int(11) NOT NULL AUTO_INCREMENT,
`Amount` float DEFAULT NULL,
`Description` varchar(400) CHARACTER SET utf8 COLLATE utf8_persian_ci NOT NULL,
`CreatedOn` datetime NOT NULL,
`CreatedBy` varchar(400) CHARACTER SET utf8 COLLATE utf8_persian_ci NOT NULL,
`ModifiedOn` datetime NOT NULL,
`ModifiedBy` varchar(400) CHARACTER SET utf8 COLLATE utf8_persian_ci NOT NULL,
`Payee_Id` int(11) NOT NULL,
PRIMARY KEY (`Id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_persian_ci AUTO_INCREMENT=1 ;
CREATE TABLE IF NOT EXISTS `payees` (
`Id` int(11) NOT NULL AUTO_INCREMENT,
`Name` varchar(400) CHARACTER SET utf8 COLLATE utf8_persian_ci NOT NULL,
`CreatedOn` datetime NOT NULL,
`CreatedBy` varchar(400) CHARACTER SET utf8 COLLATE utf8_persian_ci NOT NULL,
`ModifiedOn` datetime NOT NULL,
`ModifiedBy` varchar(400) CHARACTER SET utf8 COLLATE utf8_persian_ci NOT NULL,
PRIMARY KEY (`Id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_persian_ci AUTO_INCREMENT=1 ;
</pre></div><br />
پس از اين تغييرات، برنامه بدون مشكل اجرا خواهد شد (ايجاد بانك اطلاعاتي خالي به همراه ايجاد ساختار جداول و خاموش كردن database migrations كه توسط اين پروايدر پشتيباني نميشود).<br />
<br />
به علاوه پروايدر تجاري ديگري هم در سايت devart.com براي MySQL و EF Code first <a href="http://www.devart.com/dotconnect/mysql/">مهيا است</a> كه مباحث database migrations را به خوبي مديريت ميكند.<br />
<br />
<br />
<b>مشكل!</b><br />
اگر به همين نحو برنامه را اجرا كنيم، فيلدهاي يونيكد فارسي ثبت شده در MySQL با «??????? ?? ????» مقدار دهي خواهند شد و تنظيم CHARACTER SET utf8 COLLATE utf8_persian_ci نيز كافي نبوده است (اين مورد با SQLite يا نگارشهاي مختلف SQL Server بدون مشكل كار ميكند و نياز به تنظيم اضافهتري ندارد):<br />
<br />
<div align="left" dir="ltr"><pre language="Sql" name="code">ALTER TABLE `bills` DEFAULT CHARACTER SET utf8 COLLATE utf8_persian_ci
</pre></div><br />
براي رفع اين مشكل توصيه شده است كه CharSet=UTF8 را به رشته اتصالي به بانك اطلاعاتي اضافه كنيم. اما در اين حالت خطاي زير ظاهر ميشود:<br />
The provider did not return a ProviderManifestToken string<br />
اين مورد فقط به اشتباه بودن تعاريف رشته اتصالي بر ميگردد؛ يا عدم پشتيباني از تنظيم اضافهاي كه در رشته اتصالي ذكر شده است.<br />
مقدار صحيح آن دقيقا مساوي CHARSET=utf8 است (با همين نگارش و رعايت كوچكي و بزرگي حروف؛ مهم!):<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code"><connectionStrings>
<clear/>
<add name="Sample09Context"
connectionString="Datasource=localhost; Database=testdb; Uid=root; Pwd=123;CHARSET=utf8"
providerName="MySql.Data.MySqlClient"/>
</connectionStrings>
</pre></div><br />
<br />
به اين ترتيب، مشكل ثبت عبارات يونيكد فارسي برطرف ميشود (البته جدول هم بهتر است به DEFAULT CHARACTER SET utf8 COLLATE utf8_persian_ci تغيير پيدا كند؛ مطابق دستور Alter ايي كه در بالا ذكر شد).<br />
<br />
</div>وحيد نصيريhttp://www.blogger.com/profile/04454130327051686471noreply@blogger.comtag:blogger.com,1999:blog-7815821915651048578.post-29543768438411802592012-05-18T17:17:00.000+04:302012-05-18T17:17:35.077+04:30EF Code First #14<div dir="rtl" style="text-align: right;" trbidi="on"><br />
<b>رديابي تغييرات در EF Code first</b><br />
<br />
EF از DbContext براي ذخيره اطلاعات مرتبط با تغييرات موجوديتهاي تحت كنترل خود كمك ميگيرد. اين نوع اطلاعات توسط Change Tracker API جهت بررسي وضعيت فعلي يك شيء، مقادير اصلي و مقادير تغيير كرده آن در دسترس هستند. همچنين در اينجا امكان بارگذاري مجدد اطلاعات موجوديتها از بانك اطلاعاتي جهت اطمينان از به روز بودن آنها تدارك ديده شده است. سادهترين روش دستيابي به اين اطلاعات، استفاده از متد context.Entry ميباشد كه يك وهله از موجوديتي خاص را دريافت كرده و سپس به كمك خاصيت State خروجي آن، وضعيتهايي مانند Unchanged يا Modified را ميتوان به دست آورد. علاوه بر آن خروجي متد context.Entry، داراي خواصي مانند CurrentValues و OriginalValues نيز ميباشد. OriginalValues شامل مقادير خواص موجوديت درست در لحظه اولين بارگذاري در DbContext برنامه است. CurrentValues مقادير جاري و تغيير يافته موجوديت را باز ميگرداند. به علاوه اين خروجي امكان فراخواني متد GetDatabaseValues را جهت بدست آوردن مقادير جديد ذخيره شده در بانك اطلاعاتي نيز ارائه ميدهد. ممكن است در اين بين، خارج از Context جاري، اطلاعات بانك اطلاعاتي توسط كاربر ديگري تغيير كرده باشد. به كمك GetDatabaseValues ميتوان به اين نوع اطلاعات نيز دست يافت.<br />
حداقل چهار كاربرد عملي جالب را از اطلاعات موجود در Change Tracker API ميتوان مثال زد كه در ادامه به بررسي آنها خواهيم پرداخت.<br />
<br />
<br />
<b>كلاسهاي مدل مثال جاري</b><br />
<br />
در اينجا يك رابطه many-to-one بين جدول هزينههاي اقلام خريداري شده يك شخص و جدول فروشندگان تعريف شده است:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System;
namespace EF_Sample09.DomainClasses
{
public abstract class BaseEntity
{
public int Id { get; set; }
public DateTime CreatedOn { set; get; }
public string CreatedBy { set; get; }
public DateTime ModifiedOn { set; get; }
public string ModifiedBy { set; get; }
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System;
namespace EF_Sample09.DomainClasses
{
public class Bill : BaseEntity
{
public decimal Amount { set; get; }
public string Description { get; set; }
public virtual Payee Payee { get; set; }
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Collections.Generic;
namespace EF_Sample09.DomainClasses
{
public class Payee : BaseEntity
{
public string Name { get; set; }
public virtual ICollection<Bill> Bills { set; get; }
}
}
</pre></div><br />
<br />
به علاوه همانطور كه ملاحظه ميكنيد، اين كلاسها از يك abstract class به نام BaseEntity مشتق شدهاند. هدف از اين كلاس پايه تنها تامين يك سري خواص تكراري در كلاسهاي برنامه است و هدف از آن، مباحث ارث بري مانند TPH، TPT و TPC نيست. <br />
به همين جهت براي اينكه اين كلاس پايه تبديل به يك جدول مجزا و يا سبب يكي شدن تمام كلاسها در يك جدول نشود، تنها كافي است آنرا به عنوان DbSet معرفي نكنيم و يا ميتوان از متد Ignore نيز استفاده كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Data.Entity;
using EF_Sample09.DomainClasses;
namespace EF_Sample09.DataLayer.Context
{
public class Sample09Context : MyDbContextBase
{
public DbSet<Bill> Bills { set; get; }
public DbSet<Payee> Payees { set; get; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Ignore<BaseEntity>();
base.OnModelCreating(modelBuilder);
}
}
}
</pre></div><br />
<br />
<br />
<b>الف) به روز رساني اطلاعات Context در صورتيكه از متد context.Database.ExecuteSqlCommand مستقيما استفاده شود</b><br />
<br />
در قسمت قبل با متد context.Database.ExecuteSqlCommand براي اجراي مستقيم عبارات SQL بر روي بانك اطلاعاتي آشنا شديم. اگر اين متد در نيمه كار يك Context فراخواني شود، به معناي كنار گذاشتن Change Tracker API ميباشد؛ زيرا اكنون در سمت بانك اطلاعاتي اتفاقاتي رخ دادهاند كه هنوز در Context جاري كلاينت منعكس نشدهاند:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System;
using System.Data.Entity;
using EF_Sample09.DataLayer.Context;
using EF_Sample09.DomainClasses;
namespace EF_Sample09
{
class Program
{
static void Main(string[] args)
{
Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample09Context, Configuration>());
using (var db = new Sample09Context())
{
var payee = new Payee { Name = "فروشگاه سر كوچه" };
var bill = new Bill { Amount = 4900, Description = "يك سطل ماست", Payee = payee };
db.Bills.Add(bill);
db.SaveChanges();
}
using (var db = new Sample09Context())
{
var bill1 = db.Bills.Find(1);
bill1.Description = "ماست";
db.Database.ExecuteSqlCommand("Update Bills set Description=N'سطل ماست' where id=1");
Console.WriteLine(bill1.Description);
db.Entry(bill1).Reload(); //Refreshing an Entity from the Database
Console.WriteLine(bill1.Description);
db.SaveChanges();
}
}
}
}
</pre></div><br />
در اين مثال ابتدا دو ركورد به بانك اطلاعاتي اضافه ميشوند. سپس توسط متد db.Bills.Find، اولين ركورد جدول Bills بازگشت داده ميشود. در ادامه، خاصيت توضيحات آن به روز شده و سپس با استفاده از متد db.Database.ExecuteSqlCommand نيز بار ديگر خاصيت توضيحات اولين ركورد به روز خواهد شد.<br />
اكنون اگر مقدار bill1.Description را بررسي كنيم، هنوز داراي مقدار پيش از فراخواني db.Database.ExecuteSqlCommand ميباشد، زيرا تغييرات سمت بانك اطلاعاتي هنوز به Context مورد استفاده منعكس نشده است.<br />
در اينجا براي هماهنگي كلاينت با بانك اطلاعاتي، كافي است متد Reload را بر روي موجوديت مورد نظر فراخواني كنيم.<br />
<br />
<br />
<br />
<b>ب) يكسان سازي ي و ك اطلاعات رشتهاي دريافتي پيش از ذخيره سازي در بانك اطلاعاتي</b><br />
<br />
يكي از الزامات برنامههاي فارسي، يكسان سازي ي و ك دريافتي از كاربر است. براي اين منظور بايد پيش از فراخواني متد SaveChanges نهايي، مقادير رشتهاي كليه موجوديتها را يافته و به روز رساني كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Reflection;
using EF_Sample09.DataLayer.Toolkit;
using EF_Sample09.DomainClasses;
namespace EF_Sample09.DataLayer.Context
{
public class MyDbContextBase : DbContext
{
public void RejectChanges()
{
foreach (var entry in this.ChangeTracker.Entries())
{
switch (entry.State)
{
case EntityState.Modified:
entry.State = EntityState.Unchanged;
break;
case EntityState.Added:
entry.State = EntityState.Detached;
break;
}
}
}
public override int SaveChanges()
{
applyCorrectYeKe();
auditFields();
return base.SaveChanges();
}
private void applyCorrectYeKe()
{
//پيدا كردن موجوديتهاي تغيير كرده
var changedEntities = this.ChangeTracker
.Entries()
.Where(x => x.State == EntityState.Added || x.State == EntityState.Modified);
foreach (var item in changedEntities)
{
if (item.Entity == null) continue;
//يافتن خواص قابل تنظيم و رشتهاي اين موجوديتها
var propertyInfos = item.Entity.GetType().GetProperties(
BindingFlags.Public | BindingFlags.Instance
).Where(p => p.CanRead && p.CanWrite && p.PropertyType == typeof(string));
var pr = new PropertyReflector();
//اعمال يكپارچگي نهايي
foreach (var propertyInfo in propertyInfos)
{
var propName = propertyInfo.Name;
var val = pr.GetValue(item.Entity, propName);
if (val != null)
{
var newVal = val.ToString().Replace("ی", "ي").Replace("ک", "ك");
if (newVal == val.ToString()) continue;
pr.SetValue(item.Entity, propName, newVal);
}
}
}
}
private void auditFields()
{
// var auditUser = User.Identity.Name; // in web apps
var auditDate = DateTime.Now;
foreach (var entry in this.ChangeTracker.Entries<BaseEntity>())
{
// Note: You must add a reference to assembly : System.Data.Entity
switch (entry.State)
{
case EntityState.Added:
entry.Entity.CreatedOn = auditDate;
entry.Entity.ModifiedOn = auditDate;
entry.Entity.CreatedBy = "auditUser";
entry.Entity.ModifiedBy = "auditUser";
break;
case EntityState.Modified:
entry.Entity.ModifiedOn = auditDate;
entry.Entity.ModifiedBy = "auditUser";
break;
}
}
}
}
}
</pre></div><br />
<br />
اگر به كلاس Context مثال جاري كه در ابتداي بحث معرفي شد دقت كرده باشيد به اين نحو تعريف شده است (بجاي DbContext از MyDbContextBase مشتق شده):<br />
public class Sample09Context : MyDbContextBase<br />
علت هم اين است كه يك سري كد تكراري را كه ميتوان در تمام Contextها قرار داد، بهتر است در يك كلاس پايه تعريف كرده و سپس از آن ارث بري كرد.<br />
تعاريف كامل كلاس MyDbContextBase را در كدهاي فوق ملاحظه ميكنيد.<br />
در اينجا كار با تحريف متد SaveChanges شروع ميشود. سپس در متد applyCorrectYeKe كليه موجوديتهاي تحت نظر ChangeTracker كه تغيير كرده باشند يا به آن اضافه شده باشند، يافت شده و سپس خواص رشتهاي آنها جهت يكساني سازي ي و ك، بررسي ميشوند.<br />
<br />
<br />
<b>ج) سادهتر سازي به روز رساني فيلدهاي بازبيني يك ركورد مانند DateCreated، DateLastUpdated و امثال آن بر اساس وضعيت جاري يك موجوديت</b><br />
<br />
در كلاس MyDbContextBase فوق، كار متد auditFields، مقدار دهي خودكار خواص تكراري تاريخ ايجاد، تاريخ به روز رساني، شخص ايجاد كننده و شخص تغيير دهنده يك ركورد است. به كمك ChangeTracker ميتوان به موجوديتهايي از نوع كلاس پايه BaseEntity دست يافت. در اينجا اگر entry.State آنها مساوي EntityState.Added بود، هر چهار خاصيت ياد شده به روز ميشوند. اگر حالت موجوديت جاري، EntityState.Modified بود، تنها خواص مرتبط با تغييرات ركورد به روز خواهند شد.<br />
به اين ترتيب ديگر نيازي نيست تا در حين ثبت يا ويرايش اطلاعات برنامه نگران اين چهار خاصيت باشيم؛ زيرا به صورت خودكار مقدار دهي خواهند شد.<br />
<br />
<br />
<b>د) پياده سازي قابليت لغو تغييرات در برنامه</b><br />
<br />
علاوه بر اينها در كلاس MyDbContextBase، متد RejectChanges نيز تعريف شده است تا بتوان در صورت نياز، حالت موجوديتهاي تغيير كرده يا اضافه شده را به حالت پيش از عمليات، بازگرداند.<br />
<br />
<br />
<br />
</div>وحيد نصيريhttp://www.blogger.com/profile/04454130327051686471noreply@blogger.comtag:blogger.com,1999:blog-7815821915651048578.post-10100702138908746972012-05-16T10:06:00.000+04:302012-05-16T10:06:46.333+04:30EF Code First #13<div dir="rtl" style="text-align: right;" trbidi="on"><br />
<b>استفاده مستقيم از عبارات SQL در EF Code first</b><br />
<br />
طراحي اكثر ORMهاي موجود به نحوي است كه برنامه نهايي شما را مستقل از بانك اطلاعاتي كنند و اين پروايدر نهايي است كه معادلهاي صحيح بسياري از توابع توكار بانك اطلاعاتي مورد استفاده را در اختيار EF قرار ميدهد. براي مثال در يك بانك اطلاعاتي تابعي به نام substr تعريف شده، در بانك اطلاعاتي ديگري همين تابع substring نام دارد. اگر برنامه را به كمك كوئريهاي LINQ تهيه كنيم، نهايتا پروايدر نهايي مخصوص بانك اطلاعاتي مورد استفاده است كه اين معادلها را در اختيار EF قرار ميدهد و برنامه بدون مشكل كار خواهد كرد. اما يك سري از موارد شايد معادلي در ساير بانكهاي اطلاعاتي نداشته باشند؛ براي مثال رويههاي ذخيره شده يا توابع تعريف شده توسط كاربر. امكان استفاده از يك چنين تواناييهايي نيز با اجراي مستقيم عبارات SQL در EF Code first پيش بيني شده و بديهي است در اين حالت برنامه به يك بانك اطلاعاتي خاص گره خواهد خورد؛ همچنين مزيت استفاده از كوئريهاي Strongly typed تحت نظر كامپايلر را نيز از دست خواهيم داد. به علاوه بايد به يك سري مسايل امنيتي نيز دقت داشت كه در ادامه بررسي خواهند شد.<br />
<br />
<br />
<b>كلاسهاي مدل مثال جاري</b><br />
<br />
در مثال جاري قصد داريم نحوه استفاده از رويههاي ذخيره شده و توابع تعريف شده توسط كاربر مخصوص SQL Server را بررسي كنيم. در اينجا كلاسهاي پزشك و بيماران او، كلاسهاي مدل برنامه را تشكيل ميدهند:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Collections.Generic;
namespace EF_Sample08.DomainClasses
{
public class Doctor
{
public int Id { set; get; }
public string Name { set; get; }
public virtual ICollection<Patient> Patients { set; get; }
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">namespace EF_Sample08.DomainClasses
{
public class Patient
{
public int Id { set; get; }
public string Name { set; get; }
public virtual Doctor Doctor { set; get; }
}
}
</pre></div><br />
كلاس <b>Context</b> برنامه به نحو زير تعريف شده:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Data.Entity;
using EF_Sample08.DomainClasses;
namespace EF_Sample08.DataLayer.Context
{
public class Sample08Context : DbContext
{
public DbSet<Doctor> Doctors { set; get; }
public DbSet<Patient> Patients { set; get; }
}
}
</pre></div><br />
و اينبار كلاس <b>DbMigrationsConfiguration</b> تعريف شده اندكي با مثالهاي قبلي متفاوت است:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Data.Entity.Migrations;
using EF_Sample08.DomainClasses;
using System.Collections.Generic;
namespace EF_Sample08.DataLayer.Context
{
public class Configuration : DbMigrationsConfiguration<Sample08Context>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
AutomaticMigrationDataLossAllowed = true;
}
protected override void Seed(Sample08Context context)
{
addData(context);
addSP(context);
addFn(context);
base.Seed(context);
}
private static void addData(Sample08Context context)
{
var patient1 = new Patient { Name = "p1" };
var patient2 = new Patient { Name = "p2" };
var doctor1 = new Doctor { Name = "doc1", Patients = new List<Patient> { patient1, patient2 } };
context.Doctors.Add(doctor1);
}
private static void addFn(Sample08Context context)
{
context.Database.ExecuteSqlCommand(
@"IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[FindDoctorPatientsCount]')
AND type in (N'FN', N'IF', N'TF', N'FS', N'FT'))
DROP FUNCTION [dbo].[FindDoctorPatientsCount]");
context.Database.ExecuteSqlCommand(
@"CREATE FUNCTION FindDoctorPatientsCount(@Doctor_Id INT)
RETURNS INT
BEGIN
RETURN
(
SELECT COUNT(*)
FROM Patients
WHERE Doctor_Id = @Doctor_Id
);
END");
}
private static void addSP(Sample08Context context)
{
context.Database.ExecuteSqlCommand(
@"IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[FindDoctorsStartWith]')
AND type in (N'P', N'PC'))
DROP PROCEDURE [dbo].[FindDoctorsStartWith]
");
context.Database.ExecuteSqlCommand(
@"CREATE PROCEDURE FindDoctorsStartWith(@name NVARCHAR(400))
AS
SELECT *
FROM Doctors
WHERE [Name] LIKE @name + '%'");
}
}
}
</pre></div><br />
در اينجا از متد Seed علاوه بر مقدار دهي اوليه جداول، براي تعريف يك رويه ذخيره شده به نام FindDoctorsStartWith و يك تابع سفارشي به نام FindDoctorPatientsCount نيز استفاده شده است. متد context.Database.ExecuteSqlCommand مستقيما يك عبارت SQL را بر روي بانك اطلاعاتي اجرا ميكند.<br />
<br />
در ادامه كدهاي كامل برنامه نهايي را ملاحظه ميكنيد:<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System;
using System.Data;
using System.Data.Entity;
using System.Data.Objects.SqlClient;
using System.Data.SqlClient;
using System.Linq;
using EF_Sample08.DataLayer.Context;
using EF_Sample08.DomainClasses;
namespace EF_Sample08
{
class Program
{
static void Main(string[] args)
{
Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample08Context, Configuration>());
using (var db = new Sample08Context())
{
runSp(db);
runFn(db);
usingSqlFunctions(db);
}
}
private static void usingSqlFunctions(Sample08Context db)
{
var doctorsWithNumericNameList = db.Doctors.Where(x => SqlFunctions.IsNumeric(x.Name) == 1).ToList();
if (doctorsWithNumericNameList.Any())
{
//do something
}
}
private static void runFn(Sample08Context db)
{
var doctorIdParameter = new SqlParameter
{
ParameterName = "@doctor_id",
Value = 1,
SqlDbType = SqlDbType.Int
};
var patientsCount = db.Database.SqlQuery<int>("select dbo.FindDoctorPatientsCount(@doctor_id)", doctorIdParameter).FirstOrDefault();
Console.WriteLine(patientsCount);
}
private static void runSp(Sample08Context db)
{
var nameParameter = new SqlParameter
{
ParameterName = "@name",
Value = "doc",
Direction = ParameterDirection.Input,
SqlDbType = SqlDbType.NVarChar
};
var doctors = db.Database.SqlQuery<Doctor>("exec FindDoctorsStartWith @name", nameParameter).ToList();
if (doctors.Any())
{
foreach (var item in doctors)
{
Console.WriteLine(item.Name);
}
}
}
}
}
</pre></div><br />
<b>توضيحات</b><br />
<br />
همانطور كه ملاحظه ميكنيد، براي اجراي مستقيم يك عبارت SQL صرفنظر از اينكه يك رويه ذخيره شده است يا يك تابع و يا يك كوئري معمولي، بايد از متد db.Database.SqlQuery استفاده كرد. خروجي اين متد از نوع IEnumerable است و اين توانايي را دارد كه ركوردهاي بازگشت داده شده از بانك اطلاعاتي را به خواص يك كلاس به صورت خودكار نگاشت كند.<br />
پارامتر اول متد db.Database.SqlQuery، عبارت SQL مورد نظر است. پارامتر دوم آن بايد توسط وهلههايي از كلاس SqlParameter مقدار دهي شود. به كمك SqlParameter نام پارامتر مورد استفاده، مقدار و نوع آن مشخص ميگردد. همچنين Direction آن نيز براي استفاده از رويههاي ذخيره شده ويژهاي كه داراي پارامتري از نوع out هستند درنظر گرفته شده است.<br />
<br />
<b>چند نكته</b><br />
<br />
- در متد runSp فوق، متد الحاقي ToList را حذف كرده و برنامه را اجرا كنيد. بلافاصله پيغام خطاي «The SqlParameter is already contained by another SqlParameterCollection.» ظاهر خواهد شد. علت هم اين است كه با بكارگيري متد ToList، تمام عمليات يكبار انجام شده و نتيجه بازگشت داده ميشود اما اگر به صورت مستقيم از خروجي IEnumerable آن استفاده كنيم، در حلقه foreach تعريف شده، ممكن است اين فراخواني چندبار انجام شود. به همين جهت ذكر متد ToList در اينجا ضروري است.<br />
<br />
- عنوان شد كه در اينجا بايد به مسايل امنيتي دقت داشت. بديهي است امكان نوشتن يك چنين كوئريهايي نيز وجود دارد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">db.Database.SqlQuery<Doctor>("exec FindDoctorsStartWith "+ txtName.Text, nameParameter).ToList()
</pre></div><br />
در اين حالت به ظاهر مشغول به استفاده از رويههاي ذخيره شدهاي هستيم كه عنوان ميشود در برابر حملات تزريق SQL در امان هستند، اما چون در كدهاي ما به نحو ناصحيحي با جمع زدن رشتهها مقدار دهي شده است، برنامه و بانك اطلاعاتي ديگر در امان نخواهند بود. بنابراين در اين حالت استفاده از پارامترها را نبايد فراموش كرد. <br />
زمانيكه از كوئريهاي LINQ استفاده ميشود تمام اين مسايل توسط EF مديريت خواهد شد. اما اگر قصد داريد مستقيما عبارات SQL را فراخواني كنيد، تامين امنيت برنامه به عهده خودتان خواهد بود.<br />
<br />
- در متد usingSqlFunctions از SqlFunctions.IsNumeric استفاده شده است. اين مورد مختص به SQL Server است و امكان استفاده از توابع توكار ويژه SQL Server را در كوئريهاي LINQ برنامه فراهم ميسازد. براي مثال متدالحاقي از پيش تعريف شدهاي به نام IsNumeric به صورت مستقيم در دسترس نيست، اما به كمك كلاس SqlFunctions اين تابع و بسياري از توابع ديگر توكار SQL Server قابل استفاده خواهند بود.<br />
اگر علاقمند هستيد كه ليست اين توابع را مشاهده كنيد، در ويژوال استوديو بر روي SqlFunctions كليك راست كرده و گزينه Go to definition را انتخاب كنيد.<br />
<br />
<br />
</div>وحيد نصيريhttp://www.blogger.com/profile/04454130327051686471noreply@blogger.comtag:blogger.com,1999:blog-7815821915651048578.post-71900940676992300692012-05-15T10:58:00.001+04:302012-05-15T11:00:56.219+04:30EF Code First #12<div dir="rtl" style="text-align: right;" trbidi="on"><br />
<b>پياده سازي الگوي Context Per Request در برنامههاي مبتني بر EF Code first</b><br />
<br />
در طراحي برنامههاي چند لايه مبتني بر EF مرسوم نيست كه در هر كلاس و متدي كه قرار است از امكانات آن استفاده كند، يكبار DbContext و كلاس مشتق شده از آن وهله سازي شوند؛ به اين ترتيب امكان انجام امور مختلف در طي يك تراكنش از بين ميرود. براي حل اين مشكل الگويي مطرح شده است به نام Session/Context Per Request و يا به اشتراك گذاري يك Unit of work در لايههاي مختلف برنامه در طي يك درخواست، كه در ادامه يك پياده سازي آنرا با هم مرور خواهيم كرد.<br />
البته اين سشن با سشن ASP.NET يكي نيست. در NHibernate معادل DbContextايي كه در اينجا ملاحظه ميكنيد، Session نام دارد.<br />
<br />
<br />
<b>اهميت بكارگيري الگوي Unit of work و به اشتراك گذاري آن در طي يك درخواست</b><br />
<br />
در الگوي واحد كار يا همان DbContext در اينجا، تمام درخواستهاي رسيده به آن، در صف قرار گرفته و تمام آنها در پايان كار، به بانك اطلاعاتي اعمال ميشوند. براي مثال زمانيكه شيءايي را به يك وهله از DbContext اضافه/حذف ميكنيم، يا در ادامه مقدار خاصيتي را تغيير ميدهيم، هيچكدام از اين تغييرات تا زمانيكه متد SaveChanges فراخواني نشود، به بانك اطلاعاتي اعمال نخواهند شد. اين مساله مزاياي زير را به همراه خواهد داشت:<br />
<br />
<b>الف) كارآيي بهتر</b><br />
در اينجا از يك كانكشن باز شده، حداكثر استفاده صورت ميگيرد. چندين و چند عمليات در طي يك batch به بانك اطلاعاتي اعمال ميگردند؛ بجاي اينكه براي اعمال هركدام، يكبار اتصال جداگانهاي به بانك اطلاعاتي باز شود.<br />
<br />
<b>ب) بررسي مسايل همزماني</b><br />
استفاده از يك الگوي واحد كار، امكان بررسي خودكار تمام تغييرات انجام شده بر روي يك موجوديت را در متدها و لايههاي مختلف ميسر كرده و به اين ترتيب مسايل مرتبط با ConcurrencyMode عنوان شده در قسمتهاي قبل به نحو بهتري قابل مديريت خواهند بود.<br />
<br />
<b>ج) استفاده صحيح از تراكنشها</b><br />
الگوي واحد كار به صورت خودكار از تراكنشها استفاده ميكند. اگر در حين فراخواني متد SaveChanges مشكلي رخ دهد، كل عمليات Rollback خواهد شد و تغييري در بانك اطلاعاتي رخ نخواهد داد. بنابراين استفاده از يك تراكنش در حين چند عمليات ناشي از لايههاي مختلف برنامه، منطقيتر است تا اينكه هر كدام، در تراكنشي جدا مشغول به كار باشند.<br />
<br />
<br />
<b>كلاسهاي مدل مثال جاري </b><br />
<br />
در مثالي كه در اين قسمت بررسي خواهيم كرد، از كلاسهاي مدل گروه محصولات كمك گرفته شده است:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Collections.Generic;
namespace EF_Sample07.DomainClasses
{
public class Category
{
public int Id { get; set; }
public virtual string Name { get; set; }
public virtual string Title { get; set; }
public virtual ICollection<Product> Products { get; set; }
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.ComponentModel.DataAnnotations;
namespace EF_Sample07.DomainClasses
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
[ForeignKey("CategoryId")]
public virtual Category Category { get; set; }
public int CategoryId { get; set; }
}
}
</pre></div><br />
<br />
در كلاس Product، يك خاصيت اضافي به نام CategoryId اضافه شده است كه توسط ويژگي ForeignKey، به عنوان كليد خارجي جدول معرفي خواهد شد. از اين خاصيت در برنامههاي ASP.NET براي مقدار دهي يك كليد خارجي توسط يك DropDownList پر شده با ليست گروهها، استفاده خواهيم كرد.<br />
<br />
<br />
<br />
<b>پياده سازي الگوي واحد كار</b><br />
<br />
همانطور كه در قسمت قبل نيز ذكر شد، DbContext در EF Code first بر اساس الگوي واحد كار تهيه شده است، اما براي به اشتراك گذاشتن آن بين لايههاي مختلف برنامه نياز است يك لايه انتزاعي را براي آن تهيه كنيم، تا بتوان آنرا به صورت خودكار توسط كتابخانههاي Dependency Injection يا به اختصار DI در زمان نياز به استفاده از آن، به كلاسهاي استفاده كننده تزريق كنيم. كتابخانهي DI ايي كه در اين قسمت مورد استفاده قرار ميگيرد، كتابخانه معروف <a href="http://docs.structuremap.net/">StructureMap</a> است. براي دريافت آن ميتوانيد از Nuget استفاده كنيد؛ يا از صفحه اصلي آن در Github : (<a href="https://github.com/structuremap/structuremap/downloads">^</a>).<br />
اينترفيس پايه الگوي واحد كار ما به شرح زير است:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Data.Entity;
using System;
namespace EF_Sample07.DataLayer.Context
{
public interface IUnitOfWork
{
IDbSet<TEntity> Set<TEntity>() where TEntity : class;
int SaveChanges();
}
}
</pre></div><br />
<br />
براي استفاده اوليه آن، تنها تغييري كه در برنامه حاصل ميشود به نحو زير است:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Data.Entity;
using EF_Sample07.DomainClasses;
namespace EF_Sample07.DataLayer.Context
{
public class Sample07Context : DbContext, IUnitOfWork
{
public DbSet<Category> Categories { set; get; }
public DbSet<Product> Products { set; get; }
#region IUnitOfWork Members
public new IDbSet<TEntity> Set<TEntity>() where TEntity : class
{
return base.Set<TEntity>();
}
#endregion
}
}
</pre></div><br />
<b>توضيحات:</b><br />
با كلاس Context در قسمتهاي قبل آشنا شدهايم. در اينجا به معرفي كلاسهايي خواهيم پرداخت كه در معرض ديد EF Code first قرار خواهند گرفت.<br />
DbSetها هم معرف الگوي Repository هستند. كلاس Sample07Context، معرفي الگوي واحد كار يا Unit of work برنامه است.<br />
براي اينكه بتوانيم تعاريف كلاسهاي سرويس برنامه را مستقل از تعريف كلاس Sample07Context كنيم، يك اينترفيس جديد را به نام IUnitOfWork به برنامه اضافه كردهايم. <br />
در اينجا كلاس Sample07Context پياده سازي كننده اينترفيس IUnitOfWork خواهد بود (اولين تغيير). <br />
دومين تغيير هم استفاده از متد base.Set ميباشد. به اين ترتيب به سادگي ميتوان به DbSetهاي مختلف در حين كار با IUnitOfWork دسترسي پيدا كرد. به عبارتي ضرورتي ندارد به ازاي تك تك DbSetها يكبار خاصيت جديدي را به اينترفيس IUnitOfWork اضافه كرد. به كمك استفاده از امكانات Generics مهيا، اينبار<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">uow.Set<Product>
</pre></div><br />
معادل همان db.Products سابق است؛ در حالتيكه از Sample07Context به صورت مستقيم استفاده شود.<br />
همچنين نيازي به پياده سازي متد SaveChanges نيست؛ زيرا پياده سازي آن در كلاس DbContext قرار دارد.<br />
<br />
<br />
<b>استفاده از الگوي واحد كار در كلاسهاي لايه سرويس برنامه</b><br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using EF_Sample07.DomainClasses;
using System.Collections.Generic;
namespace EF_Sample07.ServiceLayer
{
public interface ICategoryService
{
void AddNewCategory(Category category);
IList<Category> GetAllCategories();
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using EF_Sample07.DomainClasses;
using System.Collections.Generic;
namespace EF_Sample07.ServiceLayer
{
public interface IProductService
{
void AddNewProduct(Product product);
IList<Product> GetAllProducts();
}
}
</pre></div><br />
لايه سرويس برنامه را با دو اينترفيس جديد شروع ميكنيم. هدف از اين اينترفيسها، ارائه پياده سازيهاي متفاوت، به ازاي ORMهاي مختلف است. براي مثال در كلاسهاي زير كه نام آنها با Ef شروع شده است، پياده سازي خاص Ef Code first را تدارك خواهيم ديد. اين پياده سازي، قابل انتقال به ساير ORMها نيست چون نه پياده سازي يكساني را از مباحث LINQ ارائه ميدهند و نه متدهاي الحاقي همانندي را به همراه دارند و نه اينكه مباحث نگاشت كلاسهاي آنها به جداول مختلف يكي است:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using EF_Sample07.DataLayer.Context;
using EF_Sample07.DomainClasses;
namespace EF_Sample07.ServiceLayer
{
public class EfCategoryService : ICategoryService
{
IUnitOfWork _uow;
IDbSet<Category> _categories;
public EfCategoryService(IUnitOfWork uow)
{
_uow = uow;
_categories = _uow.Set<Category>();
}
public void AddNewCategory(Category category)
{
_categories.Add(category);
}
public IList<Category> GetAllCategories()
{
return _categories.ToList();
}
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using EF_Sample07.DataLayer.Context;
using EF_Sample07.DomainClasses;
namespace EF_Sample07.ServiceLayer
{
public class EfProductService : IProductService
{
IUnitOfWork _uow;
IDbSet<Product> _products;
public EfProductService(IUnitOfWork uow)
{
_uow = uow;
_products = _uow.Set<Product>();
}
public void AddNewProduct(Product product)
{
_products.Add(product);
}
public IList<Product> GetAllProducts()
{
return _products.Include(x => x.Category).ToList();
}
}
}
</pre></div><br />
<br />
<b>توضيحات:</b><br />
همانطور كه ملاحظه ميكنيد در هيچكدام از كلاسهاي سرويس برنامه، وهله سازي مستقيمي از الگوي واحد كار وجود ندارد. اين لايه از برنامه اصلا نميداند كه كلاسي به نام Sample07Context وجود خارجي دارد يا خير. <br />
همچنين لايه اضافي ديگري را به نام Repository جهت مخفي سازي سازوكار EF به برنامه اضافه نكردهايم. اين لايه شايد در نگاه اول برنامه را مستقل از ORM جلوه دهد اما در عمل قابل انتقال نيست و سبب تحميل سربار اضافي بي موردي به برنامه ميشود؛ ORMها ويژگيهاي يكساني را ارائه نميدهند. حتي در حالت استفاده از LINQ، پياده سازيهاي يكساني را به همراه ندارند.<br />
بنابراين اگر قرار است برنامه مستقل از ORM كار كند، نياز است لايه استفاده كننده از سرويس برنامه، با دو اينترفيس IProductService و ICategoryService كار كند و نه به صورت مستقيم با پياده سازي آنها. به اين ترتيب هر زمان كه لازم شد، فقط بايد پياده سازيهاي كلاسهاي سرويس را تغيير داد؛ باز هم برنامه نهايي بدون نياز به تغييري كار خواهد كرد.<br />
<br />
تا اينجا به معماري پيچيدهاي نرسيدهايم و اصطلاحا <a href="http://en.wikipedia.org/wiki/Overengineering">over-engineering</a> صورت نگرفته است. يك اينترفيس بسيار ساده IUnitOfWork به برنامه اضافه شده؛ در ادامه اين اينترفيس به كلاسهاي سرويس برنامه تزريق شده است (تزريق وابستگي در سازنده كلاس). كلاسهاي سرويس ما «ميدانند» كه EF وجود خارجي دارد و سعي نكردهايم توسط لايه اضافي ديگري آنرا مخفي كنيم. شيوه كار با IDbSet تعريف شده دقيقا همانند روال متداولي است كه با EF Code first كار ميشود و بسيار طبيعي جلوه ميكند.<br />
<br />
<br />
<b>استفاده از الگوي واحد كار و كلاسهاي سرويس تهيه شده در يك برنامه كنسول ويندوزي</b><br />
<br />
در ادامه براي وهله سازي اينترفيسهاي سرويس و واحد كار برنامه، از كتابخانه StructureMap كه ياد شد، استفاده خواهيم كرد. بنابراين، تمام برنامههاي نهايي ارائه شده در اين قسمت، ارجاعي را به اسمبلي StructureMap.dll نياز خواهند داشت.<br />
كدهاي برنامه كنسول مثال جاري را در ادامه ملاحظه خواهيد كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Collections.Generic;
using System.Data.Entity;
using EF_Sample07.DataLayer.Context;
using EF_Sample07.DomainClasses;
using EF_Sample07.ServiceLayer;
using StructureMap;
namespace EF_Sample07
{
class Program
{
static void Main(string[] args)
{
Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample07Context, Configuration>());
HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize();
ObjectFactory.Initialize(x =>
{
x.For<IUnitOfWork>().CacheBy(InstanceScope.Hybrid).Use<Sample07Context>();
x.For<ICategoryService>().Use<EfCategoryService>();
});
var uow = ObjectFactory.GetInstance<IUnitOfWork>();
var categoryService = ObjectFactory.GetInstance<ICategoryService>();
var product1 = new Product { Name = "P100", Price = 100 };
var product2 = new Product { Name = "P200", Price = 200 };
var category1 = new Category
{
Name = "Cat100",
Title = "Title100",
Products = new List<Product> { product1, product2 }
};
categoryService.AddNewCategory(category1);
uow.SaveChanges();
}
}
}
</pre></div><br />
در اينجا بيشتر هدف، معرفي نحوه استفاده از StructureMap است.<br />
ابتدا توسط متد ObjectFactory.Initialize مشخص ميكنيم كه اگر برنامه نياز به اينترفيس IUnitOfWork داشت، لطفا كلاس Sample07Context را وهله سازي كرده و مورد استفاده قرار بده. اگر ICategoryService مورد استفاده قرار گرفت، وهله مورد نظر بايد از كلاس EfCategoryService تامين شود.<br />
توسط ObjectFactory.GetInstance نيز ميتوان به وهلهاي از اين كلاسها دست يافت و نهايتا با فراخواني uow.SaveChanges ميتوان اطلاعات را ذخيره كرد.<br />
<br />
<b>چند نكته:</b><br />
- به كمك كتابخانه StructureMap، تزريق IUnitOfWork به سازنده كلاس EfCategoryService به صورت خودكار انجام ميشود. اگر به كدهاي فوق دقت كنيد ما فقط با اينترفيسها مشغول به كار هستيم، اما وهلهسازيها در پشت صحنه انجام ميشود.<br />
- حين معرفي IUnitOfWork از متد CacheBy با پارامتر InstanceScope.Hybrid استفاده شده است. اين enum مقادير زير را ميتواند بپذيرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">public enum InstanceScope
{
PerRequest = 0,
Singleton = 1,
ThreadLocal = 2,
HttpContext = 3,
Hybrid = 4,
HttpSession = 5,
HybridHttpSession = 6,
Unique = 7,
Transient = 8,
}
</pre></div><br />
براي مثال اگر در برنامهاي نياز داشتيد يك كلاس به صورت Singleton عمل كند، فقط كافي است نحوه كش شدن آنرا تغيير دهيد.<br />
حالت PerRequest در برنامههاي وب كاربرد دارد (و حالت پيش فرض است). با انتخاب آن وهله سازي كلاس مورد نظر به ازاي هر درخواست رسيده انجام خواهد شد.<br />
در حالت ThreadLocal، به ازاي هر Thread، وهلهاي متفاوت در اختيار مصرف كننده قرار ميگيرد.<br />
با انتخاب حالت HttpContext، به ازاي هر HttpContext ايجاد شده، كلاس معرفي شده يكبار وهله سازي ميگردد.<br />
حالت Hybrid تركيبي است از حالتهاي HttpContext و ThreadLocal. اگر برنامه وب بود، از HttpContext استفاده خواهد كرد در غيراينصورت به ThreadLocal سوئيچ ميكند.<br />
<br />
<br />
<b>استفاده از الگوي واحد كار و كلاسهاي سرويس تهيه شده در يك برنامه ASP.NET MVC</b><br />
<br />
يك برنامه خالي ASP.NET MVC را آغاز كنيد. سپس يك HomeController جديد را نيز به آن اضافه نمائيد و كدهاي آنرا مطابق اطلاعات زير تغيير دهيد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Web.Mvc;
using EF_Sample07.DomainClasses;
using EF_Sample07.ServiceLayer;
using EF_Sample07.DataLayer.Context;
using System.Collections.Generic;
namespace EF_Sample07.MvcAppSample.Controllers
{
public class HomeController : Controller
{
IProductService _productService;
ICategoryService _categoryService;
IUnitOfWork _uow;
public HomeController(IUnitOfWork uow, IProductService productService, ICategoryService categoryService)
{
_productService = productService;
_categoryService = categoryService;
_uow = uow;
}
[HttpGet]
public ActionResult Index()
{
var list = _productService.GetAllProducts();
return View(list);
}
[HttpGet]
public ActionResult Create()
{
ViewBag.CategoriesList = new SelectList(_categoryService.GetAllCategories(), "Id", "Name");
return View();
}
[HttpPost]
public ActionResult Create(Product product)
{
if (this.ModelState.IsValid)
{
_productService.AddNewProduct(product);
_uow.SaveChanges();
}
return RedirectToAction("Index");
}
[HttpGet]
public ActionResult CreateCategory()
{
return View();
}
[HttpPost]
public ActionResult CreateCategory(Category category)
{
if (this.ModelState.IsValid)
{
_categoryService.AddNewCategory(category);
_uow.SaveChanges();
}
return RedirectToAction("Index");
}
}
}
</pre></div><br />
نكته مهم اين كنترلر، تزريق وابستگيها در سازنده كلاس كنترلر است؛ به اين ترتيب كنترلر جاري نميداند كه با كدام پياده سازي خاصي از اين اينترفيسها قرار است كار كند.<br />
اگر برنامه را به همين نحو اجرا كنيم، موتور ASP.NET MVC ايراد خواهد گرفت كه يك كنترلر بايد داراي سازندهاي بدون پارامتر باشد تا من بتوانم به صورت خودكار وهلهاي از آنرا ايجاد كنم. براي رفع اين مشكل از كتابخانه StructureMap براي تزريق خودكار وابستگيها كمك خواهيم گرفت:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System;
using System.Data.Entity;
using System.Web.Mvc;
using System.Web.Routing;
using EF_Sample07.DataLayer.Context;
using EF_Sample07.ServiceLayer;
using StructureMap;
namespace EF_Sample07.MvcAppSample
{
// Note: For instructions on enabling IIS6 or IIS7 classic mode,
// visit http://go.microsoft.com/?LinkId=9394801
public class MvcApplication : System.Web.HttpApplication
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());
}
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}
protected void Application_Start()
{
Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample07Context, Configuration>());
HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize();
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
initStructureMap();
}
private static void initStructureMap()
{
ObjectFactory.Initialize(x =>
{
x.For<IUnitOfWork>().HttpContextScoped().Use(() => new Sample07Context());
x.ForRequestedType<ICategoryService>().TheDefaultIsConcreteType<EfCategoryService>();
x.ForRequestedType<IProductService>().TheDefaultIsConcreteType<EfProductService>();
});
//Set current Controller factory as StructureMapControllerFactory
ControllerBuilder.Current.SetControllerFactory(new StructureMapControllerFactory());
}
protected void Application_EndRequest(object sender, EventArgs e)
{
ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects();
}
}
public class StructureMapControllerFactory : DefaultControllerFactory
{
protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
{
return ObjectFactory.GetInstance(controllerType) as Controller;
}
}
}
</pre></div><br />
<b>توضيحات:</b><br />
كدهاي فوق متعلق به كلاس Global.asax.cs هستند. در اينجا در متد Application_Start، متد initStructureMap فراخواني شده است.<br />
با پياده سازي ObjectFactory.Initialize در كدهاي برنامه كنسول معرفي شده آشنا شديم. اينبار فقط حالت كش شدن كلاس Context برنامه را HttpContextScoped قرار دادهايم تا به ازاي هر درخواست رسيده يك بار الگوي واحد كار وهله سازي شود.<br />
نكته مهمي كه در اينجا اضافه شدهاست، استفاده از متد ControllerBuilder.Current.SetControllerFactory ميباشد. اين متد نياز به وهلهاي از نوع DefaultControllerFactory دارد كه نمونهاي از آنرا در كلاس StructureMapControllerFactory مشاهده ميكنيد. به اين ترتيب در زمان وهله سازي خودكار يك كنترلر، اينبار StructureMap وارد عمل شده و وابستگيهاي برنامه را مطابق تعاريف ObjectFactory.Initialize ذكر شده، به سازنده كلاس كنترلر تزريق ميكند.<br />
همچنين در متد Application_EndRequest با فراخواني ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects از نشتي اتصالات به بانك اطلاعاتي جلوگيري خواهيم كرد. چون وهله الگوي كار برنامه HttpScoped تعريف شده، در پايان يك درخواست به صورت خودكار توسط StructureMap پاكسازي ميشود و به نشتي منابع نخواهيم رسيد.<br />
<br />
<br />
<b>استفاده از الگوي واحد كار و كلاسهاي سرويس تهيه شده در يك برنامه ASP.NET Web forms</b><br />
<br />
در يك برنامه ASP.NET Web forms نيز ميتوان اين مباحث را پياده سازي كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System;
using System.Data.Entity;
using EF_Sample07.DataLayer.Context;
using EF_Sample07.ServiceLayer;
using StructureMap;
namespace EF_Sample07.WebFormsAppSample
{
public class Global : System.Web.HttpApplication
{
private static void initStructureMap()
{
ObjectFactory.Initialize(x =>
{
x.For<IUnitOfWork>().HttpContextScoped().Use(() => new Sample07Context());
x.ForRequestedType<ICategoryService>().TheDefaultIsConcreteType<EfCategoryService>();
x.ForRequestedType<IProductService>().TheDefaultIsConcreteType<EfProductService>();
x.SetAllProperties(y=>
{
y.OfType<IUnitOfWork>();
y.OfType<ICategoryService>();
y.OfType<IProductService>();
});
});
}
void Application_Start(object sender, EventArgs e)
{
Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample07Context, Configuration>());
HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize();
initStructureMap();
}
void Application_EndRequest(object sender, EventArgs e)
{
ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects();
}
</pre></div><br />
در اينجا كدهاي كلاس Global.asax.cs را ملاحظه ميكنيد. توضيحات آن با قسمت ASP.NET MVC آنچنان تفاوتي ندارد و يكي است. البته منهاي تعاريف SetAllProperties كه جديد است و در ادامه به علت اضافه كردن آنها خواهيم رسيد.<br />
در ASP.NET Web forms برخلاف ASP.NET MVC نياز است كار وهله سازي اينترفيسها را به صورت دستي انجام دهيم. براي اين منظور و كاهش كدهاي تكراري برنامه ميتوان يك كلاس پايه را به نحو زير تعريف كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Web.UI;
using StructureMap;
namespace EF_Sample07.WebFormsAppSample
{
public class BasePage : Page
{
public BasePage()
{
ObjectFactory.BuildUp(this);
}
}
}
</pre></div><br />
سپس براي استفاده از آن خواهيم داشت:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System;
using EF_Sample07.DataLayer.Context;
using EF_Sample07.DomainClasses;
using EF_Sample07.ServiceLayer;
namespace EF_Sample07.WebFormsAppSample
{
public partial class AddProduct : BasePage
{
public IUnitOfWork UoW { set; get; }
public IProductService ProductService { set; get; }
public ICategoryService CategoryService { set; get; }
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
bindToCategories();
}
}
private void bindToCategories()
{
ddlCategories.DataTextField = "Name";
ddlCategories.DataValueField = "Id";
ddlCategories.DataSource = CategoryService.GetAllCategories();
ddlCategories.DataBind();
}
protected void btnAdd_Click(object sender, EventArgs e)
{
var product = new Product
{
Name = txtName.Text,
Price = int.Parse(txtPrice.Text),
CategoryId = int.Parse(ddlCategories.SelectedItem.Value)
};
ProductService.AddNewProduct(product);
UoW.SaveChanges();
Response.Redirect("~/Default.aspx");
}
}
}
</pre></div><br />
<br />
اينبار وابستگيهاي كلاس افزودن محصولات، به صورت خواصي عمومي تعريف شدهاند. اين خواص عمومي توسط متد SetAllProperties كه در فايل global.asax.cs معرفي شدند، بايد يكبار تعريف شوند (مهم!). <br />
سپس اگر دقت كرده باشيد، اينبار كلاس AddProduct از BasePage ما ارث بري كرده است. در سازند كلاس BasePage، با فراخواني متد ObjectFactory.BuildUp، تزريق وابستگيها به خواص عمومي كلاس جاري صورت ميگيرد. <br />
در ادامه نحوه استفاده از اين اينترفيسها را جهت مقدار دهي يك DropDownList يا ذخيره سازي اطلاعات يك محصول مشاهده ميكنيد. در اينجا نيز كار با اينترفيسها انجام شده و كلاس جاري دقيقا نميداند كه با چه وهلهاي مشغول به كار است. تنها در زمان اجرا است كه توسط StructureMap ، به ازاي هر اينترفيس معرفي شده، وهلهاي مناسب بر اساس تعاريف فايل Global.asax.cs در اختيار برنامه قرار ميگيرد.<br />
<br />
كدهاي كامل مثالهاي اين سري را از آدرس زير هم ميتوانيد دريافت كنيد: (<a href="https://dotnettipsrepository.svn.codeplex.com/svn/Trunk/ORMs/EF/">^</a>)<br />
<br />
</div>وحيد نصيريhttp://www.blogger.com/profile/04454130327051686471noreply@blogger.comtag:blogger.com,1999:blog-7815821915651048578.post-89960660322839939792012-05-13T22:51:00.000+04:302012-05-13T22:51:16.980+04:30EF Code First #11<div dir="rtl" style="text-align: right;" trbidi="on"><br />
<b>استفاده از الگوي <a href="http://martinfowler.com/eaaCatalog/repository.html">Repository</a> اضافي در EF Code first؛ آري يا خير؟!</b><br />
<br />
اگر در ويژوال استوديو، اشارهگر ماوس را بر روي تعريف DbContext قرار دهيم، راهنماي زير ظاهر ميشود:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">A DbContext instance represents a combination of the Unit Of Work and Repository patterns such that
it can be used to query from a database and group together changes that will then be written back to
the store as a unit. DbContext is conceptually similar to ObjectContext.
</pre></div><br />
در اينجا تيم EF صراحتا عنوان ميكند كه DbContext در EF Code first همان الگوي Unit Of Work را پياده سازي كرده و در داخل كلاس مشتق شده از آن، DbSetها همان Repositories هستند (فقط نامها تغيير كردهاند؛ اصول يكي است).<br />
به عبارت ديگر با نام بردن صريح از اين الگوها، مقصود زير را دنبال ميكنند:<br />
لطفا بر روي اين لايه Abstraction ايي كه ما تهيه ديدهايم، يك لايه Abstraction ديگر را ايجاد نكنيد! <br />
«لايه Abstraction ديگر» يعني پياده سازي الگوهاي Unit Of Work و Repository جديد، برفراز الگوهاي Unit Of Work و Repository توكار موجود!<br />
كار اضافهاي كه در بسياري از سايتها مشاهده ميشود و ... متاسفانه اكثر آنها هم اشتباه هستند! در ذيل روشهاي تشخيص پياده سازيهاي نادرست الگوي Repository را بر خواهيم شمرد:<br />
<b>1) قرار دادن متد Save تغييرات نهايي انجام شده، در داخل كلاس Repository</b><br />
متد Save بايد داخل كلاس Unit of work تعريف شود نه داخل كلاس Repository. دقيقا همان كاري كه در EF Code first به درستي انجام شده. متد SaveChanges توسط DbContext ارائه ميشود. علت هم اين است كه در زمان Save ممكن است با چندين Entity و چندين جدول مشغول به كار باشيم. حاصل يك تراكنش، بايد نهايتا ذخيره شود نه اينكه هر كدام از اينها، تراكنش خاص خودشان را داشته باشند.<br />
<b>2) نداشتن دركي از الگوي Unit of work</b><br />
به Unit of work به شكل يك تراكنش نگاه كنيد. در داخل آن با انواع و اقسام موجوديتها از كلاسها و جداول مختلف كار شده و حاصل عمليات، به بانك اطلاعاتي اعمال ميگردد. پياده سازيهاي اشتباه الگوي Repository، تمام امكانات را در داخل همان كلاس Repository قرار ميدهند؛ كه اشتباه است. اين نوع كلاسها فقط براي كار با يك Entity بهينه شدهاند؛ در حاليكه در دنياي واقعي، اطلاعات ممكن است از دو Entity مختلف دريافت و نتيجه محاسبات مفروضي به Entity سوم اعمال شود. تمام اين عمليات يك تراكنش را تشكيل ميدهد، نه اينكه هر كدام، تراكنش مجزاي خود را داشته باشند.<br />
<b>3) وهله سازي از DbContext به صورت مستقيم داخل كلاس Repository</b><br />
<b>4) Dispose اشياء DbContext داخل كلاس Repository</b><br />
هر بار وهله سازي DbContext مساوي است با باز شدن يك اتصال به بانك اطلاعاتي و همچنين از آنجائيكه راهنماي ذكر شده فوق را در مورد DbContext مطالعه نكردهاند، زمانيكه در يك متد با سه وهله از سه Repository موجوديتهاي مختلف كار ميكنيد، سه تراكنش و سه اتصال مختلف به بانك اطلاعاتي گشوده شده است. اين مورد ذاتا اشتباه است و سربار بالايي را نيز به همراه دارد.<br />
ضمن اينكه بستن DbContext در يك Repository، امكان اعمال كوئريهاي بعدي LINQ را غيرممكن ميكند. به ظاهر يك شيء IQueryable در اختيار داريم كه ميتوان بر روي آن انواع و اقسام كوئريهاي LINQ را تعريف كرد اما ... در اينجا با LINQ to Objects كه بر روي اطلاعات موجود در حافظه كار ميكند سر و كار نداريم. اتصال به بانك اطلاعاتي با بستن DbContext قطع شده، بنابراين كوئري LINQ بعدي شما كار نخواهد كرد.<br />
همچنين در EF نميتوان يك Entity را از يك Context به Context ديگري ارسال كرد. در پياده سازي صحيح الگوي Repository (دقيقا همان چيزي كه در EF Code first به صورت توكار وجود دارد)، Context بايد بين Repositories كه در اينجا فقط نامش DbSet تعريف شده، به اشتراك گذاشته شود. علت هم اين است كه EF از Context براي رديابي تغييرات انجام شده بر روي موجوديتها استفاده ميكند (همان سطح اول كش كه در قسمتهاي قبل به آن اشاره شد). اگر به ازاي هر Repository يكبار وهله سازي DbContext انجام شود، هر كدام كش جداگانه خاص خود را خواهند داشت.<br />
5) عدم امكان استفاده از تنها يك DbConetext به ازاي يك Http Request<br />
هنگاميكه وهله سازي DbContext به داخل يك Repository منتقل ميشود و الگوي واحد كار رعايت نميگردد، امكان به اشتراك گذاري آن بين Repositoryهاي تعريف شده وجود نخواهد داشت. اين مساله در برنامههاي وب سبب كاهش كارآيي ميگردد (باز و بسته شدن بيش از حد اتصال به بانك اطلاعاتي در حاليكه ميشد تمام اين عمليات را با يك DbContext انجام داد).<br />
<br />
نمونهاي از اين پياده سازي اشتباه را <a href="https://github.com/tugberkugurlu/GenericRepository">در اينجا</a> ميتوانيد پيدا كنيد. متاسفانه شبيه به همين پياده سازي، در پروژه MVC Scaffolding نيز بكارگرفته شده است.<br />
<br />
<br />
<b>چرا تعريف لايه ديگري بر روي لايه Abstraction موجود در EF Code first اشتباه است؟</b><br />
<br />
يكي از دلايلي كه حين تعريف الگوي Repository دوم بر روي لايه موجود عنوان ميشود، اين است:<br />
«<b>به اين ترتيب به سادگي ميتوان ORM مورد استفاده را تغيير داد</b>» چون پياده سازي استفاده از ORM، در پشت اين لايه مخفي شده و ما هر زمان كه بخواهيم به ORM ديگري كوچ كنيم، فقط كافي است اين لايه را تغيير دهيم و نه كل برنامه را.<br />
ولي سؤال اين است كه هرچند اين مساله از هزار فرسنگ بالاتر درست است، اما واقعا تابحال ديدهايد كه پروژهاي را با يك ORM شروع كنند و بعد سوئيچ كنند به ORM ديگري؟!<br />
ضمنا براي اينكه واقعا لايه اضافي پياده سازي شده انتقال پذير باشد، شما بايد كاملا دست و پاي ORM موجود را بريده و تواناييهاي در دسترس آن را به سطح نازلي كاهش دهيد تا پياده سازي شما قابل انتقال باشد. براي مثال يك سري از قابليتهاي پيشرفته و بسيار جالب در NH هست كه در EF نيست و برعكس. آيا واقعا ميتوان به همين سادگي ORM مورد استفاده را تغيير داد؟ فقط در يك حالت اين امر ميسر است: از قابليتهاي پيشرفته ابزار موجود استفاده نكنيم و از آن در سطحي بسيار ساده و ابتدايي كمك بگيريم تا از قابليتهاي مشترك بين ORMهاي موجود استفاده شود.<br />
ضمن اينكه مباحث نگاشت كلاسها به جداول را چكار خواهيد كرد؟ EF راه و روش خاص خودش را دارد، NH چندين و چند روش خاص خودش را دارد! اينها به اين سادگي قابل انتقال نيستند كه شخصي عنوان كند: «هر زمان كه علاقمند بوديم، ORM مورد استفاده را ميشود عوض كرد!»<br />
<br />
دليل دومي كه براي تهيه لايه اضافهتري بر روي DbContext عنوان ميكنند اين است:<br />
«<b>با استفاده از الگوي Repository نوشتن آزمونهاي واحد سادهتر ميشود</b>». زمانيكه برنامه بر اساس Interfaceها كار ميكند ميتوان آنها را بجاي اشاره به بانك اطلاعاتي، به نمونهاي موجود در حافظه، در زمان آزمون تغيير داد. <br />
اين مورد در حالت كلي درست است اما .... نه در مورد بانكهاي اطلاعاتي!<br />
زمانيكه در يك آزمون واحد، پياده سازي جديدي از الگوي Interface مخزن ما تهيه ميشود و اينبار بجاي بانك اطلاعاتي با يك سري شيء قرارگرفته در حافظه سروكار داريم، آيا موارد زير را هم ميتوان به سادگي آزمايش كرد؟<br />
ارتباطات بين جداولرا، cascade delete، فيلدهاي identity، فيلدهاي unique، كليدهاي تركيبي، نوعهاي خاص تعريف شده در بانك اطلاعاتي و مسايلي از اين دست.<br />
پاسخ: خير! تغيير انجام شده، سبب كار برنامه با اطلاعات موجود در حافظه خواهد شد، يعني LINQ to Objects. <br />
شما در حالت استفاده از LINQ to Objects آزادي عمل فوق العادهاي داريد. ميتوانيد از انواع و اقسام متدها حين تهيه كوئريهاي LINQ استفاده كنيد كه هيچكدام معادلي در بانك اطلاعاتي نداشته و ... به ظاهر آزمون واحد شما پاس ميشود؛ اما در عمل بر روي يك بانك اطلاعاتي واقعي كار نخواهد كرد.<br />
البته شايد شخصي عنوان كه بله ميشود تمام اينها نيازمنديها را در حالت كار با اشياء درون حافظه هم پياده سازي كرد ولي ... در نهايت پياده سازي آن بسيار پيچيده و در حد پياده سازي يك بانك اطلاعاتي واقعي خواهد شد كه واقعا ضرورتي ندارد.<br />
<br />
و پاسخ صحيح در اينجا و اين مساله خاص اين است:<br />
لطفا در حين كار با بانكهاي اطلاعاتي مباحث mocking را فراموش كنيد. بجاي SQL Server، رشته اتصالي و تنظيمات برنامه را به SQL Server CE تغيير داده و آزمايشات خود را انجام دهيد. پس از پايان كار هم بانك اطلاعاتي را delete كنيد. به اين نوع آزمونها اصطلاحا integration tests گفته ميشود. لازم است برنامه با يك بانك اطلاعاتي واقعي تست شود و نه يك سري شيء ساده قرار گرفته در حافظه كه هيچ قيدي همانند شرايط كار با يك بانك اطلاعاتي واقعي، بر روي آنها اعمال نميشود.<br />
ضمنا بايد درنظر داشت بانكهاي اطلاعاتي كه تنها در حافظه كار كنند نيز وجود دارند. براي مثال SQLite حالت كار كردن صرفا در حافظه را پشتيباني ميكند. زمانيكه آزمون واحد شروع ميشود، يك بانك اطلاعاتي واقعي را در حافظه تشكيل داده و پس از پايان كار هم ... اثري از اين بانك اطلاعاتي باقي نخواهد ماند و براي اين نوع كارها بسيار سريع است.<br />
<br />
<br />
<b>نتيجه گيري:</b><br />
حين استفاده از EF code first، الگوي واحد كار، همان DbContext است و الگوي مخزن، همان DbSetها. ضرورتي به ايجاد يك لايه محافظ اضافي بر روي اينها وجود ندارد. <br />
در اينجا بهتر است يك لايه اضافي را به نام مثلا Service ايجاد كرد و تمام اعمال كار با EF را به آن منتقل نمود. سپس در قسمتهاي مختلف برنامه ميتوان از متدهاي اين لايه استفاده كرد. به عبارتي در فايلهاي Code behind برنامه شما نبايد كدهاي EF مشاهده شوند. يا در كنترلرهاي MVC نيز به همين ترتيب. اينها مصرف كننده نهايي لايه سرويس ايجاد شده خواهند بود.<br />
همچنين بجاي نوشتن آزمونهاي واحد، به Integration tests سوئيچ كنيد تا بتوان برنامه را در شرايط كار با يك بانك اطلاعاتي واقعي تست كرد.<br />
<br />
<br />
<b>براي مطالعه بيشتر:</b><br />
<div align="left" dir="ltr"><a href="http://jamesmckay.net/2011/03/abstracting-your-orm-is-a-futile-exercise/">Abstracting your ORM is a futile exercise</a><br />
<a href="http://ayende.com/blog/3955/repository-is-the-new-singleton">Repository is the new Singleton</a><br />
<a href="http://ayende.com/blog/3973/night-of-the-living-repositories">Night of the living Repositories</a><br />
<a href="http://ayende.com/blog/4784/architecting-in-the-pit-of-doom-the-evils-of-the-repository-abstraction-layer">Architecting in the pit of doom: The evils of the repository abstraction layer</a><br />
<a href="http://thoai-nguyen.blogspot.com/2012/01/is-repository-old-skool.html">Is Repository old skool?</a><br />
<a href="http://blog.jschlesinger.net/2011/04/ef-code-first-wheres-my-repository.html">EF Code First: Where’s My Repository?</a><br />
<a href="http://stackoverflow.com/questions/5625746/generic-repository-with-ef-4-1-what-is-the-point">Generic Repository With EF 4.1 what is the point</a><br />
<br />
</div></div>وحيد نصيريhttp://www.blogger.com/profile/04454130327051686471noreply@blogger.comtag:blogger.com,1999:blog-7815821915651048578.post-8563484846662524002012-05-12T17:54:00.003+04:302012-05-12T18:04:04.039+04:30EF Code First #10<div dir="rtl" style="text-align: right;" trbidi="on"><br />
حين كار با ORMهاي پيشرفته، ويژگيهاي جالب توجهي در اختيار برنامه نويسها قرار ميگيرد كه در زمان استفاده از كلاسهاي متداول SQLHelper از آنها خبري نيست؛ مانند:<br />
الف) Deferred execution<br />
ب) Lazy loading<br />
ج) Eager loading<br />
<br />
<b>نحوه بررسي SQL نهايي توليدي توسط EF</b><br />
<br />
براي توضيح موارد فوق، <a href="http://www.dotnettips.info/2010/08/sql-wcf-ria-services.html">نياز به مشاهده خروجي</a> SQL نهايي حاصل از ORM است و همچنين شمارش تعداد بار رفت و برگشت به بانك اطلاعاتي. بهترين ابزاري را كه براي اين منظور ميتوان پيشنهاد داد، برنامه EF Profiler است. براي دريافت آن ميتوانيد به اين آدرس مراجعه كنيد: (<a href="http://efprof.com/Trial">^</a>) و (<a href="http://efprof.com/Download">^</a>)<br />
<br />
پس از وارد كردن نام و آدرس ايميل، يك مجوز يك ماهه آزمايشي، به آدرس ايميل شما ارسال خواهد شد. <br />
زمانيكه اين فايل را در ابتداي اجراي برنامه به آن معرفي ميكنيد، محل ذخيره سازي نهايي آن جهت بازبيني بعدي، مسير MyUserName\Local Settings\Application Data\EntityFramework Profiler خواهد بود. <br />
<br />
استفاده از اين برنامه هم بسيار ساده است:<br />
الف) در برنامه خود، ارجاعي را به اسمبلي HibernatingRhinos.Profiler.Appender.dll كه در پوشه برنامه EFProf موجود است، اضافه كنيد.<br />
ب) در نقطه آغاز برنامه، متد زير را فراخواني نمائيد:<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize();
</pre></div><br />
نقطه آغاز برنامه ميتواند متد Application_Start برنامههاي وب، در متد Program.Main برنامههاي ويندوزي كنسول و WinForms و در سازنده كلاس App برنامههاي WPF باشد.<br />
ج) برنامه EFProf را اجرا كنيد.<br />
<br />
مزاياي استفاده از اين برنامه<br />
1) وابسته به بانك اطلاعاتي مورد استفاده نيست. (برخلاف براي مثال برنامه معروف SQL Server Profiler كه فقط به همراه SQL Server ارائه ميشود)<br />
2) خروجي SQL نمايش داده شده را فرمت كرده و به همراه Syntax highlighting نيز هست.<br />
3) كار اين برنامه صرفا به لاگ كردن SQL توليدي خلاصه نميشود. يك سري از Best practices را نيز به شما گوشزد ميكند. بنابراين اگر نياز داريد سيستم خود را بر اساس ديدگاه يك متخصص بررسي كنيد (يك Code review ارزشمند)، اين ابزار ميتواند بسيار مفيد باشد.<br />
4) ميتواند كوئريهاي سنگين و سبك را به خوبي تشخيص داده و گزارشات آماري جالبي را به شما ارائه دهد.<br />
5) ميتواند دقيقا مشخص كند، كوئري را كه مشاهده ميكنيد از طريق كدام متد در كدام كلاس صادر شده است و دقيقا از چه سطري.<br />
6) امكان گروه بندي خودكار كوئريهاي صادر شده را بر اساس DbContext مورد استفاده به همراه دارد.<br />
و ...<br />
<br />
استفاده از اين برنامه حين كار با EF «الزامي» است! (البته نسخههاي NH و ساير ORMهاي ديگر آن نيز موجود است و اين مباحث در مورد تمام ORMهاي پيشرفته صادق است)<br />
مدام بايد بررسي كرد كه صفحه جاري چه تعداد كوئري را به بانك اطلاعاتي ارسال كرده و به چه نحوي. همچنين آيا ميتوان با اعمال اصلاحاتي، اين وضع را بهبود بخشيد. بنابراين عدم استفاده از اين برنامه حين كار با ORMs، همانند راه رفتن در خواب است! ممكن است تصور كنيد برنامه دارد به خوبي كار ميكند اما ... در پشت صحنه فقط صفحه جاري برنامه، 100 كوئري را به بانك اطلاعاتي ارسال كرده، در حاليكه شما تنها نياز به يك كوئري داشتهايد.<br />
<br />
<br />
<b>كلاسهاي مدل مثال جاري</b><br />
<br />
كلاسهاي مدل مثال جاري از يك دپارتمان كه داراي تعدادي كارمند ميباشد، تشكيل شده است. ضمنا هر كارمند تنها در يك دپارتمان ميتواند مشغول به كار باشد و رابطه many-to-many نيست :<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">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; }
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">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; }
}
}
</pre></div><br />
نگاشت دستي اين كلاسها هم ضرورتي ندارد، زيرا قراردادهاي توكار EF Code first را رعايت كرده و EF در اينجا به سادگي ميتواند primary key و روابط one-to-many را بر اساس navigation properties تعريف شده، تشخيص دهد.<br />
<br />
در اينجا كلاس Context برنامه به شرح زير است:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">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; }
}
}
</pre></div><br />
<br />
و تنظيمات ابتدايي نحوه به روز رساني و آغاز بانك اطلاعاتي نيز مطابق كدهاي زير ميباشد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">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);
}
}
}
</pre></div><br />
<b>نكته: تهيه خروجي XML از نگاشتهاي خودكار تهيه شده</b><br />
<br />
اگر علاقمند باشيد كه پشت صحنه نگاشتهاي خودكار EF Code first را در يك فايل XML جهت بررسي بيشتر ذخيره كنيد، ميتوان از متد كمكي زير استفاده كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">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);
}
}
</pre></div><br />
بهتر است پسوند فايل XML توليدي را edmx قيد كنيد تا بتوان آنرا با دوبار كليك بر روي فايل، در ويژوال استوديو نيز مشاهده كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using (var db = new Sample06Context())
{
ExportMappings(db, "mappings.edmx");
}
</pre></div><br />
<div align="center"><img src="https://dotnettipsrepository.svn.codeplex.com/svn/Trunk/Images/EF/ef11.PNG" /></div><br />
<br />
<b>الف) بررسي Deferred execution يا بارگذاري به تاخير افتاده</b><br />
<br />
براي توضيح مفهوم Deferred loading/execution بهترين مثالي را كه ميتوان ارائه داد، صفحات جستجوي تركيبي در برنامهها است. براي مثال يك صفحه جستجو را طراحي كردهايد كه حاوي دو تكست باكس دريافت FirstName و LastName كاربر است. كنار هر كدام از اين تكست باكسها نيز يك چكباكس قرار دارد. به عبارتي كاربر ميتواند جستجويي تركيبي را در اينجا انجام دهد. نحوه پياده سازي صحيح اين نوع مثالها در EF Code first به چه نحوي است؟<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">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);
}
}
}
}
</pre></div><br />
نحوه صحيح اين نوع پياده سازي تركيبي را در متد FindEmployees مشاهده ميكنيد. نكته مهم آن، استفاده از نوع IQueryable و متد AsQueryable است و امكان تركيب كوئريها با هم.<br />
به نظر شما با فراخواني متد FindEmployees به نحو زير كه هر دو شرط آن توسط كاربر انتخاب شده است، چه تعداد كوئري به بانك اطلاعاتي ارسال ميشود؟<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">var list = FindEmployees("f name1", "l name1", true, true);
</pre></div><br />
شايد پاسخ دهيد كه سه بار : يكبار در متد db.Employees.AsQueryable و دوبار هم در حين ورود به بدنه شرطهاي ياد شده و اينجا است كه كساني كه قبلا با رويههاي ذخيره شده كار كرده باشند، شروع به فرياد و فغان ميكنند كه ما قبلا اين مسايل رو با يك SP در يك رفت و برگشت مديريت ميكرديم!<br />
پاسخ صحيح: «فقط يكبار»! آنهم تنها در زمان فراخواني متد ToList و نه قبل از آن.<br />
براي اثبات اين مدعا نياز است به خروجي SQL لاگ شده توسط EF Profiler مراجعه كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="Sql" name="code">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 */)
</pre></div><br />
<br />
IQueryable قلب LINQ است و تنها بيانگر يك عبارت (expression) از ركوردهايي ميباشد كه مد نظر شما است و نه بيشتر. براي مثال زمانيكه يك IQueryable را همانند مثال فوق فيلتر ميكنيد، هنوز چيزي از بانك اطلاعاتي يا منبع دادهاي دريافت نشده است. هنوز هيچ اتفاقي رخ نداده است و هنوز رفت و برگشتي به منبع دادهاي صورت نگرفته است. به آن بايد به شكل يك expression builder نگاه كرد و نه ليستي از اشياء فيلتر شدهي ما. به اين مفهوم، deferred execution (اجراي به تاخير افتاده) نيز گفته ميشود. <br />
كوئري LINQ شما تنها زماني بر روي بانك اطلاعاتي اجرا ميشود كه كاري بر روي آن صورت گيرد مانند فراخواني متد ToList، فراخواني متد First يا FirstOrDefault و امثال آن. تا پيش از اين فقط به شكل يك عبارت در برنامه وجود دارد و نه بيشتر.<br />
اطلاعات بيشتر: «<a href="http://www.dotnettips.info/2010/10/iqueryable-ienumerable-orms.html">تفاوت بين IQueryable و IEnumerable در حين كار با ORMs</a>»<br />
<br />
<br />
<br />
<b>ب) بررسي Lazy Loading يا واكشي در صورت نياز</b><br />
<br />
در مطلب جاري اگر به كلاسهاي مدل برنامه دقت كنيد، تعدادي از خواص به صورت virtual تعريف شدهاند. چرا؟<br />
تعريف يك خاصيت به صورت virtual، پايه و اساس lazy loading است و به كمك آن، تا به اطلاعات شيءايي نياز نباشد، وهله سازي نخواهد شد. به اين ترتيب ميتوان به كارآيي بيشتري در حين كار با ORMs رسيد. براي مثال در كلاسهاي فوق، اگر تنها نياز به دريافت نام يك دپارتمان هست، نبايد حين وهله سازي از شيء دپارتمان، شيء ليست كارمندان مرتبط با آن نيز وهله سازي شده و از بانك اطلاعاتي دريافت شوند. به اين وهله سازي با تاخير، lazy loading گفته ميشود.<br />
Lazy loading پياده سازي سادهاي نداشته و مبتني است بر بكارگيري AOP frameworks يا كتابخانههايي كه امكان تشكيل اشياء Proxy پويا را در پشت صحنه فراهم ميكنند. علت virtual تعريف كردن خواص رابط نيز به همين مساله بر ميگردد، تا اين نوع كتابخانهها بتوانند در نحوه تعريف اينگونه خواص virtual در زمان اجرا، در پشت صحنه دخل و تصرف كنند. البته حين استفاده از EF يا انواع و اقسام ORMs ديگر با اين نوع پيچيدگيها روبرو نخواهيم شد و تشكيل اشياء Proxy در پشت صحنه انجام ميشوند.<br />
<br />
<b>يك مثال:</b> قصد داريم اولين دپارتمان ثبت شده در حين آغاز برنامه را يافته و سپس ليست كارمندان آنرا نمايش دهيم:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">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);
}
}
}
</pre></div><br />
<br />
رفتار يك ORM جهت تعيين اينكه آيا نياز است براي دريافت اطلاعات بين جداول Join صورت گيرد يا خير، واكشي حريصانه و غيرحريصانه را مشخص ميسازد.<br />
در حالت واكشي حريصانه به ORM خواهيم گفت كه لطفا جهت دريافت اطلاعات فيلدهاي جداول مختلف، از همان ابتداي كار در پشت صحنه، Join هاي لازم را تدارك ببين. در حالت واكشي غيرحريصانه به ORM خواهيم گفت به هيچ عنوان حق نداري Join ايي را تشكيل دهي. هر زماني كه نياز به اطلاعات فيلدي از جدولي ديگر بود بايد به صورت مستقيم به آن مراجعه كرده و آن مقدار را دريافت كني.<br />
به صورت خلاصه برنامه نويس در حين كار با ORM هاي پيشرفته نيازي نيست Join بنويسد. تنها بايد ORM را طوري تنظيم كند كه آيا اينكار را حتما خودش در پشت صحنه انجام دهد (واكشي حريصانه)، يا اينكه خير، به هيچ عنوان SQL هاي توليدي در پشت صحنه نبايد حاوي Join باشند (lazy loading).<br />
<br />
در مثال فوق به صورت خودكار دو كوئري به بانك اطلاعاتي ارسال ميگردد:<br />
<br />
<div align="left" dir="ltr"><pre language="Sql" name="code">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 */)
</pre></div><br />
يكبار زمانيكه قرار است اطلاعات دپارتمان يك (db.Departments.Find) دريافت شود. تا اين لحظه خبري از جدول Employees نيست. چون lazy loading فعال است و فقط اطلاعاتي را كه نياز داشتهايم فراهم كرده است.<br />
زمانيكه برنامه به حلقه ميرسد، نياز است اطلاعات dept1.Employees را دريافت كند. در اينجا است كه كوئري دوم، به بانك اطلاعاتي صادر خواهد شد (بارگذاري در صورت نياز).<br />
<br />
<br />
<b>ج) بررسي Eager Loading يا واكشي حريصانه</b><br />
<br />
حالت lazy loading بسيار جذاب به نظر ميرسد؛ براي مثال ميتوان خواص حجيم يك جدول را به جدول مرتبط ديگري منتقل كرد. مثلا فيلدهاي متني طولاني يا اطلاعات باينري فايلهاي ذخيره شده، تصاوير و امثال آن. به اين ترتيب تا زمانيكه نيازي به اينگونه اطلاعات نباشد، lazy loading از بارگذاري آنها جلوگيري كرده و سبب افزايش كارآيي برنامه ميشود.<br />
اما ... همين lazy loading در صورت استفاده نا آگاهانه ميتواند سرور بانك اطلاعاتي را در يك برنامه چندكاربره از پا درآورد! نيازي هم نيست تا شخصي به سايت شما حمله كند. مهاجم اصلي همان برنامه نويس كم اطلاع است! <br />
اينبار مثال زير را درنظر بگيريد كه بجاي دريافت اطلاعات يك شخص، مثلا قصد داريم، اطلاعات كليه دپارتمانها را توسط يك Grid نمايش دهيم (فرقي نميكند برنامه وب يا ويندوز باشد؛ اصول يكي است):<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">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);
}
}
}
</pre></div><b>يك نكته:</b> اگر سعي كنيم كد فوق را اجرا كنيم به خطاي زير برخواهيم خورد:<br />
<br />
<div align="left" dir="ltr"><pre language="xml" name="code">There is already an open DataReader associated with this Command which must be closed first
</pre></div><br />
براي رفع اين مشكل نياز است گزينه MultipleActiveResultSets=True را به كانكشن استرينگ اضافه كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code"><connectionStrings>
<clear/>
<add
name="Sample06Context"
connectionString="Data Source=(local);Initial Catalog=testdb2012;Integrated Security = true;MultipleActiveResultSets=True;"
providerName="System.Data.SqlClient"
/>
</connectionStrings>
</pre></div><br />
<b>سؤال:</b> به نظر شما در دو حلقه تو در توي فوق چندبار رفت و برگشت به بانك اطلاعاتي صورت ميگيرد؟ با توجه به اينكه در متد Seed ذكر شده در ابتداي مطلب، تعداد ركوردها مشخص است.<br />
پاسخ: 7 بار!<br />
<br />
<div align="center"><img src="https://dotnettipsrepository.svn.codeplex.com/svn/Trunk/Images/EF/ef10.PNG" /></div><br />
و اينجا است كه عنوان شد استفاده از EF Profiler در حين توسعه برنامههاي مبتني بر ORM «الزامي» است! اگر از اين نكته اطلاعي نداشتيد، بهتر است يكبار تمام صفحات گزارشگيري برنامههاي خود را كه حاوي يك Grid هستند، توسط EF Profiler بررسي كنيد. اگر در اين برنامه پيغام خطاي n+1 select را دريافت كرديد، يعني در حال استفاده ناصحيح از امكانات lazy loading ميباشيد.<br />
<br />
آيا ميتوان اين وضعيت را بهبود بخشيد؟ زمانيكه كار ما گزارشگيري از اطلاعات با تعداد ركوردهاي بالا است، استفاده ناصحيح از ويژگي Lazy loading ميتواند به شدت كارآيي بانك اطلاعاتي را پايين بياورد. براي حل اين مساله در زمانهاي قديم (!) بين جداول join مينوشتند؛ الان چطور؟<br />
در EF متدي به نام Include جهت Eager loading اطلاعات موجوديتهاي مرتبط به هم درنظر گرفته شده است كه در پشت صحنه همينكار را انجام ميدهد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">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);
}
}
}
</pre></div><br />
همانطور كه ملاحظه ميكنيد اينبار به كمك متد Include، نسبت به واكشي حريصانه Employees اقدام كردهايم. اكنون اگر برنامه را اجرا كنيم، فقط يك رفت و برگشت به بانك اطلاعاتي انجام خواهد شد و كار Join نويسي به صورت خودكار توسط EF مديريت ميگردد:<br />
<br />
<div align="left" dir="ltr"><pre language="Sql" name="code">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
</pre></div><br />
<br />
متد Include در نگارشهاي اخير EF پيشرفت كرده است و همانند مثال فوق، امكان كار با lambda expressions را جهت تعريف خواص مورد نظر به صورت strongly typed ارائه ميدهد. در نگارشهاي قبلي اين متد، تنها امكان استفاده از رشتهها براي معرفي خواص وجود داشت.<br />
همچنين توسط متد Include امكان eager loading چندين سطح با هم نيز وجود دارد؛ مثلا x.Employees.Kids و همانند آن.<br />
<br />
<br />
<b>چند نكته در مورد نحوه خاموش كردن Lazy loading</b><br />
<br />
امكان خاموش كردن Lazy loading در تمام كلاسهاي برنامه با تنظيم خاصيت Configuration.LazyLoadingEnabled كلاس Context برنامه به نحو زير ميسر است:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">public class Sample06Context : DbContext
{
public Sample06Context()
{
this.Configuration.LazyLoadingEnabled = false;
}
</pre></div><br />
يا اگر تنها در مورد يك كلاس نياز است اين خاموش سازي صورت گيرد، كلمه كليدي virtual را حذف كنيد. براي مثال با نوشتن public ICollection<Employee> Employees بجاي public virtual ICollection<Employee> Employees در اولين بار وهله سازي كلاس دپارتمان، ليست كارمندان آن به نال تنظيم ميشود. البته در اين حالت null object pattern را نيز فراموش نكنيد (وهله سازي پيش فرض Employees در سازنده كلاس):<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">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>();
}
}
</pre></div><br />
به اين ترتيب به خطاي null reference object بر نخواهيم خورد. همچنين وهله سازي، با مقدار دهي ليست دريافتي از بانك اطلاعاتي متفاوت است. در اينجا نيز بايد از متد Include استفاده كرد.<br />
<br />
بنابراين در صورت خاموش كردن lazy loading، حتما نياز است از متد Include استفاده شود. اگرlazy loading فعال است، جهت تبديل آن به eager loading از متد Include استفاده كنيد (اما اجباري نيست).</div>وحيد نصيريhttp://www.blogger.com/profile/04454130327051686471noreply@blogger.comtag:blogger.com,1999:blog-7815821915651048578.post-47437478291229224172012-05-11T13:47:00.003+04:302012-05-11T13:54:03.043+04:30EF Code First #9<div dir="rtl" style="text-align: right;" trbidi="on"><br />
<b>تنظيمات ارث بري كلاسها در EF Code first</b><br />
<br />
<br />
بانكهاي اطلاعاتي مبتني بر SQL، تنها روابطي از نوع «has a» يا «داراي» را پشتيباني ميكنند؛ اما در دنياي شيءگرا روابطي مانند «is a» يا «هست» نيز قابل تعريف هستند. براي توضيحات بيشتر به مدلهاي زير دقت نمائيد:<br />
<br />
<div align="center"><img src="https://dotnettipsrepository.svn.codeplex.com/svn/Trunk/Images/EF/ef05.PNG" /></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System;
namespace EF_Sample05.DomainClasses.Models
{
public abstract class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime DateOfBirth { get; set; }
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">namespace EF_Sample05.DomainClasses.Models
{
public class Coach : Person
{
public string TeamName { set; get; }
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">namespace EF_Sample05.DomainClasses.Models
{
public class Player : Person
{
public int Number { get; set; }
public string Description { get; set; }
}
}
</pre></div><br />
در اين مدلها كه بر اساس ارث بري از كلاس شخص، تهيه شدهاند؛ بازيكن، يك شخص است. مربي نيز يك شخص است؛ و به اين ترتيب خوانده ميشوند:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">Coach "is a" Person
Player "is a" Person
</pre></div><br />
در EF Code first سه روش جهت كار با اين نوع كلاسها و كلا ارث بري وجود دارد كه در ادامه به آنها خواهيم پرداخت:<br />
<br />
<b>الف) Table per Hierarchy يا TPH</b><br />
<br />
همانطور كه از نام آن نيز پيدا است، كل سلسله مراتبي را كه توسط ارث بري تعريف شده است، تبديل به يك جدول در بانك اطلاعاتي ميكند. اين حالت، شيوه برخورد پيش فرض EF Code first با ارث بري كلاسها است و نياز به هيچگونه تنظيم خاصي ندارد.<br />
براي آزمايش اين مساله، كلاس Context را به نحو زير تعريف نمائيد و سپس اجازه دهيد تا EF بانك اطلاعاتي معادل آنرا توليد كند:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Data.Entity;
using EF_Sample05.DomainClasses.Models;
namespace EF_Sample05.DataLayer.Context
{
public class Sample05Context : DbContext
{
public DbSet<Person> People { set; get; }
}
}
</pre></div><br />
ساختار جدول توليد شده آن همانند تصوير زير است:<br />
<br />
<div align="center"><img src="https://dotnettipsrepository.svn.codeplex.com/svn/Trunk/Images/EF/ef06.PNG" /></div><br />
همانطور كه ملاحظه ميكنيد، تمام كلاسهاي مشتق شده از كلاس شخص را تبديل به يك جدول كرده است؛ به علاوه يك فيلد جديد را هم به نام Discriminator به اين جدول اضافه نموده است. براي درك بهتر عملكرد اين فيلد، چند ركورد را توسط برنامه به بانك اطلاعاتي اضافه ميكنيم. حاصل آن به شكل زير خواهد بود:<br />
<br />
<div align="center"><img src="https://dotnettipsrepository.svn.codeplex.com/svn/Trunk/Images/EF/ef07.PNG" /></div><br />
از فيلد Discriminator جهت ثبت نام كلاسهاي متناظر با هر ركورد، استفاده شده است. به اين ترتيب EF حين كار با اشياء دقيقا ميداند كه چگونه بايد خواص متناظر با كلاسهاي مختلف را مقدار دهي كند.<br />
به علاوه اگر به ساختار جدول تهيه شده دقت كنيد، مشخص است كه در حالت TPH، نياز است فيلدهاي متناظر با كلاسهاي مشتق شده از كلاس پايه، همگي null پذير باشند. براي نمونه فيلد Number كه از نوع int تعريف شده، در سمت بانك اطلاعاتي نال پذير تعريف شده است.<br />
و براي كوئري نوشتن در اين حالت ميتوان از متد الحاقي OfType جهت فيلتر كردن اطلاعات بر اساس كلاسي خاص، كمك گرفت:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">db.People.OfType<Coach>().FirstOrDefault(x => x.LastName == "Coach L1")
</pre></div><br />
<br />
<b>سفارشي سازي نحوه نگاشت TPH</b><br />
<br />
همانطور كه عنوان شد، TPH نياز به تنظيمات خاصي ندارد و حالت پيش فرض است؛ اما براي مثال ميتوان بر روي مقادير و نوع ستون Discriminator توليدي، كنترل داشت. براي اين منظور بايد از Fluent API به نحو زير استفاده كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Data.Entity.ModelConfiguration;
using EF_Sample05.DomainClasses.Models;
namespace EF_Sample05.DataLayer.Mappings
{
public class CoachConfig : EntityTypeConfiguration<Coach>
{
public CoachConfig()
{
// For TPH
this.Map(m => m.Requires(discriminator: "PersonType").HasValue(1));
}
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Data.Entity.ModelConfiguration;
using EF_Sample05.DomainClasses.Models;
namespace EF_Sample05.DataLayer.Mappings
{
public class PlayerConfig : EntityTypeConfiguration<Player>
{
public PlayerConfig()
{
// For TPH
this.Map(m => m.Requires(discriminator: "PersonType").HasValue(2));
}
}
}
</pre></div><br />
در اينجا توسط متد Map، نام فيلد discriminator به PersonType تغيير كرده. همچنين چون مقدار پيش فرض تعيين شده توسط متد HasValue عددي است، نوع اين فيلد در سمت بانك اطلاعاتي به int null تغيير ميكند.<br />
<br />
<br />
<b>ب) Table per Type يا TPT</b><br />
<br />
در حالت TPT، به ازاي هر كلاس موجود در سلسله مراتب تعيين شده، يك جدول در سمت بانك اطلاعاتي تشكيل ميگردد. <br />
در جداول متناظر با Sub classes، تنها همان فيلدهايي وجود خواهند داشت كه در كلاسهاي هم نام وجود دارد و فيلدهاي كلاس پايه در آنها ذكر نخواهد گرديد. همچنين اين جداول داراي يك Primary key نيز خواهند بود (كه دقيقا همان كليد اصلي جدول پايه است كه به آن Shared primary key هم گفته ميشود). اين كليد اصلي، به عنوان كليد خارجي اشاره كننده به كلاس يا جدول پايه نيز تنظيم ميگردد:<br />
<br />
<div align="center"><img src="https://dotnettipsrepository.svn.codeplex.com/svn/Trunk/Images/EF/ef08.PNG" /></div><br />
براي تنظيم اين نوع ارث بري، تنها كافي است ويژگي Table را بر روي Sub classes قرار داد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.ComponentModel.DataAnnotations;
namespace EF_Sample05.DomainClasses.Models
{
[Table("Coaches")]
public class Coach : Person
{
public string TeamName { set; get; }
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.ComponentModel.DataAnnotations;
namespace EF_Sample05.DomainClasses.Models
{
[Table("Players")]
public class Player : Person
{
public int Number { get; set; }
public string Description { get; set; }
}
}
</pre></div><br />
يا اگر حالت Fluent API را ترجيح ميدهيد، همانطور كه در قسمتهاي قبل نيز ذكر شد، معادل ويژگي Table در اينجا، متد ToTable است.<br />
<br />
<b>ج) Table per Concrete type يا TPC</b><br />
<br />
در تعاريف ارث بري كه تاكنون بررسي كرديم، مرسوم است كلاس پايه را از نوع abstract تعريف كنند. به اين ترتيب هدف اصلي، Sub classes تعريف شده خواهند بود؛ چون نميتوان مستقيما وهلهاي را از كلاس abstract تعريف شده ايجاد كرد.<br />
در حالت TPC، به ازاي هر sub class غير abstract، يك جدول ايجاد ميشود. هر جدول نيز حاوي فيلدهاي كلاس پايه ميباشد (برخلاف حالت TPT كه جداول متناظر با كلاسهاي مشتق شده، تنها حاوي همان خواص و فيلدهاي كلاسهاي متناظر بودند و نه بيشتر). به اين ترتيب عملا جداول تشكيل شده در بانك اطلاعاتي، از وجود ارث بري در سمت كدهاي ما بيخبر خواهند بود.<br />
<br />
<div align="center"><img src="https://dotnettipsrepository.svn.codeplex.com/svn/Trunk/Images/EF/ef09.PNG" /></div><br />
براي پياده سازي TPC نياز است از Fluent API استفاده شود:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.ComponentModel.DataAnnotations;
using System.Data.Entity.ModelConfiguration;
using EF_Sample05.DomainClasses.Models;
namespace EF_Sample05.DataLayer.Mappings
{
public class PersonConfig : EntityTypeConfiguration<Person>
{
public PersonConfig()
{
// for TPC
this.Property(x => x.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
}
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Data.Entity.ModelConfiguration;
using EF_Sample05.DomainClasses.Models;
namespace EF_Sample05.DataLayer.Mappings
{
public class CoachConfig : EntityTypeConfiguration<Coach>
{
public CoachConfig()
{
// For TPH
//this.Map(m => m.Requires(discriminator: "PersonType").HasValue(1));
// for TPT
//this.ToTable("Coaches");
//for TPC
this.Map(m =>
{
m.MapInheritedProperties();
m.ToTable("Coaches");
});
}
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Data.Entity.ModelConfiguration;
using EF_Sample05.DomainClasses.Models;
namespace EF_Sample05.DataLayer.Mappings
{
public class PlayerConfig : EntityTypeConfiguration<Player>
{
public PlayerConfig()
{
// For TPH
//this.Map(m => m.Requires(discriminator: "PersonType").HasValue(2));
// for TPT
//this.ToTable("Players");
//for TPC
this.Map(m =>
{
m.MapInheritedProperties();
m.ToTable("Players");
});
}
}
}
</pre></div><br />
ابتدا نوع فيلد Id از حالت Identity خارج شده است. اين مورد جهت كار با TPC ضروري است در غيراينصورت EF هنگام ثبت، به مشكل بر ميخورد، از اين لحاظ كه براي دو شيء، به يك Id خواهد رسيد و امكان ثبت را نخواهد داد. بنابراين در يك چنين حالتي استفاده از نوع Guid براي تعريف primary key شايد بهتر باشد. بديهي است در اين حالت بايد Id را به صورت دستي مقدار دهي نمود.<br />
در ادامه توسط متد MapInheritedProperties، به همان مقصود لحاظ كردن تمام فيلدهاي ارث بري شده در جدول حاصل، خواهيم رسيد. همچنين نام جداول متناظر نيز ذكر گرديده است.<br />
<br />
<br />
<b>سؤال : از اين بين، بهتر است از كداميك استفاده شود؟</b><br />
<br />
- براي حالتهاي ساده از TPH استفاده كنيد. براي مثال يك بانك اطلاعاتي قديمي داريد كه هر جدول آن 200 تا يا شايد بيشتر فيلد دارد! امكان تغيير طراحي آن هم وجود ندارد. براي اينكه بتوان به حس بهتري حين كاركردن با اين نوع سيستمهاي قديمي رسيد، ميشود از تركيب TPH و ComplexTypes (كه در قسمتهاي قبل در مورد آن بحث شد) براي مديريت بهتر اين نوع جداول در سمت كدهاي برنامه استفاده كرد.<br />
- اگر علاقمند به استفاده از روابط پليمرفيك هستيد ( براي مثال در كلاسي ديگر، ارجاعي به كلاس پايه Person وجود دارد) و sub classes داراي تعداد فيلدهاي كمي هستند، از TPH استفاده كنيد.<br />
- اگر تعداد فيلدهاي sub classes زياد است و بسيار بيشتر است از كلاس پايه، از روش TPT استفاده كنيد.<br />
- اگر عمق ارث بري و تعداد سطوح تعريف شده بالا است، بهتر است از TPC استفاده كنيد. حالت TPT از join استفاده ميكند و حالت TPC از union براي تشكيل كوئريها كمك خواهد گرفت</div>وحيد نصيريhttp://www.blogger.com/profile/04454130327051686471noreply@blogger.comtag:blogger.com,1999:blog-7815821915651048578.post-83935613708215878662012-05-10T13:47:00.002+04:302012-05-10T13:53:00.394+04:30EF Code First #8<div dir="rtl" style="text-align: right;" trbidi="on"><br />
<b>ادامه بحث بررسي جزئيات نحوه نگاشت كلاسها به جداول، توسط EF Code first</b><br />
<br />
<br />
<b>استفاده از Viewهاي SQL Server در EF Code first</b><br />
<br />
از Viewها عموما همانند يك جدول فقط خواندني استفاده ميشود. بنابراين نحوه نگاشت اطلاعات يك كلاس به يك View دقيقا همانند نحوه نگاشت اطلاعات يك كلاس به يك جدول است و تمام نكاتي كه تا كنون بررسي شدند، در اينجا نيز صادق است. اما ...<br />
الف) بر اساس تنظيمات توكار EF Code first، نام مفرد كلاسها، حين نگاشت به جداول، تبديل به اسم جمع ميشوند. بنابراين اگر View ما در سمت بانك اطلاعاتي چنين تعريفي دارد:<br />
<div align="left" dir="ltr"><pre language="Sql" name="code">Create VIEW EmployeesView
AS
SELECT id,
FirstName
FROM Employees
</pre></div><br />
در سمت كدهاي برنامه نياز است به اين شكل تعريف شود:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.ComponentModel.DataAnnotations;
namespace EF_Sample04.Models
{
[Table("EmployeesView")]
public class EmployeesView
{
public int Id { set; get; }
public string FirstName { set; get; }
}
}
</pre></div><br />
در اينجا به كمك ويژگي Table، نام دقيق اين View را در بانك اطلاعاتي مشخص كردهايم. به اين ترتيب تنظيمات توكار EF بازنويسي خواهد شد و ديگر به دنبال EmployeesViews نخواهد گشت؛ يا جدول متناظر با آنرا به صورت خودكار ايجاد نخواهد كرد.<br />
ب) View شما نياز است داراي يك فيلد Primary key نيز باشد.<br />
ج) اگر از مهاجرت خودكار توسط MigrateDatabaseToLatestVersion استفاده كنيم، پيغام خطاي زير را دريافت خواهيم كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="Sql" name="code">There is already an object named 'EmployeesView' in the database.
</pre></div><br />
علت اين است كه هنوز جدول dbo.__MigrationHistory از وجود آن مطلع نشده است، زيرا يك View، خارج از برنامه و در سمت بانك اطلاعاتي اضافه ميشود.<br />
براي حل اين مشكل ميتوان همانطور كه در قسمتهاي قبل نيز عنوان شد، EF را طوري تنظيم كرد تا كاري با بانك اطلاعاتي نداشته باشد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">Database.SetInitializer<Sample04Context>(null);
</pre></div><br />
به اين ترتيب EmployeesView در همين لحظه قابل استفاده است.<br />
و يا به حالت امن مهاجرت دستي سوئيچ كنيد:<br />
<div align="left" dir="ltr"><pre language="Sql" name="code">Add-Migration Init -IgnoreChanges
Update-Database
</pre></div><br />
پارامتر IgnoreChanges سبب ميشود تا متدهاي Up و Down كلاس مهاجرت توليد شده، خالي باشد. يعني زمانيكه دستور Update-Database انجام ميشود، نه Viewايي دراپ خواهد شد و نه جدول اضافهاي ايجاد ميگردد. فقط جدول dbo.__MigrationHistory به روز ميشود كه <u>هدف اصلي</u> ما نيز همين است.<br />
همچنين در اين حالت كنترل كاملي بر روي كلاسهاي Up و Down وجود دارد. ميتوان CreateTable اضافي را به سادگي از اين كلاسها حذف كرد.<br />
<br />
ضمن اينكه بايد دقت داشت يكي از اهداف كار با ORMs، فراهم شدن امكان استفاده از بانكهاي اطلاعاتي مختلف، بدون اعمال تغييري در كدهاي برنامه ميباشد (فقط تغيير كانكشن استرينگ، به علاوه تعيين Provider جديد، بايد جهت اين مهاجرت كفايت كند). بنابراين اگر از View استفاده ميكنيد، اين برنامه به SQL Server گره خواهد خورد و ديگر از ساير بانكهاي اطلاعاتي كه از اين مفهوم پشتيباني نميكنند، نميتوان به سادگي استفاده كرد.<br />
<br />
<br />
<br />
<b>استفاده از فيلدهاي XML اس كيوال سرور</b><br />
<br />
در حال حاضر پشتيباني توكاري توسط EF Code first از فيلدهاي ويژه XML اس كيوال سرور وجود ندارد؛ اما استفاده از آنها با رعايت چند نكته ساده، به نحو زير است:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.ComponentModel.DataAnnotations;
using System.Xml.Linq;
namespace EF_Sample04.Models
{
public class MyXMLTable
{
public int Id { get; set; }
[Column(TypeName = "xml")]
public string XmlValue { get; set; }
[NotMapped]
public XElement XmlValueWrapper
{
get { return XElement.Parse(XmlValue); }
set { XmlValue = value.ToString(); }
}
}
}
</pre></div><br />
<br />
در اينجا توسط TypeName ويژگي Column، نوع توكار xml مشخص شده است. اين فيلد در طرف كدهاي كلاسهاي برنامه، به صورت string تعريف ميشود. سپس اگر نياز بود به اين خاصيت توسط LINQ to XML دسترسي يافت، ميتوان يك فيلد محاسباتي را همانند خاصيت XmlValueWrapper فوق تعريف كرد. نكته ديگري را كه بايد به آن دقت داشت، استفاده از ويژگي NotMapped ميباشد، تا EF سعي نكند خاصيتي از نوع XElement را (يك CLR Property) به بانك اطلاعاتي نگاشت كند.<br />
<br />
و همچنين اگر علاقمند هستيد كه اين قابليت به صورت توكار اضافه شود، ميتوانيد <a href="http://data.uservoice.com/forums/72025-ado-net-entity-framework-ef-feature-suggestions/suggestions/1051783-xml-datatype?ref=title">اينجا راي دهيد</a>!<br />
<br />
<br />
<br />
<b>نحوه تعريف Composite keys در EF Code first</b><br />
<br />
كلاس نوع فعاليت زير را درنظر بگيريد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">namespace EF_Sample04.Models
{
public class ActivityType
{
public int UserId { set; get; }
public int ActivityID { get; set; }
}
}
</pre></div><br />
در جدول متناظر با اين كلاس، نبايد دو ركورد تكراري حاوي شماره كاربري و شماره فعاليت يكساني باهم وجود داشته باشند. بنابراين بهتر است بر روي اين دو فيلد، يك كليد تركيبي تعريف كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Data.Entity.ModelConfiguration;
using EF_Sample04.Models;
namespace EF_Sample04.Mappings
{
public class ActivityTypeConfig : EntityTypeConfiguration<ActivityType>
{
public ActivityTypeConfig()
{
this.HasKey(x => new { x.ActivityID, x.UserId });
}
}
}
</pre></div><br />
در اينجا نحوه معرفي بيش از يك كليد را در متد HasKey ملاحظه ميكنيد.<br />
<br />
<b>يك نكته:</b><br />
اينبار اگر سعي كنيم مثلا از متد db.ActivityTypes.Find با يك پارامتر استفاده كنيم، پيغام خطاي «The number of primary key values passed must match number of primary key values defined on the entity» را دريافت خواهيم كرد. براي رفع آن بايد هر دو كليد، در اين متد قيد شوند:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">var activity1 = db.ActivityTypes.Find(4, 1);
</pre></div><br />
ترتيب آنها هم بر اساس ترتيبي كه در كلاس ActivityTypeConfig، ذكر شده است، مشخص ميگردد. بنابراين در اين مثال، اولين پارامتر متد Find، به ActivityID اشاره ميكند و دومين پارامتر به UserId.<br />
<br />
<br />
<b>بررسي نحوه تعريف نگاشت جداول خود ارجاع دهنده (Self Referencing Entity)</b><br />
<br />
سناريوهاي كاربردي بسياري را جهت جداول خود ارجاع دهنده ميتوان متصور شد و عموما تمام آنها براي مدل سازي اطلاعات چند سطحي كاربرد دارند. براي مثال يك كارمند را درنظر بگيريد. مدير اين شخص هم يك كارمند است. مسئول اين مدير هم يك كارمند است و الي آخر. نمونه ديگر آن، طراحي منوهاي چند سطحي هستند و يا يك مشتري را درنظر بگيريد. مشتري ديگري كه توسط اين مشتري معرفي شده است نيز يك مشتري است. اين مشتري نيز ميتواند يك مشتري ديگر را به شما معرفي كند و اين سلسله مراتب به همين ترتيب ميتواند ادامه پيدا كند.<br />
در طراحي بانكهاي اطلاعاتي، براي ايجاد يك چنين جداولي، يك كليد خارجي را كه به كليد اصلي همان جدول اشاره ميكند، ايجاد خواهند كرد؛ اما در EF Code first چطور؟<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Collections.Generic;
namespace EF_Sample04.Models
{
public class Employee
{
public int Id { set; get; }
public string FirstName { get; set; }
public string LastName { get; set; }
//public int? ManagerID { get; set; }
public virtual Employee Manager { get; set; }
}
}
</pre></div><br />
در اين كلاس، خاصيت Manager داراي ارجاعي است به همان كلاس؛ يعني يك كارمند ميتواند مسئول كارمند ديگري باشد. براي تعريف نگاشت اين كلاس به بانك اطلاعاتي ميتوان از روش زير استفاده كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Data.Entity.ModelConfiguration;
using EF_Sample04.Models;
namespace EF_Sample04.Mappings
{
public class EmployeeConfig : EntityTypeConfiguration<Employee>
{
public EmployeeConfig()
{
this.HasOptional(x => x.Manager)
.WithMany()
//.HasForeignKey(x => x.ManagerID)
.WillCascadeOnDelete(false);
}
}
}
</pre></div><br />
با توجه به اينكه يك كارمند ميتواند مسئولي نداشته باشد (خودش مدير ارشد است)، به كمك متد HasOptional مشخص كردهايم كه فيلد Manager_Id را كه ميخواهي به اين كلاس اضافه كني بايد نال پذير باشد. توسط متد WithMany طرف ديگر رابطه مشخص شده است. <br />
اگر نياز بود فيلد Manager_Id اضافه شده نام ديگري داشته باشد، يك خاصيت nullable مانند ManagerID را كه در كلاس Employee مشاهده ميكنيد، اضافه نمائيد. سپس در طرف تعاريف نگاشتها به كمك متد HasForeignKey، بايد صريحا عنوان كرد كه اين خاصيت، همان كليد خارجي است. از اين نكته در ساير حالات تعاريف نگاشتها نيز ميتوان استفاده كرد، خصوصا اگر از يك بانك اطلاعاتي موجود قرار است استفاده شود و از نامهاي ديگري بجز نامهاي پيش فرض EF استفاده كرده است.<br />
<br />
<div align="center"><img src="https://dotnettipsrepository.svn.codeplex.com/svn/Trunk/Images/EF/ef04.PNG" /></div><br />
<br />
<br />
مثالهاي اين سري رو از اين آدرس هم ميتونيد دريافت كنيد: (<a href="https://dotnettipsrepository.svn.codeplex.com/svn/Trunk/ORMs/EF/">^</a>)<br />
<br />
</div>وحيد نصيريhttp://www.blogger.com/profile/04454130327051686471noreply@blogger.comtag:blogger.com,1999:blog-7815821915651048578.post-12612261837480772202012-05-09T11:01:00.000+04:302012-05-09T11:01:40.451+04:30EF Code First #7<div dir="rtl" style="text-align: right;" trbidi="on"><br />
<b>مديريت روابط بين جداول در EF Code first به كمك Fluent API</b><br />
<br />
EF Code first بجاي اتلاف وقت شما با نوشتن فايلهاي XML تهيه نگاشتها يا تنظيم آنها با كد، رويه Convention over configuration را پيشنهاد ميدهد. همين رويه، جهت مديريت روابط بين جداول نيز برقرار است. روابط one-to-one، one-to-many، many-to-many و موارد ديگر را بدون يك سطر تنظيم اضافي، صرفا بر اساس يك سري قراردادهاي توكار ميتواند تشخيص داده و اعمال كند. عموما زماني نياز به تنظيمات دستي وجود خواهد داشت كه قراردادهاي توكار رعايت نشوند و يا براي مثال قرار است با يك بانك اطلاعاتي قديمي از پيش موجود كار كنيم.<br />
<br />
<br />
<b>مفاهيمي به نامهاي Principal و Dependent</b><br />
<br />
در EF Code first از يك سري واژههاي خاص جهت بيان ابتدا و انتهاي روابط استفاده شده است كه عدم آشنايي با آنها درك خطاهاي حاصل را مشكل ميكند:<br />
الف) Principal : طرفي از رابطه است كه ابتدا در بانك اطلاعاتي ذخيره خواهد شد.<br />
ب) Dependent : طرفي از رابطه است كه پس از ثبت Principal در بانك اطلاعاتي ذخيره ميشود.<br />
Principal ميتواند بدون نياز به Dependent وجود داشته باشد. وجود Dependent بدون Principal ممكن نيست زيرا ارتباط بين اين دو توسط يك كليد خارجي تعريف ميشود.<br />
<br />
<br />
<b>كدهاي مثال مديريت روابط بين جداول</b><br />
<br />
در دنياي واقعي، همهي مثالها به مدل بلاگ و مطالب آن ختم نميشوند. به همين جهت نياز است يك مدل نسبتا پيچيدهتر را در اينجا بررسي كنيم. در ادامه كدهاي كامل مثال جاري را مشاهده خواهيد كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Collections.Generic;
namespace EF_Sample35.Models
{
public class Customer
{
public int Id { set; get; }
public string FirstName { set; get; }
public string LastName { set; get; }
public virtual AlimentaryHabits AlimentaryHabits { set; get; }
public virtual ICollection<CustomerAlias> Aliases { get; set; }
public virtual ICollection<Role> Roles { get; set; }
public virtual Address Address { get; set; }
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">namespace EF_Sample35.Models
{
public class CustomerAlias
{
public int Id { get; set; }
public string Aka { get; set; }
public virtual Customer Customer { get; set; }
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Collections.Generic;
namespace EF_Sample35.Models
{
public class Role
{
public int Id { set; get; }
public string Name { set; get; }
public virtual ICollection<Customer> Customers { set; get; }
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">namespace EF_Sample35.Models
{
public class AlimentaryHabits
{
public int Id { get; set; }
public bool LikesPasta { get; set; }
public bool LikesPizza { get; set; }
public int AverageDailyCalories { get; set; }
public virtual Customer Customer { get; set; }
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Collections.Generic;
namespace EF_Sample35.Models
{
public class Address
{
public int Id { set; get; }
public string City { set; get; }
public string StreetAddress { set; get; }
public string PostalCode { set; get; }
public virtual ICollection<Customer> Customers { set; get; }
}
}
</pre></div><br />
<div align="center"><img src="https://dotnettipsrepository.svn.codeplex.com/svn/Trunk/Images/EF/ef03.PNG" /></div><br />
<br />
همچنين <b>تعاريف نگاشتهاي برنامه</b> نيز مطابق كدهاي زير است:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;
namespace EF_Sample35.Mappings
{
public class CustomerAliasConfig : EntityTypeConfiguration<CustomerAlias>
{
public CustomerAliasConfig()
{
// one-to-many
this.HasRequired(x => x.Customer)
.WithMany(x => x.Aliases)
.WillCascadeOnDelete();
}
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;
namespace EF_Sample35.Mappings
{
public class CustomerConfig : EntityTypeConfiguration<Customer>
{
public CustomerConfig()
{
// one-to-one
this.HasOptional(x => x.AlimentaryHabits)
.WithRequired(x => x.Customer)
.WillCascadeOnDelete();
// many-to-many
this.HasMany(p => p.Roles)
.WithMany(t => t.Customers)
.Map(mc =>
{
mc.ToTable("RolesJoinCustomers");
mc.MapLeftKey("RoleId");
mc.MapRightKey("CustomerId");
});
// many-to-one
this.HasOptional(x => x.Address)
.WithMany(x => x.Customers)
.WillCascadeOnDelete();
}
}
}
</pre></div><br />
<br />
به همراه <b>Context</b> زير:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Data.Entity;
using System.Data.Entity.Migrations;
using EF_Sample35.Mappings;
using EF_Sample35.Models;
namespace EF_Sample35.DataLayer
{
public class Sample35Context : DbContext
{
public DbSet<AlimentaryHabits> AlimentaryHabits { set; get; }
public DbSet<Customer> Customers { set; get; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new CustomerConfig());
modelBuilder.Configurations.Add(new CustomerAliasConfig());
base.OnModelCreating(modelBuilder);
}
}
public class Configuration : DbMigrationsConfiguration<Sample35Context>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
AutomaticMigrationDataLossAllowed = true;
}
protected override void Seed(Sample35Context context)
{
base.Seed(context);
}
}
}
</pre></div><br />
<br />
كه نهايتا منجر به توليد چنين <b>ساختاري در بانك اطلاعاتي</b> ميگردد:<br />
<br />
<div align="center"><img src="https://dotnettipsrepository.svn.codeplex.com/svn/Trunk/Images/EF/ef02.PNG" /></div><br />
<br />
<b>توضيحات كامل كدهاي فوق:</b><br />
<br />
<b>تنظيمات روابط one-to-one و يا one-to-zero</b><br />
<br />
زمانيكه رابطهاي 0..1 و يا 1..1 است، مطابق قراردادهاي توكار EF Code first تنها كافي است يك navigation property را كه بيانگر ارجاعي است به شيء ديگر، تعريف كنيم (در هر دو طرف رابطه). <br />
براي مثال در مدلهاي فوق يك مشتري كه در حين ثبت اطلاعات اصلي او، «ممكن است» اطلاعات جانبي ديگري (AlimentaryHabits) نيز از او تنها در طي يك ركورد، دريافت شود. قصد هم نداريم يك ComplexType را تعريف كنيم. نياز است جدول AlimentaryHabits جداگانه وجود داشته باشد.<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">namespace EF_Sample35.Models
{
public class Customer
{
// ...
public virtual AlimentaryHabits AlimentaryHabits { set; get; }
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">namespace EF_Sample35.Models
{
public class AlimentaryHabits
{
// ...
public virtual Customer Customer { get; set; }
}
}
</pre></div><br />
در اينجا خواص virtual تعريف شده در دو طرف رابطه، به EF خواهد گفت كه رابطهاي، 1:1 برقرار است. در اين حالت اگر برنامه را اجرا كنيم، به خطاي زير برخواهيم خورد:<br />
<br />
<div align="left" dir="ltr"><pre language="xml" name="code">Unable to determine the principal end of an association between
the types 'EF_Sample35.Models.Customer' and 'EF_Sample35.Models.AlimentaryHabits'.
The principal end of this association must be explicitly configured using either
the relationship fluent API or data annotations.
</pre></div><br />
EF تشخيص داده است كه رابطه 1:1 برقرار است؛ اما با قاطعيت نميتواند طرف Principal را تعيين كند. بنابراين بايد اندكي به او كمك كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;
namespace EF_Sample35.Mappings
{
public class CustomerConfig : EntityTypeConfiguration<Customer>
{
public CustomerConfig()
{
// one-to-one
this.HasOptional(x => x.AlimentaryHabits)
.WithRequired(x => x.Customer)
.WillCascadeOnDelete();
}
}
}
</pre></div><br />
<br />
همانطور كه ملاحظه ميكنيد در اينجا توسط متد WithRequired طرف Principal و توسط متد HasOptional، طرف Dependent تعيين شده است. به اين ترتيب EF ميتوان يك رابطه 1:1 را تشكيل دهيد. <br />
توسط متد WillCascadeOnDelete هم مشخص ميكنيم كه اگر Principal حذف شد، لطفا Dependent را به صورت خودكار حذف كن.<br />
<br />
توضيحات ساختار جداول تشكيل شده:<br />
هر دو جدول با همان خواص اصلي كه در دو كلاس وجود دارند، تشكيل شدهاند.<br />
فيلد Id جدول AlimentaryHabits اينبار ديگر Identity نيست. اگر به تعريف قيد FK_AlimentaryHabits_Customers_Id دقت كنيم، در اينجا مشخص است كه فيلد Id جدول AlimentaryHabits، به فيلد Id جدول مشتريها متصل شده است (يعني در آن واحد هم primary key است و هم foreign key). به همين جهت به اين روش one-to-one association with shared primary key هم گفته ميشود (كليد اصلي جدول مشتري با جدول AlimentaryHabits به اشتراك گذاشته شده است).<br />
<br />
<br />
<b>تنظيمات روابط one-to-many</b><br />
<br />
براي مثال همان مشتري فوق را درنظر بگيريد كه داراي تعدادي نام مستعار است:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Collections.Generic;
namespace EF_Sample35.Models
{
public class Customer
{
// ...
public virtual ICollection<CustomerAlias> Aliases { get; set; }
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">namespace EF_Sample35.Models
{
public class CustomerAlias
{
// ...
public virtual Customer Customer { get; set; }
}
}
</pre></div><br />
همين ميزان تنظيم كفايت ميكند و نيازي به استفاده از Fluent API براي معرفي روابط نيست.<br />
در طرف Principal، يك مجموعه يا ليستي از Dependent وجود دارد. در Dependent هم يك navigation property معرف طرف Principal اضافه شده است.<br />
جدول CustomerAlias اضافه شده، توسط يك كليد خارجي به جدول مشتري مرتبط ميشود.<br />
<br />
<b>سؤال:</b> اگر در اينجا نيز بخواهيم CascadeOnDelete را اعمال كنيم، چه بايد كرد؟<br />
پاسخ: جهت سفارشي سازي نحوه تعاريف روابط حتما نياز به استفاده از Fluent API به نحو زير ميباشد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;
namespace EF_Sample35.Mappings
{
public class CustomerAliasConfig : EntityTypeConfiguration<CustomerAlias>
{
public CustomerAliasConfig()
{
// one-to-many
this.HasRequired(x => x.Customer)
.WithMany(x => x.Aliases)
.WillCascadeOnDelete();
}
}
}
</pre></div><br />
اينكار را بايد در كلاس تنظيمات CustomerAlias انجام داد تا بتوان Principal را توسط متد HasRequired به Customer و سپس dependent را به كمك متد WithMany مشخص كرد. در ادامه ميتوان متد WillCascadeOnDelete يا هر تنظيم سفارشي ديگري را نيز اعمال نمود.<br />
متد HasRequired سبب خواهد شد فيلد Customer_Id، به صورت not null در سمت بانك اطلاعاتي تعريف شود؛ متد HasOptional عكس آن است.<br />
<br />
<br />
<b>تنظيمات روابط many-to-many</b><br />
<br />
براي تنظيم روابط many-to-many تنها كافي است دو سر رابطه ارجاعاتي را به يكديگر توسط يك ليست يا مجموعه داشته باشند:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Collections.Generic;
namespace EF_Sample35.Models
{
public class Role
{
// ...
public virtual ICollection<Customer> Customers { set; get; }
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Collections.Generic;
namespace EF_Sample35.Models
{
public class Customer
{
// ...
public virtual ICollection<Role> Roles { get; set; }
}
}
</pre></div><br />
همانطور كه مشاهده ميكنيد، يك مشتري ميتواند چندين نقش داشته باشد و هر نقش ميتواند به چندين مشتري منتسب شود.<br />
اگر برنامه را به اين ترتيب اجرا كنيم، به صورت خودكار يك رابطه many-to-many تشكيل خواهد شد (بدون نياز به تنظيمات نگاشتهاي آن). نكته جالب آن تشكيل خودكار جدول ارتباط دهنده واسط يا اصطلاحا join-table ميباشد:<br />
<br />
<div align="left" dir="ltr"><pre language="Sql" name="code">CREATE TABLE [dbo].[RolesJoinCustomers](
[RoleId] [int] NOT NULL,
[CustomerId] [int] NOT NULL,
)
</pre></div><br />
<b>سؤال:</b> نامهاي خودكار استفاده شده را ميخواهيم تغيير دهيم. چكار بايد كرد؟<br />
پاسخ: اگر بانك اطلاعاتي براي بار اول است كه توسط اين روش توليد ميشود شايد اين پيش فرضها اهميتي نداشته باشد و نسبتا هم مناسب هستند. اما اگر قرار باشد از يك بانك اطلاعاتي موجود كه امكان تغيير نام فيلدها و جداول آن وجود ندارد استفاده كنيم، نياز به سفارشي سازي تعاريف نگاشتها به كمك Fluent API خواهيم داشت:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;
namespace EF_Sample35.Mappings
{
public class CustomerConfig : EntityTypeConfiguration<Customer>
{
public CustomerConfig()
{
// many-to-many
this.HasMany(p => p.Roles)
.WithMany(t => t.Customers)
.Map(mc =>
{
mc.ToTable("RolesJoinCustomers");
mc.MapLeftKey("RoleId");
mc.MapRightKey("CustomerId");
});
}
}
}
</pre></div><br />
<br />
<b>تنظيمات روابط many-to-one</b><br />
<br />
در تكميل مدلهاي مثال جاري، به دو كلاس زير خواهيم رسيد. در اينجا تنها در كلاس مشتري است كه ارجاعي به كلاس آدرس او وجود دارد. در كلاس آدرس، يك navigation property همانند حالت 1:1 تعريف نشده است:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">namespace EF_Sample35.Models
{
public class Address
{
public int Id { set; get; }
public string City { set; get; }
public string StreetAddress { set; get; }
public string PostalCode { set; get; }
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Collections.Generic;
namespace EF_Sample35.Models
{
public class Customer
{
// …
public virtual Address Address { get; set; }
}
}
</pre></div><br />
اين رابطه توسط EF Code first به صورت خودكار به يك رابطه many-to-one تفسير خواهد شد و نيازي به تنظيمات خاصي ندارد.<br />
زمانيكه جداول برنامه تشكيل شوند، جدول Addresses موجوديتي مستقل خواهد داشت و جدول مشتري با يك فيلد به نام Address_Id به جدول آدرسها متصل ميگردد. اين فيلد نال پذير است؛ به عبارتي ذكر آدرس مشتري الزامي نيست.<br />
اگر نياز بود اين تعاريف نيز توسط Fluent API سفارشي شوند، بايد خاصيت public virtual ICollection<Customer> Customers به كلاس Address نيز اضافه شود تا بتوان رابطه زير را توسط كدهاي برنامه تعريف كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;
namespace EF_Sample35.Mappings
{
public class CustomerConfig : EntityTypeConfiguration<Customer>
{
public CustomerConfig()
{
// many-to-one
this.HasOptional(x => x.Address)
.WithMany(x => x.Customers)
.WillCascadeOnDelete();
}
}
}
</pre></div><br />
متد HasOptional سبب ميشود تا فيلد Address_Id اضافه شده به جدول مشتريها، null پذير شود.<br />
<br />
</div>وحيد نصيريhttp://www.blogger.com/profile/04454130327051686471noreply@blogger.comtag:blogger.com,1999:blog-7815821915651048578.post-82395760113875877632012-05-08T00:13:00.002+04:302012-05-08T00:39:14.379+04:30EF Code First #6<div dir="rtl" style="text-align: right;" trbidi="on"><br />
<b>ادامه بررسي Fluent API جهت تعريف نگاشت كلاسها به بانك اطلاعاتي</b><br />
<br />
در قسمتهاي قبل با استفاده از متاديتا و data annotations جهت بررسي نحوه نگاشت اطلاعات كلاسها به جداول بانك اطلاعاتي آشنا شديم. اما اين موارد تنها قسمتي از تواناييهاي Fluent API مهيا در EF Code first را ارائه ميدهند. يكي از دلايل آن هم به محدود بودن تواناييهاي ذاتي Attributes بر ميگردد. براي مثال حين كار با Attributes امكان استفاده از متغيرها يا lambda expressions و امثال آن وجود ندارد. به علاوه شايد عدهاي علاقمند نباشند تا كلاسهاي خود را با data annotations شلوغ كنند.<br />
<br />
در قسمت دوم اين سري، مروري مقدماتي داشتيم بر Fluent API. در آنجا ذكر شد كه امكان تعريف نگاشتها به كمك تواناييهاي Fluent API به دو روش زير ميسر است:<br />
الف) ميتوان از متد protected override void OnModelCreating در كلاس مشتق شده از DbContext كار را شروع كرد.<br />
ب) و يا اگر بخواهيم كلاس Context برنامه را شلوغ نكنيم بهتر است به ازاي هر كلاس مدل برنامه، يك كلاس mapping مشتق شده از EntityTypeConfiguration را تعريف نمائيم. سپس ميتوان اين كلاسها را در متد OnModelCreating ياد شده، توسط متد modelBuilder.Configurations.Add جهت استفاده و اعمال، معرفي كرد.<br />
<br />
كلاسهاي مدلي را كه در اين قسمت بررسي خواهيم كرد، همان كلاسهاي User و Project قسمت سوم هستند و هدف اين قسمت بيشتر تطابق Fluent API با اطلاعات ارائه شده در قسمت سوم است؛ براي مثال در اينجا چگونه بايد از خاصيتي صرفنظر كرد، مسايل همزماني را اعمال نمود و امثال آن.<br />
بنابراين يك پروژه جديد كنسول را آغاز نمائيد. سپس با كمك NuGet ارجاعات لازم را به اسمبليهاي EF اضافه نمائيد.<br />
در پوشه Models اين پروژه، سه كلاس تكميل شده زير، از قسمت سوم وجود دارند:<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System;
using System.Collections.Generic;
namespace EF_Sample03.Models
{
public class User
{
public int Id { set; get; }
public DateTime AddDate { set; get; }
public string Name { set; get; }
public string LastName { set; get; }
public string FullName
{
get { return Name + " " + LastName; }
}
public string Email { set; get; }
public string Description { set; get; }
public byte[] Photo { set; get; }
public IList<Project> Projects { set; get; }
public byte[] RowVersion { set; get; }
public InterestComponent Interests { set; get; }
public User()
{
Interests = new InterestComponent();
}
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System;
namespace EF_Sample03.Models
{
public class Project
{
public int Id { set; get; }
public DateTime AddDate { set; get; }
public string Title { set; get; }
public string Description { set; get; }
public virtual User User { set; get; }
public byte[] RowVesrion { set; get; }
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">namespace EF_Sample03.Models
{
public class InterestComponent
{
public string Interest1 { get; set; }
public string Interest2 { get; set; }
}
}
</pre></div><br />
<br />
سپس يك پوشه جديد به نام Mappings را به پروژه اضافه نمائيد. به ازاي هر كلاس فوق، يك كلاس جديد را جهت تعاريف اطلاعات نگاشتها به كمك Fluent API اضافه خواهيم كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Data.Entity.ModelConfiguration;
using EF_Sample03.Models;
namespace EF_Sample03.Mappings
{
public class InterestComponentConfig : ComplexTypeConfiguration<InterestComponent>
{
public InterestComponentConfig()
{
this.Property(x => x.Interest1).HasMaxLength(450);
this.Property(x => x.Interest2).HasMaxLength(450);
}
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Data.Entity.ModelConfiguration;
using EF_Sample03.Models;
namespace EF_Sample03.Mappings
{
public class ProjectConfig : EntityTypeConfiguration<Project>
{
public ProjectConfig()
{
this.Property(x => x.Description).IsMaxLength();
this.Property(x => x.RowVesrion).IsRowVersion();
}
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Data.Entity.ModelConfiguration;
using EF_Sample03.Models;
using System.ComponentModel.DataAnnotations;
namespace EF_Sample03.Mappings
{
public class UserConfig : EntityTypeConfiguration<User>
{
public UserConfig()
{
this.HasKey(x => x.Id);
this.Property(x => x.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
this.ToTable("tblUser", schemaName: "guest");
this.Property(p => p.AddDate).HasColumnName("CreateDate").HasColumnType("date").IsRequired();
this.Property(x => x.Name).HasMaxLength(450);
this.Property(x => x.LastName).IsMaxLength().IsConcurrencyToken();
this.Property(x => x.Email).IsFixedLength().HasMaxLength(255); //nchar(128)
this.Property(x => x.Photo).IsOptional();
this.Property(x => x.RowVersion).IsRowVersion();
this.Ignore(x => x.FullName);
}
}
}
</pre></div><br />
<b>توضيحاتي در مورد كلاسهاي تنظيمات نگاشتهاي خواص به جداول و فيلدهاي بانك اطلاعاتي</b><br />
<br />
<b>نظم بخشيدن به تعاريف نگاشتها</b><br />
همانطور كه ملاحظه ميكنيد، جهت نظم بيشتر پروژه و شلوغ نشدن متد OnModelCreating كلاس Context برنامه، كه در ادامه كدهاي آن معرفي خواهد شد، به ازاي هر كلاس مدل، يك كلاس تنظيمات نگاشتها را اضافه كردهايم. <br />
كلاسهاي معمولي نگاشتها ازكلاس EntityTypeConfiguration مشتق خواهند شد و جهت تعريف كلاس InterestComponent به عنوان Complex Type، اينبار از كلاس ComplexTypeConfiguration ارث بري شده است.<br />
<br />
<b>تعيين طول فيلدها</b><br />
در كلاس InterestComponentConfig، به كمك متد HasMaxLength، همان كار ويژگي MaxLength را ميتوان شبيه سازي كرد كه در نهايت، طول فيلد nvarchar تشكيل شده در بانك اطلاعاتي را مشخص ميكند. اگر نياز است اين فيلد nvarchar از نوع max باشد، نيازي به تنظيم خاصي نداشته و حالت پيش فرض است يا اينكه ميتوان صريحا از متد IsMaxLength نيز براي معرفي nvarchar max استفاده كرد.<br />
<br />
<b>تعيين مسايل همزماني</b><br />
در قسمت سوم با ويژگيهاي ConcurrencyCheck و Timestamp آشنا شديم. در اينجا اگر نوع خاصيت byte array بود و نياز به تعريف آن به صورت timestamp وجود داشت، ميتوان از متد IsRowVersion استفاده كرد. معادل ويژگي ConcurrencyCheck در اينجا، متد IsConcurrencyToken است.<br />
<br />
<b>تعيين كليد اصلي جدول</b><br />
اگر پيش فرضهاي EF Code first مانند وجود خاصيتي به نام Id يا ClassName+Id رعايت شود، نيازي به كار خاصي نخواهد بود. اما اگر اين قراردادها رعايت نشوند، ميتوان از متد HasKey (كه نمونهاي از آنرا در كلاس UserConfig فوق مشاهده ميكنيد)، استفاده كرد.<br />
<br />
<b>تعيين فيلدهاي توليد شده توسط بانك اطلاعاتي</b><br />
به كمك متد HasDatabaseGeneratedOption، ميتوان مشخص كرد كه آيا يك فيلد Identity است و يا يك فيلد محاسباتي ويژه و يا هيچكدام.<br />
<br />
<b>تعيين نام جدول و schema آن</b><br />
اگر نياز است از قراردادهاي نامگذاري خاصي پيروي شود، ميتوان از متد ToTable جهت تعريف نام جدول متناظر با كلاس جاري استفاده كرد. همچنين در اينجا امكان تعريف schema نيز وجود دارد.<br />
<br />
<b>تعيين نام و نوع سفارشي فيلدها</b><br />
همچنين اگر نام فيلدها نيز بايد از قراردادهاي ديگري پيروي كنند، ميتوان آنها را به صورت صريح توسط متد HasColumnName معرفي كرد. اگر نياز است اين خاصيت به نوع خاصي در بانك اطلاعاتي نگاشت شود، بايد از متد HasColumnType كمك گرفت. براي مثال در اينجا بجاي نوع datetime، از نوع ويژه date استفاده شده است.<br />
<br />
<b>معرفي فيلدها به صورت nchar بجاي nvarchar</b><br />
براي نمونه اگر قرار است هش كلمه عبور در بانك اطلاعاتي ذخيره شود، چون طول آن ثابت ميباشد، توصيه شدهاست كه بجاي nvarchar از nchar براي تعريف آن استفاده شود. براي اين منظور تنها كافي است از متد IsFixedLength استفاده شود. در اين حالت طول پيش فرض 128 براي فيلد درنظر گرفته خواهد شد. بنابراين اگر نياز است از طول ديگري استفاده شود، ميتوان همانند سابق از متد HasMaxLength كمك گرفت.<br />
ضمنا اين فيلدها همگي يونيكد هستند و با n شروع شدهاند. اگر ميخواهيد از varchar يا char استفاده كنيد، ميتوان از متد IsUnicode با پارامتر false استفاده كرد.<br />
<br />
<b>معرفي يك فيلد به صورت null پذير در سمت بانك اطلاعاتي</b><br />
استفاده از متد IsOptional، فيلد را در سمت بانك اطلاعاتي به صورت فيلدي با امكان پذيرش مقادير null معرفي ميكند.<br />
البته در اينجا به صورت پيش فرض byte arrayها به همين نحو معرفي ميشوند و تنظيم فوق صرفا جهت ارائه توضيحات بيشتر در نظر گرفته شد.<br />
<br />
<b>صرفنظر كردن از خواص محاسباتي در تعاريف نگاشتها</b><br />
با توجه به اينكه خاصيت FullName به صورت يك خاصيت محاسباتي فقط خواندني، در كدهاي برنامه تعريف شده است، با استفاده از متد Ignore، از نگاشت آن به بانك اطلاعاتي جلوگيري خواهيم كرد.<br />
<br />
<br />
<b>معرفي كلاسهاي تعاريف نگاشتها به برنامه</b><br />
<br />
استفاده از كلاسهاي Config فوق خودكار نيست و نياز است توسط متد modelBuilder.Configurations.Add معرفي شوند:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Data.Entity;
using System.Data.Entity.Migrations;
using EF_Sample03.Mappings;
using EF_Sample03.Models;
namespace EF_Sample03.DataLayer
{
public class Sample03Context : DbContext
{
public DbSet<User> Users { set; get; }
public DbSet<Project> Projects { set; get; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new InterestComponentConfig());
modelBuilder.Configurations.Add(new ProjectConfig());
modelBuilder.Configurations.Add(new UserConfig());
//modelBuilder.ComplexType<InterestComponent>();
//modelBuilder.Ignore<InterestComponent>();
base.OnModelCreating(modelBuilder);
}
}
public class Configuration : DbMigrationsConfiguration<Sample03Context>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
AutomaticMigrationDataLossAllowed = true;
}
protected override void Seed(Sample03Context context)
{
base.Seed(context);
}
}
}
</pre></div><br />
در اينجا كلاس Context برنامه مثال جاري را ملاحظه ميكنيد؛ به همراه كلاس Configuration مهاجرت خودكار كه در قسمتهاي قبل بررسي شد.<br />
در متد OnModelCreating نيز ميتوان يك كلاس را از نوع Complex معرفي كرد تا براي آن در بانك اطلاعاتي جدول جداگانهاي تعريف نشود. اما بايد دقت داشت كه اينكار را فقط يكبار ميتوان انجام داد؛ يا توسط كلاس InterestComponentConfig و يا توسط متد modelBuilder.ComplexType. اگر هر دو با هم فراخواني شوند، EF يك استثناء را صادر خواهد كرد.<br />
<br />
و در نهايت، قسمت آغازين برنامه اينبار به شكل زير خواهد بود كه از آغاز كننده MigrateDatabaseToLatestVersion (قسمت چهارم اين سري) نيز استفاده كرده است:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System;
using System.Data.Entity;
using EF_Sample03.DataLayer;
namespace EF_Sample03
{
class Program
{
static void Main(string[] args)
{
Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample03Context, Configuration>());
using (var db = new Sample03Context())
{
var project1 = db.Projects.Find(1);
if (project1 != null)
{
Console.WriteLine(project1.Title);
}
}
}
}
}
</pre></div><br />
ضمنا رشته اتصالي مورد استفاده تعريف شده در فايل كانفيگ برنامه نيز به صورت زير تعريف شده است:<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code"><connectionStrings>
<clear/>
<add
name="Sample03Context"
connectionString="Data Source=(local);Initial Catalog=testdb2012;Integrated Security = true"
providerName="System.Data.SqlClient"
/>
/connectionStrings>
</pre></div><br />
<br />
در قسمتهاي بعد مباحث پيشرفتهتري از تنظيمات نگاشتها را به كمك Fluent API، بررسي خواهيم كرد. براي مثال روابط ارث بري، many-to-many و ... چگونه تعريف ميشوند.<br />
<br />
<br />
</div>وحيد نصيريhttp://www.blogger.com/profile/04454130327051686471noreply@blogger.comtag:blogger.com,1999:blog-7815821915651048578.post-70899079434255442002012-05-07T09:05:00.000+04:302012-05-07T09:05:03.842+04:30EF Code First #5<div dir="rtl" style="text-align: right;" trbidi="on"><br />
در قسمت قبل خاصيت AutomaticMigrationsEnabled را در كلاس Configuration به true تنظيم كرديم. به اين ترتيب، عمليات ساده شده، اما يك سري از قابليتهاي رديابي تغييرات را از دست خواهيم داد و اين عمليات، صرفا يك عمليات رو به جلو خواهد بود.<br />
اگر AutomaticMigrationsEnabled را مجددا به false تنظيم كنيم و هربار به كمك دستوارت Add-Migration و Update-Database تغييرات مدلها را به بانك اطلاعاتي اعمال نمائيم، علاوه بر تشكيل تاريخچه اين تغييرات در برنامه، امكان بازگشت به عقب و لغو تغييرات صورت گرفته نيز مهيا ميگردد.<br />
<br />
<b>هدف قرار دادن مرحلهاي خاص يا لغو آن</b><br />
<br />
به همان پروژه قسمت قبل مراجعه نمائيد. در كلاس Configuration آن، خاصيت AutomaticMigrationsEnabled را به false تنظيم كنيد. سپس يك خاصيت جديد را به كلاس Project اضافه نموده و برنامه را اجرا نمائيد. بلافاصله خطاي زير را دريافت خواهيم كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="JScript" name="code">Unable to update database to match the current model because there are pending changes and
automatic migration is disabled. Either write the pending model changes to a code-based migration
or enable automatic migration. Set DbMigrationsConfiguration.AutomaticMigrationsEnabled to true
to enable automatic migration.
</pre></div><br />
EF تشخيص داده است كه كلاس مدل برنامه، با بانك اطلاعاتي تطابق ندارد و همچنين ويژگي مهاجرت خودكار نيز فعال نيست. بنابراين اعمال code-based migration را توصيه كرده است.<br />
براي اين منظور به كنسول پاورشل NuGet مراجعه نمائيد (منوي Tools در ويژوال استوديو، گزينه Library package manager آن و سپس انتخاب گزينه package manager console). در ادامه فرمان add-m را نوشته و دكمه tab را فشار دهيد. يك منوي Auto Complete ظاهر خواهد شد كه از آن ميتوان فرمان add-migration را انتخاب نمود. در اينجا يك نام را هم نياز است وارد كرد؛ براي مثال:<br />
<br />
<div align="left" dir="ltr"><pre language="JScript" name="code">Add-Migration AddSomeProp2ToProject
</pre></div><br />
به اين ترتيب كلاس زير را به صورت خودكار توليد خواهد كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">namespace EF_Sample02.Migrations
{
using System.Data.Entity.Migrations;
public partial class AddSomeProp2ToProject : DbMigration
{
public override void Up()
{
AddColumn("Projects", "SomeProp", c => c.String());
AddColumn("Projects", "SomeProp2", c => c.String());
}
public override void Down()
{
DropColumn("Projects", "SomeProp2");
DropColumn("Projects", "SomeProp");
}
}
}
</pre></div><br />
مدلهاي برنامه را با بانك اطلاعاتي تطابق داده و دريافته است كه هنوز دو خاصيت در اينجا به بانك اطلاعاتي اضافه نشدهاند.<br />
از متد Up براي اعمال تغييرات و از متد Down براي بازگشت به قبل استفاده ميگردد. نام فايل اين كلاس هم طبق معمول چيزي است شبيه به timeStamp_AddSomeProp2ToProject.cs .<br />
<br />
در ادامه نياز است اين تغييرات به بانك اطلاعاتي اعمال شوند. به همين منظور دستور زير را در كنسول پاورشل وارد نمائيد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">Update-Database -Verbose
</pre></div><br />
پارامتر Verbose آن سبب خواهد شد تا جزئيات عمليات به صورت مفصل گزارش داده شود كه شامل دستورات ALTER TABLE نيز هست:<br />
<br />
<div align="left" dir="ltr"><pre language="Sql" name="code">Using NuGet project 'EF_Sample02'.
Using StartUp project 'EF_Sample02'.
Target database is: 'testdb2012' (DataSource: (local), Provider: System.Data.SqlClient, Origin: Configuration).
Applying explicit migrations: [201205061835024_AddSomeProp2ToProject].
Applying explicit migration: 201205061835024_AddSomeProp2ToProject.
ALTER TABLE [Projects] ADD [SomeProp] [nvarchar](max)
ALTER TABLE [Projects] ADD [SomeProp2] [nvarchar](max)
[Inserting migration history record]
</pre></div><br />
اكنون مجددا يك خاصيت ديگر را مثلا به نام public string SomeProp3، به كلاس Project اضافه نمائيد.<br />
سپس همين روال بايد مجددا تكرار شود. دستورات زير را در كنسول پاورشل NuGet اجرا نمائيد:<br />
<br />
<div align="left" dir="ltr"><pre language="JScript" name="code">Add-Migration AddSomeProp3ToProject
Update-Database -Verbose
</pre></div><br />
اينبار نيز يك كلاس جديد به نام AddSomeProp3ToProject به پروژه اضافه خواهد شد و سپس بر اساس آن، امكان به روز رساني بانك اطلاعاتي ميسر ميگردد.<br />
<br />
در ادامه براي مثال به اين نتيجه رسيدهايم كه نيازي به خاصيت public string SomeProp3 اضافه شده، نبوده است. روش متداول، باز هم مانند سابق است. ابتدا خاصيت را از كلاس Project حذف خواهيم كرد و سپس دو دستور Add-Migration و Update-Database را اجرا خواهيم نمود.<br />
اما با توجه به اينكه مهاجرت خودكار را غيرفعال كردهايم و هربار با فراخواني دستور Add-Migration يك كلاس جديد، با متدهاي Up و Down به پروژه، جهت نگهداري سوابق عمليات اضافه ميشوند، ميتوان دستور Update-Database را جهت فراخواني متد Down صرفا يك مرحله موجود نيز فراخواني نمود.<br />
<br />
<b>نكته:</b><br />
اگر علاقمند باشيد كه راهنماي مفصل پارامترهاي دستور Update-Database را مشاهده كنيد، تنها كافي است دستور زير را در كنسول پاورشل اجرا نمائيد:<br />
<br />
<div align="left" dir="ltr"><pre language="Sql" name="code">get-help update-database -detailed
</pre></div><br />
به عنوان نمونه اگر در حين فراخواني دستور Update-Database احتمال از دست رفتن اطلاعات باشد، عمليات متوقف ميشود. براي وادار كردن پروسه به انجام تغييرات بر روي بانك اطلاعاتي ميتوان از پارامتر Force در اينجا استفاده كرد.<br />
<br />
در ادامه براي اينكه دستور Update-Database تنها يك مرحله مشخص را كه سابقه آن در برنامه موجود است، هدف قرار دهد، بايد از پارامتر TargetMigration به همراه نام كلاس مرتبط استفاده كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="Sql" name="code">Update-Database -TargetMigration:"AddSomeProp2ToProject" -Verbose
</pre></div><br />
اگر دقت كرده باشيد در اينجا AddSomeProp<b>2</b>ToProject بجاي AddSomeProp<b>3</b>ToProject بكارگرفته شده است. اگر يك مرحله قبل را هدف قرار دهيم، متد Down را اجرا خواهد كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="Sql" name="code">Using NuGet project 'EF_Sample02'.
Using StartUp project 'EF_Sample02'.
Target database is: 'testdb2012' (DataSource: (local), Provider: System.Data.SqlClient, Origin: Configuration).
Reverting migrations: [201205061845485_AddSomeProp3ToProject].
Reverting explicit migration: 201205061845485_AddSomeProp3ToProject.
DECLARE @var0 nvarchar(128)
SELECT @var0 = name
FROM sys.default_constraints
WHERE parent_object_id = object_id(N'Projects')
AND col_name(parent_object_id, parent_column_id) = 'SomeProp3';
IF @var0 IS NOT NULL
EXECUTE('ALTER TABLE [Projects] DROP CONSTRAINT ' + @var0)
ALTER TABLE [Projects] DROP COLUMN [SomeProp3]
[Deleting migration history record]
</pre></div><br />
همانطور كه ملاحظه ميكنيد در اينجا عمليات حذف ستون SomeProp3 انجام شده است. البته اين خاصيت به صورت خودكار از كدهاي برنامه (كلاس Project در اين مثال) حذف نميشود و فرض بر اين است كه پيشتر اينكار را انجام دادهايد.<br />
<br />
<br />
<b>سفارشي سازي كلاسهاي مهاجرت</b><br />
<br />
تمام كلاسهاي خودكار مهاجرت توليد شده توسط پاورشل، از كلاس DbMigration ارث بري ميكنند. در اين كلاس امكانات قابل توجهي مانند AddColumn، AddForeignKey، AddPrimaryKey، AlterColumn، CreateIndex و امثال آن وجود دارند كه در تمام كلاسهاي مشتق شده از آن، قابل استفاده هستند. حتي متد Sql نيز در آن پيش بيني شده است كه در صورت نياز به اجراي دستوارت خام SQL، ميتوان از آن استفاده كرد.<br />
براي مثال فرض كنيد مجددا همان خاصيت public string SomeProp3 را به كلاس Project اضافه كردهايم. اما اينبار نياز است حين تشكيل اين فيلد در بانك اطلاعاتي، يك مقدار پيش فرض نيز براي آن درنظر گرفته شود كه در صورت نال بودن مقدار خاصيت آن در برنامه، به صورت خودكار توسط بانك اطلاعاتي مقدار دهي گردد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">namespace EF_Sample02.Migrations
{
using System.Data.Entity.Migrations;
public partial class AddSomeProp3ToProject : DbMigration
{
public override void Up()
{
AddColumn("Projects", "SomeProp3", c => c.String(defaultValue: "some data"));
Sql("Update Projects set SomeProp3=N'some data'");
}
public override void Down()
{
DropColumn("Projects", "SomeProp3");
}
}
}
</pre></div><br />
متد String در اينجا چنين امضايي دارد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">public ColumnModel String(bool? nullable = null, int? maxLength = null, bool? fixedLength = null,
bool? isMaxLength = null, bool? unicode = null, string defaultValue = null, string defaultValueSql = null,
string name = null, string storeType = null)
</pre></div><br />
كه براي نمونه در اينجا پارامتر defaultValue آنرا در كلاس AddSomeProp3ToProject مقدار دهي كردهايم.<br />
براي اعمال اين تغييرات تنها كافي است دستور Update-Database -Verbose اجرا گردد. اينبار خروجي SQL اجرا شده آن به نحو زير است كه شامل مقدار پيش فرض نيز شده است:<br />
<br />
<div align="left" dir="ltr"><pre language="Sql" name="code">ALTER TABLE [Projects] ADD [SomeProp3] [nvarchar](max) DEFAULT 'some data'
</pre></div><br />
تعيين مقدار پيش فرض، زمانيكه يك فيلد not null تعريف شدهاست نيز ميتواند مفيد باشد. همچنين در اينجا امكان اجراي دستورات مستقيم SQL نيز وجود دارد كه نمونهاي از آنرا در متد Up فوق مشاهده ميكنيد.<br />
<br />
<br />
<b>افزودن ركوردهاي پيش فرض در حين به روز رساني بانك اطلاعاتي</b><br />
<br />
در قسمتهاي قبل با متد Seed كه به همراه آغاز كنندههاي بانك اطلاعاتي EF ارائه شدهاند، جهت افزودن ركوردهاي اوليه و پيش فرض به بانك اطلاعاتي آشنا شديد. در اينجا نيز با تحريف متد Seed در كلاس Configuration، چنين امري ميسر است:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">namespace EF_Sample02.Migrations
{
using System;
using System.Data.Entity.Migrations;
internal sealed class Configuration : DbMigrationsConfiguration<EF_Sample02.Sample2Context>
{
public Configuration()
{
this.AutomaticMigrationsEnabled = false;
this.AutomaticMigrationDataLossAllowed = true;
}
protected override void Seed(EF_Sample02.Sample2Context context)
{
context.Users.AddOrUpdate(
a => a.Name,
new Models.User { Name = "Vahid", AddDate = DateTime.Now },
new Models.User { Name = "Test", AddDate = DateTime.Now });
}
}
}
</pre></div><br />
متد AddOrUpdate در EF 4.3 اضافه شده است. اين متد ابتدا بررسي ميكند كه آيا ركورد مورد نظر در بانك اطلاعاتي وجود دارد يا خير. اگر خير، آنرا اضافه خواهد كرد در غيراينصورت، نمونه موجود را به روز رساني ميكند. اولين پارامتر آن، identifierExpression نام دارد. توسط آن مشخص ميشود كه بر اساس چه خاصيتي بايد در مورد update يا add تصميمگيري شود. دراينجا اگر نياز به ذكر بيش از يك خاصيت وجود داشت، از anonymously type object ميتوان كمك گرفت new { p.Name, p.LastName } .<br />
<br />
<br />
<b>توليد اسكريپت به روز رساني بانك اطلاعاتي</b><br />
<br />
بهترين كار و امنترين روش حين انجام اين نوع به روز رسانيها، تهيه اسكريپت SQL فراميني است كه بايد بر روي بانك اطلاعاتي اجرا شوند. سپس ميتوان اين دستورات و اسكريپت نهايي را دستي هم اجرا كرد (كه روش متداولتري است در محيط كاري).<br />
براي اينكار تنها كافي است دستور زير را در كنسول پاورشل اجرا نمائيم:<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">Update-Database -Verbose -Script
</pre></div><br />
پس از اجراي اين دستور، يك فايل اسكريپت با پسوند sql توليد شده و بلافاصله در ويژوال استوديو جهت مرور نيز گشوده خواهد شد. براي نمونه محتواي آن براي افزودن خاصيت جديد SomeProp5 به صورت زير است:<br />
<br />
<div align="left" dir="ltr"><pre language="Sql" name="code">ALTER TABLE [Projects] ADD [SomeProp5] [nvarchar](max)
INSERT INTO [__MigrationHistory] ([MigrationId], [CreatedOn], [Model], [ProductVersion]) VALUES
('201205060852004_AutomaticMigration', '2012-05-06T08:52:00.937Z', 0x1F8B0800000............ '4.3.1')
</pre></div><br />
همانطور كه ملاحظه ميكنيد، در يك مرحله، جدول پروژهها را به روز خواهد كرد و در مرحله بعد، سابقه آنرا در جدول __MigrationHistory ثبت ميكند.<br />
<br />
<b>يك نكته:</b><br />
اگر دستور فوق را بر روي برنامهاي كه با بانك اطلاعاتي هماهنگ است اجرا كنيم، خروجي را مشاهده نخواهيم كرد. براي اين منظور ميتوان مرحله خاصي را توسط پارامتر SourceMigration هدف گيري كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">Update-Database -Verbose -Script -SourceMigration:"stepName"
</pre></div><br />
<br />
<br />
<br />
<b>استفاده از DB Migrations در عمل</b><br />
<br />
البته اين يك روش<u> پيشنهادي</u> و امن است:<br />
الف) در ابتداي اجرا برنامه، پارامتر ورودي متد System.Data.Entity.Database.SetInitializer را به نال تنظيم كنيد تا برنامه تغييري را بر روي بانك اطلاعاتي اعمال نكند.<br />
ب) توسط دستور enable-migrations، فايلهاي اوليه DB Migration را ايجاد كنيد. پيش فرضهاي آن را نيز تغيير ندهيد.<br />
ج) هر بار كه كلاسهاي مدل برنامه تغيير كردند و پس از آن نياز به به روز رساني ساختار بانك اطلاعاتي وجود داشت دو دستور زير را اجرا كنيد:<br />
<div align="left" dir="ltr"><pre language="JScript" name="code">Add-Migration AddSomePropToProject
Update-Database -Verbose -Script
</pre></div><br />
به اين ترتيب سابقه تغييرات در برنامه نگهداري شده و همچنين بدون اجراي دستورات بر روي بانك اطلاعاتي، اسكريپت نهايي اعمال تغييرات توليد ميگردد.<br />
د) اسكريپت توليد شده را بررسي كرده و پس از تائيد و افزودن به سورس كنترل، به صورت دستي بر روي بانك اطلاعاتي اجرا كنيد (مثلا توسط management studio).<br />
<br />
<br />
</div>وحيد نصيريhttp://www.blogger.com/profile/04454130327051686471noreply@blogger.comtag:blogger.com,1999:blog-7815821915651048578.post-10666371782296066322012-05-06T13:49:00.003+04:302012-05-06T13:59:57.597+04:30EF Code First #4<div dir="rtl" style="text-align: right;" trbidi="on"><br />
<b>آشنايي با Code first migrations</b><br />
<br />
ويژگي Code first migrations براي اولين بار در EF 4.3 ارائه شد و هدف آن سهولت هماهنگ سازي كلاسهاي مدل برنامه با بانك اطلاعاتي است؛ به صورت خودكار يا با تنظيمات دقيق دستي.<br />
<br />
همانطور كه در قسمتهاي قبل نيز به آن اشاره شد، تا پيش از EF 4.3، پنج روال جهت آغاز به كار با بانك اطلاعاتي در EF code first وجود داشت و دارد:<br />
1) در اولين بار اجراي برنامه، در صورتيكه بانك اطلاعاتي اشاره شده در رشته اتصالي وجود خارجي نداشته باشد، نسبت به ايجاد خودكار آن اقدام ميگردد. اينكار پس از وهله سازي اولين DbContext و همچنين صدور يك كوئري به بانك اطلاعاتي انجام خواهد شد.<br />
2) DropCreateDatabaseAlways : همواره پس از شروع برنامه، ابتدا بانك اطلاعاتي را drop كرده و سپس نمونه جديدي را ايجاد ميكند.<br />
3) DropCreateDatabaseIfModelChanges : اگر EF Code first تشخيص دهد كه تعاريف مدلهاي شما با بانك اطلاعاتي مشخص شده توسط رشته اتصالي، هماهنگ نيست، آنرا drop كرده و نمونه جديدي را توليد ميكند.<br />
4) با مقدار دهي پارامتر متد System.Data.Entity.Database.SetInitializer به نال، ميتوان فرآيند آغاز خودكار بانك اطلاعاتي را غيرفعال كرد. در اين حالت شخص ميتواند تغييرات انجام شده در كلاسهاي مدل برنامه را به صورت دستي به بانك اطلاعاتي اعمال كند.<br />
5) ميتوان با پياده سازي اينترفيس IDatabaseInitializer، يك آغاز كننده بانك اطلاعاتي سفارشي را نيز توليد كرد.<br />
<br />
اكثر اين روشها در حين توسعه يك برنامه يا خصوصا جهت سهولت انجام آزمونهاي خودكار بسيار مناسب هستند، اما به درد محيط كاري نميخورند؛ زيرا drop يك بانك اطلاعاتي به معناي از دست دادن تمام اطلاعات ثبت شده در آن است. براي رفع اين مشكل مهم، مفهومي به نام «Migrations» در EF 4.3 ارائه شده است تا بتوان بانك اطلاعاتي را بدون تخريب آن، بر اساس اطلاعات تغيير كردهي كلاسهاي مدل برنامه، تغيير داد. البته بديهي است زمانيكه توسط NuGet نسبت به دريافت و نصب EF اقدام ميشود، همواره آخرين نگارش پايدار كه حاوي اطلاعات و فايلهاي مورد نياز جهت كار با «Migrations» است را نيز دريافت خواهيم كرد.<br />
<br />
<br />
<b>تنظيمات ابتدايي Code first migrations</b><br />
<br />
در اينجا قصد داريم همان مثال قسمت قبل را ادامه دهيم. در آن مثال از يك نمونه سفارشي سازي شده DropCreateDatabaseAlways استفاده شد.<br />
نياز است از منوي Tools در ويژوال استوديو، گزينه Library package manager آن، گزينه package manager console را انتخاب كرد تا كنسول پاورشل NuGet ظاهر شود.<br />
اطلاعات مرتبط با پاورشل EF، به صورت خودكار توسط NuGet نصب ميشود. براي مثال جهت مشاهده آنها به مسير packages\EntityFramework.4.3.1\tools در كنار پوشه پروژه خود مراجعه نمائيد.<br />
در ادامه در پايين صفحه، زمانيكه كنسول پاورشل NuGet ظاهر ميشود، ابتدا بايد دقت داشت كه قرار است فرامين را بر روي چه پروژهاي اجرا كنيم. براي مثال اگر تعاريف DbContext را به يك اسمبلي و پروژه class library مجزا انتقال دادهايد، گزينه Default project را در اين قسمت بايد به اين پروژه مجزا، تغيير دهيد.<br />
سپس در خط فرمان پاور شل، دستور enable-migrations را وارد كرده و دكمه enter را فشار دهيد.<br />
پس از اجراي اين دستور، يك سري اتفاقات رخ خواهد داد:<br />
الف) پوشهاي به نام Migrations به پروژه پيش فرض مشخص شده در كنسول پاورشل، اضافه ميشود.<br />
ب) دو كلاس جديد نيز در آن پوشه تعريف خواهند شد به نامهاي Configuration.cs و يك نام خودكار مانند number_InitialCreate.cs<br />
ج) در كنسول پاور شل، پيغام زير ظاهر ميگردد:<br />
<div align="left" dir="ltr"><pre language="xml" name="code">Detected database created with a database initializer. Scaffolded migration '201205050805256_InitialCreate'
corresponding to current database schema. To use an automatic migration instead, delete the Migrations
folder and re-run Enable-Migrations specifying the -EnableAutomaticMigrations parameter.
</pre></div><br />
با توجه به اينكه در مثال قسمت سوم، از آغاز كننده سفارشي سازي شده DropCreateDatabaseAlways استفاده شده بود، اطلاعات آن در جدول سيستمي dbo.__MigrationHistory در بانك اطلاعاتي برنامه موجود است (تصويري از آنرا در قسمت اول اين سري مشاهده كرديد). سپس با توجه به ساختار بانك اطلاعاتي جاري، دو كلاس خودكار زير را ايجاد كرده است:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">namespace EF_Sample02.Migrations
{
using System;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Linq;
internal sealed class Configuration : DbMigrationsConfiguration<EF_Sample02.Sample2Context>
{
public Configuration()
{
AutomaticMigrationsEnabled = false;
}
protected override void Seed(EF_Sample02.Sample2Context context)
{
// This method will be called after migrating to the latest version.
// You can use the DbSet<T>.AddOrUpdate() helper extension method
// to avoid creating duplicate seed data. E.g.
//
// context.People.AddOrUpdate(
// p => p.FullName,
// new Person { FullName = "Andrew Peters" },
// new Person { FullName = "Brice Lambson" },
// new Person { FullName = "Rowan Miller" }
// );
//
}
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">namespace EF_Sample02.Migrations
{
using System.Data.Entity.Migrations;
public partial class InitialCreate : DbMigration
{
public override void Up()
{
CreateTable(
"Users",
c => new
{
Id = c.Int(nullable: false, identity: true),
Name = c.String(),
LastName = c.String(),
Email = c.String(),
Description = c.String(),
Photo = c.Binary(),
RowVersion = c.Binary(nullable: false, fixedLength: true, timestamp: true, storeType: "rowversion"),
Interests_Interest1 = c.String(maxLength: 450),
Interests_Interest2 = c.String(maxLength: 450),
AddDate = c.DateTime(nullable: false),
})
.PrimaryKey(t => t.Id);
CreateTable(
"Projects",
c => new
{
Id = c.Int(nullable: false, identity: true),
Title = c.String(maxLength: 50),
Description = c.String(),
RowVesrion = c.Binary(nullable: false, fixedLength: true, timestamp: true, storeType: "rowversion"),
AddDate = c.DateTime(nullable: false),
AdminUser_Id = c.Int(),
})
.PrimaryKey(t => t.Id)
.ForeignKey("Users", t => t.AdminUser_Id)
.Index(t => t.AdminUser_Id);
}
public override void Down()
{
DropIndex("Projects", new[] { "AdminUser_Id" });
DropForeignKey("Projects", "AdminUser_Id", "Users");
DropTable("Projects");
DropTable("Users");
}
}
}
</pre></div><br />
<br />
در اين كلاس خودكار، نحوه ايجاد جداول بانك اطلاعاتي تعريف شدهاند. در متد تحريف شده Up، كار ايجاد بانك اطلاعاتي و در متد تحريف شده Down، دستورات حذف جداول و قيود ذكر شدهاند.<br />
به علاوه اينبار متد Seed را در كلاس مشتق شده از DbMigrationsConfiguration، ميتوان تحريف و مقدار دهي كرد.<br />
علاوه بر اينها جدول سيستمي dbo.__MigrationHistory نيز با اطلاعات جاري مقدار دهي ميگردد.<br />
<br />
<br />
<b>فعال سازي گزينههاي مهاجرت خودكار</b><br />
<br />
براي استفاده از اين كلاسها، ابتدا به فايل Configuration.cs مراجعه كرده و خاصيت AutomaticMigrationsEnabled را true كنيد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">internal sealed class Configuration : DbMigrationsConfiguration<EF_Sample02.Sample2Context>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
}
</pre></div><br />
پس از آن EF به صورت خودكار كار استفاده و مديريت «Migrations» را عهدهدار خواهد شد. البته براي اين منظور بايد نوع آغاز كننده بانك اطلاعاتي را از DropCreateDatabaseAlways قبلي به نمونه جديد MigrateDatabaseToLatestVersion نيز تغيير دهيم:<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">//Database.SetInitializer(new Sample2DbInitializer());
Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample2Context, Migrations.Configuration>());
</pre></div><br />
<b>يك نكته:</b><br />
كلاس Migrations.Configuration كه بايد در حين وهله سازي از MigrateDatabaseToLatestVersion قيد شود (همانند كدهاي فوق)، از نوع internal sealed معرفي شده است. بنابراين اگر اين كلاس را در يك اسمبلي جداگانه قرار دادهايد، نياز است فايل را ويرايش كرده و internal sealed آنرا به public تغيير دهيد.<br />
<br />
روش ديگر معرفي كلاسهاي Context و Migrations.Configuration، حذف متد Database.SetInitializer و استفاده از فايل app.config يا web.config است به نحو زير ( در اينجا حرف ` اصطلاحا back tick نام دارد. فشردن دكمه ~ در حين تايپ انگليسي):<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code"><entityFramework>
<contexts>
<context type="EF_Sample02.Sample2Context, EF_Sample02">
<databaseInitializer
type="System.Data.Entity.MigrateDatabaseToLatestVersion`2[[EF_Sample02.Sample2Context, EF_Sample02],
[EF_Sample02.Migrations.Configuration, EF_Sample02]], EntityFramework"
/>
</context>
</contexts>
</entityFramework>
</pre></div><br />
<b>آزمودن ويژگي مهاجرت خودكار</b><br />
<br />
اكنون براي آزمايش اين موارد، يك خاصيت دلخواه را به كلاس Project به نام public string SomeProp اضافه كنيد. سپس برنامه را اجرا نمائيد.<br />
در ادامه به بانك اطلاعاتي مراجعه كرده و فيلدهاي جدول Projects را بررسي كنيد:<br />
<br />
<div align="left" dir="ltr"><pre language="Sql" name="code">CREATE TABLE [dbo].[Projects](
---...
[SomeProp] [nvarchar](max) NULL,
---...
</pre></div><br />
بله. اينبار فيلد SomeProp بدون از دست رفتن اطلاعات و drop بانك اطلاعاتي، به جدول پروژهها اضافه شده است.<br />
<br />
<br />
<b>عكس العمل ويژگي مهاجرت خودكار در مقابل از دست رفتن اطلاعات</b><br />
<br />
در ادامه، خاصيت public string SomeProp را كه در قسمت قبل به كلاس پروژه اضافه كرديم، حذف كنيد. اكنون مجددا برنامه را اجرا نمائيد. برنامه بلافاصله با استثناي زير متوقف خواهد شد:<br />
<br />
<div align="left" dir="ltr"><pre language="Sql" name="code">Automatic migration was not applied because it would result in data loss.
</pre></div><br />
از آنجائيكه حذف يك خاصيت مساوي است با حذف يك ستون در جدول بانك اطلاعاتي، امكان از دست رفتن اطلاعات در اين بين بسيار زياد است. بنابراين ويژگي مهاجرت خودكار ديگر اعمال نخواهد شد و اين مورد به نوعي يك محافظت خودكار است كه درنظر گرفته شده است.<br />
البته در EF Code first اين مساله را نيز ميتوان كنترل نمود. به كلاس Configuration اضافه شده توسط پاورشل مراجعه كرده و خاصيت AutomaticMigrationDataLossAllowed را به true تنظيم كنيد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">internal sealed class Configuration : DbMigrationsConfiguration<EF_Sample02.Sample2Context>
{
public Configuration()
{
this.AutomaticMigrationsEnabled = true;
this.AutomaticMigrationDataLossAllowed = true;
}
</pre></div><br />
اين تغيير به اين معنا است كه خودمان صريحا مجوز حذف يك ستون و اطلاعات مرتبط به آنرا صادر كردهايم.<br />
پس از اين تغيير، مجددا برنامه را اجرا كنيد. ستون SomeProp به صورت خودكار حذف خواهد شد، اما اطلاعات ركوردهاي موجود تغييري نخواهند كرد.<br />
<br />
<br />
<b>استفاده از Code first migrations بر روي يك بانك اطلاعاتي موجود</b><br />
<br />
تفاوت يك ديتابيس موجود با بانك اطلاعاتي توليد شده توسط EF Code first در نبود جدول سيستمي dbo.__MigrationHistory است.<br />
به اين ترتيب زمانيكه فرمان enable-migrations را در يك پروژه EF code first متصل به بانك اطلاعاتي قديمي موجود اجرا ميكنيم، پوشه Migration در آن ايجاد خواهد شد اما تنها حاوي فايل Configuration.cs است و نه فايلي شبيه به number_InitialCreate.cs .<br />
بنابراين نياز است به صورت صريح به EF اعلام كنيم كه نياز است تا جدول سيستمي dbo.__MigrationHistory و فايل number_InitialCreate.cs را نيز توليد كند. براي اين منظور كافي است دستور زير را در خط فرمان پاورشل NuGet پس از فراخواني enable-migrations اوليه، اجرا كنيم:<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">add-migration Initial -IgnoreChanges
</pre></div><br />
با بكارگيري پارامتر IgnoreChanges، متد Up در فايل number_InitialCreate.cs توليد نخواهد شد. به اين ترتيب نگران نخواهيم بود كه در اولين بار اجراي برنامه، تعاريف ديتابيس موجود ممكن است اندكي تغيير كند.<br />
سپس دستور زير را جهت به روز رساني جدول سيستمي dbo.__MigrationHistory اجرا كنيد:<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">update-database
</pre></div><br />
پس از آن جهت سوئيچ به مهاجرت خودكار، خاصيت AutomaticMigrationsEnabled = true را در فايل Configuration.cs همانند قبل مقدار دهي كنيد.<br />
<br />
<br />
<b>مشاهده دستوارت SQL به روز رساني بانك اطلاعاتي</b><br />
<br />
اگر علاقمند هستيد كه دستورات T-SQL به روز رساني بانك اطلاعاتي را نيز مشاهده كنيد، دستور Update-Database را با پارامتر Verbose آغاز نمائيد:<br />
<div align="left" dir="ltr"><pre language="Sql" name="code">Update-Database -Verbose
</pre></div><br />
و اگر تنها نياز به مشاهده اسكريپت توليدي بدون اجراي آنها بر روي بانك اطلاعاتي مدنظر است، از پارامتر Script بايد استفاده كرد:<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">update-database -Script
</pre></div><br />
<br />
<br />
<b>نكتهاي در مورد جدول سيستمي dbo.__MigrationHistory</b><br />
<br />
تنها دليلي كه اين جدول در SQL Server البته (ونه براي مثال در SQL Server CE) به صورت سيستمي معرفي ميشود اين است كه «جلوي چشم نباشد»! به اين ترتيب در SQL Server management studio در بين ساير جداول معمولي بانك اطلاعاتي قرار نميگيرد. اما براي EF تفاوتي نميكند كه اين جدول سيستمي است يا خير.<br />
همين سيستمي بودن آن ممكن است بر اساس سطح دسترسي كاربر اتصالي به بانك اطلاعاتي مساله ساز شود. براي نمونه ممكن است schema كاربر متصل dbo نباشد. همينجا است كه كار به روز رساني اين جدول متوقف خواهد شد. <br />
بنابراين اگر قصد داشتيد خواص سيستمي آنرا لغو كنيد، تنها كافي است دستورات T-SQL زير را در SQL Server اجرا نمائيد:<br />
<br />
<div align="left" dir="ltr"><pre language="Sql" name="code">SELECT * INTO [TempMigrationHistory]
FROM [__MigrationHistory]
DROP TABLE [__MigrationHistory]
EXEC sp_rename [TempMigrationHistory], [__MigrationHistory]
</pre></div><br />
<br />
<b>ساده سازي پروسه مهاجرت خودكار</b><br />
<br />
كل پروسهاي را كه در اين قسمت مشاهده كرديد، به صورت ذيل نيز ميتوان خلاصه كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Data.Entity.Migrations.Infrastructure;
using System.IO;
namespace EF_Sample02
{
public class Configuration<T> : DbMigrationsConfiguration<T> where T : DbContext
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
AutomaticMigrationDataLossAllowed = true;
}
}
public class SimpleDbMigrations
{
public static void UpdateDatabaseSchema<T>(string SQLScriptPath = "script.sql") where T : DbContext
{
var configuration = new Configuration<T>();
var dbMigrator = new DbMigrator(configuration);
saveToFile(SQLScriptPath, dbMigrator);
dbMigrator.Update();
}
private static void saveToFile(string SQLScriptPath, DbMigrator dbMigrator)
{
if (string.IsNullOrWhiteSpace(SQLScriptPath)) return;
var scriptor = new MigratorScriptingDecorator(dbMigrator);
var script = scriptor.ScriptUpdate(sourceMigration: null, targetMigration: null);
File.WriteAllText(SQLScriptPath, script);
Console.WriteLine(script);
}
}
}
</pre></div><br />
سپس براي استفاده از آن خواهيم داشت:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">SimpleDbMigrations.UpdateDatabaseSchema<Sample2Context>();
</pre></div><br />
در اين كلاس ذخيره سازي اسكريپت توليدي جهت به روز رساني بانك اطلاعاتي جاري در يك فايل نيز درنظر گرفته شده است.<br />
<br />
<br />
<br />
تا اينجا مهاجرت خودكار را بررسي كرديم. در قسمت بعدي Code-Based Migrations را ادامه خواهيم داد.</div>وحيد نصيريhttp://www.blogger.com/profile/04454130327051686471noreply@blogger.comtag:blogger.com,1999:blog-7815821915651048578.post-36009010262424836612012-05-05T17:37:00.002+04:302012-05-05T17:45:15.328+04:30EF Code First #3<div dir="rtl" style="text-align: right;" trbidi="on"><br />
<b>بررسي تعاريف نگاشتها به كمك متاديتا در EF Code first</b><br />
<br />
در قسمت قبل مروري سطحي داشتيم بر امكانات مهياي جهت تعاريف نگاشتها در EF Code first. در اين قسمت، حالت استفاده از متاديتا يا همان data annotations را با جزئيات بيشتري بررسي خواهيم كرد. <br />
براي اين منظور پروژه كنسول جديدي را آغاز نمائيد. همچنين به كمك NuGet، ارجاعات لازم را به اسمبلي EF، اضافه كنيد. در ادامه مدلهاي زير را به پروژه اضافه نمائيد؛ يك شخص كه تعدادي پروژه منتسب ميتواند داشته باشد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System;
using System.Collections.Generic;
namespace EF_Sample02.Models
{
public class User
{
public int Id { set; get; }
public DateTime AddDate { set; get; }
public string Name { set; get; }
public string LastName { set; get; }
public string Email { set; get; }
public string Description { set; get; }
public byte[] Photo { set; get; }
public IList<Project> Projects { set; get; }
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System;
namespace EF_Sample02.Models
{
public class Project
{
public int Id { set; get; }
public DateTime AddDate { set; get; }
public string Title { set; get; }
public string Description { set; get; }
public virtual User User { set; get; }
}
}
</pre></div><br />
به خاصيت public virtual User User در كلاس Project اصطلاحا Navigation property هم گفته ميشود.<br />
دو كلاس زير را نيز جهت تعريف كلاس Context كه بيانگر كلاسهاي شركت كننده در تشكيل بانك اطلاعاتي هستند و همچنين كلاس آغاز كننده بانك اطلاعاتي سفارشي را به همراه تعدادي ركورد پيش فرض مشخص ميكنند، به پروژه اضافه نمائيد.<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System;
using System.Collections.Generic;
using System.Data.Entity;
using EF_Sample02.Models;
namespace EF_Sample02
{
public class Sample2Context : DbContext
{
public DbSet<User> Users { set; get; }
public DbSet<Project> Projects { set; get; }
}
public class Sample2DbInitializer : DropCreateDatabaseAlways<Sample2Context>
{
protected override void Seed(Sample2Context context)
{
context.Users.Add(new User
{
AddDate = DateTime.Now,
Name = "Vahid",
LastName = "N.",
Email = "name@site.com",
Description = "-",
Projects = new List<Project>
{
new Project
{
Title = "Project 1",
AddDate = DateTime.Now.AddDays(-10),
Description = "..."
}
}
});
base.Seed(context);
}
}
}
</pre></div><br />
به علاوه در فايل كانفيگ برنامه، تنظيمات رشته اتصالي را نيز اضافه نمائيد:<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code"><connectionStrings>
<add
name="Sample2Context"
connectionString="Data Source=(local);Initial Catalog=testdb2012;Integrated Security = true"
providerName="System.Data.SqlClient"
/>
</connectionStrings>
</pre></div><br />
همانطور كه ملاحظه ميكنيد، در اينجا name به نام كلاس مشتق شده از DbContext اشاره ميكند (يكي از قراردادهاي توكار EF Code first است).<br />
<br />
<b>يك نكته:</b><br />
مرسوم است كلاسهاي مدل را در يك class library جداگانه اضافه كنند به نام DomainClasses و كلاسهاي مرتبط با DbContext را در پروژه class library ديگري به نام DataLayer. هيچكدام از اين پروژهها نيازي به فايل كانفيگ و تنظيمات رشته اتصالي ندارند؛ زيرا اطلاعات لازم را از فايل كانفيگ پروژه اصلي كه اين دو پروژه class library را به خود الحاق كرده، دريافت ميكنند. دو پروژه class library اضافه شده تنها بايد ارجاعاتي را به اسمبليهاي EF و data annotations داشته باشند.<br />
<br />
در ادامه به كمك متد Database.SetInitializer كه در قسمت دوم به بررسي آن پرداختيم و با استفاده از كلاس سفارشي Sample2DbInitializer فوق، نسبت به ايجاد يك بانك اطلاعاتي خالي تشكيل شده بر اساس تعاريف كلاسهاي دومين پروژه، اقدام خواهيم كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System;
using System.Data.Entity;
namespace EF_Sample02
{
class Program
{
static void Main(string[] args)
{
Database.SetInitializer(new Sample2DbInitializer());
using (var db = new Sample2Context())
{
var project1 = db.Projects.Find(1);
Console.WriteLine(project1.Title);
}
}
}
}
</pre></div><br />
تا زمانيكه وهلهاي از Sample2Context ساخته نشود و همچنين يك كوئري نيز به بانك اطلاعاتي ارسال نگردد، Sample2DbInitializer در عمل فراخواني نخواهد شد.<br />
ساختار بانك اطلاعاتي پيش فرض تشكيل شده نيز مطابق اسكريپت زير است:<br />
<br />
<div align="left" dir="ltr"><pre language="Sql" name="code">CREATE TABLE [dbo].[Users](
[Id] [int] IDENTITY(1,1) NOT NULL,
[AddDate] [datetime] NOT NULL,
[Name] [nvarchar](max) NULL,
[LastName] [nvarchar](max) NULL,
[Email] [nvarchar](max) NULL,
[Description] [nvarchar](max) NULL,
[Photo] [varbinary](max) NULL,
CONSTRAINT [PK_Users] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,
IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
</pre></div><br />
<div align="left" dir="ltr"><pre language="Sql" name="code">CREATE TABLE [dbo].[Projects](
[Id] [int] IDENTITY(1,1) NOT NULL,
[AddDate] [datetime] NOT NULL,
[Title] [nvarchar](max) NULL,
[Description] [nvarchar](max) NULL,
[User_Id] [int] NULL,
CONSTRAINT [PK_Projects] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,
IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[Projects] WITH CHECK ADD CONSTRAINT [FK_Projects_Users_User_Id] FOREIGN KEY([User_Id])
REFERENCES [dbo].[Users] ([Id])
GO
ALTER TABLE [dbo].[Projects] CHECK CONSTRAINT [FK_Projects_Users_User_Id]
GO
</pre></div><br />
توضيحاتي در مورد ساختار فوق، جهت يادآوري مباحث دو قسمت قبل:<br />
- خواصي با نام Id تبديل به primary key و identity field شدهاند.<br />
- نام جداول، همان نام خواص تعريف شده در كلاس Context است.<br />
- تمام رشتهها به nvarchar از نوع max نگاشت شدهاند و null پذير ميباشند.<br />
- خاصيت تصوير كه با آرايهاي از بايتها تعريف شده به varbinary از نوع max نگاشت شده است.<br />
- بر اساس ارتباط بين كلاسها فيلد User_Id در جدول Projects اضافه شده است كه توسط قيدي به نام FK_Projects_Users_User_Id، جهت تعريف كليد خارجي عمل ميكند. اين نام گذاري پيش فرض هم بر اساس نام خواص در دو كلاس انجام ميشود. <br />
- schema پيش فرض بكارگرفته شده، dbo است.<br />
- null پذيري پيش فرض فيلدها بر اساس اصول زبان مورد استفاده تعيين شده است. براي مثال در سي شارپ، نوع int نال پذير نيست يا نوع DateTime نيز به همين ترتيب يك value type است. بنابراين در اينجا اين دو نوع به صورت not null تعريف شدهاند (صرفنظر از اينكه در SQL Server هر دو نوع ياد شده، null پذير هم ميتوانند باشند). بديهي است امكان تعريف nullable types نيز وجود دارد.<br />
<br />
<br />
<b>مروري بر انواع متاديتاي قابل استفاده در EF Code first</b><br />
<br />
<b>1) Key</b><br />
همانطور كه ملاحظه كرديد اگر نام خاصيتي Id يا ClassName+Id باشد، به صورت خودكار به عنوان primary key جدول، مورد استفاده قرار خواهد گرفت. اين يك قرارداد توكار است.<br />
اگر يك چنين خاصيتي با نامهاي ذكر شده در كلاس وجود نداشته باشد، ميتوان با مزين سازي خاصيتي مفروض با ويژگي Key كه در فضاي نام System.ComponentModel.DataAnnotations قرار دارد، آنرا به عنوان Primary key معرفي نمود. براي مثال:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">public class Project
{
[Key]
public int ThisIsMyPrimaryKey { set; get; }
</pre></div><br />
و ضمنا بايد دقت داشت كه حين كار با ORMs فرقي نميكند EF باشد يا ساير فريم وركهاي ديگر، داشتن يك key جهت عملكرد صحيح فريم ورك، ضروري است. بر اساس يك Key است كه Entity معنا پيدا ميكند.<br />
<br />
<br />
<b>2) Required</b><br />
ويژگي Required كه در فضاي نام System.ComponentModel.DataAnnotations تعريف شده است، سبب خواهد شد يك خاصيت به صورت not null در بانك اطلاعاتي تعريف شود. همچنين در مباحث اعتبارسنجي برنامه، پيش از ارسال اطلاعات به سرور نيز نقش خواهد داشت. در صورت نال بودن خاصيتي كه با ويژگي Required مزين شده است، يك استثناي اعتبارسنجي پيش از ذخيره سازي اطلاعات در بانك اطلاعاتي صادر ميگردد. اين ويژگي علاوه بر EF Code first در ASP.NET MVC نيز به نحو يكساني تاثيرگذار است.<br />
<br />
<br />
<b>3) MaxLength و MinLength</b><br />
اين دو ويژگي نيز در فضاي نام System.ComponentModel.DataAnnotations قرار دارند (اما در اسمبلي EntityFramework.dll تعريف شدهاند و جزو اسمبلي پايه System.ComponentModel.DataAnnotations.dll نيستند). در ذيل نمونهاي از تعريف اينها را مشاهده ميكنيد. همچنين بايد درنظر داشت كه روش ديگر تعريف متاديتا، تركيب آنها در يك سطر نيز ميباشد. يعني الزامي ندارد در هر سطر يك متاديتا را تعريف كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">[MaxLength(50, ErrorMessage = "حداكثر 50 حرف"), MinLength(4, ErrorMessage = "حداقل 4 حرف")]
public string Title { set; get; }
</pre></div><br />
ويژگي MaxLength بر روي طول فيلد تعريف شده در بانك اطلاعاتي تاثير دارد. براي مثال در اينجا فيلد Title از نوع nvarchar با طول 30 تعريف خواهد شد.<br />
ويژگي MinLength در بانك اطلاعاتي معنايي ندارد.<br />
هر دوي اين ويژگيها در پروسه اعتبار سنجي اطلاعات مدل دريافتي تاثير دارند. براي مثال در اينجا اگر طول عنوان كمتر از 4 حرف باشد، يك استثناي اعتبارسنجي صادر خواهد شد.<br />
<br />
ويژگي ديگري نيز به نام StringLength وجود دارد كه جهت تعيين حداكثر طول رشتهها به كار ميرود. اين ويژگي سازگاري بيشتر با ASP.NET MVC دارد از اين جهت كه Client side validation آنرا نيز فعال ميكند.<br />
<br />
<br />
<b>4) Table و Column</b><br />
اين دو ويژگي نيز در فضاي نام System.ComponentModel.DataAnnotations قرار دارند، اما در اسمبلي EntityFramework.dll تعريف شدهاند. بنابراين اگر تعاريف مدلهاي شما در پروژه Class library جداگانهاي قراردارند، نياز خواهد بود تا ارجاعي را به اسمبلي EntityFramework.dll نيز داشته باشند.<br />
اگر از نام پيش فرض جداول تشكيل شده خرسند نيستيد، ويژگي Table را بر روي يك كلاس قرار داده و نام ديگري را تعريف كنيد. همچنين اگر Schema كاربري رشته اتصالي به بانك اطلاعاتي شما dbo نيست، بايد آنرا در اينجا صريحا ذكر كنيد تا كوئريهاي تشكيل شده به درستي بر روي بانك اطلاعاتي اجرا گردند:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">[Table("tblProject", Schema="guest")]
public class Project
</pre></div><br />
توسط ويژگي Column سه خاصيت يك فيلد بانك اطلاعاتي را ميتوان تعيين كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">[Column("DateStarted", Order = 4, TypeName = "date")]
public DateTime AddDate { set; get; }
</pre></div><br />
به صورت پيش فرض، خاصيت فوق با همين نام AddDate در بانك اطلاعاتي ظاهر ميگردد. اگر براي مثال قرار است از يك بانك اطلاعاتي قديمي استفاده شود يا قرار نيست از شيوه نامگذاري خواص در سي شارپ در يك بانك اطلاعاتي پيروي شود، توسط ويژگي Column ميتوان اين تعاريف را سفارشي نمود. <br />
توسط پارامتر Order آن كه از صفر شروع ميشود، ترتيب قرارگيري فيلدها در حين تشكيل يك جدول مشخص ميگردد. <br />
اگر نياز است نوع فيلد تشكيل شده را نيز سفارشي سازي نمائيد، ميتوان از پارامتر TypeName استفاده كرد. براي مثال در اينجا علاقمنديم از نوع date مهيا در SQL Server 2008 استفاده كنيم و نه از نوع datetime پيش فرض آن.<br />
<br />
<b>نكتهاي در مورد Order:</b><br />
Order پيش فرض تمام خواصي كه قرار است به بانك اطلاعاتي نگاشت شوند، به int.MaxValue تنظيم شدهاند. به اين معنا كه تنظيم فوق با Order=4 سبب خواهد شد تا اين فيلد، پيش از تمام فيلدهاي ديگر قرار گيرد. بنابراين نياز است Order اولين خاصيت تعريف شده را به صفر تنظيم نمود. (البته اگر واقعا نياز به تنظيم دستي Order داشتيد)<br />
<br />
<br />
<b>نكاتي در مورد تنظيمات ارث بري در حالت استفاده از متاديتا:</b><br />
حداقل سه حالت ارث بري را در EF code first ميتوان تعريف و مديريت كرد:<br />
<b>الف)</b> Table per Hierarchy - TPH<br />
حالت پيش فرض است. نيازي به هيچگونه تنظيمي ندارد. معناي آن اين است كه «لطفا تمام اطلاعات كلاسهايي را كه از هم ارث بري كردهاند در يك جدول بانك اطلاعاتي قرار بده». فرض كنيد يك كلاس پايه شخص را داريد كه كلاسهاي بازيكن و مربي از آن ارث بري ميكنند. زمانيكه كلاس پايه شخص توسط DbSet در كلاس مشتق شده از DbContext در معرض استفاده EF قرار ميگيرد، بدون نياز به هيچ تنظيمي، تمام اين سه كلاس، تبديل به يك جدول شخص در بانك اطلاعاتي خواهند شد. يعني يك table به ازاي سلسله مراتبي (Hierarchy) كه تعريف شده.<br />
<b>ب)</b> Table per Type - TPT<br />
به اين معنا است كه به ازاي هر نوع، بايد يك جدول تشكيل شود. به عبارتي در مثال قبل، يك جدول براي شخص، يك جدول براي مربي و يك جدول براي بازيكن تشكيل خواهد شد. دو جدول مربي و بازيكن با يك كليد خارجي به جدول شخص مرتبط ميشوند. تنها تنظيمي كه در اينجا نياز است، قرار دادن ويژگي Table بر روي نام كلاسهاي بازيكن و مربي است. به اين ترتيب حالت پيش فرض الف (TPH) اعمال نخواهد شد.<br />
<b>ج)</b> Table per Concrete Type - TPC<br />
در اين حالت فقط دو جدول براي بازيكن و مربي تشكيل ميشوند و جدولي براي شخص تشكيل نخواهد شد. خواص كلاس شخص، در هر دو جدول مربي و بازيكن به صورت جداگانهاي تكرار خواهد شد. تنظيم اين مورد نياز به استفاده از Fluent API دارد.<br />
<br />
توضيحات بيشتر اين موارد به همراه مثال، موكول خواهد شد به مباحث استفاده از Fluent API كه براي تعريف تنظيمات پيشرفته نگاشتها طراحي شده است. استفاده از متاديتا تنها قسمت كوچكي از تواناييهاي Fluent API را شامل ميشود.<br />
<br />
<br />
<br />
<b>5) ConcurrencyCheck و Timestamp</b><br />
هر دوي اين ويژگيها در فضاي نام System.ComponentModel.DataAnnotations و اسمبلي به همين نام تعريف شدهاند.<br />
در EF Code first دو راه براي مديريت مسايل همزماني وجود دارد:<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">[ConcurrencyCheck]
public string Name { set; get; }
[Timestamp]
public byte[] RowVersion { set; get; }
</pre></div><br />
زمانيكه از ويژگي ConcurrencyCheck استفاده ميشود، تغيير خاصي در سمت بانك اطلاعاتي صورت نخواهد گرفت، اما در برنامه، كوئريهاي update و delete ايي كه توسط EF صادر ميشوند، اينبار اندكي متفاوت خواهند بود. براي مثال برنامه جاري را به نحو زير تغيير دهيد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System;
using System.Data.Entity;
namespace EF_Sample02
{
class Program
{
static void Main(string[] args)
{
Database.SetInitializer(new Sample2DbInitializer());
using (var db = new Sample2Context())
{
//update
var user = db.Users.Find(1);
user.Name = "User name 1";
db.SaveChanges();
}
}
}
}
</pre></div><br />
متد Find بر اساس primary key عمل ميكند. به اين ترتيب، اول ركورد يافت شده و سپس نام آن تغيير كرده و در ادامه، اطلاعات ذخيره خواهند شد.<br />
اكنون اگر توسط SQL Server Profiler كوئري update حاصل را بررسي كنيم، به نحو زير خواهد بود:<br />
<br />
<div align="left" dir="ltr"><pre language="Sql" name="code">exec sp_executesql N'update [dbo].[Users]
set [Name] = @0
where (([Id] = @1) and ([Name] = @2))
',N'@0 nvarchar(max) ,@1 int,@2 nvarchar(max) ',@0=N'User name 1',@1=1,@2=N'Vahid'
</pre></div><br />
همانطور كه ملاحظه ميكنيد، براي به روز رساني فقط از primary key جهت يافتن ركورد استفاده نكرده، بلكه فيلد Name را نيز دخالت داده است. از اين جهت كه مطمئن شود در اين بين، ركوردي كه در حال به روز رساني آن هستيم، توسط كاربر ديگري در شبكه تغيير نكرده باشد و اگر در اين بين تغييري رخ داده باشد، يك استثناء صادر خواهد شد.<br />
همين رفتار در مورد delete نيز وجود دارد:<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">//delete
var user = db.Users.Find(1);
db.Users.Remove(user);
db.SaveChanges();
</pre></div>كه خروجي آن به صورت زير است:<br />
<br />
<div align="left" dir="ltr"><pre language="Sql" name="code">exec sp_executesql N'delete [dbo].[Users]
where (([Id] = @0) and ([Name] = @1))',N'@0 int,@1 nvarchar(max) ',@0=1,@1=N'Vahid'
</pre></div><br />
در اينجا نيز به علت مزين بودن خاصيت Name به ويژگي ConcurrencyCheck، فقط همان ركوردي كه يافت شده بايد حذف شود و نه نمونه تغيير يافته آن توسط كاربري ديگر در شبكه.<br />
البته در اين مثال شايد اين پروسه تنها چند ميلي ثانيه به نظر برسد. اما در برنامهاي با رابط كاربري، شخصي ممكن است اطلاعات يك ركورد را در يك صفحه دريافت كرده و 5 دقيقه بعد بر روي دكمه save كليك كند. در اين بين ممكن است شخص ديگري در شبكه همين ركورد را تغيير داده باشد. بنابراين اطلاعاتي را كه شخص مشاهده ميكند، فاقد اعتبار شدهاند.<br />
<br />
ConcurrencyCheck را بر روي هر فيلدي ميتوان بكاربرد، اما ويژگي Timestamp كاربرد مشخص و محدودي دارد. بايد به خاصيتي از نوع byte array اعمال شود (كه نمونهاي از آنرا در بالا در خاصيت public byte[] RowVersion مشاهده نموديد). علاوه بر آن، اين ويژگي بر روي بانك اطلاعاتي نيز تاثير دارد (نوع فيلد را در SQL Server تبديل به timestamp ميكند و نه از نوع varbinary مانند فيلد تصوير). SQL Server با اين نوع فيلد به خوبي آشنا است و قابليت مقدار دهي خودكار آنرا دارد. بنابراين نيازي نيست در حين تشكيل اشياء در برنامه، قيد شود.<br />
پس از آن، اين فيلد مقدار دهي شده به صورت خودكار توسط بانك اطلاعاتي، در تمام updateها و deleteهاي EF Code first حضور خواهد داشت:<br />
<br />
<div align="left" dir="ltr"><pre language="Sql" name="code">exec sp_executesql N'delete [dbo].[Users]
where ((([Id] = @0) and ([Name] = @1)) and ([RowVersion] = @2))',N'@0 int,@1 nvarchar(max) ,
@2 binary(8)',@0=1,@1=N'Vahid',@2=0x00000000000007D1
</pre></div><br />
از اين جهت كه اطمينان حاصل شود، واقعا مشغول به روز رساني يا حذف ركوردي هستيم كه در ابتداي عمليات از بانك اطلاعاتي دريافت كردهايم. اگر در اين بين RowVesrion تغيير كرده باشد، يعني كاربر ديگري در شبكه اين ركورد را تغيير داده و ما در حال حاضر مشغول به كار با ركوردي غيرمعتبر هستيم.<br />
بنابراين استفاده از Timestamp را ميتوان به عنوان يكي از best practices طراحي برنامههاي چند كاربره ASP.NET درنظر داشت.<br />
<br />
<br />
<b>6) NotMapped و DatabaseGenerated</b><br />
اين دو ويژگي نيز در فضاي نام System.ComponentModel.DataAnnotations قرار دارند، اما در اسمبلي EntityFramework.dll تعريف شدهاند.<br />
به كمك ويژگي DatabaseGenerated، مشخص خواهيم كرد كه اين فيلد قرار است توسط بانك اطلاعاتي توليد شود. براي مثال خواصي از نوع public int Id به صورت خودكار به فيلدهايي از نوع identity كه توسط بانك اطلاعاتي توليد ميشوند، نگاشت خواهند شد و نيازي نيست تا به صورت صريح از ويژگي DatabaseGenerated جهت مزين سازي آنها كمك گرفت. البته اگر علاقمند نيستيد كه primary key شما از نوع identity باشد، ميتوانيد از گزينه DatabaseGeneratedOption.None استفاده نمائيد:<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { set; get; }
</pre></div><br />
DatabaseGeneratedOption در اينجا يك enum است كه به نحو زير تعريف شده است:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">public enum DatabaseGeneratedOption
{
None = 0,
Identity = 1,
Computed = 2
}
</pre></div><br />
تا اينجا حالتهاي None و Identity آن، بحث شدند. <br />
در SQL Server امكان تعريف فيلدهاي محاسباتي و Computed با T-SQL نويسي نيز وجود دارد. اين نوع فيلدها در هربار insert يا update يك ركورد، به صورت خودكار توسط بانك اطلاعاتي مقدار دهي ميشوند. بنابراين اگر قرار است خاصيتي به اين نوع فيلدها در SQL Server نگاشت شود، ميتوان از گزينه DatabaseGeneratedOption.Computed استفاده كرد. <br />
يا اگر براي فيلدي در بانك اطلاعاتي default value تعريف كردهايد، مثلا براي فيلد date متد getdate توكار SQL Server را به عنوان پيش فرض درنظر گرفتهايد و قرار هم نيست توسط برنامه مقدار دهي شود، باز هم ميتوان آنرا از نوع DatabaseGeneratedOption.Computed تعريف كرد. <br />
البته بايد درنظر داشت كه اگر خاصيت DateTime تعريف شده در اينجا به همين نحو بكاربرده شود، اگر مقداري براي آن در حين تعريف يك وهله جديد از كلاس User دركدهاي برنامه درنظر گرفته نشود، يك مقدار پيش فرض حداقل به آن انتساب داده خواهد شد (چون value type است). بنابراين نياز است اين خاصيت را از نوع nullable تعريف كرد (public DateTime? AddDate).<br />
<br />
همچنين اگر يك خاصيت محاسباتي در كلاسي به صورت ReadOnly تعريف شده است (توسط كدهاي مثلا سي شارپ يا وي بي):<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">[NotMapped]
public string FullName
{
get { return Name + " " + LastName; }
}
</pre></div><br />
بديهي است نيازي نيست تا آنرا به يك فيلد بانك اطلاعاتي نگاشت كرد. اين نوع خواص را با ويژگي NotMapped ميتوان مزين كرد.<br />
همچنين بايد دقت داشت در اين حالت، از اين نوع خواص ديگر نميتوان در كوئريهاي EF استفاده كرد. چون نهايتا اين كوئريها قرار هستند به عبارات SQL ترجمه شوند و چنين فيلدي در جدول بانك اطلاعاتي وجود ندارد. البته بديهي است امكان تهيه كوئري LINQ to Objects (كوئري از اطلاعات درون حافظه) هميشه مهيا است و اهميتي ندارد كه اين خاصيت درون بانك اطلاعاتي معادلي دارد يا خير.<br />
<br />
<br />
<b>7) ComplexType</b><br />
ComplexType يا Component mapping مربوط به حالتي است كه شما يك سري خواص را در يك كلاس تعريف ميكنيد، اما قصد نداريد اينها واقعا تبديل به يك جدول مجزا (به همراه كليد خارجي) در بانك اطلاعاتي شوند. ميخواهيد اين خواص دقيقا در همان جدول اصلي كنار مابقي خواص قرار گيرند؛ اما در طرف كدهاي ما به شكل يك كلاس مجزا تعريف و مديريت شوند. <br />
يك مثال:<br />
كلاس زير را به همراه ويژگي ComplexType به برنامه مطلب جاري اضافه نمائيد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.ComponentModel.DataAnnotations;
namespace EF_Sample02.Models
{
[ComplexType]
public class InterestComponent
{
[MaxLength(450, ErrorMessage = "حداكثر 450 حرف")]
public string Interest1 { get; set; }
[MaxLength(450, ErrorMessage = "حداكثر 450 حرف")]
public string Interest2 { get; set; }
}
}
</pre></div><br />
سپس خاصيت زير را نيز به كلاس User اضافه كنيد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">public InterestComponent Interests { set; get; }
</pre></div><br />
همانطور كه ملاحظه ميكنيد كلاس InterestComponent فاقد Id است؛ بنابراين هدف از آن تعريف يك Entity نيست و قرار هم نيست در كلاس مشتق شده از DbContext تعريف شود. از آن صرفا جهت نظم بخشيدن به يك سري خاصيت مرتبط و همخانواده استفاده شده است (مثلا آدرس يك، آدرس 2، تا آدرس 10 يك شخص، يا تلفن يك تلفن 2 يا موبايل 10 يك شخص).<br />
اكنون اگر پروژه را اجرا نمائيم، ساختار جدول كاربر به نحو زير تغيير خواهد كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="Sql" name="code">CREATE TABLE [dbo].[Users](
---...
[Interests_Interest1] [nvarchar](450) NULL,
[Interests_Interest2] [nvarchar](450) NULL,
---...
</pre></div><br />
در اينجا خواص كلاس InterestComponent، داخل همان كلاس User تعريف شدهاند و نه در يك جدول مجزا. تنها در سمت كدهاي ما است كه مديريت آنها منطقيتر شدهاند.<br />
<br />
<b>يك نكته:</b><br />
يكي از الگوهايي كه حين تشكيل مدلهاي برنامه عموما مورد استفاده قرار ميگيرد، null object pattern نام دارد. براي مثال:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">namespace EF_Sample02.Models
{
public class User
{
public InterestComponent Interests { set; get; }
public User()
{
Interests = new InterestComponent();
}
}
}
</pre></div><br />
در اينجا در سازنده كلاس User، به خاصيت Interests وهلهاي از كلاس InterestComponent نسبت داده شده است. به اين ترتيب ديگر در كدهاي برنامه مدام نيازي نخواهد بود تا بررسي شود كه آيا Interests نال است يا خير. همچنين استفاده از اين الگو حين كار با يك ComplexType ضروري است؛ زيرا EF امكان ثبت ركورد جاري را در صورت نال بودن خاصيت Interests (صرفنظر از اينكه خواص آن مقدار دهي شدهاند يا خير) نخواهد داد.<br />
<br />
<br />
<b>8) ForeignKey</b><br />
اين ويژگي نيز در فضاي نام System.ComponentModel.DataAnnotations قرار دارد، اما در اسمبلي EntityFramework.dll تعريف شدهاست.<br />
اگر از قراردادهاي پيش فرض نامگذاري كليدهاي خارجي در EF Code first خرسند نيستيد، ميتوانيد توسط ويژگي ForeignKey، نامگذاري مورد نظر خود را اعمال نمائيد. بايد دقت داشت كه ويژگي ForeignKey را بايد به يك Reference property اعمال كرد. همچنين در اين حالت، كليد خارجي را با يك value type نيز ميتوان نمايش داد:<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">[ForeignKey("FK_User_Id")]
public virtual User User { set; get; }
public int FK_User_Id { set; get; }
</pre></div><br />
در اينجا فيلد اضافي دوم FK_User_Id به جدول Project اضافه نخواهد شد (چون توسط ويژگي ForeignKey تعريف شده است و فقط يكبار تعريف ميشود). اما در اين حالت نيز وجود Reference property ضروري است.<br />
<br />
<br />
<b>9) InverseProperty</b><br />
اين ويژگي نيز در فضاي نام System.ComponentModel.DataAnnotations قرار دارد، اما در اسمبلي EntityFramework.dll تعريف شدهاست.<br />
از ويژگي InverseProperty براي تعريف روابط دو طرفه استفاده ميشود.<br />
براي مثال دو كلاس زير را درنظر بگيريد:<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">public class Book
{
public int ID {get; set;}
public string Title {get; set;}
[InverseProperty("Books")]
public Author Author {get; set;}
}
public class Author
{
public int ID {get; set;}
public string Name {get; set;}
[InverseProperty("Author")]
public virtual ICollection<Book> Books {get; set;}
}
</pre></div><br />
اين دو كلاس همانند كلاسهاي User و Project فوق هستند. ذكر ويژگي InverseProperty براي مشخص سازي ارتباطات بين اين دو غيرضروري است و قراردادهاي توكار EF Code first يك چنين مواردي را به خوبي مديريت ميكنند.<br />
اما اكنون مثال زير را درنظر بگيريد:<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">public class Book
{
public int ID {get; set;}
public string Title {get; set;}
public Author FirstAuthor {get; set;}
public Author SecondAuthor {get; set;}
}
public class Author
{
public int ID {get; set;}
public string Name {get; set;}
public virtual ICollection<Book> BooksAsFirstAuthor {get; set;}
public virtual ICollection<Book> BooksAsSecondAuthor {get; set;}
}
</pre></div><br />
اين مثال ويژهاي است از كتابخانهاي كه كتابهاي آن، تنها توسط دو نويسنده نوشته شدهاند. اگر برنامه را بر اساس اين دو كلاس اجرا كنيم، EF Code first قادر نخواهد بود تشخيص دهد، روابط كدام به كدام هستند و در جدول Books چهار كليد خارجي را ايجاد ميكند. براي مديريت اين مساله و تعين ابتدا و انتهاي روابط ميتوان از ويژگي InverseProperty كمك گرفت:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">public class Book
{
public int ID {get; set;}
public string Title {get; set;}
[InverseProperty("BooksAsFirstAuthor")]
public Author FirstAuthor {get; set;}
[InverseProperty("BooksAsSecondAuthor")]
public Author SecondAuthor {get; set;}
}
public class Author
{
public int ID {get; set;}
public string Name {get; set;}
[InverseProperty("FirstAuthor")]
public virtual ICollection<Book> BooksAsFirstAuthor {get; set;}
[InverseProperty("SecondAuthor")]
public virtual ICollection<Book> BooksAsSecondAuthor {get; set;}
}
</pre></div><br />
اينبار اگر برنامه را اجرا كنيم، بين اين دو جدول تنها دو رابطه تشكيل خواهد شد و نه چهار رابطه؛ چون EF اكنون ميداند كه ابتدا و انتهاي روابط كجا است. همچنين ذكر ويژگي InverseProperty در يك سر رابطه كفايت ميكند و نيازي به ذكر آن در طرف دوم نيست.<br />
<br />
<br />
<br />
<br />
</div>وحيد نصيريhttp://www.blogger.com/profile/04454130327051686471noreply@blogger.comtag:blogger.com,1999:blog-7815821915651048578.post-76181169738659471892012-05-04T09:36:00.000+04:302012-05-04T09:36:49.972+04:30EF Code First #2<div dir="rtl" style="text-align: right;" trbidi="on"><br />
در قسمت قبل با تنظيمات و قراردادهاي ابتدايي EF Code first آشنا شديم، هرچند اين تنظيمات حجم كدنويسي ابتدايي راه اندازي سيستم را به شدت كاهش ميدهند، اما كافي نيستند. در اين قسمت نگاهي سطحي و مقدماتي خواهيم داشت بر امكانات مهيا جهت تنظيم ويژگيهاي مدلهاي برنامه در EF Code first.<br />
<br />
<b>تنظيمات EF Code first توسط اعمال متاديتاي خواص</b><br />
<br />
اغلب متاديتاي مورد نياز جهت اعمال تنظيمات EF Code first در اسمبلي System.ComponentModel.DataAnnotations.dll قرار دارند. بنابراين اگر مدلهاي خود را در اسمبلي و پروژه class library جداگانهاي تعريف و نگهداري ميكنيد (مثلا به نام DomainClasses)، نياز است ابتدا ارجاعي را به اين اسمبلي به پروژه جاري اضافه نمائيم. همچنين تعدادي ديگر از متاديتاي قابل استفاده در خود اسمبلي EntityFramework.dll قرار دارند. بنابراين در صورت نياز بايد ارجاعي را به اين اسمبلي نيز اضافه نمود.<br />
همان مثال قبل را در اينجا ادامه ميدهيم. دو كلاس Blog و Post در آن تعريف شده (به اين نوع كلاسها POCO – the Plain Old CLR Objects نيز گفته ميشود)، به همراه كلاس Context كه از كلاس DbContext مشتق شده است. ابتدا ديتابيس قبلي را دستي drop كنيد. سپس در كلاس Blog، خاصيت public int Id را مثلا به public int MyTableKey تغيير دهيد و پروژه را اجرا كنيد. برنامه بلافاصله با خطاي زير متوقف ميشود:<br />
<br />
<div align="left" dir="ltr"><pre language="xml" name="code">One or more validation errors were detected during model generation:
\tSystem.Data.Entity.Edm.EdmEntityType: : EntityType 'Blog' has no key defined.
</pre></div><br />
زيرا EF Code first در اين كلاس خاصيتي به نام Id يا BlogId را نيافتهاست و امكان تشكيل Primary key جدول را ندارد. براي رفع اين مشكل تنها كافي است ويژگي Key را به اين خاصيت اعمال كنيم:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace EF_Sample01.Models
{
public class Blog
{
[Key]
public int MyTableKey { set; get; }
</pre></div><br />
همچنين تعدادي ويژگي ديگر مانند MaxLength و Required را نيز ميتوان بر روي خواص كلاس اعمال كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace EF_Sample01.Models
{
public class Blog
{
[Key]
public int MyTableKey { set; get; }
[MaxLength(100)]
public string Title { set; get; }
[Required]
public string AuthorName { set; get; }
public IList<Post> Posts { set; get; }
}
}
</pre></div><br />
اين ويژگيها دو مقصود مهم را برآورده ميسازند:<br />
الف) بر روي ساختار بانك اطلاعاتي تشكيل شده تاثير دارند:<br />
<br />
<div align="left" dir="ltr"><pre language="Sql" name="code">CREATE TABLE [dbo].[Blogs](
[MyTableKey] [int] IDENTITY(1,1) NOT NULL,
[Title] [nvarchar](100) NULL,
[AuthorName] [nvarchar](max) NOT NULL,
CONSTRAINT [PK_Blogs] PRIMARY KEY CLUSTERED
(
[MyTableKey] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,
IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
</pre></div><br />
همانطور كه ملاحظه ميكنيد در اينجا طول فيلد Title به 100 تنظيم شده است و همچنين فيلد AuthorName اينبار NOT NULL است. به علاوه primary key نيز بر اساس ويژگي Key اعمالي تعيين شده است.<br />
البته براي اجراي كدهاي تغيير كرده مدل، فعلا بانك اطلاعاتي قبلي را دستي ميتوان حذف كرد تا بتوان به ساختار جديد رسيد. در مورد جزئيات مبحث DB Migration در قسمتهاي بعدي مفصلا بحث خواهد شد.<br />
<br />
ب) اعتبار سنجي اطلاعات پيش از ارسال كوئري به بانك اطلاعاتي<br />
براي مثال اگر در حين تعريف وهلهاي از كلاس Blog، خاصيت AuthorName مقدار دهي نگردد، پيش از اينكه رفت و برگشتي به بانك اطلاعاتي صورت گيرد، يك validation error را دريافت خواهيم كرد. يا براي مثال اگر طول اطلاعات خاصيت Title بيش از 100 حرف باشد نيز مجددا در حين ثبت اطلاعات، يك استثناي اعتبار سنجي را مشاهده خواهيم كرد. البته امكان تعريف پيغامهاي خطاي سفارشي نيز وجود دارد. براي اين حالت تنها كافي است پارامتر ErrorMessage اين ويژگيها را مقدار دهي كرد. براي مثال:<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">[Required(ErrorMessage = "لطفا نام نويسنده را مشخص نمائيد")]
public string AuthorName { set; get; }
</pre></div><br />
نكتهي مهمي كه در اينجا وجود دارد، وجود يك اكوسيستم هماهنگ و سازگار است. اين نوع اعتبار سنجي هم با EF Code first هماهنگ است و هم براي مثال در ASP.NET MVC به صورت خودكار جهت اعتبار سنجي سمت سرور و كلاينت يك مدل ميتواند مورد استفاده قرار گيرد و مفاهيم و روشهاي مورد استفاده در آن نيز يكي است.<br />
<br />
<br />
<b>تنظيمات EF Code first به كمك Fluent API</b><br />
<br />
اگر علاقمند به استفاده از متاديتا، جهت تعريف قيود و ويژگيهاي خواص كلاسهاي مدل خود نيستيد، روش ديگري نيز در EF Code first به نام Fluent API تدارك ديده شده است. در اينجا امكان تعريف همان ويژگيها توسط كدنويسي نيز وجود دارد، به علاوه اعمال قيود ديگري كه توسط متاديتاي مهيا قابل تعريف نيستند.<br />
محل تعريف اين قيود، كلاس Context كه از كلاس DbContext مشتق شده است، ميباشد و در اينجا، كار با تحريف متد OnModelCreating شروع ميشود:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Data.Entity;
using EF_Sample01.Models;
namespace EF_Sample01
{
public class Context : DbContext
{
public DbSet<Blog> Blogs { set; get; }
public DbSet<Post> Posts { set; get; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>().HasKey(x => x.MyTableKey);
modelBuilder.Entity<Blog>().Property(x => x.Title).HasMaxLength(100);
modelBuilder.Entity<Blog>().Property(x => x.AuthorName).IsRequired();
base.OnModelCreating(modelBuilder);
}
}
}
</pre></div><br />
به كمك پارامتر modelBuilder، امكان دسترسي به متدهاي تنظيم كننده ويژگيهاي خواص يك مدل يا موجوديت وجود دارد. در اينجا چون ميتوان متدها را به صورت يك زنجيره به هم متصل كرد و همچنين حاصل نهايي شبيه به جمله بندي انگليسي است، به آن Fluent API يا API روان نيز گفته ميشود.<br />
البته در اين حالت امكان تعريف ErrorMessage وجود ندارد و براي اين منظور بايد از همان data annotations استفاده كرد.<br />
<br />
<br />
<b>نحوه مديريت صحيح تعاريف نگاشتها به كمك Fluent API</b><br />
<br />
OnModelCreating محل مناسبي جهت تعريف حجم انبوهي از تنظيمات كلاسهاي مختلف مدلهاي برنامه نيست. در حد سه چهار سطر مشكلي ندارد اما اگر بيشتر شد بهتر است از روش زير استفاده شود:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Data.Entity;
using EF_Sample01.Models;
using System.Data.Entity.ModelConfiguration;
namespace EF_Sample01
{
public class BlogConfig : EntityTypeConfiguration<Blog>
{
public BlogConfig()
{
this.Property(x => x.Id).HasColumnName("MyTableKey");
this.Property(x => x.RowVersion).HasColumnType("Timestamp");
}
}
</pre></div><br />
با ارث بري از كلاس EntityTypeConfiguration، ميتوان به ازاي هر كلاس مدل، تنظيمات را جداگانه انجام داد. به اين ترتيب اصل SRP يا Single responsibility principle نقض نخواهد شد. سپس براي استفاده از اين كلاسهاي Config تك مسئوليتي به نحو زير ميتوان اقدام كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new BlogConfig());
</pre></div><br />
<br />
<br />
<br />
<b>نحوه تنظيمات ابتدايي نگاشت كلاسها به بانك اطلاعاتي در EF Code first</b><br />
<br />
الزامي ندارد كه EF Code first حتما با يك بانك اطلاعاتي از نو تهيه شده بر اساس پيش فرضهاي آن كار كند. در اينجا ميتوان از بانكهاي اطلاعاتي موجود نيز استفاده كرد. اما در اين حالت نياز خواهد بود تا مثلا نام جدولي خاص با كلاسي مفروض در برنامه، يا نام فيلدي خاص كه مطابق استانداردهاي نامگذاري خواص در سي شارپ تعريف نشده، با خاصيتي در يك كلاس تطابق داده شوند. براي مثال اينبار تعاريف كلاس Blog را به نحو زير تغيير دهيد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace EF_Sample01.Models
{
[Table("tblBlogs")]
public class Blog
{
[Column("MyTableKey")]
public int Id { set; get; }
[MaxLength(100)]
public string Title { set; get; }
[Required(ErrorMessage = "لطفا نام نويسنده را مشخص نمائيد")]
public string AuthorName { set; get; }
public IList<Post> Posts { set; get; }
[Timestamp]
public byte[] RowVersion { set; get; }
}
}
</pre></div><br />
در اينجا فرض بر اين است كه نام جدول متناظر با كلاس Blog در بانك اطلاعاتي مثلا tblBlogs است و نام خاصيت Id در بانك اطلاعاتي مساوي فيلدي است به نام MyTableKey. چون نام خاصيت را مجددا به Id تغيير دادهايم، ديگر ضرورتي به ذكر ويژگي Key وجود نداشته است. براي تعريف اين دو از ويژگيهاي Table و Column جهت سفارشي سازي نامهاي خواص و كلاس استفاده شده است.<br />
يا اگر در كلاس خود خاصيتي محاسبه شده بر اساس ساير خواص، تعريف شده است و قصد نداريم آنرا به فيلدي در بانك اطلاعاتي نگاشت كنيم، ميتوان از ويژگي NotMapped براي مزين سازي و تعريف آن كمك گرفت.<br />
به علاوه اگر از نام پيش فرض كليد خارجي تشكيل شده خرسند نيستيد ميتوان به كمك ويژگي ForeignKey، نسبت به تعريف مقداري جديد مطابق تعاريف يك بانك اطلاعاتي موجود، اقدام كرد.<br />
همچنين خاصيت ديگري به نام RowVersion در اينجا اضافه شده كه با ويژگي TimeStamp مزين گرديده است. از اين خاصيت ويژه براي بررسي مسايل همزماني ثبت اطلاعات در EF استفاده ميشود. به علاوه بانك اطلاعاتي ميتواند به صورت خودكار آنرا در حين ثبت مقدار دهي كند.<br />
تمام اين تغييرات را به كمك Fluent API نيز ميتوان انجام داد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">modelBuilder.Entity<Blog>().ToTable("tblBlogs");
modelBuilder.Entity<Blog>().Property(x => x.Id).HasColumnName("MyTableKey");
modelBuilder.Entity<Blog>().Property(x => x.RowVersion).HasColumnType("Timestamp");
</pre></div><br />
<br />
<br />
<b>تبديل پروژههاي قديمي EF به كلاسهاي EF Code first به صورت خودكار </b><br />
<br />
روش متداول كار با EF از روز اول آن، مهندسي معكوس خودكار اطلاعات يك بانك اطلاعاتي و تبديل آن به يك فايل EDMX بوده است. هنوز هم ميتوان از اين روش در اينجا نيز بهره جست. براي مثال اگر قصد داريد يك پروژه قديمي را تبديل به نمونه جديد Code first كنيد، يا يك بانك اطلاعاتي موجود را مهندسي معكوس كنيد، بر روي پروژه در Solution explorer كليك راست كرده و گزينه Add|New Item را انتخاب كنيد. سپس از صفحه ظاهر شده، ADO.NET Entity data model را انتخاب كرده و در ادامه گزينه «Generate from database» را انتخاب كنيد. اين روال مرسوم كار با EF Database first است.<br />
پس از اتمام كار به entity data model designer مراجعه كرده و بر روي صفحه كليك راست نمائيد. از منوي ظاهر شده گزينه «Add code generation item» را انتخاب كنيد. سپس در صفحه باز شده از ليست قالبهاي موجود، گزينه «ADO.NET DbContext Generator» را انتخاب نمائيد. اين گزينه به صورت خودكار اطلاعات فايل EDMX قديمي يا موجود شما را تبديل به كلاسهاي مدل Code first معادل به همراه كلاس DbContext معرف آنها خواهد كرد.<br />
<br />
روش ديگري نيز براي انجام اينكار وجود دارد. نياز است افزونهي به نام <a href="http://visualstudiogallery.msdn.microsoft.com/72a60b14-1581-4b9b-89f2-846072eff19d">Entity Framework Power Tools</a> را دريافت كنيد. پس از نصب، از منوي Entity Framework آن گزينهي «Reverse Engineer Code First» را انتخاب نمائيد. در اينجا ميتوان مشخصات اتصال به بانك اطلاعاتي را تعريف و سپس نسبت به توليد خودكار كدهاي مدلها و DbContext مرتبط اقدام كرد.<br />
<br />
<br />
<br />
<b>استراتژيهاي مقدماتي تشكيل بانك اطلاعاتي در EF Code first</b><br />
<br />
اگر مثال اين سري را دنبال كرده باشيد، مشاهده كردهايد كه با اولين بار اجراي برنامه، يك بانك اطلاعاتي پيش فرض نيز توليد خواهد شد. يا اگر تعاريف ويژگيهاي يك فيلد را تغيير داديم، نياز است تا بانك اطلاعاتي را دستي drop كرده و اجازه دهيم تا بانك اطلاعاتي جديدي بر اساس تعاريف جديد مدلها تشكيل شود كه ... هيچكدام از اينها بهينه نيستند.<br />
در اينجا دو استراتژي مقدماتي را در حين آغاز يك برنامه ميتوان تعريف كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">System.Data.Entity.Database.SetInitializer(new DropCreateDatabaseIfModelChanges<Context>());
// or
System.Data.Entity.Database.SetInitializer(new DropCreateDatabaseAlways<Context>());
</pre></div><br />
ميتوان بانك اطلاعاتي را در صورت تغيير اطلاعات يك مدل به صورت خودكار drop كرده و نسبت به ايجاد نمونهاي جديد اقدام كرد (DropCreateDatabaseIfModelChanges)؛ يا در حين آزمايش برنامه هميشه (DropCreateDatabaseAlways) با شروع برنامه، ابتدا بايد بانك اطلاعاتي drop شده و سپس نمونه جديدي توليد گردد.<br />
محل فراخواني اين دستور هم بايد در نقطه آغازين برنامه، پيش از وهله سازي اولين DbContext باشد. مثلا در برنامههاي وب در متد Application_Start فايل global.asax.cs يا در برنامههاي WPF در متد سازنده كلاس App ميتوان بانك اطلاعاتي را آغاز نمود.<br />
البته الزامي به استفاده از كلاسهاي DropCreateDatabaseIfModelChanges يا DropCreateDatabaseAlways وجود ندارد. ميتوان با پياده سازي اينترفيس IDatabaseInitializer از نوع كلاس Context تعريف شده در برنامه، همان عمليات را شبيه سازي كرد يا سفارشي نمود:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">public class MyInitializer : IDatabaseInitializer<Context>
{
public void InitializeDatabase(Context context)
{
if (context.Database.Exists() ||
context.Database.CompatibleWithModel(throwIfNoMetadata: false))
context.Database.Delete();
context.Database.Create();
}
}
</pre></div><br />
سپس براي استفاده از اين كلاس در ابتداي برنامه، خواهيم داشت:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">System.Data.Entity.Database.SetInitializer(new MyInitializer());
</pre></div><br />
<br />
نكته:<br />
اگر از يك بانك اطلاعاتي موجود استفاده ميكنيد (محيط كاري) و نيازي به پيش فرضهاي EF Code first نداريد و همچنين اين بانك اطلاعاتي نيز نبايد drop شود يا تغيير كند، ميتوانيد تمام اين پيش فرضها را با دستور زير غيرفعال كنيد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">Database.SetInitializer<Context>(null);
</pre></div><br />
بديهي است اين دستور نيز بايد پيش از ايجاد اولين وهله از شيء DbContext فراخواني شود.<br />
<br />
<br />
همچنين بايد درنظر داشت كه در آخرين نگارشهاي پايدار EF Code first، اين موارد بهبود يافتهاند و مبحثي تحت عنوان DB Migration ايجاد شده است تا نيازي نباشد هربار بانك اطلاعاتي drop شود و تمام اطلاعات از دست برود. ميتوان صرفا تغييرات كلاسها را به بانك اطلاعاتي اعمال كرد كه به صورت جداگانه، در قسمتي مجزا بررسي خواهد شد. به اين ترتيب ديگر نيازي به drop بانك اطلاعاتي نخواهد بود. به صورت پيش فرض در صورت از دست رفتن اطلاعات يك استثناء را سبب خواهد شد (كه توسط برنامه نويس قابل تنظيم است) و در حالت خودكار يا دستي با تنظيمات ويژه قابل اعمال است.<br />
<br />
<br />
<br />
<b>تنظيم استراتژيهاي آغاز بانك اطلاعاتي در فايل كانفيگ برنامه</b><br />
<br />
الزامي ندارد كه حتما متد Database.SetInitializer را دستي فراخواني كنيم. با اندكي تنظيم فايلهاي app.config و يا web.config نيز ميتوان نوع استراتژي مورد استفاده را تعيين كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code"><appSettings>
<add key="DatabaseInitializerForType MyNamespace.MyDbContextClass, MyAssembly"
value="MyNamespace.MyInitializerClass, MyAssembly" />
</appSettings>
<appSettings>
<add key="DatabaseInitializerForType MyNamespace.MyDbContextClass, MyAssembly"
value="Disabled" />
</appSettings>
</pre></div><br />
يكي از دو حالت فوق بايد در قسمت appSettings فايل كانفيگ برنامه تنظيم شود. حالت دوم براي غيرفعال كردن پروسه آغاز بانك اطلاعاتي و اعمال تغييرات به آن، بكار ميرود.<br />
براي نمونه در مثال جاري، جهت استفاده از كلاس MyInitializer فوق، ميتوان از تنظيم زير نيز استفاده كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code"><appSettings>
<add key="DatabaseInitializerForType EF_Sample01.Context, EF_Sample01"
value="EF_Sample01.MyInitializer, EF_Sample01" />
</appSettings>
</pre></div><br />
<br />
<br />
<b>اجراي كدهاي ويژه در حين تشكيل يك بانك اطلاعاتي جديد</b><br />
<br />
امكان سفارشي سازي اين آغاز كنندههاي پيش فرض نيز وجود دارد. براي مثال:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">public class MyCustomInitializer : DropCreateDatabaseIfModelChanges<Context>
{
protected override void Seed(Context context)
{
context.Blogs.Add(new Blog { AuthorName = "Vahid", Title = ".NET Tips" });
context.Database.ExecuteSqlCommand("CREATE INDEX IX_title ON tblBlogs (title)");
base.Seed(context);
}
}
</pre></div><br />
در اينجا با ارث بري از كلاس DropCreateDatabaseIfModelChanges يك آغاز كننده سفارشي را تعريف كردهايم. سپس با تحريف متد Seed آن ميتوان در حين آغاز يك بانك اطلاعاتي، تعدادي ركورد پيش فرض را به آن افزود. كار ذخيره سازي نهايي در متد base.Seed انجام ميشود.<br />
براي استفاده از آن اينبار در حين فراخواني متد System.Data.Entity.Database.SetInitializer، از كلاس MyCustomInitializer استفاده خواهيم كرد.<br />
و يا توسط متد context.Database.ExecuteSqlCommand ميتوان دستورات SQL را مستقيما در اينجا اجرا كرد. عموما دستوراتي در اينجا مدنظر هستند كه توسط ORMها پشتيباني نميشوند. براي مثال تغيير collation يك ستون يا افزودن يك ايندكس و مواردي از اين دست.<br />
<br />
<br />
<b>سطح دسترسي مورد نياز جهت فراخواني متد Database.SetInitializer</b><br />
<br />
استفاده از متدهاي آغاز كننده بانك اطلاعاتي نياز به سطح دسترسي بر روي بانك اطلاعاتي master را در SQL Server دارند (زيرا با انجام كوئري بر روي اين بانك اطلاعاتي مشخص ميشود، آيا بانك اطلاعاتي مورد نظر پيشتر تعريف شده است يا خير). البته اين مورد حين كار با SQL Server CE شايد اهميتي نداشته باشد. بنابراين اگر كاربري كه با آن به بانك اطلاعاتي متصل ميشويم سطح دسترسي پاييني دارد نياز است Persist Security Info=True را به رشته اتصالي اضافه كرد. البته اين مورد را پس از انجام تغييرات بر روي بانك اطلاعاتي جهت امنيت بيشتر حذف كنيد (يا به عبارتي در محيط كاري Persist Security Info=False بايد باشد).<br />
<br />
<div align="left" dir="ltr"><pre language="xml" name="code">Server=(local);Database=yourDatabase;User ID=yourDBUser;Password=yourDBPassword;Trusted_Connection=False;Persist Security Info=True
</pre></div><br />
<br />
<b>تعيين Schema و كاربر فراخوان دستورات SQL</b><br />
<br />
در EF Code first به صورت پيش فرض همه چيز بر مبناي كاربري با دسترسي مديريتي يا dbo schema در اس كيوال سرور تنظيم شده است. اما اگر كاربر خاصي براي كار با ديتابيس تعريف گردد كه در هاستهاي اشتراكي بسيار مرسوم است، ديگر از دسترسي مديريتي dbo خبري نخواهد بود. اينبار نام جداول ما بجاي dbo.tableName مثلا someUser.tableName ميباشند و عدم دقت به اين نكته، اجراي برنامه را غيرممكن ميسازد. <br />
براي تغيير و تعيين صريح كاربر متصل شده به بانك اطلاعاتي اگر از متاديتا استفاده ميكنيد، روش زير بايد بكارگرفته شود:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">[Table("tblBlogs", Schema="someUser")]
public class Blog
</pre></div><br />
و يا در حالت بكارگيري Fluent API به نحو زير قابل تنظيم است:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">modelBuilder.Entity<Blog>().ToTable("tblBlogs", schemaName:"someUser");
</pre></div><br />
<br />
<br />
<br />
<br />
<br />
</div>وحيد نصيريhttp://www.blogger.com/profile/04454130327051686471noreply@blogger.comtag:blogger.com,1999:blog-7815821915651048578.post-7730242175393110642012-05-03T10:07:00.000+04:302012-05-03T10:07:29.402+04:30EF Code First #1<div dir="rtl" style="text-align: right;" trbidi="on"><br />
در ادامه بحث ASP.NET MVC ميشود به ابزاري به نام MVC Scaffolding اشاره كرد. كار اين ابزار كه توسط يكي از اعضاي تيم ASP.NET MVC به نام <a href="http://blog.stevensanderson.com/">استيو اندرسون</a> تهيه شده، توليد كدهاي اوليه يك برنامه كامل ASP.NET MVC از روي مدلهاي شما ميباشد. حجم بالايي از كدهاي تكراري آغازين برنامه را ميشود توسط اين ابزار توليد و بعد سفارشي كرد. MVC Scaffolding حتي قابليت توليد كد بر اساس الگوي Repository و يا نوشتن Unit tests مرتبط را نيز دارد. بديهي است اين ابزار جاي يك برنامه نويس را نميتواند پر كند اما كدهاي آغازين يك سري كارهاي متداول و تكراري را به خوبي ميتواند پياده سازي و ارائه دهد. زير ساخت اين ابزار، علاوه بر ASP.NET MVC، آشنايي با Entity framework code first است.<br />
در طي سري ASP.NET MVC كه در اين سايت تا به اينجا مطالعه كرديد من به شدت سعي كردم از ابزارگرايي پرهيز كنم. چون شخصي كه نميداند مسيريابي چيست، اطلاعات چگونه به يك كنترلر منتقل يا به يك View ارسال ميشوند، قراردادهاي پيش فرض فريم ورك چيست يا زير ساخت امنيتي يا فيلترهاي ASP.NET MVC كدامند، چطور ميتواند از ابزار پيشرفته Code generator ايي استفاده كند، يا حتي در ادامه كدهاي توليدي آنرا سفارشي سازي كند؟ بنابراين براي استفاده از اين ابزار و درك كدهاي توليدي آن، نياز به يك پيشنياز ديگر هم وجود دارد: «Entity framework code first»<br />
امسال دو كتاب خوب در اين زمينه منتشر شدهاند به نامهاي:<br />
<a href="http://www.amazon.com/Programming-Entity-Framework-Julia-Lerman/dp/1449312969">Programming Entity Framework: DbContext</a>, ISBN: 978-1-449-31296-1<br />
<a href="http://www.amazon.com/Programming-Entity-Framework-Code-First/dp/1449312942">Programming Entity Framework: Code First</a>, ISBN: 978-1-449-31294-7<br />
كه هر دو به صورت اختصاصي به مقوله EF Code first پرداختهاند.<br />
<br />
<br />
در طي روزهاي بعدي EF Code first را با هم مرور خواهيم كرد و البته اين مرور مستقل است از نوع فناوري ميزبان آن؛ ميخواهد يك برنامه كنسول باشد يا WPF يا يك سرويس ويندوز NT و يا ... يك برنامه وب.<br />
البته از ديدگاه مايكروسافت، M در MVC به معناي EF Code first است. به همين جهت MVC3 به صورت پيش فرض ارجاعي را به اسمبليهاي آن دارد و يا حتي به روز رساني كه براي آن ارائه داده نيز در جهت تكميل همين بحث است.<br />
<br />
<br />
<b>مروري سريع بر تاريخچه Entity framework code first</b><br />
<br />
ويژوال استوديو 2010 و دات نت 4، به همراه EF 4.0 ارائه شدند. با اين نگارش امكان استفاده از حالتهاي طراحي database first و model first مهيا است. پس از آن، به روز رسانيهاي EF خارج از نوبت و به صورت منظم، هر از چندگاهي ارائه ميشوند و در زمان نگارش اين مطلب، آخرين نگارش پايدار در دسترس آن 4.3.1 ميباشد. از زمان EF 4.1 به بعد، نوع جديدي از مدل سازي به نام Code first به اين فريم ورك اضافه شد و در نگارشهاي بعدي آن، مباحث DB migration جهت ساده سازي تطابق اطلاعات مدلها با بانك اطلاعاتي، اضافه گرديدند. در روش Code first، كار با طراحي كلاسها كه در اينجا مدل دادهها ناميده ميشوند، شروع گرديده و سپس بر اساس اين اطلاعات، توليد يك بانك اطلاعاتي جديد و يا استفاده از نمونهاي موجود ميسر ميگردد.<br />
پيشتر در روش database first ابتدا يك بانك اطلاعاتي موجود، مهندسي معكوس ميشد و از روي آن فايل XML ايي با پسوند EDMX توليد ميگشت. سپس به كمك entity data model designer ويژوال استوديو، اين فايل نمايش داده شده و يا امكان اعمال تغييرات بر روي آن ميسر ميشد. همچنين در روش ديگري به نام model first نيز كار از entity data model designer جهت طراحي موجوديتها آغاز ميگشت.<br />
اما با روش Code first ديگر در ابتداي امر مدل فيزيكي و يك بانك اطلاعاتي وجود خارجي ندارد. در اينجا EF تعاريف كلاسهاي شما را بررسي كرده و بر اساس آن، اطلاعات نگاشتهاي خواص كلاسها به جداول و فيلدهاي بانك اطلاعاتي را تشكيل ميدهد. البته عموما تعاريف ساده كلاسها بر اين منظور كافي نيستند. به همين جهت از يك سري متاديتا به نام ويژگيها يا اصطلاحا data annotations مهيا در فضاي نام System.ComponentModel.DataAnnotations براي افزودن اطلاعات لازم مانند نام فيلدها، جداول و يا تعاريف روابط ويژه نيز استفاده ميگردد. به علاوه در روش Code first يك API جديد به نام Fluent API نيز جهت تعاريف اين ويژگيها و روابط، با كدنويسي مستقيم نيز درنظر گرفته شده است. نهايتا از اين اطلاعات جهت نگاشت كلاسها به بانك اطلاعاتي و يا براي توليد ساختار يك بانك اطلاعاتي خالي جديد نيز ميتوان كمك گرفت.<br />
<br />
<br />
<br />
<b>مزاياي EF Code first</b><br />
<br />
- مطلوب برنامه نويسها! : برنامه نويسهايي كه مدتي تجربه كار با ابزارهاي طراح را داشته باشند به خوبي ميدانند اين نوع ابزارها عموما demo-ware هستند. چندجا كليك ميكنيد، دوبار Next، سه بار OK و ... به نظر ميرسد كار تمام شده. اما واقعيت اين است كه عمري را بايد صرف نگهداري و يا پياده سازي جزئياتي كرد كه انجام آنها با كدنويسي مستقيم بسيار سريعتر، سادهتر و با كنترل بيشتري قابل انجام است.<br />
- سرعت: براي كار با EF Code first نيازي نيست در ابتداي كار بانك اطلاعاتي خاصي وجود داشته باشد. كلاسهاي خود را طراحي و شروع به كدنويسي كنيد.<br />
- سادگي: در اينجا ديگر از فايلهاي EDMX خبري نيست و نيازي نيست مرتبا آنها را به روز كرده يا نگهداري كرد. تمام كارها را با كدنويسي و كنترل بيشتري ميتوان انجام داد. به علاوه كنترل كاملي بر روي كد نهايي تهيه شده نيز وجود دارد و توسط ابزارهاي توليد كد، ايجاد نميشوند.<br />
- طراحي بهتر بانك اطلاعاتي نهايي: اگر طرح دقيقي از مدلهاي برنامه داشته باشيم، ميتوان آنها را به المانهاي كوچك و مشخصي، تقسيم و refactor كرد. همين مساله در نهايت مباحث database normalization را به نحوي مطلوب و با سرعت بيشتري ميسر ميكند.<br />
- امكان استفاده مجدد از طراحي كلاسهاي انجام شده در ساير ORMهاي ديگر. چون طراحي مدلهاي برنامه به بانك اطلاعاتي خاصي گره نميخورند و همچنين الزاما هم قرار نيست جزئيات كاري EF در آنها لحاظ شود، اين كلاسها در صورت نياز در ساير پروژهها نيز به سادگي قابل استفاده هستند.<br />
- رديابي سادهتر تغييرات: روش اصولي كار با پروژههاي نرم افزاري همواره شامل استفاده از يك ابزار سورس كنترل مانند SVN، Git، مركوريال و امثال آن است. به اين ترتيب رديابي تغييرات انجام شده به سادگي توسط اين ابزارها ميسر ميشوند.<br />
- سادهتر شدن طراحيهاي پيچيدهتر: براي مثال پياده سازي ارث بري، ايجاد كلاسهاي خود ارجاع دهنده و امثال آن با كدنويسي سادهتر است.<br />
<br />
<br />
<b>دريافت آخرين نگارش EF</b><br />
<br />
<br />
براي دريافت و نصب آخرين نگارش <a href="http://msdn.microsoft.com/en-us/data/ef">EF</a> نياز است از <a href="http://www.dotnettips.info/search/label/NuGet">NuGet</a> استفاده شود و اين مزايا را به همراه دارد:<br />
به كمك NuGet امكان با خبر شدن از به روز رساني جديد صورت گرفته به صورت خودكار درنظر گرفته شده است و همچنين كار دريافت بستههاي مرتبط و به روز رساني ارجاعات نيز در اين حالت خودكار است. به علاوه توسط NuGet امكان دسترسي به كتابخانههايي كه مثلا در گوگلكد قرار دارند و به صورت معمول امكان دريافت آنها براي ما ميسر نيست، نيز بدون مشكل فراهم است (براي نمونه ELMAH، كه اصل آن از گوگلكد قابل دريافت است؛ اما بسته نيوگت آن نيز در دسترس ميباشد).<br />
پس از نصب NuGet، تنها كافي است بر روي گره References در Solution explorer ويژوال استوديو، كليك راست كرده و به كمك NuGet آخرين نگارش EF را نصب كرد. در گالري آنلاين آن، عموما EF اولين گزينه است (به علت تعداد بالاي دريافت آن).<br />
حين استفاده از NuGet جهت نصب Ef، ابتدا ارجاعاتي به اسمبليهاي زير به برنامه اضافه خواهند شد:<br />
System.ComponentModel.DataAnnotations.dll<br />
System.Data.Entity.dll<br />
EntityFramework.dll<br />
بديهي است بدون استفاده از NuGet، تمام اين كارها را بايد دستي انجام داد. <br />
سپس در پوشهاي به نام packages، فايلهاي مرتبط با EF قرار خواهند گرفت كه شامل اسمبلي آن به همراه ابزارهاي DB Migration است. همچنين فايل packages.config كه شامل تعاريف اسمبليهاي نصب شده است به پروژه اضافه ميشود. NuGet به كمك اين فايل و شماره نگارش درج شده در آن، شما را از به روز رسانيهاي بعدي مطلع خواهد ساخت:<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code"><?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="EntityFramework" version="4.3.1" />
</packages>
</pre></div><br />
همچنين اگر به فايل app.config يا web.config برنامه نيز مراجعه كنيد، يك سري تنظيمات ابتدايي اتصال به بانك اطلاعاتي در آن ذكر شده است:<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code"><?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=4.3.1.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
</configSections>
<entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework">
<parameters>
<parameter value="Data Source=(localdb)\v11.0; Integrated Security=True; MultipleActiveResultSets=True" />
</parameters>
</defaultConnectionFactory>
</entityFramework>
</configuration>
</pre></div><br />
همانطور كه ملاحظه ميكنيد بانك اطلاعاتي پيش فرضي كه در اينجا ذكر شده است، <a href="http://blogs.msdn.com/b/jerrynixon/archive/2012/02/26/sql-express-v-localdb-v-sql-compact-edition.aspx">LocalDB</a> ميباشد. اين بانك اطلاعاتي را از <a href="http://www.microsoft.com/download/en/details.aspx?id=29062">اين آدرس</a> نيز ميتوانيد دريافت كنيد.<br />
<br />
البته ضرورتي هم به استفاده از آن نيست و از ساير نگارشهاي SQL Server نيز ميتوان استفاده كرد ولي خوب ... مزيت استفاده از آن براي كاربر نهايي اين است كه «نيازي به يك مهندس براي نصب، راه اندازي و نگهداري ندارد». تنها مشكل آن اين است كه از ويندوز XP پشتيباني نميكند. البته SQL Server CE 4.0 اين محدوديت را ندارد.<br />
ضمن اينكه بايد درنظر داشت EF به فناوري ميزبان خاصي گره نخورده است و مثالهايي كه در اينجا بررسي ميشوند صرفا تعدادي برنامه كنسول معمولي هستند و نكات عنوان شده در آنها در تمام فناوريهاي ميزبان موجود به يك نحو كاربرد دارند.<br />
<br />
<br />
<b>قراردادهاي پيش فرض EF Code first</b><br />
<br />
عنوان شد كه اطلاعات كلاسهاي ساده تشكيل دهنده مدلهاي برنامه، براي تعريف جداول و فيلدهاي يك بانك اطلاعات و همچنين مشخص سازي روابط بين آنها كافي نيستند و مرسوم است براي پر كردن اين خلاء از يك سري متاديتا و يا Fluent API مهيا نيز استفاده گردد. اما در EF Code first يك سري قرار داد توكار نيز وجود دارند كه مطلع بودن از آنها سبب خواهد شد تا حجم كدنويسي و تنظيمات جانبي اين فريم ورك به حداقل برسند. براي نمونه مدلهاي معروف بلاگ و مطالب آنرا درنظر بگيريد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">namespace EF_Sample01.Models
{
public class Post
{
public int Id { set; get; }
public string Title { set; get; }
public string Content { set; get; }
public virtual Blog Blog { set; get; }
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Collections.Generic;
namespace EF_Sample01.Models
{
public class Blog
{
public int Id { set; get; }
public string Title { set; get; }
public string AuthorName { set; get; }
public IList<Post> Posts { set; get; }
}
}
</pre></div><br />
<br />
يكي از قراردادهاي EF Code first اين است كه كلاسهاي مدل شما را جهت يافتن خاصيتي به نام Id يا ClassId مانند BlogId، جستجو ميكند و از آن به عنوان primary key و فيلد identity جدول بانك اطلاعاتي استفاده خواهد كرد.<br />
همچنين در كلاس Blog، خاصيت ليستي از Posts و در كلاس Post خاصيت virtual ايي به نام Blog وجود دارند. به اين ترتيب روابط بين دو كلاس و ايجاد كليد خارجي متناظر با آنرا به صورت خودكار انجام خواهد داد.<br />
نهايتا از اين اطلاعات جهت تشكيل database schema يا ساختار بانك اطلاعاتي استفاده ميگردد. <br />
اگر به فضاهاي نام دو كلاس فوق دقت كرده باشيد، به كلمه Models ختم شدهاند. به اين معنا كه در پوشهاي به همين نام در پروژه جاري قرار دارند. يا مرسوم است كلاسهاي مدل را در يك پروژه class library مجزا به نام DomainClasses نيز قرار دهند. اين پروژه نيازي به ارجاعات اسمبليهاي EF ندارد و تنها به اسمبلي System.ComponentModel.DataAnnotations.dll نياز خواهد داشت.<br />
<br />
<br />
<b>EF Code first چگونه كلاسهاي مورد نظر را انتخاب ميكند؟</b><br />
<br />
ممكن است دهها و صدها كلاس در يك پروژه وجود داشته باشند. EF Code first چگونه از بين اين كلاسها تشخيص خواهد داد كه بايد از كداميك استفاده كند؟ اينجا است كه مفهوم جديدي به نام DbContext معرفي شده است. براي تعريف آن يك كلاس ديگر را به پروژه براي مثال به نام Context اضافه كنيد. همچنين مرسوم است كه اين كلاس را در پروژه class library ديگري به نام DataLayer اضافه ميكنند. اين پروژه نياز به ارجاعي به اسمبليهاي EF خواهد داشت. در ادامه كلاس جديد اضافه شده بايد از كلاس DbContext مشتق شود:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Data.Entity;
using EF_Sample01.Models;
namespace EF_Sample01
{
public class Context : DbContext
{
public DbSet<Blog> Blogs { set; get; }
public DbSet<Post> Posts { set; get; }
}
}
</pre></div><br />
سپس در اينجا به كمك نوع جنريكي به نام DbSet، كلاسهاي دومين برنامه را معرفي ميكنيم. به اين ترتيب، EF Code first ابتدا به دنبال كلاسي مشتق شده از DbContext خواهد گشت. پس از يافتن آن، خواصي از نوع DbSet را بررسي كرده و نوعهاي متناظر با آنرا به عنوان كلاسهاي دومين درنظر ميگيرد و از ساير كلاسهاي برنامه صرفنظر خواهد كرد. اين كل كاري است كه بايد انجام شود.<br />
اگر دقت كرده باشيد، نام كلاسهاي موجوديتها، مفرد هستند و نام خواص تعريف شده به كمك DbSet، جمع ميباشند كه نهايتا متناظر خواهند بود با نام جداول بانك اطلاعاتي تشكيل شده. <br />
<br />
<br />
<b>تشكيل خودكار بانك اطلاعاتي و افزودن اطلاعات به جداول</b><br />
<br />
تا اينجا بدون تهيه يك بانك اطلاعاتي نيز ميتوان از كلاس Context تهيه شده استفاده كرد و كار كدنويسي را آغاز نمود. بديهي است جهت اجراي نهايي كدها، نياز به يك بانك اطلاعاتي خواهد بود. اگر تنظيمات پيش فرض فايل كانفيگ برنامه را تغيير ندهيم، از همان defaultConnectionFactory ياده شده استفاده خواهد كرد. در اين حالت نام بانك اطلاعاتي به صورت خودكار تنظيم شده و مساوي «EF_Sample01.Context» خواهد بود. <br />
براي سفارشي سازي آن نياز است فايل app.config يا web.config برنامه را اندكي ويرايش نمود:<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code"><?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
...
</configSections>
<connectionStrings>
<clear/>
<add name="Context"
connectionString="Data Source=(local);Initial Catalog=testdb2012;Integrated Security = true"
providerName="System.Data.SqlClient"
/>
</connectionStrings>
...
</configuration>
</pre></div><br />
در اينجا به بانك اطلاعاتي testdb2012 در وهله پيش فرض SQL Server نصب شده، اشاره شده است. فقط بايد دقت داشت كه تگ configSections بايد در ابتداي فايل قرار گيرد و مابقي تنظيمات پس از آن.<br />
يا اگر علاقمند باشيد كه از SQL Server CE استفاده كنيد، تنظيمات رشته اتصالي را به نحو زير مقدار دهي نمائيد:<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code"><connectionStrings>
<add name="MyContextName"
connectionString="Data Source=|DataDirectory|\Store.sdf"
providerName="System.Data.SqlServerCe.4.0" />
</connectionStrings>
</pre></div><br />
در هر دو حالت، name بايد به نام كلاس مشتق شده از DbContext اشاره كند كه در مثال جاري همان Context است.<br />
يا اگر علاقمند بوديد كه اين قرارداد توكار را تغيير داده و نام رشته اتصالي را با كدنويسي تعيين كنيد، ميتوان به نحو زير عمل كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">public class Context : DbContext
{
public Context()
: base("ConnectionStringName")
{
}
</pre></div><br />
<br />
البته ضرورتي ندارد اين بانك اطلاعاتي از پيش موجود باشد. در اولين بار اجراي كدهاي زير، به صورت خودكار بانك اطلاعاتي و جداول Blogs و Posts و روابط بين آنها تشكيل ميگردد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using EF_Sample01.Models;
namespace EF_Sample01
{
class Program
{
static void Main(string[] args)
{
using (var db = new Context())
{
db.Blogs.Add(new Blog { AuthorName = "Vahid", Title = ".NET Tips" });
db.SaveChanges();
}
}
}
}
</pre></div><br />
<div align="center"><img src="https://dotnettipsrepository.svn.codeplex.com/svn/Trunk/Images/EF/ef01.PNG" /></div><br />
در اين تصوير چند نكته حائز اهميت هستند:<br />
الف) نام پيش فرض بانك اطلاعاتي كه به آن اشاره شد (اگر تنظيمات رشته اتصالي قيد نگردد).<br />
ب) تشكيل خودكار primary key از روي خواصي به نام Id<br />
ج) تشكيل خودكار روابط بين جداول و ايجاد كليد خارجي (به كمك خاصيت virtual تعريف شده)<br />
د) تشكيل جدول سيستمي به نام dbo.__MigrationHistory كه از آن براي نگهداري سابقه به روز رسانيهاي ساختار جداول كمك گرفته خواهد شد.<br />
ه) نوع و طول فيلدهاي متني، nvarchar از نوع max است.<br />
<br />
تمام اينها بر اساس پيش فرضها و قراردادهاي توكار EF Code first انجام شده است.<br />
<br />
در كدهاي تعريف شده نيز، ابتدا يك وهله از شيء Context ايجاد شده و سپس به كمك آن ميتوان به جدول Blogs اطلاعاتي را افزود و در آخر ذخيره نمود. استفاده از using هم دراينجا نبايد فراموش شود، زيرا اگر استثنايي در اين بين رخ دهد، كار پاكسازي منابع و بستن اتصال گشوده شده به بانك اطلاعاتي به صورت خودكار انجام خواهد شد.<br />
در ادامه اگر بخواهيم مطلبي را به Blog ثبت شده اضافه كنيم، خواهيم داشت:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using EF_Sample01.Models;
namespace EF_Sample01
{
class Program
{
static void Main(string[] args)
{
//addBlog();
addPost();
}
private static void addPost()
{
using (var db = new Context())
{
var blog = db.Blogs.Find(1);
db.Posts.Add(new Post
{
Blog = blog,
Content = "data",
Title = "EF"
});
db.SaveChanges();
}
}
private static void addBlog()
{
using (var db = new Context())
{
db.Blogs.Add(new Blog { AuthorName = "Vahid", Title = ".NET Tips" });
db.SaveChanges();
}
}
}
}
</pre></div><br />
متد db.Blogs.Find، بر اساس primary key بلاگ ثبت شده، يك وهله از آنرا يافته و سپس از آن جهت تشكيل شيء Post و افزودن آن به جدول Posts استفاده ميشود. متد Find ابتدا Contxet جاري را جهت يافتن شيءايي با id مساوي يك جستجو ميكند (اصطلاحا به آن first level cache هم گفته ميشود). اگر موفق به يافتن آن شد، بدون صدور كوئري اضافهاي به بانك اطلاعاتي از اطلاعات همان شيء استفاده خواهد كرد. در غيراينصورت نياز خواهد داشت تا ابتدا كوئري لازم را به بانك اطلاعاتي ارسال كرده و اطلاعات شيء Blog متناظر با id=1 را دريافت كند. همچنين اگر نياز داشتيم تا تنها با سطح اول كش كار كنيم، در EF Code first ميتوان از خاصيتي به نام Local نيز استفاده كرد. براي مثال خاصيت db.Blogs.Local بيانگر اطلاعات موجود در سطح اول كش ميباشد.<br />
نهايتا كوئري Insert توليد شده توسط آن به شكل زير خواهد بود (لاگ شده توسط برنامه <a href="http://www.dotnettips.info/2010/08/sql-wcf-ria-services.html">SQL Server Profiler</a>):<br />
<br />
<div align="left" dir="ltr"><pre language="Sql" name="code">exec sp_executesql N'insert [dbo].[Posts]([Title], [Content], [Blog_Id])
values (@0, @1, @2)
select [Id]
from [dbo].[Posts]
where @@ROWCOUNT > 0 and [Id] = scope_identity()',
N'@0 nvarchar(max) ,@1 nvarchar(max) ,@2 int',
@0=N'EF',
@1=N'data',
@2=1
</pre></div><br />
<br />
اين نوع كوئرهاي پارامتري چندين مزيت مهم را به همراه دارند:<br />
الف) به صورت خودكار تشكيل ميشوند. تمام كوئريهاي پشت صحنه EF پارامتري هستند و نيازي نيست مرتبا مزاياي اين امر را گوشزد كرد و باز هم عدهاي با جمع زدن رشتهها نسبت به نوشتن كوئريهاي نا امن SQL اقدام كنند.<br />
ب) كوئرهاي پارامتري در مقابل حملات تزريق اس كيوال مقاوم هستند.<br />
ج) SQL Server با كوئريهاي پارامتري همانند رويههاي ذخيره شده رفتار ميكند. يعني query execution plan محاسبه شده آنها را كش خواهد كرد. همين امر سبب بالا رفتن كارآيي برنامه در فراخوانيهاي بعدي ميگردد. الگوي كلي مشخص است. فقط پارامترهاي آن تغيير ميكنند.<br />
د) مصرف حافظه SQL Server كاهش مييابد. چون SQL Server مجبور نيست به ازاي هر كوئري اصطلاحا Ad Hoc رسيده يكبار execution plan متفاوت آنها را محاسبه و سپس كش كند. اين مورد مشكل مهم تمام برنامههايي است كه از كوئريهاي پارامتري استفاده نميكنند؛ تا حدي كه گاهي تصور ميكنند شايد SQL Server دچار نشتي حافظه شده، اما مشكل جاي ديگري است.<br />
<br />
<br />
<b>مشكل! ساختار بانك اطلاعاتي تشكيل شده مطلوب كار ما نيست.</b><br />
<br />
تا همينجا با حداقل كدنويسي و تنظيمات مرتبط با آن، پيشرفت خوبي داشتهايم؛ اما نتيجه حاصل آنچنان مطلوب نيست و نياز به سفارشي سازي دارد. براي مثال طول فيلدها را نياز داريم به مقدار ديگري تنظيم كنيم، تعدادي از فيلدها بايد به صورت not null تعريف شوند يا نام پيش فرض بانك اطلاعاتي بايد مشخص گردد و مواردي از اين دست. با اين موارد در قسمتهاي بعدي بيشتر آشنا خواهيم شد.</div>وحيد نصيريhttp://www.blogger.com/profile/04454130327051686471noreply@blogger.comtag:blogger.com,1999:blog-7815821915651048578.post-55955238304057467652012-05-01T09:27:00.001+04:302012-05-01T09:31:19.769+04:30RadioButtonList در ASP.NET MVC<div dir="rtl" style="text-align: right;" trbidi="on"><br />
براي تهيه يك RadioButtonList نيز ميتوان از همان نكتهي <a href="http://www.dotnettips.info/2012/04/checkboxlist-aspnet-mvc.html">CheckBoxList</a> استفاده كرد: نام عناصر radio button اضافه شده به صفحه را يكسان وارد ميكنيم. به اين ترتيب يك گروه تشكيل خواهد شد و زمانيكه اطلاعات اين عناصر به سرور ارسال ميشود، اينبار بجاي يك آرايه، تنها مقدار كنترل انتخاب شده، ارسال ميگردد. يك مثال:<br />
يك پروژه جديد و خالي ASP.NET MVC را آغاز كنيد. سپس كنترلر Home و View خالي Index را نيز ايجاد نمائيد. محتويات اين دو را به نحو زير تغيير دهيد:<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code">@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<fieldset>
<legend>HandleForm1 (Normal)</legend>
@using (Html.BeginForm(actionName: "HandleForm1", controllerName: "Home"))
{
@:your favorite tech: <br />
@Html.RadioButton(name: "tech", value: ".NET", isChecked: true) @:DOTNET <br />
@Html.RadioButton(name: "tech", value: "JAVA", isChecked: false) @:JAVA <br />
@Html.RadioButton(name: "tech", value: "PHP", isChecked: false) @:PHP <br />
<input type="submit" value="Submit" />
}
</fieldset>
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Collections.Generic;
using System.Web.Mvc;
namespace MvcApplication23.Controllers
{
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index()
{
return View();
}
[HttpPost]
public ActionResult HandleForm1(string tech)
{
return RedirectToAction("Index");
}
}
}
</pre></div><br />
در اينجا سه RadioButton با نامي يكسان در صفحه اضافه شدهاند. سپس داخل متد HandleForm1 يك breakpoint قرار دهيد. اكنون برنامه را اجرا كنيد و فرم را به سرور ارسال نمائيد. پارامتر tech با value عنصر انتخابي مقدار دهي خواهد شد.<br />
<br />
<b>تهيه يك RadioButtonList عمومي</b><br />
<br />
اطلاعات فوق را ميتوان تبديل به يك HtmlHelper با قابليت استفاده مجدد نيز نمود:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">@helper RadioButtonList(string groupName, IEnumerable<System.Web.Mvc.SelectListItem> items)
{
<div class="RadioButtonList">
@foreach (var item in items)
{
@item.Text
<input type="radio" name="@groupName"
value="@item.Value"
@if (item.Selected) { <text>checked="checked"</text> }
/>
<br />
}
</div>
}
</pre></div><br />
براي مثال يك فايل را در مسير app_code\Helpers.cshtml ايجاد كرده و اطلاعات فوق را به آن اضافه نمائيد.<br />
اينبار براي استفاده از آن خواهيم داشت:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Collections.Generic;
using System.Web.Mvc;
namespace MvcApplication23.Controllers
{
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index()
{
ViewBag.Tags = new[]
{
new SelectListItem { Text = ".NET", Value = "Val1", Selected = true },
new SelectListItem { Text = "JAVA", Value = "Val2", Selected = false },
new SelectListItem { Text = "PHP", Value = "Val3", Selected = false }
};
return View();
}
[HttpPost]
public ActionResult HandleForm2(string preferredTechnology)
{
return RedirectToAction("Index");
}
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="XML" name="code">@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<fieldset>
<legend>HandleForm2 (Helper)</legend>
@using (Html.BeginForm(actionName: "HandleForm2", controllerName: "Home"))
{
@:your favorite tech: <br />
@Helpers.RadioButtonList("preferredTechnology", (SelectListItem[])ViewBag.Tags)
<input type="submit" value="Submit" />
}
</fieldset>
</pre></div><br />
متد سفارشي تهيه شده، يك آرايه از SelectListItem ها را دريافت كرده و به صورت خودكار تبديل به RadioButtonList ميكند. بر اساس نام آن ميتوان به مقدار انتخاب شده ارسالي به سرور در كنترلر مرتبط، دسترسي يافت.<br />
<br />
<br />
<b>تهيه يك Templated helper سفارشي</b><br />
<br />
در عمل زمانيكه با مدلها كار ميكنيم و اطلاعات برنامه قرار است Strongly typed باشند، مرسوم است ليستي از انتخابها را به صورت يك enum تعريف كنند. براي مثال مدل زير را به برنامه اضافه كنيد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.ComponentModel.DataAnnotations;
namespace MvcApplication23.Models
{
public enum Gender
{
[Display(Name = "مرد")]
Male,
[Display(Name = "زن")]
Female,
}
public class User
{
[ScaffoldColumn(false)]
public int Id { set; get; }
[Display(Name = "نام")]
public string Name { set; get; }
[Display(Name = "جنسيت")]
[UIHint("EnumRadioButtonList")]
public Gender Gender { set; get; }
}
}
</pre></div><br />
قصد داريم يك Templated helper سفارشي را به نام EnumRadioButtonList، ايجاد كنيم تا در زمان فراخواني متد Html.EditorForModel، به صورت خودكار enum تعريف شده را به صورت يك RadioButtonList نمايش دهد.<br />
براي اين منظور فايل جديد Views\Shared\EditorTemplates\EnumRadioButtonList.cshtml را به برنامه اضافه كنيد. محتواي آنرا به نحو زير تغيير دهيد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">@using System.ComponentModel.DataAnnotations
@using System.Globalization
@model Enum
@{
Func<Enum, string> getDescription = enumItem =>
{
var type = enumItem.GetType();
var memInfo = type.GetMember(enumItem.ToString());
if (memInfo != null && memInfo.Any())
{
var attrs = memInfo[0].GetCustomAttributes(typeof(DisplayAttribute), false);
if (attrs != null && attrs.Any())
return ((DisplayAttribute)attrs[0]).GetName();
}
return enumItem.ToString();
};
var listItems = Enum.GetValues(Model.GetType())
.OfType<Enum>()
.Select(enumItem =>
new SelectListItem()
{
Text = getDescription(enumItem),
Value = enumItem.ToString(),
Selected = enumItem.Equals(Model)
});
string prefix = ViewData.TemplateInfo.HtmlFieldPrefix;
ViewData.TemplateInfo.HtmlFieldPrefix = string.Empty;
int index = 0;
foreach (var li in listItems)
{
string fieldName = string.Format(CultureInfo.InvariantCulture, "{0}_{1}", prefix, index++);
<div class="editor-radio">
@Html.RadioButton(prefix, li.Value, li.Selected, new { @id = fieldName })
@Html.Label(fieldName, li.Text)
</div>
}
ViewData.TemplateInfo.HtmlFieldPrefix = prefix;
}
</pre></div><br />
در اينجا به كمك Reflection به اطلاعات enum دريافتي دسترسي خواهيم داشت. بر اين اساس ميتوان نام عناصر آنرا يافت و تبديل به يك RadioButtonList كرد. البته كار به همينجا ختم نميشود. در اين بين بايد دقت داشت كه ممكن است از ويژگي Display (مانند مدل نمونه فوق) بر روي تك تك عناصر يك enum نيز استفاده شود. به همين جهت اين مورد نيز بايد پردازش گردد.<br />
نهايتا براي استفاده از اين Templated helper سفارشي، كنترلر و View برنامه را به نحو زير ميتوان تغيير داد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Collections.Generic;
using System.Web.Mvc;
using MvcApplication23.Models;
namespace MvcApplication23.Controllers
{
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index()
{
var user = new User { Id = 1, Name = "name 1", Gender = Gender.Male };
return View(user);
}
[HttpPost]
public ActionResult HandleForm3(User user)
{
return RedirectToAction("Index");
}
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="XML" name="code">@model MvcApplication23.Models.User
@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<fieldset>
<legend>HandleForm3 (EditorForModel)</legend>
@using (Html.BeginForm(actionName: "HandleForm3", controllerName: "Home"))
{
@Html.EditorForModel()
<input type="submit" value="Submit" />
}
</fieldset>
</pre></div><br />
براي استفاده از يك templated helper سفارشي چندين روش وجود دارد:<br />
الف) همانند مثال فوق از ويژگي UIHint استفاده شود.<br />
ب) نام فايل را به enum.cshtml تغيير دهيم. به اين ترتيب از اين پس كليه enumها در صورت استفاده از متد Html.EditorForModel، به صورت خودكار تبديل به يك RadioButtonList ميشوند.<br />
ج) متد زير نيز همين كار را انجام ميدهد:<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">@Html.EditorFor(model => model.EnumProperty, "EnumRadioButtonList")
</pre></div><br />
<br />
</div>وحيد نصيريhttp://www.blogger.com/profile/04454130327051686471noreply@blogger.comtag:blogger.com,1999:blog-7815821915651048578.post-31120401808005520642012-04-29T20:43:00.001+04:302012-04-29T20:44:38.982+04:30CheckBoxList در ASP.NET MVC<div dir="rtl" style="text-align: right;" trbidi="on"><br />
ASP.NET MVC به همراه HtmlHelper توكاري جهت نمايش يك ChekBoxList نيست؛ اما سيستم Model binder آن، اين نوع كنترلها را به خوبي پشتيباني ميكند. براي مثال، يك پروژه جديد خالي ASP.NET MVC را آغاز كنيد. سپس يك كنترلر Home جديد را نيز به آن اضافه كنيد. در ادامه، براي متد Index آن، يك View خالي را ايجاد نمائيد. سپس محتواي اين View را به نحو زير تغيير دهيد:<br />
<div align="left" dir="ltr"><pre language="XML" name="code">@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
@using (Html.BeginForm())
{
<input type='checkbox' name='Result' value='value1' />
<input type='checkbox' name='Result' value='value2' />
<input type='checkbox' name='Result' value='value3' />
<input type="submit" value="submit" />
}
</pre></div><br />
و كنترلر Home را نيز مطابق كدهاي زير ويرايش كنيد:<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Web.Mvc;
namespace MvcApplication21.Controllers
{
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index()
{
return View();
}
[HttpPost]
public ActionResult Index(string[] result)
{
return View();
}
}
}
</pre></div><br />
يك breakpoint را در تابع Index دوم كه آرايهاي را دريافت ميكند، قرار دهيد. سپس برنامه را اجرا كرده، تعدادي از checkboxها را انتخاب و فرم نمايش داده شده را به سرور ارسال كنيد:<br />
<br />
<div align="center"><img src="https://dotnettipsrepository.svn.codeplex.com/svn/Trunk/Images/MVC/mvc14.png" /></div><br />
بله. همانطور كه ملاحظه ميكنيد، تمام عناصر ارسالي انتخاب شده كه داراي نامي مشابه بودهاند، به يك آرايه قابل بايند هستند و سيستم model binder ميداند كه چگونه بايد اين اطلاعات را دريافت و پردازش كند.<br />
از اين مقدمه ميتوان به عنوان پايه و اساس نوشتن يك HtmlHelper سفارشي CheckBoxList استفاده كرد. <br />
براي اين منظور يك پوشه جديد را به نام app_code، به ريشه پروژه اضافه نمائيد. سپس يك فايل خالي را به نام Helpers.cshtml نيز به آن اضافه كنيد. محتواي اين فايل را به نحو زير تغيير دهيد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">@helper CheckBoxList(string name, List<System.Web.Mvc.SelectListItem> items)
{
<div class="checkboxList">
@foreach (var item in items)
{
@item.Text
<input type="checkbox" name="@name"
value="@item.Value"
@if (item.Selected) { <text>checked="checked"</text> }
/>
< br />
}
</div>
}
</pre></div><br />
و براي استفاده از آن، كنترلر Home را مطابق كدهاي زير ويرايش كنيد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Collections.Generic;
using System.Web.Mvc;
namespace MvcApplication21.Controllers
{
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index()
{
ViewBag.Tags = new List<SelectListItem>
{
new SelectListItem { Text = "Item1", Value = "Val1", Selected = false },
new SelectListItem { Text = "Item2", Value = "Val2", Selected = false },
new SelectListItem { Text = "Item3", Value = "Val3", Selected = true }
};
return View();
}
[HttpPost]
public ActionResult GetTags(string[] tags)
{
return View();
}
[HttpPost]
public ActionResult Index(string[] result)
{
return View();
}
}
}
</pre></div><br />
و در اين حالت View برنامه به شكل زير درخواهد آمد:<br />
<div align="left" dir="ltr"><pre language="XML" name="code">@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
@using (Html.BeginForm())
{
<input type='checkbox' name='Result' value='value1' />
<input type='checkbox' name='Result' value='value2' />
<input type='checkbox' name='Result' value='value3' />
<input type="submit" value="submit" />
}
@using (Html.BeginForm(actionName: "GetTags", controllerName: "Home"))
{
@Helpers.CheckBoxList("Tags", (List<SelectListItem>)ViewBag.Tags)
<input type="submit" value="submit" />
}
</pre></div><br />
با توجه به اينكه كدهاي Razor قرار گرفته در پوشه خاص app_code در ريشه سايت، به صورت خودكار در حين اجراي برنامه كامپايل ميشوند، متد Helpers.CheckBoxList در تمام Viewهاي برنامه در دسترس خواهد بود. در اين متد، يك نام و ليستي از SelectListItemها دريافت ميگردد. سپس به صورت خودكار يك CheckboxList را توليد خواهد كرد. براي دريافت مقادير ارسالي آن به سرور هم بايد مطابق متد GetTags تعريف شده در كنترلر Home عمل كرد. در اينجا Value عناصر انتخابي به صورت آرايهاي از رشتهها در دسترس خواهد بود.<br />
<br />
<b>روشي جامعتر</b><br />
در آدرس زير ميتوانيد يك HtmlHelper بسيار جامع را جهت توليد CheckBoxList در ASP.NET MVC بيابيد. در همان صفحه روش استفاده از آن، به همراه چندين مثال ارائه شده است: <br />
<a href="https://github.com/devnoob/MVC3-Html.CheckBoxList-custom-extension">https://github.com/devnoob/MVC3-Html.CheckBoxList-custom-extension</a><br />
<br />
</div>وحيد نصيريhttp://www.blogger.com/profile/04454130327051686471noreply@blogger.comtag:blogger.com,1999:blog-7815821915651048578.post-71961794261951087302012-04-27T00:26:00.002+04:302012-04-27T00:28:49.445+04:30ASP.NET MVC #24<div dir="rtl" style="text-align: right;" trbidi="on"><br />
<b>مروري بر نمونه سؤالات ASP.NET MVC امتحانات مايكروسافت در چند سال اخير</b><br />
<br />
در قسمت آخر سري ASP.NET MVC بد نيست مروري داشته باشيم بر نمونه سؤالات امتحانات مايكروسافت؛ امتحانات <a href="http://www.microsoft.com/learning/en/us/exam.aspx?id=70-515">70-515</a> و <a href="http://www.microsoft.com/learning/en/us/exam.aspx?id=70-519">70-519</a> كه در آنها تعدادي از سؤالات به ASP.NET MVC اختصاص دارند. در اين سؤالات امكان انتخاب بيش از يك گزينه نيز وجود دارد.<br />
<br />
<br />
1) شما در حال توسعه يك برنامهي ASP.NET MVC هستيد. بايد درخواست Ajax ايي از صفحهاي صادر شده و خروجي زير را از اكشن متدي دريافت كند:<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">["Adventure Works","Contoso"]
</pre></div><br />
كدام نوع خروجي اكشن متد زير را براي اينكار مناسب ميدانيد؟<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">a) AjaxHelper
b) XDocument
c) JsonResult
d) DataContractJsonSerializer
</pre></div><br />
<br />
<br />
2) شما در حال طراحي يك برنامه ASP.NET MVC هستيد. محتواي يك View بايد بر اساس نيازمنديهاي زير تشكيل شود:<br />
الف) ارائه محتواي رندر شده user controls/partial views به مرورگر<br />
ب) كار انتخاب user controls/partial views مناسب در اكشن متد كنترلر بايد انجام شود<br />
استفاده از كدام روش زير را توصيه ميكنيد؟<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">a) Use the Html.RenderPartial extension method
b) Use the Html.RenderAction extension method
c) Use the PartialViewResult class
d) Use the ContentResult class
</pre></div><br />
<br />
3) در حين طراحي يك برنامه ASP.NET MVC، نياز است منطق مديريت استثناهاي رخ داده و همچنين ثبت وقايع مرتبط را در يك مكان يا كلاس مركزي مديريت كنيد. كدام روش زير را پيشنهاد ميدهيد؟<br />
a) استفاده از try/catch در تمام متدها<br />
b) تحريف متد OnException در كنترلرها<br />
c) مزين سازي تمام كنترلرها به ويژگي HandleError سفارشي شده<br />
d) مزين سازي تمام كنترلرها به ويژگي HandleError پيش فرض<br />
<br />
<br />
4) شما در حال توزيع برنامهي ASP.NET MVC خود جهت اجرا بر روي IIS 6.x هستيد. چه ملاحظاتي را بايد مدنظر داشته باشيد تا برنامه به درستي كار كند؟<br />
a) تنظيم IIS به نحويكه تمام درخواستها را بر اساس wildcard خاصي به aspnet_isapi.dll هدايت كند.<br />
b) تنظيم IIS به نحويكه تمام درخواستها را بر اساس wildcard خاصي به aspnet_wp.exe هدايت كند.<br />
c) تغيير برنامه به نحويكه تمام درخواستها را به يك HttpHandler خاص هدايت كند.<br />
d) تغيير برنامه به نحويكه تمام درخواستها را به يك HttpModule خاص هدايت كند. <br />
<br />
<br />
5) شما در حال توسعه برنامهي ASP.NET MVC هستيد كه در پوشه Views/Shared/DisplayTemplates آن، فايلي به نام score.cshtml به عنوان يك templated helper نمايش سفارشي اعداد صحيح تعريف شده است. مدل برنامه هم مطابق تعاريف زير است:<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">public class Player
{
public String Name { get; set; }
public int LastScore { get; set; }
public int HighScore { get; set; }
}
</pre></div><br />
در اينجا اگر نياز باشد تا فايل score.cshtml ياد شده به صورت خودكار به خاصيت LastScore در حين فراخواني متد HtmlHelper.DisplayForModel اعمال شود، چه روشي را پيشنهاد ميدهيد؟<br />
a) فايل score.cshtml بايد به LastScore.cshtml تغيير نام يابد.<br />
b) فايل ياد شده بايد از پوشه Views/Shared/DisplayTemplates به پوشه Views/Player/DisplayTemplates منتقل شود.<br />
c) بايد از ويژگي UIHint به همراه مقدار score جهت مزين سازي خاصيت LastScore استفاده كرد.<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">[UIHint("Score")]
</pre></div>d) بايد از ويژگي زير براي مزين سازي خاصيت مورد نظر استفاده كرد:<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">[Display(Name="LastScore", ShortName="Score")]
</pre></div><br />
<br />
6) شما در حال طراحي برنامهي ASP.NET MVC هستيد كه در آن متد Edit كنترلري بايد تنها توسط كاربران اعتبارسنجي شده قابل دسترسي باشد. استفاده از كدام دو گزينه زير را براي اين منظور توصيه ميكنيد؟<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">a) [Authorize(Users = "")]
b) [Authorize(Roles = "")]
c) [Authorize(Users = "*")]
d) [Authorize(Roles = "*")]
</pre></div><br />
7) قطعه كد HTML زير را درنظر بگيريد:<br />
<div align="left" dir="ltr"><pre language="XML" name="code"><span id="ref">
<a name=Reference>Check out</a>
the FAQ on
<a href="http://www.contoso.com">
Contoso</a>'s web site for more information:
<a href="http://www.contoso.com/faq">FAQ</a>.
</span>
<a href="http://www.contoso.com/home">Home</a>
</pre></div><br />
قصد داريم به كمك jQuery در span ايي با id مساوي ref، متن تمام لينكها را ضخيم كنيم. كدام گزينه زير را پيشنهاد ميدهيد؟<br />
<br />
<div align="left" dir="ltr"><pre language="JScript" name="code">a) $("#ref").filter("a[href]").bold();
b) $("ref").filter("a").css("bold");
c) $("a").css({fontWeight:"bold"});
d) $("#ref a[href]").css({fontWeight:"bold"});
</pre></div><br />
<br />
</div>وحيد نصيريhttp://www.blogger.com/profile/04454130327051686471noreply@blogger.comtag:blogger.com,1999:blog-7815821915651048578.post-31317387879773255242012-04-26T11:12:00.001+04:302012-04-26T11:13:05.434+04:30ASP.NET MVC #23<div dir="rtl" style="text-align: right;" trbidi="on"><br />
<b>اجراي برنامههاي ASP.NET MVC توسط نگارشهاي متفاوت IIS</b><br />
<br />
تا اينجا براي اجراي برنامههاي ASP.NET MVC از وب سرور توكار VS.NET استفاده شد كه صرفا جهت آزمايش برنامهها طراحي شده است. تا اين تاريخ سه رده از وب سرورهاي مايكروسافت ارائه شدهاند كه براي نصب ASP.NET MVC ميتوانند مورد استفاده قرار گيرند و هر كدام هم نكتههاي خاص خودشان را دارند كه در ادامه به بررسي آنها خواهيم پرداخت.<br />
<br />
<br />
<b>اجراي برنامههاي ASP.NET MVC بر روي IIS 5.x ويندوز XP</b><br />
<br />
پس از ايجاد يك دايركتوري مجازي بر روي پوشه يك برنامه ASP.NET MVC و سعي در اجراي برنامه، بلافاصله پيغام خطاي HTTP 403 forbidden مشاهده ميشود. <br />
اولين كاري كه براي رفع اين مساله بايد صورت گيرد، كليك راست بر روي نام دايركتوري مجازي در كنسول IIS، انتخاب گزينه خواص و سپس مراجعه به برگه «ASP.NET» آن است. در اينجا شماره نگارش دات نت فريم ورك مورد استفاده را به 4 تغيير دهيد (براي نمونه ASP.NET MVC 3.0 مبتني بر دات نت فريم ورك 4 است).<br />
بعد از اين تغيير، بازهم موفق به اجراي برنامههاي ASP.NET MVC بر روي IIS 5.x نخواهيم شد؛ چون در آن زمان مفاهيم مسيريابي و Routing كه اصل و پايه ASP.NET MVC هستند وجود خارجي نداشتند. اين نگارش از IIS به صورت پيش فرض تنها قادر به پردازش درخواستهاي رسيدهاي كه به يك فايل فيزيكي بر روي سرور اشاره ميكند، ميباشد (يعني مشكلي با اجراي برنامههاي ASP.NET Web forms ندارد).<br />
براي رفع اين مشكل، مجددا بر روي نام دايركتوري مجازي برنامه در كنسول IIS كليك راست كرده و گزينه خواص را انتخاب كنيد. در صفحه ظاهر شده، در برگه «Virtual directory» آن، بر روي دكمه «Configuration» كليك نمائيد. در صفحه باز شده مجددا بر روي دكمه «Add» كليك كنيد.<br />
در صفحه باز شده، مسير Executable را C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll وارد كرده و Extension را به .* (دات هرچي) تنظيم كنيد. همين مقدار تنظيم، براي اجراي برنامههاي ASP.NET MVC بر روي IIS 5.x ويندوز XP كفايت ميكند.<br />
<br />
كاري كه در اينجا انجام شده است، نگاشت تمام درخواستهاي رسيده صرفنظر از پسوند فايلها، به موتور ASP.NET ميباشد. به صورت پيش فرض در IIS 5.x درخواستها تنها بر اساس پسوند فايلها پردازش ميشوند. مثلا اگر فايل درخواستي aspx است، درخواست رسيده به aspnet_isapi.dll ياد شده هدايت خواهد شد. اگر پسوند فايل php است به isapi مخصوص آن (در صورت نصب) هدايت ميگردد و به همين ترتيب براي ساير سيستمهاي ديگر. زمانيكه Extension به «دات هرچي» و Executable به aspnet_isapi.dll دات نت 4 تنظيم ميشود، دايركتوري مجازي تنظيم شده تنها جهت سرويس دهي به يك برنامه ASP.NET عمل خواهد كرد و تمام درخواستهاي رسيده به آن، به موتور اجرايي ASP.NET هدايت ميشوند.<br />
<br />
بديهي است تنظيمات فوق تنها به يك دايركتوري مجازي اعمال شدند. اگر نياز باشد تا بر روي تمام سايتها تاثير گذار شود، اينبار در كنسول IIS 5.x بر روي «Default web site» كليك راست كرده و گزينه خواص را انتخاب كنيد. در صفحه باز شده به برگه «Home directory» مراجعه كرده و مراحل ذكر شده را تكرار كنيد.<br />
<br />
<b>مشكل! اين روش بهينه نيست.</b><br />
روش فوق خوبه، كار ميكنه، اما بهينه نيست؛ از اين جهت كه «نگاشت تمام درخواستها به موتور ASP.NET» يعني پروسه پردازش درخواست يك فايل تصويري، js يا css هم بايد از فيلتر موتور ASP.NET عبور كند كه ضروري نيست.<br />
براي رفع اين مشكل، توصيه شده است كه سيستم مسيريابي ASP.NET MVC را در IIS 5.x «پسوند دار» كنيد. به اين نحو كه با مراجعه به فايل Global.asax.cs، تعاريف مسيريابي را به نحو زير ويرايش كنيد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add(
new Route("{controller}.aspx/{action}/{id}", new MvcRouteHandler())
{
Defaults = new RouteValueDictionary(new
{
controller = "Home",
action = "Index",
id = UrlParameter.Optional
})
});
</pre></div><br />
<br />
اينبار براي مثال مسير http://localhost/MyMvcApp/home.aspx/index به علت داشتن پسوند aspx وارد موتور پردازشي ASP.NET خواهد شد. البته در اين حالت URL هاي تميز ASP.NET MVC را از دست خواهيم داد و مدام بايد دقت داشت كه مسيرهاي كنترلرها حتما بايد به aspx ختم شوند. ضمنا با اين تنظيم، ديگر نيازي به تغيير تعاريف نگاشتها در كنسول مديريتي IIS، نخواهد بود.<br />
<br />
<br />
<b>اجراي برنامههاي ASP.NET MVC بر روي IIS 6.x ويندوز سرور 2003</b><br />
<br />
تمام نكات عنوان شده جهت IIS 5.x در IIS 6.x نيز صادق هستند. به علاوه براي اجراي برنامههاي ASP.NET بر روي IIS 6.x بايد به دو نكته مهم ديگر نيز دقت داشت:<br />
الف) ASP.NET 4 به صورت پيش فرض در IIS 6.x غيرفعال است كه بايد با مراجعه به قسمت Web Services Extensions در كنسول مديريتي IIS، آنرا از حالت prohibited خارج كرد.<br />
ب) در هر Application pool تنها از يك نگارش دات نت فريم ورك ميتوان استفاده كرد. براي مثال اگر هم اكنون AppPool1 مشغول سرويس دهي به يك سايت ASP.NET 3.5 است، از آن نميتوانيد جهت اجراي برنامههاي ASP.NET MVC 3 به بعد استفاده كنيد. زيرا براي مثال ASP.NET MVC 3 مبتني بر دات نت فريم ورك 4 است. به همين جهت حتما نياز است تا يك Application pool مجزا را براي برنامههاي دات نت 4 در IIS 6 اضافه نمائيد و سپس در تنظيمات سايت، از اين Application pool جديد استفاده نمائيد.<br />
البته روش صحيح و اصولي كار با IIS از نگارش 6 به بعد هم مطابق شرحي است كه عنوان شد. براي دستيابي به بهترين كارآيي و امنيت بيشتر، بهتر است به ازاي هر سايت، از يك Application pool مجزا استفاده نمائيد.<br />
<br />
اطلاعات تكميلي: <br />
<a href="http://www.dotnettips.info/2011/02/aspnet-40-iis-6.html">نکات نصب برنامههاي ASP.NET 4.0 بر روي IIS 6</a><br />
<a href="http://www.dotnettips.info/2011/07/aspnet-iis.html">مروري بر تاريخچه محدوديت حافظه مصرفي برنامههاي ASP.NET در IIS</a><br />
<br />
<br />
<br />
<b>اجراي برنامههاي ASP.NET MVC بر روي IIS 7.x ويندوز 7 و ويندوز سرور 2008</b><br />
<br />
اگر برنامه ASP.NET MVC در IIS 7.x در حالت يكپارچه (integrated mode) اجرا شود، بدون نياز به هيچگونه تغييري در تنظيمات سرور يا برنامه، بدون مشكل قابل اجرا خواهد بود. بديهي است در اينجا نيز بهتر است به ازاي هر برنامه، يك Application pool مجزا را ايجاد كرد.<br />
اما در حالت classic (كه براي برنامههاي جديد توصيه نميشود) نياز است همان مراحل IIS 5,x تكرار شود. البته اينبار مسير زير را بايد طي كرد تا به صفحه افزودن نگاشتها رسيد:<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">Right-click on a web site -> Properties -> Home Directory tab -> click on the Configuration button -> Mappings tab
</pre></div><br />
<br />
<br />
<b>نكتهاي مهم در تمام نگارشهاي IIS</b><br />
<br />
ترتيب نصب دات نت فريم ورك 4 و IIS مهم است. اگر ابتدا IIS نصب شود و سپس دات نت فريم ورك 4، به صورت خودكار، كار نگاشت اطلاعات ASP.NET به IIS صورت خواهد گرفت. <br />
اگر ابتدا دات نت فريم ورك 4 نصب شود و سپس IIS، براي مثال ديگر از برگه ASP.NET در IIS 6.x خبري نخواهد بود. براي رفع اين مشكل دستور زير را در خط فرمان اجرا كنيد:<br />
<br />
<div align="left" dir="ltr"><pre language="xml" name="code">C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319\aspnet_regiis.exe /i
</pre></div><br />
به اين ترتيب، اطلاعات مرتبط با موتور ASP.NET مجددا به تنظيمات IIS اضافه خواهند شد.<br />
<br />
<br />
</div>وحيد نصيريhttp://www.blogger.com/profile/04454130327051686471noreply@blogger.comtag:blogger.com,1999:blog-7815821915651048578.post-37215022339056948982012-04-25T22:35:00.002+04:302012-04-25T23:36:14.597+04:30ASP.NET MVC #22<div dir="rtl" style="text-align: right;" trbidi="on"><br />
<b>تهيه سايتهاي چند زبانه و بومي سازي نمايش اطلاعات در ASP.NET MVC</b><br />
<br />
زمانيكه دات نت فريم ورك نياز به انجام اعمال حساس به مسايل بومي را داشته باشد، ابتدا به مقادير تنظيم شده دو خاصيت زير دقت ميكند:<br />
الف) System.Threading.Thread.CurrentThread.CurrentCulture <br />
بر اين اساس دات نت ميتواند تشخيص دهد كه براي مثال خروجي متد DateTime.Now.ToString در كانادا و آمريكا بايد با هم تفاوت داشته باشند. مثلا در آمريكا ابتدا ماه، سپس روز و در آخر سال نمايش داده ميشود و در كانادا ابتدا سال، بعد ماه و در آخر روز نمايش داده خواهد شد. يا نمونهي ديگري از اين دست ميتواند نحوه نمايش علامت واحد پولي كشورها باشد.<br />
ب) System.Threading.Thread.CurrentThread.CurrentUICulture<br />
مقدار CurrentUICulture بر روي بارگذاري فايلهاي مخصوصي به نام Resource، تاثير گذار است.<br />
<br />
اين خواص را يا به صورت دستي ميتوان تنظيم كرد و يا ASP.NET، اين اطلاعات را از هدر Accept-Language دريافتي از مرورگر كاربر به صورت خودكار مقدار دهي ميكند. البته براي اين منظور نياز است يك سطر زير را به فايل وب كانفيگ برنامه اضافه كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code"><system.web>
<globalization culture="auto" uiCulture="auto" />
</pre></div><br />
يا اگر نياز باشد تا برنامه را ملزم به نمايش اطلاعات Resource مرتبط با فرهنگ بومي خاصي كرد نيز ميتوان در همين قسمت مقادير culture و uiCulture را دستي تنظيم نمود و يا اگر همانند برنامههايي كه چند لينك را بالاي صفحه نمايش ميدهند كه براي مثال به نگارشهاي فارسي/عربي/انگليسي اشاره ميكند، اينكار را با كد نويسي نيز ميتوان انجام داد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">System.Threading.Thread.CurrentThread.CurrentCulture =
System.Globalization.CultureInfo.CreateSpecificCulture("fa");
</pre></div><br />
<br />
جهت آزمايش اين مطلب، ابتدا تنظيم globalization فوق را به فايل وب كانفيگ برنامه اضافه كنيد. سپس به مسير زير در IE مراجعه كنيد:<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code">IE -> Tools -> Internet options -> General tab -> Languages
</pre></div><br />
در اينجا ميتوان هدر Accept-Language را مقدار دهي كرد. براي نمونه اگر مقدار زبان پيش فرض را به فرانسه تنظيم كنيم (به عنوان اولين زبان تعريف شده در ليست) و سپس سعي در نمايش مقدار decimal زير را داشته باشيم:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">string.Format("{0:C}", 10.5M)
</pre></div><br />
اگر زبان پيش فرض، انگليسي آمريكايي باشد، $ نمايش داده خواهد شد و اگر زبان به فرانسه تنظيم شود، يورو در كنار عدد مبلغ نمايش داده ميشود.<br />
تا اينجا تنها با تنظيم culture=auto به اين نتيجه رسيدهايم. اما ساير قسمتهاي صفحه چطور؟ براي مثال برچسبهاي نمايش داده شده را چگونه ميتوان به صورت خودكار بر اساس Accept-Language مرجح كاربر تنظيم كرد؟ خوشبختانه در دات نت، زير ساخت مديريت برنامههاي چند زبانه به صورت توكار وجود دارد كه در ادامه به بررسي آن خواهيم پرداخت.<br />
<br />
<br />
<b>آشنايي با ساختار فايلهاي Resource</b><br />
<br />
<br />
فايلهاي Resource يا منبع، در حقيقت فايلهايي هستند مبتني بر XML با پسوند resx و هدف آنها ذخيره سازي رشتههاي متناظر با فرهنگهاي مختلف ميباشد و براي استفاده از آنها حداقل يك فايل منبع پيش فرض بايد تعريف شود. براي نمونه فايل mydata.resx را در نظر بگيريد. براي ايجاد فايل منبع اسپانيايي متناظر، بايد فايلي را به نام mydata.es.resx توليد كرد. البته نوع فرهنگ مورد استفاده را كاملتر نيز ميتوان ذكر كرد براي مثال mydata.es-mex.resx جهت فرهنگ اسپانيايي مكزيكي بكارگرفته خواهد شد، يا mydata.fr-ca.resx به فرانسوي كانادايي اشاره ميكند. سپس مديريت منابع دات نت فريم ورك بر اساس مقدار CurrentUICulture جاري، اطلاعات فايل متناظري را بارگذاري خواهد كرد. اگر فايل متناظري وجود نداشت، از اطلاعات همان فايل پيش فرض استفاده ميگردد.<br />
حين تهيه برنامهها نيازي نيست تا مستقيما با فايلهاي XML منابع كار كرد. زمانيكه اولين فايل منبع توليد ميشود، به همراه آن يك فايل cs يا vb نيز ايجاد خواهد شد كه امكان دسترسي به كليدهاي تعريف شده در فايلهاي XML را به صورت strongly typed ميسر ميكند. اين فايلهاي خودكار، تنها براي فايل پيش فرض mydata.resx توليد ميشوند،از اين جهت كه تعاريف اطلاعات ساير فرهنگهاي متناظر نيز بايد با همان كليدهاي فايل پيش فرض آغاز شوند. تنها «مقادير» كليدهاي تعريف شده در كلاسهاي منبع متفاوت هستند. <br />
اگر به خواص فايلهاي resx در VS.NET دقت كنيم، نوع Build action آنها به embedded resource تنظيم شده است.<br />
<br />
<br />
<b>مثالي جهت بررسي استفاده از فايلهاي Resource</b><br />
<br />
يك پروژه جديد خالي ASP.NET MVC را آغاز كنيد. فايل وب كانفيگ آنرا ويرايش كرده و تنظيمات globalization ابتداي بحث را به آن اضافه كنيد. سپس مدل، كنترلر و View متناظر با متد Index آنرا با محتواي زير به پروژه اضافه نمائيد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">namespace MvcApplication19.Models
{
public class Employee
{
public int Id { set; get; }
public string Name { set; get; }
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Web.Mvc;
using MvcApplication19.Models;
namespace MvcApplication19.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
var employee = new Employee { Name = "Name 1" };
return View(employee);
}
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="XML" name="code">@model MvcApplication19.Models.Employee
@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<fieldset>
<legend>Employee</legend>
<div class="display-label">
Name
</div>
<div class="display-field">
@Html.DisplayFor(model => model.Name)
</div>
</fieldset>
<fieldset>
<legend>Employee Info</legend>
@Html.DisplayForModel()
</fieldset>
</pre></div><br />
قصد داريم در View فوق بر اساس uiCulture كاربر مراجعه كننده به سايت، برچسب Name را مقدار دهي كنيم. اگر كاربري از ايران مراجعه كند، «نام كارمند» نمايش داده شود و ساير كاربران، «Employee Name» را مشاهده كنند. همچنين اين تغييرات بايد بر روي متد Html.DisplayForModel نيز تاثيرگذار باشد.<br />
براي اين منظور بر روي پوشه Views/Home كه محل قرارگيري فايل Index.cshtml فوق است كليك راست كرده و گزينه Add|New Item را انتخاب كنيد. سپس در صفحه ظاهر شده، گزينه «Resources file» را انتخاب كرده و براي مثال نام Index_cshtml.resx را وارد كنيد. <br />
به اين ترتيب اولين فايل منبع مرتبط با View جاري كه فايل پيش فرض نيز ميباشد ايجاد خواهد شد. اين فايل، به همراه فايل Index_cshtml.Designer.cs توليد ميشود. سپس همين مراحل را طي كنيد، اما اينبار نام Index_cshtml.<b>fa</b>.resx را حين افزودن فايل منبع وارد نمائيد كه براي تعريف اطلاعات بومي ايران مورد استفاده قرار خواهد گرفت. فايل دومي كه اضافه شده است، فاقد فايل cs همراه ميباشد.<br />
اكنون فايل Index_cshtml.resx را در VS.NET باز كنيد. از بالاي صفحه، به كمك گزينه Access modifier، سطح دسترسي متدهاي فايل cs همراه آنرا به public تغيير دهيد. پيش فرض آن internal است كه براي كار ما مفيد نيست. از اين جهت كه امكان دسترسي به متدهاي استاتيك تعريف شده در فايل خودكار Index_cshtml.Designer.cs را در View هاي برنامه، نخواهيم داشت. سپس دو جفت «نام-مقدار» را در فايل resx وارد كنيد. مثلا نام را Name و مقدار آنرا «Employee Name» و سپس نام ديگر را NameIsNotRight و مقدار آنرا «Name is required» وارد نمائيد.<br />
در ادامه فايل Index_cshtml.fa.resx را باز كنيد. در اينجا نيز دو جفت «نام-مقدار» متناظر با فايل پيش فرض منبع را بايد وارد كرد. كليدها يا نامها يكي است اما قسمت مقدار اينبار بايد فارسي وارد شود. مثلا نام را Name و مقدار آنرا «نام كارمند» وارد نمائيد. سپس كليد يا نام NameIsNotRight و مقدار «لطفا نام را وارد نمائيد» را تنظيم نمائيد.<br />
تا اينجا كار تهيه فايلهاي منبع متناظر با View جاري به پايان ميرسد.<br />
در ادامه با كمك فايل Index_cshtml.Designer.cs كه هربار پس از تغيير فايل resx متناظر آن به صورت خودكار توسط VS.NET توليد و به روز ميشود، ميتوان به كليدها يا نامهايي كه تعريف كردهايم، در قسمتهاي مختلف برنامه دست يافت. براي نمونه تعريف كليد Name در اين فايل به نحو زير است:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">namespace MvcApplication19.Views.Home {
public class Index_cshtml {
public static string Name {
get {
return ResourceManager.GetString("Name", resourceCulture);
}
}
}
}
</pre></div><br />
بنابراين براي استفاده از آن در هر View ايي تنها كافي است بنويسيم:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">@MvcApplication19.Views.Home.Index_cshtml.Name
</pre></div><br />
به اين ترتيب بر اساس تنظيمات محلي كاربر، اطلاعات به صورت خودكار از فايلهاي Index_cshtml.fa.resx فارسي يا فايل پيش فرض Index_cshtml.resx، دريافت ميگردد.<br />
علاوه بر امكان دسترسي مستقيم به كليدهاي تعريف شده در فايلهاي منبع، امكان استفاده از آنها توسط data annotations نيز ميسر است. در اين حالت ميتوان مثلا پيغامهاي اعتبار سنجي را بومي كرد يا حين استفاده از متد Html.DisplayForModel، بر روي برچسب نمايش داده شده خودكار، تاثير گذار بود. براي اينكار بايد اندكي مدل برنامه را ويرايش كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.ComponentModel.DataAnnotations;
namespace MvcApplication19.Models
{
public class Employee
{
[ScaffoldColumn(false)]
public int Id { set; get; }
[Display(ResourceType = typeof(MvcApplication19.Views.Home.Index_cshtml),
Name = "Name")]
[Required(ErrorMessageResourceType = typeof(MvcApplication19.Views.Home.Index_cshtml),
ErrorMessageResourceName = "NameIsNotRight")]
public string Name { set; get; }
}
}
</pre></div><br />
همانطور كه ملاحظه ميكنيد، حين تعريف ويژگيهاي Display يا Required، امكان تعريف نام كلاس متناظر با فايل resx خاصي وجود دارد. به علاوه ErrorMessageResourceName به نام يك كليد در اين فايل و يا پارامتر Name ويژگي Display نيز به نام كليدي در فايل منبع مشخص شده، اشاره ميكنند. اين اطلاعات توسط متدهاي Html.DisplayForModel، Html.ValidationMessageFor، Html.LabelFor و امثال آن به صورت خودكار مورد استفاده قرار خواهند گرفت. <br />
<br />
<br />
<b>نكتهاي در مورد كش كردن اطلاعات</b><br />
در اين مثال اگر فيلتر OutputCache را بر روي متد Index تعريف كنيم، حتما نياز است به هدر Accept-Language نيز دقت داشت. در غيراينصورت تمام كاربران، صرفنظر از تنظيمات بومي آنها، يك صفحه را مشاهده خواهند كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">[OutputCache(Duration = 60, VaryByHeader = "Accept-Language")]
public ActionResult Index()
</pre></div><br />
<br />
</div>وحيد نصيريhttp://www.blogger.com/profile/04454130327051686471noreply@blogger.comtag:blogger.com,1999:blog-7815821915651048578.post-75055763281611098122012-04-24T17:48:00.000+04:302012-04-24T17:48:04.670+04:30ASP.NET MVC #21<div dir="rtl" style="text-align: right;" trbidi="on"><br />
<b>آشنايي با تكنيكهاي Ajax در ASP.NET MVC </b><br />
<br />
اهميت آشنايي با Ajax، ارائه تجربه كاربري بهتري از برنامههاي وب، به مصرف كنندگان نهايي آن ميباشد. به اين ترتيب ميتوان درخواستهاي غيرهمزماني (asynchronous) را با فرمت XML يا Json به سرور ارسال كرد و سپس نتيجه نهايي را كه حجم آن نسبت به يك صفحه كامل بسيار كمتر است، به كاربر ارائه داد. غيرهمزمان بودن درخواستها سبب ميشود تا ترد اصلي رابط كاربري برنامه قفل نشده و كاربر در اين بين ميتواند به ساير امور خود بپردازد. به اين ترتيب ميتوان برنامههاي وبي را كه شبيه به برنامههاي دسكتاپ هستند توليد نمود؛ كل صفحه مرتبا به سرور ارسال نميشود، flickering و چشمك زدن صفحه كاهش خواهد يافت (چون نيازي به ترسيم مجدد كل صفحه نخواهد بود و عموما قسمتي جزئي از يك صفحه به روز ميشود) يا بدون نياز به ارسال كل صفحه به سرور، به كاربري خواهيم گفت كه آيا اطلاعاتي كه وارد كرده است معتبر ميباشد يا نه (نمونهاي از آن را در قسمت Remote validation اعتبار سنجي اطلاعات ملاحظه نموديد).<br />
<br />
<br />
<b>مروري بر محتويات پوشه Scripts يك پروژه جديد ASP.NET MVC در ويژوال استوديو</b><br />
<br />
با ايجاد هر پروژه ASP.NET MVC جديدي در ويژوال استوديو، يك سري اسكريپت هم به صورت خودكار در پوشه Scripts آن اضافه ميشوند. تعدادي از اين فايلها توسط مايكروسافت پياده سازي شدهاند. براي مثال:<br />
MicrosoftAjax.debug.js<br />
MicrosoftAjax.js<br />
MicrosoftMvcAjax.debug.js<br />
MicrosoftMvcAjax.js<br />
MicrosoftMvcValidation.debug.js<br />
MicrosoftMvcValidation.js<br />
<br />
اين فايلها از ASP.NET MVC 3 به بعد، صرفا جهت سازگاري با نگارشهاي قبلي قرار دارند و استفاده از آنها اختياري است. بنابراين با خيال راحت آنها را delete كنيد! روش توصيه شده جهت پياده سازي ويژگيهاي Ajax ايي، استفاده از كتابخانههاي مرتبط با jQuery ميباشد؛ از اين جهت كه 100ها افزونه براي كار با آن توسط گروه وسيعي از برنامه نويسها در سراسر دنيا تاكنون تهيه شده است. به علاوه فريم ورك jQuery تنها منحصر به اعمال Ajax ايي نيست و از آن جهت دستكاري DOM (document object model) و CSS صفحه نيز ميتوان استفاده كرد. همچنين حجم كمي نيز داشته، با انواع و اقسام مرورگرها سازگار است و مرتبا هم به روز ميشود. <br />
<br />
در اين پوشه سه فايل ديگر پايه كتابخانه jQuery نيز قرار دارند:<br />
jquery-xyz-vsdoc.js<br />
jquery-xyz.js<br />
jquery-xyz.min.js<br />
<br />
فايل vsdoc براي ارائه نهايي برنامه طراحي نشده است. هدف از آن ارائه Intellisense بهتري از jQuery در VS.NET ميباشد. فايلي كه بايد به كلاينت ارائه شود، فايل min يا فشرده شده آن است. اگر به آن نگاهي بيندازيم به نظر obfuscated مشاهده ميشود. علت آن هم حذف فواصل، توضيحات و همچنين كاهش طول متغيرها است تا اندازه فايل نهايي به حداقل خود كاهش پيدا كند. البته اين فايل از ديدگاه مفسر جاوا اسكريپت يك مرورگر، فايل بينقصي است!<br />
اگر علاقمند هستيد كه سورس اصلي jQuery را مطالعه كنيد، به فايل jquery-xyz.js مراجعه نمائيد.<br />
محل الحاق اسكريپتهاي عمومي مورد نياز برنامه نيز بهتر است در فايل master page يا layout برنامه باشد كه به صورت پيش فرض اينكار انجام شده است.<br />
ساير فايلهاي اسكريپتي كه در اين پوشه مشاهده ميشوند، يك سري افزونه عمومي يا نوشته شده توسط تيم ASP.NET MVC برفراز jQuery هستند.<br />
<br />
به چهار نكته نيز حين استفاده از اسكريپتهاي موجود بايد دقت داشت:<br />
الف) هميشه از متد Url.Content همانند تعاريفي كه در فايل Views\Shared\_Layout.cshtml مشاهده ميكنيد، براي مشخص سازي مسير ريشه سايت، استفاده نمائيد. به اين ترتيب صرفنظر از آدرس جاري صفحه، همواره آدرس صحيح قرارگيري پوشه اسكريپتها در صفحه ذكر خواهد شد.<br />
ب) ترتيب فايلهاي js مهم هستند. ابتدا بايد كتابخانه اصلي jQuery ذكر شود و سپس افزونههاي آنها.<br />
ج) اگر اسكريپتهاي jQuery در فايل layout سايت تعريف شدهاند؛ نيازي به تعريف مجدد آنها در Viewهاي سايت نيست.<br />
د) اگر View ايي به اسكريپت ويژهاي جهت اجرا نياز دارد، بهتر است آنرا به شكل يك section داخل view تعريف كرد و سپس به كمك متد RenderSection اين قسمت را در layout سايت مقدار دهي نمود. مثالي از آنرا در قسمت 20 اين سري مشاهده نموديد (افزودن نمايش جمع هر ستون گزارش).<br />
<br />
<br />
<b>يك نكته</b><br />
اگر آخرين به روز رسانيهاي ASP.NET MVC را نيز نصب كرده باشيد، فايلي به نام packages.config به صورت پيش فرض به هر پروژه جديد ASP.NET MVC اضافه ميشود. به اين ترتيب VS.NET به كمك NuGet اين امكان را خواهد يافت تا شما را از آخرين به روز رسانيهاي اين كتابخانهها مطلع كند.<br />
<br />
<br />
<b>آشنايي با Ajax Helpers توكار ASP.NET MVC</b><br />
<br />
اگر به تعاريف خواص و متدهاي كلاس WebViewPage دقت كنيم:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System;
namespace System.Web.Mvc
{
public abstract class WebViewPage<TModel> : WebViewPage
{
protected WebViewPage();
public AjaxHelper<TModel> Ajax { get; set; }
public HtmlHelper<TModel> Html { get; set; }
public TModel Model { get; }
public ViewDataDictionary<TModel> ViewData { get; set; }
public override void InitHelpers();
protected override void SetViewData(ViewDataDictionary viewData);
}
}
</pre></div><br />
علاوه بر خاصيت Html كه وهلهاي از آن امكان دسترسي به Html helpers توكار ASP.NET MVC را در يك View فراهم ميكند، خاصيتي به نام Ajax نيز وجود دارد كه توسط آن ميتوان به تعدادي متد AjaxHelper توكار دسترسي داشت. براي مثال توسط متد Ajax.ActionLink ميتوان قسمتي از صفحه را به كمك ويژگيهاي Ajax، به روز رساني كرد.<br />
<br />
<br />
<b>مثالي در مورد به روز رساني قسمتي از صفحه به كمك متد Ajax.ActionLink</b><br />
<br />
ابتدا نياز است فايل Views\Shared\_Layout.cshtml را اندكي ويرايش كرد. براي اين منظور سطر الحاق jquery.unobtrusive-ajax.min.js را به فايل layout برنامه اضافه نمائيد (اگر اين سطر اضافه نشود، متد Ajax.ActionLink همانند يك لينك معمولي رفتار خواهد كرد):<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code"><head>
<title>@ViewBag.Title</title>
<link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
<script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script>
</head>
</pre></div><br />
سپس مدل ساده و منبع داده زير را نيز به پروژه اضافه كنيد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">namespace MvcApplication18.Models
{
public class Employee
{
public int Id { set; get; }
public string Name { set; get; }
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Collections.Generic;
namespace MvcApplication18.Models
{
public static class EmployeeDataSource
{
public static IList<Employee> CreateEmployees()
{
var list = new List<Employee>();
for (int i = 0; i < 1000; i++)
{
list.Add(new Employee { Id = i + 1, Name = "name " + i });
}
return list;
}
}
}
</pre></div><br />
در ادامه كنترلر جديدي را به برنامه با محتواي زير اضافه كنيد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Linq;
using System.Web.Mvc;
using MvcApplication18.Models;
namespace MvcApplication18.Controllers
{
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index()
{
return View();
}
[HttpPost] //for IE-8
public ActionResult EmployeeInfo(int? id)
{
if (!Request.IsAjaxRequest())
return View("Error");
if (!id.HasValue)
return View("Error");
var list = EmployeeDataSource.CreateEmployees();
var data = list.Where(x => x.Id == id.Value).FirstOrDefault();
if (data == null)
return View("Error");
return PartialView(viewName: "_EmployeeInfo", model: data);
}
}
}
</pre></div><br />
بر روي متد Index كليك راست كرده و گزينه Add view را انتخاب كنيد. يك View خالي را به آن اضافه نمائيد. همچنين بر روي متد EmployeeInfo كليك راست كرده و با انتخاب گزينه Add view در صفحه ظاهر شده يك partial view را اضافه نمائيد. جهت تمايز بين partial view و view هم بهتر است نام partial view با يك underline شروع شود.<br />
كدهاي partial view مورد نظر را به نحو زير تغيير دهيد:<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code">@model MvcApplication18.Models.Employee
<strong>Name:</strong> @Model.Name
</pre></div><br />
سپس كدهاي View متناظر با متد Index را نيز به صورت زير اعمال كنيد:<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code">@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<div id="EmployeeInfo">
@Ajax.ActionLink(
linkText: "Get Employee-1 info",
actionName: "EmployeeInfo",
controllerName: "Home",
routeValues: new { id = 1 },
ajaxOptions: new AjaxOptions
{
HttpMethod = "POST",
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "EmployeeInfo",
LoadingElementId = "Progress"
})
</div>
<div id="Progress" style="display: none">
<img src="@Url.Content("~/Content/images/loading.gif")" alt="loading..." />
</div>
</pre></div><br />
<b>توضيحات جزئيات كدهاي فوق</b><br />
<br />
متد Ajax.ActionLink لينكي را توليد ميكند كه با كليك كاربر بر روي آن، اطلاعات اكشن متد واقع در كنترلري مشخص، به كمك ويژگيهاي jQuery Ajax دريافت شده و سپس در مقصدي كه توسط UpdateTargetId مشخص ميگردد، بر اساس مقدار InsertionMode، درج خواهد شد (ميتواند قبل از آن درج شود يا پس از آن و يا اينكه كل محتواي مقصد را بازنويسي كند). HttpMethod آن هم به POST تنظيم شده تا با IE مشكلي نباشد. از اين جهت كه IE پيغامهاي GET را كش ميكند و مساله ساز خواهد شد. توسط پارامتر routeValues، آرگومان مورد نظر به متد EmployeeInfo ارسال خواهد شد. <br />
به علاوه يكي ديگر از خواص كلاس AjaxOptions، براي معرفي حالت بروز خطايي در سمت سرور به نام OnFailure در نظر گرفته شده است. در اينجا ميتوان نام يك متد JavaScript ايي را مشخص كرده و پيغام خطاي عمومي را در صورت فراخواني آن به كاربر نمايش داد. يا توسط خاصيت Confirm آن ميتوان يك پيغام را پيش از ارسال اطلاعات به سرور به كاربر نمايش داد.<br />
به اين ترتيب در مثال فوق، id=1 به متد EmployeeInfo به صورت غيرهمزمان ارسال ميگردد. سپس كارمندي بر اين اساس يافت شده و در ادامه partial view مورد نظر بر اساس اطلاعات كاربر مذكور، رندر خواهد شد. نتيجه كار، در يك div با id مساوي EmployeeInfo درج ميگردد (InsertionMode.Replace). متد Ajax.ActionLink از اين جهت داخل div تعريف شدهاست كه پس از كليك كاربر و جايگزيني محتوا، محو شود. اگر نيازي به محو آن نبود، آنرا خارج از div تعريف كنيد.<br />
عمليات دريافت اطلاعات از سرور ممكن است مدتي طول بكشد (براي مثال دريافت اطلاعات از بانك اطلاعاتي). به همين جهت بهتر است در اين بين از تصاويري كه نمايش دهنده انجام عمليات است، استفاده شود. براي اين منظور يك div با id مساوي Progress تعريف شده و id آن به LoadingElementId انتساب داده شده است. اين div با توجه به display: none آن، در ابتداي امر به كاربر نمايش داده نخواهد شد؛ در آغاز كار دريافت اطلاعات از سرور توسط متد Ajax.ActionLink نمايان شده و پس از خاتمه كار مجددا مخفي خواهد شد.<br />
به علاوه اگر به كدهاي فوق دقت كرده باشيد، از متد Request.IsAjaxRequest نيز استفاده شده است. به اين ترتيب ميتوان تشخيص داد كه آيا درخواست رسيده از طرف jQuery Ajax صادر شده است يا خير. البته آنچنان روش قابل ملاحظهاي نيست؛ چون امكان دستكاري Http Headers هميشه وجود دارد؛ اما بررسي آن ضرري ندارد. البته اين نوع بررسيها را در ASP.NET MVC بهتر است تبديل به يك فيلتر سفارشي نمود؛ به اين ترتيب حجم if و else نويسي در متدهاي كنترلرها به حداقل خواهد رسيد. براي مثال:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">[AttributeUsage(AttributeTargets.Class|AttributeTargets.Method)]
public class AjaxOnlyAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
base.OnActionExecuting(filterContext);
}
else
{
throw new InvalidOperationException("This operation can only be accessed via Ajax requests");
}
}
}
</pre></div><br />
و براي استفاده از آن خواهيم داشت:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">[AjaxOnly]
public ActionResult SomeAjaxAction()
{
return Content("Hello!");
}
</pre></div><br />
<br />
در مورد كلمه unobtrusive در قسمت بررسي نحوه اعتبار سنجي اطلاعات، توضيحاتي را ملاحظه نمودهايد. در اينجا نيز از ويژگيهاي data-* براي معرفي پارامترهاي مورد نياز حين ارسال اطلاعات به سرور، استفاده ميگردد. براي مثال خروجي متد Ajax.ActionLink به شكل زير است. به اين ترتيب امكان حذف كدهاي جاوا اسكريپت از صفحه فراهم ميشود و توسط يك فايل jquery.unobtrusive-ajax.min.js كه توسط تيم ASP.NET MVC تهيه شده، اطلاعات مورد نياز به سرور ارسال خواهد گرديد:<br />
<div align="left" dir="ltr"><pre language="XML" name="code"><a data-ajax="true" data-ajax-loading="#Progress" data-ajax-method="POST"
data-ajax-mode="replace" data-ajax-update="#EmployeeInfo"
href="/Home/EmployeeInfo/1">Get Employee-1 info</a>
</pre></div><br />
در كل اين روش قابليت نگهداري بهتري نسبت به روش اسكريپت نويسي مستقيم داخل صفحات را به همراه دارد. به علاوه جدا سازي افزونه اسكريپت وفق دهنده اين اطلاعات با متد jQuery.Ajax از صفحه جاري، كه امكان كش شدن آنرا به سادگي ميسر ميسازد.<br />
<br />
<br />
<b>به روز رساني اطلاعات قسمتي از صفحه بدون استفاده از متد Ajax.ActionLink</b><br />
<br />
الزامي به استفاده از متد Ajax.ActionLink و فايل jquery.unobtrusive-ajax.min.js وجود ندارد. اينكار را مستقيما به كمك jQuery نيز ميتوان به نحو زير انجام داد:<br />
<br />
<div align="left" dir="ltr"><pre language="JScript" name="code"><a href="#" onclick="LoadEmployeeInfo()">Get Employee-1 info</a>
@section javascript
{
<script type="text/javascript">
function LoadEmployeeInfo() {
showProgress();
$.ajax({
type: "POST",
url: "/Home/EmployeeInfo",
data: JSON.stringify({ id: 1 }),
contentType: "application/json; charset=utf-8",
dataType: "json",
// controller is returning a simple text, not json
complete: function (xhr, status) {
var data = xhr.responseText;
if (status === 'error' || !data) {
//handleError
}
else {
$('#EmployeeInfo').html(data);
}
hideProgress();
}
});
}
function showProgress() {
$('#Progress').css("display", "block");
}
function hideProgress() {
$('#Progress').css("display", "none");
}
</script>
}
</pre></div><br />
<b>توضيحات:</b><br />
توسط متد jQuery.Ajax نيز ميتوان درخواستهاي Ajax ايي خود را به سرور ارسال كرد. در اينجا type نوع http verb مورد نظر را مشخص ميكند كه به POST تنظيم شده است. Url آدرس كنترلر را دريافت ميكند. البته حين استفاده از متد توكار Ajax.ActionLink، اين لينك به صورت خودكار بر اساس تعاريف مسيريابي برنامه تنظيم ميشود. اما در صورت استفاده مستقيم از jQuery.Ajax بايد دقت داشت كه با تغيير تعاريف مسيريابي برنامه نياز است تا اين Url نيز به روز شود.<br />
سه سطر بعدي نوع اطلاعاتي را كه بايد به سرور POST شوند مشخص ميكند. نوع json است و همچنين contentType آن براي ارسال اطلاعات يونيكد ضروري است. از متد <a href="http://www.dotnettips.info/2009/11/jquery-ajax.html">JSON.stringify</a> براي تبديل اشياء به رشته كمك گرفتهايم. اين متد در تمام مرورگرهاي امروزي به صورت توكار پشتيباني ميشود و استفاده از آن سبب خواهد شد تا اطلاعات به نحو صحيحي encode شده و به سرور ارسال شوند. بنابراين اين رشته ارسالي اطلاعات را به صورت دستي تهيه نكنيد؛ چون كاراكترهاي زيادي هستند كه ممكن است مشكل ساز شده و بايد پيش از ارسال به سرور اصطلاحا escape يا encode شوند.<br />
متداول است از پارامتر success براي دريافت نتيجه عمليات متد jQuery.Ajax استفاده شود. اما در اينجا از پارامتر complete آن استفاده شده است. علت هم اينجا است كه return PartialView يك رشته را بر ميگرداند. پارامتر success انتظار دريافت خروجي از نوع json را دارد. به همين جهت در اين مثال خاص بايد از پارامتر complete استفاده كرد تا بتوان به رشته بدون فرمت خروجي بدون مشكل دسترسي پيدا كرد.<br />
به علاوه چون از يك section براي تعريف اسكريپتهاي مورد نياز استفاده كردهايم، براي درج خودكار آن در هدر صفحه بايد قسمت هدر فايل layout برنامه را به صورت زير مقدار دهي كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">@RenderSection("javascript", required: false)
</pre></div><br />
<br />
<br />
<b>دسترسي به اطلاعات يك مدل در View، به كمك jQuery Ajax</b><br />
<br />
اگر جزئي از صفحه كه قرار است به روز شود، پيچيده است، روش استفاده از partial viewها توصيه ميشود؛ براي مثال ميتوان اطلاعات يك مدل را به همراه يك گريد كامل از اطلاعات، رندر كرد و سپس در صفحه درج نمود. اما اگر تنها به اطلاعات چند خاصيت از مدلي نياز داشتيم، ميتوان از روشهايي با سربار كمتر نيز استفاده كرد. براي مثال متد جديد زير را به كنترلر Home اضافه كنيد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">[HttpPost] //for IE-8
public ActionResult EmployeeInfoData(int? id)
{
if (!Request.IsAjaxRequest())
return Json(false);
if (!id.HasValue)
return Json(false);
var list = EmployeeDataSource.CreateEmployees();
var data = list.Where(x => x.Id == id.Value).FirstOrDefault();
if (data == null)
return Json(false);
return Json(data);
}
</pre></div><br />
سپس View برنامه را نيز به نحو زير تغيير دهيد:<br />
<br />
<div align="left" dir="ltr"><pre language="JScript" name="code"><a href="#" onclick="LoadEmployeeInfoData()">Get Employee-2 info</a>
@section javascript
{
<script type="text/javascript">
function LoadEmployeeInfoData() {
showProgress();
$.ajax({
type: "POST",
url: "/Home/EmployeeInfoData",
data: JSON.stringify({ id: 1 }),
contentType: "application/json; charset=utf-8",
dataType: "json",
// controller is returning the json data
success: function (result) {
if (result) {
alert(result.Id + ' - ' + result.Name);
}
hideProgress();
},
error: function (result) {
alert(result.status + ' ' + result.statusText);
hideProgress();
}
});
}
function showProgress() {
$('#Progress').css("display", "block");
}
function hideProgress() {
$('#Progress').css("display", "none");
}
</script>
}
</pre></div><br />
در اين مثال، كنترلر برنامه، اطلاعات مدل را تبديل به Json كرده و بازگشت خواهد داد. سپس ميتوان به اطلاعات اين مدل و خواص آن در View برنامه، در پارامتر success متد jQuery.Ajax، مطابق كدهاي فوق دسترسي يافت. اينبار چون خروجي كنترلر تعريف شده از نوع Json است، امكان استفاده از پارامتر success فراهم شده است. همه چيز هم در اينجا خودكار است؛ تبديل يك شيء به Json و برعكس.<br />
<b>يك نكته:</b> اگر نوع متد كنترلر، HttpGet باشد، نياز خواهد بود تا پارامتر دوم متد بازگشت Json، مساوي JsonRequestBehavior.AllowGet قرار داده شود.<br />
<br />
<br />
<b>ارسال اطلاعات فرمها به سرور، به كمك ويژگيهاي Ajax</b><br />
<br />
متد كمكي توكار ديگري به نام Ajax.BeginForm در ASP.NET MVC وجود دارد كه كار ارسال غيرهمزمان اطلاعات يك فرم را به سرور انجام داده و سپس اطلاعاتي را از سرور دريافت و قسمتي از صفحه را به روز خواهد كرد. مكانيزم كاري كلي آن بسيار شبيه به متد Ajax.ActionLink ميباشد. در ادامه با تكميل مثال قسمت جاري، به بررسي اين ويژگي خواهيم پرداخت.<br />
ابتدا متد جستجوي زير را به كنترلر برنامه اضافه كنيد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">[HttpPost] //for IE-8
public ActionResult SearchEmployeeInfo(string data)
{
if (!Request.IsAjaxRequest())
return Content(string.Empty);
if (string.IsNullOrWhiteSpace(data))
return Content(string.Empty);
var employeesList = EmployeeDataSource.CreateEmployees();
var list = employeesList.Where(x => x.Name.Contains(data)).ToList();
if (list == null || !list.Any())
return Content(string.Empty);
return PartialView(viewName: "_SearchEmployeeInfo", model: list);
}
</pre></div><br />
سپس بر روي نام متد كليك راست كرده و گزينه add view را انتخاب كنيد. در صفحه باز شده، گزينه create a stronlgly typed view را انتخاب كرده و قالب scaffolding را هم بر روي list قرار دهيد. سپس گزينه ايجاد partial view را نيز انتخاب كنيد. نام آنرا هم _SearchEmployeeInfo وارد نمائيد. براي نمونه خروجي حاصل به نحو زير خواهد بود:<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code">@model IEnumerable<MvcApplication18.Models.Employee>
<table>
<tr>
<th>
Name
</th>
</tr>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
</tr>
}
</table>
</pre></div><br />
تا اينجا يك متد جستجو را ايجاد كردهايم كه ميتواند ليستي از ركوردهاي كارمندان را بر اساس قسمتي از نام آنها كه توسط كاربري جستجو شده است، بازگشت دهد. سپس اين اطلاعات را به partial view مورد نظر ارسال كرده و يك جدول را بر اساس آن توليد خواهيم نمود.<br />
اكنون به فايل Index.cshtml مراجعه كرده و فرم Ajax ايي زير را اضافه نمائيد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">@using (Ajax.BeginForm(actionName: "SearchEmployeeInfo",
controllerName: "Home",
ajaxOptions: new AjaxOptions
{
HttpMethod = "POST",
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "EmployeeInfo",
LoadingElementId = "Progress"
}))
{
@Html.TextBox("data")
<input type="submit" value="Search" />
}
</pre></div><br />
اينبار بجاي استفاده از متد Html.BeginForm از متد Ajax.BeginForm استفاده شده است. به كمك آن اطلاعات Html.TextBox تعريف شده، به كنترلر Home و متد SearchEmployeeInfo آن، بر اساس HttpMethod تعريف شده، ارسال گرديده و نتيجه آن در يك div با id مساوي EmployeeInfo درج ميگردد. همچنين اگر اطلاعاتي يافت نشد، به كمك متد return Content يك رشته خالي بازگشت داده ميشود.<br />
متد Ajax.BeginForm نيز از ويژگيهاي data-* براي تعريف اطلاعات مورد نياز ارسالي به سرور استفاده ميكند. بنابراين نياز به سطر الحاق jquery.unobtrusive-ajax.min.js در فايل layout برنامه جهت وفق دادن اين اطلاعات unobtrusive به اطلاعات مورد نياز متد jQuery.Ajax وجود دارد.<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code"><form action="/Home/SearchEmployeeInfo" data-ajax="true"
data-ajax-loading="#Progress" data-ajax-method="POST"
data-ajax-mode="replace" data-ajax-update="#EmployeeInfo"
id="form0" method="post">
<input id="data" name="data" type="text" value="" />
<input type="submit" value="Search" />
</form>
</pre></div><br />
<br />
<b>كتابخانه كمكي «ASP.net MVC Awesome - jQuery Ajax Helpers»</b><br />
علاوه بر متدهاي توكار Ajax همراه با ASP.NET MVC، ساير علاقمندان نيز يك سري Ajax helper را بر اساس افزونههاي jQuery تدارك ديدهاند كه از آدرس زير قابل دريافت هستند:<br />
<a href="http://awesome.codeplex.com/">http://awesome.codeplex.com/</a><br />
<br />
<br />
<b>افزودن فرمها به كمك jQuery.Ajax و فعال سازي اعتبار سنجي سمت كلاينت</b><br />
<br />
در ASP.NET MVC چون ViewState حذف شده است، امكان تزريق فرمهاي جديد به صفحه يا به روز رساني قسمتي از صفحه توسط jQuery Ajax به سهولت و بدون دريافت پيغام «viewstate is corrupted» در حين ارسال اطلاعات به سرور، ميسر است. <br />
در اين حالت بايد به يك نكته مهم نيز دقت داشت: «اعتبار سنجي سمت كلاينت ديگر كار نميكند». علت اينجا است كه در حين بارگذاري متداول يك صفحه، متد زير به صورت خودكار فراخواني ميگردد:<br />
<div align="left" dir="ltr"><pre language="JScript" name="code">$.validator.unobtrusive.parse("#{form-id}");
</pre></div><br />
اما با به روز رساني قسمتي از صفحه، ديگر اينچنين نخواهد بود و نياز است اين فراخواني را دستي انجام دهيم. براي مثال:<br />
<br />
<div align="left" dir="ltr"><pre language="JScript" name="code">$.ajax
({
url: "/{controller}/{action}/{id}",
type: "get",
success: function(data)
{
$.validator.unobtrusive.parse("#{form-id}");
}
});
//or
$.get('/{controller}/{action}/{id}', function (data) { $.validator.unobtrusive.parse("#{form-id}"); });
</pre></div><br />
شبيه به همين مساله را حين استفاده از Ajax.BeginForm نيز بايد مد نظر داشت:<br />
<br />
<div align="left" dir="ltr"><pre language="JScript" name="code">@using (Ajax.BeginForm(
"Action1",
"Controller",
null,
new AjaxOptions {
OnSuccess = "onSuccess",
UpdateTargetId = "result"
},
null)
)
{
<input type="submit" value="Save" />
}
var onSuccess = function(result) {
// enable unobtrusive validation for the contents
// that was injected into the <div id="result"></div> node
$.validator.unobtrusive.parse("#result");
};
</pre></div><br />
در اين مثال در پارامتر UpdateTargetId، مجددا يك فرم رندر ميشود. بنابراين اعتبار سنجي سمت كلاينت آن ديگر كار نخواهد كرد مگر اينكه با مقدار دهي خاصيت OnSuccess، مجددا متد unobtrusive.parse را فراخواني كنيم.<br />
<br />
<br />
</div>وحيد نصيريhttp://www.blogger.com/profile/04454130327051686471noreply@blogger.comtag:blogger.com,1999:blog-7815821915651048578.post-38328066666119443742012-04-22T18:09:00.001+04:302012-04-22T18:12:32.923+04:30ASP.NET MVC #20<div dir="rtl" style="text-align: right;" trbidi="on"><br />
<b>تهيه گزارشات تحت وب به كمك WebGrid</b><br />
<br />
WebGrid از ASP.NET MVC 3.0 به صورت توكار به شكل يك Html Helper در دسترس ميباشد و هدف از آن سادهتر سازي تهيه گزارشات تحت وب است. البته اين گريد، تنها گريد مهياي مخصوص ASP.NET MVC نيست و پروژه MVC Contrib يا شركت Telerik نيز نمونههاي ديگري را ارائه دادهاند؛ اما از اين جهت كه اين Html Helper، بدون نياز به كتابخانههاي جانبي در دسترس است، بررسي آن ضروري ميباشد.<br />
<br />
<br />
<b>صورت مساله</b><br />
<br />
ليستي از كارمندان به همراه حقوق ماهيانه آنها در دست است. اكنون نياز به گزارشي تحت وب، با مشخصات زير ميباشد:<br />
1- گزارش بايد داراي صفحه بندي بوده و هر صفحه تنها 10 رديف را نمايش دهد.<br />
2- سطرها بايد يك در ميان داراي رنگي متفاوت باشند.<br />
3- ستون حقوق كارمندان در پايين هر صفحه، بايد داراي جمع باشد.<br />
4- بتوان با كليك بر روي عنوان هر ستون، اطلاعات را بر اساس ستون انتخابي، مرتب ساخت.<br />
5- لينكهاي حذف يا ويرايش يك رديف نيز در اين گزارش مهيا باشد.<br />
6- ليست تهيه شده، داراي ستوني به نام «رديف» نيست. اين ستون را نيز به صورت خودكار اضافه كنيد.<br />
7- ليست نهايي اطلاعات، داراي ستوني به نام ماليات نيست. فقط حقوق كارمندان ذكر شده است. ستون محاسبه شده ماليات نيز بايد به صورت خودكار در اين گزارش نمايش داده شود. اين ستون نيز بايد داراي جمع پايين هر صفحه باشد.<br />
8- تمام اعداد اين گزارش در حين نمايش بايد داراي جدا كننده سه رقمي باشند.<br />
9- تاريخهاي موجود در ليست، ميلادي هستند. نياز است اين تاريخها در حين نمايش شمسي شوند.<br />
10- انتهاي هر صفحه گزارش بايد بتوان برچسب «صفحه y/n» را مشاهده كرد. n در اينجا منظور تعداد كل صفحات است و y شماره صفحه جاري ميباشد.<br />
11- انتهاي هر صفحه گزارش بايد بتوان برچسب «ركوردهاي y تا x از n» را مشاهده كرد. n در اينجا منظور تعداد كل ركوردها است.<br />
12- نام كوچك هر كارمند، ضخيم نمايش داده شود.<br />
13- به ازاي هر شماره كارمندي، يك تصوير در پوشه images سايت وجود دارد. براي مثال images/id.jpg. ستوني براي نمايش تصوير متناظر با هر كارمند نيز بايد اضافه شود.<br />
14- به ازاي هر كارمند، تعدادي پروژه هم وجود دارد. پروژههاي متناظر را توسط يك گريد تو در تو نمايش دهيد.<br />
<br />
<br />
<b>راه حل به كمك استفاده از WebGrid</b><br />
<br />
ابتدا يك پروژه خالي ASP.NET MVC را آغاز كنيد. سپس مدلهاي زير را به آن اضافه نمائيد (يك كارمند كه ميتواند تعداد پروژه منتسب داشته باشد):<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System;
using System.Collections.Generic;
namespace MvcApplication17.Models
{
public class Employee
{
public int Id { set; get; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime AddDate { get; set; }
public double Salary { get; set; }
public IList<Project> Projects { get; set; }
}
}
</pre></div><br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">namespace MvcApplication17.Models
{
public class Project
{
public int Id { set; get; }
public string Name { set; get; }
}
}
</pre></div><br />
سپس منبع داده نمونه زير را به پروژه اضافه كنيد. به عمد از ORM خاصي استفاده نشده تا بتوانيد پروژه جاري را به سادگي در يك پروژه آزمايشي جديد، تكرار كنيد.<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System;
using System.Collections.Generic;
namespace MvcApplication17.Models
{
public static class EmployeeDataSource
{
public static IList<Employee> CreateEmployees()
{
var list = new List<Employee>();
var rnd = new Random();
for (int i = 1; i <= 1000; i++)
{
list.Add(new Employee
{
Id = i + 1000,
FirstName = "fName " + i,
LastName = "lName " + i,
AddDate = DateTime.Now.AddYears(-rnd.Next(1, 10)),
Salary = rnd.Next(400, 3000),
Projects = CreateRandomProjects()
});
}
return list;
}
private static IList<Project> CreateRandomProjects()
{
var list = new List<Project>();
var rnd = new Random();
for (int i = 0; i < rnd.Next(1, 7); i++)
{
list.Add(new Project
{
Id = i,
Name = "Project " + i
});
}
return list;
}
}
}
</pre></div><br />
<br />
در ادامه يك كنترلر جديد را با محتواي زير اضافه نمائيد:<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Web.Mvc;
using MvcApplication17.Models;
namespace MvcApplication17.Controllers
{
public class HomeController : Controller
{
[HttpPost]
public ActionResult Delete(int? id)
{
return RedirectToAction("Index");
}
[HttpGet]
public ActionResult Edit(int? id)
{
return View();
}
[HttpGet]
public ActionResult Index(string sort, string sortdir, int? page = 1)
{
var list = EmployeeDataSource.CreateEmployees();
return View(list);
}
}
}
</pre></div><br />
علت تعريف متد index با پارامترهاي sort و غيره به URLهاي خودكاري از نوع زير بر ميگردد:<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code">http://localhost:3034/?sort=LastName&sortdir=ASC&page=3
</pre></div><br />
همانطور كه ملاحظه ميكنيد، گريد رندر شده، از يك سري كوئري استرينگ براي مشخص سازي صفحه جاري، يا جهت مرتب سازي (صعودي و نزولي بودن آن) يا فيلد پيش فرض مرتب سازي، كمك ميگيرد.<br />
<br />
سپس يك View خالي را نيز براي متد Index ايجاد كنيد. تا اينجا تنظيمات اوليه پروژه انجام شد.<br />
<b>كدهاي كامل View را در ادامه ملاحظه ميكنيد:</b><br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">@using System.Globalization
@model IList<MvcApplication17.Models.Employee>
@{
ViewBag.Title = "Index";
}
@helper WebGridPageFirstItem(WebGrid grid)
{
@(((grid.PageIndex + 1) * grid.RowsPerPage) - (grid.RowsPerPage - 1));
}
@helper WebGridPageLastItem(WebGrid grid)
{
if (grid.TotalRowCount < (grid.PageIndex + 1 * grid.RowsPerPage))
{
@grid.TotalRowCount;
}
else
{
@((grid.PageIndex + 1) * grid.RowsPerPage);
}
}
<h2>Employees List</h2>
@{
var grid = new WebGrid(
source: Model,
canPage: true,
rowsPerPage: 10,
canSort: true,
defaultSort: "FirstName"
);
var salaryPageSum = 0;
var taxPageSum = 0;
var rowIndex = ((grid.PageIndex + 1) * grid.RowsPerPage) - (grid.RowsPerPage - 1);
}
<div id="container">
@grid.GetHtml(
tableStyle: "webgrid",
headerStyle: "webgrid-header",
footerStyle: "webgrid-footer",
alternatingRowStyle: "webgrid-alternating-row",
selectedRowStyle: "webgrid-selected-row",
rowStyle: "webgrid-row-style",
htmlAttributes: new { id = "MyGrid" },
mode: WebGridPagerModes.All,
columns: grid.Columns(
grid.Column(header: "#",
style: "text-align-center-col",
format: @<text>@(rowIndex++)</text>),
grid.Column(columnName: "FirstName", header: "First Name",
format: @<span style='font-weight: bold'>@item.FirstName</span>,
style: "text-align-center-col"),
grid.Column(columnName: "LastName", header: "Last Name"),
grid.Column(header: "Image",
style: "text-align-center-col",
format: @<text><img alt="@item.Id" src="@Url.Content("~/images/" + @item.Id + ".jpg")" /></text>),
grid.Column(columnName: "AddDate", header: "Start",
style: "text-align-center-col",
format: item =>
{
int ym = item.AddDate.Year;
int mm = item.AddDate.Month;
int dm = item.AddDate.Day;
var persianCalendar = new PersianCalendar();
int ys = persianCalendar.GetYear(new DateTime(ym, mm, dm, new GregorianCalendar()));
int ms = persianCalendar.GetMonth(new DateTime(ym, mm, dm, new GregorianCalendar()));
int ds = persianCalendar.GetDayOfMonth(new DateTime(ym, mm, dm, new GregorianCalendar()));
return ys + "/" + ms.ToString("00") + "/" + ds.ToString("00");
}),
grid.Column(columnName: "Salary", header: "Salary",
format: item =>
{
salaryPageSum += item.Salary;
return string.Format("${0:n0}", item.Salary);
},
style: "text-align-center-col"),
grid.Column(header: "Tax", canSort: true,
format: item =>
{
var tax = item.Salary * 0.2;
taxPageSum += tax;
return string.Format("${0:n0}", tax);
}),
grid.Column(header: "Projects", columnName: "Projects",
style: "text-align-center-col",
format: item =>
{
var subGrid = new WebGrid(
source: item.Projects,
canPage: false,
canSort: false
);
return subGrid.GetHtml(
htmlAttributes: new { id = "MySubGrid" },
tableStyle: "webgrid",
headerStyle: "webgrid-header",
footerStyle: "webgrid-footer",
alternatingRowStyle: "webgrid-alternating-row",
selectedRowStyle: "webgrid-selected-row",
rowStyle: "webgrid-row-style"
);
}),
grid.Column(header: "",
style: "text-align-center-col",
format: item => @Html.ActionLink(linkText: "Edit", actionName: "Edit",
controllerName: "Home", routeValues: new { id = item.Id },
htmlAttributes: null)),
grid.Column(header: "",
format: @<form action="/Home/Delete/@item.Id" method="post"><input type="submit"
onclick="return confirm('Do you want to delete this record?');"
value="Delete"/></form>),
grid.Column(header: "", format: item => item.GetSelectLink("Select"))
)
)
<strong>Page:</strong> @(grid.PageIndex + 1) / @grid.PageCount,
<strong>Records:</strong> @WebGridPageFirstItem(@grid) - @WebGridPageLastItem(@grid) of @grid.TotalRowCount
@*
@if (@grid.HasSelection)
{
@RenderPage("~/views/path/_partial_view.cshtml", new { Employee = grid.SelectedRow })
}
*@
</div>
@section script{
<script type="text/javascript">
$(function () {
$('#MyGrid tbody:first').append(
'<tr class="total-row"><td></td>\
<td></td><td></td><td></td>\
<td><strong>Total:</strong></td>\
<td>@string.Format("${0:n0}", @salaryPageSum)</td>\
<td>@string.Format("${0:n0}", @taxPageSum)</td>\
<td></td><td></td><td></td></tr>');
});
</script>
}
</pre></div><br />
<br />
<b>توضيحات ريز جزئيات View فوق</b><br />
<br />
<br />
<b>تعريف ابتدايي شيء WebGrid و مقدار دهي آن</b><br />
در ابتدا نياز است يك وهله از شيء WebGrid را ايجاد كنيم. در اينجا ميتوان تنظيم كرد كه آيا نياز است اطلاعات نمايش داده شده داراي صفحه بندي (canPage) خودكار باشند؟ منبع داده (source) كدام است. در صورت فعال سازي صفحه بندي خودكار، چه تعداد رديف (rowsPerPage) در هر صفحه نمايش داده شود. آيا نياز است بتوان با كليك بر روي سر ستونها، اطلاعات را بر اساس فيلد متناظر با آن مرتب (canSort) ساخت؟ همچنين در صورت نياز به مرتب سازي، اولين باري كه گريد نمايش داده ميشود، بر اساس چه فيلدي (defaultSort) بايد مرتب شده نمايش داده شود:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">@{
var grid = new WebGrid(
source: Model,
canPage: true,
rowsPerPage: 10,
canSort: true,
defaultSort: "FirstName"
);
var salaryPageSum = 0;
var taxPageSum = 0;
var rowIndex = ((grid.PageIndex + 1) * grid.RowsPerPage) - (grid.RowsPerPage - 1);
}
</pre></div><br />
در اينجا همچنين سه متغير كمكي هم تعريف شده كه از اينها براي تهيه جمع ستونهاي حقوق و ماليات و همچنين نمايش شماره رديف جاري استفاده ميشود. فرمول نحوه محاسبه اولين رديف هر صفحه را هم ملاحظه ميكنيد. شماره رديفهاي بعدي، rowIndex++ خواهند بود.<br />
<br />
<br />
<b>تعريف رنگ و لعاب گريد نمايش داده شده</b><br />
در ادامه به كمك متد grid.GetHtml، رشتهاي معادل اطلاعات HTML صفحه جاري، بازگشت داده ميشود. در اينجا ميتوان يك سري خواص تكميلي را تنظيم نمود. براي مثال:<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">tableStyle: "webgrid",
headerStyle: "webgrid-header",
footerStyle: "webgrid-footer",
alternatingRowStyle: "webgrid-alternating-row",
selectedRowStyle: "webgrid-selected-row",
rowStyle: "webgrid-row-style",
htmlAttributes: new { id = "MyGrid" },
</pre></div><br />
هر كدام از اين رشتهها در حين رندر نهايي گريد، تبديل به يك class خواهند شد. براي نمونه:<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code"><div id="container">
<table class="webgrid" id="MyGrid">
<thead>
<tr class="webgrid-header">
</pre></div><br />
به اين ترتيب با اندكي ويرايش css سايت، ميتوان انواع و اقسام رنگها را به سطرها و ستونهاي گريد نهايي اعمال كرد. براي مثال اطلاعات زير را به فايل css سايت اضافه نمائيد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSS" name="code">/* Styles for WebGrid
-----------------------------------------------------------*/
.webgrid
{
width: 100%;
margin: 0px;
padding: 0px;
border: 0px;
border-collapse: collapse;
font-family: Tahoma;
font-size: 9pt;
}
.webgrid a
{
color: #000;
}
.webgrid-header
{
padding: 0px 5px;
text-align: center;
border-bottom: 2px solid #739ace;
height: 20px;
border-top: 2px solid #D6E8FF;
border-left: 2px solid #D6E8FF;
border-right: 2px solid #D6E8FF;
}
.webgrid-header th
{
background-color: #eaf0ff;
border-right: 1px solid #ddd;
}
.webgrid-footer
{
padding: 6px 5px;
text-align: center;
background-color: #e8eef4;
border-top: 2px solid #3966A2;
height: 25px;
border-bottom: 2px solid #D6E8FF;
border-left: 2px solid #D6E8FF;
border-right: 2px solid #D6E8FF;
}
.webgrid-alternating-row
{
height: 22px;
background-color: #f2f2f2;
border-bottom: 1px solid #d2d2d2;
border-left: 2px solid #D6E8FF;
border-right: 2px solid #D6E8FF;
}
.webgrid-row-style
{
height: 22px;
border-bottom: 1px solid #d2d2d2;
border-left: 2px solid #D6E8FF;
border-right: 2px solid #D6E8FF;
}
.webgrid-selected-row
{
font-weight: bold;
}
.text-align-center-col
{
text-align: center;
}
.total-row
{
background-color:#f9eef4;
}
</pre></div><br />
همانطور كه ملاحظه ميكنيد، رنگهاي رديفها، هدر و فوتر گريد و غيره در اينجا تنظيم ميشوند.<br />
به علاوه اگر دقت كرده باشيد در تعاريف گريد، htmlAttributes هم مقدار دهي شده است. در اينجا به كمك يك anonymously typed object، مقدار id گريد مشخص شده است. از اين id در حين كار با jQuery استفاده خواهيم كرد.<br />
<br />
<br />
<b>تعيين نوع Pager</b><br />
پارامتر ديگري كه در متد grid.GetHtml تنظيم شده است، mode: WebGridPagerModes.All ميباشد. WebGridPagerModes يك enum با محتواي زير است و توسط آن ميتوان نوع Pager گريد را تعيين كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">[Flags]
public enum WebGridPagerModes
{
Numeric = 1,
//
NextPrevious = 2,
//
FirstLast = 4,
//
All = 7,
}
</pre></div><br />
<b>نحوه تعريف ستونهاي گريد</b><br />
اكنون به مهمترين قسمت تهيه گزارش رسيدهايم. در اينجا با مقدار دهي پارامتر columns، نحوه نمايش اطلاعات ستونهاي مختلف مشخص ميگردد. مقداري كه بايد در اينجا تنظيم شود، آرايهاي از نوع WebGridColumn ميباشد و مرسوم است به كمك متد كمكي grid.Columns، اينكار را انجام داد.<br />
متد كمكي grid.Column، يك وهله از شيء WebGridColumn را بر ميگرداند و از آن براي تعريف هر ستون استفاده خواهيم كرد. توسط پارامتر columnName آن، نام فيلدي كه بايد اطلاعات ستون جاري از آن اخذ شود مشخص ميشود. به كمك پارامتر header، عبارت سرستون متناظر تنظيم ميگردد. پارامتر format، مهمترين و توانمندترين پارامتر متد grid.Column است:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">grid.Column(columnName: "FirstName", header: "First Name",
format: @<span style='font-weight: bold'>@item.FirstName</span>,
style: "text-align-center-col"),
grid.Column(columnName: "LastName", header: "Last Name"),
</pre></div><br />
پارامتر format، به نحو زير تعريف شده است:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">Func<dynamic, object> format
</pre></div><br />
به اين معنا كه هر بار پيش از رندر سطر جاري، زمانيكه قرار است سلولي رندر شود، يك شيء dynamic در اختيار شما قرار ميگيرد. اين شيء dynamic يك ركورد از اطلاعات Model جاري است. به اين ترتيب به اطلاعات تمام سلولهاي رديف جاري دسترسي خواهيم داشت. بر اين اساس هر نوع پردازشي را كه لازم بود، انجام دهيد (شبيه به فرمول نويسي در ابزارهاي گزارش سازي، اما اينبار با كدهاي سي شارپ) و مقدار فرمت شده نهايي را به صورت يك رشته بر گردانيد. اين رشته نهايتا در سلول جاري درج خواهد شد.<br />
اگر از پارامتر فرمت استفاده نشود، همان مقدار فيلد جاري بدون تغييري رندر ميگردد.<br />
حداقل به دو نحو ميتوان پارامتر فرمت را مقدار دهي كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">format: @<span style='font-weight: bold'>@item.FirstName</span>
or
format: item =>
{
salaryPageSum += item.Salary;
return string.Format("${0:n0}", item.Salary);
}
</pre></div><br />
مستقيما از توانمنديهاي Razor استفاده كنيد. مثلا يك تگ كامل را بدون نياز به محصور سازي آن بين "" شروع كنيد. سپس @item به وهلهاي از ركورد در دسترس اشاره ميكند كه در اينجا وهلهاي از شيء كارمند است.<br />
و يا همانند روشي كه براي محاسبه جمع حقوق هر صفحه مشاهده ميكنيد، مستقيما از lambda expressions براي تعريف يك anonymous delegate استفاده كنيد.<br />
<br />
<br />
<b>نحوه اضافه كردن ستون رديف</b><br />
ستون رديف، يك ستون محاسبه شده (calculated field) است:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">grid.Column(header: "#",
style: "text-align-center-col",
format: @<text>@(rowIndex++)</text>),
</pre></div><br />
نيازي نيست حتما يك grid.Column، به فيلدي در كلاس كارمند اشاره كند. مقدار سفارشي آن را به كمك پارامتر format تعيين خواهيم كرد. هر بار كه قرار است يك رديف رندر شود، يكبار اين پارامتر فراخواني خواهد شد. فرمول محاسبه rowIndex ابتداي صفحه را نيز پيشتر ملاحظه نموديد.<br />
<br />
<br />
<b>نحوه اضافه كردن ستون سفارشي تصاوير كارمندها</b><br />
ستون تصوير كارمندها نيز مستقيما در كلاس كارمند تعريف نشده است. بنابراين ميتوان آنرا با مقدار دهي صحيح پارامتر format ايجاد كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code">grid.Column(header: "Image",
style: "text-align-center-col",
format: @<text><img alt="@item.Id" src="@Url.Content("~/images/" + @item.Id + ".jpg")" /></text>),
</pre></div><br />
در اين مثال، تصاوير كارمندها در پوشه images واقع در ريشه سايت، قرار دارند. به همين جهت از متد Url.Content براي مقدار دهي صحيح آن استفاده كرديم. به علاوه در اينجا @item.Id به Id ركورد در حال رندر اشاره ميكند.<br />
<br />
<br />
<b>نحوه تبديل تاريخها به تاريخ شمسي</b><br />
در ادامه بازهم به كمك پارامتر format، يك وهله از شيء dynamic اشاره كننده به ركورد در حال رندر را دريافت ميكنيم. سپس فرصت خواهيم داشت تا بر اين اساس، فرمول نويسي كنيم. دست آخر هم رشته مورد نظر نهايي را بازگشت ميدهيم:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">grid.Column(columnName: "AddDate", header: "Start",
style: "text-align-center-col",
format: item =>
{
int ym = item.AddDate.Year;
int mm = item.AddDate.Month;
int dm = item.AddDate.Day;
var persianCalendar = new PersianCalendar();
int ys = persianCalendar.GetYear(new DateTime(ym, mm, dm, new GregorianCalendar()));
int ms = persianCalendar.GetMonth(new DateTime(ym, mm, dm, new GregorianCalendar()));
int ds = persianCalendar.GetDayOfMonth(new DateTime(ym, mm, dm, new GregorianCalendar()));
return ys + "/" + ms.ToString("00") + "/" + ds.ToString("00");
}),
</pre></div><br />
<br />
<b>اضافه كردن ستون سفارشي ماليات</b><br />
در كلاس كارمند، خاصيت حقوق وجود دارد اما ماليات خير. با توجه به آن ميتوانيم به كمك پارامتر format، به اطلاعات شيء dynamic در حال رندر دسترسي داشته باشيم. بنابراين به اطلاعات حقوق دسترسي داريم و سپس با كمي فرمول نويسي، مقدار نهايي مورد نظر را بازگشت خواهيم داد. همچنين در اينجا ميتوان نحوه بازگشت مقدار حقوق را به صورت رشتهاي حاوي جدا كنندههاي سه رقمي نيز مشاهده كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">grid.Column(columnName: "Salary", header: "Salary",
format: item =>
{
salaryPageSum += item.Salary;
return string.Format("${0:n0}", item.Salary);
},
style: "text-align-center-col"),
grid.Column(header: "Tax", canSort: true,
format: item =>
{
var tax = item.Salary * 0.2;
taxPageSum += tax;
return string.Format("${0:n0}", tax);
}),
</pre></div><br />
<br />
<b>اضافه كردن گرديدهاي تو در تو </b><br />
متد Grid.GetHtml، يك رشته را بر ميگرداند. بنابراين در هر چند سطح كه نياز باشد ميتوان يك گريد را بر اساس اطلاعات دردسترس رندر كرد و سپس بازگشت داد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">grid.Column(header: "Projects", columnName: "Projects",
style: "text-align-center-col",
format: item =>
{
var subGrid = new WebGrid(
source: item.Projects,
canPage: false,
canSort: false
);
return subGrid.GetHtml(
htmlAttributes: new { id = "MySubGrid" },
tableStyle: "webgrid",
headerStyle: "webgrid-header",
footerStyle: "webgrid-footer",
alternatingRowStyle: "webgrid-alternating-row",
selectedRowStyle: "webgrid-selected-row",
rowStyle: "webgrid-row-style"
);
}),
</pre></div><br />
در اينجا كار اصلي از طريق پارامتر format شروع ميشود. سپس به كمك item.Projects به ليست پروژههاي هر كارمند دسترسي خواهيم داشت. بر اين اساس يك گريد جديد را توليد كرد و سپس رشته معادل با آن را به كمك متد subGrid.GetHtml دريافت و بازگشت ميدهيم. اين رشته در سلول جاري درج خواهد شد. به نوعي يك گزارش master detail يا sub report را توليد كردهايم.<br />
<br />
<br />
<b>اضافه كردن دكمههاي ويرايش، حذف و انتخاب</b><br />
هر سه دكمه ويرايش، حذف و انتخاب در ستونهايي سفارشي قرار خواهند گرفت. بنابراين مقدار دهي header و format متد grid.Column كفايت ميكند:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">grid.Column(header: "",
style: "text-align-center-col",
format: item => @Html.ActionLink(linkText: "Edit", actionName: "Edit",
controllerName: "Home", routeValues: new { id = item.Id },
htmlAttributes: null)),
grid.Column(header: "",
format: @<form action="/Home/Delete/@item.Id" method="post"><input type="submit"
onclick="return confirm('Do you want to delete this record?');"
value="Delete"/></form>),
grid.Column(header: "", format: item => item.GetSelectLink("Select"))
</pre></div><br />
نكته جديدي كه در اينجا وجود دارد متد item.GetSelectLink ميباشد. اين متد جزو متدهاي توكار گريد است و كار آن بازگشت دادن شيء grid.SelectedRow ميباشد. اين شيء پويا، حاوي اطلاعات ركورد انتخاب شده است. براي مثال اگر نياز باشد اين اطلاعات به صفحهاي ارسال شود، ميتوان از روش زير استفاده كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">@if (@grid.HasSelection)
{
@RenderPage("~/views/path/_partial_view.cshtml", new { Employee = grid.SelectedRow })
}
</pre></div><br />
<b>نمايش برچسبهاي صفحه x از n و ركوردهاي x تا y از z</b><br />
در يك گزارش خوب بايد مشخص باشد كه صفحه جاري، كدامين صفحه از چه تعداد صفحه كلي است. يا ركوردهاي صفحه جاري چه بازهاي از تعداد ركوردهاي كلي را تشكيل ميدهند. براي اين منظور چند متد كمكي به نامهاي WebGridPageFirstItem و WebGridPageLastItem تهيه شدهاند كه آنها را در ابتداي View ارائه شده، مشاهده نموديد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code"><strong>Page:</strong> @(grid.PageIndex + 1) / @grid.PageCount,
<strong>Records:</strong> @WebGridPageFirstItem(@grid) - @WebGridPageLastItem(@grid) of @grid.TotalRowCount
</pre></div><br />
<b>نمايش جمع ستونهاي حقوق و ماليات در هر صفحه</b><br />
گريد توكار همراه با ASP.NET MVC در اين مورد راه حلي را ارائه نميدهد. بنابراين بايد اندكي دست به ابتكار زد. مثلا:<br />
<br />
<div align="left" dir="ltr"><pre language="JScript" name="code">@section script{
<script type="text/javascript">
$(function () {
$('#MyGrid tbody:first').append(
'<tr class="total-row"><td></td>\
<td></td><td></td><td></td>\
<td><strong>Total:</strong></td>\
<td>@string.Format("${0:n0}", @salaryPageSum)</td>\
<td>@string.Format("${0:n0}", @taxPageSum)</td>\
<td></td><td></td><td></td></tr>');
});
</script>
}
</pre></div><br />
در اين مثال به كمك jQuery با توجه به اينكه id گريد ما MyGrid است، يك رديف سفارشي كه همان جمع محاسبه شده است، به tbody جدول نهايي توليدي اضافه ميشود. از tbody:first هم در اينجا استفاده شده است تا رديف اضافه شده به گريدهاي تو در تو اعمال نشود.<br />
سپس فايل Views\Shared\_Layout.cshtml را گشوده و از section تعريف شده، براي مقدار دهي master page سايت، استفاده نمائيد:<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code"><head>
<title>@ViewBag.Title</title>
<link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
<script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
@RenderSection("script", required: false)
</head>
</pre></div><br />
</div>وحيد نصيريhttp://www.blogger.com/profile/04454130327051686471noreply@blogger.comtag:blogger.com,1999:blog-7815821915651048578.post-8099060522939276192012-04-20T14:16:00.000+04:302012-04-20T14:16:07.425+04:30ASP.NET MVC #19<div dir="rtl" style="text-align: right;" trbidi="on"><br />
<b>مروري بر امكانات Caching اطلاعات در ASP.NET MVC</b><br />
<br />
در برنامههاي وب، بالاترين حد كارآيي برنامهها از طريق بهينه سازي الگوريتمها حاصل نميشود، بلكه با بكارگيري امكانات Caching سبب خواهيم شد تا اصلا كدي اجرا نشود. در ASP.NET MVC اين هدف از طريق بكارگيري فيلتري به نام OutputCache ميسر ميگردد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Web.Mvc;
namespace MvcApplication16.Controllers
{
public class HomeController : Controller
{
[OutputCache(Duration = 60, VaryByParam = "none")]
public ActionResult Index()
{
return View();
}
}
}
</pre></div><br />
همانطور كه ملاحظه ميكنيد، OutputCache را به يك اكشن متد يا حتي به يك كنترلر نيز ميتوان اعمال كرد. به اين ترتيب HTML نهايي حاصل از View متناظر با اكشن متد جاري فراخواني شده، Cache خواهد شد. سپس زمانيكه درخواست بعدي به سرور ارسال ميشود، نتيجه دريافت شده، همان اطلاعات Cache شده قبلي است و عملا در سمت سرور كدي اجرا نخواهد شد. در اينجا توسط پارامتر Duration، مدت زمان معتبر بودن كش حاصل، برحسب ثانيه مشخص ميشود. VaryByParam مشخص ميكند كه اگر متدي پارامتري را دريافت ميكند، آيا بايد به ازاي هر مقدار دريافتي، مقادير كش شده متفاوتي ذخيره شوند يا خير. در اينجا چون متد Index پارامتري ندارد، از مقدار none استفاده شده است.<br />
<br />
<br />
<b>مثال يك</b><br />
يك پروژه جديد خالي ASP.NET MVC را آغاز كنيد. سپس كنترلر جديد Home را نيز به آن اضافه نمائيد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System;
using System.Web.Mvc;
namespace MvcApplication16.Controllers
{
public class HomeController : Controller
{
[OutputCache(Duration = 60, VaryByParam = "none")]
public ActionResult Index()
{
ViewBag.ControllerTime = DateTime.Now;
return View();
}
}
}
</pre></div><br />
همچنين كدهاي View متد Index را نيز به نحو زير تغيير دهيد:<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code">@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
<p>@ViewBag.ControllerTime</p>
<p>@DateTime.Now</p>
</pre></div><br />
در اينجا نمايش دو زمان دريافتي از كنترلر و زمان محاسبه شده در View را مشاهده ميكنيد. هدف اين است كه بررسي كنيم آيا فيلتر OutputCache بر روي اين دو مقدار تاثيري دارد يا خير.<br />
برنامه را اجرا نمائيد. سپس چند بار صفحه را Refresh كنيد. مشاهده خواهيد كرد كه هر دو زمان ياد شده تا 60 ثانيه، تغييري نخواهند كرد و حاصل نهايي از Cache خواهنده ميشود.<br />
كاربرد يك چنين حالتي براي مثال نمايش اطلاعات بازديدهاي يك سايت است. نبايد به ازاي هر كاربر وارد شده به سايت، يكبار به بانك اطلاعاتي مراجعه كرد و آمار جديدي را تهيه نمود. يا براي نمونه اگر جايي قرار است اطلاعات وضعيت آب و هوا نمايش داده شود، بهتر است اين اطلاعات، مثلا هر نيم ساعت يكبار به روز شود و نه به ازاي هر بازديد جديد از سايت، توسط صدها بازديد كننده همزمان. يا براي مثال كش كردن خروجي فيد RSS يك بلاگ به مدت چند ساعت نيز ايده خوبي است. از اين لحاظ كه اگر اطلاعات بلاگ شما روزي يكبار به روز ميشود، نيازي نيست تا به ازاي هر برنامه فيدخوان، يكبار اطلاعات از بانك اطلاعاتي دريافت شده و پروسه رندر نهايي فيد صورت گيرد. منوهاي پوياي يك سايت نيز در همين رده قرار ميگيرند. دريافت اطلاعات منوهاي پوياي سايت به ازاي هر درخواست رسيده كاربري جديد، كار اشتباهي است. اين اطلاعات نيز بايد كش شوند تا بار سرور كاهش يابد. البته تمام اينها زماني ميسر خواهند شد كه اطلاعات سمت سرور كش شوند.<br />
<br />
<br />
<b>مثال دو</b><br />
همان مثال قبلي را در اينجا جهت بررسي پارامتر VaryByParam به نحو زير تغيير ميدهيم:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System;
using System.Web.Mvc;
namespace MvcApplication16.Controllers
{
public class HomeController : Controller
{
[OutputCache(Duration = 60, VaryByParam = "none")]
public ActionResult Index(string parameter)
{
ViewBag.Msg = parameter ?? string.Empty;
ViewBag.ControllerTime = DateTime.Now;
return View();
}
}
}
</pre></div><br />
<br />
در اينجا يك پارامتر به متد Index اضافه شده است. مقدار آن به ViewBag.Msg انتساب داده شده و سپس در View ، در بين تگهاي h2 نمايش داده خواهد شد. همچنين يك فرم ساده هم جهت ارسال parameter به متد Index اضافه شده است:<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code">@{
ViewBag.Title = "Index";
}
<h2>@ViewBag.Msg</h2>
<p>@ViewBag.ControllerTime</p>
<p>@DateTime.Now</p>
@using (Html.BeginForm())
{
@Html.TextBox("parameter")
<input type="submit" />
}
</pre></div><br />
اكنون برنامه را اجرا كنيد. در TextBox نمايش داده شده يكبار مثلا بنويسيد Test1 و فرم را به سرور ارسال نمائيد. سپس مقدار Test2 را وارد كرده و ارسال نمائيد. در بار دوم، خروجي صفحه همانند زماني است كه مقدار Test1 ارسال شده است. علت اين است كه مقدار VaryByParam به none تنظيم شده است و صرفنظر از ورودي كاربر، همان اطلاعات كش شده قبلي بازگشت داده خواهد شد. براي رفع اين مشكل، متد Index را به نحو زير تغيير دهيد، به طوريكه مقدار VaryByParam به نام پارامتر متد جاري اشاره كند:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">[OutputCache(Duration = 60, VaryByParam = "parameter")]
public ActionResult Index(string parameter)
</pre></div><br />
در ادامه مجددا برنامه را اجرا كنيد. اكنون يكبار مقدار Test1 را به سرور ارسال كنيد. سپس مقدار Test2 را ارسال نمائيد. مجددا همين دو مرحله را با مقادير Test1 و Test2 تكرار كنيد. مشاهده خواهيد كرد كه اينبار اطلاعات بر اساس مقدار پارامتر ارسالي كش شده است.<br />
<br />
<br />
<br />
<b>تنظيمات متفاوت OutputCache</b><br />
<br />
الف) VaryByParam : اگر مساوي none قرار گيرد، همواره همان مقدار كش شده قبلي نمايش داده ميشود. اگر مقدار آن به نام پارامتر خاصي تنظيم شود، اطلاعات كش شده بر اساس مقادير متفاوت پارامتر دريافتي، متفاوت خواهند بود. در اينجا پارامترهاي متفاوت را با يك «,» ميتوان از هم جدا ساخت. اگر تعداد پارامترها زياد است ميتوان مقدار VaryByParam را مساوي با * قرار داد. در اين حالت به ازاي مقادير متفاوت دريافتي پارامترهاي مختلف، اطلاعات مجزايي در كش قرار خواهد گرفت. اين روش آخر آنچنان توصيه نميشود چون سربار بالايي دارد و حجم بالايي از اطلاعات بر اساس پارامترهاي مختلف، بايد در كش قرار گيرند.<br />
ب) Location : مكان قرارگيري اطلاعات كش شده را مشخص ميكند. مقدار آن نيز بر اساس يك enum به نام OutputCacheLocation مشخص ميگردد. در اين حالت براي مثال ميتوان مكانهاي Server، Client و ServerAndClient را مقدار دهي نمود. مقدار Downstream به معناي كش شدن اطلاعات بر روي پروكسي سرورهاي بين راه و يا مرورگرها است. پيش فرض آن Any است كه تركيبي از Server و Downstream ميباشد.<br />
اگر قرار است اطلاعات يكساني به تمام كاربران نمايش داده شود، مثلا محتواي ليست يك منوي پويا، محل قرارگيري اطلاعات كش بايد سمت سرور باشد. اگر نياز است به ازاي هر كاربر محتواي اطلاعات كش شده متفاوت باشد، بهتر است محل سمت كلاينت را مقدار دهي نمود.<br />
ج) VaryByHeader : اطلاعات، بر اساس هدرهاي مشخص شده، كش ميشوند. براي مثال مرسوم است كه از Accept-Language در اينجا استفاده شود تا اطلاعات مثلا فرانسوي كش شده، به كاربر آلماني تحويل داده نشود.<br />
د) VaryByCustom : در اين حالت نام يك متد استاتيك تعريف شده در فايل global.asax.cs بايد مشخص گردد. توسط اين متد كليد رشتهاي اطلاعاتي كه قرار است كش شود، بازگشت داده خواهد شد.<br />
ه) SqlDependency : در اين حالت اطلاعات تا زمانيكه تغييري در جداول بانك اطلاعاتي SQL Server صورت نگيرد، كش خواهد شد.<br />
و) Nostore : به پروكسي سرورهاي بين راه و همچنين مرورگرها اطلاع ميدهد كه اطلاعات را نبايد كش كنند. اگر قسمت اعتبار سنجي اين سري را به خاطر داشته باشيد، چنين تعريفي در قسمت Remote validation بكارگرفته شد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">[OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
</pre></div><br />
و يا ميتوان براي اينكار يك فيلتر سفارشي را نيز تهيه كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System;
using System.Web.Mvc;
namespace MvcApplication16.Helper
{
/// <summary>
/// Adds "Cache-Control: private, max-age=0" header,
/// ensuring that the responses are not cached by the user's browser.
/// </summary>
public class NoCachingAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
base.OnActionExecuted(filterContext);
filterContext.HttpContext.Response.CacheControl = "private";
filterContext.HttpContext.Response.Cache.SetMaxAge(TimeSpan.FromSeconds(0));
}
}
}
</pre></div><br />
كار اين فيلتر اضافه كردن هدر «Cache-Control: private, max-age=0» به Response است.<br />
<br />
<br />
<b>استفاده از فايل Web.Config براي معرفي تنظيمات Caching</b><br />
<br />
يكي ديگر از تنظيمات ويژگي OutputCache، پارامتر CacheProfile است كه امكان تنظيم آن در فايل web.config نيز وجود دارد. براي نمونه تنظيمات زير را به قسمت system.web فايل وب كانفيگ برنامه اضافه كنيد:<br />
<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code"><system.web>
<caching>
<outputCacheSettings>
<outputCacheProfiles>
<add name="Aggressive" location="ServerAndClient" duration="300"/>
<add name="Mild" duration="100" location="Server" />
</outputCacheProfiles>
</outputCacheSettings>
</caching>
</pre></div><br />
سپس مثلا براي استفاده از پروفايلي به نام Aggressive، خواهيم داشت:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">[OutputCache(CacheProfile = "Aggressive", VaryByParam = "parameter")]
public ActionResult Index(string parameter)
</pre></div><br />
<br />
<b>استفاده از ويژگي به نام donut caching</b><br />
<br />
تا اينجا به اين نتيجه رسيديم كه OutputCache، كل خروجي يك View را بر اساس پارامترهاي مختلفي كه دريافت ميكند، كش خواهد كرد. در اين بين اگر بخواهيم تنها قسمت كوچكي از صفحه كش نشود چه بايد كرد؟ براي حل اين مشكل قابليتي به نام cache substitution كه به donut caching هم معروف است (چون آنرا ميتوان به شكل يك <a href="http://en.wikipedia.org/wiki/Doughnut">donut</a> تصور كرد!) در ASP.NET MVC قابل استفاده است.<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code">@{ Response.WriteSubstitution(ctx => DateTime.Now.ToShortTimeString()); }
</pre></div><br />
همانطور كه ملاحظه ميكنيد براي تعريف يك چنين اطلاعاتي بايد از متد Response.WriteSubstitution در يك view استفاده كرد. در اين مثال، نمايش زمان جاري معرفي شده، صرف نظر از وضعيت كش صفحه جاري، كش نخواهد شد.<br />
<br />
عكس آن هم ممكن است. فرض كنيد كه صفحه جاري شما از سه partial view تشكيل شده است. هر كدام از اين partial viewها نيز مزين به OutpuCache هستند. اما صفحه اصلي درج كننده اطلاعات اين سه partial view فاقد ويژگي Output كش است. در اين حالت تنها اطلاعات اين partial viewها كش خواهند شد و ساير قسمتهاي صفحه با هر بار درخواست از سرور، مجددا بر اساس اطلاعات جديد به روز خواهند شد. حالت توصيه شده نيز همين مورد است و متد Response.WriteSubstitution را صرفا جهت اطلاعات عمومي درنظر داشته باشيد.<br />
<br />
<br />
<b>استفاده از امكانات Data Caching به صورت مستقيم</b><br />
<br />
مطالبي كه تا اينجا عنوان شدند به كش كردن اطلاعات Response اختصاص داشتند. اما امكانات Caching موجود، به اين مورد خلاصه نشده و ميتوان اطلاعات و اشياء را نيز كش كرد. براي مثال اطلاعات «با سطح دسترسي عمومي» دريافتي از بانك اطلاعاتي توسط يك كوئري را نيز ميتوان كش كرد. جهت انجام اينكار ميتوان از متدهاي HttpRuntime.Cache.Insert و يا HttpContext.Cache.Insert استفاده كرد. استفاده از HttpContext.Cache.Insert حين نوشتن Unit tests دردسر كمتري دارد و mocking آن ساده است؛ از اين جهت كه بر اساس HttpContextBase تعريف شدهاست.<br />
در ادامه يك كلاس كمكي نوشتن اطلاعات در cache و سپس بازيابي آنرا ملاحظه ميكنيد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System;
using System.Web;
using System.Web.Caching;
namespace MvcApplication16.Helper
{
public static class CacheManager
{
public static void CacheInsert(this HttpContextBase httpContext, string key, object data, int durationMinutes)
{
if (data == null) return;
httpContext.Cache.Add(
key,
data,
null,
DateTime.Now.AddMinutes(durationMinutes),
TimeSpan.Zero,
CacheItemPriority.AboveNormal,
null);
}
public static T CacheRead<T>(this HttpContextBase httpContext, string key)
{
var data = httpContext.Cache[key];
if (data != null)
return (T)data;
return default(T);
}
public static void InvalidateCache(this HttpContextBase httpContext, string key)
{
httpContext.Cache.Remove(key);
}
}
}
</pre></div><br />
و براي استفاده از آن در يك اكشن متد، ابتدا نياز است فضاي نام اين كلاس تعريف شود و سپس براي نمونه متد HttpContext.CacheInsert در دسترس خواهد بود. HttpContext يكي از خواص تعريف شده در شيء كنترلر است كه با ارث بري كنترلرها از آن، همواره در دسترس ميباشد.<br />
در اينجا براي نمونه اطلاعات يك ليست جنريك دريافتي از بانك اطلاعاتي را مثلا 10 دقيقه (بسته به پارامتر durationMinutes آن) ميتوان كش كرد و سپس توسط متد CacheRead آنرا دريافت نمود. اگر متد CacheRead نال برگرداند به معناي خالي بودن كش است. بنابراين يكبار اطلاعات را از بانك اطلاعاتي دريافت نموده و سپس آنرا كش خواهيم كرديم.<br />
البته هستند ORMهايي كه يك چنين كارهايي را به صورت توكار پشتيباني كنند. به مكانيزم آن، Second level cache هم گفته ميشود؛ به علاوه امكان استفاده از پروايدرهاي ديگري را بجز كش IIS براي ذخيره سازي موقتي اطلاعات نيز فراهم ميكنند.<br />
همچنين بايد دقت داشت اين اعداد مدت زمان، هيچگونه ضمانتي ندارند. اگر IIS احساس كند كه با كمبود منابع مواجه شده است، به سادگي شروع به حذف اطلاعات موجود در كش خواهد كرد.<br />
<br />
<br />
<b>نكته امنيتي مهم!</b><br />
به هيچ عنوان از OutputCache در صفحاتي كه نياز به اعتبار سنجي دارند، استفاده نكنيد و به همين جهت در قسمت كش كردن اطلاعات، بر روي «اطلاعاتي با سطح دسترسي عمومي» تاكيد شد.<br />
فرض كنيد كارمندي به صفحه مشاهده فيش حقوقي خودش مراجعه كرده است. اين ماه هم اضافه حقوق آنچناني داشته است. شما هم اين صفحه را به مدت سه ساعت كش كردهايد. آيا ميتوانيد تصور كنيد اگر همين گزارش كش شده با اين اطلاعات، به ساير كارمندان نمايش داده شود چه قشقرقي به پا خواهد شد؟!<br />
بنابراين هيچگاه اطلاعات مخصوص به يك كاربر اعتبار سنجي شده را كش نكنيد و «تنها» اطلاعاتي نياز به كش شدن دارند كه عمومي باشند. براي مثال ليست آخرين اخبار سايت؛ ليست آخرين مدخلهاي فيد RSS سايت؛ ليست اطلاعات منوي عمومي سايت؛ ليست تعداد كاربران مراجعه كننده به سايت در طول يك روز؛ گزارش آب و هوا و كليه اطلاعاتي با سطح دسترسي عمومي كه كش شدن آنها مشكل ساز نباشد.<br />
به صورت خلاصه هيچگاه در كدهاي شما چنين تعريفي نبايد مشاهده شود:<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">[Authorize]
[OutputCache(Duration = 60)]
public ActionResult Index()
</pre></div><br />
<br />
<br />
<br />
</div>وحيد نصيريhttp://www.blogger.com/profile/04454130327051686471noreply@blogger.comtag:blogger.com,1999:blog-7815821915651048578.post-27330738689557025412012-04-19T10:45:00.002+04:302012-04-19T12:24:42.687+04:30ASP.NET MVC #18<div dir="rtl" style="text-align: right;" trbidi="on"><br />
<b>اعتبار سنجي كاربران در ASP.NET MVC</b><br />
<br />
دو مكانيزم اعتبارسنجي كاربران به صورت توكار در ASP.NET MVC در دسترس هستند: Forms authentication و Windows authentication. <br />
در حالت Forms authentication، برنامه موظف به نمايش فرم لاگين به كاربرها و سپس بررسي اطلاعات وارده توسط آنها است. برخلاف آن، Windows authentication حالت يكپارچه با اعتبار سنجي ويندوز است. براي مثال زمانيكه كاربري به يك دومين ويندوزي وارد ميشود، از همان اطلاعات ورود او به شبكه داخلي، به صورت خودكار و يكپارچه جهت استفاده از برنامه كمك گرفته خواهد شد و بيشترين كاربرد آن در برنامههاي نوشته شده براي اينترانتهاي داخلي شركتها است. به اين ترتيب كاربران يك بار به دومين وارد شده و سپس براي استفاده از برنامههاي مختلف ASP.NET، نيازي به ارائه نام كاربري و كلمه عبور نخواهند داشت. Forms authentication بيشتر براي برنامههايي كه از طريق اينترنت به صورت عمومي و از طريق انواع و اقسام سيستم عاملها قابل دسترسي هستند، توصيه ميشود (و البته منعي هم براي استفاده در حالت اينترانت ندارد).<br />
ضمنا بايد به معناي اين دو كلمه هم دقت داشت: هدف از Authentication اين است كه مشخص گردد هم اكنون چه كاربري به سايت وارد شده است. Authorization، سطح دسترسي كاربر وارد شده به سيستم و اعمالي را كه مجاز است انجام دهد، مشخص ميكند.<br />
<br />
<br />
<b>فيلتر Authorize در ASP.NET MVC</b><br />
<br />
يكي ديگر از فيلترهاي امنيتي ASP.NET MVC به نام Authorize، كار محدود ساختن دسترسي به متدهاي كنترلرها را انجام ميدهد. زمانيكه اكشن متدي به اين فيلتر يا ويژگي مزين ميشود، به اين معنا است كه كاربران اعتبارسنجي نشده، امكان دسترسي به آنرا نخواهند داشت. فيلتر Authorize همواره قبل از تمامي فيلترهاي تعريف شده ديگر اجرا ميشود.<br />
فيلتر Authorize با پياده سازي اينترفيس System.Web.Mvc.IAuthorizationFilter توسط كلاس System.Web.Mvc.AuthorizeAttribute در دسترس ميباشد. اين كلاس علاوه بر پياده سازي اينترفيس ياد شده، داراي دو خاصيت مهم زير نيز ميباشد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">public string Roles { get; set; } // comma-separated list of role names
public string Users { get; set; } // comma-separated list of usernames
</pre></div><br />
زمانيكه فيلتر Authorize به تنهايي بكارگرفته ميشود، هر كاربر اعتبار سنجي شدهاي در سيستم قادر خواهد بود به اكشن متد مورد نظر دسترسي پيدا كند. اما اگر همانند مثال زير، از خواص Roles و يا Users نيز استفاده گردد، تنها كاربران اعتبار سنجي شده مشخصي قادر به دسترسي به يك كنترلر يا متدي در آن خواهند شد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">[Authorize(Roles="Admins")]
public class AdminController : Controller
{
[Authorize(Users="Vahid")]
public ActionResult DoSomethingSecure()
{
}
}
</pre></div><br />
در اين مثال، تنها كاربراني با نقش Admins قادر به دسترسي به كنترلر جاري Admin خواهند بود. همچنين در بين اين كاربران ويژه، تنها كاربري به نام Vahid قادر است متد DoSomethingSecure را فراخواني و اجرا كند.<br />
<br />
اكنون سؤال اينجا است كه فيلتر Authorize چگونه از دو مكانيزم اعتبار سنجي ياد شده استفاده ميكند؟ براي پاسخ به اين سؤال، فايل web.config برنامه را باز نموده و به قسمت authentication آن دقت كنيد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code"><authentication mode="Forms">
<forms loginUrl="~/Account/LogOn" timeout="2880" />
</authentication>
</pre></div><br />
به صورت پيش فرض، برنامههاي ايجاد شده توسط VS.NET جهت استفاده از حالت Forms يا همان Forms authentication تنظيم شدهاند. در اينجا كليه كاربران اعتبار سنجي نشده، به كنترلري به نام Account و متد LogOn در آن هدايت ميشوند.<br />
براي تغيير آن به حالت اعتبار سنجي يكپارچه با ويندوز، فقط كافي است مقدار mode را به Windows تغيير داد و تنظيمات forms آنرا نيز حذف كرد.<br />
<br />
<br />
<b>يك نكته: اعمال تنظيمات اعتبار سنجي اجباري به تمام صفحات سايت</b><br />
تنظيم زير نيز در فايل وب كانفيگ برنامه، همان كار افزودن ويژگي Authorize را انجام ميدهد با اين تفاوت كه تمام صفحات سايت را به صورت خودكار تحت پوشش قرار خواهد داد (البته منهاي loginUrl ايي كه در تنظيمات فوق مشاهده نموديد):<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code"><authorization>
<deny users="?" />
</authorization>
</pre></div><br />
در اين حالت دسترسي به تمام آدرسهاي سايت تحت تاثير قرار ميگيرند، منجمله دسترسي به تصاوير و فايلهاي CSS و غيره. براي اينكه اين موارد را براي مثال در حين نمايش صفحه لاگين نيز نمايش دهيم، بايد تنظيم زير را پيش از تگ system.web به فايل وب كانفيگ برنامه اضافه كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code"><!-- we don't want to stop anyone seeing the css and images -->
<location path="Content">
<system.web>
<authorization>
<allow users="*" />
</authorization>
</system.web>
</location>
</pre></div><br />
در اينجا پوشه Content از سيستم اعتبارسنجي اجباري خارج ميشود و تمام كاربران به آن دسترسي خواهند داشت.<br />
به علاوه امكان امن ساختن تنها قسمتي از سايت نيز ميسر است؛ براي مثال:<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code"><location path="secure">
<system.web>
<authorization>
<allow roles="Administrators" />
<deny users="*" />
</authorization>
</system.web>
</location>
</pre></div><br />
در اينجا مسيري به نام secure، نياز به اعتبارسنجي اجباري دارد. به علاوه تنها كاربراني در نقش Administrators به آن دسترسي خواهند داشت.<br />
<br />
<br />
<b>نكته: به تنظيمات انجام شده در فايل Web.Config دقت داشته باشيد</b><br />
همانطور كه ميشود دسترسي به يك مسير را توسط تگ location بازگذاشت، امكان بستن آن هم فراهم است (بجاي allow از deny استفاده شود). همچنين در ASP.NET MVC به سادگي ميتوان تنظيمات مسيريابي را در فايل global.asax.cs تغيير داد. براي مثال اينبار مسير دسترسي به صفحات امن سايت، Admin خواهد بود نه Secure. در اين حالت چون از فيلتر Authorize استفاده نشده و همچنين فايل web.config نيز تغيير نكرده، اين صفحات بدون محافظت رها خواهند شد. <br />
بنابراين اگر از تگ location براي امن سازي قسمتي از سايت استفاده ميكنيد، حتما بايد پس از تغييرات مسيريابي، فايل web.config را هم به روز كرد تا به مسير جديد اشاره كند.<br />
به همين جهت در ASP.NET MVC بهتر است كه صريحا از فيلتر Authorize بر روي كنترلرها (جهت اعمال به تمام متدهاي آن) يا بر روي متدهاي خاصي از كنترلرها استفاده كرد.<br />
امكان تعريف AuthorizeAttribute در فايل global.asax.cs و متد RegisterGlobalFilters آن به صورت سراسري نيز وجود دارد. اما در اين حالت حتي صفحه لاگين سايت هم ديگر در دسترس نخواهد بود. براي رفع اين مشكل در ASP.NET MVC 4 فيلتر ديگري به نام AllowAnonymousAttribute معرفي شده است تا بتوان قسمتهايي از سايت را مانند صفحه لاگين، از سيستم اعتبارسنجي اجباري خارج كرد تا حداقل كاربر بتواند نام كاربري و كلمه عبور خودش را وارد نمايد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">[System.Web.Mvc.AllowAnonymous]
public ActionResult Login()
{
return View();
}
</pre></div><br />
بنابراين در ASP.NET MVC 4.0، فيلتر AuthorizeAttribute را سراسري تعريف كنيد. سپس در كنترلر لاگين برنامه از فيلتر AllowAnonymous استفاده نمائيد.<br />
البته نوشتن فيلتر سفارشي AllowAnonymousAttribute در ASP.NET MVC 3.0 نيز ميسر است. براي مثال:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">public class LogonAuthorize : AuthorizeAttribute {
public override void OnAuthorization(AuthorizationContext filterContext) {
if (!(filterContext.Controller is AccountController))
base.OnAuthorization(filterContext);
}
}
</pre></div><br />
در اين فيلتر سفارشي، اگر كنترلر جاري از نوع AccountController باشد، از سيستم اعتبار سنجي اجباري خارج خواهد شد. مابقي كنترلرها همانند سابق پردازش ميشوند. به اين معنا كه اكنون ميتوان LogonAuthorize را به صورت يك فيلتر سراسري در فايل global.asax.cs معرفي كرد تا به تمام كنترلرها، منهاي كنترلر Account اعمال شود.<br />
<br />
<br />
<br />
<b>مثالي جهت بررسي حالت Windows Authentication</b><br />
<br />
يك پروژه جديد خالي ASP.NET MVC را آغاز كنيد. سپس يك كنترلر جديد را به نام Home نيز به آن اضافه كنيد. در ادامه متد Index آنرا با ويژگي Authorize، مزين نمائيد. همچنين بر روي نام اين متد كليك راست كرده و يك View خالي را براي آن ايجاد كنيد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Web.Mvc;
namespace MvcApplication15.Controllers
{
public class HomeController : Controller
{
[Authorize]
public ActionResult Index()
{
return View();
}
}
}
</pre></div><br />
محتواي View متناظر با متد Index را هم به شكل زير تغيير دهيد تا نام كاربر وارد شده به سيستم را نمايش دهد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
Current user: @User.Identity.Name
</pre></div><br />
به علاوه در فايل Web.config برنامه، حالت اعتبار سنجي را به ويندوز تغيير دهيد:<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code"><authentication mode="Windows" />
</pre></div><br />
اكنون اگر برنامه را اجرا كنيد و وب سرور آزمايشي انتخابي هم IIS Express باشد، پيغام HTTP Error 401.0 - Unauthorized نمايش داده ميشود. علت هم اينجا است كه Windows Authentication به صورت پيش فرض در اين وب سرور غيرفعال است. براي فعال سازي آن به مسير My Documents\IISExpress\config مراجعه كرده و فايل applicationhost.config را باز نمائيد. تگ windowsAuthentication را يافته و ويژگي enabled آنرا كه false است به true تنظيم نمائيد. اكنون اگر برنامه را مجددا اجرا كنيم، در محل نمايش User.Identity.Name، نام كاربر وارد شده به سيستم نمايش داده خواهد شد.<br />
همانطور كه مشاهده ميكنيد در اينجا همه چيز يكپارچه است و حتي نيازي نيست صفحه لاگين خاصي را به كاربر نمايش داد. همينقدر كه كاربر توانسته به سيستم ويندوزي وارد شود، بر اين اساس هم ميتواند از برنامههاي وب موجود در شبكه استفاده كند.<br />
<br />
<br />
<br />
<b>بررسي حالت Forms Authentication</b><br />
<br />
براي كار با Forms Authentication نياز به محلي براي ذخيره سازي اطلاعات كاربران است. اكثر مقالات را كه مطالعه كنيد شما را به مباحث membership مطرح شده در زمان ASP.NET 2.0 ارجاع ميدهند. اين روش در ASP.NET MVC هم كار ميكند؛ اما الزامي به استفاده از آن نيست.<br />
<br />
براي بررسي حالت اعتبار سنجي مبتني بر فرمها، يك برنامه خالي ASP.NET MVC جديد را آغاز كنيد. يك كنترلر Home ساده را نيز به آن اضافه نمائيد.<br />
سپس نياز است نكته «تنظيمات اعتبار سنجي اجباري تمام صفحات سايت» را به فايل وب كانفيگ برنامه اعمال نمائيد تا نيازي نباشد فيلتر Authorize را در همه جا معرفي كرد. سپس نحوه معرفي پيش فرض Forms authentication تعريف شده در فايل web.config نيز نياز به اندكي اصلاح دارد:<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code"><authentication mode="Forms">
<!--one month ticket-->
<forms name=".403MyApp"
cookieless="UseCookies"
loginUrl="~/Account/LogOn"
defaultUrl="~/Home"
slidingExpiration="true"
protection="All"
path="/"
timeout="43200"/>
</authentication>
</pre></div><br />
در اينجا استفاده از كوكيها اجباري شده است. loginUrl به كنترلر و متد لاگين برنامه اشاره ميكند. defaultUrl مسيري است كه كاربر پس از لاگين به صورت خودكار به آن هدايت خواهد شد. همچنين نكتهي مهم ديگري را كه بايد رعايت كرد، name ايي است كه در اين فايل config عنوان ميكنيد. اگر بر روي يك وب سرور، چندين برنامه وب ASP.Net را در حال اجرا داريد، بايد براي هر كدام از اينها نامي جداگانه و منحصربفرد انتخاب كنيد، در غيراينصورت تداخل رخ داده و گزينه مرا به خاطر بسپار شما كار نخواهد كرد.<br />
كار slidingExpiration كه در اينجا تنظيم شده است نيز به صورت زير ميباشد:<br />
اگر لاگين موفقيت آميزي ساعت 5 عصر صورت گيرد و timeout شما به عدد 10 تنظيم شده باشد، اين لاگين به صورت خودكار در 5:10 منقضي خواهد شد. اما اگر در اين حين در ساعت 5:05 ، كاربر، يكي از صفحات سايت شما را مرور كند، زمان منقضي شدن كوكي ذكر شده به 5:15 تنظيم خواهد شد(مفهوم تنظيم slidingExpiration). لازم به ذكر است كه اگر كاربر پيش از نصف زمان منقضي شدن كوكي (مثلا در 5:04)، يكي از صفحات را مرور كند، تغييري در اين زمان نهايي منقضي شدن رخ نخواهد داد.<br />
اگر timeout ذكر نشود، زمان منقضي شدن كوكي ماندگار (persistent) مساوي زمان جاري + زمان منقضي شدن سشن كاربر كه پيش فرض آن 30 دقيقه است، خواهد بود.<br />
<br />
سپس يك مدل را به نام Account به پوشه مدلهاي برنامه با محتواي زير اضافه نمائيد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.ComponentModel.DataAnnotations;
namespace MvcApplication15.Models
{
public class Account
{
[Required(ErrorMessage = "Username is required to login.")]
[StringLength(20)]
public string Username { get; set; }
[Required(ErrorMessage = "Password is required to login.")]
[DataType(DataType.Password)]
public string Password { get; set; }
public bool RememberMe { get; set; }
}
}
</pre></div><br />
همچنين مطابق تنظيمات اعتبار سنجي مبتني بر فرمهاي فايل وب كانفيگ، نياز به يك AccountController نيز هست:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Web.Mvc;
using MvcApplication15.Models;
namespace MvcApplication15.Controllers
{
public class AccountController : Controller
{
[HttpGet]
public ActionResult LogOn()
{
return View();
}
[HttpPost]
public ActionResult LogOn(Account loginInfo, string returnUrl)
{
return View();
}
}
}
</pre></div><br />
در اينجا در حالت HttpGet فرم لاگين نمايش داده خواهد شد. بنابراين بر روي اين متد كليك راست كرده و گزينه Add view را انتخاب كنيد. سپس در صفحه باز شده گزينه Create a strongly typed view را انتخاب كرده و مدل را هم بر روي كلاس Account قرار دهيد. قالب scaffolding را هم Create انتخاب كنيد. به اين ترتيب فرم لاگين برنامه ساخته خواهد شد.<br />
اگر به متد HttpPost فوق دقت كرده باشيد، علاوه بر دريافت وهلهاي از شيء Account، يك رشته را به نام returnUrl نيز تعريف كرده است. علت هم اينجا است كه سيستم Forms authentication، صفحه بازگشت را به صورت خودكار به شكل يك كوئري استرينگ به انتهاي Url جاري اضافه ميكند. مثلا:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">http://localhost/Account/LogOn?ReturnUrl=something
</pre></div><br />
بنابراين اگر يكي از پارامترهاي متد تعريف شده به نام returnUrl باشد، به صورت خودكار مقدار دهي خواهد شد.<br />
<br />
تا اينجا زمانيكه برنامه را اجرا كنيم، ابتدا بر اساس تعاريف مسيريابي پيش فرض برنامه، آدرس كنترلر Home و متد Index آن فراخواني ميگردد. اما چون در وب كانفيگ برنامه authorization را فعال كردهايم، برنامه به صورت خودكار به آدرس مشخص شده در loginUrl قسمت تعاريف اعتبارسنجي مبتني بر فرمها هدايت خواهد شد. يعني آدرس كنترلر Account و متد LogOn آن درخواست ميگردد. در اين حالت صفحه لاگين نمايان خواهد شد.<br />
<br />
مرحله بعد، اعتبار سنجي اطلاعات وارد شده كاربر است. بنابراين نياز است كنترلر Account را به نحو زير بازنويسي كرد:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System.Web.Mvc;
using System.Web.Security;
using MvcApplication15.Models;
namespace MvcApplication15.Controllers
{
public class AccountController : Controller
{
[HttpGet]
public ActionResult LogOn(string returnUrl)
{
if (User.Identity.IsAuthenticated) //remember me
{
if (shouldRedirect(returnUrl))
{
return Redirect(returnUrl);
}
return Redirect(FormsAuthentication.DefaultUrl);
}
return View(); // show the login page
}
[HttpGet]
public void LogOut()
{
FormsAuthentication.SignOut();
}
private bool shouldRedirect(string returnUrl)
{
// it's a security check
return !string.IsNullOrWhiteSpace(returnUrl) &&
Url.IsLocalUrl(returnUrl) &&
returnUrl.Length > 1 &&
returnUrl.StartsWith("/") &&
!returnUrl.StartsWith("//") &&
!returnUrl.StartsWith("/\\");
}
[HttpPost]
public ActionResult LogOn(Account loginInfo, string returnUrl)
{
if (this.ModelState.IsValid)
{
if (loginInfo.Username == "Vahid" && loginInfo.Password == "123")
{
FormsAuthentication.SetAuthCookie(loginInfo.Username, loginInfo.RememberMe);
if (shouldRedirect(returnUrl))
{
return Redirect(returnUrl);
}
FormsAuthentication.RedirectFromLoginPage(loginInfo.Username, loginInfo.RememberMe);
}
}
this.ModelState.AddModelError("", "The user name or password provided is incorrect.");
ViewBag.Error = "Login faild! Make sure you have entered the right user name and password!";
return View(loginInfo);
}
}
}
</pre></div><br />
در اينجا با توجه به گزينه «مرا به خاطر بسپار»، اگر كاربري پيشتر لاگين كرده و كوكي خودكار حاصل از اعتبار سنجي مبتني بر فرمهاي او نيز معتبر باشد، مقدار User.Identity.IsAuthenticated مساوي true خواهد بود. بنابراين نياز است در متد LogOn از نوع HttpGet به اين مساله دقت داشت و كاربر اعتبار سنجي شده را به صفحه پيشفرض تعيين شده در فايل web.config برنامه يا returnUrl هدايت كرد.<br />
در متد LogOn از نوع HttpPost، كار اعتبارسنجي اطلاعات ارسالي به سرور انجام ميشود. در اينجا فرصت خواهد بود تا اطلاعات دريافتي، با بانك اطلاعاتي مقايسه شوند. اگر اطلاعات مطابقت داشتند، ابتدا كوكي خودكار FormsAuthentication تنظيم شده و سپس به كمك متد RedirectFromLoginPage كاربر را به صفحه پيش فرض سيستم هدايت ميكنيم. يا اگر returnUrl ايي وجود داشت، آنرا پردازش خواهيم كرد.<br />
براي پياده سازي خروج از سيستم هم تنها كافي است متد FormsAuthentication.SignOut فراخواني شود تا تمام اطلاعات سشن و كوكيهاي مرتبط، به صورت خودكار حذف گردند.<br />
<br />
تا اينجا فيلتر Authorize بدون پارامتر و همچنين در حالت مشخص سازي صريح كاربران به نحو زير را پوشش داديم:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">[Authorize(Users="Vahid")]
</pre></div><br />
اما هنوز حالت استفاده از Roles در فيلتر Authorize باقي مانده است. براي فعال سازي خودكار بررسي نقشهاي كاربران نياز است يك Role provider سفارشي را با پياده سازي كلاس RoleProvider، طراحي كنيم. براي مثال:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">using System;
using System.Web.Security;
namespace MvcApplication15.Helper
{
public class CustomRoleProvider : RoleProvider
{
public override bool IsUserInRole(string username, string roleName)
{
if (username.ToLowerInvariant() == "ali" && roleName.ToLowerInvariant() == "user")
return true;
// blabla ...
return false;
}
public override string[] GetRolesForUser(string username)
{
if (username.ToLowerInvariant() == "ali")
{
return new[] { "User", "Helpdesk" };
}
if(username.ToLowerInvariant()=="vahid")
{
return new [] { "Admin" };
}
return new string[] { };
}
public override void AddUsersToRoles(string[] usernames, string[] roleNames)
{
throw new NotImplementedException();
}
public override string ApplicationName
{
get
{
throw new NotImplementedException();
}
set
{
throw new NotImplementedException();
}
}
public override void CreateRole(string roleName)
{
throw new NotImplementedException();
}
public override bool DeleteRole(string roleName, bool throwOnPopulatedRole)
{
throw new NotImplementedException();
}
public override string[] FindUsersInRole(string roleName, string usernameToMatch)
{
throw new NotImplementedException();
}
public override string[] GetAllRoles()
{
throw new NotImplementedException();
}
public override string[] GetUsersInRole(string roleName)
{
throw new NotImplementedException();
}
public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames)
{
throw new NotImplementedException();
}
public override bool RoleExists(string roleName)
{
throw new NotImplementedException();
}
}
}
</pre></div><br />
در اينجا حداقل دو متد IsUserInRole و GetRolesForUser بايد پياده سازي شوند و مابقي اختياري هستند. <br />
بديهي است در يك برنامه واقعي اين اطلاعات بايد از يك بانك اطلاعاتي خوانده شوند؛ براي نمونه به ازاي هر كاربر تعدادي نقش وجود دارد. به ازاي هر نقش نيز تعدادي كاربر تعريف شده است (يك رابطه many-to-many بايد تعريف شود).<br />
در مرحله بعد بايد اين Role provider سفارشي را در فايل وب كانفيگ برنامه در قسمت system.web آن تعريف و ثبت كنيم:<br />
<br />
<div align="left" dir="ltr"><pre language="XML" name="code"><roleManager>
<providers>
<clear />
<add name="CustomRoleProvider" type="MvcApplication15.Helper.CustomRoleProvider"/>
</providers>
</roleManager>
</pre></div><br />
<br />
همين مقدار براي راه اندازي بررسي نقشها در ASP.NET MVC كفايت ميكند. اكنون امكان تعريف نقشها، حين بكارگيري فيلتر Authorize ميسر است:<br />
<br />
<div align="left" dir="ltr"><pre language="CSharp" name="code">[Authorize(Roles = "Admin")]
public class HomeController : Controller
</pre></div><br />
<br />
<br />
</div>وحيد نصيريhttp://www.blogger.com/profile/04454130327051686471noreply@blogger.com