65.9K
CodeProject 正在变化。 阅读更多。
Home

理解 WCF 中的契约

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (30投票s)

2013 年 10 月 11 日

CPOL

21分钟阅读

viewsIcon

175055

downloadIcon

5091

本文解释了 WCF 支持的契约类型

 

介绍  

WCF 中的合同提供互操作性,这是客户端进行通信所必需的。在本文中,我将解释合同的类型以及如何在实际应用中使用这些合同。在本文中,我尝试通过一个简单的例子来解释所有这些内容。在这个例子中,我以一个真实世界的长方体为例,它有 3 个属性(长、宽、高),我们的服务计算长方体的体积和总表面积。通过消息合同和数据合同两种方式进行演示。

背景

正是这些合同,客户端和服务才就通信时使用的操作类型和结构达成一致。这是客户端和服务之间的一项正式协议,用于定义描述服务功能、独立于平台的标准方式。WCF 定义了四种类型的合同:

  • 服务合同
  • 数据合同
  • 消息合同
  • 错误合同 

服务合同

服务合同**描述了服务终结点上可用的操作**(方法),并向外部公开。服务合同描述了服务公开的、客户端可调用的操作(函数),除此之外,它还描述了:

  • 操作、接口和服务方法与独立于平台的描述的映射关系
  • 服务可以与其他方进行的消息交换模式,可能是一次性/请求-回复/双工。

要创建服务合同,您可以定义一个具有相关方法的接口,这些方法代表了一组服务操作,然后使用**ServiceContract**特性修饰该接口/类,以表明它是一个服务合同。接口中应包含在服务合同中的方法应使用**OperationContract**特性进行修饰。

[ServiceContract]
interface ICuboidService 
{
    [OperationContract]
    CuboidDetail CalculateDetails2(CuboidInfo cInfo);
}

数据合同  

一句话来说,数据合同**描述要交换的数据**。它是服务和客户端之间的正式协议,**包含有关他们将要交换的数据的信息**。需要在此指出的重要一点是,双方不必共享相同的数据类型即可通信,它们只需要共享相同的数据合同。数据合同定义了哪些参数和返回类型将被序列化/反序列化(**二进制<==>XML**)以便在一方与另一方之间传输。数据合同可以通过注释类、枚举甚至结构来定义,但不能注释接口。

要定义数据合同,只需使用**[DataContract]**特性修饰一个类或枚举,如下所示。

[DataContract] 
public class CuboidInfo 
{ 
}

消息合同

WCF 使用 SOAP 消息进行通信。大多数时候,开发人员更专注于开发**数据合同**、序列化数据等。有时开发人员还需要控制 SOAP 消息格式。在这种情况下,WCF 提供了消息合同来根据需要自定义消息。

**消息合同**用于控制消息正文的结构和序列化过程。它也用于在 SOAP 标头中发送/访问信息。默认情况下,WCF 会负责创建 SOAP 消息。
根据服务**数据合同**和**操作合同**。

因此,当您需要完全控制 SOAP 消息的结构以及序列化过程时,特别是当您的服务需要与不同类型的客户端互操作,或者服务需要为消息和消息部分提供额外的安全层时,消息就是一个数据包,WCF 使用这个数据包在源到目的地传输信息。这个消息包含一个**信封**、一个**标头**和一个**正文**。在处理消息合同时,有一些规则需要遵循。

  • 当使用消息合同类型作为参数时,**操作中只能使用一个参数**。
  • 服务操作**应返回 MessageContract 类型**,**或者**不应返回任何值。
  • 操作将**仅接受和返回消息合同类型**。不允许使用其他数据类型。

数据合同与消息合同之间的选择。

**90%** 的时间,**数据合同就足够了**。只有当您需要非常紧密、非常具体地控制 SOAP 消息的布局时,才需要消息合同。**大多数时候,您不需要**。

消息合同允许您明确说明哪些元素(标量类型或复合类型,如**数据合同**)将包含在 SOAP 标头中,哪些将包含在 SOAP 正文中。

如果您有一个通信伙伴,您已经与其就非常具体的格式达成一致,并且您必须调整 SOAP 消息以精确匹配该给定布局,那么您可能需要它。这几乎是您**需要**并且**应该使用消息合同**的**唯一有效场景**。

然而,有时对 SOAP 消息结构的完全控制与对其内容的控制同样重要。当**互操作性很重要,或者需要特别控制消息或消息部分的安全性问题**时,尤其如此。在这些情况下,您可以创建一个*消息合同*,使您能够将参数或返回值类型用于*直接序列化为所需 SOAP 消息的类型*。

所以,长话短说:**始终**使用**数据合同**,几乎从不使用消息合同(**除非您确实需要**)。

错误合同

错误合同**为服务中发生的错误向客户端提供了文档化的视图**。这有助于我们轻松识别发生了什么错误以及在哪里发生的。默认情况下,当我们在服务中抛出任何异常时,它不会到达客户端。客户端对服务器端发生的事情了解得越少,交互就越解耦。这种现象(不允许错误的实际原因到达客户端)被称为**错误掩蔽**。默认情况下,服务端的所有异常都会作为**FaultException**到达客户端,通过使所有服务异常都无法区分,WCF 将客户端与服务解耦。

虽然默认的**错误掩蔽策略**是**WCF 的最佳实践**,但有时**客户端需要以预先规定的有意义的方式响应这些异常**。WCF 提供了通过**SOAP Fault**合同处理错误消息并将其传达给客户端的选项。**SOAP** 错误基于行业标准,独立于任何特定于技术的异常,它是将特定于技术的异常映射到一些中性错误信息的一种方式。因此,当服务遇到意外错误时,服务可以抛出 **FaultException<T>** 类的实例,而不是抛出原始的 CLR 异常(特定于技术)。

使用代码

本文已分为 4 个模块:

  • WCF 服务类库 (ContractLib.dll):实际的服务逻辑,它定义并实现服务,并公开一些函数供外部使用。
  • 用于托管 WCF 服务类库的控制台宿主应用程序 (ContractLibHost.exe):托管 WCF 类库。
  • 控制台客户端应用程序 (ContractClient.exe):将使用此服务的客户端应用程序。
  • Web 客户端 (ContractWebClient):将使用此服务的网站。

第一个模块:WCF 服务类库 (ContractLib.dll)

要创建此项目,您可以简单地选择“**类库**”项目,从项目向导选项中选择。让我们称它为“ContractLib”,它实现了实际的业务逻辑。项目已包含一个文件 Class1.cs,在编写任何服务库代码之前,让我们进行一些清理工作。

一点清理

  • 从项目中删除文件 Class1.cs
  • 向项目中添加一个名为ICuboidService的新**接口**,将添加一个新文件 ICuboidService.cs 到项目中。
  • 向项目中添加一个名为CuboidService的新**类**,它将实现ICuboidService接口,将添加一个新文件 CuboidService.cs 到项目中。

定义服务接口 - 数据合同(s)

CuboidFaulltException 的定义  
[DataContract] 
public class CuboidFaultException
{ 
    private string _reason; 
    private string _source; 
    private string _detail; 
    private string _helpLink;
    
    [DataMember] 
    public string Reason 
    { 
        get { return _reason; } 
        set { _reason = value; } 
    }
   
    [DataMember]
    public string Source
    {
        get { return _source; }
        set { _source = value; }
    }
 
    [DataMember]
    public string Detail
    {
        get { return _detail; }
        set { _detail = value; }
    }
 
    [DataMember]
    public string HelpLink
    {
        get { return _helpLink; }
        set { _helpLink = value; }
    }
}
解释
这个**数据合同**定义了当服务抛出**CuboidFaultException**时要交换的数据。由于服务也实现了错误合同,因此这些信息将从服务发送到客户端。我定义了 4 个字段,用于将错误信息发送到客户端,您可以根据需要定义更多字段,以将其他上下文信息发送到客户端。

原因 (Reason):异常的实际原因,为什么服务抛出了**CuboidFaultException**。

来源 (Source):抛出异常的来源信息,在此字段中,您可以发送函数名称、异常发生的行号,或您认为适合作为错误的来源的任何信息。
错误字段。

详细信息 (Detail):您可以在此处打包有关错误实际详细信息的信息。

帮助链接 (HelpLink):有关此类型错误的文档的网页链接,任何您认为有帮助的东西。

CuboidDimension 的定义
[DataContract]       
public class CuboidDimension       
{       
      [DataMember]    public int Length;       
      [DataMember]    public int Width;       
      [DataMember]    public int Height;       
} 
解释

这个数据合同简单地定义了一个长方体的 3 个维度,即长、宽和高。

CuboidInfo 的定义
[DataContract]       
public class CuboidInfo       
{       
      [DataMember] public int ID;       
      [DataMember] public CuboidDimension Dimension = new CuboidDimension();       
} 
解释 

这个数据合同定义了一个类,它包含两个字段:第一个字段是长方体的**ID**,第二个字段是前面定义的**CuboidDimension**类的对象,它包含长方体的尺寸(长、宽、高)。

CuboidDetail 的定义
[DataContract]       
public class CuboidDetail       
{       
      [DataMember]        public int ID;       
      [DataMember]        public double SurfaceArea;       
      [DataMember]        public double Volume;       
}
解释 
这个数据合同简单地定义了一个类,它包含 3 个字段:第一个字段是长方体的**ID**,第二个字段是一个 double,它将保存长方体的**表面积**值,第三个字段是
一个 double,它将包含长方体的**体积**。

定义服务接口 - 消息合同(s)  

CuboidInfoRequest 的定义  
[MessageContract]       
public class CuboidInfoRequest       
{       
    private int m_nID ;       
    private CuboidDimension m_Dims = new CuboidDimension();
    [MessageHeader]
    public int ID         
    {         
        get { return m_nID; }         
        set { m_nID = value; }         
    }
    [MessageBodyMember]       
    public CuboidDimension Dimension       
    {       
        get { return m_Dims; }       
        set { m_Dims = value; }       
    }       
} 
 
 
解释   
这个消息合同简单地定义了一个类,它包含两个字段:第一个字段是长方体的 ID,已被定义为消息标头。消息正文中包含一个成员 **CuboidDimension**,它是长方体的详细信息(尺寸)。

定义服务接口 - 消息合同(s)

CuboidInfoResponse 的定义 
[MessageContract]
public class CuboidDetailResponse
{
    private int m_nID;
    private double m_dblArea;
    private double m_dblVolume;
 
    [MessageHeader]
    public int ID
    {
	get { return m_nID; }
        set { m_nID = value; }
	}
 
    [MessageBodyMember]
    public double SurfaceArea
    {
	get { return m_dblArea ; }
        set { m_dblArea = value ; }
	}
 
    [MessageBodyMember]
    public double Volume
    {
	get { return m_dblVolume; }
        set { m_dblVolume = value; }
	}
}
   
解释   

这个消息合同定义了一个类,它包含 3 个字段:长方体的**ID**已被定义为**消息标头**。**消息正文**包含两个成员,即长方体的**体积**和**表面积**。

定义服务接口 – 服务合同、操作合同、错误合同 

定义服务合同 ICuboidService  
[ServiceContract]
interface ICuboidService
{
    [OperationContract]
    [FaultContract(typeof(CuboidFaultException))]
    CuboidDetailResponse CalculateDetails1(CuboidInfoRequest cInfo);
   
    [OperationContract]
    [FaultContract(typeof(CuboidFaultException))]
    CuboidDetail CalculateDetails2(CuboidInfo cInfo);
 
    [OperationContract]
    [FaultContract(typeof(CuboidFaultException))]
    CuboidDetail CalculateDetails3(int nID, CuboidDimension cInfo);
} 
 
 
 解释  

服务定义了 3 个方法作为服务的公开操作。

第一个方法 **CalculateDetails1** 接受 **CuboidInfoRequest** 消息合同类型作为输入参数,并返回 **CuboidDetailResponse** 消息合同类型。

遵守规则。 

  • 当使用消息合同类型作为参数时,**操作中只能使用一个参数**。
  • 服务操作**应返回 MessageContract 类型**,**或者**不应返回任何值。
  • 操作将**仅接受和返回消息合同类型**。不允许使用其他数据类型。

实现了一个**错误合同**,它在一切正常时抛出 **CuboidFaultException**。

第二个方法 **CalculateDetails2** 接受 **CuboidInfo** 数据合同类型作为输入参数,并返回 **CuboidDetail** 数据合同类型。尽管方法只接受一个参数。
实现了一个**错误合同**,它在一切正常时抛出 **CuboidFaultException**。与消息合同不同,使用数据合同不强制执行任何规则。

第三个方法 **CalculateDetails3** 接受 2 个参数,第一个参数是一个 int,第二个参数是 **CuboidDimension** 数据合同类型作为输入参数,并返回 **CuboidDetail** 数据合同类型。实现了一个**错误合同**,它在一切正常时抛出 **CuboidFaultException**。使用数据合同不强制执行任何规则,与消息合同不同。

使用数据合同不强制执行任何规则,与消息合同不同。

实现服务接口

实现方法 CalculateDetails1 
public CuboidDetailResponse CalculateDetails1(CuboidInfoRequest cInfo)
{
    if ((cInfo.Dimension.ID <=0) || (cInfo.Dimension.Length <= 0) ||  (cInfo.Dimension.Width <= 0) || (cInfo.Dimension.Height <= 0))
    {
        CuboidFaultException faultEx = new CuboidFaultException ();
        faultEx.Reason = "Any dimension of Cuboid (length/width/height) can not be zero or negative value";
        faultEx.Source = "CalculateDetails1()";
        faultEx.HelpLink = "http://praveenkatiyar.wordpress.com";
        StringBuilder sbDetail = new StringBuilder ("");
        if (cInfo.Dimension.ID <= 0) sbDetail.Append ( "[ID is <= 0] ");
        if (cInfo.Dimension.Length <= 0) sbDetail.Append ( "[Length is <= 0] ");
        if (cInfo.Dimension.Width <= 0) sbDetail.Append ( "[Width is <= 0] ");
        if (cInfo.Dimension.Height <= 0) sbDetail.Append ( "[Height is <= 0]") ;
        faultEx.Detail = sbDetail.ToString();
        throw new FaultException<CuboidFaultException>(faultEx);
    }
 
    CuboidDetailResponse dInfo = new CuboidDetailResponse();
    dInfo.ID = cInfo.ID ;
    dInfo.Volume = (cInfo.Dimension.Length * cInfo.Dimension.Width * cInfo.Dimension.Height);
    dInfo.SurfaceArea = 2 * ( (cInfo.Dimension.Length * cInfo.Dimension.Width) + 
                              (cInfo.Dimension.Width * cInfo.Dimension.Height) + 
                              (cInfo.Dimension.Length * cInfo.Dimension.Height ));
    return dInfo;
} 
 解释 

操作接受一个 MessageContrat(**CuboidInfoRequest**)类型参数。操作检查长方体的 ID 和属性(长、宽、高)是否不为零或负数。如果是,则抛出一个**CuboidFaultException**类型的**FaultException**,并为**CuboidFaultException**的字段赋值。如果一切正常,则计算所需值,创建一个新的**MessageContract**对象(**CuboidDetailResponse**)并为其字段赋值并返回该对象。

实现方法 CalculateDetails2
public CuboidDetail CalculateDetails2(CuboidInfo cInfo)
{
    if ((cInfo.Dimension.ID <=0) || (cInfo.Dimension.Length <= 0) ||  (cInfo.Dimension.Width <= 0) || (cInfo.Dimension.Height <= 0))
    {
        CuboidFaultException faultEx = new CuboidFaultException ();
        faultEx.Reason = "Any dimension of Cuboid (length/width/height) can not be zero or negative value";
        faultEx.Source = "CalculateDetails2()";
        faultEx.HelpLink = "http://praveenkatiyar.wordpress.com";
        StringBuilder sbDetail = new StringBuilder (""); 
        if (cInfo.Dimension.ID <= 0) sbDetail.Append ( "[ID is <= 0] ");
        if (cInfo.Dimension.Length <= 0) sbDetail.Append ( "[Length is <= 0] ");
        if (cInfo.Dimension.Width <= 0) sbDetail.Append ( "[Width is <= 0] ");
        if (cInfo.Dimension.Height <= 0) sbDetail.Append ( "[Height is <= 0]") ;
        faultEx.Detail = sbDetail.ToString();
        throw new FaultException<CuboidFaultException>(faultEx);
    }
 
    CuboidDetail dInfo = new CuboidDetail();
            dInfo.ID = cInfo.ID;
            dInfo.Volume = (cInfo.Dimension.Length * cInfo.Dimension.Width * cInfo.Dimension.Height);
            dInfo.SurfaceArea = 2 * ((cInfo.Dimension.Length * cInfo.Dimension.Width) +    
                                    (cInfo.Dimension.Width * cInfo.Dimension.Height) + 
                                    (cInfo.Dimension.Length * cInfo.Dimension.Height));
    return dInfo;
}  
 解释 

操作接受一个 DataContrat(**CuboidInfo**)类型参数。方法检查长方体的 ID 和属性(长、宽、高)是否不为零或负数。如果是,则简单地创建并抛出一个**CuboidFaultException**类型的**FaultException**,并为**CuboidFaultException**的字段赋值。如果一切正常,则计算所需值,创建一个新的**DataContract**对象(**CuboidDetail**)并为其字段赋值并返回该对象。

实现方法 CalculateDetails3 
public CuboidDetail CalculateDetails3(int nID, CuboidDimension  cDims)
{
    if ((nID <=0) || (cDims.Length <= 0) || (cDims.Width <= 0) || (cDims.Height <= 0))
    {
        CuboidFaultException faultEx = new CuboidFaultException();
        faultEx.Reason = "Any dimension of Cuboid (length/width/height) can not be zero or negative value";
        faultEx.Source = "CalculateDetails3()";
        faultEx.HelpLink = "http://praveenkatiyar.wordpress.com";
        StringBuilder sbDetail = new StringBuilder("");         
        if (nID <= 0) sbDetail.Append ( "[ID is <= 0] "); 
        if (cDims.Length <= 0) sbDetail.Append("[Length is <= 0] ");
        if (cDims.Width <= 0) sbDetail.Append("[Width is <= 0] ");
        if (cDims.Height <= 0) sbDetail.Append("[Height is <= 0]");
        faultEx.Detail = sbDetail.ToString();
        throw new FaultException<CuboidFaultException>(faultEx);
    }
 
    CuboidDetail dInfo = new CuboidDetail();
    dInfo.ID = nID;
    dInfo.Volume = (cDims.Length * cDims.Width * cDims.Height);
    dInfo.SurfaceArea = 2 * ((cDims.Length * cDims.Width) + (cDims.Width * cDims.Height) + (cDims.Length * cDims.Height));
    return dInfo;
} 
解释 

操作接受两个参数:

  • 第一个参数是长方体的(整数)ID。
  • 第二个参数是 DataContract **CuboidDimension** 的对象,这表明在使用 DataContract 时,可以传递多个参数。

操作检查长方体的 ID 和属性(长、宽、高)是否不为零或负数。如果是,则简单地创建并抛出一个**CuboidFaultException**类型的**FaultException**,并为**CuboidFaultException**的字段赋值。如果一切正常,则计算所需值,创建一个新的**DataContract**对象(**CuboidDetail**)并为其字段赋值并返回该对象。
**构建项目**(类库),**第一模块完成。 **

第二模块:宿主应用程序 (ContractLibHost.exe)

我在本项目中使用了自宿主。要创建此项目,您可以简单地选择“**控制台应用程序**”项目,从项目向导选项中选择。让我们称它为“ContractLibHost”,此应用程序将自宿主服务。在编写任何宿主应用程序代码之前,让我们进行一些清理工作。

添加应用程序配置

  • 向项目中添加一个**应用程序配置文件 (App.config)**。

定义配置

在编写任何宿主代码之前,让我们先定义配置。

假设

宿主应用程序将为服务公开以下终结点:

  • CuboidService 将在**9011 端口**上公开**HTTP 终结点**。
  • 对应的**mex** 终结点** (IMetadatExchange)** 用于 HTTP 终结点。
为**CuboidService**定义配置
定义终结点 
<service name="ContractLib.CuboidService" behaviorConfiguration="CuboidServiceBehavior">
    <host>
        <baseAddresses>
            <add baseAddress="https://:9011/CuboidService"/>
        </baseAddresses>
    </host>
    <endpoint address="https://:9011/CuboidService" binding="wsHttpBinding" contract="ContractLib.ICuboidService"/>
    <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
</service> 
 解释

服务定义了一个具有**wsHttpBinding**的**终结点**,在 9011 端口上,另一个终结点定义为**mex**,用于发布元数据,**IMetadataExchange**是**mex**终结点的标准合同。

定义行为
<behaviors>
    <serviceBehaviors>
        <!– CalcService Behavior –>
        <behavior name="CuboidServiceBehavior">
            <serviceMetadata httpGetEnabled="true"/>
            <serviceDebug includeExceptionDetailInFaults="true "/>
       </behavior>
    </serviceBehaviors>
</behaviors>
 解释

服务行为定义了两个属性:

<serviceMetadata httpGetEnabled="true" /> 

获取或设置一个值,该值指示是否通过 HTTP/GET 请求发布服务元数据,true 表示是,元数据可用于通过 HTTP/GET 请求检索。

<serviceDebug includeExceptionDetailInFaults="true "/>

IncludeExceptionDetailsInFaults 设置为 true **以使客户端能够获取有关内部服务方法异常的信息;仅建议将其作为临时调试服务应用程序的方式。在生产服务器上,此属性**必须设置为 false**。

托管服务

由于已定义了服务配置,在编写任何代码之前,

  • 将 **System.ServiceModel** 的引用添加到项目中。
让我们编写代码来托管服务。
try
{
    ServiceHost host = new ServiceHost(typeof(ContractLib.CuboidService));
    host.Open();
    Console.WriteLine("Service is Hosted as https://:9011/CuboidService");
    Console.WriteLine("\nPress  key to stop the service.");
    Console.ReadKey();
    host.Close();
}
catch (Exception eX)
{
    Console.WriteLine("There was en error while Hosting Service [" + eX.Message + "]");
    Console.WriteLine("\nPress  key to close.");
    Console.ReadKey();
} 
解释

ServiceHost 类提供服务的宿主。

构建并执行宿主。

以管理员模式打开命令提示符(**以管理员身份运行**)并执行宿主。输出如下。


创建客户端 (ContractClient.exe)

创建基本项目 

使用基于控制台的应用程序。命名为 ContractClient

生成代理  

当宿主应用程序(ContractLibHost.exe) 正在运行时,打开另一个命令提示符,

  • 切换到创建客户端项目的文件夹,假设(E:\ContractClient)
  • 发出以下命令来生成服务代理。
SvcUtil https://:9011/CuboidService /out:CuboidServiceProxy.cs /config:App.config <return>

此命令将生成两个文件。

  • CuboidServiceProxy.cs
  • App.Config

将这两个文件添加到客户端项目中。

在编写任何代码之前,让我们检查 app.config 文件。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <system.serviceModel>
        <bindings>
		. . . .
        </bindings>
        <client>
		. . . .
        </client>
    </system.serviceModel>
</configuration>  

绑定部分包含标准绑定和自定义绑定的集合。每个条目都是一个绑定元素,可以通过其唯一名称来标识。

<bindings>
    <wsHttpBinding>
        <binding name="WSHttpBinding_ICuboidService" />
    </wsHttpBinding>
</bindings>

客户端部分包含客户端用于连接到服务的终结点列表。

<client>
    <endpoint address="https://:9011/CuboidService" binding="wsHttpBinding"    bindingConfiguration="WSHttpBinding_ICuboidService" contract="CuboidServiceReference.ICuboidService" name="WSHttpBinding_ICuboidService">
        <identity>
            <userPrincipalName value="PRAVEEN-WIN7\BriskTech" />
        </identity>
    </endpoint>
</client>    

 注意: 

 <userPrincipalName value="PRAVEEN-WIN7\BriskTech" /> 

在您的机器上会有所不同。

使用代理调用服务 

声明一些临时变量来读取输入。

int nID = 0;
int nLength = 0;
int nWidth = 0;
int nHeight = 0;

读取长方体的 ID、长度、宽度和高度。

Console.WriteLine(); 
Console.Write("Enter ID : ");
string strID = Console.ReadLine();
bool bID = int.TryParse(strID, out nID);
 
Console.WriteLine(); 
Console.Write("Enter Length : ");
string strLength = Console.ReadLine();
bool bLength = int.TryParse(strLength, out nLength);
 
Console.WriteLine(); 
Console.Write("Enter Width : ");
string strWidth = Console.ReadLine();
bool bWidth = int.TryParse(strWidth, out nWidth);
 
Console.WriteLine(); 
Console.Write("Enter Height : ");
string strHeight = Console.ReadLine();
bool bHeight = int.TryParse(strHeight, out nHeight);

确保所有输入均有效,否则退出。

bool bValid = ((bID) && (bWidth) && (bLength) && (bHeight));
if (bValid == false )
{
    if (bID == false) Console.WriteLine("ID should be a positive  numeric value");
    if (bLength == false) Console.WriteLine("Length should be a positive numeric value");
    if (bWidth == false) Console.WriteLine("Width should be a positive numeric value");
    if (bHeight == false) Console.WriteLine("Height should be a positive numeric value");
    return ;
} 

创建服务代理对象,如果失败则退出。   

CuboidServiceClient objClient = null ;
try
{
    objClient = new CuboidServiceClient ();
}
catch (Exception eX)
{
    objClient = null;
    Console.WriteLine("\nFailed while creating Service proxy [" + eX.Message + "]" );
    return;
}  
调用方法 1,使用消息合同 

将所有内容包含在 try 块中。

try { 

创建对象来保存请求参数,**CuboidInfoRequest** 定义为消息合同。

CuboidInfoRequest msgRequest = new CuboidInfoRequest();
msgRequest. Dimension = new CuboidDimension ();
将输入值分配给请求对象。
msgRequest.ID = nID; 
msgRequest.Dimension.Length = nLength;
msgRequest.Dimension.Width = nWidth;
msgRequest.Dimension.Height = nHeight;
 显示打包在消息请求中的输入值。
Console.WriteLine("\nCalling Method : CalculateDetails1");
Console.WriteLine("Message Request");
Console.WriteLine("Cuboid ID ............. : {0:D2}", msgRequest.ID);
Console.WriteLine("Cuboid Length ......... : {0:F2}", msgRequest.Dimension.Length);
Console.WriteLine("Cuboid Width .......... : {0:F2}", msgRequest.Dimension.Width);
Console.WriteLine("Cuboid Height ......... : {0:F2}", msgRequest.Dimension.Height); 
创建一个对象来保存消息响应。**CuboidDetailResponse** 已定义为**消息合同**。
CuboidDetailResponse msgResponse = new CuboidDetailResponse ();
msgResponse.SurfaceArea = objClient.CalculateDetails1(ref msgRequest.ID, msgRequest.Dimension, out msgResponse.Volume);
 显示第一个服务方法的输出。
Console.WriteLine("\nMessage Response"); 
Console.WriteLine("Total Surface Area .. : {0:F2}", msgResponse.SurfaceArea);
Console.WriteLine("Total Volume ........ : {0:F2}", msgResponse.Volume); 

如果服务端出现问题,或者您将 0 或 -1 作为任何维度传递,则显示服务发送的信息,请记住服务已实现**错误合同**。

catch (FaultException<CuboidFaultException> eX)
{
    Console.WriteLine("\nThere was an error while fetching details . . .");
    Console.WriteLine("Reason .............. : " + eX.Detail.Reason);
    Console.WriteLine("Source .............. : " + eX.Detail.Source);
    Console.WriteLine("Details ............. : " + eX.Detail.Detail);
    Console.WriteLine("Help Link ........... : " + eX.Detail.HelpLink);
}  
调用方法 2,使用数据合同

将所有内容包含在 try 块中。

try { 

创建对象来保存输入值,**CuboidInfo** 定义为数据合同。

CuboidInfo dataRequest = new CuboidInfo();
dataRequest.Dimension = new CuboidDimension ();
将输入值分配给输入数据对象。
dataRequest.ID = nID; 
dataRequest.Dimension.Length = nLength;
dataRequest.Dimension.Width = nWidth;
dataRequest.Dimension.Height = nHeight;
 显示打包在数据合同对象中的输入值。
Console.WriteLine("\nCalling Method : CalculateDetails2");
Console.WriteLine("Input Data ");
Console.WriteLine("Cuboid ID ............. : {0:D2}", dataRequest.ID);
Console.WriteLine("Cuboid Length ......... : {0:F2}", dataRequest.Dimension.Length);
Console.WriteLine("Cuboid Width .......... : {0:F2}", dataRequest.Dimension.Width);
Console.WriteLine("Cuboid Height ......... : {0:F2}", dataRequest.Dimension.Height); 
创建一个对象来保存方法 **CalculateDetails2** 返回的数据合同对象,**CuboidDetail** 已定义为**数据合同**。
CuboidDetail dataResponse = new CuboidDetail();
dataResponse = objClient.CalculateDetails2(dataRequest); 

显示第二个服务方法的输出。  

Console.WriteLine("\nData Returned");
Console.WriteLine("Total Surface Area .. : {0:F2}", dataResponse.SurfaceArea);
Console.WriteLine("Total Volume ........ : {0:F2}", dataResponse.Volume);  

如果服务端出现问题,或者您将 0 或 -1 作为任何维度传递,则显示服务发送的错误信息,请记住服务已实现错误合同。

catch (FaultException<CuboidFaultException> eX)
{
    Console.WriteLine("\nThere was an error while fetching details . . .");
    Console.WriteLine("Reason .............. : " + eX.Detail.Reason);
    Console.WriteLine("Source .............. : " + eX.Detail.Source);
    Console.WriteLine("Details ............. : " + eX.Detail.Detail);
    Console.WriteLine("Help Link ........... : " + eX.Detail.HelpLink);
}     
调用方法 3,使用数据合同

将所有内容包含在 try 块中。

try { 

创建对象来保存输入值,**CuboidInfo** 定义为数据合同。

CuboidDimension dimsCubiod = new CuboidDimension();
将输入值分配给输入数据对象。
dimsCubiod.Length = nLength;
dimsCubiod.Width = nWidth;
dimsCubiod.Height = nHeight;
 显示打包在数据合同对象中的输入值。
Console.WriteLine("\nCalling Method : CalculateDetails3");
Console.WriteLine("Input Data ");
Console.WriteLine("Cuboid ID ............. : {0:D2}", nID);
Console.WriteLine("Cuboid Length ......... : {0:F2}", dimsCubiod.Length);
Console.WriteLine("Cuboid Width .......... : {0:F2}", dimsCubiod.Width);
Console.WriteLine("Cuboid Height ......... : {0:F2}", dimsCubiod.Height);  
创建一个对象来保存方法 **CalculateDetails3** 返回的数据合同对象,**CuboidDetail** 已定义为**数据合同**。
CuboidDetail dataResponse = new CuboidDetail();
dataResponse = objClient.CalculateDetails3(nID, dimsCubiod); 

显示第二个服务方法的输出。  

Console.WriteLine("\nData Returned");
Console.WriteLine("Total Surface Area .. : {0:F2}", dataResponse.SurfaceArea);
Console.WriteLine("Total Volume ........ : {0:F2}", dataResponse.Volume);  

如果服务端出现问题,或者您将 0 或负值作为任何维度传递,则显示服务发送的错误信息,请记住服务已实现错误合同。 

catch (FaultException<CuboidFaultException> eX)
{
    Console.WriteLine("\nThere was an error while fetching details . . .");
    Console.WriteLine("Reason .............. : " + eX.Detail.Reason);
    Console.WriteLine("Source .............. : " + eX.Detail.Source);
    Console.WriteLine("Details ............. : " + eX.Detail.Detail);
    Console.WriteLine("Help Link ........... : " + eX.Detail.HelpLink);
}     

构建并执行客户端,不同输入的输出如下。

输出   

 当您提供所有有效输入时的输出。  

其中一个输入无效时的输出。   

Console Client Output, with one Invalid Inputs 

输入两个无效。

Console Client Output, with more Invalid Inputs
创建 Web 客户端

创建基本项目

创建一个网站。 

生成代理

当宿主应用程序运行时,右键单击客户端应用程序项目,右键单击网站。

添加服务引用

在地址栏中键入**CuboidService**的**mex**终结点地址,如下所示。

设计 Web 客户端的 UI 

向页面添加一些控件,如下所示。  




控件列表及其要修改的属性如下。

ID 类型  文本
lblID 标签  ID
txtID   文本框
lblLength Label 长度
txtLength 文本框
lblWidth Label 宽度
txtWidth 文本框
lblHeight Label 高度
txtHeight 文本框
btnCalc1 Button CalculateDetails1
btnCalc2 Button CalculateDetails2
btnCalc3 Button CalculateDetails3
lblMethod Label
lblArea Label
lblVolume Label
lblStatus Label

编写 Page_Load 事件 

添加完所有控件后,让我们编写 Web 页面的 Page_Load 处理程序。当您想初始化控件并赋予一些初始值时,它很有用。双击页面,IDE 将为 Page_Load 事件添加一个处理程序并打开它,代码如下。

protected void Page_Load(object sender, EventArgs e)
{
    if (!(Page.IsPostBack))
    {
        txtID.Text = "1";
        txtLength.Text = "10";
        txtWidth.Text = "20";
        txtHeight.Text = "30";
        Session["ID"] = "1";
        Session["Length"] = "10";
        Session["Width"] = "20";
        Session["Height"] = "30";
    }
}
解释 

在 Page_Load 时,如果页面是第一次加载(不是因为回发),我们用一些初始值初始化控件,并将相同的值存储到会话中,以便在回发期间可以从会话中存储的值初始化控件。

编写辅助函数以简化生活

在我们编写按钮单击事件的代码之前,让我们编写 2 个辅助函数。**第一个函数**是 **ParseAndValidate** 函数,如下所示。

protected bool ParseAndValidateInputs(ref int nID, ref int nLength, ref int nWidth, ref int nHeight)
{
    bool bID = int.TryParse(txtID.Text, out nID);
    bool bLength = int.TryParse(txtLength.Text, out nLength);
    bool bWidth = int.TryParse(txtWidth.Text, out nWidth);
    bool bHeight = int.TryParse(txtHeight.Text, out nHeight);
 
    bool bValid = ((bID) && (bWidth) && (bLength) && (bHeight));
    if (bValid == false)
    {
        StringBuilder sbTmp = new StringBuilder();
        sbTmp.Append("<Font Color=#ff0000>");
        if (bID) sbTmp.Append("<p>ID should be a positive  numeric value");
        if (bLength) sbTmp.Append("<p>Length should be a positive numeric value");
        if (bWidth) sbTmp.Append("<p>Width should be a positive numeric value");
        if (bHeight) sbTmp.Append("<p>Height should be a positive numeric value");
        sbTmp.Append("</Font>");
        lblStatus.Text = sbTmp.ToString();
        return  false ;
    }
    return true ;
} 
解释 

验证页面控件中输入的值,使用标准的库函数 **TryParse** 从字符串解析数值。值存储在通过函数传递的 ref 变量中。如果输入不是数字,函数返回 false,并将原因显示在分配给状态的标签(**lblStatus**)上。

**第二个函数**创建服务代理对象,并返回新创建的对象,如下所示。

protected CuboidServiceClient GetServiceClientObject()
{
    CuboidServiceClient objClient = null;
    try
    {
        objClient = new CuboidServiceClient();
    }
    catch (Exception eX)
    {
        objClient = null;
        lblStatus.Text = "<Font color=#ff0000>Failed while creating Service proxy ..... </font>";
    }
    return objClient ;
} 
解释  

简单地创建一个服务代理对象,并返回新创建的对象。如果创建失败,则将错误原因显示在分配给状态显示的标签(**lblStatus**)上,并**返回 null**。

调用方法 CalculateDetails1  

当 Button btnCalc1 被单击时,将调用此方法。这是其处理程序。在此事件中,我们只需解析通过 Web 页面控件输入的输入值,然后调用方法。以下是事件处理程序的分解。

protected void btnCalc1_Click(object sender, EventArgs e)
{

声明变量来保存不同的值,并解析页面控件中的输入(ID、长度、宽度和高度)。如果无效(输入了非数字值,允许输入 0 和 -1),则返回。

int nID=0, nLength=0, nWidth=0, nHeight=0;
if (ParseAndValidateInputs ( ref nID, ref nLength, ref nWidth, ref nHeight ) == false) return;

您获得了允许的值,因此将其存储到会话中,这在回发时很有用,因为您可以在回发期间用会话中存储的值初始化控件。

Session["ID"] = nID.ToString () ;
Session["Length"] = nLength .ToString ();
Session["Width"] = nWidth .ToString ();
Session["Height"] = nHeight.ToString () ;

创建服务代理对象,如果对象创建失败,则返回; 

CuboidServiceClient objClient = GetServiceClientObject();
if (objClient == null) return; 

一旦收集了输入,并且代理对象已准备好,就可以调用服务了。将所有内容包含在 try 块中,用于错误捕获,以及捕获服务抛出的**FaultException**。

//  Call Method 1
try
{

创建 MessageRequest 对象,并将输入值分配给请求。 

cuboidInfoRequest msgRequest = new CuboidInfoRequest ();
msgRequest.Dimension = new  CuboidDimension();
                
msgRequest.ID = nID ;
msgRequest.Dimension.Length = nLength ;
msgRequest.Dimension.Width = nWidth ;
msgRequest.Dimension.Height = nHeight ; 

为 Message Response 对象分配空间。调用服务函数并将响应保存。

CuboidDetailResponse msgResponse = new  CuboidDetailResponse ();
msgResponse.SurfaceArea = objClient.CalculateDetails1 ( ref msgRequest.ID, msgRequest.Dimension, out msgResponse.Volume );

将响应输出显示到指定的控件。

lblMethod.Text = "<Font color=#0000ff>Method Called ..... : CalculateDetails1 </font>";
lblStatus.Text = "<font Color=#008000>Status : Success </font>";
lblArea.Text = "<Font color=#000080>Total Surface Area .. : " +  msgResponse.SurfaceArea.ToString ("F2") + "</font>" ;
lblVolume.Text = "<Font color=#000080>Total Volume .. : " +  msgResponse.Volume.ToString ("F2") + "</font>" ;           

关闭 try 块。

}

查看是否有任何错误(**CuboidFaultException**)来自服务。

catch (FaultException<CuboidFaultException> eX)
{
        lblMethod.Text = "<Font color=#ff0000>Method Called ..... : CalculateDetails1 </font>";
        StringBuilder sbErr = new StringBuilder ("<font Color=#ff0000>Status : Error ");
        sbErr .Append ( "<br>Reason   : " ); sbErr.Append ( eX.Detail.Reason );
        sbErr .Append ( "<br>Source   : " ); sbErr.Append ( eX.Detail.Source );
        sbErr .Append ( "<br>Details  : " ); sbErr.Append ( eX.Detail.Detail  );
        sbErr .Append ( "<br>HelpLink : " ); sbErr.Append ( eX.Detail.HelpLink );
        sbErr .Append ( "</Font>");
        lblStatus.Text = sbErr.ToString(); ;
        lblArea.Text = "" ;
        lblVolume.Text = ""; 
}
 解释

如果您收到了**CuboidFaultException**,它包含了客户端可能感兴趣的所有信息。这里您只需**在指定的标签上显示该信息**,该标签用于**状态**。

调用方法 CalculateDetails2  

当 button **btnCalc2** 被单击时,将调用此方法。这是其处理程序。在此事件中,我们只需解析通过 Web 页面控件输入的输入值,然后调用方法。以下是事件处理程序的分解。


protected void btnCalc2_Click(object sender, EventArgs e)
{
声明变量来保存不同的值,并解析页面控件中的输入(ID、长度、宽度和高度)。如果无效(输入了非数字值),则返回,允许输入 0 和 -1。
int nID=0, nLength=0, nWidth=0, nHeight=0;
if (ParseAndValidateInputs ( ref nID, ref nLength, ref nWidth, ref nHeight ) == false) return;
 您获得了允许的值,因此将其存储到会话中,这在回发时很有用,因为您可以在回发时用会话中存储的值初始化控件。
Session["ID"] = nID.ToString () ;
Session["Length"] = nLength .ToString ();
Session["Width"] = nWidth .ToString ();
Session["Height"] = nHeight.ToString () ;
 创建服务代理对象,如果对象创建失败,则返回;
CuboidServiceClient objClient = GetServiceClientObject();
if (objClient == null) return; 
一旦收集了输入,并且代理对象已准备好,就可以调用服务了。将所有内容包含在 try 块中,用于错误捕获,以及捕获服务抛出的**FaultException**。
try
{

将所有内容包含在 try 块中。 

    CuboidInfo dataRequest = new  CuboidInfo();
    dataRequest.Dimension = new  CuboidDimension();
    dataRequest.ID = nID;
    dataRequest.Dimension.Length = nLength;
    dataRequest.Dimension.Width = nWidth;
    dataRequest.Dimension.Height = nHeight;
 收集返回的对象。
    CuboidDetail dataResponse = new  CuboidDetail();
    dataResponse = objClient.CalculateDetails2(dataRequest);
 在指定的标签上显示返回对象中的值。
    lblMethod.Text = "<Font color=#0000ff>Method Called ..... : CalculateDetails2 </font>"
    lblArea.Text = "<Font color=#000080>Total Surface Area .. : " +   dataResponse.SurfaceArea.ToString("F2") + "</font>"; 
    lblVolume.Text = "<Font color=#000080>Total Volume .. : " + dataResponse.Volume.ToString("F2") + "</font>";
    lblStatus.Text = "<font Color=#008000>Status : Success </font>";
}  

关闭 try 块,并检查服务是否抛出了**CuboidFaultException**。如果是,则只需在指定用于状态显示的标签上显示 **CuboidFaultException** 的详细信息,并清除用于显示输出值(表面积、体积)的其他标签。

catch (FaultException< CuboidFaultException> eX)
{
    lblMethod.Text = "<Font color=#ff0000>Method Called ..... : CalculateDetails2 </font>";
    StringBuilder sbErr = new StringBuilder("<font Color=#ff0000>Status : Error "); 
    sbErr.Append("<br>Reason   : "); sbErr.Append(eX.Detail.Reason);
    sbErr.Append("<br>Source   : "); sbErr.Append(eX.Detail.Source);
    sbErr.Append("<br>Details  : "); sbErr.Append(eX.Detail.Detail);
    sbErr.Append("<br>HelpLink : "); sbErr.Append(eX.Detail.HelpLink);
    sbErr.Append("</Font>"); 
    lblArea.Text = "";     
    lblVolume.Text = ""; 
    lblStatus.Text = sbErr.ToString();
}

在指定的标签上显示 CuboidFaultException 的详细信息,并清除用于显示输出值(表面积、体积)的标签。

调用方法 CalculateDetails3   

当 button **btnCalc3** 被单击时,将调用此方法。这是其处理程序。在此事件中,我们只需解析通过 Web 页面控件输入的输入值,然后调用方法。以下是事件处理程序的分解。
protected void btnCalc3_Click(object sender, EventArgs e)
{
 声明变量来保存不同的值,并解析页面控件中的输入(ID、长度、宽度和高度)。如果无效(输入了非数字值),则返回,允许输入 0 和 -1)。
int nID=0, nLength=0, nWidth=0, nHeight=0;
if (ParseAndValidateInputs ( ref nID, ref nLength, ref nWidth, ref nHeight ) == false) return;
您获得了允许的值,因此将其存储到会话中,这在回发时很有用,因为您可以在回发时用会话中存储的值初始化控件。
Session["ID"] = nID.ToString () ;
Session["Length"] = nLength .ToString ();
Session["Width"] = nWidth .ToString ();
Session["Height"] = nHeight.ToString () ;
 创建服务代理对象,如果对象创建失败,则返回;
CuboidServiceClient objClient = GetServiceClientObject();
if (objClient == null) return;

一旦收集了输入,并且代理对象已准备好,就可以调用服务了。将所有内容包含在 try 块中,用于错误捕获,以及捕获服务抛出的**FaultException**。

try
{ 

创建 CuboidDimension 对象,将输入值分配给该对象,以作为输入参数发送。

CuboidDimension dimData = new CuboidDimension(); 
dimData.Length = nLength; 
dimData.Width = nWidth; 
dimData.Height = nHeight;

创建对象以从服务返回的对象中收集值。

CuboidDetail dataResponse = new CuboidDetail(); 
dataResponse = objClient.CalculateDetails3(nID, dimData);

将输出显示在指定的标签上。

lblMethod.Text = "<Font color=#0000ff>Method Called ..... : CalculateDetails3 </font>"; 
lblArea.Text = "<Font color=#000080>Total Surface Area .. : " +   dataResponse.SurfaceArea.ToString("F2") + "</font>";
lblVolume.Text = "<Font color=#000080>Total Volume .. : " + dataResponse.Volume.ToString("F2") + "</font>"; 
lblStatus.Text = "<font Color=#008000>Status : Success </font>";

关闭 try 块。

}

在发生服务端错误的情况下处理 CuboidFaultException,并将 CuboidFaultExcepti 的字段值以红色显示在指定为状态的标签上。

catch (FaultException<CuboidFaultException> eX) 
{ 
    lblMethod.Text = "<Font color=#ff0000>Method Called ..... : CalculateDetails3 </font>";
    StringBuilder sbErr = new StringBuilder("<font Color=#ff0000>Status : Error "); 
    sbErr.Append("<br>Reason   : ");
    sbErr.Append(eX.Detail.Reason);
    sbErr.Append("<br>Source   : ");
    sbErr.Append(eX.Detail.Source);
    sbErr.Append("<br>Details  : "); 
    sbErr.Append(eX.Detail.Detail);
    sbErr.Append("<br>HelpLink : "); 
    sbErr.Append(eX.Detail.HelpLink);
    sbErr.Append("</Font>"); 
    lblArea.Text = "";
    lblVolume.Text = "";
    lblStatus.Text = sbErr.ToString();
}

输出  

以下是 CuboidService 的基于 Web 的客户端的输出:

当所有输入均有效,并且调用了 CalculateDetails1。

当所有输入均有效,并且调用了 CalculateDetails2。

当所有输入均有效,并且调用了 CalculateDetails3。

当一个或多个输入无效,并且调用了 CalculateDetails1。

当所有输入均有效,并且调用了 CalculateDetails2。

当所有输入均有效,并且调用了 CalculateDetails3。

关注点   

在本文中,我以一种非常简单的方式描述了所有三种合同类型,我还解释了每种合同类型的适用性。什么时候应该使用它们,我希望您会像我一样享受阅读这篇文章。

非常感谢您的阅读。

历史

初始版本。

© . All rights reserved.