Today we are releasing the first preview of Visual Studio 2017 version 15.5. You can either download it from here, or if you already have Preview installed, you’ll receive a notification that the update is available.
Download: Visual Studio 2013 Update 5 (2013.5) RC
NET 5.0 RC 1. منتشر شد
Today, we are shipping .NET 5.0 Release Candidate 1 (RC1). It is a near-final release of .NET 5.0, and the first of two RCs before the official release in November. RC1 is a “go live” release; you are supported using it in production.
معرفی NET Community Toolkit.
تدارک ساختار ابتدایی این مطلب
در اینجا اینترفیسی را که بیانگر ساختار شیء شخص است، به صورت ذیل ایجاد میکنیم:
export interface Person { name: string; age: number; }
export class LinqTestsComponent { people: Person[] = [ { name: "User 4", age: 27 }, { name: "User 5", age: 42 }, { name: "User 6", age: 8 }, { name: "User 1", age: 20 }, { name: "User 2", age: 35 }, { name: "User 3", age: 78 } ]; }
همچنین سه متد ذیل را نیز برای لاگ کردن عنوان آزمایش، نمایش محتوای آرایهی اصلی و نمایش نتیجهی آزمایش، به این کلاس اضافه میکنیم:
logTitle(title: string) { console.log(`%c${title}`, "background: #222; color: #bada55"); } logOriginalArray() { console.log(`original this.people:${JSON.stringify(this.people)}`); } logResult(message: string, result: any) { console.log(`${message}:${JSON.stringify(result)}`); }
معادل متد Where در TypeScript
متد filter که جزو متدهای توکار ES5 است، میتواند معادلی برای متد Where، جهت فیلتر کردن عناصر بر اساس یک خاصیت، یا چندین خاصیت باشد:
equivalentToWhere() { const youngerThan40 = this.people.filter(person => person.age < 40); // ECMAScript 5 this.logResult("People younger than 40", youngerThan40); // Filtering on Multiple Criteria const youngsters = this.people.filter( person => person.age < 40 && person.name.toLocaleLowerCase().indexOf("user") !== -1); this.logResult("Users younger than 40", youngsters); }
People younger than 40:[ {"name":"User 4","age":27}, {"name":"User 6","age":8}, {"name":"User 1","age":20}, {"name":"User 2","age":35} ] Users younger than 40:[ {"name":"User 4","age":27}, {"name":"User 6","age":8}, {"name":"User 1","age":20}, {"name":"User 2","age":35} ]
معادل متد Any در TypeScript
متد some که جزو متدهای توکار ES5 است، میتواند معادلی برای متد Any باشد. اگر یکی از عناصر آرایه، بر اساس شرط تعیین شده یافت شود، این متد مقدار true را باز میگرداند:
equivalentToAny() { const anyUnder40 = this.people.some(person => person.age < 40); // ECMAScript 5 this.logResult("Are any people under 40?", anyUnder40); // true // Filtering on Criteria Matching any Object Properties const filterBy = "user"; const anyUsers = this.people.filter(person => Object.keys(person).some(property => { let value = (<any>person)[property]; if (typeof value === "string") { value = value.toLocaleLowerCase(); } return value.toString().indexOf(filterBy) !== -1; }) ); this.logResult("anyUsers", anyUsers); }
Are any people under 40?:true anyUsers:[ {"name":"User 4","age":27}, {"name":"User 5","age":42}, {"name":"User 6","age":8}, {"name":"User 1","age":20}, {"name":"User 2","age":35}, {"name":"User 3","age":78} ]
در مثال دوم، جستجویی بر روی تمام خواص شیء شخص انجام شدهاست. در اینجا توسط متد Object.keys، لیست خواص شیء یافت شدهاست. سپس بر روی این لیست توسط متد some، بررسی شدهاست که آیا خاصیت رشتهای وجود دارد که مساوی عبارت filterBy باشد؟ حاصل این بررسی به عنوان شرط متد filter جهت بازگشت آرایهی متناظری از اشخاص یافت شده، استفاده شدهاست.
معادل متد Contains در TypeScript
متد includes که جزو متدهای توکار ES7 است، میتواند معادلی برای متد Contains باشد و کار آن بررسی وجود عنصری در یک لیست است:
equivalentToContains() { const searchElement: Person = { name: "User 4", age: 27 }; const containsUser4 = this.people.includes(searchElement); // ECMAScript 2016 = ECMAScript 7 this.logResult("Contains searchElement", containsUser4); // false -> only compares by reference and not by value. const indexOfUser4 = this.people.indexOf(searchElement); // ECMAScript 5 this.logResult("indexOfUser4", indexOfUser4); // -1 -> only compares by reference and not by value. const stringifiedObj = JSON.stringify(searchElement); const includesUser4 = this.people.some(person => JSON.stringify(person) === stringifiedObj); this.logResult("includesUser4", includesUser4); // true -> compares by by value. }
یکی از روشهای مقایسهی بر اساس تمام مقادیر خواص یک شیء، استفاده از متد JSON.stringify است که اگر آنرا با متد some ترکیب کنیم، میتوان به نتیجهی مطلوبی رسید:
Contains searchElement:false indexOfUser4:-1 includesUser4:true
معادل متد All در TypeScript
متد every که جزو متدهای توکار ES5 است، میتواند معادلی برای متد All باشد و کار آن بررسی صحت شرط اعمالی، بر روی تک تک عناصر لیست است. اگر این بررسی با موفقیت صورت گرفت، مقدار true را بازگشت میدهد:
equivalentToAll() { const allUnder30 = this.people.every(person => person.age < 30); // ECMAScript 5 this.logResult("Are all people under 30?", allUnder30); // false }
Are all people under 30?:false
معادل متدهای First و FirstOrDefault در TypeScript
میتوان از متدهای filter و یا find بومی ES5 و ES 6 برای شبیه سازی متدهای First (یافتن اولین عنصر درخواستی در یک لیست) و FirstOrDefault استفاده کرد:
equivalentToFirstOrDefault() { const vahidOrDefault = this.people.filter(item => item.name === "Vahid")[0] || null; // ECMAScript 5 this.logResult("vahidOrDefault", vahidOrDefault); const user1OrDefault = this.people.find(item => item.name === "User 1") || null; // ECMAScript 2015 = ECMAScript 6 this.logResult("user1OrDefault", user1OrDefault); }
معادل متد FindIndex در TypeScript
متد findIndex که جزو متدهای توکار ES6 است، میتواند معادلی برای متد FindIndex در جهت یافتن ایندکس عنصری در یک لیست، بر اساس شرط درخواستی، باشد.
equivalentToFindIndex() { const index = this.people.findIndex(person => person.age === 8); // ECMAScript 2015 = ECMAScript 6 this.logResult("index of the user with age 8", index) }
index of the user with age 8:2
معادل متد Select در TypeScript
متد map که جزو متدهای توکار ES5 است، میتواند معادلی برای متد Select، برای تغییر شکل نهایی خروجی یک لیست باشد:
equivalentToSelect() { const names = this.people.map(person => person.name); // ECMAScript 5 this.logResult("Selected the names of people", names); }
Selected the names of people:["User 4","User 5","User 6","User 1","User 2","User 3"]
معادل متد Aggregate در TypeScript
متد reduce که جزو متدهای توکار ES5 است، میتواند شبیه به متد Aggregate عمل کند و لیستی از عناصر را به یک مقدار کاهش دهد:
equivalentToAggregate() { // ECMAScript 5 const aggregate = this.people.reduce((person1, person2) => { return { name: "", age: person1.age + person2.age }; }); this.logResult("Aggregate age", aggregate.age); // { age: 210 } }
Aggregate age:210
معادل متد ForEach در TypeScript
متد forEach که جزو متدهای توکار ES5 است، میتواند معادلی برای متد ForEach باشد که روشی functional برای پیمودن عناصر یک لیست است:
equivalentToForEach() { // ECMAScript 5 this.people.forEach(person => { this.logResult("person", person); }); }
person:{"name":"User 4","age":27} person:{"name":"User 5","age":42} person:{"name":"User 6","age":8} person:{"name":"User 1","age":20} person:{"name":"User 2","age":35} person:{"name":"User 3","age":78}
معادل متد OrderBy در TypeScript
متد sort که جزو متدهای توکار ES5 است، میتواند معادلی برای متد OrderBy باشد که جهت مرتب سازی عناصر یک لیست از آن استفاده میشود:
// ECMAScript 5 let orderedByAgeAscending = this.people.sort((person1, person2) => { const a = person1.age; const b = person2.age; return a > b ? 1 : -1; }); this.logResult("Ordered by age ascending", orderedByAgeAscending);
- مساوی صفر باشد، تغییری را به وجود نمیآورد.
- کمتر از صفر باشد، اولین عنصر را قبل از دومین عنصر قرار میدهد.
- بیشتر از صفر باشد، دومین عنصر را پس از اولین عنصر قرار میدهد.
منطق مقایسهی فوق را به صورت ذیل نیز میتوان خلاصه کرد:
orderedByAgeAscending = this.people.sort((person1, person2) => person1.age - person2.age); this.logResult("Ordered by age ascending", orderedByAgeAscending);
Ordered by age ascending:[ {"name":"User 6","age":8}, {"name":"User 1","age":20}, {"name":"User 4","age":27}, {"name":"User 2","age":35}, {"name":"User 5","age":42}, {"name":"User 3","age":78} ]
const orderedByName = this.people.sort((person1, person2) => { // name1.localeCompare(name2) // is case insensitive // or use toUpperCase() to ignore character casing const name1 = person1.name.toUpperCase(); const name2 = person2.name.toUpperCase(); return name1 > name2 ? 1 : -1; }) this.logResult("Ordered by name", orderedByName); this.logOriginalArray();
Ordered by name:[ {"name":"User 1","age":20}, {"name":"User 2","age":35}, {"name":"User 3","age":78}, {"name":"User 4","age":27}, {"name":"User 5","age":42}, {"name":"User 6","age":8} ] original this.people:[ {"name":"User 1","age":20}, {"name":"User 2","age":35}, {"name":"User 3","age":78}, {"name":"User 4","age":27}, {"name":"User 5","age":42}, {"name":"User 6","age":8} ]
امکان ترکیب زنجیروار متدهای کار بر روی لیستها در TypeScript
همانند LINQ، در اینجا نیز میتوان متدهای فوق را به صورت زنجیروار بر روی یک لیست فراخوانی و اجرا کرد:
chainFunctionCalls() { const namesOfPeopleOver30OrderedDesc = this.people .filter(person => person.age > 30) .map(person => person.name) .sort((name1, name2) => { // name1.localeCompare(name2) // is case insensitive // or use toUpperCase() to ignore character casing name1 = name1.toUpperCase(); name2 = name2.toUpperCase(); return name2 > name1 ? 1 : -1; }); this.logResult("the names of all people over 30 ordered by name descending", namesOfPeopleOver30OrderedDesc); }
the names of all people over 30 ordered by name descending:["User 5","User 3","User 2"]
معادل متد Skip در TypeScript
متد splice که جزو متدهای توکار ES5 است، میتواند شبیه به متد Skip عمل کند. این متد آرایهای را بازگشت میدهد که حاوی عناصری است که پس از تعداد ذکر شده، در آرایهی اصلی وجود دارند:
equivalentToSkip() { const skip2 = this.people.splice(2); // ECMAScript 5 this.logResult("skip2 -> the deleted elements", skip2); this.logOriginalArray(); }
skip2 -> the deleted elements:[ {"name":"User 3","age":78}, {"name":"User 4","age":27}, {"name":"User 5","age":42}, {"name":"User 6","age":8} ] original this.people:[ {"name":"User 1","age":20}, {"name":"User 2","age":35} ]
ردیابی تغییرات در سمت کلاینت توسط Web API
فرض کنید میخواهیم از سرویسهای REST-based برای انجام عملیات CRUD روی یک Object graph استفاده کنیم. همچنین میخواهیم رویکردی در سمت کلاینت برای بروز رسانی کلاس موجودیتها پیاده سازی کنیم که قابل استفاده مجدد (reusable) باشد. علاوه بر این دسترسی دادهها توسط مدل Code-First انجام میشود.
در مثال جاری یک اپلیکیشن کلاینت (برنامه کنسول) خواهیم داشت که سرویسهای ارائه شده توسط پروژه Web API را فراخوانی میکند. هر پروژه در یک Solution مجزا قرار دارد، با این کار یک محیط n-Tier را شبیه سازی میکنیم.
مدل زیر را در نظر بگیرید.
همانطور که میبینید مدل مثال جاری مشتریان و شماره تماس آنها را ارائه میکند. میخواهیم مدلها و کد دسترسی به دادهها را در یک سرویس Web API پیاده سازی کنیم تا هر کلاینتی که به HTTP دسترسی دارد بتواند از آن استفاده کند. برای ساخت سرویس مذکور مراحل زیر را دنبال کنید.
- در ویژوال استودیو پروژه جدیدی از نوع ASP.NET Web Application بسازید و قالب پروژه را Web API انتخاب کنید. نام پروژه را به Recipe4.Service تغییر دهید.
- کنترلر جدیدی با نام CustomerController به پروژه اضافه کنید.
- کلاسی با نام BaseEntity ایجاد کنید و کد آن را مطابق لیست زیر تغییر دهید. تمام موجودیتها از این کلاس پایه مشتق خواهند شد که خاصیتی بنام TrackingState را به آنها اضافه میکند. کلاینتها هنگام ویرایش آبجکت موجودیتها باید این فیلد را مقدار دهی کنند. همانطور که میبینید این خاصیت از نوع TrackingState enum مشتق میشود. توجه داشته باشید که این خاصیت در دیتابیس ذخیره نخواهد شد. با پیاده سازی enum وضعیت ردیابی موجودیتها بدین روش، وابستگیهای EF را برای کلاینت از بین میبریم. اگر قرار بود وضعیت ردیابی را مستقیما از EF به کلاینت پاس دهیم وابستگیهای بخصوصی معرفی میشدند. کلاس DbContext اپلیکیشن در متد OnModelCreating به EF دستور میدهد که خاصیت TrackingState را به جدول موجودیت نگاشت نکند.
public abstract class BaseEntity { protected BaseEntity() { TrackingState = TrackingState.Nochange; } public TrackingState TrackingState { get; set; } } public enum TrackingState { Nochange, Add, Update, Remove, }
- کلاسهای موجودیت Customer و PhoneNumber را ایجاد کنید و کد آنها را مطابق لیست زیر تغییر دهید.
public class Customer : BaseEntity { public int CustomerId { get; set; } public string Name { get; set; } public string Company { get; set; } public virtual ICollection<Phone> Phones { get; set; } } public class Phone : BaseEntity { public int PhoneId { get; set; } public string Number { get; set; } public string PhoneType { get; set; } public int CustomerId { get; set; } public virtual Customer Customer { get; set; } }
- با استفاده از NuGet Package Manager کتابخانه Entity Framework 6 را به پروژه اضافه کنید.
- کلاسی با نام Recipe4Context ایجاد کنید و کد آن را مطابق لیست زیر تغییر دهید. در این کلاس از یکی از قابلیتهای جدید EF 6 بنام "Configuring Unmapped Base Types" استفاده کرده ایم. با استفاده از این قابلیت جدید هر موجودیت را طوری پیکربندی میکنیم که خاصیت TrackingState را نادیده بگیرند. برای اطلاعات بیشتر درباره این قابلیت EF 6 به این لینک مراجعه کنید.
public class Recipe4Context : DbContext { public Recipe4Context() : base("Recipe4ConnectionString") { } public DbSet<Customer> Customers { get; set; } public DbSet<Phone> Phones { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { // Do not persist TrackingState property to data store // This property is used internally to track state of // disconnected entities across service boundaries. // Leverage the Custom Code First Conventions features from Entity Framework 6. // Define a convention that performs a configuration for every entity // that derives from a base entity class. modelBuilder.Types<BaseEntity>().Configure(x => x.Ignore(y => y.TrackingState)); modelBuilder.Entity<Customer>().ToTable("Customers"); modelBuilder.Entity<Phone>().ToTable("Phones"); } }
- فایل Web.config پروژه را باز کنید و رشته اتصال زیر را به قسمت ConnectionStrings اضافه نمایید.
<connectionStrings> <add name="Recipe4ConnectionString" connectionString="Data Source=.; Initial Catalog=EFRecipes; Integrated Security=True; MultipleActiveResultSets=True" providerName="System.Data.SqlClient" /> </connectionStrings>
- فایل Global.asax را باز کنید و کد زیر را به متد Application_Start اضافه نمایید. این کد بررسی Entity Framework Model Compatibility را غیرفعال میکند و به JSON serializer دستور میدهد که self-referencing loop خواص پیمایشی را نادیده بگیرد. این حلقه بدلیل رابطه bidirectional بین موجودیتهای Customer و PhoneNumber بوجود میآید.
protected void Application_Start() { // Disable Entity Framework Model Compatibilty Database.SetInitializer<Recipe1Context>(null); // The bidirectional navigation properties between related entities // create a self-referencing loop that breaks Web API's effort to // serialize the objects as JSON. By default, Json.NET is configured // to error when a reference loop is detected. To resolve problem, // simply configure JSON serializer to ignore self-referencing loops. GlobalConfiguration.Configuration.Formatters.JsonFormatter .SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore; ... }
- کلاسی با نام EntityStateFactory بسازید و کد آن را مطابق لیست زیر تغییر دهید. این کلاس مقدار خاصیت TrackingState که به کلاینتها ارائه میشود را به مقادیر متناظر کامپوننتهای ردیابی EF تبدیل میکند.
public static EntityState Set(TrackingState trackingState) { switch (trackingState) { case TrackingState.Add: return EntityState.Added; case TrackingState.Update: return EntityState.Modified; case TrackingState.Remove: return EntityState.Deleted; default: return EntityState.Unchanged; } }
- در آخر کد کنترلر CustomerController را مطابق لیست زیر بروز رسانی کنید.
public class CustomerController : ApiController { // GET api/customer public IEnumerable<Customer> Get() { using (var context = new Recipe4Context()) { return context.Customers.Include(x => x.Phones).ToList(); } } // GET api/customer/5 public Customer Get(int id) { using (var context = new Recipe4Context()) { return context.Customers.Include(x => x.Phones).FirstOrDefault(x => x.CustomerId == id); } } [ActionName("Update")] public HttpResponseMessage UpdateCustomer(Customer customer) { using (var context = new Recipe4Context()) { // Add object graph to context setting default state of 'Added'. // Adding parent to context automatically attaches entire graph // (parent and child entities) to context and sets state to 'Added' // for all entities. context.Customers.Add(customer); foreach (var entry in context.ChangeTracker.Entries<BaseEntity>()) { entry.State = EntityStateFactory.Set(entry.Entity.TrackingState); if (entry.State == EntityState.Modified) { // For entity updates, we fetch a current copy of the entity // from the database and assign the values to the orginal values // property from the Entry object. OriginalValues wrap a dictionary // that represents the values of the entity before applying changes. // The Entity Framework change tracker will detect // differences between the current and original values and mark // each property and the entity as modified. Start by setting // the state for the entity as 'Unchanged'. entry.State = EntityState.Unchanged; var databaseValues = entry.GetDatabaseValues(); entry.OriginalValues.SetValues(databaseValues); } } context.SaveChanges(); } return Request.CreateResponse(HttpStatusCode.OK, customer); } [HttpDelete] [ActionName("Cleanup")] public HttpResponseMessage Cleanup() { using (var context = new Recipe4Context()) { context.Database.ExecuteSqlCommand("delete from phones"); context.Database.ExecuteSqlCommand("delete from customers"); return Request.CreateResponse(HttpStatusCode.OK); } } }
- در ویژوال استودیو پروژه جدیدی از نوع Console Application بسازید و نام آن را به Recipe4.Client تغییر دهید.
- فایل program.cs را باز کنید و کد آن را مطابق لیست زیر تغییر دهید.
internal class Program { private HttpClient _client; private Customer _bush, _obama; private Phone _whiteHousePhone, _bushMobilePhone, _obamaMobilePhone; private HttpResponseMessage _response; private static void Main() { Task t = Run(); t.Wait(); Console.WriteLine("\nPress <enter> to continue..."); Console.ReadLine(); } private static async Task Run() { var program = new Program(); program.ServiceSetup(); // do not proceed until clean-up completes await program.CleanupAsync(); program.CreateFirstCustomer(); // do not proceed until customer is added await program.AddCustomerAsync(); program.CreateSecondCustomer(); // do not proceed until customer is added await program.AddSecondCustomerAsync(); // do not proceed until customer is removed await program.RemoveFirstCustomerAsync(); // do not proceed until customers are fetched await program.FetchCustomersAsync(); } private void ServiceSetup() { // set up infrastructure for Web API call _client = new HttpClient { BaseAddress = new Uri("http://localhost:62799/") }; // add Accept Header to request Web API content negotiation to return resource in JSON format _client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue ("application/json")); } private async Task CleanupAsync() { // call the cleanup method from the service _response = await _client.DeleteAsync("api/customer/cleanup/"); } private void CreateFirstCustomer() { // create customer #1 and two phone numbers _bush = new Customer { Name = "George Bush", Company = "Ex President", // set tracking state to 'Add' to generate a SQL Insert statement TrackingState = TrackingState.Add, }; _whiteHousePhone = new Phone { Number = "212 222-2222", PhoneType = "White House Red Phone", // set tracking state to 'Add' to generate a SQL Insert statement TrackingState = TrackingState.Add, }; _bushMobilePhone = new Phone { Number = "212 333-3333", PhoneType = "Bush Mobile Phone", // set tracking state to 'Add' to generate a SQL Insert statement TrackingState = TrackingState.Add, }; _bush.Phones.Add(_whiteHousePhone); _bush.Phones.Add(_bushMobilePhone); } private async Task AddCustomerAsync() { // construct call to invoke UpdateCustomer action method in Web API service _response = await _client.PostAsync("api/customer/updatecustomer/", _bush, new JsonMediaTypeFormatter()); if (_response.IsSuccessStatusCode) { // capture newly created customer entity from service, which will include // database-generated Ids for all entities _bush = await _response.Content.ReadAsAsync<Customer>(); _whiteHousePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _bush.CustomerId); _bushMobilePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _bush.CustomerId); Console.WriteLine("Successfully created Customer {0} and {1} Phone Numbers(s)", _bush.Name, _bush.Phones.Count); foreach (var phoneType in _bush.Phones) { Console.WriteLine("Added Phone Type: {0}", phoneType.PhoneType); } } else Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase); } private void CreateSecondCustomer() { // create customer #2 and phone numbers _obama = new Customer { Name = "Barack Obama", Company = "President", // set tracking state to 'Add' to generate a SQL Insert statement TrackingState = TrackingState.Add, }; _obamaMobilePhone = new Phone { Number = "212 444-4444", PhoneType = "Obama Mobile Phone", // set tracking state to 'Add' to generate a SQL Insert statement TrackingState = TrackingState.Add, }; // set tracking state to 'Modifed' to generate a SQL Update statement _whiteHousePhone.TrackingState = TrackingState.Update; _obama.Phones.Add(_obamaMobilePhone); _obama.Phones.Add(_whiteHousePhone); } private async Task AddSecondCustomerAsync() { // construct call to invoke UpdateCustomer action method in Web API service _response = await _client.PostAsync("api/customer/updatecustomer/", _obama, new JsonMediaTypeFormatter()); if (_response.IsSuccessStatusCode) { // capture newly created customer entity from service, which will include // database-generated Ids for all entities _obama = await _response.Content.ReadAsAsync<Customer>(); _whiteHousePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _obama.CustomerId); _bushMobilePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _obama.CustomerId); Console.WriteLine("Successfully created Customer {0} and {1} Phone Numbers(s)", _obama.Name, _obama.Phones.Count); foreach (var phoneType in _obama.Phones) { Console.WriteLine("Added Phone Type: {0}", phoneType.PhoneType); } } else Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase); } private async Task RemoveFirstCustomerAsync() { // remove George Bush from underlying data store. // first, fetch George Bush entity, demonstrating a call to the // get action method on the service while passing a parameter var query = "api/customer/" + _bush.CustomerId; _response = _client.GetAsync(query).Result; if (_response.IsSuccessStatusCode) { _bush = await _response.Content.ReadAsAsync<Customer>(); // set tracking state to 'Remove' to generate a SQL Delete statement _bush.TrackingState = TrackingState.Remove; // must also remove bush's mobile number -- must delete child before removing parent foreach (var phoneType in _bush.Phones) { // set tracking state to 'Remove' to generate a SQL Delete statement phoneType.TrackingState = TrackingState.Remove; } // construct call to remove Bush from underlying database table _response = await _client.PostAsync("api/customer/updatecustomer/", _bush, new JsonMediaTypeFormatter()); if (_response.IsSuccessStatusCode) { Console.WriteLine("Removed {0} from database", _bush.Name); foreach (var phoneType in _bush.Phones) { Console.WriteLine("Remove {0} from data store", phoneType.PhoneType); } } else Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase); } else { Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase); } } private async Task FetchCustomersAsync() { // finally, return remaining customers from underlying data store _response = await _client.GetAsync("api/customer/"); if (_response.IsSuccessStatusCode) { var customers = await _response.Content.ReadAsAsync<IEnumerable<Customer>>(); foreach (var customer in customers) { Console.WriteLine("Customer {0} has {1} Phone Numbers(s)", customer.Name, customer.Phones.Count()); foreach (var phoneType in customer.Phones) { Console.WriteLine("Phone Type: {0}", phoneType.PhoneType); } } } else { Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase); } } }
- در آخر کلاسهای Customer, Phone و BaseEntity را به پروژه کلاینت اضافه کنید. چنین کدهایی بهتر است در لایه مجزایی قرار گیرند و بین لایههای مختلف اپلیکیشن به اشتراک گذاشته شوند.
اگر اپلیکیشن کلاینت را اجرا کنید با خروجی زیر مواجه خواهید شد.
شرح مثال جاری
با اجرای اپلیکیشن Web API شروع کنید. این اپلیکیشن یک MVC Web Controller دارد که پس از اجرا شما را به صفحه خانه هدایت میکند. در این مرحله سایت در حال اجرا است و سرویسها قابل دسترسی هستند.
سپس اپلیکیشن کنسول را باز کنید و روی خط اول کد فایل program.cs یک breakpoint قرار داده و آن را اجرا کنید. ابتدا آدرس سرویس را نگاشت میکنیم و از سرویس درخواست میکنیم که اطلاعات را با فرمت JSON بازگرداند.
سپس توسط متد DeleteAsync که روی آبجکت HttpClient تعریف شده است اکشن متد Cleanup را روی سرویس فراخوانی میکنیم. این فراخوانی تمام دادههای پیشین را حذف میکند.
در قدم بعدی یک مشتری بهمراه دو شماره تماس میسازیم. توجه کنید که برای هر موجودیت مشخصا خاصیت TrackingState را مقدار دهی میکنیم تا کامپوننتهای Change-tracking در EF عملیات لازم SQL برای هر موجودیت را تولید کنند.
سپس توسط متد PostAsync که روی آبجکت HttpClient تعریف شده اکشن متد UpdateCustomer را روی سرویس فراخوانی میکنیم. اگر به این اکشن متد یک breakpoint اضافه کنید خواهید دید که موجودیت مشتری را بعنوان یک پارامتر دریافت میکند و آن را به context جاری اضافه مینماید. با اضافه کردن موجودیت به کانتکست جاری کل object graph اضافه میشود و EF شروع به ردیابی تغییرات آن میکند. دقت کنید که آبجکت موجودیت باید Add شود و نه Attach.
قدم بعدی جالب است، هنگامی که از خاصیت DbChangeTracker استفاده میکنیم. این خاصیت روی آبجکت context تعریف شده و یک <IEnumerable<DbEntityEntry را با نام Entries ارائه میکند. در اینجا بسادگی نوع پایه EntityType را تنظیم میکنیم. این کار به ما اجازه میدهد که در تمام موجودیت هایی که از نوع BaseEntity هستند پیمایش کنیم. اگر بیاد داشته باشید این کلاس، کلاس پایه تمام موجودیتها است. در هر مرحله از پیمایش (iteration) با استفاده از کلاس EntityStateFactory مقدار خاصیت TrackingState را به مقدار متناظر در سیستم ردیابی EF تبدیل میکنیم. اگر کلاینت مقدار این فیلد را به Modified تنظیم کرده باشد پردازش بیشتری انجام میشود. ابتدا وضعیت موجودیت را از Modified به Unchanged تغییر میدهیم. سپس مقادیر اصلی را با فراخوانی متد GetDatabaseValues روی آبجکت Entry از دیتابیس دریافت میکنیم. فراخوانی این متد مقادیر موجود در دیتابیس را برای موجودیت جاری دریافت میکند. سپس مقادیر بدست آمده را به کلکسیون OriginalValues اختصاص میدهیم. پشت پرده، کامپوننتهای EF Change-tracking بصورت خودکار تفاوتهای مقادیر اصلی و مقادیر ارسالی را تشخیص میدهند و فیلدهای مربوطه را با وضعیت Modified علامت گذاری میکنند. فراخوانیهای بعدی متد SaveChanges تنها فیلدهایی که در سمت کلاینت تغییر کرده اند را بروز رسانی خواهد کرد و نه تمام خواص موجودیت را.
در اپلیکیشن کلاینت عملیات افزودن، بروز رسانی و حذف موجودیتها توسط مقداردهی خاصیت TrackingState را نمایش داده ایم.
متد UpdateCustomer در سرویس ما مقادیر TrackingState را به مقادیر متناظر EF تبدیل میکند و آبجکتها را به موتور change-tracking ارسال میکند که نهایتا منجر به تولید دستورات لازم SQL میشود.
نکته: در اپلیکیشنهای واقعی بهتر است کد دسترسی دادهها و مدلهای دامنه را به لایه مجزایی منتقل کنید. همچنین پیاده سازی فعلی change-tracking در سمت کلاینت میتواند توسعه داده شود تا با انواع جنریک کار کند. در این صورت از نوشتن مقادیر زیادی کد تکراری جلوگیری خواهید کرد و از یک پیاده سازی میتوانید برای تمام موجودیتها استفاده کنید.