I was asked yesterday in the Hebrew C#/.NET Framework MSDN forums a tough question – is it possible to dynamically call a WCF service using only the contract name, operation name, and metadata address?
At first, I agreed with the answer given in the forum – move from SOAP bindings to WebHttpBinding
(“REST”). This of course makes things a lot easier, only requiring you to create a WebHttpRequest
and parse the response. However the question remains - is it possible to do this in the case of a SOAP-based service endpoint?
The short answer is – YES!
The full answer is – YES, but you’ll need to do a lot of coding to make it work properly, and even more coding for complex scenarios (who said passing a data contract?)
How is it done, you ask?
First let’s start with the contract – you have a simple contract that looks like so:
1: [ServiceContract]
2: public interface ICalculator
3: {
4: [OperationContract]
5: double Add(double n1, double n2);
6: [OperationContract]
7: double Subtract(double n1, double n2);
8: [OperationContract]
9: double Multiply(double n1, double n2);
10: [OperationContract]
11: double Divide(double n1, double n2);
12: }
At this point, the implementation doesn’t matter, but you can assume the service compiles and loads successfully.
Second, make sure your service has either a MEX endpoint or metadata exposed over HTTP GET
. Read this for more information about the difference between the two.
Third – do the client coding!!! To create the client code, I took some ideas from the following links:
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.ServiceModel;
5: using System.ServiceModel.Description;
6: using System.Globalization;
7: using System.Collections.ObjectModel;
8: using System.CodeDom.Compiler;
9:
10: namespace Client
11: {
12: class Program
13: {
14: static void Main(string[] args)
15: {
16:
17:
18: Uri mexAddress =
new Uri("http://localhost:8732/CalculatorService/?wsdl");
19:
20: MetadataExchangeClientMode mexMode = MetadataExchangeClientMode.HttpGet;
21: string contractName = "ICalculator";
22: string operationName = "Add";
23: object[] operationParameters = new object[] { 1, 2 };
24:
25:
26: MetadataExchangeClient mexClient =
new MetadataExchangeClient(mexAddress, mexMode);
27: mexClient.ResolveMetadataReferences = true;
28: MetadataSet metaSet = mexClient.GetMetadata();
29:
30:
31: WsdlImporter importer = new WsdlImporter(metaSet);
32: Collection<ContractDescription> contracts =
importer.ImportAllContracts();
33: ServiceEndpointCollection allEndpoints = importer.ImportAllEndpoints();
34:
35:
36: ServiceContractGenerator generator = new ServiceContractGenerator();
37: var endpointsForContracts =
new Dictionary<string, IEnumerable<ServiceEndpoint>>();
38:
39: foreach (ContractDescription contract in contracts)
40: {
41: generator.GenerateServiceContractType(contract);
42:
43: endpointsForContracts[contract.Name] = allEndpoints.Where(
44: se => se.Contract.Name == contract.Name).ToList();
45: }
46:
47: if (generator.Errors.Count != 0)
48: throw new Exception("There were errors during code compilation.");
49:
50:
51: CodeGeneratorOptions options = new CodeGeneratorOptions();
52: options.BracingStyle = "C";
53: CodeDomProvider codeDomProvider = CodeDomProvider.CreateProvider("C#");
54:
55:
56:
57: CompilerParameters compilerParameters = new CompilerParameters(
58: new string[] {
59: "System.dll", "System.ServiceModel.dll",
60: "System.Runtime.Serialization.dll" });
61: compilerParameters.GenerateInMemory = true;
62:
63: CompilerResults results = codeDomProvider.CompileAssemblyFromDom(
64: compilerParameters, generator.TargetCompileUnit);
65:
66: if (results.Errors.Count > 0)
67: {
68: throw new Exception("There were errors during
generated code compilation");
69: }
70: else
71: {
72:
73:
74: Type clientProxyType = results.CompiledAssembly.GetTypes().First(
75: t => t.IsClass &&
76: t.GetInterface(contractName) != null &&
77: t.GetInterface(typeof(ICommunicationObject).Name) != null);
78:
79:
80: ServiceEndpoint se = endpointsForContracts[contractName].First();
81:
82:
83:
84:
85: object instance = results.CompiledAssembly.CreateInstance(
86: clientProxyType.Name,
87: false,
88: System.Reflection.BindingFlags.CreateInstance,
89: null,
90: new object[] { se.Binding, se.Address },
91: CultureInfo.CurrentCulture, null);
92:
93:
94: object retVal = instance.GetType().GetMethod(operationName).
95: Invoke(instance, operationParameters);
96:
97: Console.WriteLine(retVal.ToString());
98: }
99: }
100: }
101: }
I’ve placed comments that describe the code, but basically it imports the WSDL, generates types for the contract (service + data), generates C# code from it, compiles it, and uses reflection to create a proxy and invoke the correct method.
If you want to use this technique to call methods that require a data contract, you will need some extra work to create the correct type and initialize it.
A compiled and running version of this code (+ the service) can be found here.
Hope you find this piece of code useful.