要如何給 WebService 的 URL ,然後可以動態的呼叫它呢?
如果參數是 Complex Type 呢?
問題
客戶有個系統,會動態的產生 WebService ,同時參數是物件,而非是一般的字串。
要如何給 WebService 的 URL ,然後可以動態的呼叫它呢?
研究
一般來說,要使用別人的 WebService ,會透過 VS.NET 來加入 Web 參考。
VS.NET 會幫我們建立 client 端的 proxy 物件,讓我們很輕易的呼叫它,而不用自己去組出 SOAP 內容,再送給 WebService 。
詳細可以參考 分享使用Web Service介接的方式 這篇文章。
那要如何動態的建立 client 端的 proxy 物件並呼叫 WebMethod 呢 ?
1.經由 ServiceDescription 透過 WebService 的 URL 取得 WSDL
System.Net.WebClient client = new System.Net.WebClient(); //Connect To the web service System.IO.Stream stream = client.OpenRead(string.Format("{0}?wsdl", "WebService 的 URL")); //Read the WSDL file describing a service. ServiceDescription description = ServiceDescription.Read(stream);
2.經由 ServiceDescriptionImporter 匯入 ServiceDescription。並產程式碼到 CodeCompileUnit
//--Initialize a service description importer. ServiceDescriptionImporter importer = new ServiceDescriptionImporter(); //Use SOAP 1.2. (有些Java的WebService不Support 1.2,請設成空字串 importer.ProtocolName = "Soap12"; importer.AddServiceDescription(description, null, null); //--Generate a proxy client. importer.Style = ServiceDescriptionImportStyle.Client; importer.CodeGenerationOptions = System.Xml.Serialization.CodeGenerationOptions.GenerateProperties;
3.透過 CodeDomProvider 來編譯成組件
//Initialize a Code-DOM tree into which we will import the service. CodeNamespace codenamespace = new CodeNamespace(); CodeCompileUnit codeunit = new CodeCompileUnit(); codeunit.Namespaces.Add(codenamespace); //Import the service into the Code-DOM tree. //This creates proxy code that uses the service. ServiceDescriptionImportWarnings warning = importer.Import(codenamespace, codeunit); if (warning == 0) { //--Generate the proxy code CodeDomProvider provider = CodeDomProvider.CreateProvider("CSharp"); //--Compile the assembly proxy with the appropriate references string[] assemblyReferences = new string[] { "System.dll", "System.Web.Services.dll", "System.Web.dll", "System.Xml.dll", "System.Data.dll"}; //--Add parameters CompilerParameters parms = new CompilerParameters(assemblyReferences); parms.GenerateInMemory = true; CompilerResults results = provider.CompileAssemblyFromDom(parms, codeunit); // step 4 從這裡開始 ... }
4.從編譯好的組件中建立 WebService Client 端 Proxy 物件
//--Finally, Invoke the web service method Object wsvcClass = results.CompiledAssembly.CreateInstance(serviceName);
5.從建立好的 WebService Proxy 物件,取出 MethodInfo 來動態執行
MethodInfo mi = wsvcClass.GetType().GetMethod(methodName); return mi.Invoke(wsvcClass, new object(){"這裡放要要傳給 WebService Method 的 參數 "});
如果參數是物件的話,將會發生型別轉換的錯誤,如下,
類型 'System.String' 的物件無法轉換成類型 'xxx'。
那要怎麼辦呢? 我們需要可以將 String 轉成 物件的方式。
5.1.我們可以傳遞 JSON 字串,再透過 JSON.NET 來轉成 物件。
所以在呼叫時,要判斷參數是否為一般型別或是字串,不是的話,就轉成對應的物件,再呼叫它。如下,
//這裡取出 WebService 的參數 //判斷如果是物件或是Array的話,就透過 JSON.NET 來轉成WS要的物件 ParameterInfo[] pInfos = mi.GetParameters(); ArrayList newArgs = new ArrayList(); int i = 0; foreach (ParameterInfo p in pInfos) { Type pType = p.ParameterType; if (pType.IsPrimitive || pType == typeof(string)) { newArgs.Add(args[i]); } else { //透過 JSON.NET 轉成物件 var argObj = JsonConvert.DeserializeObject((string)args[i], pType); newArgs.Add(argObj); } } return mi.Invoke(wsvcClass, newArgs.ToArray());
完整的程式(參考Call a Web Service Without Adding a Web Reference,並加入JSON轉成物件)如下,
public static Object CallWebService(string webServiceAsmxUrl, string serviceName, string methodName, object[] args) { System.Net.WebClient client = new System.Net.WebClient(); //Connect To the web service System.IO.Stream stream = client.OpenRead(string.Format("{0}?wsdl", webServiceAsmxUrl)); //Read the WSDL file describing a service. ServiceDescription description = ServiceDescription.Read(stream); //Load the DOM //--Initialize a service description importer. ServiceDescriptionImporter importer = new ServiceDescriptionImporter(); //Use SOAP 1.2. (有些Java的WebService不Support 1.2,請設成空字串 importer.ProtocolName = "Soap12"; importer.AddServiceDescription(description, null, null); //--Generate a proxy client. importer.Style = ServiceDescriptionImportStyle.Client; importer.CodeGenerationOptions = System.Xml.Serialization.CodeGenerationOptions.GenerateProperties; //Initialize a Code-DOM tree into which we will import the service. CodeNamespace codenamespace = new CodeNamespace(); CodeCompileUnit codeunit = new CodeCompileUnit(); codeunit.Namespaces.Add(codenamespace); //Import the service into the Code-DOM tree. //This creates proxy code that uses the service. ServiceDescriptionImportWarnings warning = importer.Import(codenamespace, codeunit); if (warning == 0) { //--Generate the proxy code CodeDomProvider provider = CodeDomProvider.CreateProvider("CSharp"); //--Compile the assembly proxy with the // appropriate references string[] assemblyReferences = new string[] { "System.dll", "System.Web.Services.dll", "System.Web.dll", "System.Xml.dll", "System.Data.dll"}; //--Add parameters CompilerParameters parms = new CompilerParameters(assemblyReferences); parms.GenerateInMemory = true; //(Thanks for this line nikolas) CompilerResults results = provider.CompileAssemblyFromDom(parms, codeunit); //--Check For Errors if (results.Errors.Count > 0) { foreach (CompilerError oops in results.Errors) { System.Diagnostics.Debug.WriteLine("========Compiler error============"); System.Diagnostics.Debug.WriteLine(oops.ErrorText); } throw new Exception("Compile Error Occured calling WebService."); } //--Finally, Invoke the web service method Object wsvcClass = results.CompiledAssembly.CreateInstance(serviceName); MethodInfo mi = wsvcClass.GetType().GetMethod(methodName); //這裡取出 WebService 的參數 //判斷如果是物件或是Array的話,就透過JSON.NET來轉成WS要的物件 ParameterInfo[] pInfos = mi.GetParameters(); ArrayList newArgs = new ArrayList(); int i = 0; foreach (ParameterInfo p in pInfos) { Type pType = p.ParameterType; if (pType.IsPrimitive || pType == typeof(string)) { newArgs.Add(args[i]); } else { //透過 JSON.NET 轉成物件 var argObj = JsonConvert.DeserializeObject((string)args[i], pType); newArgs.Add(argObj); } } return mi.Invoke(wsvcClass, newArgs.ToArray()); } else { return null; } }
註: 有些Java的WebService不Support SOAP 1.2 ,所以 importer.ProtocolName 請設定成 "Soap" (預設就是"Soap") 。
測試
我們建立 WebService 來測試,如下,
TestService.asmx.cs
public class User { public int Id { get; set; } public string Name { get; set; } } public class Dep { public int Id { get; set; } public string Name { get; set; } public List<User> Users { get; set;} } /// <summary> /// http://localhost:3414/TestService.asmx /// </summary> [WebService(Namespace = "http://tempuri.org/")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] [System.ComponentModel.ToolboxItem(false)] public class TestService : System.Web.Services.WebService { [WebMethod] public void Silent() { // Do something } [WebMethod] public string HelloWorld() { return "Hello World"; } [WebMethod] public string SingleValue(string name) { return string.Format("Hello World, {0}", name); } [WebMethod] public string SimpleArray(string[] names) { return string.Format("Hello World, {0}", string.Join(",", names)); } [WebMethod] public string SimpleObj(User user) { return string.Format("Hello UserInfo, {0}-{1}", user.Id, user.Name); } [WebMethod] public User GetUser(int id, string name) { return new User() { Id = id, Name = name }; } [WebMethod] public List<User> GetDepUsers(Dep dep) { return dep.Users; } [WebMethod] public List<User> GetUsers(User[] users) { List<User> result = new List<User>(); foreach (User usr in users) { result.Add(usr); } return result; } }
Client端 Console 程式,測試沒有參數、字串參數、物件參數,如下,
Program.cs
string wsUrl = @"http://localhost:3414/TestService.asmx"; var silentResult = CallWebService(wsUrl, "TestService", "Silent", null); Console.WriteLine("Silent:{0}", JsonConvert.SerializeObject(silentResult)); //null var helloWorldResult = CallWebService(wsUrl, "TestService", "HelloWorld", null); Console.WriteLine("HelloWorld:{0}", JsonConvert.SerializeObject(helloWorldResult)); //Hello World var singleValueResult = CallWebService(wsUrl, "TestService", "SingleValue", new object[] { "亂馬客" }); Console.WriteLine("SingleValue:{0}", JsonConvert.SerializeObject(singleValueResult)); //Hello World, 亂馬客 string simpleArg = @"{'Id':'999', 'Name':'亂馬客'}"; var SimpleObjResult = CallWebService(wsUrl, "TestService", "SimpleObj", new object[] { simpleArg }); Console.WriteLine("SimpleObj:{0}", JsonConvert.SerializeObject(SimpleObjResult)); //Hello UserInfo, 999-亂馬客 string simpleArrayArg = @"['RM', '亂馬客']"; var SimpleArrayResult = CallWebService(wsUrl, "TestService", "SimpleArray", new object[] { simpleArrayArg }); Console.WriteLine("SimpleArray:{0}", JsonConvert.SerializeObject(SimpleArrayResult)); //Hello World, RM,亂馬客 string depUsersArg = @"{'Id':'1', 'Name':'技術開發部', 'Users':[{'Id':'1', 'Name':'RM'},{'Id':'999', 'Name':'亂馬客'}]}"; var depUsersObjResult = CallWebService(wsUrl, "TestService", "GetDepUsers", new object[] { depUsersArg }); Console.WriteLine("GetDepUsers:{0}", JsonConvert.SerializeObject(depUsersObjResult)); // 1, RM / 999, 亂馬客 string usersArg = @"[{'Id':'1', 'Name':'RM'},{'Id':'999', 'Name':'亂馬客'}]"; var getUsersResult = CallWebService(wsUrl, "TestService", "GetUsers", new object[] { usersArg }); Console.WriteLine("GetUsers:{0}", JsonConvert.SerializeObject(getUsersResult)); // 1, RM / 999, 亂馬客 Console.ReadKey();
所以 Array, Object 的參數,也可以傳遞了哦!
Source Code: https://github.com/rainmakerho/DynamicInvokeWebService
註: CallWebService 這個 Method ,目前只判斷一般型別及字串,如果您的 WebMethod 有其他沒有加入的判斷,請自行修改。
參考資料
Call a Web Service Without Adding a Web Reference
Hi,
亂馬客Blog已移到了 「亂馬客 : Re:從零開始的軟體開發生活」
請大家繼續支持 ^_^