ActiveRecord Session Scope and WCF Redux
Awhile ago, I posted a solution I was using for a project for managing SessionScope with WCF using an ICallContextInitializer. Because of another issue in the codebase (an error in a custom listener), an exception was getting thrown when session.Dispose() was called in the AfterInvoke method. What was interesting was that it appeared to the client that the WCF call had completed successfully — even though this was not the case. It turns out that ICallContextInitializer is not the best WCF extension to use to manage the scope. Here’s a quick console application that will show the behavior of throwing an exception in AfterInvoke. Notice how the console window will crash before the ReadKey is hit:
1: class Program
2: {
3: static void Main(string[] args)
4: {
5: using (var host = new ServiceHost(typeof(Operation)))
6: {
7: var endpointAddress = new EndpointAddress("net.tcp://localhost/optest");
8: var endpoint = new ServiceEndpoint(ContractDescription.GetContract(typeof(IOperation)),
9: new NetTcpBinding(),
10: endpointAddress);
11: endpoint.Behaviors.Add(new ThrowBehavior());
12: host.Description.Endpoints.Add(endpoint);
13: host.Open();
14:
15: var op = ChannelFactory<IOperation>.CreateChannel(new NetTcpBinding(), endpointAddress);
16: op.Op();
17: }
18:
19: Console.ReadKey();
20: }
21: }
22:
23:
[ServiceContract]
24: public interface IOperation
25: {
26: [OperationContract]
27: void Op();
28: }
29:
30: public class Operation : IOperation
31: {
32: public void Op()
33: {
34: }
35: }
36:
37: internal class ThrowBehavior : IEndpointBehavior
38: {
39: public void Validate(ServiceEndpoint endpoint)
40: {
41: }
42:
43: public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
44: {
45: }
46:
47: public void Apply DispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
48: {
49: foreach (DispatchOperation operation in endpointDispatcher.DispatchRuntime.Operations)
50: {
51: operation.CallContextInitializers.Add(new ThrowCallContextInitializer());
52: }
53: }
54:
55: public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
56: {
57: }
58: }
59:
60: internal class ThrowCallContextInitializer : ICallContextInitializer
61: {
62: public object BeforeInvoke(InstanceContext instanceContext, IClientChannel channel, Message message)
63: {
64: return null;
65: }
66:
67: public void AfterInvoke(object correlationState)
68: {
69: throw new InvalidOperationException();
70: }
71: }
My revised code uses a DispatchMessageInterceptor instead:
1: public class ARSessionScopeBehavior : IEndpointBehavior
2: {
3:
4: /// <summary>
5: /// Implement to pass data at runtime to bindings to support custom behavior.
6: /// </summary>
7: /// <param name="endpoint">The endpoint to modify.</param>
8: /// <param name="bindingParameters">The objects that binding elements require to support the behavior.</param>
9: public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
10: {
11: }
12:
13: /// <summary>
14: /// Implements a modification or extension of the service across an endpoint.
15: /// </summary>
16: /// <param name="endpoint">The endpoint that exposes the contract.</param>
17: /// <param name="endpointDispatcher">The endpoint dispatcher to be modified or extended.</param>
18: public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
19: {
20: endpointDispatcher.DispatchRuntime.MessageInspectors.Add(new ARDispatcherMessageInspector());
21: }
22:
23: /// <summary>
24: /// Implements a modification or extension of the client across an endpoint.
25: /// </summary>
26: /// <param name="endpoint">The endpoint that is to be customized.</param>
27: /// <param name="clientRuntime">The client runtime to be customized.</param>
28: public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
29: {
30: }
31:
32: /// <summary>
33: /// Implement to confirm that the endpoint meets some intended criteria.
34: /// </summary>
35: /// <param name="endpoint">The endpoint to validate.</param>
36: public void Validate(ServiceEndpoint endpoint)
37: {
38: }
39:
40: }
1: public class ARDispatchMessageInspector : IDispatchMessageInspector
2: {
3:
4: private static readonly ILogger Logger = LogManager.GetLogger(ARDispatchMessageInspector);
5:
6: private const string ActiveRecordSessionScopeKey = "wcf.ar.sessionscope";
7:
8: #region Implementation of IDispatchMessageInspector
9:
10: /// <summary>
11: /// Called after an inbound message has been received but before the message is dispatched to the intended operation.
12: /// </summary>
13: /// <returns>
14: /// The object used to correlate state. This object is passed back in the <see cref="M:System.ServiceModel.Dispatcher.IDispatchMessageInspector.BeforeSendReply(System.ServiceModel.Channels.Message@,System.Object)" /> method.
15: /// </returns>
16: /// <param name="request">The request message.</param>
17: /// <param name="channel">The incoming channel.</param>
18: /// <param name="instanceContext">The current service instance.</param>
19: public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
20: {
21: var scope = Local.Data[ActiveRecordSessionScopeKey] as SessionScope;
22: if (scope == null)
23: {
24: Logger.Debug("Creating new ActiveRecord SessonScope");
25: scope = new SessionScope();
26: Local.Data[ActiveRecordSessionScopeKey] = scope;
27: }
28: return null;
29: }
30:
31: /// <summary>
32: /// Called after the operation has returned but before the reply message is sent.
33: /// </summary>
34: /// <param name="reply">The reply message. This value is null if the operation is one way.</param>
35: /// <param name="correlationState">The correlation object returned from the <see cref="M:System.ServiceModel.Dispatcher.IDispatchMessageInspector.AfterReceiveRequest(System.ServiceModel.Channels.Message@,System.ServiceModel.IClientChannel,System.ServiceModel.InstanceContext)" /> method.</param>
36: public void BeforeSendReply(ref Message reply, object correlationState)
37: {
38: var scope = Local.Data[ActiveRecordSessionScopeKey] as SessionScope;
39: if (scope != null)
40: {
41: if (SessionScope.Current == scope)
42: {
43: Logger.Debug("Disposing ActiveRecord SessonScope");
44: scope.Dispose();
45: }
46: else
47: {
48: Logger.Warn(
49: "The current Active Record session scope does not equal the correlated session scope from WCF. This scope will not " +
50: "be disposed, however, this could lead to entities not being properly flushed.");
51: }
52: Local.Data[ActiveRecordSessionScopeKey] = null;
53: }
54: }
So far, no problems. Funny enough, when searching for solutions to this problem, I noticed some other posts where people recommended using the ICallContextInitializer to manage unit of work. I assume they just have not had any issues when calling Dispose() and have not seen this behavior.