永久性範例示範如何自訂 Windows Communication Foundation (WCF) 執行階段以啟用永久性執行個體內容。 它會使用 SQL Server 2005 做為備份存放區 (在此例中為 SQL Server 2005 Express)。 不過,也會提供存取自訂儲存機制的方法。
注意
此範例的安裝程序與建置指示位於本文的結尾。
這個範例牽涉到擴充 WCF 的通道層和服務模型層。 因此,在進入實作的詳細資訊之前,您必須先了解一些基礎概念。
在現實情境中,經常可以看到持久性實例上下文的例子。 例如,購物車應用程式可以在購物到一半時先暫停,改天再繼續購物。 因此當我們隔天造訪購物車時,原來的內容會被還原。 請注意,在您斷開連線時,伺服器上的購物車應用程式不會維護購物車實例。 相反地,它將狀態保存到耐久性儲存媒體中,並在為還原的環境建立新實例時使用這些狀態。 因此,提供相同內容的服務執行個體與之前的執行個體不同 (因為它們沒有相同的記憶體位址)。
耐久性實例的上下文是由一種小型協定所實現,該協定在用戶端和服務之間交換上下文識別碼。 您會在用戶端上建立此上下文識別碼,並傳輸至服務。 建立服務執行個體時,服務執行階段會嘗試從持續性儲存體 (Persistent Storage) (預設為 SQL Server 2005 資料庫) 載入對應至此內容識別碼的持續狀態。 如果沒有可用的狀態,新執行個體就會使用本身的預設狀態。 服務實作會使用自訂屬性,以標示會變更服務實作狀態的作業,讓執行階段可以在呼叫作業之後儲存服務執行個體。
在前面的描述中,有兩個易辨的步驟可達到目標:
- 變更在網路上傳送的訊息,以攜帶上下文識別碼。
- 更改服務的區域行為,以實作自訂的實例邏輯。
由於列表中的第一個會影響網路上的訊息,因此應將其實作為自訂通道,並連接至通道層。 後一項只會影響服務本機行為,因此可藉由延伸數個服務擴充點來進行實作。 在接下來的章節中,將會討論這些延伸項目。
永久性 InstanceContext 通道
首先查看的是通道層擴展。 撰寫自訂通道的第一個步驟為決定通道的通訊結構。 引入新的網路通訊協定時,通道應該可以和通道堆疊中絕大多數的其他通道搭配使用。 因此,該系統應支援所有訊息交換模式。 但是不管其通訊結構為何,通道的核心功能都是一樣的。 更具體來說,從用戶端的角度,應該將內容識別碼寫出至訊息;而從服務的角度來看,應該從訊息讀取此內容識別碼,並傳遞至較上層。 因此,將會針對所有永久性執行個體內容通道實作,建立充當為抽象基底類別的 DurableInstanceContextChannelBase 類別。 這個類別包含一般狀態電腦管理函式以及兩個受保護的成員,可在訊息之間套用及讀取內容資訊。
class DurableInstanceContextChannelBase
{
//…
protected void ApplyContext(Message message)
{
//…
}
protected string ReadContextId(Message message)
{
//…
}
}
這兩個方法會使用 IContextManager 實作,將內容識別碼寫入或從訊息中讀取。 (IContextManager 是一種自訂介面,可用來定義所有內容管理員的合約)。通道可以在自訂 SOAP 標頭或 HTTP Cookie 標頭中加入內容識別碼。 每個內容管理員實作都繼承自 ContextManagerBase 類別,而這個類別則包含用於所有內容管理員的一般功能。 您可以使用此類別中的 GetContextId 方法,從用戶端產生內容識別碼。 第一次產生內容識別碼時,方法會將此內容識別碼儲存至文字檔,而這個文字檔的名稱則是由遠端端點位址 (將使用 @ 字元取代典型 URI 中無效的檔案名稱字元) 所建構。
之後相同的遠端端點需要此內容識別碼時,就會檢查是否有適當的檔案。 如果有,就會讀取內容識別碼並傳回。 否則會傳回新產生的內容識別碼,並儲存至檔案。 使用預設組態時,這些檔案會放置在 ContextStore 目錄中,而這個目錄則是在目前使用者的暫存目錄中。 不過,您可以使用繫結項目來設定這個位置。
您可以設定傳輸內容識別碼的機制。 這個內容識別碼可以寫入 HTTP Cookie 標頭或自訂 SOAP 標頭中。 自訂 SOAP 標頭方法可讓您使用此通訊協定搭配非 HTTP 通訊協定 (例如,TCP 或具名管道)。 有兩個可實作這兩個選項的類別,MessageHeaderContextManager 和 HttpCookieContextManager。
兩者都將內容 ID 適當地寫入訊息中。 例如,MessageHeaderContextManager 類別會在 WriteContext 方法中將它寫入 SOAP 標頭。
public override void WriteContext(Message message)
{
string contextId = this.GetContextId();
MessageHeader contextHeader =
MessageHeader.CreateHeader(DurableInstanceContextUtility.HeaderName,
DurableInstanceContextUtility.HeaderNamespace,
contextId,
true);
message.Headers.Add(contextHeader);
}
ApplyContext 類別中的 ReadContextId 和 DurableInstanceContextChannelBase 方法,可以分別叫用 IContextManager.ReadContext 和 IContextManager.WriteContext。 不過,這些內容管理員不是由 DurableInstanceContextChannelBase 類別直接創建的。 而是使用 ContextManagerFactory 類別來執行該功能。
IContextManager contextManager =
ContextManagerFactory.CreateContextManager(contextType,
this.contextStoreLocation,
this.endpointAddress);
傳送通道時即可叫用 ApplyContext 方法。 它將上下文識別碼插入到傳出訊息中。 接收通道便會叫用 ReadContextId 方法。 這個方法會確定可在傳入訊息中使用內容識別碼,並將該內容識別碼新增至 Properties 類別的 Message 集合中。 當無法讀取上下文 ID 時,系統也會擲回 CommunicationException,因此導致通道中止。
message.Properties.Add(DurableInstanceContextUtility.ContextIdProperty, contextId);
在繼續之前,您必須理解 Properties 類別中的 Message 集合的用法。 一般來說,在通道層中將資料從較低層傳遞至較高層時,就會使用此 Properties 集合。 如此一來,就可以使用一致的方法對較高層提供需要的資料,而不用在意通訊協定詳細資料。 換句話說,通道層可以將內容識別碼當作 SOAP 標頭或 HTTP Cookie 標頭而傳送和接收。 但較高層不需要了解這些細節,因為通道層會在 Properties 集合中提供這些資訊。
現在,在使用DurableInstanceContextChannelBase類別的情況下,必須實作所有十個必要的介面 (IOutputChannel、IInputChannel、IOutputSessionChannel、IInputSessionChannel、IRequestChannel、IReplyChannel、IRequestSessionChannel、IReplySessionChannel、IDuplexChannel、IDuplexSessionChannel)。 這些介面類似於所有可用的訊息交換模式(資料報、單工、雙工及其有狀態的變體)。 這些實作都會繼承先前所述的基底類別,並會適當地呼叫 ApplyContext 和 ReadContextId。 例如實作 IOutputChannel 介面的 DurableInstanceContextOutputChannel,它會從傳送訊息的每個方法中呼叫 ApplyContext 方法。
public void Send(Message message, TimeSpan timeout)
{
// Apply the context information before sending the message.
this.ApplyContext(message);
//…
}
另一方面,實作 DurableInstanceContextInputChannel 介面的 IInputChannel 會在接收訊息的每個方法中呼叫 ReadContextId 方法。
public Message Receive(TimeSpan timeout)
{
//…
ReadContextId(message);
return message;
}
此外,這些通道實作會將方法叫用委派給通道堆疊中位置更低的通道。 不過,工作階段變數會有一些基本邏輯,確定針對導致建立工作階段的第一個訊息而言,會傳送內容識別碼而且此內容識別碼為唯讀。
if (isFirstMessage)
{
//…
this.ApplyContext(message);
isFirstMessage = false;
}
DurableInstanceContextBindingElement 類別和 DurableInstanceContextBindingElementSection 類別接著會將這些通道實作適當地加入 WCF 通道執行階段。 如需繫結元素和繫結元素區段的詳細資料,請參閱 HttpCookieSession 通道範例文件。
服務模型層擴充
現在上下文識別碼已傳遞通過通道層,可以實作服務行為以自訂實例化。 在此範例中,可以使用存放管理員,在持續存放區中載入及儲存狀態。 如先前所述,這個範例提供了使用 SQL Server 2005 做為其後端存放區的存放管理員。 不過,您也可以將自訂存放機制新增至此延伸項目。 若要這樣做,將會宣告公用介面,而所有存放管理員都必須實作此公用介面。
public interface IStorageManager
{
object GetInstance(string contextId, Type type);
void SaveInstance(string contextId, object state);
}
SqlServerStorageManager 類別包含預設 IStorageManager 實作。 在其 SaveInstance 方法中,指定的物件會使用 XmlSerializer 序列化,並儲存至 SQL Server 資料庫。
XmlSerializer serializer = new XmlSerializer(state.GetType());
string data;
using (StringWriter writer = new StringWriter(CultureInfo.InvariantCulture))
{
serializer.Serialize(writer, state);
data = writer.ToString();
}
using (SqlConnection connection = new SqlConnection(GetConnectionString()))
{
connection.Open();
string update = @"UPDATE Instances SET Instance = @instance WHERE ContextId = @contextId";
using (SqlCommand command = new SqlCommand(update, connection))
{
command.Parameters.Add("@instance", SqlDbType.VarChar, 2147483647).Value = data;
command.Parameters.Add("@contextId", SqlDbType.VarChar, 256).Value = contextId;
int rows = command.ExecuteNonQuery();
if (rows == 0)
{
string insert = @"INSERT INTO Instances(ContextId, Instance) VALUES(@contextId, @instance)";
command.CommandText = insert;
command.ExecuteNonQuery();
}
}
}
在 GetInstance 方法中,會針對指定的內容識別碼讀取序列化資料,並將從中建構的物件傳回給呼叫端。
object data;
using (SqlConnection connection = new SqlConnection(GetConnectionString()))
{
connection.Open();
string select = "SELECT Instance FROM Instances WHERE ContextId = @contextId";
using (SqlCommand command = new SqlCommand(select, connection))
{
command.Parameters.Add("@contextId", SqlDbType.VarChar, 256).Value = contextId;
data = command.ExecuteScalar();
}
}
if (data != null)
{
XmlSerializer serializer = new XmlSerializer(type);
using (StringReader reader = new StringReader((string)data))
{
object instance = serializer.Deserialize(reader);
return instance;
}
}
使用這些存放管理員的使用者不應該直接實例化它們。 他們使用抽象化存放管理員建立細節的 StorageManagerFactory 類別。 這個類別包含 GetStorageManager 這個靜態成員,此成員會建立提供之存放管理員型別的執行個體。 如果型別參數為 null,這個方法就會建立預設 SqlServerStorageManager 類別的執行個體,並傳回之。 也會驗證提供的型別,以確定會實作 IStorageManager 介面。
public static IStorageManager GetStorageManager(Type storageManagerType)
{
IStorageManager storageManager = null;
if (storageManagerType == null)
{
return new SqlServerStorageManager();
}
else
{
object obj = Activator.CreateInstance(storageManagerType);
// Throw if the specified storage manager type does not
// implement IStorageManager.
if (obj is IStorageManager)
{
storageManager = (IStorageManager)obj;
}
else
{
throw new InvalidOperationException(
ResourceHelper.GetString("ExInvalidStorageManager"));
}
return storageManager;
}
}
現在已實作從持續性儲存體 (Persistent Storage) 中讀取及寫入執行個體的必要基礎結構。 接著便需要採取變更服務行為的必要步驟。
此處理序的第一個步驟為儲存上下文 ID,這個上下文 ID 透過通道層傳遞到當前的 InstanceContext。 InstanceContext 是一種執行階段元件,並充當為 WCF 發送器和服務執行個體之間的連結。 可以用於對服務執行個體提供額外的狀態和行為。 這點很重要,因為在有狀態的通訊中,內容識別碼只會與第一則訊息一起傳送。
WCF 可讓您使用其可擴充物件模式來新增新狀態和行為,進而擴充 InstanceContext 執行階段元件。 在 WCF 中使用可擴充物件模式,可以搭配新功能來擴充現有執行階段類別,或新增狀態功能到物件。 可擴充物件模式中有三個介面 - IExtensibleObject<T>、IExtension<T> 和 IExtensionCollection<T>:
IExtensibleObject<T> 介面是由允許自訂其功能之延伸模組的物件所實作。
IExtension<T> 介面是由本身為 T 類型之類別延伸模組的物件所實作。
IExtensionCollection<T> 介面是一個包含 IExtensions 的集合,並且允許透過類型來擷取 IExtensions。
因此,您應該建立 InstanceContextExtension 類別,並實作 IExtension 介面及定義必要的狀態以儲存內容識別碼。 這個類別也會提供保存正在使用的存放管理員的狀態。 一旦儲存新狀態,就無法進行修改。 所以您應該在建構執行個體時提供狀態,並將該狀態儲存至執行個體,並只能透過唯讀屬性來進行存取。
// Constructor
public DurableInstanceContextExtension(string contextId,
IStorageManager storageManager)
{
this.contextId = contextId;
this.storageManager = storageManager;
}
// Read only properties
public string ContextId
{
get { return this.contextId; }
}
public IStorageManager StorageManager
{
get { return this.storageManager; }
}
InstanceContextInitializer 類別會實作 IInstanceContextInitializer 介面,並將執行個體內容延伸項目新增至已建構之 InstanceContext 的 Extensions 集合中。
public void Initialize(InstanceContext instanceContext, Message message)
{
string contextId =
(string)message.Properties[DurableInstanceContextUtility.ContextIdProperty];
DurableInstanceContextExtension extension =
new DurableInstanceContextExtension(contextId,
storageManager);
instanceContext.Extensions.Add(extension);
}
如先前所述,您會從 Properties 類別的 Message 集合中讀取內容識別碼,並將它傳遞至延伸類別的建構函式。 這樣便可示範如何使用一致的方法,在各層之間交換資訊。
下一個重要的步驟為覆寫服務執行個體的建立處理序。 WCF 允許實作自訂具現化行為,並使用 IInstanceProvider 介面將這些行為連結至執行階段。 將實作新 InstanceProvider 類別以執行該工作。 在建構函式中,會接受預期來自執行個體提供者的服務類型。 稍後使用此來建立新的執行個體。 在 GetInstance 實作中,會建立用來尋找持久化執行個體的存放管理員執行個體。 如果傳回 null,則會具現化服務類型的新執行個體,並傳回給呼叫端。
public object GetInstance(InstanceContext instanceContext, Message message)
{
object instance = null;
DurableInstanceContextExtension extension =
instanceContext.Extensions.Find<DurableInstanceContextExtension>();
string contextId = extension.ContextId;
IStorageManager storageManager = extension.StorageManager;
instance = storageManager.GetInstance(contextId, serviceType);
instance ??= Activator.CreateInstance(serviceType);
return instance;
}
下一個重要的步驟為將 InstanceContextExtension、InstanceContextInitializer 和 InstanceProvider 類別安裝至服務模型執行階段。 您可以使用自訂屬性以標示服務實作類別,便可安裝行為。
DurableInstanceContextAttribute 中包含這個屬性的實作,並且會實作 IServiceBehavior 介面以擴充整個服務執行階段。
這個類別所擁有的屬性,可以接受要使用的存放管理員類型。 透過這種方法的實作,使用者便可指定自己的 IStorageManager 實作作為這個屬性的參數。
在 ApplyDispatchBehavior 實作中,將會驗證目前 InstanceContextMode 屬性的 ServiceBehavior。 如果將此屬性設定為 Singleton,則無法啟用永久性執行個體,並會引發 InvalidOperationException 來通知主機。
ServiceBehaviorAttribute serviceBehavior =
serviceDescription.Behaviors.Find<ServiceBehaviorAttribute>();
if (serviceBehavior != null &&
serviceBehavior.InstanceContextMode == InstanceContextMode.Single)
{
throw new InvalidOperationException(
ResourceHelper.GetString("ExSingletonInstancingNotSupported"));
}
之後,便會在針對每個端點所建立的 DispatchRuntime 中,建立並安裝存放管理員執行個體、執行個體內容初始設定式和執行個體提供者。
IStorageManager storageManager =
StorageManagerFactory.GetStorageManager(storageManagerType);
InstanceContextInitializer contextInitializer =
new InstanceContextInitializer(storageManager);
InstanceProvider instanceProvider =
new InstanceProvider(description.ServiceType);
foreach (ChannelDispatcherBase cdb in serviceHostBase.ChannelDispatchers)
{
ChannelDispatcher cd = cdb as ChannelDispatcher;
if (cd != null)
{
foreach (EndpointDispatcher ed in cd.Endpoints)
{
ed.DispatchRuntime.InstanceContextInitializers.Add(contextInitializer);
ed.DispatchRuntime.InstanceProvider = instanceProvider;
}
}
}
到目前為止,這個範例產生的通道已經啟用了自訂線路協定,以進行自訂內容識別碼的交換,並且覆寫預設的實例化行為,以從持續性儲存體載入這些實例。
剩下的就是找到將服務執行個體儲存到持久性儲存裝置的方法。 如前所述,已提供必要的功能讓您在 IStorageManager 實作中儲存狀態。 現在則必須將此項與 WCF 執行階段整合。 也需要適用於服務實作類別方法的其他屬性。 而這個屬性應該會套用至變更服務執行個體狀態的方法。
SaveStateAttribute 類別會實作此功能。 也會實作 IOperationBehavior 類別以修改每個作業的 WCF 執行階段。 當您使用這個屬性來標記方法時,WCF 執行階段會叫用 ApplyBehavior 方法,同時建構適當的 DispatchOperation。 在這個方法實作中,只有一行程式碼:
dispatch.Invoker = new OperationInvoker(dispatch.Invoker);
這個指示會建立 OperationInvoker 型別的執行個體,並將它指派至已建構之 Invoker 的 DispatchOperation 屬性。
OperationInvoker 類別則是對 DispatchOperation 所建立之預設作業啟動程式的包裝函式。 這個類別會實作 IOperationInvoker 介面。 在 Invoke 方法實作中,實際的方法叫用會委派至內部操作呼叫器。 但是在傳回結果之前,可以使用 InstanceContext 中的存放管理員來儲存服務執行個體。
object result = innerOperationInvoker.Invoke(instance,
inputs, out outputs);
// Save the instance using the storage manager saved in the
// current InstanceContext.
InstanceContextExtension extension =
OperationContext.Current.InstanceContext.Extensions.Find<InstanceContextExtension>();
extension.StorageManager.SaveInstance(extension.ContextId, instance);
return result;
使用擴充功能
通道層和服務模型層延伸模組都已完成,現在可以在 WCF 應用程式中使用。 服務必須使用自訂繫結將通道新增至通道堆疊,然後以適當的屬性來標記服務實作類別。
[DurableInstanceContext]
[ServiceBehavior(InstanceContextMode=InstanceContextMode.PerSession)]
public class ShoppingCart : IShoppingCart
{
//…
[SaveState]
public int AddItem(string item)
{
//…
}
//…
}
用戶端應用程式必須使用自訂繫結,將 DurableInstanceContextChannel 新增至通道堆疊。 若要以宣告方式在組態檔中設定通道,則必須將繫結項目區段新增至繫結項目延伸集合中。
<system.serviceModel>
<extensions>
<bindingElementExtensions>
<add name="durableInstanceContext"
type="Microsoft.ServiceModel.Samples.DurableInstanceContextBindingElementSection, DurableInstanceContextExtension, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
</bindingElementExtensions>
</extensions>
</system.serviceModel>
現在,繫結元素可以像其他標準繫結元素一樣,用於自訂繫結:
<bindings>
<customBinding>
<binding name="TextOverHttp">
<durableInstanceContext contextType="HttpCookie"/>
<reliableSession />
<textMessageEncoding />
<httpTransport />
</binding>
</customBinding>
</bindings>
推論
這個範例會顯示如何建立自訂通訊協定通道,以及如何自訂服務行為進而啟用它。
使用者可以透過組態區段來指定 IStorageManager 的實作,進一步提升擴充套件的功能。 這樣做便可修改備份存放區,而不需要重新編譯服務程式碼。
此外,您可以嘗試實作一個類別(例如,StateBag),用來封裝物件的狀態。 該類別也負責在變更狀態時保存狀態。 透過這個方法,您就可以避免使用 SaveState 屬性並更精確地執行保存工作 (例如,可以在實際上變更狀態時保存狀態,而不是每次呼叫具有 SaveState 屬性的方法時就儲存狀態)。
當您執行範例時,會顯示如下的輸出。 用戶端會將兩個項目新增至購物車,然後從服務中取得其購物車的項目清單。 在每個主控台視窗中按下 ENTER 鍵,即可關閉服務與用戶端。
Enter the name of the product: apples
Enter the name of the product: bananas
Shopping cart currently contains the following items.
apples
bananas
Press ENTER to shut down client
注意
重建服務則會覆寫資料庫檔案。 若要觀察每次執行範例時所保留的狀態,請在各個執行動作之間務必不要重建範例。
若要安裝、建置及執行範例
若要建置解決方案,請依照建置 Windows Communication Foundation 範例中的指示操作。
若要在單一或多部電腦組態中執行此範例,請遵循執行 Windows Communication Foundation 範例中的指示進行。
注意
您必須執行 SQL Server 2005 或 SQL Express 2005,才能執行此範例。 如果正在執行 SQL Server 2005,則必須修改服務之連線字串的組態。 在多台電腦中執行時,只有伺服器電腦上需要 SQL Server。