EF Code First #12
دسترسی به آن فقط برای نویسندگان سایت با حداقل یک مطلب ارسالی در طی یک ماه قبل است. البته همهی کاربران عضو، میتوانند مشارکت کنند و در سایت مطلب ارسال کنند. از این لحاظ محدودیتی وجود ندارد.
string data = null; var result = data ?? "value";
if (data == null) { data = "value"; } var result = data;
برای مثال بسیاری از نتایج بازگشتی از متدها، چند سطحی هستند:
class Response { public string Result { set; get; } public int Code { set; get; } } class WebRequest { public Response GetDataFromWeb(string url) { // ... return new Response { Result = null }; } }
var webData = new WebRequest().GetDataFromWeb("https://www.dntips.ir/"); if (webData != null && webData.Result != null) { Console.WriteLine(webData.Result); }
در این حالت اگر اشارهگر را به محل && انتقال دهیم، افزونهی ReSharper پیشنهاد یکی کردن این بررسیها را ارائه میدهد:
به این ترتیب تمام چند سطح بررسی نال، به یک عبارت بررسی .? دار، خلاصه خواهد شد:
if (webData?.Result != null) { Console.WriteLine(webData.Result); }
البته باید دقت داشت که برای تمام سطوح باید از .? استفاده کرد (برای مثال response?.Results?.Status)؛ در غیر اینصورت همانند سابق در صورت استفادهی از دات معمولی، به یک null reference exception میرسیم.
کار با متدها و Delegates
این عملگر جدید مقایسهی با نال را بر روی متدها (علاوه بر خواص و فیلدها) نیز میتوان بکار برد. برای مثال خلاصه شدهی فراخوانی ذیل:
if (x != null) { x.Dispose(); }
x?.Dispose();
و یا بکار گیری آن بر روی delegates (روش قدیمی):
var copy = OnMyEvent; if (copy != null) { copy(this, new EventArgs()); }
OnMyEvent?.Invoke(this, new EventArgs());
استفاده از Null Conditional Operator بر روی Value types
الف) مقایسه با نال
کد ذیل را درنظر بگیرید:
var code = webData?.Code;
if (webData?.Code > 0) { }
ب) بازگشت مقدار پیش فرض دیگری بجای نال
اگر نیاز بود بجای null مقدار پیش فرض دیگری را بازگشت دهیم، میتوان از null-coalescing operator سابق استفاده کرد:
int count = response?.Results?.Count ?? 0;
ج) دسترسی به مقدار Value یک متغیر nullable
نمونهی دیگر آن قطعه کد ذیل است:
int? x = 10; //var value = x?.Value; // invalid Console.WriteLine(x?.ToString());
کار با indexer property و بررسی نال
اگر به عنوان بحث دقت کرده باشید، یک s جمع در انتهای Null-conditional operators ذکر شدهاست. به این معنا که این عملگر مقایسهی با نال، صرفا یک شکل و فرم .? را ندارد. مثال ذیل در حین کار با آرایهها و لیستها بسیار مشاهده میشود:
if (response != null && response.Results != null && response.Results.Addresses != null && response.Results.Addresses[0] != null && response.Results.Addresses[0].Zip == "63368") { }
if(response?.Results?.Addresses?[0]?.Zip == "63368") { }
موارد استفادهی ناصحیح از عملگرهای مقایسهی با نال
خوب، عملگر .? کار مقایسهی با نال را خصوصا در دسترسیهای چند سطحی به خواص و متدها بسیار ساده میکند. اما آیا باید در همه جا از آن استفاده کرد؟ آیا باید از این پس کلا استفاده از دات را فراموش کرد و بجای آن از .? در همه جا استفاده کرد؟
مثال ذیل را درنظر بگیرید:
public void DoSomething(Customer customer) { string address = customer?.Employees ?.SingleOrDefault(x => x.IsAdmin)?.Address?.ToString(); SendPackage(address); }
روش بهتر انجام اینکار، بررسی وضعیت customer و انتقال مابقی زنجیرهی LINQ به یک متد مجزای دیگر است:
public void DoSomething(Customer customer) { Contract.Requires(customer != null); string address = customer.GetAdminAddress(); SendPackage(address); }
در Asp.net دو چرخهی حیات مهم
وجود دارند که اساس چارچوب MVC را تشکیل میدهند
:
- چرخهی حیات برنامه (Application)؛ از لحظهای که برنامه برای اولین بار اجرا میشود و تا لحظهی خاتمهی آن را شامل میشود.
- چرخهی حیات یک درخواست ( Request )؛ مسیری که یک درخواست طی میکند، اصطلاحا PipeLine نامیده میشود که همان چرخهی حیات یک درخواست نیز هست و از لحظهای که درخواست تحویل asp.net شده، تا زمانیکه درخواست ارسال میشود را شامل میشود.
تمرکز بنده بیشتر بر روی روند و مسیری است که یک درخواست طی میکند و قصد دارم با بهره گیری از کتاب Pro Asp.net Mvc 5 و دیگر منابع، چرخهی حیات درخواست را در برنامههای Mvc بررسی کرده و در مقالات آتی ماژولها و هندلرها را بررسی کنم.
در asp.net ، برنامه global فایلهای شامل دو فایل Global.asax , Global.asax.cs است.
فایل Global.asax که هیچ گاه نیاز به ویرایش آن نداریم محتویاتی مانند زیر دارد:
<%@ Application Codebehind="Global.asax.cs" Inherits="YourAppName.MvcApplication" Language="C#" %>
در این مقاله منظور از فایل global فایل Global.asax.cs است که مشتق شده از کلاس System.Web.HttpApplication است:
public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { ...// } }
Asp.net برای پاسخگویی به درخواستهای واصله، وهلههایی از کلاس MvcApplication را میسازد ولی این دو متد صرفا در نقاط شروع و پایان برنامه فراخوانی شده و عملا در وهلههای یاد شده صدا زده نخواهند شد و به جای آنها رویدادهایی را که در ذیل آنها را معرفی میکنیم، فراخوانی شده و چرخهی حیات درخواست را برای ما مشخص میسازند .
BeginRequest : به عنوان اولین رویداد، به محض وصول یک درخواست جدید رخ خواهد داد.AuthenticateRequest ,PostAuthenticateRequest : رویداد AuthenticateRequest برای شناسایی کاربر ارسال کننده درخواست، کاربرد دارد و پس از پردازش کلیهی توابع، رویداد PostAuthenticateRequest صدا زده میشود.
AuthorizeRequest :بههنگام صدور مجوزهای یک درخواست رخ میدهد و مشابه رویداد بالا پس از پردازش کلیهی توابع، رویداد PostAuthorizeRequest صدا زده خواهد شد.ResolveRequestCache : پس از صدور مجوزهای یک درخواست در رویداد authorization زمانیکه ماژولهای کش میخواهند اطلاعاتی را از کش سرور مطالبه کنند، رخ میدهد و به مانند دو رخداد قبلی، PostResolveRequestCache نیز پس از اتمام پردازش توابع رویداد رخ میدهد.
MapRequestHandler : زمانی که Asp.net میخواهد هندلری را برای پاسخگویی به درخواست واصله انتخاب کند رخ میدهد و PostMapRequestHandler نیز پس از این انتخاب، تریگر میشود.AcquireRequestState : جهت بدست آوردن دادههایی نظیر سشن و ... مرتبط با درخواست جاری کاربرد داشته و PostAcquireRequestState نیز پس از پردازش توابع رویداد رخ خواهد داد.
PreRequestHandlerExecute : بالافاصله قبل و همچنین بلافاصله بعد از این که یک هندلر بخواهد درخواستی را پردازش کند، رخ میدهد. PostRequestHandlerExecute نیز همانند دیگر رویدادهای گذشته، پس از اتمام پردازش توابع، این رویداد رخ خواهد داد.ReleaseRequestState : زمانی رخ میدهد که دادههای مرتبط با درخواست جاری، در ادامهی روند پردازش درخواست مورد نیاز نباشند و پس از پردازش توابع رویداد، PostReleaseRequestState رخ خواهد داد .
UpdateRequestCache : به جهت اینکه ماژولهای مسئول کش، توانایی به روز رسانی دادههای خود، برای پاسخگویی به درخواستهای بعدی را داشته باشند، این رویداد رخ میدهد.
LogRequest : قبل از انجام عملیات لاگین برای درخواست جاری رخ میدهد و پس از پردازش توابع رویداد نیز PostLogRequest تریگر میشود.EndRequest : پس از پایان کار پردازش درخواست جاری و مهیا شدن پاسخ مرتبط جهت ارسال به مرورگر تریگر خواهد شد.
PreSendRequestHeaders : قبل از ارسال HTTP headers به مرورگر این رویداد رخ خواهد داد.
PreSendRequestContent : بعد از ارسال شدن هدرها و قبل از ارسال محتوای صفحه به مرورگر رخ میدهد.Error : هر زمان و در هر مرحله از پردازش درخواست، چنانچه خطایی صورت پذیرد این رویداد رخ خواهد داد.
فریم ورک Asp.net جهت مدیریت بهتر یک درخواست، در تمام مسیر پردازش، رویدادهای بالا را مهیا کرده است. در ادامه نحوهی هندل کردن رویدادهای چرخهی حیات درخواست را در فایل global توضیح میدهم. هر چند که استفاده از این فایل بدین منظور، صرفا برای مدیریت مسائل ابتدایی مناسب بوده و در یک پروژهی بزرگ موجب به هم ریختگی فایل global با کدهای زیاد و خوانایی پایین بوده که قابلیت استفاده مجدد در دیگر پروژهها را نیز ندارد.
Asp.net این مشکل را با معرفی ماژولها که در مقالات آتی توضیح خواهم داد، مرتفع کرده است.
در فایل global هر گاه متدی را با پیشوند Application_ و نام یکی از رویدادهای بالا بنویسید Asp.net آن را به عنوان هندلری برای رویداد مذکور میشناسد. به عنوان مثال متدی با نام Application_BeginRequest متد رویداد BeginRequest میباشد.
ابتدا یک پروژهی MVC جدید را به نام SimpleApp ایجاد کرده و فایل global آن را مطابق ذیل تغییر میدهیم:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Routing; namespace SimpleApp { public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RouteConfig.RegisterRoutes(RouteTable.Routes); } protected void Application_BeginRequest() { RecordEvent("BeginRequest"); } protected void Application_AuthenticateRequest() { RecordEvent("AuthenticateRequest"); } protected void Application_PostAuthenticateRequest() { RecordEvent("PostAuthenticateRequest"); } private void RecordEvent(string name) { List<string> eventList = Application["events"] as List<string>; if (eventList == null) { Application["events"] = eventList = new List<string>(); } eventList.Add(name); } } }
در اینجا متدی به نام RecordEvent را در کدهای ذکر شده مشاهده میکنید که نام یک رویداد را دریافت و جهت در دسترس قرار دادن در کل برنامه به خاصیت Application از کلاس HttpApplication نسبت داده و متد مذکور را از سه متد دیگر فراخوانی کردهایم. این متدها در زمان رخ دادن رویدادهای BeginRequest, AuthenticateRequest, PostAuthenticateRequest صدا زده خواهند شد.
حال جهت نمایش اطلاعات رویداد نیاز است تغییراتی مشابه ذیل در کنترلر Home ایجاد نماییم.
using System.Web.Mvc; namespace SimpleApp.Controllers { public class HomeController : Controller { public ActionResult Index() { return View(HttpContext.Application["events"]); } } }
ویوی مرتبط با اکشن متد index را مطابق کدهای ذیل بازنویسی میکنیم:
@model List<string> @{ ViewBag.Title = "Events List"; } <h5>Events</h5> <table> @foreach (string eventName in Model) { <tr> <td>@eventName</td> </tr> } </table>
در این مقاله سعی کردیم ابتدا چرخهی حیات یک Request را فرا گرفته و سپس از طریق فایل global و توسط متدهایی با پیشوند Application_ +نام رویداد (اصطلاحا متدهای ویژه نامیده میشوند) چرخه حیات یک درخواست را مدیریت کنیم.
در SampleProject1 مدل Product را داریم:
public partial class Product : Entity { public int Id { get; set; } public string Name { get; set; } public Nullable<byte> ProductTypeId { get; set; } }
public partial class ProductType : Entity { public byte Id { get; set; } public string Name { get; set; } }
List<Assembly> allAssemblies = new List<Assembly>(); string path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); foreach (string dll in Directory.GetFiles(path, "*.Common.dll")) allAssemblies.Add(Assembly.LoadFile(dll)); var type = typeof(Entity); List<Type> types = allAssemblies .SelectMany(s => s.GetTypes()) .Where(p => type.IsAssignableFrom(p)).ToList(); List<string> entities = new List<string>(); foreach (var item in types) { entities.Add(item.Name); } types.Add(typeof(Entity));
public class ContextGenerator { public void Generate(List<string> entities, params Type[] types) { StringBuilder code = new StringBuilder(); code.AppendLine(@" using System.Data.Entity; using System.Data.Entity.Core.EntityClient; using SampleProject1.Common.Models; using SampleProject1.Common.Models.Mapping; using SampleProject2.Common.Models; using SampleProject2.Common.Models.Mapping; namespace DbContextGenerator { public partial class TestContext : DbContext { static TestContext() { Database.SetInitializer<TestContext>(null); } public TestContext() : base(""Data Source=.;Initial Catalog=Test;Integrated Security=True;MultipleActiveResultSets=True"") { } "); var pluralizeHelper = new PluralizeHelper(); foreach (var entity in entities) { code.AppendLine($@"public DbSet<{entity}> {pluralizeHelper.Pluralize(entity)} {{ get; set; }}"); } code.AppendLine(@"protected override void OnModelCreating(DbModelBuilder modelBuilder)"); code.AppendLine(@"{"); foreach (var entity in entities) { code.AppendLine($@"modelBuilder.Configurations.Add(new {entity}Map());"); } code.AppendLine(@"}"); code.AppendLine(@"}"); code.AppendLine(@"}"); CSharpCodeProvider provider = new CSharpCodeProvider(); CompilerParameters parameters = new CompilerParameters(); parameters.ReferencedAssemblies.Add("System.Drawing.dll"); parameters.ReferencedAssemblies.Add("System.Data.dll"); parameters.ReferencedAssemblies.Add("System.Data.Entity.dll"); parameters.ReferencedAssemblies.Add("System.ComponentModel.dll"); foreach (var type in types) { parameters.ReferencedAssemblies.Add(type.Assembly.Location); } parameters.ReferencedAssemblies.Add(typeof(DbSet).Assembly.Location); parameters.ReferencedAssemblies.Add(typeof(DbContext).Assembly.Location); parameters.ReferencedAssemblies.Add(typeof(IQueryable).Assembly.Location); parameters.ReferencedAssemblies.Add(typeof(IQueryable<>).Assembly.Location); parameters.ReferencedAssemblies.Add(typeof(System.ComponentModel.IListSource).Assembly.Location); parameters.GenerateExecutable = false; parameters.GenerateInMemory = false; parameters.OutputAssembly = "ProjectContext.dll"; CompilerResults results = provider.CompileAssemblyFromSource(parameters, code.ToString()); if (results.Errors.HasErrors) { StringBuilder sb = new StringBuilder(); foreach (CompilerError error in results.Errors) { sb.AppendLine(String.Format("Error ({0}): {1}", error.ErrorNumber, error.ErrorText)); } throw new InvalidOperationException(sb.ToString()); } } }
new ContextGenerator().Generate(entities, types.ToArray()); // generate dbContext
حال برای استفاده از Context تولید شده، به صورت زیر شیءایی را ساخته:
static DbContext _dbContext=null; public static DbContext GetDbContextInstance() { if (_dbContext == null) { string path = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); var dllversionAssm = Assembly.LoadFile(path + "\\ProjectContext.dll"); Type type = dllversionAssm.GetType("DbContextGenerator.TestContext"); _dbContext = (DbContext)Activator.CreateInstance(type); } return _dbContext; }
و سپس برای ساخت DbSet از هر Entity به کد زیر نیاز خواهیم داشت:
public static System.Data.Entity.DbSet<T> Get<T>() where T : class { var set = GetDbContextInstance().Set<T>(); return set; }
هم اکنون میتوان رکوردهای Entityها را واکشی کرده و یا آنها را با یکدیگر Join بزنیم:
var products = Get<Product>().ToList(); var productTypes = Get<ProductType>().ToList(); var query = from p in Get<Product>() join pt in Get<ProductType>() on p.ProductTypeId equals pt.Id select new { Id = p.Id, Name = p.Name, ProductType = pt.Name }; var JoinResult = query.ToList();
و نتیجه واکشی ها
عملگرهای پرس و جوی تبدیل، توالیهایی را که از جنس <IEnumerable<T هستند، به انواع دیگر مجموعه تبدیل میکنند.
از عملگرهای پرس و جوی زیر میتوان برای تبدیل توالیها استفاده کرد :
- OfType
- Cast
- ToArray
- ToList
- ToDictionary
- ToLookup
عملگر OfType
این عملگر عناصری از توالی را که نوع آنها را مشخص میکنیم باز میگرداند.
امضاء عملگر پرس و جوی OfType به صورت زیر است :
public static IEnumerable<TResult> OfType<TResult>(this IEnumerable source)
IEnumerable input = new object[] { "Apple", 33, "Sugar", 44, 'a', new DateTime()}; IEnumerable<string> query = input.OfType<string>(); foreach (var item in query) { Console.WriteLine(item); }
Apple Sugar
مثال :کد زیر یک ساختار سلسله مراتبی شیء گرا را نمایش میدهد:
class Ingredient { public string Name { get; set; } } class DryIngredient : Ingredient { public int Grams { get; set; } } class WetIngredient : Ingredient { public int Millilitres { get; set; } }
IEnumerable<Ingredient> input = new Ingredient[] { new DryIngredient { Name = "Flour" }, new WetIngredient { Name = "Milk" }, new WetIngredient { Name = "Water" } }; IEnumerable<WetIngredient> query = input.OfType<WetIngredient>(); foreach (WetIngredient item in query) { Console.WriteLine(item.Name); }
Milk Water
پیاده سازی توسط عبارتهای جستجو
معادل این عملگر، کلمهی کلیدی جدیدی در عبارتهای جستجو وجود ندارد و ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگر Cast
عملگر Cast همانند عملگر OfType رفتار میکند. این عملگر یک توالی ورودی را دریافت و بر اساس نوع مشخص شده، توالی خروجی را تولید میکند. همهی عناصر توالی ورودی به نوع مشخص شده Cast میشوند. اما بر عکس عملگر OfType که عناصری را که با نوع دادهی ما سازگاری نداشت، نادیده میگرفت، این عملگر در صورت عدم موفقیت در عملیات تغییر نقش (Cast)، یک استثناء را پرتاب میکند.
IEnumerable input = new object[] { "Apple", 33, "Sugar", 44, 'a', new DateTime() }; IEnumerable<string> query = input.Cast<string>(); foreach (string item in query) { Console.WriteLine(item); }
Unhandled Exception: System.InvalidCastException: Unable to cast object of type 'System.Int32' to type 'System.String'.
پیاده سازی توسط عبارتهای جستجو
کلمهی کلیدی جایگزینی برای عملگر Cast، در عبارتهای جستجو وجود ندارد.این عملگر با استفاده از متغیر Range که در مطالب قبلی این سری معرفی شد، قابل پیاده سازی میباشد.
IEnumerable input = new object[]{ "Apple", "Sugar", "Flour" }; IEnumerable<string> query = from string i in input select i; foreach (var item in query) { Console.WriteLine(item); }
عملگر ToArray
عملگر ToArray یک توالی ورودی را دریافت و یک توالی خروجی را به صورت آرایه تولید میکند. این عملگر باعث اجرای سریع پرس و جو میشود و رفتار پیش فرض LINQ را که اجرای با تاخیر میباشد، تحریف/بازنویسی (Override) میکند.
مثال: در این مثال یک توالی از نوع <IEnumerable<string به یک آرایه رشتهای تبدیل شده است (تبدیل لیست به آرایه).
IEnumerable<string> input = new List<string> { "Apple", "Sugar", "Flour" }; string[] array = input.ToArray();
پیاده سازی توسط عبارتهای جستجو
معادل این عملگر، کلمهی کلیدی جدیدی در عبارتهای جستجو وجود ندارد و ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگر ToList
عملگر ToList همچون ToArray، اجرای با تاخیر را نادیده میگیرد. عملگر ToList همانطور که از نامش پیداست، توالی خروجی را بهصورت لیست مهیا میکند.
مثال:
IEnumerable<string> input = new[] { "Apple", "Sugar", "Flour" }; List<string> list = input.ToList();
پیاده سازی توسط عبارتهای جستجو
معادل این عملگر، کلمهی کلیدی جدیدی در عبارتهای جستجو وجود ندارد و ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگر ToDictionary
این عملگر توالی ورودی را به یک دیکشنری جنریک تبدیل میکند (<Dictinary<TKey,TValue) .
سادهترین امضاء عملگر ToDictionary، یک عبارت Lambda میباشد. این عبارت Lambda نشان دهندهی یک تابع است که عنصر کلید(Key) را در دیکشنری، مشخص میکند.
مثال:
class Recipe { public int Id { get; set; } public string Name { get; set; } public int Rating { get; set; } } IEnumerable<Recipe> recipes = new[] { new Recipe { Id = 1, Name = "Apple Pie", Rating = 5 }, new Recipe { Id = 2, Name = "Cherry Pie", Rating = 2 }, new Recipe { Id = 3, Name = "Beef Pie", Rating = 3 } }; Dictionary<int, Recipe> dict = recipes.ToDictionary(x => x.Id); foreach (KeyValuePair<int, Recipe> item in dict) { Console.WriteLine($"Key={item.Key}, Recipe={item.Value}"); }
خروجی مثال بالا:
Key=1, Recipe=Apple Pie Key=2, Recipe=Cherry Pie Key=3, Recipe=Beef Pie
پیاده سازی توسط عبارتهای جستجو
معادل این عملگر، کلمهی کلیدی جدیدی در عبارتهای جستجو وجود ندارد و ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگر ToLookup
این عملگر رفتاری شبیه به عملگر ToDictionary را دارد، اما به جای تولید خروجی از نوع دیکشنری، نمونهای از جنس ILookUp را ایجاد میکند.
در کد زیر خروجی ایجاد شده توسط lookup دستورالعملها (Recipes) را بر حسب امتیاز آنها گروه بندی کرده است. در این مثال کلید، بر حسب Byte میباشد.
مثال :
class Recipe { public int Id { get; set; } public string Name { get; set; } public byte Rating { get; set; } } IEnumerable<Recipe> recipes = new[] { new Recipe { Id = 1, Name = "Apple Pie", Rating = 5 }, new Recipe { Id = 1, Name = "Banana Pie", Rating = 5 }, new Recipe { Id = 2, Name = "Cherry Pie", Rating = 2 }, new Recipe { Id = 3, Name = "Beef Pie", Rating = 3 } }; ILookup<byte, Recipe> look = recipes.ToLookup(x => x.Rating); foreach (IGrouping<byte, Recipe> ratingGroup in look) { byte rating = ratingGroup.Key; Console.WriteLine($"Rating {rating}"); foreach (var recipe in ratingGroup) { Console.WriteLine($" - {recipe.Name}"); } }
Rating 5 - Apple Pie - Banana Pie Rating 2 - Cherry Pie Rating 3 - Beef Pie
پیاده سازی توسط عبارتهای جستجو
معادل این عملگر، کلمهی کلیدی جدیدی در عبارتهای جستجو وجود ندارد و ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگرهای عناصر Element Operators
این عملگرها، یک توالی ورودی را دریافت و تنها یک عنصر از توالی ورودی و یا یک عنصر را به عنوان عنصر پیش فرض باز میگردانند. این نوع عملگرها توالی خروجی را تولید نمیکنند.
عملگر First
این عملگر اولین عنصر توالی را باز میگرداند.
مثال :
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 150}, new Ingredient {Name = "Flour", Calories = 50}, new Ingredient {Name = "Butter", Calories = 500} }; Ingredient element = ingredients.First(); Console.WriteLine(element.Name);
Sugar
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 150}, new Ingredient {Name = "Flour", Calories = 50}, new Ingredient {Name = "Butter", Calories = 500} }; Ingredient element = ingredients.First(x=>x.Calories==150); Console.WriteLine(element.Name);
Milk
Unhandled Exception: System.InvalidOperationException: Sequence contains no elements
Ingredient[] ingredients = { }; Ingredient element = ingredients.First();
Unhandled Exception: System.InvalidOperationException: Sequence contains no matching element
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 150}, new Ingredient {Name = "Flour", Calories = 50}, new Ingredient {Name = "Butter", Calories = 500} }; Ingredient element = ingredients.First(x=>x.Calories==1500);
پیاده سازی توسط عبارتهای جستجو
معادل این عملگر، کلمهی کلیدی جدیدی در عبارتهای جستجو وجود ندارد و ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگر FirstOrDefault
عملگر FirstOrDefalt همانند عملگر First عمل میکند، اما با این تفاوت که به جای پرتاب یک استثناء در شرایط معرفی شده در عملگر First، یک مقدار پیش فرض را بر اساس نوع عناصر توالی باز میگرداند. در صورتیکه توالی از نوع عددی باشد، مقدار 0 و اگر عناصر توالی از انواع ارجاعی باشند، مقدار Null و برای مقادیر منطقی، ارزش False بهعنوان مقادیر پیش فرض باز گردانده میشوند.
Ingredient[] ingredients = { }; Ingredient element = ingredients.FirstOrDefault(); Console.WriteLine(element == null);
True
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 150}, new Ingredient {Name = "Flour", Calories = 50}, new Ingredient {Name = "Butter", Calories = 500} }; Ingredient element = ingredients.FirstOrDefault(x=>x.Calories==1500); Console.WriteLine(element==null);
True
پیاده سازی توسط عبارتهای جستجو
معادل این عملگر، کلمهی کلیدی جدیدی در عبارتهای جستجو وجود ندارد و ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگر Last
این عملگر آخرین عنصر توالی را باز میگرداند. همچون عملگر First، این عملگر نیز یک امضاء برای دریافت یک عبارت شرط یا پیش بینی دارد. این پیش بینی، آخرین عنصری را که شرط را تامین کند، باز میگرداند. باز هم مثل عملگر First، در صورتی که توالی هیچ عنصری نداشته باشد و یا عدم تامین شرط توسط عناصر توالی، استثنایی رخ خواهد داد.
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 150}, new Ingredient {Name = "Flour", Calories = 50}, new Ingredient {Name = "Butter", Calories = 500} }; Ingredient element = ingredients.Last(x=>x.Calories==500); Console.WriteLine(element.Name);
Flour
پیاده سازی توسط عبارتهای جستجو
معادل این عملگر، کلمهی کلیدی جدیدی در عبارتهای جستجو وجود ندارد و ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگر LastOrDefault
این عملگر همچون عملگر FirstOrDefault عمل میکند. از بروز استثناء جلوگیری کرده و مقدار پیش فرض را به خروجی ارسال میکند.
پیاده سازی توسط عبارتهای جستجو
معادل این عملگر، کلمهی کلیدی جدیدی در عبارتهای جستجو وجود ندارد و ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگر Single
عملگر Single ، تنها عنصر توالی ورودی را باز میگرداند.در صورتی که توالی ما بیش از یک عنصر داشته باشد و یا توالی هیچ عنصری نداشته باشد، یک استثناء رخ خواهد داد.
Unhandled Exception: System.InvalidOperationException: Sequence contains more than one matching element Unhandled Exception: System.InvalidOperationException: Sequence contains no matching element
Ingredient[] ingredients = { new Ingredient { Name = "Sugar", Calories = 500 } }; Ingredient element = ingredients.Single(); Console.WriteLine(element.Name);
Sugar
Ingredient[] ingredients = { new Ingredient { Name = "Sugar", Calories = 500 }, new Ingredient {Name = "Butter", Calories = 150}, new Ingredient {Name = "Milk", Calories = 500} }; Ingredient element = ingredients.Single(x => x.Calories == 150); Console.WriteLine(element.Name);
Butter
پیاده سازی توسط عبارتهای جستجو
معادل این عملگر، کلمهی کلیدی جدیدی در عبارتهای جستجو وجود ندارد و ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگر SingleOrDefault
عملگر SingleOrDefault همچون عملگر Single عمل میکند؛ اما با این تفاوت که اگر توالی هیچ عنصری نداشته باشد، مقدار پیش فرض نوع توالی، باز گردانده میشود و در صورتیکه هیچ عنصری شرط مشخص شده را تامین نکند، باز هم مقدار پیش فرض توالی، به جای رخ دادن استثناء باز گردانده میشود.
Ingredient[] ingredients = { new Ingredient { Name = "Sugar", Calories = 500 }, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 50} }; Ingredient element = ingredients.SingleOrDefault(x => x.Calories == 9999); Console.WriteLine(element==null);
True
پیاده سازی توسط عبارتهای جستجو
معادل این عملگر، کلمهی کلیدی جدیدی در عبارتهای جستجو وجود ندارد و ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگر ElementAt
عملگر ElementAt عنصری را در یک جایگاه مشخص شدهی در توالی، باز میگرداند.
مثال: در کد زیر سومین عنصر توالی ورودی انتخاب میشود:
Ingredient[] ingredients = { new Ingredient { Name = "Sugar", Calories = 500 }, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 50} }; Ingredient element = ingredients.ElementAt(2); Console.WriteLine(element.Name);
Milk
System.ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection.
پیاده سازی توسط عبارتهای جستجو
معادل این عملگر، کلمهی کلیدی جدیدی در عبارتهای جستجو وجود ندارد و ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگر ElementAtOrDefualt
عملگر ElementAtOrDefualt نیز همچون عملگر ElementAt کار میکند؛ اما در صورت وارد کردن اندیسی بزرگتر از اندیس مجاز توالی، دیگر یک استثناء رخ نخواهد داد و یک مقدار پیش فرض، بر اساس نوع عناصر توالی باز گردانده میشود.
Ingredient[] ingredients = { new Ingredient { Name = "Sugar", Calories = 500 }, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 50} }; Ingredient element = ingredients.ElementAtOrDefault(5); Console.WriteLine(element==null);
True
پیاده سازی توسط عبارتهای جستجو
معادل این عملگر، کلمهی کلیدی جدیدی در عبارتهای جستجو وجود ندارد و ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگر DefaultIfEmpty
عملگر DefaultIfEmpty یک توالی را دریافت کرده و به دو شکل عمل میکند:
1- اگر توالی شامل حداقل یک عنصر باشد، این توالی بدون هیچ تغییری به خروجی ارسال میشود.
2- اگر توالی هیچ عنصری نداشته باشد، توالی خروجی خالی نخواهد بود. در این حالت توالی خروجی تنها یک عضو دارد و آن هم مقدار پیش فرضی بر اساس نوع توالی میباشد.
مثال :
Ingredient[] ingredients = { new Ingredient { Name = "Sugar", Calories = 500 }, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 50} }; IEnumerable<Ingredient> query = ingredients.DefaultIfEmpty(); foreach (Ingredient item in query) { Console.WriteLine(item.Name); }
Sugar Egg Milk
کد زیر حالت دوم معرفی شدهی در تعریف DefaultIfEmpty را نشان میدهد.
Ingredient[] ingredients = { }; IEnumerable<Ingredient> query = ingredients.DefaultIfEmpty(); foreach (Ingredient item in query) { Console.WriteLine(item == null); }
True
پیاده سازی توسط عبارتهای جستجو
معادل این عملگر، کلمهی کلیدی جدیدی در عبارتهای جستجو وجود ندارد و ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
اصل چهارم: Starve for loosely coupled designs
"به دنبال طراحی با اتصال سست بین اجزا باش"
اتصال بین اجزای برنامه نویسی باعث سختتر شدن مدیریت تغییرات میشود؛ چرا که با تغییر یک بخش، بخشهای متصل نیز دچار مشکل خواهند شد. اتصالها از لحاظ نوع قدرت متفاوتند و اساسا سیستمی بدون اتصال وجود ندارد. لذا باید به دنبال یک طراحی با کمترین میزان قدرت اتصال یا همان سست اتصال باشیم.
تا به اینجا، اصلهای دوم و سوم ما را در کاهش وابستگی و اتصال قوی کمک کردهاند. استفاده از واسطها، باعث کاهش وابستگی به نوع پیاده سازی میشود. استفاده از ترکیب نیز به نوعی باعث از بین رفتن وابستگی قوی بین کلاسهای فرزند و کلاس والد میشود و با روشی دیگر (استفاده از شیء در برگرفته شده برای پیاده سازی وظیفهی تغییر کننده) وظایف را در کلاسها پیاده سازی میکند. در زیر نمونهی اتصال قوی و نتیجهی آن را میبینیم:
public class StrongCoupledConcreteA { public string GenerateString(string s) { return s + " from" + this.GetType().ToString(); } } public class StrongCoupledConcreteB { public void GenerateString(ref string s) { s += " from" + this.GetType().ToString(); } } public class Printer { bool condition; public Printer(bool cond) { condition = cond; } public void SetCondition(bool value) { condition = value; } public void Print() { string result; string input = " this message is"; if (condition) { var stringGenerator = new StrongCoupledConcreteA(); result = stringGenerator.GenerateString(input); } else { var stringGenerator = new StrongCoupledConcreteB(); result = input; stringGenerator.GenerateString(ref result); } Console.WriteLine(result); } } public class Context { Printer printer; public void DoWork() { printer = new Printer(true); printer.Print(); printer.SetCondition(false); printer.Print(); } }
حال کد بازنویسی شده را با آن مقایسه کنید:
public interface IStringGenerator { string GenerateString(string s); } public class LooslyCoupledConcreteA : IStringGenerator { public string GenerateString(string s) { return s + " from " + this.GetType().ToString(); } } public class LooslyCoupledConcreteB : IStringGenerator { public string GenerateString(string s) { return s + " from " + this.GetType().ToString(); } } public class Printer { bool condition; public Printer(bool cond) { condition = cond; } public void SetCondition(bool value) { condition = value; } public void Print() { string result; string input = " this message is"; IStringGenerator generator; if (condition) { generator = new LooslyCoupledConcreteA(); } else { generator = new LooslyCoupledConcreteB(); } result = generator.GenerateString(input); Console.WriteLine(result); } }
با کمی دقت مشاهده میکنیم که در کلاسهای strongly coupled با اینکه هدف هر دو کلاس تولید یک رشته است، ولی عدم وجود پروتکل باعث شده است نحوهی گرفتن ورودی و برگرداندن خروجی متفاوت شود و در نتیجه نیازمند به اضافه کردن پیچیدگی در کلاس فراخوانی کنندهی آنها میشویم. این در حالی است که در روش loosely coupled با ایجاد یک پروتکل (واسط IStringGenerator ) این پیچیدگی از بین رفته است. در اینجا نوع اتصال (وابستگی) از جنس اتصال (وابستگی) قوی به تعریف (prototype) و شاید به نوعی نحوهی پیاده سازی متد میباشد.
SOLID Principles *
پنج اصل بعدی به اصول SOLID معروف هستند.
S: Single Responsibility
O: Open/Closed
L: Liskov’s Substitution
I: Interface Segregation
D: Dependency Injection
اصل پنجم: Single responsibility
"به دنبال ماژولهای تک مسئولیتی باش"
در این قسمت مقصود از مسئولیت، «دلیلی است که کلاس باید تغییر کند» بدین معنا که اگر کلاسی با چند دلیل متفاوت مجبور به تغییر شود، آن کلاس چند مسئولیتی است. کلاسهای چند مسئولیتی عموما کد حجیمی دارند؛ نام آنها تعریف دقیقی را از مسئولیتشان ارائه نمیدهد و با عنوانی بسیار کلی نامگذاری میشوند و اشکال زدایی آنها بسیار طاقت فرساست. از طرفی، چند مسئولیتی بودن یک کلاس، باعث از بین رفتن مزایای توارث میشود. مثلا فرض کنید دو مسئولیت A,B در واسطی بیان میشوند که به یکدیگر مرتبط نبوده و مستقلند. برای مسئولیت A دو پیاده سازی و برای مسئولیت B، سه پیاده سازی در نظر گرفته شده است و جمعا برای پشتیبانی از تمامی حالات باید شش کلاس پیاده ساز، در نظر گرفته شود که توارث را سخت و بی معنی میکند زیرا قابلیت استفاده مجدد را از توارث سلب کرده است. با این وجود عملا رعایت همچین نکتهای در دنیای واقعی کار سختی است.
مثال زیر این مشکل را بیان میدارد:
// single responsibility principle - bad example interface IEmail { void SetSender(string sender); void SetReceiver(string receiver); void SetContent(string content); } class Email : IEmail { public void SetSender(string sender) { throw new NotImplementedException(); } public void SetReceiver(string receiver) { throw new NotImplementedException(); } public void SetContent(string content) { throw new NotImplementedException(); } }
در این مثال کلاس Email دارای دو مسئولیت (دلیل برای تغییر) است: الف- نحوه مقداردهی فرستنده و گیرنده براساس پروتکلهای مختلف مانند IMAP, POP3 ، بدین معنا که با تغییر پروتکل نیاز به تغییر پیاده سازی خواهیم شد. ب- تعریف محتوای پیام، بدین معنا که برای پشتیبانی از محتوای html, xml نیاز به تغییر کلاس Email داریم.
با تغییر طراحی خواهیم داشت:
// single responsibility principle - good example public interface IMessage { void SetSender(string sender); void SetReceiver(string receiver); void SetContent(IContent content); } public interface IContent { string GetAsString(); // used for serialization } public class Email : IMessage { public void SetSender(string sender) { throw new NotImplementedException(); } public void SetReceiver(string receiver) { throw new NotImplementedException(); } public void SetContent(IContent content) { throw new NotImplementedException(); } }
در اینجا واسط IContent مسئولیت پشتیبانی از xml, html را
خواهد داشت و نیازی به تغییر کلاس Email برای
پشتیبانی از این فرمتهای محتوای پیام را نخواهیم داشت.
اصل ششم: Open for
extension, close for modification : Open/Closed Principle
"پذیرای توسعه و
بازدارنده از تغییر هر آنچه که هست، باش"
ا ین اصل میگوید طراحی باید به گونهای باشد که با
اضافه شدن یک ویژگی، کدهای قبلی تغییری نکنند و فقط کدهای جدید برای پیاده سازی
ویژگی جدید نوشته شوند.
public class AreaCalculator { public double Area(object[] shapes) { double area = 0; foreach (var shape in shapes) { if (shape is Square) { Square square = (Square)shape; area += Math.Sqrt(square.Height); } if (shape is Triangle) { Triangle triangle = (Triangle)shape; double TotalHalf = (triangle.FirstSide + triangle.SecondSide + triangle.ThirdSide) / 2; area += Math.Sqrt(TotalHalf * (TotalHalf - triangle.FirstSide) * (TotalHalf - triangle.SecondSide) * (TotalHalf - triangle.ThirdSide)); } if (shape is Circle) { Circle circle = (Circle)shape; area += circle.Radius * circle.Radius * Math.PI; } } return area; } } public class Square { public double Height { get; set; } } public class Circle { public double Radius { get; set; } } public class Triangle { public double FirstSide { get; set; } public double SecondSide { get; set; } public double ThirdSide { get; set; } }
در اینجا کلاس AreaCalculator برای محاسبه مساحت تمام اشیاء ورودی، مساحت تک تک اشیاء را محاسبه میکند و نتیجه را برمیگرداند. در این مثال با اضافه شدن شکل هندسی جدید، باید کد این کلاس تغییر کند که با اصل Open/Closed مغایر است. برای بهبود این کد طراحی زیر پیشنهاد شده است:
public class AreaCalculator { public double Area(Shape[] shapes) { double area = 0; foreach (var shape in shapes) { area += shape.Area(); } return area; } } public abstract class Shape { public abstract double Area(); } public class Square : Shape { public double Height { get { return _height; } } private double _height; public Square(double Height) { _height = Height; } public override double Area() { return Math.Sqrt(_height); } } public class Circle : Shape { public double Radius { get { return _radius; } } private double _radius; public Circle(double Radius) { _radius = Radius; } public override double Area() { return _radius * _radius * Math.PI; } } public class Triangle : Shape { public double FirstSide { get { return _firstSide; } } public double SecondSide { get { return _secondSide; } } public double ThirdSide { get { return _thirdSide; } } private double _firstSide; private double _secondSide; private double _thirdSide; public Triangle(double FirstSide, double SecondSide, double ThirdSide) { _firstSide = FirstSide; _secondSide = SecondSide; _thirdSide = ThirdSide; } public override double Area() { double TotalHalf = (_firstSide + _secondSide + _thirdSide) / 2; return Math.Sqrt(TotalHalf * (TotalHalf - _firstSide) * (TotalHalf - _secondSide) * (TotalHalf - _thirdSide)); } }
در این طراحی، پیچیدگی محاسبه مساحت هر شکل به کلاس آن شکل منتقل شده است و با اضافه شدن شکل جدید نیازی به تغییر کلاس AreaCalculator نداریم.
در مقالهی بعدی به سه اصل دیگر اصول SOLID خواهم پرداخت.
ساخت یک Mini ORM با AutoMapper
کلاس پایه AdoMapper
public abstract class AdoMapper<T> where T : class { private readonly SqlConnection _connection; protected AdoMapper(string connectionString) { _connection = new SqlConnection(connectionString); } protected virtual IEnumerable<T> ExecuteCommand(SqlCommand command) { command.Connection = _connection; command.CommandType = CommandType.StoredProcedure; _connection.Open(); try { var reader = command.ExecuteReader(); try { return Mapper.Map<IDataReader, IEnumerable<T>>(reader); } finally { reader.Close(); } } finally { _connection.Close(); } } protected virtual T GetRecord(SqlCommand command) { command.Connection = _connection; _connection.Open(); try { var reader = command.ExecuteReader(); try { reader.Read(); return Mapper.Map<IDataReader, T>(reader); } finally { reader.Close(); } } finally { _connection.Close(); } } protected virtual IEnumerable<T> GetRecords(SqlCommand command) { command.Connection = _connection; _connection.Open(); try { var reader = command.ExecuteReader(); try { return Mapper.Map<IDataReader, IEnumerable<T>>(reader); } finally { reader.Close(); } } finally { _connection.Close(); } } }
نحوهی استفاده از کلاس پایه AdoMapper
در کدهای ذیل نحوهی ارث بری از کلاس پایه AdoMapper و سپس استفاده از متدهای آنرا ملاحظه میکنید:
public class UsersService : AdoMapper<User>, IUsersService { public UsersService(string connectionString) : base(connectionString) { } public IEnumerable<User> GetAll() { using (var command = new SqlCommand("SELECT * FROM Users")) { return GetRecords(command); } } public User GetById(int id) { using (var command = new SqlCommand("SELECT * FROM Users WHERE Id = @id")) { command.Parameters.Add(new SqlParameter("id", id)); return GetRecord(command); } } }
تعریف پروفایل مخصوص AutoMapper
ORMهای تمام عیار، کار نگاشت فیلدهای بانک اطلاعاتی را به خواص اشیاء دات نتی، به صورت خودکار انجام میدهند. در اینجا همانند روشهای متداول کار با AutoMapper نیاز است این نگاشت را به صورت دستی یکبار تعریف کرد:
public class UsersProfile : Profile { protected override void Configure() { this.CreateMap<IDataRecord, User>(); } public override string ProfileName { get { return this.GetType().Name; } } }
Mapper.Initialize(cfg => // In Application_Start() { cfg.AddProfile<UsersProfile>(); });
سفارشی سازی نگاشتهای AutoMapper
فرض کنید کلاس Advertisement زیر، معادل است با جدول Advertisements بانک اطلاعاتی؛ با این تفاوت که در کلاس تعریف شده، خاصیت TitleWithOtherName تطابقی با هیچکدام از فیلدهای بانک اطلاعاتی ندارد. بنابراین اطلاعاتی نیز به آن نگاشت نخواهد شد.
public class Advertisement { public int Id { set; get; } public string Title { get; set; } public string Description { get; set; } public int UserId { get; set; } public string TitleWithOtherName { get; set; } }
public class AdvertisementsProfile : Profile { protected override void Configure() { this.CreateMap<IDataRecord, Advertisement>() .ForMember(dest => dest.TitleWithOtherName, options => options.MapFrom(src => src.GetString(src.GetOrdinal("Title")))); } public override string ProfileName { get { return this.GetType().Name; } } }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید.
نکته : برای فهم بهتر مطالب آشنایی اولیه با مفاهیم WCF الزامی است.
ابتدا مدل زیر را در نظر بگیرید:
[DataContract] public class Book { [DataMember] public int Code { get; set; } [DataMember] public string Name { get; set; } }
[ServiceContract] public interface ISampleService { [OperationContract] IEnumerable<Book> GetAll(); [OperationContract] void Save( Book book ); }
public class SampleService : ISampleService { public List<Book> ListOfBook { get; private set; } public SampleService() { ListOfBook = new List<Book>(); } public IEnumerable<Book> GetAll() { ListOfBook.AddRange( new Book[] { new Book(){Code=1 , Name="Book1"}, new Book(){Code=2 , Name="Book2"}, } ); return ListOfBook; } public void Save( Book book ) { ListOfBook.Add( book ); } }
حالا یک پروژه Console Application بسازید و از روش AddServiceReference سرویس مورد نظر را به Client اضافه کنید. برنامه را تست کنید. بدون هیچ مشکلی کار میکند.
حالا اگر در نسخه بعدی سیستم مجبور شویم به مدل Book یک خاصیت دیگر به نام Author را نیز اضافه کنیم و امکان Update کردن سرویس در سمت کلاینت وجود نداشته باشد چه اتفاقی خواهد افتاد.
به صورت زیر:
[DataContract] public class Book { [DataMember] public int Code { get; set; } [DataMember] public string Name { get; set; } [DataMember] public string Author { get; set; } }
نکته : برای Value Typeها مقادیر پیش فرض و برای Reference Typeها مقدار Null.
اگر برای DataMemberAttribute خاصیت IsRequired را برابر true کنیم از این پس برای هر درخواستی که مقدار Author آن مقدار نداشته باشد یک Protocol Exception پرتاب میشود. به صورت زیر:
[DataMember( IsRequired = true )] public string Author { get; set; }
روش دیگر این است که Desrialize کردن مدل را تغییر دهیم. بدین معنی که هر گاه مقدار Author برابر Null بود یک مقدار پیش فرض برای آن در نظر بگیریم. این کار با نوشتن یک متد و قراردادن OnDeserializingAttribute به راحتی امکان پذیر است. کلاس Book به صورت زیر تغییر میکند.
[DataContract] public class Book { [DataMember] public int Code { get; set; } [DataMember] public string Name { get; set; } [DataMember( IsRequired = true )] public string Author { get; set; } [OnDeserializing] private void OnDeserializing( StreamingContext context ) { if ( string.IsNullOrEmpty( Author ) ) { Author = "Masoud Pakdel"; } } }
روش بعدی استفاده از اینترفیس IExtensibleDataObject است. بعد از اینکه کلاس Book این اینترفیس را پیاده سازی کرد مشکل Versioning Round Trip حل میشود. به این صورت که سرویس یا کلاینتی که نسخه قدیمی را میشناسد اگر نسخه جدید را دریافت کند خصوصیاتی را که نمیشناسد مثل Author در خاصیت ExtensionData ذخیره میشود و هنگامی که کلاس Book برای سرویس یا کلاینتی که نسخه جدید را میشناسد DataContractSerializer اطلاعات مورد نظر را از خصوصیت ExtensionData بیرون میکشد و کلاس Book جدید را باز سازی میکند. بررسی کلاس ExtensionData توسط خود DataContractSreializer انجام میشود و نیاز به هیچ گونه ای کد نویسی ندارد.
[DataContract] public class Book : IExtensibleDataObject { [DataMember] public int Code { get; set; } [DataMember] public string Name { get; set; } [DataMember] public string Author { get; set; } public virtual ExtensionDataObject ExtensionData { get { return _extensionData; } set { _extensionData = value; } } private ExtensionDataObject _extensionData; }
public IEnumerable<Book> GetAll() { ListOfBook.AddRange( new Book[] { new Book(){Code=1 , Name="Book1", Author="Masoud Pakdel"}, new Book(){Code=2 , Name="Book2" }, } ); return ListOfBook; }
همان طور که میبینید این نسخه از کلاینت هیچ گونه اطلاعی از وجود یک خاصیت به نام Author ندارد ولی از طریق ExtensionData متوجه میشود یک خاصیت به نام Author به مدل سمت سرور اضافه شده است.
اما در صورتی که قصد داشته باشیم که یک سرویس خاص از همان نسخه قدیمی کلاس Book استفاده کند و نیاز به نسخه جدید آن نداشته باشد میتوانیم این کار را از طریق مقدار دهی True به خاصیت IgnoreExtensionDataObject در ServiceBehaviorAttribute انجام داد. بدین شکل
[ServiceBehavior( IgnoreExtensionDataObject = true )] public class SampleService : ISampleService
منابع :