Skip to content

Commit b35bc4f

Browse files
committed
Merge branch 'develop'
2 parents aeb9938 + 3a95866 commit b35bc4f

File tree

9 files changed

+495
-60
lines changed

9 files changed

+495
-60
lines changed

.semver

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
22
:major: 2
3-
:minor: 0
4-
:patch: 9
3+
:minor: 1
4+
:patch: 0
55
:special: ''

README.md

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,18 +83,34 @@ Exposes the full configuration available. See the [Configuration](#configuration
8383

8484
Async Support
8585
-------------
86-
WCF service contract interfaces that define task based async methods will automatically work with the .NET 4.5 async/await support.
86+
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:
8787

88-
var proxy = WcfClientProxy.Create<IService>();
89-
int result = await proxy.MakeCallAsync("test");
88+
[ServiceContract]
89+
interface IService
90+
{
91+
[OperationContract]
92+
int MakeCall(string input);
93+
}
9094

91-
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.
95+
then the proxy returned by calling `WcfClientProxy.Create<IService>()` will automatically define a `Task<int> MakeCallAsync(string input)` operation contract method at runtime.
96+
97+
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.
9298

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

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

104+
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:
105+
106+
var proxy = WcfClientProxy.Create<IService>();
107+
int result = await ((dynamic) proxy).MakeCallAsync("test");
108+
109+
WCF service contract interfaces that define task based async methods at compile will automatically work with the C# 5 async/await support.
110+
111+
var proxy = WcfClientProxy.Create<IServiceAsync>();
112+
int result = await proxy.MakeCallAsync("test");
113+
98114
### Async Limitations
99115
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.
100116

@@ -128,12 +144,54 @@ will configure the proxy based on the `<endpoint/>` as setup in the _app.config_
128144
#### SetEndpoint(Binding binding, EndpointAddress endpointAddress)
129145
Configures the proxy to communicate with the endpoint using the given `binding` at the `endpointAddress`
130146

147+
#### HandleRequestArgument\<TArgument\>(Func\<TArgument, string, bool\> where, Action\<TArgument\> handler)
148+
_overload:_ `HandleRequestArgument<TArgument>(Func<TArgument, string, bool> where, Func<TArgument, TArgument> handler)`
149+
150+
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.
151+
152+
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.
153+
154+
This example sets the `AccessKey` property for all requests inheriting from `IAuthenticatedRequest`:
155+
156+
var proxy = WcfClientProxy.Create<IService>(c =>
157+
{
158+
c.HandleRequestArgument<IAuthenticatedRequest>(req =>
159+
{
160+
req.AccessKey = "...";
161+
});
162+
});
163+
164+
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.
165+
166+
For example, a service contract defined as:
167+
168+
[ServiceContract]
169+
public interface IAuthenticatedService
170+
{
171+
[OperationContract]
172+
void OperationOne(string accessKey, string input);
173+
174+
[OperationContract]
175+
void OperationTwo(string accessKey, string anotherInput);
176+
}
177+
178+
can have the `accessKey` parameter automatically filled in with the following proxy configuration:
179+
180+
var proxy = WcfClientProxy.Create<IService>(c =>
181+
{
182+
c.HandleRequestArgument<string>(
183+
where: (req, paramName) => paramName == "accessKey",
184+
handler: req => "access key value");
185+
});
186+
187+
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`.
188+
131189
#### HandleResponse\<TResponse\>(Predicate\<TResponse\> where, Action\<TResponse\> handler)
132190
_overload:_ `HandleResponse<TResponse>(Predicate<TResponse> where, Func<TResponse, TResponse> handler)`
133191

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

136-
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.
194+
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.
137195

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

source/WcfClientProxyGenerator.Tests/Infrastructure/ITestService.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ public interface ITestService
1212
[OperationContract(Name = "TestMethod2")]
1313
string TestMethod(string input, string two);
1414

15+
[OperationContract]
16+
int TestMethodMixed(string input, int input2);
17+
1518
[OperationContract]
1619
void VoidMethod(string input);
1720

source/WcfClientProxyGenerator.Tests/Infrastructure/TestServiceImpl.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ public string TestMethod(string input, string two)
3737
return string.Format("Echo: {0}, {1}", input, two);
3838
}
3939

40+
public int TestMethodMixed(string input, int input2)
41+
{
42+
if (_mock != null)
43+
return _mock.Object.TestMethodMixed(input, input2);
44+
45+
return input2;
46+
}
47+
4048
public void VoidMethod(string input)
4149
{
4250
if (_mock != null)

source/WcfClientProxyGenerator.Tests/ProxyTests.cs

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1243,6 +1243,112 @@ public void Proxy_ChannelFactory_UsesConfiguredEndpoint()
12431243

12441244
#endregion
12451245

1246+
#region HandleRequestArgument
1247+
1248+
[Test]
1249+
public void HandleRequestArgument_ModifiesComplexRequest_BeforeSendingToService()
1250+
{
1251+
var mockService = new Mock<ITestService>();
1252+
mockService
1253+
.Setup(m => m.TestMethodComplex(It.IsAny<Request>()))
1254+
.Returns((Request r) => new Response
1255+
{
1256+
ResponseMessage = r.RequestMessage
1257+
});
1258+
1259+
var serviceHost = InProcTestFactory.CreateHost<ITestService>(new TestServiceImpl(mockService));
1260+
1261+
var proxy = WcfClientProxy.Create<ITestService>(c =>
1262+
{
1263+
c.SetEndpoint(serviceHost.Binding, serviceHost.EndpointAddress);
1264+
c.HandleRequestArgument<Request>(
1265+
where: (arg, param) => arg == null,
1266+
handler: arg =>
1267+
{
1268+
return new Request
1269+
{
1270+
RequestMessage = "default message"
1271+
};
1272+
});
1273+
});
1274+
1275+
proxy.TestMethodComplex(null);
1276+
mockService
1277+
.Verify(m => m.TestMethodComplex(It.Is<Request>(r => r.RequestMessage == "default message")), Times.Once());
1278+
1279+
proxy.TestMethodComplex(new Request { RequestMessage = "set" });
1280+
mockService
1281+
.Verify(m => m.TestMethodComplex(It.Is<Request>(r => r.RequestMessage == "set")), Times.Once());
1282+
}
1283+
1284+
[Test]
1285+
public void HandleRequestArgument_MatchesArgumentsOfSameType_BasedOnParameterName()
1286+
{
1287+
var mockService = new Mock<ITestService>();
1288+
mockService
1289+
.Setup(m => m.TestMethod(It.IsAny<string>() /* input */, It.IsAny<string>() /* two */))
1290+
.Returns("response");
1291+
1292+
var serviceHost = InProcTestFactory.CreateHost<ITestService>(new TestServiceImpl(mockService));
1293+
1294+
var proxy = WcfClientProxy.Create<ITestService>(c =>
1295+
{
1296+
c.SetEndpoint(serviceHost.Binding, serviceHost.EndpointAddress);
1297+
1298+
c.HandleRequestArgument<string>(
1299+
where: (arg, paramName) => paramName == "input",
1300+
handler: arg =>
1301+
{
1302+
return "always input";
1303+
});
1304+
1305+
c.HandleRequestArgument<string>(
1306+
where: (arg, paramName) => paramName == "two",
1307+
handler: arg =>
1308+
{
1309+
return "always two";
1310+
});
1311+
});
1312+
1313+
proxy.TestMethod("first argument", "second argument");
1314+
1315+
mockService
1316+
.Verify(m => m.TestMethod("always input", "always two"), Times.Once());
1317+
}
1318+
1319+
[Test]
1320+
public void HandleRequestArgument_MatchesArgumentsByBaseTypes()
1321+
{
1322+
int handleRequestArgumentCounter = 0;
1323+
1324+
var mockService = new Mock<ITestService>();
1325+
mockService
1326+
.Setup(m => m.TestMethodMixed(It.IsAny<string>(), It.IsAny<int>()))
1327+
.Returns(10);
1328+
1329+
var serviceHost = InProcTestFactory.CreateHost<ITestService>(new TestServiceImpl(mockService));
1330+
1331+
var proxy = WcfClientProxy.Create<ITestService>(c =>
1332+
{
1333+
c.SetEndpoint(serviceHost.Binding, serviceHost.EndpointAddress);
1334+
1335+
c.HandleRequestArgument<object>(
1336+
handler: arg =>
1337+
{
1338+
handleRequestArgumentCounter++;
1339+
});
1340+
});
1341+
1342+
proxy.TestMethodMixed("first argument", 100);
1343+
1344+
mockService
1345+
.Verify(m => m.TestMethodMixed("first argument", 100), Times.Once());
1346+
1347+
Assert.That(handleRequestArgumentCounter, Is.EqualTo(2));
1348+
}
1349+
1350+
#endregion
1351+
12461352
#region HandleResponse
12471353

12481354
[Test]
@@ -1717,6 +1823,51 @@ public async Task Async_HandleResponse_ActionWithoutPredicate_CanInspectResponse
17171823

17181824
#endregion
17191825

1826+
#region Dynamic Async Invocation
1827+
1828+
[Test]
1829+
public async Task Async_DynamicConversion_Proxy_ReturnsExpectedValue_WhenCallingGeneratedAsyncMethod()
1830+
{
1831+
var mockService = new Mock<ITestService>();
1832+
mockService.Setup(m => m.TestMethod("good")).Returns("OK");
1833+
1834+
var serviceHost = InProcTestFactory.CreateHost<ITestService>(new TestServiceImpl(mockService));
1835+
1836+
var proxy = WcfClientProxy.Create<ITestService>(c => c.SetEndpoint(serviceHost.Binding, serviceHost.EndpointAddress));
1837+
1838+
// ITestService does not define TestMethodAsync, it's generated at runtime
1839+
var result = await ((dynamic) proxy).TestMethodAsync("good");
1840+
1841+
Assert.AreEqual("OK", result);
1842+
}
1843+
1844+
[Test]
1845+
public async Task Async_DynamicConversion_Proxy_CanCallGeneratedAsyncVoidMethod()
1846+
{
1847+
var resetEvent = new AutoResetEvent(false);
1848+
1849+
var mockService = new Mock<ITestService>();
1850+
mockService
1851+
.Setup(m => m.VoidMethod("good"))
1852+
.Callback<string>(input =>
1853+
{
1854+
Assert.That(input, Is.EqualTo("good"));
1855+
resetEvent.Set();
1856+
});
1857+
1858+
var serviceHost = InProcTestFactory.CreateHost<ITestService>(new TestServiceImpl(mockService));
1859+
1860+
var proxy = WcfClientProxy.Create<ITestService>(c => c.SetEndpoint(serviceHost.Binding, serviceHost.EndpointAddress));
1861+
1862+
// ITestService does not define VoidMethodAsync, it's generated at runtime
1863+
await ((dynamic) proxy).VoidMethodAsync("good");
1864+
1865+
if (!resetEvent.WaitOne(300))
1866+
Assert.Fail("Timeout occurred when waiting for callback");
1867+
}
1868+
1869+
#endregion
1870+
17201871
#region Better error messages tests
17211872

17221873
[ServiceContract]

source/WcfClientProxyGenerator/DynamicProxyTypeGenerator.cs

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,11 @@ private static void GenerateAsyncTaskMethod(
241241
MethodInfo methodInfo,
242242
TypeBuilder typeBuilder)
243243
{
244-
var parameterTypes = methodInfo.GetParameters()
244+
var parameters = methodInfo
245+
.GetParameters()
246+
.ToArray();
247+
248+
var parameterTypes = parameters
245249
.Select(m => m.ParameterType)
246250
.ToArray();
247251

@@ -266,8 +270,8 @@ private static void GenerateAsyncTaskMethod(
266270
parameterTypes);
267271

268272
for (int i = 1; i <= parameterTypes.Length; i++)
269-
methodBuilder.DefineParameter(i, ParameterAttributes.None, "arg" + i);
270-
273+
methodBuilder.DefineParameter(i, ParameterAttributes.None, parameters[i-1].Name);
274+
271275
var originalOperationContract = methodInfo.GetCustomAttribute<OperationContractAttribute>();
272276

273277
var attributeCtor = typeof(OperationContractAttribute)
@@ -324,7 +328,11 @@ private static void GenerateServiceProxyMethod(
324328
MethodInfo methodInfo,
325329
TypeBuilder typeBuilder)
326330
{
327-
var parameterTypes = methodInfo.GetParameters()
331+
var parameters = methodInfo
332+
.GetParameters()
333+
.ToArray();
334+
335+
var parameterTypes = parameters
328336
.Select(m => m.ParameterType)
329337
.ToArray();
330338

@@ -335,8 +343,8 @@ private static void GenerateServiceProxyMethod(
335343
methodInfo.ReturnType,
336344
parameterTypes);
337345

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

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

391+
for (int i = 0; i < parameterTypes.Length; i++)
392+
{
393+
Type parameterType = parameterTypes[i];
394+
if (parameterType.IsByRef)
395+
continue;
396+
397+
var handleRequestParameterMethod = typeof(RetryingWcfActionInvokerProvider<>)
398+
.MakeGenericType(asyncInterfaceType)
399+
.GetMethod("HandleRequestArgument", BindingFlags.Instance | BindingFlags.NonPublic)
400+
.MakeGenericMethod(parameterType);
401+
402+
ilGenerator.Emit(OpCodes.Ldarg, 0);
403+
ilGenerator.Emit(OpCodes.Ldarg, i + 1);
404+
ilGenerator.Emit(OpCodes.Ldstr, parameters[i].Name);
405+
ilGenerator.Emit(OpCodes.Call, handleRequestParameterMethod);
406+
ilGenerator.Emit(OpCodes.Starg_S, i + 1);
407+
}
408+
383409
// local2 = new MethodType();
384410
ilGenerator.Emit(OpCodes.Newobj, serviceCallWrapperCtor);
385411
ilGenerator.Emit(OpCodes.Stloc_2);

0 commit comments

Comments
 (0)