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 23, 2014
2 parents aeb9938 + 3a95866 commit b35bc4f
Show file tree
Hide file tree
Showing 9 changed files with 495 additions and 60 deletions.
4 changes: 2 additions & 2 deletions .semver
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
:major: 2
:minor: 0
:patch: 9
:minor: 1
:patch: 0
:special: ''
70 changes: 64 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,18 +83,34 @@ Exposes the full configuration available. See the [Configuration](#configuration

Async Support
-------------
WCF service contract interfaces that define task based async methods will automatically work with the .NET 4.5 async/await support.
At runtime, generating the proxy will ensure that all non-async operation contract methods have an associated Async implementation. What this means is that if your service contract is defined as:

var proxy = WcfClientProxy.Create<IService>();
int result = await proxy.MakeCallAsync("test");
[ServiceContract]
interface IService
{
[OperationContract]
int MakeCall(string input);
}

Service contracts that don't define task based methods can be used in an async/await fashion by calling the `WcfClientProxy.CreateAsyncProxy<TServiceInterface>()` method. This call returns a type `IAsyncProxy<TServiceInterface>` that exposes a `CallAsync()` method.
then the proxy returned by calling `WcfClientProxy.Create<IService>()` will automatically define a `Task<int> MakeCallAsync(string input)` operation contract method at runtime.

To utilize this runtime generated async method, an async friendly wrapped proxy can be generated by calling the `WcfClientProxy.CreateAsyncProxy<TServiceInterface>()` method. This call returns a type `IAsyncProxy<TServiceInterface>` that exposes a `CallAsync()` method.

For example, a service contract interface with method `int MakeCall(string input)` can be called asynchronously like:
The `int MakeCall(string input)` method can now be called asynchronously like:

var proxy = WcfClientProxy.CreateAsyncProxy<IService>();
int result = await proxy.CallAsync(s => s.MakeCall("test"));

It is also possible to call into the runtime generated Async methods dynamically without use of the `IAsyncProxy<TServiceInterface>` wrapper. For example, the same service contract interface with the non-async method defined can be called asynchronously as so:

var proxy = WcfClientProxy.Create<IService>();
int result = await ((dynamic) proxy).MakeCallAsync("test");

WCF service contract interfaces that define task based async methods at compile will automatically work with the C# 5 async/await support.

var proxy = WcfClientProxy.Create<IServiceAsync>();
int result = await proxy.MakeCallAsync("test");

### Async Limitations
Methods that define `out` or `ref` parameters are not supported when making async/await calls. Attempts to make async calls using a proxy with these parameter types will result in a runtime exception being thrown.

Expand Down Expand Up @@ -128,12 +144,54 @@ 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`

#### HandleRequestArgument\<TArgument\>(Func\<TArgument, string, bool\> where, Action\<TArgument\> handler)
_overload:_ `HandleRequestArgument<TArgument>(Func<TArgument, string, bool> where, Func<TArgument, TArgument> handler)`

Sets up the proxy to run handlers on argument values that are used for making WCF requests. An example use case would be to inject authentication keys into all requests where an argument value matches expectation.

The `TArgument` value can be a type as specific or general as needed. For example, configuring the proxy to handle the `object` type will result in the handler being run for all operation contract arguments, whereas configuring with a sealed type will result in only those types being handled.

This example sets the `AccessKey` property for all requests inheriting from `IAuthenticatedRequest`:

var proxy = WcfClientProxy.Create<IService>(c =>
{
c.HandleRequestArgument<IAuthenticatedRequest>(req =>
{
req.AccessKey = "...";
});
});

The `where` condition takes the request object as its first parameter and the actual name of the operation contract parameter secondly. This allows for conditional handling of non-specific types based on naming conventions.

For example, a service contract defined as:

[ServiceContract]
public interface IAuthenticatedService
{
[OperationContract]
void OperationOne(string accessKey, string input);
[OperationContract]
void OperationTwo(string accessKey, string anotherInput);
}

can have the `accessKey` parameter automatically filled in with the following proxy configuration:

var proxy = WcfClientProxy.Create<IService>(c =>
{
c.HandleRequestArgument<string>(
where: (req, paramName) => paramName == "accessKey",
handler: req => "access key value");
});

the proxy can now be called with with any value in the `accessKey` parameter (e.g. `proxy.OperationOne(null, "input value")` and before the request is actually started, the `accessKey` value will be swapped out with `access key value`.

#### 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.
Similar to `HandleRequestArgument`, 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 Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ public interface ITestService
[OperationContract(Name = "TestMethod2")]
string TestMethod(string input, string two);

[OperationContract]
int TestMethodMixed(string input, int input2);

[OperationContract]
void VoidMethod(string input);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ public string TestMethod(string input, string two)
return string.Format("Echo: {0}, {1}", input, two);
}

public int TestMethodMixed(string input, int input2)
{
if (_mock != null)
return _mock.Object.TestMethodMixed(input, input2);

return input2;
}

public void VoidMethod(string input)
{
if (_mock != null)
Expand Down
151 changes: 151 additions & 0 deletions source/WcfClientProxyGenerator.Tests/ProxyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1243,6 +1243,112 @@ public void Proxy_ChannelFactory_UsesConfiguredEndpoint()

#endregion

#region HandleRequestArgument

[Test]
public void HandleRequestArgument_ModifiesComplexRequest_BeforeSendingToService()
{
var mockService = new Mock<ITestService>();
mockService
.Setup(m => m.TestMethodComplex(It.IsAny<Request>()))
.Returns((Request r) => new Response
{
ResponseMessage = r.RequestMessage
});

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

var proxy = WcfClientProxy.Create<ITestService>(c =>
{
c.SetEndpoint(serviceHost.Binding, serviceHost.EndpointAddress);
c.HandleRequestArgument<Request>(
where: (arg, param) => arg == null,
handler: arg =>
{
return new Request
{
RequestMessage = "default message"
};
});
});

proxy.TestMethodComplex(null);
mockService
.Verify(m => m.TestMethodComplex(It.Is<Request>(r => r.RequestMessage == "default message")), Times.Once());

proxy.TestMethodComplex(new Request { RequestMessage = "set" });
mockService
.Verify(m => m.TestMethodComplex(It.Is<Request>(r => r.RequestMessage == "set")), Times.Once());
}

[Test]
public void HandleRequestArgument_MatchesArgumentsOfSameType_BasedOnParameterName()
{
var mockService = new Mock<ITestService>();
mockService
.Setup(m => m.TestMethod(It.IsAny<string>() /* input */, It.IsAny<string>() /* two */))
.Returns("response");

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

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

c.HandleRequestArgument<string>(
where: (arg, paramName) => paramName == "input",
handler: arg =>
{
return "always input";
});

c.HandleRequestArgument<string>(
where: (arg, paramName) => paramName == "two",
handler: arg =>
{
return "always two";
});
});

proxy.TestMethod("first argument", "second argument");

mockService
.Verify(m => m.TestMethod("always input", "always two"), Times.Once());
}

[Test]
public void HandleRequestArgument_MatchesArgumentsByBaseTypes()
{
int handleRequestArgumentCounter = 0;

var mockService = new Mock<ITestService>();
mockService
.Setup(m => m.TestMethodMixed(It.IsAny<string>(), It.IsAny<int>()))
.Returns(10);

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

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

c.HandleRequestArgument<object>(
handler: arg =>
{
handleRequestArgumentCounter++;
});
});

proxy.TestMethodMixed("first argument", 100);

mockService
.Verify(m => m.TestMethodMixed("first argument", 100), Times.Once());

Assert.That(handleRequestArgumentCounter, Is.EqualTo(2));
}

#endregion

#region HandleResponse

[Test]
Expand Down Expand Up @@ -1717,6 +1823,51 @@ public async Task Async_HandleResponse_ActionWithoutPredicate_CanInspectResponse

#endregion

#region Dynamic Async Invocation

[Test]
public async Task Async_DynamicConversion_Proxy_ReturnsExpectedValue_WhenCallingGeneratedAsyncMethod()
{
var mockService = new Mock<ITestService>();
mockService.Setup(m => m.TestMethod("good")).Returns("OK");

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

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

// ITestService does not define TestMethodAsync, it's generated at runtime
var result = await ((dynamic) proxy).TestMethodAsync("good");

Assert.AreEqual("OK", result);
}

[Test]
public async Task Async_DynamicConversion_Proxy_CanCallGeneratedAsyncVoidMethod()
{
var resetEvent = new AutoResetEvent(false);

var mockService = new Mock<ITestService>();
mockService
.Setup(m => m.VoidMethod("good"))
.Callback<string>(input =>
{
Assert.That(input, Is.EqualTo("good"));
resetEvent.Set();
});

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

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

// ITestService does not define VoidMethodAsync, it's generated at runtime
await ((dynamic) proxy).VoidMethodAsync("good");

if (!resetEvent.WaitOne(300))
Assert.Fail("Timeout occurred when waiting for callback");
}

#endregion

#region Better error messages tests

[ServiceContract]
Expand Down
38 changes: 32 additions & 6 deletions source/WcfClientProxyGenerator/DynamicProxyTypeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,11 @@ private static void GenerateAsyncTaskMethod(
MethodInfo methodInfo,
TypeBuilder typeBuilder)
{
var parameterTypes = methodInfo.GetParameters()
var parameters = methodInfo
.GetParameters()
.ToArray();

var parameterTypes = parameters
.Select(m => m.ParameterType)
.ToArray();

Expand All @@ -266,8 +270,8 @@ private static void GenerateAsyncTaskMethod(
parameterTypes);

for (int i = 1; i <= parameterTypes.Length; i++)
methodBuilder.DefineParameter(i, ParameterAttributes.None, "arg" + i);

methodBuilder.DefineParameter(i, ParameterAttributes.None, parameters[i-1].Name);
var originalOperationContract = methodInfo.GetCustomAttribute<OperationContractAttribute>();

var attributeCtor = typeof(OperationContractAttribute)
Expand Down Expand Up @@ -324,7 +328,11 @@ private static void GenerateServiceProxyMethod(
MethodInfo methodInfo,
TypeBuilder typeBuilder)
{
var parameterTypes = methodInfo.GetParameters()
var parameters = methodInfo
.GetParameters()
.ToArray();

var parameterTypes = parameters
.Select(m => m.ParameterType)
.ToArray();

Expand All @@ -335,8 +343,8 @@ private static void GenerateServiceProxyMethod(
methodInfo.ReturnType,
parameterTypes);

for (int i = 1; i <= parameterTypes.Length; i++)
methodBuilder.DefineParameter(i, ParameterAttributes.None, "arg" + i);
for (int i = 1; i <= parameters.Length; i++)
methodBuilder.DefineParameter(i, ParameterAttributes.None, parameters[i-1].Name);

Type serviceCallWrapperType;
var serviceCallWrapperFields = GenerateServiceCallWrapperType(
Expand Down Expand Up @@ -380,6 +388,24 @@ private static void GenerateServiceProxyMethod(
if (serviceCallWrapperCtor == null)
throw new Exception("Parameterless constructor not found for type: " + serviceCallWrapperType);

for (int i = 0; i < parameterTypes.Length; i++)
{
Type parameterType = parameterTypes[i];
if (parameterType.IsByRef)
continue;

var handleRequestParameterMethod = typeof(RetryingWcfActionInvokerProvider<>)
.MakeGenericType(asyncInterfaceType)
.GetMethod("HandleRequestArgument", BindingFlags.Instance | BindingFlags.NonPublic)
.MakeGenericMethod(parameterType);

ilGenerator.Emit(OpCodes.Ldarg, 0);
ilGenerator.Emit(OpCodes.Ldarg, i + 1);
ilGenerator.Emit(OpCodes.Ldstr, parameters[i].Name);
ilGenerator.Emit(OpCodes.Call, handleRequestParameterMethod);
ilGenerator.Emit(OpCodes.Starg_S, i + 1);
}

// local2 = new MethodType();
ilGenerator.Emit(OpCodes.Newobj, serviceCallWrapperCtor);
ilGenerator.Emit(OpCodes.Stloc_2);
Expand Down
Loading

0 comments on commit b35bc4f

Please sign in to comment.