Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
jweber committed Oct 14, 2014
2 parents d61df79 + 10c3316 commit aeb9938
Show file tree
Hide file tree
Showing 8 changed files with 209 additions and 49 deletions.
2 changes: 1 addition & 1 deletion .semver
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
:major: 2
:minor: 0
:patch: 8
:patch: 9
:special: ''
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Utility to generate fault tolerant and retry capable dynamic proxies for WCF ser

With normal Service Reference or ChannelFactory instantiated clients, care must be taken to abort and recreate the client in the event that a communication fault occurs. The goal of this project is to provide an easy-to-use method of creating WCF clients that are self healing and tolerant of temporary network communication errors while still being as transparently useable as default WCF clients.

Generated proxies fully support the C# 5 async/await syntax for service contracts that define `Task` based async operations. Automatic extension of non-async ready service contracts with full async support can be created through use of the [`WcfClientProxy.CreateAsyncProxy<T>()`](README.md#async-support) factory method.

Installation
------------

Expand Down Expand Up @@ -126,8 +128,12 @@ will configure the proxy based on the `<endpoint/>` as setup in the _app.config_
#### SetEndpoint(Binding binding, EndpointAddress endpointAddress)
Configures the proxy to communicate with the endpoint using the given `binding` at the `endpointAddress`

#### HandleResponse\<TResponse\>(Predicate\<TResponse\> where, Func\<TResponse, TResponse\> handler)
Sets up the proxy to allow inspection and manipulation of responses from the service.
#### HandleResponse\<TResponse\>(Predicate\<TResponse\> where, Action\<TResponse\> handler)
_overload:_ `HandleResponse<TResponse>(Predicate<TResponse> where, Func<TResponse, TResponse> handler)`

Sets up the proxy to allow inspection and manipulation of responses from the service.

The `TResponse` value can be a type as specific or general as needed. For instance, `c.HandleResponse<SealedResponseType>(...)` will only handle responses of type `SealedResponseType` whereas `c.HandleResponse<object>(...)` will be fired for all responses.

For example, if sensitive information is needed to be stripped out of certain response messages, `HandleResponse` can be used to do this.

Expand All @@ -142,6 +148,8 @@ For example, if sensitive information is needed to be stripped out of certain re

`HandleResponse` can also be used to throw exceptions on the client side based on the inspection of responses.

Multiple calls to `HandleResponse` can be made with different variations of `TResponse` and the `Predicate<TResponse>`. With this in mind, it is possible to have more than one response handler manipulate a `TResponse` object. It is important to note that there is *no guarantee* in the order that the response handlers operate at runtime.

#### MaximumRetries(int retryCount)
Sets the maximum amount of times the the proxy will additionally attempt to call the service in the event it encounters a known retry-friendly exception or response. If retryCount is set to 0, then only one request attempt will be made.

Expand Down
85 changes: 85 additions & 0 deletions source/WcfClientProxyGenerator.Tests/ProxyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1434,6 +1434,91 @@ public void HandleResponse_CanThrowException()
.With.Message.EqualTo("test"));
}

[Test]
public void HandleResponse_MultipleHandlersCanBeRunOnResponse()
{
var countdownEvent = new CountdownEvent(2);

var mockService = new Mock<ITestService>();

var serviceResponse = new Response()
{
ResponseMessage = "message",
StatusCode = 100
};

mockService
.Setup(m => m.TestMethodComplex(It.IsAny<Request>()))
.Returns(serviceResponse);

var serviceHost = InProcTestFactory.CreateHost<ITestService>(new TestServiceImpl(mockService));

var proxy = WcfClientProxy.Create<ITestService>(c =>
{
c.SetEndpoint(serviceHost.Binding, serviceHost.EndpointAddress);

c.HandleResponse<Response>(r => r.StatusCode == serviceResponse.StatusCode, r =>
{
countdownEvent.Signal();
});

c.HandleResponse<Response>(r => r.ResponseMessage == serviceResponse.ResponseMessage, r =>
{
countdownEvent.Signal();
});
});

var response = proxy.TestMethodComplex(new Request());

if (!countdownEvent.Wait(250))
Assert.Fail("Expected both callbacks to fire");
}

[Test]
public void HandleResponse_MultipleHandlersCanBeRunOnResponse_WhereHandlersAreInheritingTypes()
{
var countdownEvent = new CountdownEvent(3);

var mockService = new Mock<ITestService>();

var serviceResponse = new Response()
{
ResponseMessage = "message",
StatusCode = 100
};

mockService
.Setup(m => m.TestMethodComplex(It.IsAny<Request>()))
.Returns(serviceResponse);

var serviceHost = InProcTestFactory.CreateHost<ITestService>(new TestServiceImpl(mockService));

var proxy = WcfClientProxy.Create<ITestService>(c =>
{
c.SetEndpoint(serviceHost.Binding, serviceHost.EndpointAddress);

c.HandleResponse<object>(r =>
{
countdownEvent.Signal();
});

c.HandleResponse<IResponseStatus>(r => r.StatusCode == serviceResponse.StatusCode, r =>
{
countdownEvent.Signal();
});

c.HandleResponse<Response>(r => r.ResponseMessage == serviceResponse.ResponseMessage, r =>
{
countdownEvent.Signal();
});
});

var response = proxy.TestMethodComplex(new Request());

if (!countdownEvent.Wait(250))
Assert.Fail("Expected both callbacks to fire");
}

#region AsyncProxy


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,26 @@ public void AddExceptionToRetryOn_RetriesOnConfiguredException_WhenPredicateMatc
() => new TestException("test"));
}

[Test]
public void AddExceptionToRetryOn_RetriesOnMatchedPredicate_WhenMultiplePredicatesAreRegistered()
{
this.AssertThatCallRetriesOnException<TestException>(
c =>
{
c.AddExceptionToRetryOn<TestException>(e => e.TestExceptionMessage == "test");
c.AddExceptionToRetryOn<TestException>(e => e.TestExceptionMessage == "other");
},
() => new TestException("test"));

this.AssertThatCallRetriesOnException<TestException>(
c =>
{
c.AddExceptionToRetryOn<TestException>(e => e.TestExceptionMessage == "test");
c.AddExceptionToRetryOn<TestException>(e => e.TestExceptionMessage == "other");
},
() => new TestException("other"));
}

#endregion

#region RetryOnResponseCondition
Expand Down Expand Up @@ -108,6 +128,33 @@ public void AddResponseToRetryOn_RetriesOnConfiguredResponse_ForResponseBaseType
Assert.That(response.StatusCode, Is.EqualTo(successResponse.StatusCode));
}

[Test]
public void AddResponseToRetryOn_RetriesOnMatchedPredicate_WhenMultiplePredicatesAreRegistered()
{
var mockService = new Mock<ITestService>();

var firstFailResponse = new Response { StatusCode = 100 };
var secondFailResponse = new Response { StatusCode = 101 };
var successResponse = new Response { StatusCode = 1 };

mockService
.SetupSequence(m => m.TestMethodComplex(It.IsAny<Request>()))
.Returns(firstFailResponse)
.Returns(secondFailResponse)
.Returns(successResponse);

var actionInvoker = new RetryingWcfActionInvoker<ITestService>(
() => new TestServiceImpl(mockService),
() => new ConstantDelayPolicy(TimeSpan.FromMilliseconds(50)),
retryCount: 2);

actionInvoker.AddResponseToRetryOn<IResponseStatus>(r => r.StatusCode == firstFailResponse.StatusCode);
actionInvoker.AddResponseToRetryOn<IResponseStatus>(r => r.StatusCode == secondFailResponse.StatusCode);

var response = actionInvoker.Invoke(s => s.TestMethodComplex(new Request()));
Assert.That(response.StatusCode, Is.EqualTo(successResponse.StatusCode));
}

#endregion

public class TestException : Exception
Expand Down
2 changes: 1 addition & 1 deletion source/WcfClientProxyGenerator/Async/AsyncProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ private TResponse InvokeCallAsyncDelegate<TResponse>(Expression expression)
throw new NotSupportedException(
string.Format("OperationContract method '{0}' has parameters '{1}' marked as out or ref. These are not currently supported in async calls.",
methodCall.Method.Name,
methodParameters.Where(m => m.ParameterType.IsByRef).Select(m => m.Name)));
string.Join(", ", methodParameters.Where(m => m.ParameterType.IsByRef).Select(m => m.Name))));
}

var cachedDelegate = this.GetCallAsyncDelegate(methodCall, methodParameters);
Expand Down
2 changes: 1 addition & 1 deletion source/WcfClientProxyGenerator/DefaultProxyConfigurator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ public static readonly Func<LinearBackoffDelayPolicy> DefaultDelayPolicyFactory
= () => new LinearBackoffDelayPolicy(TimeSpan.FromMilliseconds(500), TimeSpan.FromSeconds(10));

public static readonly RetryFailureExceptionFactoryDelegate DefaultRetryFailureExceptionFactory
= (retryCount, lastException, invokInfo) => new WcfRetryFailedException(string.Format("WCF call failed after {0} retries.", retryCount), lastException);
= (retryCount, lastException, invokeInfo) => new WcfRetryFailedException(string.Format("WCF call failed after {0} retries.", retryCount), lastException);
}
}
94 changes: 62 additions & 32 deletions source/WcfClientProxyGenerator/RetryingWcfActionInvoker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@

namespace WcfClientProxyGenerator
{
class ResponseHandlerHolder
{
public object Predicate { get; set; }
public object ResponseHandler { get; set; }
}

internal class RetryingWcfActionInvoker<TServiceInterface> : IActionInvoker<TServiceInterface>
where TServiceInterface : class
{
Expand All @@ -32,8 +38,8 @@ private static ConcurrentDictionary<Type, Lazy<MethodInfo>> ResponseHandlerCache

private readonly Type _originalServiceInterfaceType;

private readonly IDictionary<Type, object> _retryPredicates;
private readonly IDictionary<Type, ResponseHandlerHolder> _responseHandlers;
private readonly IDictionary<Type, IList<object>> _retryPredicates;
private readonly IDictionary<Type, IList<ResponseHandlerHolder>> _responseHandlers;

/// <summary>
/// The method that initializes new WCF action providers
Expand All @@ -50,14 +56,14 @@ public RetryingWcfActionInvoker(
RetryFailureExceptionFactory = DefaultProxyConfigurator.DefaultRetryFailureExceptionFactory;

_wcfActionProviderCreator = wcfActionProviderCreator;
_retryPredicates = new Dictionary<Type, object>
_retryPredicates = new Dictionary<Type, IList<object>>
{
{ typeof(ChannelTerminatedException), null },
{ typeof(EndpointNotFoundException), null },
{ typeof(ServerTooBusyException), null }
{ typeof(ChannelTerminatedException), new List<object> { null } },
{ typeof(EndpointNotFoundException), new List<object> { null } },
{ typeof(ServerTooBusyException), new List<object> { null } }
};

_responseHandlers = new Dictionary<Type, ResponseHandlerHolder>();
_responseHandlers = new Dictionary<Type, IList<ResponseHandlerHolder>>();

_originalServiceInterfaceType = GetOriginalServiceInterface();
}
Expand Down Expand Up @@ -107,7 +113,10 @@ public void AddExceptionToRetryOn<TException>(Predicate<TException> where = null
where = _ => true;
}

_retryPredicates.Add(typeof(TException), where);
if (!_retryPredicates.ContainsKey(typeof(TException)))
_retryPredicates.Add(typeof(TException), new List<object>());

_retryPredicates[typeof(TException)].Add(where);
}

public void AddExceptionToRetryOn(Type exceptionType, Predicate<Exception> where = null)
Expand All @@ -117,23 +126,30 @@ public void AddExceptionToRetryOn(Type exceptionType, Predicate<Exception> where
where = _ => true;
}

_retryPredicates.Add(exceptionType, where);
if (!_retryPredicates.ContainsKey(exceptionType))
_retryPredicates.Add(exceptionType, new List<object>());

_retryPredicates[exceptionType].Add(where);
}

public void AddResponseToRetryOn<TResponse>(Predicate<TResponse> where)
{
_retryPredicates.Add(typeof(TResponse), where);
}
if (!_retryPredicates.ContainsKey(typeof(TResponse)))
_retryPredicates.Add(typeof(TResponse), new List<object>());

class ResponseHandlerHolder
{
public object Predicate { get; set; }
public object ResponseHandler { get; set; }
_retryPredicates[typeof(TResponse)].Add(where);
}

public void AddResponseHandler<TResponse>(Func<TResponse, TResponse> handler, Predicate<TResponse> @where)
{
_responseHandlers.Add(typeof(TResponse), new ResponseHandlerHolder { Predicate = @where, ResponseHandler = handler });
if (!_responseHandlers.ContainsKey(typeof(TResponse)))
_responseHandlers.Add(typeof(TResponse), new List<ResponseHandlerHolder>());

_responseHandlers[typeof(TResponse)].Add(new ResponseHandlerHolder
{
Predicate = @where,
ResponseHandler = handler
});
}

/// <summary>
Expand Down Expand Up @@ -434,18 +450,20 @@ private TResponse ExecuteResponseHandlers<TResponse>(TResponse response, Type ty
if (!this._responseHandlers.ContainsKey(@type))
return response;

var responseHandlerHolder = this._responseHandlers[@type];
IList<ResponseHandlerHolder> responseHandlerHolders = this._responseHandlers[@type];

MethodInfo predicateInvokeMethod = ResponseHandlerPredicateCache.GetOrAddSafe(@type, _ =>
{
Type predicateType = typeof(Predicate<>).MakeGenericType(@type);
return predicateType.GetMethod("Invoke", BindingFlags.Instance | BindingFlags.Public);
});

bool responseIsHandleable = responseHandlerHolder.Predicate == null
|| (bool) predicateInvokeMethod.Invoke(responseHandlerHolder.Predicate, new object[] { response });

if (!responseIsHandleable)
var handlers = responseHandlerHolders
.Where(m => m.Predicate == null
|| ((bool) predicateInvokeMethod.Invoke(m.Predicate, new object[] { response })))
.ToList();

if (!handlers.Any())
return response;

MethodInfo handlerMethod = ResponseHandlerCache.GetOrAddSafe(@type, _ =>
Expand All @@ -454,14 +472,19 @@ private TResponse ExecuteResponseHandlers<TResponse>(TResponse response, Type ty
return actionType.GetMethod("Invoke", BindingFlags.Instance | BindingFlags.Public);
});

try
foreach (var handler in handlers)
{
return (TResponse) handlerMethod.Invoke(responseHandlerHolder.ResponseHandler, new object[] { response });
}
catch (TargetInvocationException ex)
{
throw ex.InnerException;
try
{
response = (TResponse) handlerMethod.Invoke(handler.ResponseHandler, new object[] { response });
}
catch (TargetInvocationException ex)
{
throw ex.InnerException;
}
}

return response;
}

private bool ResponseInRetryable<TResponse>(TResponse response)
Expand All @@ -480,18 +503,25 @@ private bool EvaluatePredicate<TInstance>(Type key, TInstance instance)
if (!_retryPredicates.ContainsKey(key))
return false;

object predicate = _retryPredicates[key];

if (predicate == null)
return true;
var predicates = _retryPredicates[key];

MethodInfo invokeMethod = PredicateCache.GetOrAddSafe(key, _ =>
{
Type predicateType = typeof(Predicate<>).MakeGenericType(key);
return predicateType.GetMethod("Invoke", BindingFlags.Instance | BindingFlags.Public);
});

return (bool) invokeMethod.Invoke(predicate, new object[] { instance });
// See if any non-null predicate matched the instance
bool isSuccess = predicates
.Where(p => p != null)
.Any(predicate => (bool) invokeMethod.Invoke(predicate, new object[] { instance }));

// If there's a null predicate (always match), then return true
if (!isSuccess && predicates.Any(m => m == null))
return true;

// Return result of running instance through predicates
return isSuccess;
}

/// <summary>
Expand Down
Loading

0 comments on commit aeb9938

Please sign in to comment.