最近做了個Web service的工具,用到一點代碼產生的技術.
1..Net的Web Service主要有兩種,ASMX的Web Service和WCF的Web Service.後者是前者的功能增強版.比如前者只支援http協議,只能建在IIS上,安全性也依賴於IIS等,後者還支援MSMQ, Enterprise Service(似乎主要是面向COM的)等,多了一些配置(如可以配置成雙向的訊息傳送),可以設定多個EndPoint,這樣對同一個服務,可以用不同的方式訪問.可以建在Windows Service上或控制台程式即所謂self-hosting.兩者之間有幾個中間產品,如附加了SOAP Extension和後期的WSE的ASMX Web Service.
2.Web service的基本概念是:用戶端和服務端通過XML進行互動,在兩頭通過序列化和還原序列化實現XML和對象的轉換.服務端將供客戶調用的介面用XML描述(如標準的WSDL),客戶根據描述產生本地代理,通過代理向服務端發SOAP訊息,調用服務.
3.ASMX Web Service
1)在IIS上的配置:
若是IIS6.0,需要建一個虛擬目錄,指向asmx檔案所在的目錄.若是IIS7.0,則建一個新網站.
如果ASP.Net是在IIS之前裝的,那麼需要運行windows/microsoft.net/framework/v2.0.57(假設是用asp.net 2.0)下的reg_iis.exe -i 註冊asp.net.
若還不能識別asmx,需要在IIS裡,將asmx和aspnet_isapi.dll關聯.重啟IIS後就可以正確解析asmx了.
2)基本結構:
服務端需要一個asmx檔案,下面是個例子,內容很簡單,就一個directive:
< %@ WebService Language="C#" CodeBehind="~/App_Code/Service.cs" Class="Service" %>
真正的實現代碼放在service.cs檔案裡.
下面是一個簡單的Service.cs檔案的內容:
using System;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using webservicelib;
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class Service : System.Web.Services.WebService
{
public Service () {
//Uncomment the following line if using designed components
//InitializeComponent();
}
[WebMethod]
public string TestNoParam() {
return "Hello World";
}
[WebMethod]
public string TestSimpleType(int intValue, string stringValue)
{
return "good";
}
[WebMethod]
public void TestComplexType(ServiceLib lib)
{
//return lib.TestInt.ToString() + " " + lib.TestString;
}
}
可以看出,首先,這個類必須繼承System.Web.Services.WebService,其次,必須帶有WebService屬性,供客戶調用的方法則帶有WebMethod屬性.
用戶端需要一個代理類,下面是
這個代理類,如果是在Visual Studio裡開發,可以加個web reference就可以自動產生,也可以用wsdl.exe命令列程式來產生.用wsdl.exe的話,主要是指定一個WSDL的Uri參數.
3)代理類的動態產生
還有一種方法,就是用程式動態產生.動態代碼產生,主要是利用CodeDom和Reflection命名空間.
動態產生一個assembly有兩種方法.一種是用System.Reflection.Emit命名空間裡的方法先產生一個AssemblyBuilder執行個體,再用這個執行個體產生一個ModuleBuilder,再用ModuleBuilder產生TypeBuilder,也就是產生了一個類,接下來就可以用TypeBuilder裡的方法產生Method等.顯然這種方法比較麻煩.
還有一種是用CodeDom產生類的代碼,再編譯(也是用CodeDom裡的方法)成assembly.CodeDom本身好比是XmlWriter,無非是提供了一種手工拼接成代碼的手段.
.Net裡在System.Web.Services.Description命名空間裡有個ServiceDescriptionImporter類提供了組建代理程式類代碼的支援.具體的實現代碼(只列出關鍵區段):
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
using System.Web.Services.Description;
using System.Web.Services.Discovery;
using System.Web.Services.Protocols;
......
ServiceDescriptionImporter importer = new ServiceDescriptionImporter();
importer.ProtocolName = "Soap";
importer.Style = ServiceDescriptionImportStyle.Client;
DiscoveryClientProtocol dcc = new DiscoveryClientProtocol();
dcc.DiscoverAny(wsdlUrl);
dcc.ResolveAll();
foreach (object doc in dcc.Documents.Values)
{
if (doc is System.Web.Services.Description.ServiceDescription)
{
importer.AddServiceDescription(doc as System.Web.Services.Description.ServiceDescription, string.Empty, string.Empty);
}
else if (doc is XmlSchema)
{
importer.Schemas.Add(doc as XmlSchema);
}
}
if (importer.ServiceDescriptions.Count == 0)
{
throw new Exception("給定的地址沒有找到Web Service!");
}
CodeCompileUnit ccu = new CodeCompileUnit();
ccu.Namespaces.Add(new CodeNamespace(""));//這裡可以加上需要的命名空間
ServiceDescriptionImportWarnings warnings = importer.Import(ccu.Namespaces[0], ccu);
if ((warnings & ServiceDescriptionImportWarnings.NoCodeGenerated) > 0)
{
throw new Exception("組建代理程式類時出錯!");
}
如果直接產生代碼,可以這樣做:
Microsoft.CSharp.CSharpCodeProvider provider = new Microsoft.CSharp.CSharpCodeProvider();
StreamWriter sw = File.CreateText(<檔案名稱>);
ICodeGenerator codeGenerator = provider.CreateGenerator(sw);
CodeGeneratorOptions codeGeneratorOptions = new CodeGeneratorOptions();
codeGenerator.GenerateCodeFromCompileUnit(ccu, sw, codeGeneratorOptions);
sw.WriteLine();
sw.Flush();
編譯可以用下面的代碼:
ICodeCompiler compiler = provider.CreateCompiler();
CompilerParameters options = new CompilerParameters(
new string[] {
"System.dll",
"System.Web.Services.dll",
"System.Xml.dll"
}, "c://temp//test.dll",//這是要產生的dll,如果用string.Empty,則在臨時目錄用一個隨機產生的檔案名稱產生dll,如果要產生exe,指定options.GenerateExecutable = true即可.
false);
options.GenerateInMemory = false;
CompilerResults results = compiler.CompileAssemblyFromDom(options, ccu);
不過這樣產生的程式碼,開啟後可以發現僅產生了方法和類的field,沒有產生property,可以用下面的方法產生property:
foreach (CodeTypeDeclaration codeTypeDeclaration in ccu.Namespaces[0].Types)
{
ArrayList fields = new ArrayList();
foreach (CodeTypeMember member in codeTypeDeclaration.Members)
{
if (member is CodeMemberField)
{
fields.Add(member);
}
}
foreach (CodeMemberField field in fields)
{
CodeMemberProperty property = new CodeMemberProperty();
property.Name = field.Name;
property.Type = field.Type;
property.Attributes = MemberAttributes.Public;
property.CustomAttributes.AddRange(field.CustomAttributes);
field.Name = "_" + field.Name;
field.Attributes = MemberAttributes.Private;
field.CustomAttributes.Clear();
//為屬性添加Get和Set方法
property.GetStatements.Add(
new CodeMethodReturnStatement(
new CodeFieldReferenceExpression(
new CodeThisReferenceExpression(), field.Name)));
property.SetStatements.Add(
new CodeAssignStatement(
new CodeFieldReferenceExpression(
new CodeThisReferenceExpression(), field.Name),
new CodePropertySetValueReferenceExpression()));
codeTypeDeclaration.Members.Add(property);
}
}
這種方法僅適用ASMX的Web Service.
4)用程式調用CodeDom產生的代理類
先利用Reflection找到要調用的方法,然後用Invoke方法來調用:
public static object InvokeWebMethod(string proxyAssembly, string methodName, object[] parameters)
{
object returnValue = null;
Assembly assembly = Assembly.LoadFile(proxyAssembly);
foreach (Type type in assembly.GetTypes())
{
if (!type.IsSubclassOf(typeof(SoapHttpClientProtocol)))
{
continue;
}
SoapHttpClientProtocol service = Activator.CreateInstance(type) as SoapHttpClientProtocol;
Type serviceType = service.GetType();
foreach (MethodInfo method in serviceType.GetMethods(
BindingFlags.Public | BindingFlags.DeclaredOnly
| BindingFlags.Instance))
{
if (method.Name == methodName)
{
try
{
returnValue = method.Invoke(service, parameters);
return returnValue;
}
catch (Exception ex)
{
throw ex;
}
}
}
}
}
4.WCF Web Service
1)新概念:
相對於ASMX來說,增加了一些新概念:
Binding:指串連的方式,ASMX裡只支援HTTP,沒有這個概念.WCF裡還支援Tcp,MSMQ,Enterprise Service等,所以有這個概念,指定串連所用的協議.
Endpoint:ASMX裡,Web Service用一個URL來訪問,也沒有這個概念.WCF裡支援多個訪問點,所以有這個概念.
Behavior:對串連做些配置,如設定成單向或雙向訊息傳送,支援HTTP的GET或只支援POST方式等.
2)Hosting
WCF在IIS hosting的基礎上增加了Self-hosting的方式:
public class Program
{
static void Main(string[] args)
{
using (ServiceHost serviceHost = new ServiceHost(typeof(Service1)))
{
serviceHost.Open();
Console.WriteLine("Service started...");
Console.ReadLine();
serviceHost.Close();
}
}
}
其中Service1類裡存放了服務的實現代碼.
3)基本結構
相對ASMX Web Service來說,WCF Web Service稍複雜一些,增加了控制手段,更加靈活.
服務端的簡單代碼如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
namespace testWcfService
{
// NOTE: If you change the interface name "IService1" here, you must also update the reference to "IService1" in App.config.
[ServiceContract]
public interface IService1
{
[OperationContract]
string GetData(int value);
[OperationContract]
CompositeType GetDataUsingDataContract(CompositeType composite);
// TODO: Add your service operations here
}
// Use a data contract as illustrated in the sample below to add composite types to service operations
[DataContract]
public class CompositeType
{
bool boolValue = true;
string stringValue = "Hello ";
[DataMember]
public bool BoolValue
{
get { return boolValue; }
set { boolValue = value; }
}
[DataMember]
public string StringValue
{
get { return stringValue; }
set { stringValue = value; }
}
}
}
這個是用Visual Studio的模板自動產生的代碼, ServiceContract屬性大體對應ASMX裡的WebService屬性,OperationContract對應於WebMethod屬性.DataContract是新的屬性,用於使用者自訂類型.另外,用介面代替了類.
Endpoint和Behavior都放在app.config檔案裡:
< system.serviceModel>
< services>
< service name="testWcfService.Service1" behaviorConfiguration="testWcfService.Service1Behavior">
< host>
< baseAddresses>
< add baseAddress = "http://localhost:8732/testWcfService/Service1/" />
< /baseAddresses>
< /host>
< !-- Service Endpoints -->
< !-- Unless fully qualified, address is relative to base address supplied above -->
< endpoint address ="" binding="wsHttpBinding" contract="testWcfService.IService1">
< !--
Upon deployment, the following identity element should be removed or replaced to reflect the identity under which the deployed service runs. If removed, WCF will infer an appropriate identity automatically.
-->
< identity>
< dns value="localhost"/>
< /identity>
< /endpoint>
< !-- Metadata Endpoints -->
< !-- The Metadata Exchange endpoint is used by the service to describe itself to clients. -->
< !-- This endpoint does not use a secure binding and should be secured or removed before deployment -->
< endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
< /service>
< /services>
< behaviors>
< serviceBehaviors>
< behavior name="testWcfService.Service1Behavior">
< !-- To avoid disclosing metadata information, set the value below to false and remove the metadata endpoint above before deployment -->
< serviceMetadata httpGetEnabled="True"/>
< !-- To receive exception details in faults for debugging purposes, set the value below to true. Set to false before deployment to avoid disclosing exception information -->
< serviceDebug includeExceptionDetailInFaults="True" />
< /behavior>
< /serviceBehaviors>
< /behaviors>
< /system.serviceModel>"
用戶端可以用Visual Studio裡加Service Reference產生,也可以用svcutil.exe命令列程式產生,除了代理類之外,還會組建組態檔案:
< system.serviceModel>
< bindings>
< wsHttpBinding>
< binding name="WSHttpBinding_IService1" closeTimeout="00:01:00"
openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00"
bypassProxyOnLocal="false" transactionFlow="false" hostNameComparisonMode="StrongWildcard"
maxBufferPoolSize="524288" maxReceivedMessageSize="65536"
messageEncoding="Text" textEncoding="utf-8" useDefaultWebProxy="true"
allowCookies="false">
< readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
maxBytesPerRead="4096" maxNameTableCharCount="16384" />
< reliableSession ordered="true" inactivityTimeout="00:10:00"
enabled="false" />
< security mode="Message">
< transport clientCredentialType="Windows" proxyCredentialType="None"
realm="" />
< message clientCredentialType="Windows" negotiateServiceCredential="true"
algorithmSuite="Default" establishSecurityContext="true" />
< /security>
< /binding>
< /wsHttpBinding>
< /bindings>
< client>
< endpoint address="http://localhost:8732/testWcfService/Service1/"
binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_IService1"
contract="ServiceReference1.IService1" name="WSHttpBinding_IService1">
< identity>
< dns value="localhost" />
< /identity>
< /endpoint>
< /client>
< /system.serviceModel>
這裡最重要的是Endpoint,裡麵包括了串連所用的地址(EndpointAddress),連線協定(Binding),代理類(介面)名(ServiceReference1.IService1)等.
4)代理類的代碼產生
要用到System.ServiceModel和System.ServiceModel.Description命名空間,下面是稍加改寫的MSDN上的代碼:
ArrayList endPoints = new ArrayList();
MetadataExchangeClient mexClient = new MetadataExchangeClient(new EndpointAddress(serviceUri));//serviceUri參數是WCF Web Service Metadata的地址,在服務端的設定檔裡定義,一般為Endpoint的地址加上/mex
mexClient.ResolveMetadataReferences = true;
MetadataSet metaDocs = mexClient.GetMetadata();
WsdlImporter importer = new WsdlImporter(metaDocs);
ServiceContractGenerator contractGenerator = new ServiceContractGenerator();
Collection contracts = importer.ImportAllContracts();
foreach (ServiceEndpoint endPoint in importer.ImportAllEndpoints())
{
endPoints.Add(endPoint);//將Endpoint存起來供以後調用方法時用
}
foreach (ContractDescription contract in contracts)
{
contractGenerator.GenerateServiceContractType(contract);
}
if (contractGenerator.Errors.Count != 0)
{
return "產生代碼出錯!";
}
可以將產生的程式碼寫到檔案裡,如MSDN示範的那樣:
CodeDomProvider provider = CodeDomProvider.CreateProvider("C#");
CodeGeneratorOptions options = new CodeGeneratorOptions();
IndentedTextWriter textWriter = new IndentedTextWriter(new StreamWriter(proxyClass));
provider.GenerateCodeFromCompileUnit(contractGenerator.TargetCompileUnit, textWriter, options);
textWriter.Close();
如果要編譯,需要注意WCF是基於.Net Framework 3.0以後版本的,但是CodeDom到今天還只有2.0版本,所以編譯時間依賴的System.ServiceModel.dll和System.Runtime.Serialization.dll需要特別指定路徑:
CompilerParameters compileoptions = new CompilerParameters();
compileoptions.ReferencedAssemblies.Add("System.dll");
compileoptions.CompilerOptions = "/lib:/"" + Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)
+ "//Reference Assemblies//Microsoft//Framework//v3.0/" ";//這個路徑下包括了System.ServiceModel.dll和System.Runtime.Serialization.dll
另外,在Framework 2.0裡,用的是ICodeCompiler,3.0裡提示這個方法已經過時了,直接用上面產生的provider就可以了:
compileoptions.OutputAssembly = proxyAssembly;
compileoptions.IncludeDebugInformation = false;
compileoptions.GenerateInMemory = false;
compileoptions.GenerateExecutable = false;
CodeCompileUnit[] ccus = new CodeCompileUnit[1];
ccus[0] = contractGenerator.TargetCompileUnit;
CompilerResults results = provider.CompileAssemblyFromDom(compileoptions, ccus);
foreach (CompilerError err in results.Errors)
{//下面的語句有助於調試
string t = err.ErrorText;
int s = err.Line;
}
這裡沒有產生用戶端的設定檔,但所需資訊其實已經用上面的ImportAllEndpoints和mportAllContracts方法取出來了,不難利用這些資訊組建組態檔案,或者存在某個資料結構裡,動態調用.
5)利用Reflection調用代理類:
下面是一個例子:
public static object InvokeWcfMethod(string proxyAssembly, string methodName, object[] parameters, ArrayList endPoints)//就是上面產生的Endpoints
object returnValue = null;
object serviceObject = null;
Assembly assembly = Assembly.LoadFile(proxyAssembly);
foreach (Type type in assembly.GetTypes())
{
if (type.IsInterface || !(type.Namespace == null))
{
continue;
}
if (endPoints != null && endPoints.Count > 0)
{
foreach (ServiceEndpoint endPoint in endPoints)
{
bool isFinished = false;
Type contractType = assembly.GetType(endPoint.Contract.Name);
if (contractType != null)
{
foreach (Type interfaceType in type.GetInterfaces())
{
if (interfaceType == contractType)
{
serviceObject = Convert.ChangeType(Activator.CreateInstance(type, new object[]{endPoint.Binding, endPoint.Address}), type);
isFinished = true;
break;
}
}
}
if (isFinished)
{
break;
}
}
}
foreach (MethodInfo method in type.GetMethods(
BindingFlags.Public | BindingFlags.DeclaredOnly
| BindingFlags.Instance))
{
if (methodName == method.Name)
{
try
{
returnValue = method.Invoke(serviceObject, parameters);
}
catch (Exception ex)
{
throw ex;
}
}
}