最近做了个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;
}
}
}