9-4. Web API 的客戶端實現修改跟蹤問題我們想通過客戶端更新實體類,調用基於REST的Web API 服務實現把一個對象圖的插入、刪除和修改等資料庫操作。此外, 我們想通過EF6的Code First方式實現對數據的訪問。本例,我們模擬一個N層場景,用單獨的控制台應用程式作為客戶端,調用W...
9-4. Web API 的客戶端實現修改跟蹤
問題
我們想通過客戶端更新實體類,調用基於REST的Web API 服務實現把一個對象圖的插入、刪除和修改等資料庫操作。此外, 我們想通過EF6的Code First方式實現對數據的訪問。
本例,我們模擬一個N層場景,用單獨的控制台應用程式作為客戶端,調用Web API服務(web api項目)。
註:每個層用一個單獨的解決方案,這樣有助於調試和模擬N層應用。
解決方案
假設我們一個如Figure 9-4.所示模型
Figure 9-4. A 客戶和電話模型
我們的模型展示了客戶與對應的電話信息.我們把模型和資料庫代碼封裝至Web Api服務之後,讓客戶端利用HTTP來插入,更新,刪除對象。
以下步驟建立服務項目::
1.新建ASP.NET MVC 4 Web 應用程式,在嚮導中選擇Web API模板,把項目命名為: Recipe4.Service.
2.向項目添加一個新的WEB API控制器,名為: CustomerController.
3. 添加如Listing 9-19所示的BaseEntity類,作為實體類的基類和枚舉類型的TrackingState.基類包含TrackingState屬性,客戶端通過它操縱實體對象, 它接受一個TrackingState枚舉值
註意: TrackingState不會持久化到資料庫
創建我們自己的內部跟蹤狀態枚舉是為了讓客戶端保存實體狀態,這是EF的跟蹤狀態所需要的。DbContext 文件OnModelCreating方法里不映射TrackingState屬性到資料庫
Listing 9-19. Entity Base Class and TrackingState Enum Type
public class BaseEntity
{
protected BaseEntity()
{
TrackingState = TrackingState.Nochange;
}
public TrackingState TrackingState { get; set; }
}
public enum TrackingState
{
Nochange,
Add,
Update,
Remove,
}
4. 添加Listing 9-20所示的Customer 和Phone實體類
Listing 9-20. Customer and Phone Entity Classes
public class Customer:BaseEntity
{
public Customer()
{
Phones = new HashSet<Phone>();
}
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; }
}
5. 添加EF6的引用。最好是藉助 NuGet 包管理器來添加。在”引用”上右擊,選擇”管理 NuGet 程式包.從“聯機”標簽頁,定位並安裝EF6包。這樣將會下載,安裝並配置好EF6庫到你的項目中.
6. 添加類,名為:Recipe4Context, 鍵入 Listing 9-21 里的代碼,
確保它繼承自 DbContext 類. 註意EF6的“不映射基類型”的配置,我們定義一個”ignore“規範,指定不映射的屬性。
■■註意
=======================================================================
R owan Martin,微軟的EF團隊主管發佈了一篇名為Configuring Unmapped Base Types的博客: http://romiller.com/2013/01/29/ef6-code-first-configuringunmapped-base-types/. 也請認真閱讀Rowan的其它在EF上傑出的博客文章。
=======================================================================
Listing 9-21. Context Class
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)
{
//不要把TrackingState 屬性持久化,它只是用來跟蹤與Web api服務脫離的實體的狀態
//該屬性定義在基類里。
modelBuilder.Types<BaseEntity>().Configure(x => x.Ignore(y => y.TrackingState));
modelBuilder.Entity<Customer>().ToTable("Chapter9.Customer");
modelBuilder.Entity<Phone>().ToTable("Chapter9.Phone");
}
}
7. 把Listing 9-22所示的 “ Recipe4ConnectionString “連接字元串添加到Web.Config 文件的ConnectionStrings 節里.
Listing 9-22. Connection string for the Recipe1 Web API Service
<connectionStrings>
<add name="Recipe4ConnectionString"
connectionString="Data Source=.;
Initial Catalog=EFRecipes;
Integrated Security=True;
MultipleActiveResultSets=True"
providerName="System.Data.SqlClient" />
</connectionStrings>
8. 把Listing 9-23里的代碼添加到Global.asax 文件的Application_Start 方法里. 該代碼禁止EF進行模型相容性檢查,和JSON忽略序列時對象迴圈引用的問題。
Listing 9-23. Disable the Entity Framework Model Compatibility Check
protected void Application_Start()
{
//禁止EF進行模型相容性檢查
Database.SetInitializer<Recipe4Context>(null);
//使JSON序列化器忽略迴圈引用的問題 GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling
= Newtonsoft.Json.ReferenceLoopHandling.Ignore;
...
}
9. 添加一個類,名為: EntityStateFactory,插入Listing 9-24里的代碼。該工廠類會把客戶端TrackingState狀態枚舉值轉譯成EF里的實體狀態.
Listing 9-24. Customer Web API Controller
public class EntityStateFactory
{
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;
}
}
}
最後, 用Listing 9-25里的代碼替換CustomerController里的。
Listing 9-25. Customer Web API Controller
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())
{
context.Customers.Add(customer);
//把對象圖添加到Context(狀態為Added),然後它在客戶端被設置的基數狀態枚舉值翻譯成相應的實體狀態
//(包括父與子實體,也就是customer和phone)
foreach (var entry in context.ChangeTracker.Entries<BaseEntity>())
{
entry.State = EntityStateFactory.Set(entry.Entity.TrackingState);
if (entry.State == EntityState.Modified)
{
//先把實體狀態設為'Unchanged'
//再讓實體的原始副本等於從資料庫里獲取的副本
//這親,EF就會跟蹤每個屬性的狀態,會標誌出被修改過的屬性
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 chapter9.phone");
context.Database.ExecuteSqlCommand("delete from chapter9.customer");
return Request.CreateResponse(HttpStatusCode.OK);
}
}
}
10. 接下來新建一個解決方案,新建一個Recipe4.Client控制台應用程式,讓它來調用上面創建的服務。
11. 用Listing 9-26.代碼替換program.cs 里的代碼
Listing 9-26. Our Windows Console Application That Serves as Our Test Client
class Program
{
private HttpClient _client;
private Customer _bush, _obama;
private Phone _whiteHousePhone, _bushMobilePhone, _obamaMobilePhone;
private HttpResponseMessage _response;
static void Main(string[] args)
{
Task t = Run();
t.Wait();
Console.WriteLine("\npress <enter>to continue...");
Console.ReadLine();
}
private static async Task Run()
{
var program = new Program();
program.ServiceSetup();
//等待Cleanup執行結束
await program.CleanupAsync();
program.CreateFirstCustomer();
//等待AddCustomerAsync執行結束
await program.AddCustomerAsync();
program.CreateSecondCustomer();
//等待AddSecondCustomerAsync執行結束
await program.AddSecondCustomerAsync();
//等待RemoveFirstCustomerAsync執行結束
await program.RemoveFirstCustomerAsync();
//等待FetchCustomersAsync執行結束
await program.FetchCustomersAsync();
}
private void ServiceSetup()
{
//初始化對WEB API服務調用的對象
_client = new HttpClient { BaseAddress = new Uri("http://localhost:6658/") };
//添加頭部信息,設為可JSON的類型
_client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}
private async Task CleanupAsync()
{
//調用服務端的cleanup
_response = await _client.DeleteAsync("api/customer/cleanup/");
}
private void CreateFirstCustomer()
{
//創建1號客戶和他的兩個電話
_bush = new Customer
{
Name = "George Bush",
Company = "Ex President",
//設置狀態為Add,以告知服務端該實體是新增的
TrackingState = TrackingState.Add,
};
_whiteHousePhone = new Phone
{
Number = "212 222-2222",
PhoneType = "White House Red Phone",
//設置狀態為Add,以告知服務端該實體是新增的
TrackingState = TrackingState.Add,
};
_bushMobilePhone = new Phone
{
Number = "212 333-3333",
PhoneType = "Bush Mobile Phone",
//設置狀態為Add,以告知服務端該實體是新增的
TrackingState = TrackingState.Add,
};
_bush.Phones.Add(_whiteHousePhone);
_bush.Phones.Add(_bushMobilePhone);
}
private async Task AddCustomerAsync()
{
//構造對服務端UpdateCustomer的調用
_response = await _client.PostAsync("api/customer/updatecustomer/", _bush,
new JsonMediaTypeFormatter());
if (_response.IsSuccessStatusCode)
{
//獲取在服務端新創建的包含實際ID值的實體
_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 Number(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()
{
//創建第二號客戶和他的電話
_obama = new Customer
{
Name = "Barack Obama",
Company = "President",
//設置狀態為Add,以告知服務端該實體是新增的
TrackingState = TrackingState.Add,
};
_obamaMobilePhone = new Phone
{
Number = "212 444-4444",
PhoneType = "Obama Mobile Phone",
//設置狀態為Add,以告知服務端該實體是新增的
TrackingState = TrackingState.Add,
};
//設置狀態為update,以告知服務端實體是修改的
_whiteHousePhone.TrackingState = TrackingState.Update;
_obama.Phones.Add(_obamaMobilePhone);
_obama.Phones.Add(_whiteHousePhone);
}
private async Task AddSecondCustomerAsync()
{
//構造調用服務端UpdateCustomer的請求
_response = await _client.PostAsync("api/customer/updatecustomer/",
_obama, new JsonMediaTypeFormatter());
if (_response.IsSuccessStatusCode)
{
//獲取服務端新建的含有正確ID的實體
_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()
{
//從資料庫中刪除George Bush(客戶)
//先獲取George Bush實體,用包含一個參數的請求調用服務端的get方法
var query = "api/customer/" + _bush.CustomerId;
_response = _client.GetAsync(query).Result;
if (_response.IsSuccessStatusCode)
{
_bush = await _response.Content.ReadAsAsync<Customer>();
//標誌實體為Remove,告知服務端應該刪除它
_bush.TrackingState = TrackingState.Remove;
//由於在刪除父實體前必須刪除子實體,所以必須刪除它的電話
foreach (var phoneType in _bush.Phones)
{
//標誌實體為Remove,告知服務端應該刪除它
phoneType.TrackingState = TrackingState.Remove;
}
_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()
{
//獲取所有現存的客戶及對應的電話
_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);
}
}
}//end class program
12. 最後,添加與服務端同樣的Customer, Phone, BaseEntity, 和TrackingState(同Listing 9-19 and 9-20).
下麵Listing 9-26是控制台的輸出結果:
===================================================================
Successfully created Customer Geroge Bush and 2 Phone Numbers(s)
Added Phone Type: White House Red Phone
Added Phone Type: Bush Mobile Phone
Successfully created Customer Barrack Obama and 2 Phone Numbers(s)
Added Phone Type: Obama Mobile Phone
Added Phone Type: White House Red Phone
Removed Geroge Bush from database
Remove Bush Mobile Phone from data store
Customer Barrack Obama has 2 Phone Numbers(s)
Phone Type: White House Red Phone
Phone Type: Obama Mobile Phone
===================================================================
它是如何工作的?
首先啟動Web API應用程式. 當看到首頁就服務已經可用,接著打開控制台應用程式,在. program.cs首行代碼前加斷點,運行,我們首先創建與Web API管道連接並配置頭部多媒體信息類型,使它請求可以接收JSON格式.接著用HttpClient的DeleteAsyn請求,調用服務端的Cleanup方法,從而清除資料庫里之前保存的結果。
接下來我們創建一個新的Customer和兩個phone對象,註意我們是如何明確地為每個實體設置它的TrackingState屬性來告訴EF狀態跟蹤為每個實體產生什麼樣的SQL語句。
接著利用httpclient的PostAsync方法調用服務端的UpdateCustomer方法.如果你在WEB API項目的控制器的UpdateCustomer 方法前加斷點,你會看到該方法參數接收到一個Customer 對象,然後它立刻添加到context 對象里,指定實體為added,並會跟蹤它的狀態。
有興的是:我們接著鉤住context對象比較隱蔽的DbChangeTracker屬性
DbChangeTracker用<DbEntityEntry>的方式為實體暴露一個常用的Ienumerable類型. 我們簡單地分配基類的EntityType 屬性給它,能這麼做是因為我們所有的實體都是繼承自BaseEntity
. 每次迴圈我們都調用一次EntityStateFactory的Set方法,來確定實體的狀態,如果EntityType屬性值為Add,則實體狀態為Added, 如果EntityType屬性值為Update, 則實體狀態為Modified,如果實體狀態為Modified,我們需要做些額外的處理,我們先把實體狀態從Modified改為Unchanged
然後調用GetDatabaseValues 方法,獲得資料庫的值版本,然後賦給實體的OriginalValues ,這個EF跟蹤引擎會檢測到實體的哪個屬性的原始值與當前值不同,並把這些屬性狀態設為modified
. 隨後的 SaveChanges 操作將會僅更新這些修改過的屬性,並生成相應的SQL語句。
再看客戶端程式,演示了通過設置TrackingState屬性來添加,修改,和刪除實體對象
服務端的UpdateCustomer方法簡單地把TrackingState翻譯成了實體的狀態,cotext對象會根據這些狀態產生相應的SQL語句。
在本小節,我們看到了,可以將EF的數據操作封裝到Web API 服務里。客戶端可以通過HttpClient對象調用這些服務. 讓所有實體繼承於一個擁有TrackingState屬性的基類,客戶端通過設置這個屬性,讓服務端的EF跟蹤引擎生成對應的SQL語句。
在實際應用中,我們可能更喜歡創建一個新的層(Visual Studio 類庫),把EF的數據操作從WEB API服務里分離出來,作為一個單獨的層。
本節更重要的是,我們不難做到讓客戶端使用普通的類型來跟蹤實體的變化. 而這個功能可重用於我們其它的項目。