CorrelationParameter) 控制資料流向前幾章的範例皆為單一宿主應用程式對應單一工作流實例 (一對一),資料交換自然不會混淆。但實務上常見單一應用程式同時啟動多個同類型工作流實例 (一對多),例如同時處理多筆訂單、追蹤多輛卡車。此時宿主必須明確指定要與「哪一個」工作流實例通訊,而工作流本身只屬於單一宿主,無需識別宿主身分。
WF 透過 關聯權杖 解決此問題:執行階段建立儲存容器記錄工作流實例與資料的對應關係,通訊雙方需出示相同的關聯權杖才能進行資料交換,否則拋出 EventDeliveryFailedException。
| 屬性 | 用途 |
|---|---|
CorrelationParameterAttribute | 標示介面方法/事件中哪個參數攜帶關聯識別碼 (如 truckID)。建構子接收參數名稱字串。 |
CorrelationInitializerAttribute | 標示初始化關聯的方法或事件。必須先執行帶有此屬性的方法/事件,後續通訊才合法。 |
CorrelationAliasAttribute | 當事件參數 (EventArgs) 而非直接參數攜帶關聯識別碼時,指定別名路徑 (如 e.TruckID)。 |
重要規則: 單一工作流實例的關聯識別碼在其生命週期內不可變更。若嘗試用不同識別碼與同一實例通訊,將拋出例外。因此識別碼必須在啟動工作流前就已確定 (如預先產生訂單號),不可依賴資料庫自動產生或稍後指派。
本章範例模擬貨運公司追蹤卡車 GPS 位置。每輛卡車對應一個工作流實例,以 truckID 為關聯鍵。
[CorrelationParameter("truckID")]
[ExternalDataExchange]
public interface ITruckService
{
// Workflow → Host
[CorrelationInitializer]
void ReadyTruck(Int32 truckID, Int32 startingX, Int32 startingY);
void UpdateTruck(Int32 truckID, Int32 X, Int32 Y);
void RemoveTruck(Int32 truckID);
// Host → Workflow
[CorrelationAlias("truckID", "e.TruckID")]
event EventHandler<CancelTruckEventArgs> CancelTruck;
[CorrelationInitializer]
[CorrelationAlias("truckID", "e.TruckID")]
event EventHandler<AddTruckEventArgs> AddTruck;
}
關鍵點:
truckID 參數或屬性。ReadyTruck (工作流主動通知宿主) 與 AddTruck (宿主啟動卡車) 皆標記 CorrelationInitializer,任一執行即建立關聯。CorrelationAlias 將關聯參數對應至 EventArgs.TruckID。繼承 ITruckService,在執行階段以單例模式運作,負責以 (InstanceID, TruckID) 為鍵管理各工作流實例的關聯資料。
protected const string KeyFormat = "{0}.Truck_{1}";
protected static Dictionary<string, string> _dataValues = new Dictionary<string, string>();
protected static Dictionary<string, WorkflowTruckTrackingDataService> _dataServices = new Dictionary<string, WorkflowTruckTrackingDataService>();
private static object _syncLock = new object();
public static WorkflowTruckTrackingDataService GetRegisteredWorkflowDataService(Guid instanceID, Int32 truckID)
{
string key = String.Format(KeyFormat, instanceID, truckID);
return _dataServices.ContainsKey(key) ? _dataServices[key] : null;
}
public static void RegisterDataService(WorkflowTruckTrackingDataService dataService)
{
string key = String.Format(KeyFormat, dataService.InstanceID.ToString(), dataService.TruckID);
lock (_syncLock) _dataServices.Add(key, dataService);
}
因 WF 在工作流與宿主間傳遞物件參考而非副本,為避免雙邊修改同一物件導致競爭條件,連接器將資料序列化為 XML 字串儲存。
public string RetrieveTruckInfo(Guid instanceID, Int32 truckID)
{
string key = String.Format(KeyFormat, instanceID, truckID);
return _dataValues.ContainsKey(key) ? _dataValues[key] : String.Empty;
}
protected Truck UpdateTruckData(Guid instanceID, Int32 truckID, Int32 X, Int32 Y)
{
string key = String.Format(KeyFormat, instanceID, truckID);
Truck truck = _dataValues.ContainsKey(key)
? Deserialize(_dataValues[key])
: new Truck { ID = truckID };
truck.X = X; truck.Y = Y;
_dataValues[key] = Serialize(truck);
return truck;
}
public void ReadyTruck(Int32 truckID, Int32 startingX, Int32 startingY)
{
var svc = GetRegisteredWorkflowDataService(WorkflowEnvironment.WorkflowInstanceId, truckID);
UpdateTruckData(svc.InstanceID, truckID, startingX, startingY);
svc?.RaiseTruckLeavingEvent(truckID, startingX, startingY);
}
public void UpdateTruck(Int32 truckID, Int32 X, Int32 Y)
{
var svc = GetRegisteredWorkflowDataService(WorkflowEnvironment.WorkflowInstanceId, truckID);
UpdateTruckData(svc.InstanceID, truckID, X, Y);
svc?.RaiseRouteUpdatedEvent(truckID, X, Y);
}
public void RemoveTruck(Int32 truckID)
{
var svc = GetRegisteredWorkflowDataService(WorkflowEnvironment.WorkflowInstanceId, truckID);
string key = String.Format(KeyFormat, svc.InstanceID, truckID);
if (_dataValues.ContainsKey(key)) _dataValues.Remove(key);
svc?.RaiseTruckArrivedEvent(truckID);
}
public event EventHandler<CancelTruckEventArgs> CancelTruck;
public void RaiseCancelTruck(Guid instanceID, Int32 truckID)
=> CancelTruck?.Invoke(null, new CancelTruckEventArgs(instanceID, truckID));
public event EventHandler<AddTruckEventArgs> AddTruck;
public void RaiseAddTruck(Guid instanceID, Int32 truckID, Int32 routeID)
=> AddTruck?.Invoke(null, new AddTruckEventArgs(instanceID, truckID, routeID));
WorkflowTruckTrackingDataService 封裝 ExternalDataExchangeService 與連接器註冊邏輯,提供事件引發方法供連接器呼叫,並保存 InstanceID、TruckID 識別自身。
public static WorkflowTruckTrackingDataService CreateDataService(Guid instanceID, WorkflowRuntime workflowRuntime, Int32 truckID)
{
lock (_syncRoot)
{
if (_workflowRuntime == null) _workflowRuntime = workflowRuntime;
if (_dataExchangeService == null)
{
_dataExchangeService = new ExternalDataExchangeService();
_workflowRuntime.AddService(_dataExchangeService);
}
var connector = (TruckServiceDataConnector)workflowRuntime.GetService(typeof(TruckServiceDataConnector));
if (connector == null)
{
_dataConnector = new TruckServiceDataConnector();
_dataExchangeService.AddService(_dataConnector);
}
else _dataConnector = connector;
return GetRegisteredWorkflowDataService(instanceID, truckID);
}
}
private WorkflowTruckTrackingDataService(Guid instanceID, Int32 truckID)
{
_instanceID = instanceID; _truckID = truckID;
}
~WorkflowTruckTrackingDataService()
{
_workflowRuntime = _dataExchangeService = _dataConnector = null;
}
註記: 解構子而非
IDisposable用於清理,因服務從執行階段移除時不會呼叫Dispose。建構子為私有,強制透過工廠方法建立,避免循環參考。
本節完成了關聯通訊的基礎設施:定義帶有關聯屬性的介面、實作管理多實例資料的連接器與服務類別。下一節將使用 wca.exe 產生強型別活動,並設計關聯工作流與宿主應用程式整合。