public class Order { public int OrderId { get; set; } public virtual Quotation Quotation { get; set; } } public class Quotation { public int QuotationId { get; set; } public virtual Order Order { get; set; } }
public class AnchorTagHelper : TagHelper { /// <summary> /// The name of the action method. /// </summary> [HtmlAttributeName ("asp-action")] public string Action { get; set; } /// <summary> /// The name of the controller. /// </summary> [HtmlAttributeName ("asp-controller")] public string Controller { get; set; } /// <summary> /// The name of the area. /// </summary> [HtmlAttributeName ("asp-area")] public string Area { get; set; } [HtmlAttributeName ("asp-route")] public string Route { get; set; } // Can be async Task public override void Process (TagHelperContext context, TagHelperOutput output) { output.TagName = "a"; string result = string.Empty; if (!string.IsNullOrWhiteSpace (Area)) { result += "/" + Area; } if (!string.IsNullOrWhiteSpace (Controller)) { result += "/" + Controller; } if (!string.IsNullOrWhiteSpace (Action)) { result += "/" + Action; } if (!string.IsNullOrWhiteSpace (Route)) { Route = ToFriendlyHref (Route); result += "/" + Route; } output.Attributes.SetAttribute ("href", result.ToLowerInvariant ()); //output.Content.SetContent (currentAttribute.ToString ()); } private string ToFriendlyHref (object value) { string text = value.ToString (); List<char> illegalChars = new List<char> () { ' ', '.', '#', '%', '&', '*', '{', '}', '\\', ':', '<', '>', '?', ';', '@', '=', '+', '$', ',' }; illegalChars.ForEach (c => { text = text.Replace (c.ToString (), "-"); }); return text; } }
public static string GetFileName(int code) { switch (code) { case 0: return "/Images/User/Weather/Tornado.png"; //... }
public class YahooWeatherRssItem { public int Code { get; set; } //... public string FileName { get { return Util.GetFileName(Code); } } }
ASP.NET MVC #7
آشنایی با Razor Views
قبل از اینکه بحث جاری ASP.NET MVC را بتوانیم ادامه دهیم و مثلا مباحث دریافت اطلاعات از کاربر، کار با فرمها و امثال آنرا بررسی کنیم، نیاز است حداقل به دستور زبان یکی از View Engineهای ASP.NET MVC آشنا باشیم.
MVC3 موتور View جدیدی را به نام Razor معرفی کرده است که به عنوان روش برگزیده ایجاد Viewها در این سیستم به شمار میرود و فوق العاده نسبت به ASPX view engine سابق، زیباتر، سادهتر و فشردهتر طراحی شده است و یکی از اهداف آن تلفیق code و markup میباشد. در این حالت دیگر پسوند فایلهای Viewها همانند سابق ASPX نخواهد بود و به cshtml و یا vbhtml تغییر یافته است. همچنین برخلاف web forms view engine از System.Web.Page مشتق نشده است. و باید دقت داشت که Razor یک زبان برنامه نویسی جدید نیست. در اینجا از مخلوط زبانهای سی شارپ و یا ویژوال بیسیک به همراه تگهای html استفاده میشود.
البته این را هم باید عنوان کرد که این مسایل سلیقهای است. اگر با web forms view engine راحت هستید، با همان کار کنید. اگر با هیچکدام از اینها راحت نیستید (!) نمونههای دیگر هم وجود دارند، مثلا:
Razor Views یک سری قابلیت جالب را هم به همراه دارند:
1) امکان کامپایل آنها به درون یک DLL وجود دارد. مزیت: استفاده مجدد از کد، عدم نیاز به وجود صریح فایل cshtml یا vbhtml بر روی دیسک سخت.
2) آزمون پذیری: از آنجائیکه Razor viewها به صورت یک کلاس کامپایل میشوند و همچنین از System.Web.Page مشتق نخواهند شد، امکان بررسی HTML نهایی تولیدی آنهابدون نیاز به راه اندازی یک وب سرور وجود دارد.
3) IntelliSense ویژوال استودیو به خوبی آنرا پوشش میدهد.
4) با توجه به مواردی که ذکر شد، یک اتفاق جالب هم رخ داده است: امکان استفاده از Razor engine خارج از ASP.NET MVC هم وجود دارد. برای مثال یک سرویس ویندوز NT طراحی کردهاید که قرار است ایمیل فرمت شدهای به همراه اطلاعات مدلهای شما را در فواصل زمانی مشخص ارسال کند؟ میتوانید برای طراحی آن از Razor engine استفاده کنید و تهیه خروجی نهایی HTML آن نیازی به راه اندازی وب سرور و وهله سازی HttpContext ندارد.
ساختار پروژه مثال جاری
در ادامه مرور سریعی خواهیم داشت بر دستور زبان Razor engine و جهت نمایش این قابلیتها، یک مثال ساده را در ابتدا با مشخصات زیر ایجاد خواهیم کرد:
الف) یک empty ASP.NET MVC 3 project را ایجاد کنید و نوع View engine را هم در ابتدای کار Razor انتخاب نمائید.
ب) دو کلاس زیر را به پوشه مدلهای برنامه اضافه کنید:
namespace MvcApplication3.Models
{
public class Product
{
public Product(string productNumber, string name, decimal price)
{
Name = name;
Price = price;
ProductNumber = productNumber;
}
public string ProductNumber { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
}
using System.Collections.Generic;
namespace MvcApplication3.Models
{
public class Products : List<Product>
{
public Products()
{
this.Add(new Product("D123", "Super Fast Bike", 1000M));
this.Add(new Product("A356", "Durable Helmet", 123.45M));
this.Add(new Product("M924", "Soft Bike Seat", 34.99M));
}
}
}
کلاس Products صرفا یک منبع داده تشکیل شده در حافظه است. بدیهی است هر نوع ORM ایی که یک ToList را بتواند در اختیار شما قرار دهد، توانایی تشکیل لیست جنریکی از محصولات را نیز خواهد داشت و تفاوتی نمیکند که کدامیک مورد استفاده قرار گیرد.
ج) سپس یک کنترلر جدید به نام ProductsController را به پوشه Controllers برنامه اضافه میکنیم:
using System.Web.Mvc;
using MvcApplication3.Models;
namespace MvcApplication3.Controllers
{
public class ProductsController : Controller
{
public ActionResult Index()
{
var products = new Products();
return View(products);
}
}
}
د) بر روی نام متد Index کلیک راست کرده، گزینه Add view را جهت افزودن View متناظر آن، انتخاب کنید. البته میشود همانند قسمت پنجم گزینه Create a strongly typed view را انتخاب کرد و سپس Product را به عنوان کلاس مدل انتخاب نمود و در آخر خیلی سریع یک لیست از محصولات را نمایش داد، اما فعلا از این قسمت صرفنظر نمائید، چون میخواهیم آن را دستی ایجاد کرده و توضیحات و نکات بیشتری را بررسی کنیم.
ه) برای اینکه حین اجرای برنامه در VS.NET هربار نخواهیم که آدرس کنترلر Products را دستی در مرورگر وارد کنیم، فایل Global.asax.cs را گشوده و سپس در متد RegisterRoutes، در سطر Parameter defaults، مقدار پیش فرض کنترلر را مساوی Products قرار دهید.
مرجع سریع Razor
ابتدا کدهای View متد Index را به شکل زیر وارد نمائید:
@model List<MvcApplication3.Models.Product>
@{
ViewBag.Title = "Index";
var number = 12;
var data = "some text...";
<h2>line1: @data</h2>
@:line-2: @data <br />
<text>line-3:</text> @data
}
<br />
site@(data)
<br />
@@name
<br />
@(number/10)
<br />
First product: @Model.First().Name
<br />
@if (@number>10)
{
<span>@data</span>
}
else
{
<text>Plain Text</text>
}
<br />
@foreach (var item in Model)
{
<li>@item.Name, $@item.Price </li>
}
@*
A Razor Comment
*@
<br />
@("First product: " + Model.First().Name)
<br />
<img src="@(number).jpg" />
در ادامه توضیحات مرتبط با این کدها ارائه خواهد شد:
1) نحوه معرفی یک قطعه کد
@model List<MvcApplication3.Models.Product>
@{
ViewBag.Title = "Index";
var number = 12;
var data = "some text...";
<h2>line1: @data</h2>
@:line-2: @data <br />
<text>line-3:</text> @data
}
این کدها متعلق به Viewایی است که در قسمت (د) بررسی ساختار پروژه مثال جاری، ایجاد کردیم. در ابتدای آن هم نوع model مشخص شده تا بتوان سادهتر به اطلاعات شیء Model به کمک IntelliSense دسترسی داشت.
برای ایجاد یک قطعه کد در Viewایی از نوع Razor به این نحو عمل میشود:
@{ ...Code Block.... }
در اینجا مجاز هستیم کدهای سی شارپ را وارد کنیم. یک نکته جالب را هم باید درنظر داشت: امکان نوشتن تگهای html هم در این بین وجود دارد (بدون اینکه مجبور باشیم قطعه کد شروع شده را خاتمه دهیم، به حالت html معمولی سوئیچ کرده و دوباره یک قطعه کد دیگر را شروع نمائیم). مانند line1 مثال فوق. اگر کمی پایینتر از این سطر مثلا بنویسیم line2 (به عنوان یک برچسب) کامپایلر ایراد خواهد گرفت، زیرا این مورد نه متغیر است و نه از پیش تعریف شده است. به عبارتی نباید فراموش کنیم که اینجا قرار است کد نوشته شود. برای رفع این مشکل دو راه حل وجود دارد که در سطرهای دو و سه ملاحظه میکنید. یا باید از تگی به نام text برای معرفی یک برچسب در این میان استفاده کرد (سطر سه) یا اگر قرار است اطلاعاتی به شکل یک متن معمولی پردازش شود ابتدای آن مانند سطر دوم باید یک @: قرار گیرد.
کمی پایینتر از قطعه کد معرفی شده در بالا بنویسید:
<br />
site@data
اکنون اگر خروجی این View را در مرورگر بررسی کنید، دقیقا همین site@data خواهد بود. چون در این حالت Razor تصور خواهد کرد که قصد داشتهاید یک آدرس ایمیل را وارد کنید. برای این حالت خاص باید نوشت:
<br />
site@(data)
به این ترتیب data از متغیر data تعریف شده در code block قبلی برنامه دریافت و نمایش داده خواهد شد.
شبیه به همین حالت در مثال زیر هم وجود دارد:
<img src="@(number).jpg" />
در اینجا اگر پرانتزها را حذف کنیم، Razor فرض را بر این خواهد گذاشت که شیء number دارای خاصیت jpg است. بنابراین باید به نحو صریحی، بازه کاری را مشخص نمائیم.
بکار گیری این علامت @ یک نکته جنبی دیگر را هم به همراه دارد. فرض کنید در صفحه قصد دارید آدرس توئیتری شخصی را وارد کنید. مثلا:
<br />
@name
در این حالت View کامپایل نخواهد شد و Razor تصور خواهد کرد که قرار است اطلاعات متغیری به نام name را نمایش دهید. برای نمایش این اطلاعات به همین شکل، یک @ دیگر به ابتدای سطر اضافه کنید:
<br />
@@name
2) نحوه معرفی عبارات
عبارات پس از علامت @ معرفی میشوند و به صورت پیش فرض Html Encoded هستند (در قسمت 5 در اینباره بیشتر توضیح داده شد):
First product: @Model.First().Name
در این مثال با توجه به اینکه نوع مدل در ابتدای View مشخص شده است، شیء Model به لیستی از Products اشاره میکند.
یک نکته:
مشخص سازی حد و مرز صریح یک متغیر در مثال زیر نیز کاربرد دارد:
<br />
@number/10
اگر خروجی این مثال را بررسی کنید مساوی 12/10 خواهد بود و محاسبهای انجام نخواهد شد. برای حل این مشکل باز هم از پرانتز میتوان کمک گرفت:
<br />
@(number/10)
@if (@number>10)
{
<span>@data</span>
}
else
{
<text>Plain Text</text>
}
یک عبارت شرطی در اینجا با @if شروع میشود و سپس نکاتی که در «نحوه معرفی یک قطعه کد» بیان شد، در مورد عبارات داخل {} صادق خواهد بود. یعنی در اینجا نیز میتوان عبارات سی شارپ مخلوط با تگهای html را نوشت.
یک نکته: عبارت شرطی زیر نادرست است. حتما باید سطرهای کدهای سی شارپ بین {} محصور شوند؛ حتی اگر یک سطر باشند:
@if( i < 1 ) int myVar=0;
4) نحوه استفاده از حلقه foreach
@foreach (var item in Model)
{
<li>@item.Name, $@item.Price </li>
}
حلقه foreach نیز مانند عبارات شرطی با یک @ شروع شده و داخل {} بدنه آن نکات «نحوه معرفی یک قطعه کد» برقرار هستند (امکان تلفیق code و markup با هم).
کسانی که پیشتر با web forms کار کرده باشند، احتمالا الان خواهند گفت که این یک پس رفت است و بازگشت به دوران ASP کلاسیک دهه نود! ما به ندرت داخل صفحات aspx وب فرمها کد مینوشتیم. مثلا پیشتر یک GridView وجود داشت و یک دیتاسورس که به آن متصل میشد؛ مابقی خودکار بود و ما هیچ وقت حلقهای ننوشتیم. در اینجا هم این مساله با نوشتن برای مثال «html helpers» قابل کنترل است که در قسمتهای بعدی به آن پرداخته خواهد شد. به عبارتی قرار نیست به این نحو با Viewهای Razor رفتار کنیم. این قسمت فقط یک آشنایی کلی با Syntax است.
5) امکان تعریف فضای نام در ابتدای View
@using namespace;
6) نحوه نوشتن توضیحات سمت سرور:
@*
A Razor Comment / Server side Comment
*@
7) نحوه معرفی عبارات چند جزئی:
@("First product: " + Model.First().Name)
همانطور که ملاحظه میکنید، ذکر یک پرانتز برای معرفی عبارات چندجزئی کفایت میکند.
استفاده از موتور Razor خارج از ASP.NET MVC
پیشتر مطلبی را در مورد «تهیه قالب برای ایمیلهای ارسالی یک برنامه ASP.Net» در این سایت مطالعه کردهاید. اولین سؤالی هم که در ذیل آن مطلب مطرح شده این است: «در برنامههای ویندوز چطور؟» پاسخ این است که کل آن مثال بر مبنای HttpContext.Current.Server.Execute کار میکند. یعنی باید مراحل وهله سازی HttpContext و شیء Server توسط یک وب سرور و درخواست رسیده طی شود و ... شبیه سازی آن آنچنان مرسوم و کار سادهای نیست.
اما این مشکل با Razor وجود ندارد. به عبارتی در اینجا برای رندر کردن یک Razor View به html نهایی، نیازی به HttpContext نیست. بنابراین از این امکانات مثلا در یک سرویس ویندوز ان تی یا یک برنامه کنسول، WinForms، WPF و غیره هم میتوان استفاده کرد.
برای اینکه بتوان از Razor خارج از ASP.NET MVC استفاده کرد، نیاز به اندکی کدنویسی هست مثلا استفاده از کامپایلر سی شارپ یا وی بی و کامپایل پویای کد و یک سری ست آپ دیگر. پروژهای به نام RazorEngine این کپسوله سازی رو انجام داده و از اینجا http://razorengine.codeplex.com/ قابل دریافت است.
در حال حاضر این کتابخانه از مفاهیم زیر پشتیبانی میکند:
- تبدیل کلاس به جدول با پشتیبانی از خصوصیت Table
- تبدیل پراپرتیها به ستون با پشتیبانی از خصوصیت هایی چون Column,Key,MaxLength,Required,Notmapped,DatabaseGenerated,Index
- پشتیبانی از primarykey و کلیدهای ترکیبی
- کلید خارجی و روابط یک به چند و پشتیبانی از cascade on delete
- فیلد غیر نال
برای شروع ابتدا کتابخانه مورد نظر را از Nuget با دستور زیر دریافت کنید:
Install-Package SQLite.CodeFirst
solution من شامل سه پروژه است یکی برای مدلها که شامل کلاسهای زیر برای تهیه یک دفترچه تلفن ساده است:
Person
public class Person { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public virtual ICollection<PhoneBook> Numbers { get; set; } }
PhoneBook
public class PhoneBook { public int Id { get; set; } public string Field{ get; set; } public string Number { get; set; } public virtual Person Person { get; set; } }
پروژه بعدی به نام سرویس که جهت پیاده سازی کلاسهای EF است و دیگری هم یک پروژهی WPF جهت تست برنامه.
در پروژهی سرویس ما یک کلاس به نام Context داریم که مفاهیم مربوط به پیاده سازی Context در آن انجام شده است:
public class Context:DbContext { public Context():base("constr") { } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Conventions.Remove<PluralizingTableNameConvention>(); var initializer = new InitialDb(modelBuilder); Database.SetInitializer(initializer); } public DbSet<PhoneBook> PhoneBook { get; set; } public DbSet<Person> Persons { get; set; } }
اول اینکه در سطر اول متد بازنویسی شده onModelCreating، قرارداد مربوط به نامگذاری جداول را حذف میکنیم چرا که در صورت نبودن این خط، اسامی که کلاس sqllite برای آن در نظر خواهد گرفت با اسامی که برای انجام عملیات CURD استفاده میشوند متفاوت خواهد بود. برای مثال برای Person جدولی به اسم People خواهد ساخت ولی برای درج، آن را در جدول Person انجام میدهد که به خاطر نبودن جدول با خطای چنین جدولی موجود نیست روبرو میشویم.
نکتهی دوم اینکه در همین کلاس Context ما یک پیاده سازی جدید بر روی کلاس InitialDb داشته ایم که در زیر نمونه کد آن را میبینید:
public class InitialDb:SQLite.CodeFirst.SqliteCreateDatabaseIfNotExists<Context> { public InitialDb(DbModelBuilder modelBuilder) : base(modelBuilder) { } protected override void Seed(Context context) { var person = new Person() { FirstName = "ali", LastName = "yeganeh", Numbers = new List<PhoneBook>() { new PhoneBook() { Field = "Work", Number = "031551234" }, new PhoneBook() { Field = "Mobile", Number = "09123456789" }, new PhoneBook() { Field = "Home", Number = "031554321" } } }; context.Persons.Add(person); base.Seed(context); } }
سپس در پروژهی اصلی WPF در فایل AppConfig رشته اتصالی مورد نظر را وارد نمایید:
<connectionStrings> <add name="constr" connectionString="data source=.\phonebook.sqlite;foreign keys=true" providerName="System.Data.SQLite" /> </connectionStrings>
خطا:
The ADO.NET provider with invariant name 'System.Data.SQLite' is either not registered in the machine or application config file, or could not be loaded
قسمتی از فایل app.config:
<entityFramework> <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework"> <parameters> <parameter value="mssqllocaldb" /> </parameters> </defaultConnectionFactory> <providers> <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" /> <provider invariantName="System.Data.SQLite" type="System.Data.SQLite.EF6.SQLiteProviderServices, System.Data.SQLite.EF6" /> </providers> </entityFramework> <system.data> <DbProviderFactories> <remove invariant="System.Data.SQLite.EF6" /> <remove invariant="System.Data.SQLite" /> <add name="SQLite Data Provider" invariant="System.Data.SQLite" description=".Net Framework Data Provider for SQLite" type="System.Data.SQLite.SQLiteFactory, System.Data.SQLite" /> </DbProviderFactories> </system.data>
کد Load پروژه WPF:
public MainWindow() { InitializeComponent(); var context=new Context(); var list= context.Persons.ToList(); var s = ""; foreach (var person in list) { s += person.FirstName + " " + person.LastName + " has these numbers:" + Environment.NewLine; foreach (var number in person.Numbers) { s += number.Field + " : " + number.Number + Environment.NewLine; } s += Environment.NewLine; } MessageBox.Show(s); }
دانلود مثال
public class CmsUser : IdentityUser { public string DisplayName { get; set; } }
public class Post { private IList<string> _tags = new List<string>(); public int id { get; set; } public string name { get; set; } public string slug { get; set; } public string description { get; set; } public DateTime? publishTime { get; set; } public string content { get; set; } public IList<string> tags { get { return _tags; } set { _tags = value; } } public string CombineTags { get { return string.Join(",", _tags); } set { _tags = value.Split(',').Select(x => x.Trim()).ToList(); } } public string AuthorID { get; set; } // [ForeignKey("AuthorID")] // public CmsUser Author { get; set; } }
مقدمه
ایجاد کلاس لاگ
public class DBLog { public int DBLogId { get; set; } public string? LogLevel { get; set; } public string? EventName { get; set; } public string? Message { get; set; } public string? StackTrace { get; set; } public DateTime CreatedDate { get; set; }=DateTime.Now; }
ایجاد دیتابیس لاگر
public class DBLogger:ILogger { private bool _isDisposed; private readonly ApplicationDbContext _dbContext; public DBLogger(ApplicationDbContext dbContext) { _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); } public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) { var dblLogItem = new DBLog() { EventName = eventId.Name, LogLevel = logLevel.ToString(), Message = exception?.Message, StackTrace=exception?.StackTrace }; _dbContext.DBLogs.Add(dblLogItem); _dbContext.SaveChanges(); } public bool IsEnabled(LogLevel logLevel) { return true; } public IDisposable BeginScope<TState>(TState state) { return null; } }
ایجاد یک لاگ پروایدر سفارشی
حال باید یک لاگ پروایدر سفارشی را ایجاد کنیم تا بتوان یک نمونه از دیتابیس لاگر سفارشی بالا (DBLogger) را ایجاد کرد.
public class DbLoggerProvider:ILoggerProvider { private bool _isDisposed; private readonly ApplicationDbContext _dbContext; public DbLoggerProvider(ApplicationDbContext dbContext) { _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); } public ILogger CreateLogger(string categoryName) { return new DBLogger(_dbContext); } public void Dispose() { } }
همانطور که ملاحظه مینمایید، این لاگ پروایدر، از اینترفیس ILoggerProvider ارث بری کردهاست که دارای متد CreateLogger میباشد ئ این متد با شروع برنامه، یک نمونه از دیتابیس لاگر سفارشی ما را ایجاد میکند. در سازندهی این کلاس، DatabaseContext را مقدار دهی نمودهایم تا آنرا به کلاس DBLogger ارسال نماییم.
در انتها کافیست در کلاس Startup.cs این لاگ پروایدر سفارشی (DbLoggerProvider ) را صدا بزنیم.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) { . . . #region CustomLogProvider var serviceProvider = app.ApplicationServices.CreateScope().ServiceProvider; var appDbContext = serviceProvider.GetRequiredService<ApplicationDbContext>(); loggerFactory.AddProvider(new DbLoggerProvider(appDbContext)); #endregion . . .
مشکل!
public class DbLoggerProvider:ILoggerProvider { private readonly CancellationTokenSource _cancellationTokenSource = new(); private readonly IList<DBLog> _currentBatch = new List<DBLog>(); private readonly TimeSpan _interval = TimeSpan.FromSeconds(2); private readonly BlockingCollection<DBLog> _messageQueue = new(new ConcurrentQueue<DBLog>()); private readonly Task _outputTask; private readonly ApplicationDbContext _dbContext; private bool _isDisposed; public DbLoggerProvider(ApplicationDbContext dbContext) { _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); _outputTask = Task.Run(ProcessLogQueue); } public ILogger CreateLogger(string categoryName) { return new DBLogger(this,categoryName); } private async Task ProcessLogQueue() { while (!_cancellationTokenSource.IsCancellationRequested) { while (_messageQueue.TryTake(out var message)) { try { _currentBatch.Add(message); } catch { //cancellation token canceled or CompleteAdding called } } await SaveLogItemsAsync(_currentBatch, _cancellationTokenSource.Token); _currentBatch.Clear(); await Task.Delay(_interval, _cancellationTokenSource.Token); } } internal void AddLogItem(DBLog appLogItem) { if (!_messageQueue.IsAddingCompleted) { _messageQueue.Add(appLogItem, _cancellationTokenSource.Token); } } private async Task SaveLogItemsAsync(IList<DBLog> items, CancellationToken cancellationToken) { try { if (!items.Any()) { return; } // We need a separate context for the logger to call its SaveChanges several times, // without using the current request's context and changing its internal state. foreach (var item in items) { var addedEntry = _dbContext.DbLogs.Add(item); } await _dbContext.SaveChangesAsync(cancellationToken); } catch { // don't throw exceptions from logger } } [SuppressMessage("Microsoft.Usage", "CA1031:catch a more specific allowed exception type, or rethrow the exception", Justification = "don't throw exceptions from logger")] private void Stop() { _cancellationTokenSource.Cancel(); _messageQueue.CompleteAdding(); try { _outputTask.Wait(_interval); } catch { // don't throw exceptions from logger } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (!_isDisposed) { try { if (disposing) { Stop(); _messageQueue.Dispose(); _cancellationTokenSource.Dispose(); _dbContext.Dispose(); } } finally { _isDisposed = true; } } } }
public class DBLogger:ILogger { private readonly LogLevel _minLevel; private readonly DbLoggerProvider _loggerProvider; private readonly string _categoryName; public DBLogger( DbLoggerProvider loggerProvider, string categoryName ) { _loggerProvider= loggerProvider ?? throw new ArgumentNullException(nameof(loggerProvider)); _categoryName= categoryName; } public IDisposable BeginScope<TState>(TState state) { return new NoopDisposable(); } public bool IsEnabled(LogLevel logLevel) { return logLevel >= _minLevel; } public void Log<TState>( LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) { if (!IsEnabled(logLevel)) { return; } if (formatter == null) { throw new ArgumentNullException(nameof(formatter)); } var message = formatter(state, exception); if (exception != null) { message = $"{message}{Environment.NewLine}{exception}"; } if (string.IsNullOrEmpty(message)) { return; } var dblLogItem = new DBLog() { EventName = eventId.Name, LogLevel = logLevel.ToString(), Message = $"{_categoryName}{Environment.NewLine}{message}", StackTrace=exception?.StackTrace }; _loggerProvider.AddLogItem(dblLogItem); } private class NoopDisposable : IDisposable { public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { } } }
روشهای زیادی برای پیاده سازی این نوع جستجوها وجود دارد. در این مقاله سعی شده گامهای ایجاد یک ساختار پایه برای این نوع فرمها و یک ایجاد فرم نمونه بر پایه ساختار ایجاد شده را با استفاده از یکی از همین روشها شرح دهیم.
اساس این روش تولید عبارت Linq بصورت پویا با توجه به انتخابهای کاربرمی باشد.
1- برای شروع یک سلوشن خالی با نام DynamicSearch ایجاد میکنیم. سپس ساختار این سلوشن را بصورت زیر شکل میدهیم.
در این مثال پیاده سازی در قالب ساختار MVVM در نظر گرفته شده. ولی محدودتی از این نظر برای این روش قائل نیستیم.
2- کار را از پروژه مدل آغاز میکنیم. جایی که ما برای سادگی کار، 3 کلاس بسیار ساده را به ترتیب زیر ایجاد میکنیم:
namespace DynamicSearch.Model { public class Person { public Person(string name, string family, string fatherName) { Name = name; Family = family; FatherName = fatherName; } public string Name { get; set; } public string Family { get; set; } public string FatherName { get; set; } } } using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DynamicSearch.Model { public class Teacher : Person { public Teacher(int id, string name, string family, string fatherName) : base(name, family, fatherName) { ID = id; } public int ID { get; set; } public override string ToString() { return string.Format("Name: {0}, Family: {1}", Name, Family); } } } namespace DynamicSearch.Model { public class Student : Person { public Student(int stdId, Teacher teacher, string name, string family, string fatherName) : base(name, family, fatherName) { StdID = stdId; Teacher = teacher; } public int StdID { get; set; } public Teacher Teacher { get; set; } } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using DynamicSearch.Model; namespace DynamicSearch.Service { public class StudentService { public IList<Student> GetStudents() { return new List<Student> { new Student(1,new Teacher(1,"Ali","Rajabi","Reza"),"Mohammad","Hoeyni","Sadegh"), new Student(2,new Teacher(2,"Hasan","Noori","Mohsen"),"Omid","Razavi","Ahmad"), }; } } }
جهت ساخت عبارت، نیاز به سه نوع جزء داریم:
-اتصال دهنده عبارات ( "و" ، "یا")
-عملوند (در اینجا فیلدی که قصد مقایسه با عبارت مورد جستجوی کاربر را داریم)
-عملگر ("<" ، ">" ، "=" ، ....)
برای ذخیره المانهای انتخاب شده توسط کاربر، سه کلاس زیر را ایجاد میکنیم (همان سه جزء بالا):
using System; using System.Linq.Expressions; namespace DynamicSearch.ViewModel.Base { public class AndOr { public AndOr(string name, string title,Func<Expression,Expression,Expression> func) { Title = title; Func = func; Name = name; } public string Title { get; set; } public Func<Expression, Expression, Expression> Func { get; set; } public string Name { get; set; } } } using System; namespace DynamicSearch.ViewModel.Base { public class Feild : IEquatable<Feild> { public Feild(string title, Type type, string name) { Title = title; Type = type; Name = name; } public Type Type { get; set; } public string Name { get; set; } public string Title { get; set; } public bool Equals(Feild other) { return other.Title == Title; } } } using System; using System.Linq.Expressions; namespace DynamicSearch.ViewModel.Base { public class Operator { public enum TypesToApply { String, Numeric, Both } public Operator(string title, Func<Expression, Expression, Expression> func, TypesToApply typeToApply) { Title = title; Func = func; TypeToApply = typeToApply; } public string Title { get; set; } public Func<Expression, Expression, Expression> Func { get; set; } public TypesToApply TypeToApply { get; set; } } }
using System.Collections.ObjectModel; using System.Linq; using System.Linq.Expressions; namespace DynamicSearch.ViewModel.Base { public abstract class SearchFilterBase<T> : BaseViewModel { protected SearchFilterBase() { var containOp = new Operator("شامل باشد", (expression, expression1) => Expression.Call(expression, typeof(string).GetMethod("Contains"), expression1), Operator.TypesToApply.String); var notContainOp = new Operator("شامل نباشد", (expression, expression1) => { var contain = Expression.Call(expression, typeof(string).GetMethod("Contains"), expression1); return Expression.Not(contain); }, Operator.TypesToApply.String); var equalOp = new Operator("=", Expression.Equal, Operator.TypesToApply.Both); var notEqualOp = new Operator("<>", Expression.NotEqual, Operator.TypesToApply.Both); var lessThanOp = new Operator("<", Expression.LessThan, Operator.TypesToApply.Numeric); var greaterThanOp = new Operator(">", Expression.GreaterThan, Operator.TypesToApply.Numeric); var lessThanOrEqual = new Operator("<=", Expression.LessThanOrEqual, Operator.TypesToApply.Numeric); var greaterThanOrEqual = new Operator(">=", Expression.GreaterThanOrEqual, Operator.TypesToApply.Numeric); Operators = new ObservableCollection<Operator> { equalOp, notEqualOp, containOp, notContainOp, lessThanOp, greaterThanOp, lessThanOrEqual, greaterThanOrEqual, }; SelectedAndOr = AndOrs.FirstOrDefault(a => a.Name == "Suppress"); SelectedFeild = Feilds.FirstOrDefault(); SelectedOperator = Operators.FirstOrDefault(a => a.Title == "="); } public abstract IQueryable<T> GetQuarable(); public virtual ObservableCollection<AndOr> AndOrs { get { return new ObservableCollection<AndOr> { new AndOr("And","و", Expression.AndAlso), new AndOr("Or","یا",Expression.OrElse), new AndOr("Suppress","نادیده",(expression, expression1) => expression), }; } } public virtual ObservableCollection<Operator> Operators { get { return _operators; } set { _operators = value; NotifyPropertyChanged("Operators"); } } public abstract ObservableCollection<Feild> Feilds { get; } public bool IsOtherFilters { get { return _isOtherFilters; } set { _isOtherFilters = value; } } public string SearchValue { get { return _searchValue; } set { _searchValue = value; NotifyPropertyChanged("SearchValue"); } } public AndOr SelectedAndOr { get { return _selectedAndOr; } set { _selectedAndOr = value; NotifyPropertyChanged("SelectedAndOr"); NotifyPropertyChanged("SelectedFeildHasSetted"); } } public Operator SelectedOperator { get { return _selectedOperator; } set { _selectedOperator = value; NotifyPropertyChanged("SelectedOperator"); } } public Feild SelectedFeild { get { return _selectedFeild; } set { Operators = value.Type == typeof(string) ? new ObservableCollection<Operator>(Operators.Where(a => a.TypeToApply == Operator.TypesToApply.Both || a.TypeToApply == Operator.TypesToApply.String)) : new ObservableCollection<Operator>(Operators.Where(a => a.TypeToApply == Operator.TypesToApply.Both || a.TypeToApply == Operator.TypesToApply.Numeric)); if (SelectedOperator == null) { SelectedOperator = Operators.FirstOrDefault(a => a.Title == "="); } NotifyPropertyChanged("SelectedOperator"); NotifyPropertyChanged("SelectedFeild"); _selectedFeild = value; NotifyPropertyChanged("SelectedFeildHasSetted"); } } public bool SelectedFeildHasSetted { get { return SelectedFeild != null && (SelectedAndOr.Name != "Suppress" || !IsOtherFilters); } } private ObservableCollection<Operator> _operators; private Feild _selectedFeild; private Operator _selectedOperator; private AndOr _selectedAndOr; private string _searchValue; private bool _isOtherFilters = true; } }
در گام بعد، یک کلاس کمکی برای سهولت ساخت عبارات ایجاد میکنیم:
using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; using AutoMapper; namespace DynamicSearch.ViewModel.Base { public static class ExpressionExtensions { public static List<T> CreateQuery<T>(Expression whereCallExpression, IQueryable entities) { return entities.Provider.CreateQuery<T>(whereCallExpression).ToList(); } public static MethodCallExpression CreateWhereCall<T>(Expression condition, ParameterExpression pe, IQueryable entities) { var whereCallExpression = Expression.Call( typeof(Queryable), "Where", new[] { entities.ElementType }, entities.Expression, Expression.Lambda<Func<T, bool>>(condition, new[] { pe })); return whereCallExpression; } public static void CreateLeftAndRightExpression<T>(string propertyName, Type type, string searchValue, ParameterExpression pe, out Expression left, out Expression right) { var typeOfNullable = type; typeOfNullable = typeOfNullable.IsNullableType() ? typeOfNullable.GetTypeOfNullable() : typeOfNullable; left = null; var typeMethodInfos = typeOfNullable.GetMethods(); var parseMethodInfo = typeMethodInfos.FirstOrDefault(a => a.Name == "Parse" && a.GetParameters().Count() == 1); var propertyInfos = typeof(T).GetProperties(); if (propertyName.Contains(".")) { left = CreateComplexTypeExpression(propertyName, propertyInfos, pe); } else { var propertyInfo = propertyInfos.FirstOrDefault(a => a.Name == propertyName); if (propertyInfo != null) left = Expression.Property(pe, propertyInfo); } if (left != null) left = Expression.Convert(left, typeOfNullable); if (parseMethodInfo != null) { var invoke = parseMethodInfo.Invoke(searchValue, new object[] { searchValue }); right = Expression.Constant(invoke, typeOfNullable); } else { //type is string right = Expression.Constant(searchValue.ToLower()); var methods = typeof(string).GetMethods(); var firstOrDefault = methods.FirstOrDefault(a => a.Name == "ToLower" && !a.GetParameters().Any()); if (firstOrDefault != null) left = Expression.Call(left, firstOrDefault); } } public static Expression CreateComplexTypeExpression(string searchFilter, IEnumerable<PropertyInfo> propertyInfos, Expression pe) { Expression ex = null; var infos = searchFilter.Split('.'); var enumerable = propertyInfos.ToList(); for (var index = 0; index < infos.Length - 1; index++) { var propertyInfo = infos[index]; var nextPropertyInfo = infos[index + 1]; if (propertyInfos == null) continue; var propertyInfo2 = enumerable.FirstOrDefault(a => a.Name == propertyInfo); if (propertyInfo2 == null) continue; var val = Expression.Property(pe, propertyInfo2); var propertyInfos3 = propertyInfo2.PropertyType.GetProperties(); var propertyInfo3 = propertyInfos3.FirstOrDefault(a => a.Name == nextPropertyInfo); if (propertyInfo3 != null) ex = Expression.Property(val, propertyInfo3); } return ex; } public static Expression AddOperatorExpression(Func<Expression, Expression, Expression> func, Expression left, Expression right) { return func.Invoke(left, right); } public static Expression JoinExpressions(bool isFirst, Func<Expression, Expression, Expression> func, Expression expression, Expression ex) { if (!isFirst) { return func.Invoke(expression, ex); } expression = ex; return expression; } } }
using System.Collections.ObjectModel; using System.Linq; using DynamicSearch.Model; using DynamicSearch.Service; using DynamicSearch.ViewModel.Base; namespace DynamicSearch.ViewModel { public class StudentSearchFilter : SearchFilterBase<Student> { public override ObservableCollection<Feild> Feilds { get { return new ObservableCollection<Feild> { new Feild("نام دانش آموز",typeof(string),"Name"), new Feild("نام خانوادگی دانش آموز",typeof(string),"Family"), new Feild("نام خانوادگی معلم",typeof(string),"Teacher.Name"), new Feild("شماره دانش آموزی",typeof(int),"StdID"), }; } } public override IQueryable<Student> GetQuarable() { return new StudentService().GetStudents().AsQueryable(); } } }
در نهایت زمل فایل موجود در پروژه ویو:
<Window x:Class="DynamicSearch.View.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:viewModel="clr-namespace:DynamicSearch.ViewModel;assembly=DynamicSearch.ViewModel" xmlns:view="clr-namespace:DynamicSearch.View" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300"> <Window.Resources> <viewModel:StudentSearchViewModel x:Key="StudentSearchViewModel" /> <view:VisibilityConverter x:Key="VisibilityConverter" /> </Window.Resources> <Grid DataContext="{StaticResource StudentSearchViewModel}"> <WrapPanel Orientation="Vertical"> <DataGrid AutoGenerateColumns="False" Name="asd" CanUserAddRows="False" ItemsSource="{Binding BindFilter}"> <DataGrid.Columns> <DataGridTemplateColumn> <DataGridTemplateColumn.CellTemplate> <DataTemplate DataType="{x:Type viewModel:StudentSearchFilter}"> <ComboBox MinWidth="100" DisplayMemberPath="Title" ItemsSource="{Binding AndOrs}" Visibility="{Binding IsOtherFilters,Converter={StaticResource VisibilityConverter}}" SelectedItem="{Binding SelectedAndOr,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" /> </DataTemplate> </DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn> <DataGridTemplateColumn > <DataGridTemplateColumn.CellTemplate> <DataTemplate DataType="{x:Type viewModel:StudentSearchFilter}"> <ComboBox IsEnabled="{Binding SelectedFeildHasSetted}" MinWidth="100" DisplayMemberPath="Title" ItemsSource="{Binding Feilds}" SelectedItem="{Binding SelectedFeild,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged }"/> </DataTemplate> </DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn> <DataGridTemplateColumn> <DataGridTemplateColumn.CellTemplate> <DataTemplate DataType="{x:Type viewModel:StudentSearchFilter}"> <ComboBox MinWidth="100" DisplayMemberPath="Title" ItemsSource="{Binding Operators}" IsEnabled="{Binding SelectedFeildHasSetted}" SelectedItem="{Binding SelectedOperator,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" /> </DataTemplate> </DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn> <DataGridTemplateColumn Width="*"> <DataGridTemplateColumn.CellTemplate> <DataTemplate DataType="{x:Type viewModel:StudentSearchFilter}"> <TextBox IsEnabled="{Binding SelectedFeildHasSetted}" MinWidth="200" Text="{Binding SearchValue,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/> <!--<TextBox Text="{Binding SearchValue,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>--> </DataTemplate> </DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn> </DataGrid.Columns> </DataGrid> <Button Content="+" HorizontalAlignment="Left" Command="{Binding AddFilter}"/> <Button Content="Result" Command="{Binding ExecuteSearchFilter}"/> <DataGrid ItemsSource="{Binding Results}"> </DataGrid> </WrapPanel> </Grid> </Window>
برخی منابع جهت آشنایی با Expression ها:
http://msdn.microsoft.com/en-us/library/bb882637.aspx
انتخاب پویای فیلدها در LINQ
http://www.persiadevelopers.com/articles/dynamiclinqquery.aspx
نکته: کدهای نوشته شده در این مقاله، نسخههای نخستین هستند و طبیعتا جا برای بهبود بسیار دارند. دوستان میتوانند در این امر به بنده کمک کنند.
پیشنهادات جهت بهبود:
- جداسازی کدهای پیاده کننده منطق از ویو مدلها جهت افزایش قابلیت نگهداری کد و سهولت استفاده در سایر ساختارها
- افزودن توضیحات به کد
- انتخاب نامگذاریهای مناسب تر
DynamicSearch.zip
پیشنیازها
- آشنایی با مباحث Migrations در EF Code first
- آشنایی با مباحث الگوی واحد کار
- چگونه مدلهای EF را به صورت خودکار به Context اضافه کنیم؟
- چگونه تنظیمات مدلهای EF را به صورت خودکار به Context اضافه کنیم؟
کدهایی را که در این قسمت مشاهده خواهید کرد، در حقیقت همان برنامهی توسعه یافته «آشنایی با مباحث الگوی واحد کار» است و از ذکر قسمتهای تکراری آن جهت طولانی نشدن مبحث، صرفنظر خواهد شد. برای مثال Context و مدلهای محصولات و گروههای آنها به همراه کلاسهای لایه سرویس برنامهی اصلی، دقیقا همان کدهای مطلب «آشنایی با مباحث الگوی واحد کار» است.
تعریف domain classes مخصوص افزونهها
در ادامهی پروژهی افزونه پذیر فعلی، پروژهی class library جدیدی را به نام MvcPluginMasterApp.Plugin1.DomainClasses اضافه خواهیم کرد. از آن جهت تعریف کلاسهای مدل افزونهی یک استفاده میکنیم. برای مثال کلاس News را به همراه تنظیمات Fluent آن به این پروژهی جدید اضافه کنید:
using System.Data.Entity.ModelConfiguration; namespace MvcPluginMasterApp.Plugin1.DomainClasses { public class News { public int Id { set; get; } public string Title { set; get; } public string Body { set; get; } } public class NewsConfig : EntityTypeConfiguration<News> { public NewsConfig() { this.ToTable("Plugin1_News"); this.HasKey(news => news.Id); this.Property(news => news.Title).IsRequired().HasMaxLength(500); this.Property(news => news.Body).IsOptional().IsMaxLength(); } } }
PM> install-package EntityFramework
مشکل! برنامهی اصلی، همانند مطلب «آشنایی با مباحث الگوی واحد کار» دارای domain classes خاص خودش است به همراه تنظیمات Context ایی که صریحا در آن مدلهای متناظر با این پروژه در معرض دید EF قرار گرفتهاند:
public class MvcPluginMasterAppContext : DbContext, IUnitOfWork { public DbSet<Category> Categories { set; get; } public DbSet<Product> Products { set; get; }
تغییرات اینترفیس Unit of work جهت افزونه پذیری
در ادامه، اینترفیس بهبود یافتهی IUnitOfWork را جهت پذیرش DbSetهای پویا و همچنین EntityTypeConfigurationهای پویا، ملاحظه میکنید:
namespace MvcPluginMasterApp.PluginsBase { public interface IUnitOfWork : IDisposable { IDbSet<TEntity> Set<TEntity>() where TEntity : class; int SaveAllChanges(); void MarkAsChanged<TEntity>(TEntity entity) where TEntity : class; IList<T> GetRows<T>(string sql, params object[] parameters) where T : class; IEnumerable<TEntity> AddThisRange<TEntity>(IEnumerable<TEntity> entities) where TEntity : class; void SetDynamicEntities(Type[] dynamicTypes); void ForceDatabaseInitialize(); void SetConfigurationsAssemblies(Assembly[] assembly); } }
SetDynamicEntities : توسط این متد در ابتدای برنامه، نوعهای مدلهای جدید افزونهها به صورت خودکار به Context اضافه خواهند شد.
SetConfigurationsAssemblies : کار افزودن اسمبلیهای حاوی تعاریف EntityTypeConfigurationهای جدید و پویا را به عهده دارد.
ForceDatabaseInitialize: سبب خواهد شد تا مباحث migrations، پیش از شروع به کار برنامه، اعمال شوند.
در کلاس Context ذیل، نحوهی پیاده سازی این متدهای جدید را ملاحظه میکنید:
namespace MvcPluginMasterApp.DataLayer.Context { public class MvcPluginMasterAppContext : DbContext, IUnitOfWork { private readonly IList<Assembly> _configurationsAssemblies = new List<Assembly>(); private readonly IList<Type[]> _dynamicTypes = new List<Type[]>(); public void ForceDatabaseInitialize() { Database.Initialize(force: true); } public void SetConfigurationsAssemblies(Assembly[] assemblies) { if (assemblies == null) return; foreach (var assembly in assemblies) { _configurationsAssemblies.Add(assembly); } } public void SetDynamicEntities(Type[] dynamicTypes) { if (dynamicTypes == null) return; _dynamicTypes.Add(dynamicTypes); } protected override void OnModelCreating(DbModelBuilder modelBuilder) { addConfigurationsFromAssemblies(modelBuilder); addPluginsEntitiesDynamically(modelBuilder); base.OnModelCreating(modelBuilder); } private void addConfigurationsFromAssemblies(DbModelBuilder modelBuilder) { foreach (var assembly in _configurationsAssemblies) { modelBuilder.Configurations.AddFromAssembly(assembly); } } private void addPluginsEntitiesDynamically(DbModelBuilder modelBuilder) { foreach (var types in _dynamicTypes) { foreach (var type in types) { modelBuilder.RegisterEntityType(type); } } } } }
بهبود اینترفیس IPlugin جهت پذیرش نوعهای پویای EF
در قسمت اول، با اینترفیس IPlugin آشنا شدیم. هر افزونه باید دارای کلاسی باشد که این اینترفیس را پیاده سازی میکند. از آن جهت دریافت تنظیمات و یا ثبت تنظیمات مسیریابی و امثال آن استفاده میشود.
در اینجا متد GetEfBootstrapper آن کار دریافت تنظیمات EF هر افزونه را به عهد دارد.
namespace MvcPluginMasterApp.PluginsBase { public interface IPlugin { EfBootstrapper GetEfBootstrapper(); //...به همراه سایر متدهای مورد نیاز } public class EfBootstrapper { /// <summary> /// Assemblies containing EntityTypeConfiguration classes. /// </summary> public Assembly[] ConfigurationsAssemblies { get; set; } /// <summary> /// Domain classes. /// </summary> public Type[] DomainEntities { get; set; } /// <summary> /// Custom Seed method. /// </summary> public Action<IUnitOfWork> DatabaseSeeder { get; set; } } }
DomainEntities بیانگر لیست مدلها و موجودیتهای هر افزونه است.
DatabaseSeeder کار دریافت منطق متد Seed را بر عهده دارد. برای مثال اگر افزونهای نیاز است در آغاز کار تشکیل جداول آن، دیتای پیش فرض و خاصی را در بانک اطلاعاتی ثبت کند، میتوان از این متد استفاده کرد. اگر دقت کنید این Action یک وهله از IUnitOfWork را به افزونه ارسال میکند. بنابراین در این طراحی جدید، اینترفیس IUnitOfWork به پروژهی MvcPluginMasterApp.PluginsBase منتقل میشود. به این ترتیب دیگر نیازی نیست تا تک تک افزونهها ارجاع مستقیمی را به DataLayer پروژهی اصلی پیدا کنند.
تکمیل متد GetEfBootstrapper در افزونهها
اکنون جهت معرفی مدلها و تنظیمات EF آنها، تنها کافی است متد GetEfBootstrapper هر افزونه را تکمیل کنیم:
namespace MvcPluginMasterApp.Plugin1 { public class Plugin1 : IPlugin { public EfBootstrapper GetEfBootstrapper() { return new EfBootstrapper { DomainEntities = new[] { typeof(News) }, ConfigurationsAssemblies = new[] { typeof(NewsConfig).Assembly }, DatabaseSeeder = uow => { var news = uow.Set<News>(); if (news.Any()) { return; } news.Add(new News { Title = "News 1", Body = "news 1 news 1 news 1 ...." }); news.Add(new News { Title = "News 2", Body = "news 2 news 2 news 2 ...." }); } }; }
همچنین توسط delegate ایی به نام DatabaseSeeder، نحوهی دسترسی به متد Set واحد کار و سپس استفادهی از آن، برای تعریف متد Seed سفارشی نیز تکمیل شدهاست.
تدارک یک راه انداز EF، پیش از شروع به کار برنامه
در پوشهی App_Start پروژهی اصلی یا همان MvcPluginMasterApp، کلاس جدید EFBootstrapperStart را با کدهای ذیل اضافه کنید:
[assembly: PreApplicationStartMethod(typeof(EFBootstrapperStart), "Start")] namespace MvcPluginMasterApp { public static class EFBootstrapperStart { public static void Start() { var plugins = SmObjectFactory.Container.GetAllInstances<IPlugin>().ToList(); using (var uow = SmObjectFactory.Container.GetInstance<IUnitOfWork>()) { initDatabase(uow, plugins); runDatabaseSeeders(uow, plugins); } } private static void initDatabase(IUnitOfWork uow, IEnumerable<IPlugin> plugins) { foreach (var plugin in plugins) { var efBootstrapper = plugin.GetEfBootstrapper(); if (efBootstrapper == null) continue; uow.SetDynamicEntities(efBootstrapper.DomainEntities); uow.SetConfigurationsAssemblies(efBootstrapper.ConfigurationsAssemblies); } Database.SetInitializer(new MigrateDatabaseToLatestVersion<MvcPluginMasterAppContext, Configuration>()); uow.ForceDatabaseInitialize(); } private static void runDatabaseSeeders(IUnitOfWork uow, IEnumerable<IPlugin> plugins) { foreach (var plugin in plugins) { var efBootstrapper = plugin.GetEfBootstrapper(); if (efBootstrapper == null || efBootstrapper.DatabaseSeeder == null) continue; efBootstrapper.DatabaseSeeder(uow); uow.SaveAllChanges(); } } } }
همانطور که ملاحظه میکنید، ابتدا لیست تمام افزونههای موجود، به کمک StructureMap دریافت میشوند. سپس میتوان در متد initDatabase به متد GetEfBootstrapper هر افزونه دسترسی یافت و توسط آن تنظیمات مدلها را یافته و به Context اصلی برنامه اضافه کرد. سپس با فراخوانی ForceDatabaseInitialize تمام این موارد به صورت خودکار به بانک اطلاعاتی اعمال خواهند شد.
کار متد runDatabaseSeeders، یافتن DatabaseSeeder هر افزونه، اجرای آنها و سپس فراخوانی متد SaveAllChanges در آخر کار است.
کدهای کامل این سری را از اینجا میتوانید دریافت کنید:
MvcPlugin
مرتب سازی پراکسیها در سرویسهای WCF
فرض کنید یک پراکسی دینامیک (dynamic proxy) از یک کوئری دریافت کرده اید. حال میخواهید این پراکسی را در قالب یک آبجکت CLR سریال کنید. هنگامی که آبجکتهای موجودیت را بصورت POCO-based پیاده سازی میکنید، EF بصورت خودکار یک آبجکت دینامیک مشتق شده را در زمان اجرا تولید میکند که dynamix proxy نام دارد. این آبجکت برای هر موجودیت POCO تولید میشود. این آبجکت پراکسی بسیاری از خواص مجازی (virtual) را بازنویسی میکند تا عملیاتی مانند ردیابی تغییرات و بارگذاری تنبل را انجام دهد.
فرض کنید مدلی مانند تصویر زیر دارید.
ما از کلاس ProxyDataContractResolver برای deserialize کردن یک آبجکت پراکسی در سمت سرور و تبدیل آن به یک آبجکت POCO روی کلاینت WCF استفاده میکنیم. مراحل زیر را دنبال کنید.
- در ویژوال استودیو پروژه جدیدی از نوع WCF Service Application بسازید. یک Entity Data Model به پروژه اضافه کنید و جدول Clients را مطابق مدل مذکور ایجاد کنید.
- کلاس POCO تولید شده توسط EF را باز کنید و کلمه کلیدی virtual را به تمام خواص اضافه کنید. این کار باعث میشود EF کلاسهای پراکسی دینامیک تولید کند. کد کامل این کلاس در لیست زیر قابل مشاهده است.
public class Client { public virtual int ClientId { get; set; } public virtual string Name { get; set; } public virtual string Email { get; set; } }
نکته: بیاد داشته باشید که هرگاه مدل EDMX را تغییر میدهید، EF بصورت خودکار کلاسهای موجودیتها را مجددا ساخته و تغییرات مرحله قبلی را بازنویسی میکند. بنابراین یا باید این مراحل را هر بار تکرار کنید، یا قالب T4 مربوطه را ویرایش کنید. اگر هم از مدل Code-First استفاده میکنید که نیازی به این کارها نخواهد بود.
- ما نیاز داریم که DataContractSerializer از یک کلاس ProxyDataContractResolver استفاده کند تا پراکسی کلاینت را به موجودیت کلاینت برای کلاینت سرویس WCF تبدیل کند. یعنی client proxy به client entity تبدیل شود، که نهایتا در اپلیکیشن کلاینت سرویس استفاده خواهد شد. بدین منظور یک ویژگی operation behavior میسازیم و آن را به متد ()GetClient در سرویس الحاق میکنیم. برای ساختن ویژگی جدید از کدی که در لیست زیر آمده استفاده کنید. بیاد داشته باشید که کلاس ProxyDataContractResolver در فضای نام Entity Framework قرار دارد.
using System.ServiceModel.Description; using System.ServiceModel.Channels; using System.ServiceModel.Dispatcher; using System.Data.Objects; namespace Recipe8 { public class ApplyProxyDataContractResolverAttribute : Attribute, IOperationBehavior { public void AddBindingParameters(OperationDescription description, BindingParameterCollection parameters) { } public void ApplyClientBehavior(OperationDescription description, ClientOperation proxy) { DataContractSerializerOperationBehavior dataContractSerializerOperationBehavior = description.Behaviors .Find<DataContractSerializerOperationBehavior>(); dataContractSerializerOperationBehavior.DataContractResolver = new ProxyDataContractResolver(); } public void ApplyDispatchBehavior(OperationDescription description, DispatchOperation dispatch) { DataContractSerializerOperationBehavior dataContractSerializerOperationBehavior = description.Behaviors .Find<DataContractSerializerOperationBehavior>(); dataContractSerializerOperationBehavior.DataContractResolver = new ProxyDataContractResolver(); } public void Validate(OperationDescription description) { } } }
- فایل IService1.cs را باز کنید و کد آن را مطابق لیست زیر تکمیل نمایید.
[ServiceContract] public interface IService1 { [OperationContract] void InsertTestRecord(); [OperationContract] Client GetClient(); [OperationContract] void Update(Client client); }
- فایل IService1.svc.cs را باز کنید و پیاده سازی سرویس را مطابق لیست زیر تکمیل کنید.
public class Client { [ApplyProxyDataContractResolver] public Client GetClient() { using (var context = new EFRecipesEntities()) { context.Cofiguration.LazyLoadingEnabled = false; return context.Clients.Single(); } } public void Update(Client client) { using (var context = new EFRecipesEntities()) { context.Entry(client).State = EntityState.Modified; context.SaveChanges(); } } public void InsertTestRecord() { using (var context = new EFRecipesEntities()) { // delete previous test data context.ExecuteSqlCommand("delete from [clients]"); // insert new test data context.ExecuteStoreCommand(@"insert into [clients](Name, Email) values ('Armin Zia','armin.zia@gmail.com')"); } } }
- حال پروژه جدیدی از نوع Console Application به راه حل جاری اضافه کنید. این اپلیکیشن، کلاینت تست ما خواهد بود. پروژه سرویس را ارجاع کنید و کد کلاینت را مطابق لیست زیر تکمیل نمایید.
using Recipe8Client.ServiceReference1; namespace Recipe8Client { class Program { static void Main(string[] args) { using (var serviceClient = new Service1Client()) { serviceClient.InsertTestRecord(); var client = serviceClient.GetClient(); Console.WriteLine("Client is: {0} at {1}", client.Name, client.Email); client.Name = "Armin Zia"; client.Email = "arminzia@live.com"; serviceClient.Update(client); client = serviceClient.GetClient(); Console.WriteLine("Client changed to: {0} at {1}", client.Name, client.Email); } } } }
Client changed to: Armin Zia at arminzia@live.com
شرح مثال جاری
مایکروسافت استفاده از آبجکتهای POCO با WCF را توصیه میکند چرا که مرتب سازی (serialization) آبجکت موجودیت را سادهتر میکند. اما در صورتی که از آبجکتهای POCO ای استفاده میکنید که changed-based notification دارند (یعنی خواص موجودیت را با virtual علامت گذاری کرده اید و کلکسیونهای خواص پیمایشی از نوع ICollection هستند)، آنگاه EF برای موجودیتهای بازگشتی کوئریها پراکسیهای دینامیک تولید خواهد کرد.
استفاده از پراکسیهای دینامیک با WCF دو مشکل دارد. مشکل اول مربوط به سریال کردن پراکسی است. کلاس DataContractSerializer تنها قادر به serialize/deserialize کردن انواع شناخته شده (known types) است. در مثال جاری این یعنی موجودیت Client. اما از آنجا که EF برای موجودیتها پراکسی میسازد، حالا باید آبجکت پراکسی را سریال کنیم، نه خود کلاس Client را. اینجا است که از DataContractResolver استفاده میکنیم. این کلاس میتواند حین سریال کردن آبجکت ها، نوعی را به نوع دیگر تبدیل کند. برای استفاده از این کلاس ما یک ویژگی سفارشی ساختیم، که پراکسیها را به کلاسهای POCO تبدیل میکند. سپس این ویژگی را به متد ()GetClient اضافه کردیم. این کار باعث میشود که پراکسی دینامیکی که توسط متد ()GetClient برای موجودیت Client تولید میشود، به درستی سریال شود.
مشکل دوم استفاده از پراکسیها با WCF مربوط به بارگذاری تبل یا Lazy Loading میشود. هنگامی که DataContractSerializer موجودیتها را سریال میکند، تمام خواص موجودیت را دستیابی خواهد کرد که منجر به اجرای lazy-loading روی خواص پیمایشی میشود. مسلما این رفتار را نمیخواهیم. برای رفع این مشکل، مشخصا قابلیت بارگذاری تنبل را خاموش یا غیرفعال کرده ایم.