I will find out what can be done using the WCF routing service. However, "spin it to see what it can do."
My understanding of the routing service is that when a message passes, the service will try to transmit it depending on which endpoint appears first in the backup list. If this fails, it will continue to try the next, and then the next, until something works or you have nothing to try.
What I would like to do is access this event with an error so that I can:
- Record a failure
- Send an email notification that the endpoint is not working
- It is not necessary to remove the endpoint from the backup list so that it does not slow down subsequent messages from the current system.
Cannot find how to extend WCF structure to receive this particular event.
Is this what WCF routing service can do? Any push in the right direction would be greatly appreciated.
Currently, I have 30 dynamically created routing services hosted in IIS (or, more precisely, ASP.NET Development Server for Visual Studio 2010). I am setting up service routes in Global.asax as shown below.
protected void Application_Start(object sender, EventArgs e) { List<Type> serviceTypes = ServiceUtility.GetServiceTypes(); foreach (Type st in serviceTypes) { string route = String.Format("Services/{0}.svc", ServiceUtility.GetServiceName(st)); RouteTable.Routes.Add(new ServiceRoute(route, new RoutingServiceHostFactory(st), typeof(System.ServiceModel.Routing.RoutingService))); } }
ServiceUtility and RoutingServiceHostFactory are custom classes. Please note that IPolicyService is the interface of the WCF service contract in the assembly of interest to us.
public static class ServiceUtility { public static List<Type> GetServiceTypes() { Type policyInterfaceType = typeof(IPolicyService); Assembly serviceContractsAssembly = Assembly.GetAssembly(policyInterfaceType); Type[] serviceContractsAssemblyTypes = serviceContractsAssembly.GetTypes(); List<Type> serviceTypes = new List<Type>(); foreach (Type t in serviceContractsAssemblyTypes) { if (!t.IsInterface) continue; object[] attrib = t.GetCustomAttributes(typeof(ServiceContractAttribute), false); if (attrib == null || attrib.Length <= 0) continue; serviceTypes.Add(t); } return serviceTypes; } // Other stuff }
I create my ServiceHosts as follows. I briefly omitted some of my helper methods.
public class RoutingServiceHostFactory : ServiceHostFactory { private Type BackendServiceType { get; set; } private Binding BackendServiceBinding { get; set; } public RoutingServiceHostFactory(Type backendServiceType) { this.BackendServiceType = backendServiceType; this.BackendServiceBinding = ServiceUtility.GetBinding(this.BackendServiceType); } private const string DOMAIN_LIVE = "http://localhost:2521/"; private const string DOMAIN_DEAD_1 = "http://localhost:2522/"; private const string DOMAIN_DEAD_2 = "http://localhost:2524/"; private const string DOMAIN_DEAD_3 = "http://localhost:2525/"; private const string DOMAIN_DEAD_4 = "http://localhost:2526/"; private const string DOMAIN_DEAD_5 = "http://localhost:2527/"; protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses) { ServiceHost host = base.CreateServiceHost(serviceType, baseAddresses); this.BindEndpoints(host, baseAddresses); this.ConfigureRoutingBehavior(host); this.ConfigureServiceMetadataBehavior(host); this.ConfigureDebugBehavior(host); host.Description.Behaviors.Add(new RoutingServiceErrorHandlerInjector()); return host; } // Other Stuff private void ConfigureRoutingBehavior(ServiceHost host) { string deadAddress1 = ServiceUtility.GetServiceUrl(DOMAIN_DEAD_1, this.BackendServiceType); string deadAddress2 = ServiceUtility.GetServiceUrl(DOMAIN_DEAD_2, this.BackendServiceType); string deadAddress3 = ServiceUtility.GetServiceUrl(DOMAIN_DEAD_3, this.BackendServiceType); string deadAddress4 = ServiceUtility.GetServiceUrl(DOMAIN_DEAD_4, this.BackendServiceType); string deadAddress5 = ServiceUtility.GetServiceUrl(DOMAIN_DEAD_5, this.BackendServiceType); string realAddress = ServiceUtility.GetServiceUrl(DOMAIN_LIVE, this.BackendServiceType); RoutingConfiguration rc = new RoutingConfiguration(); ContractDescription contract = new ContractDescription("IRequestReplyRouter"); ServiceEndpoint deadDestination1 = new ServiceEndpoint(contract, this.BackendServiceBinding, new EndpointAddress(deadAddress1)); ServiceEndpoint deadDestination2 = new ServiceEndpoint(contract, this.BackendServiceBinding, new EndpointAddress(deadAddress2)); ServiceEndpoint deadDestination3 = new ServiceEndpoint(contract, this.BackendServiceBinding, new EndpointAddress(deadAddress3)); ServiceEndpoint deadDestination4 = new ServiceEndpoint(contract, this.BackendServiceBinding, new EndpointAddress(deadAddress4)); ServiceEndpoint deadDestination5 = new ServiceEndpoint(contract, this.BackendServiceBinding, new EndpointAddress(deadAddress5)); ServiceEndpoint realDestination = new ServiceEndpoint(contract, this.BackendServiceBinding, new EndpointAddress(realAddress)); List<ServiceEndpoint> backupList = new List<ServiceEndpoint>(); backupList.Add(deadDestination1); backupList.Add(deadDestination2); backupList.Add(deadDestination3); backupList.Add(deadDestination4); backupList.Add(deadDestination5); backupList.Add(realDestination); rc.FilterTable.Add(new MatchAllMessageFilter(), backupList); RoutingBehavior rb = new RoutingBehavior(rc); host.Description.Behaviors.Add(rb); } // Other Stuff }
Port 2521 has an actual website at the other end that hosts some WCF services. The other ports mentioned above do not have anything listening.
For context, here is my Web.config for the routing site. Please note that timeouts, etc. They are just the result of my turn, do not take them too seriously.
<?xml version="1.0"?> <configuration> <system.web> <compilation debug="true" targetFramework="4.0" /> </system.web> <system.serviceModel> <serviceHostingEnvironment aspNetCompatibilityEnabled="true" /> <bindings> <wsHttpBinding> <binding name="TestBinding" allowCookies="True" closeTimeout="00:04:00" openTimeout="00:00:10" receiveTimeout="00:05:00" sendTimeout="00:05:00" maxReceivedMessageSize="15728640"> <security> <message establishSecurityContext="true" /> </security> </binding> </wsHttpBinding> </bindings> </system.serviceModel> </configuration>
EDIT
In response to TheDoctor's answer below, I thought that I should expand on what I have been doing with this attempt since I originally posted. I tried to implement the IErrorHandler interface. However, I had no luck with that.
Note that in the above example, my RoutingServiceHostFactory has changed a bit. Now I am adding the RoutingServiceErrorHandlerInjector behavior to the service description. Note that I also added additional dead endpoints to the backup list for illustrative purposes.
public class RoutingServiceErrorHandlerInjector : IServiceBehavior { #region IServiceBehavior Members public void AddBindingParameters(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, System.ServiceModel.Channels.BindingParameterCollection bindingParameters) { } public void ApplyDispatchBehavior(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase) { foreach (ChannelDispatcher chanDisp in serviceHostBase.ChannelDispatchers) { chanDisp.ErrorHandlers.Add(new RoutingServiceErrorHandler()); } } public void Validate(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase) { } #endregion } public class RoutingServiceErrorHandler : IErrorHandler { #region IErrorHandler Members public bool HandleError(Exception error) { throw new NotImplementedException(error.Message, error); } public void ProvideFault(Exception error, MessageVersion version, ref Message fault) { throw new NotImplementedException(error.Message, error); } #endregion }
I expected that I should fire the ProvideFault or HandleError event for deadDestination1 to deadDestination5. I have breakpoints on the NotImplementedExceptions above in my debugger. But this code is never activated. Ultimately, the calls go to the real address at the end of the backup list, and the client / server application that I use to verify this RoutingService works fine. Communication is slower, but still good within the timeout.
However, if I comment on the line backupList.Add(realDestination); from the ConfigureRoutingBehavior method above, then the RoutingServiceErrorHandler.ProvideFault method will be launched ... But it contains only information related to deadDestination5. Any exceptions or errors that could have been thrown for deadDestination1 via deadDestination4 just disappear on me.
In addition, I went with the RedGate debugger, stepping over the displayed code for the RoutingService. It was difficult for me, since I was not used to debugging optimized code, so for me there were practically no variables available. But from the eye step by logic below:
// This has been taken from System.ServiceModel.Routing.RoutingService // via the RedGate decompiler - unsure about it ultimate accuracy. [AspNetCompatibilityRequirements(RequirementsMode=AspNetCompatibilityRequirementsMode.Allowed), ServiceBehavior(AddressFilterMode=AddressFilterMode.Any, InstanceContextMode=InstanceContextMode.PerSession, UseSynchronizationContext=false, ValidateMustUnderstand=false)] public sealed class RoutingService : ISimplexDatagramRouter, ISimplexSessionRouter, IRequestReplyRouter, IDuplexSessionRouter, IDisposable { [OperationBehavior(Impersonation=ImpersonationOption.Allowed), TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")] IAsyncResult IRequestReplyRouter.BeginProcessRequest(Message message, AsyncCallback callback, object state) { return this.BeginProcessRequest<IRequestReplyRouter>(message, callback, state); } private IAsyncResult BeginProcessRequest<TContract>(Message message, AsyncCallback callback, object state) { IAsyncResult result; try { System.ServiceModel.Routing.FxTrace.Trace.SetAndTraceTransfer(this.ChannelExtension.ActivityID, true); result = new ProcessRequestAsyncResult<TContract>(this, message, callback, state); } catch (Exception exception) { if (TD.RoutingServiceProcessingFailureIsEnabled()) { TD.RoutingServiceProcessingFailure(this.eventTraceActivity, OperationContext.Current.Channel.LocalAddress.ToString(), exception); } throw; } return result; } }
The relevant sections from System.ServiceModel.Routing.ProcessRequestAsyncResult are shown below. This is also debugging through RedGate, so it cannot be changed. I believe RedGate and the released source from Microsoft are accurate. #hesaiddubiously
internal class ProcessRequestAsyncResult<TContract> : TransactedAsyncResult { public ProcessRequestAsyncResult(RoutingService service, Message message, AsyncCallback callback, object state) : base(callback, state) { this.allCompletedSync = true; this.service = service; this.messageRpc = new System.ServiceModel.Routing.MessageRpc(message, OperationContext.Current, service.ChannelExtension.ImpersonationRequired); if (TD.RoutingServiceProcessingMessageIsEnabled()) { TD.RoutingServiceProcessingMessage(this.messageRpc.EventTraceActivity, this.messageRpc.UniqueID, message.Headers.Action, this.messageRpc.OperationContext.EndpointDispatcher.EndpointAddress.Uri.ToString(), (this.messageRpc.Transaction != null) ? "True" : "False"); } try { EndpointNameMessageFilter.Set(this.messageRpc.Message.Properties, service.ChannelExtension.EndpointName); this.messageRpc.RouteToSingleEndpoint<TContract>(this.service.RoutingConfig); } catch (MultipleFilterMatchesException exception) { throw System.ServiceModel.Routing.FxTrace.Exception.AsError(new ConfigurationErrorsException(System.ServiceModel.Routing.SR.ReqReplyMulticastNotSupported(this.messageRpc.OperationContext.Channel.LocalAddress), exception)); } while (this.StartProcessing()) { } } private bool StartProcessing() { bool flag = false; SendOperation operation = this.messageRpc.Operations[0]; this.currentClient = this.service.GetOrCreateClient<TContract>(operation.CurrentEndpoint, this.messageRpc.Impersonating); if (TD.RoutingServiceTransmittingMessageIsEnabled()) { TD.RoutingServiceTransmittingMessage(this.messageRpc.EventTraceActivity, this.messageRpc.UniqueID, "0", this.currentClient.Key.ToString()); } try { Message message; if ((this.messageRpc.Transaction != null) && operation.HasAlternate) { throw System.ServiceModel.Routing.FxTrace.Exception.AsError(new ConfigurationErrorsException(System.ServiceModel.Routing.SR.ErrorHandlingNotSupportedReqReplyTxn(this.messageRpc.OperationContext.Channel.LocalAddress))); } if (operation.AlternateEndpointCount > 0) { message = this.messageRpc.CreateBuffer().CreateMessage(); } else { message = this.messageRpc.Message; } operation.PrepareMessage(message); IAsyncResult result = null; using (base.PrepareTransactionalCall(this.messageRpc.Transaction)) { using (IDisposable disposable = null) { try { } finally { disposable = this.messageRpc.PrepareCall(); } result = this.currentClient.BeginOperation(message, this.messageRpc.Transaction, base.PrepareAsyncCompletion(ProcessRequestAsyncResult<TContract>.operationCallback), this); } } if (!base.CheckSyncContinue(result)) { return flag; } if (this.OperationComplete(result)) { base.Complete(this.allCompletedSync); return flag; } return true; } catch (Exception exception) { if (Fx.IsFatal(exception)) { throw; } if (!this.HandleClientOperationFailure(exception)) { throw; } return true; } } }
For my surface reading, it seems to me that ProcessRequestAsyncResult does the job of navigating through the backup list using the ProcessRequestAsyncResult.StartProcessing method. However, StartProcess () does not seem to throw every single exception, but rather selectively selects whether to throw exceptions.
It seems that only the exception for the final dead address is actually thrown by StartProcess (), and then thrown using the catching clause RoutingService.BeginProcessRequest, and then finally does everything it can to activate in my IErrorHandler implementation.
This strongly tells me that what I'm trying to do here cannot be done with the current implementation of the System.ServiceModel.Routing namespace. Note that a RoutingService is a sealed class, so I cannot extend it with a base class to change this behavior, even if I thought it was a good idea (which I don't have).
But again, note that this is a superficial reading. I am easily mistaken. In fact, I would really was considered erroneous. I would prefer to find a way to get the RoutingService to do what I want it to not, rather than minimize.