通过单元测试学习 Windows Workflow Foundation 4.5:工作流服务
工作流 WCF 服务。
概述
本文侧重于将工作流作为服务托管,特别是针对长时间运行/可持久化的工作流。
本系列的其他文章如下:
- 通过单元测试学习 Windows Workflow Foundation 4.5:CodeActivity
- 通过单元测试学习 Windows Workflow Foundation 4.5:InvokeMethod 和 DynamicActivity
- 通过单元测试学习 Windows Workflow Foundation 4.5:WorkflowApplication 和 WorkflowServiceHost
- 通过单元测试学习 Windows Workflow Foundation 4.5:WorkflowApplication 持久化
对于真正长时间运行的工作流,使用 WorflowServiceHost 和工作流持久化是唯一的选择。然而,Workflow 4 和 4.5 对持久化的支持“较弱”,而且,由于AppFabric 的弃用,工作流管理服务不再是可行的解决方案。作为应用程序开发人员,我们必须编写自己的自定义解决方案来管理工作流的生命周期。
对于一些在 4.0 之前的 Workflow Foundation 版本中有经验的开发者来说,这种“较弱”的工作流生命周期管理支持是个坏消息,会导致大量的额外维护工作和相应的思维转变,除非开发者仅从事 Biztalk 和 Sharepoint 的复杂工作流托管。
备注
我认为,WF 4 中不再提供内置的工作流定义持久化解决方案是一个明智之举,因为我认为许多工作流应用程序不需要在持久化层中持久化工作流定义,而相应的函数知道在恢复工作流实例时使用哪些工作流定义,因此无需对工作流进行序列化和反序列化。对于需要持久化工作流定义的情况,编写自定义解决方案(例如字典)并不困难。我认为最棘手的部分是在不使用 Sharepoint 或 Biztalk 的情况下,扫描持久化层中休眠的工作流并及时加载它们。
参考文献
- 创建长时间运行的工作流服务
- 什么是相关性,我为什么要初始化它?
- 相关性
- 上下文交换相关性
- WF4 中的实例相关性
- .NET Framework 的InternalReceiveMessage.cs和InternalSendMessage.cs
- 持久化最佳实践
- 工作流服务主机扩展性
- WF 4.5 有什么新特性
- WF 4.x 示例
Using the Code
源代码可在https://github.com/zijianhuang/WorkflowDemo找到。
必备组件
- Visual Studio 2015 Update 1 或 Visual Studio 2013 Update 4
- xUnit(包含)
- EssentialDiagnostics(包含)
- FonlowTesting(包含)
- 工作流持久化 SQL 数据库,默认本地数据库为 WF。
本文中的示例来自测试类:WorkflowServiceHostTests、WorkflowServiceHostPersistenceTests、WFServiceTests、WCFWithWorkflowTests、NonServiceWorkflowTests。
服务主机上的非服务工作流
条件
您有一些不可持久化的工作流需要作为服务运行。
基本解决方案
您可以创建 WCF 服务、Web API 或任何远程 API 技术来触发这些工作流的执行。然后使用 WorkflowInvoker.Invoke() 或 WorkflowApplication 在服务函数中执行工作流。
案例 1
如果您希望工作流在返回响应之前完成,您可以在服务函数的实现中使用 WorkflowInvoker.Invoke()。然而,显然工作流的执行时间不应超过 1 分钟,因为许多 Web 请求操作在服务和客户端两端的默认超时时间为 59.999 秒。在这种情况下,工作流在服务端的执行就像一个普通的函数调用。
案例 2
如果您希望服务函数在工作流执行开始后立即返回,您可以使用 WorkflowApplication,因为 Run() 方法是非阻塞的。如果客户端关心结果,服务函数应返回工作流应用程序实例的 Instance ID,这样客户端稍后就可以通过该 ID 查询结果,该 ID 由工作流逻辑持久化。在我的测试中,即使服务函数已返回响应,WorkflowApplication 实例仍在正常运行,并且持有该实例的变量已超出服务函数块的作用域。显然,工作流运行时仍然持有对 WorkflowApplication 实例的引用,因此它不会被 GC 收集。但是,请注意,WCF 服务主机并不知道长时间运行的 WorkflowApplication 实例。IIS 的默认空闲回收时间为 20 分钟,因此如果执行时间超过 20 分钟,实例可能面临异常终止的风险,因为 IIS 并不知道长时间运行的 WorkflowApplication 实例。
服务主机上的带书签的非服务工作流
条件
您有一些带书签的可持久化工作流需要作为服务运行。
解决方案
您可以创建 WCF 服务、Web API 或任何远程 API 技术来触发这些工作流的执行。然后使用 WorkflowApplication 在服务函数中执行工作流,但是,您需要编写大量函数来管理工作流空闲时的生命周期。
下面的代码用于演示在不使用 WorkflowServiceHost 的情况下,自行编写代码会多么笨拙。
服务代码
[ServiceContract]
public interface IWakeup
{
/// <summary>
/// instantiate a workflow and return the ID right after WorkflowApplication.Run()
/// </summary>
[OperationContract]
Guid Create(string bookmarkName, TimeSpan duration);
/// <summary>
/// Reload persisted instance and run and return immediately.
/// </summary>
[OperationContract]
bool LoadAndRun(Guid id);
/// <summary>
/// Send a bookmark call to a workflow running. If the workflow instance is not yet loaed upon other events, this call will reload the instance.
/// </summary>
[OperationContract]
string Wakeup(Guid id, string bookmarkName);
}
public class WakeupService : IWakeup
{
public Guid Create(string bookmarkName, TimeSpan duration)
{
var a = new WaitForSignalOrDelayWorkflow()
{
Duration = TimeSpan.FromSeconds(10),
BookmarkName = bookmarkName,
};
var app = new WorkflowApplication(a)
{
InstanceStore = WFDefinitionStore.Instance.Store,
PersistableIdle = (eventArgs) =>
{
return PersistableIdleAction.Unload;
},
OnUnhandledException = (e) =>
{
return UnhandledExceptionAction.Abort;
},
Completed = delegate (WorkflowApplicationCompletedEventArgs e)
{
},
Aborted = (eventArgs) =>
{
},
Unloaded = (eventArgs) =>
{
}
};
var id = app.Id;
app.Run();
return id;
}
public bool LoadAndRun(Guid id)
{
var app2 = new WorkflowApplication(new WaitForSignalOrDelayWorkflow())
{
Completed = e =>
{
if (e.CompletionState == ActivityInstanceState.Closed)
{
System.Runtime.Caching.MemoryCache.Default.Add(id.ToString(), e.Outputs, DateTimeOffset.MaxValue);//Save outputs at the end of the workflow.
}
},
Unloaded = e =>
{
System.Diagnostics.Debug.WriteLine("Unloaded in LoadAndRun.");
},
InstanceStore = WFDefinitionStore.Instance.Store,
};
try
{
app2.Load(id);
app2.Run();
}
catch (System.Runtime.DurableInstancing.InstanceNotReadyException ex)
{
System.Diagnostics.Trace.TraceWarning(ex.Message);
return false;
}
System.Runtime.Caching.MemoryCache.Default.Add(id.ToString()+"Instance", app2, DateTimeOffset.MaxValue);//Keep the reference to a running instance
return true;
}
public string Wakeup(Guid id, string bookmarkName)
{
var savedDic = (System.Runtime.Caching.MemoryCache.Default.Get(id.ToString())) as IDictionary<string, object>;
if (savedDic!=null)//So the long running process is completed because other events, before the bookmark call comes.
{
return (string)savedDic["Result"];
}
WorkflowApplication app2;
var runningInstance = (System.Runtime.Caching.MemoryCache.Default.Get(id.ToString() + "Instance")) as WorkflowApplication;
if (runningInstance!=null)//the workflow instance is already reloaded
{
app2 = runningInstance;
}
else
{
app2 = new WorkflowApplication(new WaitForSignalOrDelayWorkflow());
}
IDictionary<string, object> outputs=null;
AutoResetEvent syncEvent = new AutoResetEvent(false);
app2.Completed = e =>
{
if (e.CompletionState == ActivityInstanceState.Closed)
{
outputs = e.Outputs;
}
syncEvent.Set();
};
app2.Unloaded = e =>
{
syncEvent.Set();
};
if (runningInstance == null)
{
try
{
app2.InstanceStore = WFDefinitionStore.Instance.Store;
app2.Load(id);
}
catch (System.Runtime.DurableInstancing.InstanceNotReadyException ex)
{
System.Diagnostics.Trace.TraceWarning(ex.Message);
throw;
}
}
var br = app2.ResumeBookmark(bookmarkName, null);
switch (br)
{
case BookmarkResumptionResult.Success:
break;
case BookmarkResumptionResult.NotFound:
throw new InvalidOperationException($"Can not find the bookmark: {bookmarkName}");
case BookmarkResumptionResult.NotReady:
throw new InvalidOperationException($"Bookmark not ready: {bookmarkName}");
default:
throw new InvalidOperationException("hey what's up");
}
syncEvent.WaitOne();
if (outputs == null)
throw new InvalidOperationException("How can outputs be null?");
return (string)outputs["Result"];
}
}
客户端代码
readonly Uri baseUri = new Uri("net.tcp:///");
ServiceHost CreateHost()
{
var host = new ServiceHost(typeof(Fonlow.WorkflowDemo.Contracts.WakeupService), baseUri);
host.AddServiceEndpoint("Fonlow.WorkflowDemo.Contracts.IWakeup", new NetTcpBinding(), "wakeup");
ServiceDebugBehavior debug = host.Description.Behaviors.Find<ServiceDebugBehavior>();
if (debug == null)
{
host.Description.Behaviors.Add(
new ServiceDebugBehavior() { IncludeExceptionDetailInFaults = true });
}
else
{
// make sure setting is turned ON
if (!debug.IncludeExceptionDetailInFaults)
{
debug.IncludeExceptionDetailInFaults = true;
}
}
return host;
}
[Fact]
public void TestWaitForSignalOrDelay()
{
Guid id;
// Create service host.
using (var host = CreateHost())
{
host.Open();
Assert.Equal(CommunicationState.Opened, host.State);
// Create a client that sends a message to create an instance of the workflow.
var client = ChannelFactory<Fonlow.WorkflowDemo.Contracts.IWakeup>.CreateChannel(new NetTcpBinding(), new EndpointAddress(new Uri(baseUri, "wakeup")));
id = client.Create("Service wakeup", TimeSpan.FromSeconds(100));
Assert.NotEqual(Guid.Empty, id);
Thread.Sleep(2000);//so the service may have time to persist.
var ok = client.LoadAndRun(id);//Optional. Just simulate that the workflow instance may be running againt because of other events.
Assert.True(ok);
var r = client.Wakeup(id, "Service wakeup");
Assert.Equal("Someone waked me up", r);
}
}
[Fact]
public void TestWaitForSignalOrDelayAfterDelay()
{
Guid id;
// Create service host.
using (var host = CreateHost())
{
host.Open();
Assert.Equal(CommunicationState.Opened, host.State);
// Create a client that sends a message to create an instance of the workflow.
var client = ChannelFactory<Fonlow.WorkflowDemo.Contracts.IWakeup>.CreateChannel(new NetTcpBinding(), new EndpointAddress(new Uri(baseUri, "wakeup")));
id = client.Create("Service wakeup", TimeSpan.FromSeconds(1));
Assert.NotEqual(Guid.Empty, id);
Thread.Sleep(2000);//so the service may have time to persist. Upon being reloaded, no bookmark calls needed.
var ok = client.LoadAndRun(id);
Assert.True(ok);
Thread.Sleep(8000);//So the service may have saved the result.
var r = client.Wakeup(id, "Service wakeup");
Assert.Equal("I sleep for good duration", r);
}
}
备注
工作流生命周期的维护在这里并不优雅且非线程安全,也远未全面覆盖长时间运行工作流的典型场景。要编写足够用于生产环境的自行维护函数,将需要大量的努力。
工作流服务主机上的带书签的非服务工作流
条件
您有一个带书签的非服务工作流,但您想将其作为服务运行,而无需像上述解决方案那样编写维护函数的麻烦。
解决方案
使用工作流服务主机扩展性。
ResumeBookmarkEndpoint
此设计灵感来自 MSDN 上的WCF 工作流示例。然而,该示例实际上并不能正常工作,并且由于单向调用和示例中控制台程序的立即终止,错误在控制台程序终止之前不会出现。
[ServiceContract]
public interface IWorkflowWithBookmark
{
[OperationContract(Name = "Create")]
Guid Create(IDictionary<string, object> inputs);
[OperationContract(Name = "ResumeBookmark")]
void ResumeBookmark(Guid instanceId, string bookmarkName, string message);
}
public class ResumeBookmarkEndpoint : WorkflowHostingEndpoint
{
public ResumeBookmarkEndpoint(Binding binding, EndpointAddress address)
: base(typeof(IWorkflowWithBookmark), binding, address)
{
}
protected override Guid OnGetInstanceId(object[] inputs, OperationContext operationContext)
{
//Create called
if (operationContext.IncomingMessageHeaders.Action.EndsWith("Create"))
{
return Guid.Empty;
}
//CreateWithInstanceId or ResumeBookmark called. InstanceId is specified by client
else if (operationContext.IncomingMessageHeaders.Action.EndsWith("ResumeBookmark"))
{
return (Guid)inputs[0];
}
else
{
throw new InvalidOperationException("Invalid Action: " + operationContext.IncomingMessageHeaders.Action);
}
}
protected override WorkflowCreationContext OnGetCreationContext(object[] inputs, OperationContext operationContext, Guid instanceId, WorkflowHostingResponseContext responseContext)
{
WorkflowCreationContext creationContext = new WorkflowCreationContext();
if (operationContext.IncomingMessageHeaders.Action.EndsWith("Create"))
{
Dictionary<string, object> arguments = (Dictionary<string, object>)inputs[0];
if (arguments != null && arguments.Count > 0)
{
foreach (KeyValuePair<string, object> pair in arguments)
{
//arguments for the workflow
creationContext.WorkflowArguments.Add(pair.Key, pair.Value);
}
}
//reply to client with the InstanceId
responseContext.SendResponse(instanceId, null);
}
else
{
throw new InvalidOperationException("Invalid Action: " + operationContext.IncomingMessageHeaders.Action);
}
return creationContext;
}
protected override System.Activities.Bookmark OnResolveBookmark(object[] inputs, OperationContext operationContext, WorkflowHostingResponseContext responseContext, out object value)
{
Bookmark bookmark = null;
value = null;
if (operationContext.IncomingMessageHeaders.Action.EndsWith("ResumeBookmark"))
{
//bookmark name supplied by client as input to IWorkflowCreation.ResumeBookmark
bookmark = new Bookmark((string)inputs[1]);
//value supplied by client as argument to IWorkflowCreation.ResumeBookmark
value = (string)inputs[2];
responseContext.SendResponse(null, null);//Not OneWay anymore.
}
else
{
// throw new NotImplementedException(operationContext.IncomingMessageHeaders.Action);
responseContext.SendResponse(typeof(void), null);
}
return bookmark;
}
}
与 MSDN 示例的区别
- 移除服务函数 CreateWithInstanceId。我不太明白它的作用,也找不到解释它的文章。如果您有任何想法,请留言。
- 将服务函数 ResumeBookmark 更改为普通调用而不是单向调用。这样,处理书签时发生的错误就可以返回给客户端。
创建工作流服务主机的帮助函数
static WorkflowServiceHost CreateHost(Activity activity, System.ServiceModel.Channels.Binding binding, EndpointAddress endpointAddress)
{
var host = new WorkflowServiceHost(activity);
{
SqlWorkflowInstanceStoreBehavior instanceStoreBehavior = new SqlWorkflowInstanceStoreBehavior("Server =localhost; Initial Catalog = WF; Integrated Security = SSPI")
{
HostLockRenewalPeriod = new TimeSpan(0, 0, 5),
RunnableInstancesDetectionPeriod = new TimeSpan(0, 0, 2),
InstanceCompletionAction = InstanceCompletionAction.DeleteAll,
InstanceLockedExceptionAction = InstanceLockedExceptionAction.AggressiveRetry,
InstanceEncodingOption = InstanceEncodingOption.GZip,
MaxConnectionRetries = 3,
};
host.Description.Behaviors.Add(instanceStoreBehavior);
//Make sure this is cleared defined, otherwise the bookmark is not really saved in DB though a new record is created. https://msdn.microsoft.com/en-us/library/ff729670%28v=vs.110%29.aspx
WorkflowIdleBehavior idleBehavior = new WorkflowIdleBehavior()
{
TimeToPersist = TimeSpan.Zero,
TimeToUnload = TimeSpan.Zero,
};
host.Description.Behaviors.Add(idleBehavior);
WorkflowUnhandledExceptionBehavior unhandledExceptionBehavior = new WorkflowUnhandledExceptionBehavior()
{
Action = WorkflowUnhandledExceptionAction.Terminate,
};
host.Description.Behaviors.Add(unhandledExceptionBehavior);
ResumeBookmarkEndpoint endpoint = new ResumeBookmarkEndpoint(binding, endpointAddress);
host.AddServiceEndpoint(endpoint);
host.Description.Behaviors.Add(new ServiceDebugBehavior() { IncludeExceptionDetailInFaults = true });
}
return host;
}
提示
- 可以通过配置连接行为。详情请参阅 MSDN。
测试用例
[Fact]
public void TestWaitForSignalOrDelayWithBookmark()
{
var endpointAddress = new EndpointAddress("net.tcp:///nonservice/wakeup");
var endpointBinding = new NetTcpBinding(SecurityMode.None);
using (var host = CreateHost(new WaitForSignalOrDelayWorkflow(), endpointBinding, endpointAddress))
{
host.Open();
Assert.Equal(CommunicationState.Opened, host.State);
IWorkflowWithBookmark client = new ChannelFactory<IWorkflowWithBookmark>(endpointBinding, endpointAddress).CreateChannel();
Guid id = client.Create(new Dictionary<string, object> { { "BookmarkName", "NonService Wakeup" }, { "Duration", TimeSpan.FromSeconds(100) } });
Assert.NotEqual(Guid.Empty, id);
Thread.Sleep(2000);//so the service may have time to persist.
client.ResumeBookmark(id, "NonService Wakeup", "something");
}
}
[Fact]
public void TestWaitForSignalOrDelayWithWrongBookmark()
{
var endpointAddress = new EndpointAddress("net.tcp:///nonservice/wakeup");
var endpointBinding = new NetTcpBinding(SecurityMode.None);
using (var host = CreateHost(new WaitForSignalOrDelayWorkflow(), endpointBinding, endpointAddress))
{
host.Open();
Assert.Equal(CommunicationState.Opened, host.State);
IWorkflowWithBookmark client = new ChannelFactory<IWorkflowWithBookmark>(endpointBinding, endpointAddress).CreateChannel();
Guid id = client.Create(new Dictionary<string, object> { { "BookmarkName", "NonService Wakeup" }, { "Duration", TimeSpan.FromSeconds(100) } });
Assert.NotEqual(Guid.Empty, id);
Thread.Sleep(2000);//so the service may have time to persist.
var ex = Assert.Throws<FaultException>(() =>
client.ResumeBookmark(id, "NonService Wakeupkkk", "something"));
Debug.WriteLine(ex.ToString());
}
}
备注
使用 WorkflowHosting 终结点存在一些限制。尽管许多人认为 WorkflowServiceHost 是 WorkflowApplication 的包装器,但我看到的访问 WorkflowApplication 实例的访问点很少,而行为所能实现的功能相当有限。
WaitForSignalOrDelayWorkflow 的输出是字典形式的 OutArgument。但似乎没有“官方”的方法来访问它。您可能需要向工作流添加逻辑,通过 SignalR 返回结果或将其存储在缓冲区/数据库中,然后客户端稍后可以拉取。如果您有其他想法,请留言。
因此,考虑到所有这些麻烦,使用工作流服务是更好的选择。通过相关性,您可以创建会话和工作流实例之间的隐式映射,这样客户端代码和自定义服务代码就不需要显式知道工作流实例 ID。
工作流服务
在上面的示例中,我演示了几种托管某些类型工作流的方法。然而,Instance ID 和书签是工作流的实现概念,但为了信息隐藏,通常工作流服务最好不要暴露其使用 Workflow Foundation 作为实现的事实,客户端程序也无需了解这些实现细节。在工作流服务中,对实例 ID 的需求由相关性处理,而对书签功能的需求则由 Receive 活动取代,该活动映射到服务客户端调用。
条件
您的工作流包含至少一个 Receive 活动,因此 WCF 会将传入的客户端调用传递给托管在 WorkflowServiceHost 中的工作流。
解决方案
在上一篇文章《通过单元测试学习 Windows Workflow Foundation 4.5:WorkflowApplication 和 WorkflowServiceHost》中,您可能已经读到,具有 Receive 的工作流可以通过托管工作流(活动/XAML)在 WorkflowServiceHost 中成为 WCF 服务,并且这种结构足以用于自托管解决方案,如 Windows 服务或桌面程序。
Workflow 4 提供了 WorkflowService 来支持这种开发模型。
- 在 WorkflowService/XAMLX 之上设计工作流。
- 运行工作流服务以生成 WSDL 和 XSD
- 根据 WSDL 编写客户端代码
有很多教程围绕着通过编写 XAMLX 来构建工作流服务。
我假设您已经阅读了其中一些文章或类似的,所以我将介绍一些您可能没有读过的小技巧。
在 FonlowWorkflowDemo.sln 中,有一个项目 BasicWCFService,它是通过“WCF Workflow Service Application”模板创建的。
Service1.xamlx 具有以下属性:
您可能会注意到有一个“CreateClientApi.bat”。
start "Launch IIS Express" "C:\Program Files (x86)\IIS Express\iisexpress.exe" /site:"BasicWFService" /apppool:"Clr4IntegratedAppPool" /config:"C:\VsProjects\FonlowWorkflowDemo\.vs\config\applicationhost.config"
"C:\Program Files (x86)\Microsoft SDKs\Windows\v8.1A\Bin\NETFX 4.5.1 Tools\svcutil.exe" https://:3065/Service1.xamlx?singleWsdl /noConfig /language:C# /n:http://fonlow.com/WorkflowDemo,Fonlow.WorkflowDemo.Clients /directory:..\BasicWFServiceClientApi /out:WFServiceClientApiAuto.cs
pause
运行此批处理文件将在客户端 API 库“BasicWFServiceClientAPI”中生成客户端 API 代码 WFServiceClientApiAuto.cs。
测试服务
const string realWorldEndpoint = "DefaultBinding_Workflow";
[Fact]
public void TestGetData()
{
using (var client = new WorkflowProxy(realWorldEndpoint))
{
Assert.Equal("abcaaa", client.Instance.GetData("abc"));
}
}
备注
如果您阅读了我的文章“WCF for the Real World, Not Hello World”,其中客户端 API 代码是从没有实现的契约库生成的,您可能想知道为什么不运行 svcutil.exe 来处理 BasicWFService.dll。这是因为该 dll 不包含契约信息,契约信息是在工作流 WCF 服务运行时通过契约推断
生成的。因此,我必须启动服务才能获取契约信息来生成客户端 API 代码。如果您有许多工作流和相应的契约,每次有新的或更新的契约时,生成/更新客户端 API 代码并添加/更新服务引用都会非常麻烦。因此,契约优先方法可能是一个不错的选择。
契约优先工作流服务
本章灵感来自“如何:创建消耗现有服务契约的工作流服务”。请也参考“契约优先工作流服务开发”作为基础教程,我将不再在此重复。
此示例还演示了基于内容的关联,使用 customerId。
步骤 1:定义用于购买图书的工作流的服务契约
[ServiceContract(Namespace = Constants.ContractNamespace)]
public interface IBookService
{
[OperationContract]
void Buy(Guid customerId, string bookName);
[OperationContract]
[return: System.ServiceModel.MessageParameterAttribute(Name = "CheckoutResult")]
string Checkout(Guid customerId);
[OperationContract]
void Pay(Guid customerId, string paymentDetail);
}
步骤 2:导入 IBookService 契约
步骤 3:创建新的工作流服务
在步骤 2 之后,您可能会在编辑 XAMLX 文件时在工具箱中看到 IBookService 组件。
将操作契约拖放到 XAMLX 文件中,您将得到一个购买图书然后结账和付款的工作流。
步骤 4:创建关联
创建新的 XAMLX 文件时,Visual Studio 会为您创建一个 CorrelationHandle。
通常,对于顺序工作流,您只需要一个这样的句柄。
对于 Buy_Receive 活动,请确保 CanCreateInstance 属性为 true,对于 CheckOut_Receive,则为 false。
在所有 Receive 活动中,确保 CorrelationsWith 属性指向 handle
并使用 customerId 进行基于内容的关联。
步骤 5:建立具有 SQL DB 的持久化层
在 Web.config 中,为相应的服务设置以下配置,如果主机仅托管工作流服务,则为所有服务设置。
<system.serviceModel>
<behaviors>
<serviceBehaviors>
<behavior>
<!-- To avoid disclosing metadata information, set the values below to false before deployment -->
<serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/>
<!-- To receive exception details in faults for debugging purposes, set the value below to true. Set to false before deployment to avoid disclosing exception information -->
<serviceDebug includeExceptionDetailInFaults="true"/>
<sqlWorkflowInstanceStore
connectionString="Server =localhost; Initial Catalog = WF; Integrated Security = SSPI"
instanceEncodingOption="GZip"
instanceCompletionAction="DeleteAll"
instanceLockedExceptionAction="AggressiveRetry"
hostLockRenewalPeriod="00:00:02"
runnableInstancesDetectionPeriod="00:00:02"/>
</behavior>
</serviceBehaviors>
备注
如果所有请求都去同一个主机实例,并且主机实例永远不会关闭,您可能不需要实例存储。即使工作流包含一些 Persist 活动,如果没有实例存储,Persist 也不会执行任何操作。
测试用例:所有请求发送到同一个主机实例
[Collection(TestConstants.IisExpressAndInit)]
public class WFServiceContractFirstIntegrationTests
{
const string hostBaseAddress = "https://:2327/BookService.xamlx";
[Fact]
public void TestBuyBook()
{
var client = ChannelFactory<IBookService>.CreateChannel(new BasicHttpBinding(), new EndpointAddress(hostBaseAddress));
const string bookName = "Alice in Wonderland";
var customerId = Guid.NewGuid();
client.Buy(customerId, bookName);
var checkOutBookName = client.Checkout(customerId);
Assert.Equal(bookName, checkOutBookName);
client.Pay(customerId, "Visa card");
}
}
测试用例:工作流运行步骤顺序错误
[Fact]
public void TestBuyBookInWrongOrderThrows()
{
var client = ChannelFactory<IBookService>.CreateChannel(new BasicHttpBinding(), new EndpointAddress(hostBaseAddress));
const string bookName = "Alice in Wonderland";
var customerId = Guid.NewGuid();
client.Buy(customerId, bookName);
var ex = Assert.Throws<FaultException>(
() => client.Pay(customerId, "Visa card"));
Assert.Contains("correct order", ex.ToString());
}
测试用例:不存在的关联
[Fact]
public void TestNonExistingSessionThrows()
{
var client = ChannelFactory<IBookService>.CreateChannel(new BasicHttpBinding(), new EndpointAddress(hostBaseAddress));
var customerId = Guid.NewGuid();
var ex = Assert.Throws<FaultException>(
() => { client.Checkout(customerId); });
Assert.Contains("InstancePersistenceCommand", ex.ToString());
}
测试用例:同一个工作流实例的请求由 2 个主机实例处理
此测试用例模拟同一个工作流实例的请求在不同阶段由 2 个主机实例处理。
public class WFServicePersistenceTests
{
const string hostBaseAddress = "https://:2327/BookService.xamlx";
[Fact]
public void TestBuyBook()
{
IisExpressAgent agent = new IisExpressAgent();
const string bookName = "Alice in Wonderland";
var customerId = Guid.NewGuid();
agent.Start();
var client = ChannelFactory<IBookService>.CreateChannel(new BasicHttpBinding(), new EndpointAddress(hostBaseAddress));
client.Buy(customerId, bookName);
agent.Stop();
System.Threading.Thread.Sleep(5000);//WF runtime seems to be taking long time to persist data. And there may be delayed write, so closing the service may force writing data to DB.
// The wait time has better to be hostLockRenewalPeriod + runnableInstancesDetectionPeriod + 1 second.
agent.Start();
client = ChannelFactory<IBookService>.CreateChannel(new BasicHttpBinding(), new EndpointAddress(hostBaseAddress));
var checkOutBookName = client.Checkout(customerId);
Assert.Equal(bookName, checkOutBookName);
client.Pay(customerId, "Visa card");
agent.Stop();
}
}
有关微调您的工作流服务,请参考“持久化最佳实践”。
关注点
在上面说明的各种方法中,契约优先方法最适合拥有大量复杂工作流需要托管的大型企业项目。
上面演示的在工作流服务中的工作流持久化并没有持久化工作流定义,因为工作流和服务是作为一个整体存在的。虽然一个工作流可能有多个 Receive 活动,但 WCF 运行时和 WF 运行时本身就知道应该使用哪个工作流定义。我猜这可能就是 WF 4 移除了内置工作流定义持久化的原因之一。