第九章 在N層結構的應用程式中使用EF不是所有的應用都能完全地寫入到一個單個的過程中(就是駐留在一個單一的物理層中),實際上,在當今不斷發展的網路世界,大量的應用程式的結構包含經典的表現層,應用程,和數據層,並且它們可能分佈在多台電腦上,被分佈到一臺單獨的電腦上的應用程式的某個領域的邏輯層,並不...
第九章 在N層結構的應用程式中使用EF
不是所有的應用都能完全地寫入到一個單個的過程中(就是駐留在一個單一的物理層中),實際上,在當今不斷發展的網路世界,大量的應用程式的結構包含經典的表現層,應用程,和數據層,並且它們可能分佈在多台電腦上,被分佈到一臺單獨的電腦上的應用程式的某個領域的邏輯層,並不過多地涉及代理伺服器編碼,序列化,和網路協議,應用程式可以跨越很多設備,從小到一個移動設備到大到一個包含企業所有賬戶信息的數據伺服器。
幸運的是,EF可應用於WCF,WEB Api等諸如此類的多層框架中。
在本章,我們將儘量涵蓋EF中多層應用中的使用,N層是指應用程式的表示層,業務邏輯層,和數據層等被分別布暑在不同的伺服器上。這種物理上獨立分佈有助於可擴展性,可維護性,及程式的後期延伸性,但當一個處理需要跨電腦的時候,會帶來性能上的影響。N層架構給EF的狀態跟蹤帶來額外的挑戰。首先,EF的ContextObject獲取數據發送給客戶端後被銷毀,而在客戶端上的數據修改沒有被跟蹤。
在更新前,必鬚根據遞交的數據新建一個Context Object,很明顯,這個新的對象不知道前一個對象的存在,包括實體的原始值。在本章,我們將看到處理這種跟蹤挑戰上的工具方法。
在EF的之前版本中,一個開發者能利用“跟蹤實體”模板,能幫助我們跟蹤被被分離的實體。然後在EF6中,它已經被棄用,但是遺留的ObjectContext將支持跟蹤實體,本章將關註基本的用於N層的創建,讀取,更新和刪除操作。此外,將深入探討實體和代理的序列化,併發,和實體跟蹤的工作方式。
9-1.用Web Api更新單獨分離的實體
問題
你想利用基於Rest的Web服務來插入,刪除,更新到數據存儲層。此外,你想通過EF6的Code First方式來實現對數據訪問的管理。在此例中,我們效仿一個N層的場景,控制台應用的客戶端調用暴露基於REST服務的Web Api應用。每層使用單獨的VS解決方案,這樣更有利於效仿N層的配置和調試。
解決方案
假設有一個如9-1圖所示的模型
圖 9-1. 一個訂單模型
我們的模型表示訂單。我們想要把模型和資料庫代碼放到一個Web Api服務後面,以便任何客戶都可以通過HTTP來插入,更新和刪除訂單數據。為了創建這個服務,執行以下操作:
1.新建一個 ASP.NET MVC 4 Web 應用項目,命名為“Recipe1.Service”,併在嚮導中選擇Web API模板。。
2. 向項目中添加一個新的“控制器”,命名為“OrderController”.
3. 添加Order類,代碼如Listing 9-1所示:
Listing 9-1. Order Entity Class
public class Order
{
public int OrderId { get; set; }
public string Product { get; set; }
public int Quantity { get; set; }
public string Status { get; set; }
public byte[] TimeStamp { get; set; }
}
4. 在“Recipe1.Service ”項目中添加EF6的引用。最好是藉助 NuGet 包管理器來添加。在”引用”上右擊,選擇”管理 NuGet 程式包.從“聯機”標簽頁,定位並安裝EF6包。這樣將會下載,安裝並配置好EF6庫到你的項目中。
5. 然後添加一個新的類“Recipe1Context”,鍵入如Listing 9-2的代碼,並確保該類繼承自EF6的DbContext
297
Listing 9-2. Context Class
public class Recipe1Context : DbContext
{
public Recipe1Context() : base("Recipe1ConnectionString") { }
public DbSet<Order> Orders { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>().ToTable("Chapter9.Order");
// Following configuration enables timestamp to be concurrency token
modelBuilder.Entity<Order>().Property(x => x.TimeStamp)
.IsConcurrencyToken()
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Computed);
}
}
6. 在Web.Configk中ConnectionStrings節里插入連接資料庫的配置,如Listing 9-3:
Listing 9-3. Connection String for the Recipe1 Web API Service
<connectionStrings>
<add name="Recipe1ConnectionString"
connectionString="Data Source=.;
Initial Catalog=EFRecipes;
Integrated Security=True;
MultipleActiveResultSets=True"
providerName="System.Data.SqlClient" />
</connectionStrings>
7. 把Listing 9-4所示的代碼插入到Global.asax的 Application_Start 方法中
Listing 9-4. Disable the Entity Framework Model Compatibility Check
protected void Application_Start()
{
// Disable Entity Framework Model Compatibilty
Database.SetInitializer<Recipe1Context>(null);
...
}
8. 最後,用Listing 9-5所示代碼替換OrderController里的代碼。
Listing 9-5. Code for the OrderController
public class OrderController : ApiController
{
// GET api/order
public IEnumerable<Order> Get()
{
using (var context = new Recipe1Context())
{
return context.Orders.ToList();
}
}
// GET api/order/5
public Order Get(int id)
{
using (var context = new Recipe1Context())
{
return context.Orders.FirstOrDefault(x => x.OrderId == id);
}
}
// POST api/order
public HttpResponseMessage Post(Order order)
{
// Cleanup data from previous requests
Cleanup();
using (var context = new Recipe1Context())
{
context.Orders.Add(order);
context.SaveChanges();
// create HttpResponseMessage to wrap result, assigning Http Status code of 201,
// which informs client that resource created successfully
var response = Request.CreateResponse(HttpStatusCode.Created, order);
// add location of newly-created resource to response header
response.Headers.Location = new Uri(Url.Link("DefaultApi",
new { id = order.OrderId }));
return response;
}
}
// PUT api/order/5
public HttpResponseMessage Put(Order order)
{
using (var context = new Recipe1Context())
{
context.Entry(order).State = EntityState.Modified;
context.SaveChanges();
// return Http Status code of 200, informing client that resouce updated successfully
return Request.CreateResponse(HttpStatusCode.OK, order);
}
}
// DELETE api/order/5
public HttpResponseMessage Delete(int id)
{
using (var context = new Recipe1Context())
{
var order = context.Orders.FirstOrDefault(x => x.OrderId == id);
context.Orders.Remove(order);
context.SaveChanges();
// Return Http Status code of 200, informing client that resouce removed successfully
return Request.CreateResponse(HttpStatusCode.OK);
}
}
private void Cleanup()
{
using (var context = new Recipe1Context())
{
context.Database.ExecuteSqlCommand("delete from chapter9.[order]");
}
}
}
需要著重指出的是,我們可以利用大量的工具(比如代碼生成模板)生成可運作的控制器, 保存上述修改。
接下來創建一個調用上述Web API服務的客戶端。
9. 新建一個包含控制台應用程式的解決方案,命名為” Recipe1.Client“.
10. 添加與Listing 9-1同樣的order實體類
最後,用Listing 9-6的代碼替換program.cs里的代碼
Listing 9-6. Our Windows Console Application That Serves as Our Test Client
private HttpClient _client;
private Order _order;
private static void Main()
{
Task t = Run();
t.Wait();
Console.WriteLine("\nPress <enter> to continue...");
Console.ReadLine();
}
private static async Task Run()
{
// create instance of the program class
var program = new Program();
program.ServiceSetup();
program.CreateOrder();
// do not proceed until order is added
await program.PostOrderAsync();
program.ChangeOrder();
// do not proceed until order is changed
await program.PutOrderAsync();
// do not proceed until order is removed
await program.RemoveOrderAsync();
}
private void ServiceSetup()
{
//指定調用Web API的URL
_client = new HttpClient { BaseAddress = new Uri("http://localhost:3237/") };
//接受請求的頭部內容
//通過JSON格式返回資源
_client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}
private void CreateOrder()
{
//創建新訂單
_order = new Order { Product = "Camping Tent", Quantity = 3, Status = "Received" };
}
private async Task PostOrderAsync()
{
// 利用Web API客戶端API調用服務
var response = await _client.PostAsJsonAsync("api/order", _order);
Uri newOrderUri;
if (response.IsSuccessStatusCode)
{
// 為新的資源捕獲Uri
newOrderUri = response.Headers.Location;
// 獲取從服務端返回的包含資料庫自增Id的訂單
_order = await response.Content.ReadAsAsync<Order>();
Console.WriteLine("Successfully created order. Here is URL to new resource: {0}", newOrderUri);
}
else
Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase);
}
private void ChangeOrder()
{
// 更新訂單
_order.Quantity = 10;
}
private async Task PutOrderAsync()
{
//構造HttpPut調用Web API服務中相應的Put方法
var response = await _client.PutAsJsonAsync("api/order", _order);
if (response.IsSuccessStatusCode)
{
// 獲取從服務端返回的更新後包含新的quanity的訂單
_order = await response.Content.ReadAsAsync<Order>();
Console.WriteLine("Successfully updated order: {0}", response.StatusCode);
}
else
Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase);
}
private async Task RemoveOrderAsync()
{
// 移除訂單
var uri = "api/order/" + _order.OrderId;
var response = await _client.DeleteAsync(uri);
if (response.IsSuccessStatusCode)
Console.WriteLine("Sucessfully deleted order: {0}", response.StatusCode);
else
Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase);
}
客戶端輸出結果如 Listing 9-6:
==========================================================================
Successfully created order:Here is URL to new resource: http://localhost:3237/api/order/1054
Successfully updated order: OK
Sucessfully deleted order: OK
==========================================================================
它是如何工作的?
先運行Web API應用程式.這個Web API應用程式包含一個MVC Web Controller, 啟動後會打開首頁。至此網站和服務已經可用。接下來打開控制台應用程式,在program.cs前面設置一個斷點,運行控制台應用程式。首先我們用URI和一些配置建立與Web API服務的管道連接,接受由WEB AP服務端返回的JSON格式的頭部信息,然後我們用PostAsJsonAsync方法發送WEB API請求,並從HttpClient對象里返回信息創建新的Order對象.如果你在WEB AP服務端controller的Post Action方法里添加斷點,你將看到它接收到一個Order對象參數並把它添加到訂單實體的Context中.確保該對象狀態為Added,並讓Context跟蹤它.最後,調用SaveChanges方法把新的訂單插入到資料庫。並封裝一個HTTP狀態碼201和URI定位新創建的資源到HttpResponseMessage對象返回給調用它的應用程式. 當使用 ASP.NET Web API, 要確保我們的客戶端生成一個HTTP Post請求來插入新的數據,該HTTP Post請求調用相應的Web API controller 中的Post action方法。
再查看客戶端,我們執行下一個操作,改變訂單的quantity,用HttpClient 對象的PutAsJsonAsync方法把新的訂單發送給Web API.如果WEB API服務的Web API controller里的 Put Action 方法添加斷點, 可以看到該方法參數接收到一個訂單對象. 接關調用context 對象的Entry 方法傳遞訂單實體的引用,然後設置State為 Modified 狀態. 隨後調用SaveChanges產生一個SQL更新語句.會更新訂單的所有列. 在此小節,我們看到瞭如何只更新想要更新的屬性. 並返回給調用者一個為200 HTTP狀態碼。再看客戶端,我們最後調用移除操作,它會把狀態從資料庫中刪除. 我們通過把訂單的Id附加到URI中,並調用Web API 的DeleteAsync。在服務端,我們從資料庫中獲取目標訂單,並傳給訂單的context 對象的Remove方法,使訂單狀態設置為deleted.
隨後調用SaveChanges產生一個SQL的刪除語句,並把訂單從資料庫中刪除。在此小節,我們將EF數據操作封裝在Web API服務後,客戶端可能通過HttpClient對象調用服務,通過Web API的 HTTP 方法發佈, 利用Post action方法來添加新記錄, Put action方法更新一條記錄, 和Delete action方法刪除一條記錄.同時我們學習到了EF6的Code First方式,當然在實際應用中,我們可能更願意創建一個單獨的層(VS的類庫項目),把EF6資料庫訪問代碼從Web API服務中分離出來。