[.NET]動態呼叫 WebService,並且 Pass Complex Type 參數

要如何給 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'。

image

那要怎麼辦呢? 我們需要可以將 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();

image

所以 Array, Object 的參數,也可以傳遞了哦!  

Source Code: https://github.com/rainmakerho/DynamicInvokeWebService

註: CallWebService 這個 Method ,目前只判斷一般型別及字串,如果您的 WebMethod 有其他沒有加入的判斷,請自行修改。

參考資料

Call a Web Service Without Adding a Web Reference

ServiceDescription 類別

ServiceDescriptionImporter 類別

CodeDomProvider 類別

CodeDomProvider.CompileAssemblyFromDom 方法

JSON.NET

分享使用Web Service介接的方式

Hi, 

亂馬客Blog已移到了 「亂馬客​ : Re:從零開始的軟體開發生活

請大家繼續支持 ^_^