I am creating a service (FaultTolerantCommunication
) that wraps a call to a (web) service. Its responsibility is to switch the endpoint URL in the event of a TimeoutException
.
Currently the web service is a soap web service but I am attempting to make it reusable for other remote services such as a JSON based web service, for example.
I am looking for a review on the design. I have not really done any defensive programming yet in terms of argument validation and null checking but please feel free to suggest improvements on this too.
The idea is for the client code to call the service as below:
I would probably extract an interface from FaultTolerantCommunication
and inject an instance using an IoC container.
Client Code:
var faultTolerantCommunication = new FaultTolerantCommunication<FooRequest, FooResponse, ISoapClient>(this.serviceCaller);
FooResponse response = faultTolerantCommunication.Request(request, this.soapClient);
Fault Tolerant Communication
/// <summary>
/// Fault Tolerant Communication. Recurses to fall over web service endpoints in event of timeout.
/// </summary>
public class FaultTolerantCommunication<TRequest, TResponse, TClient>
where TRequest : FooRequest, new()
where TResponse : FooResponse, new()
where TClient : ISoapClient
{
private readonly Configuration config;
private readonly FooIntegration FooIntegration;
private readonly ExceptionLogger exceptionLogger;
private TClient client;
private readonly SoapClientSwitcher<TClient> switcher;
public FaultTolerantCommunication(FooIntegration FooIntegration)
{
// Read the config during construction.
config = new ConfigReader().Read();
// Constructor inject FooIntegration
this.FooIntegration = FooIntegration;
// Exception Logger
this.exceptionLogger = new ExceptionLogger("FaultTolerantCommunication");
// Generic SoapClient
this.client = default(TClient);
// Soap Client Switcher
switcher = new SoapClientSwitcher<TClient>(config);
}
/// <summary>
/// Do a single request.
/// </summary>
/// <param name="request">The Request</param>
/// <param name="client">The Client</param>
/// <returns>The Response</returns>
private TResponse DoRequest(TRequest request, TClient client)
{
// Set the endpoint soap client.
FooIntegration.ChangeEndPoint(client);
// Call the service
FooResponse response = this.FooIntegration.DoTransaction(request);
// Return the response
return (TResponse)response;
}
/// <summary>
/// Recursively fall back to secondary and stand-in endpoints on TimeoutException
/// </summary>
/// <param name="request">The Request</param>
/// <param name="client">The Client</param>
/// <returns>The Response</returns>
public TResponse Request(TRequest request, TClient client)
{
try
{
return DoRequest(request, client);
}
catch (TimeoutException timeoutException)
{
// Log the time out
exceptionLogger.DbLogException(timeoutException);
// Change endpoint
this.client = switcher.Switch(this.client);
FooIntegration.ChangeEndPoint(client);
// Recurse
return Request(request, client);
}
}
}
Soap Client Switcher
internal class SoapClientSwitcher<TClient> where TClient : ISoapClient
{
private readonly Configuration config;
internal SoapClientSwitcher(Configuration config)
{
this.config = config;
}
internal TClient Switch(TClient client)
{
if (client is SoapClient)
{
client = (TClient)new SoapClientFactory().GetSecondarySoapClient(config);
return client;
}
if (client is SecondarySoapClient)
{
client = (TClient)new SoapClientFactory().GetStandInSoapClient(config);
return client;
}
if (client is StandInSoapClient)
{
client = (TClient)new SoapClientFactory().GetPrimarySoapClient(config);
return client;
}
return client;
}
}
ISoapClient
public interface ISoapClient : IClient<string, string>
{
string EndPointUrl { get; set; }
}
IClient
public interface IClient<TRequest, TResponse>
{
TResponse Transaction(TRequest transaction);
}
Soap Client Factory
public class SoapClientFactory
{
private ISoapClient soapCient;
public ISoapClient GetPrimarySoapClient(Configuration configuration)
{
soapCient = new SoapClient(configuration);
return soapCient;
}
public ISoapClient GetSecondarySoapClient(Configuration configuration)
{
soapCient = new SecondarySoapClient(configuration);
return soapCient;
}
public ISoapClient GetStandInSoapClient(Configuration configuration)
{
soapCient = new StandInSoapClient(configuration);
return soapCient;
}
}
Soap Client
public class SoapClient : ISoapClient
{
private wsSoapClient client;
private string password;
public SoapClient(Configuration configuration)
{
// Some Config reading which is omitted for brevity
EndpointAddress address = SetEndPoint(settings);
client = new wsSoapClient(binding, address);
}
protected virtual EndpointAddress SetEndPoint(AppSettingsSection settings)
{
EndpointAddress address = new EndpointAddress(settings.Settings["EndpointUrl"].Value);
return address;
}
public string Transaction(string transaction)
{
Console.WriteLine("Calling real soapClient");
return client.Transaction(transaction, password);
}
public string EndPointUrl { get; set; }
}
Request
if all endpoints fail? You should at least limit the recursion level. Timeouts can occur even if the endpoint is still 'up' due to heavy load - switching every new request to the next endpoint will most likely bring that one down too and so on....Some sort of load-balancing, maybe detect if the time it takes to get a reply degrades, might help remove some of the timeouts. Do all endpoint have the same 'capacity'/ability to handle the same number of clients? \$\endgroup\$