WCF服务编程 读书笔记——第2章 服务契约

操作重载
诸如 C++ 和 C# 等编程语言都支持方法重载,即允许具有相同名称的两个方法可以定义不同的参数。例如,如下的 C# 接口就是有效的定义:

interface ICalculator
{
    int Add(int arg1,int arg2);
    double Add(double arg1,double arg2);
}

然而,基于 WSDL 的操作却不支持操作重载。因此,在编译如下的契约定义时,装载服
务宿主就会抛出 InvalidOperationException异常:

// 无效的契约定义:
[ServiceContract]
interface ICalculator
{
    [OperationContract]
    int Add(int arg1,int arg2);
    [OperationContract]
    double Add(double arg1,double arg2);
}

但是,我们可以手动地启用操作重载。实现的窍门就是使用 OperationContract特性的Name 属性,为操作指定别名:

[AttributeUsage(AttributeTargets.Method)]
public sealed class OperationContractAttribute : Attribute
{
    public string Name
    {get;set;}
    // 更多成员
}

我们需要同时为服务与客户端的操作指定别名。在服务端,要为重载的操作提供唯一的标识名,如例 2-1 所示。
例 2-1:服务端的操作重载

    [ServiceContract]
    interface ICalculator
    {
        [OperationContract(Name = "AddInt")]
        int Add(int arg1, int arg2);
        [OperationContract(Name = "AddDouble")]
        double Add(double arg1, double arg2);
    }

当客户端导入契约并生成代理时,导入的操作就会包含定义的别名:

    [ServiceContract]
    public interface ICalculator
    {
        [OperationContract]
        int AddInt(int arg1, int arg2);
        [OperationContract]
        double AddDouble(double arg1, double arg2);
    }
    public partial class CalculatorClient : ClientBase<ICalculator>, ICalculator
    {
        public int AddInt(int arg1, int arg2)
        {
            return Channel.AddInt(arg1, arg2);
        }
        public double AddDouble(double arg1, double arg2)
        {
            return Channel.AddDouble(arg1, arg2);
        }
        // 代理的其余内容
    }

客户端虽然可以使用生成的代理和契约,但我们还需要进行修改,使客户端代码支持操作重载。方法是将导入的代理与契约的方法名修改为重载的名称,并确保代理类能够使用重载方法调用内部代理,例如:

public int Add(int arg1,int arg2)
{
  return Channel.Add(arg1,arg2);
}

最后,在客户端使用导入契约的 Name 属性,指定别名并重载方法,使它与导入的操作名保持一致,如例 2-2 所示。
例 2-2:客户端操作重载

    [ServiceContract]
    public interface ICalculator
    {
        [OperationContract(Name = "AddInt")]
        int Add(int arg1, int arg2);
        [OperationContract(Name = "AddDouble")]
        double Add(double arg1, double arg2);
    }
    public partial class CalculatorClient : ClientBase<ICalculator>, ICalculator
    {
        public int Add(int arg1, int arg2)
        {
            return Channel.Add(arg1, arg2);
        }
        public double Add(double arg, double arg2)
        {
            return Channel.Add(arg1, arg2);
        }
        //代理的其余内容
    }

现在,通过操作重载,客户端就能够提供更加自然与优雅的编程模型,具有良好的可读性:

CalculatorClient proxy = new CalculatorClient();
int result1 = proxy.Add(1,2);
double result2 = proxy.Add(1.0,2.0);
proxy.Close();

契约的继承
服务契约接口支持继承功能,我们可以定义一个契约层级。但是, ServiceContract特性却是不能继承的:

[AttributeUsage(Inherited = false,...)]
public sealed class ServiceContractAttribute : Attribute
{...}

因此,接口层级中的每级接口都必须显式的标记 ServiceContract 特性,如例 2-3 所示。
例 2-3:服务端契约层级

    [ServiceContract]
    interface ISimpleCalculator
    {
        [OperationContract]
        int Add(int arg1, int arg2);
    }
    [ServiceContract]
    interface IScientificCalculator : ISimpleCalculator
    {
        [OperationContract]
        int Multiply(int arg1, int arg2);
    }

至于一个契约层级的实现,一个单独的服务类能够实现整个契约层级,这与经典的C#编程完全一致:

    class MyCalculator : IScientificCalculator
    {
        public int Add(int arg1, int arg2)
        {
            return arg1 + arg2;
        }
        public int Multiply(int arg1, int arg2)
        {
            return arg1 * arg2;
        }
    }

宿主可以为契约层级最底层的接口公开一个单独的终结点:

  <service name = "MyCalculator">
    <endpoint
    address = "http://localhost:8001/MyCalculator/"
    binding = "basicHttpBinding"
    contract = "IScientificCalculator"/>
  </service>

客户端契约层级
当客户端导入一个服务终结点的元数据时,如果该终结点的契约属于接口层级的一部分,则生成的客户端契约将不再维持原来的层级关系。相反,它会取消层级,组成一个单独的契约,名称为终结点的契约名。这个单独的契约包含了层级中从上至下所有接口定义的操作。然而,如果使用perationContract特性中的 Action与 ResponseAction属性,那么导入的接口定义仍然可以保留原来定义每个操作的契约名。

    [AttributeUsage(AttributeTargets.Method)]
    public sealed class OperationContractAttribute : Attribute
    {
        public string Action
        { get; set; }
        public string ReplyAction
        { get; set; }
        // 更多成员
    }

最后,一个单独的代理类可以实现导入契约的所有方法。如果给定例 2-3 的定义,导入的契约以及生成的代理类如例 2-4 所示。
例 2-4:取消层级关系的客户端定义

    [ServiceContract]
    public interface IScientificCalculator
    {
        [OperationContract(Action = ".../ISimpleCalculator/Add",
        ReplyAction = ".../ISimpleCalculator/AddResponse")]
        int Add(int arg1, int arg2);
        [OperationContract(Action = ".../IScientificCalculator/Multiply",
        ReplyAction = ".../IScientificCalculator/MultiplyResponse")]
        int Multiply(int arg1, int arg2);
    }
    public partial class ScientificCalculatorClient :
    ClientBase<IScientificCalculator>, IScientificCalculator
    {
        public int Add(int arg1, int arg2)
        {...}
        public int Multiply(int arg1, int arg2)
        {...}
        //代理的其余内容
    }

恢复客户端层级
客户端可以手工修改代理以及导入契约的定义,恢复契约层级,如例 2-5 所示。
例 2-5:客户端契约层级

    [ServiceContract]
    public interface ISimpleCalculator
    {
        [OperationContract]
        int Add(int arg1, int arg2);
    }
    public partial class SimpleCalculatorClient : ClientBase<ISimpleCalculator>,
    ISimpleCalculator
    {
        public int Add(int arg1, int arg2)
        {
            return Channel.Add(arg1, arg2);
        }
        //代理的其余内容
    }
    [ServiceContract]
    public interface IScientificCalculator : ISimpleCalculator
    {
        [OperationContract]
        int Multiply(int arg1, int arg2);
    }
    public partial class ScientificCalculatorClient :
    ClientBase<IScientificCalculator>, IScientificCalculator
    {
        public int Add(int arg1, int arg2)
        {
            return Channel.Add(arg1, arg2);
        }
        public int Multiply(int arg1, int arg2)
        {
            return Channel.Multiply(arg1, arg2);
        }
        //代理的其余内容
    }

在不同的操作上使用Action属性值,客户端可以分解服务契约层级中合成契约的定义,提供接口与代理的定义。如例2-5中的ISimpleCalculator和SimpleCalculatorClient。在该例中,并不需要设置 Action和ResponseAction属性值,我们完全可以移除它们。然后,手动地将接口添加到客户端所需要的接口链中:

[ServiceContract]
public interface IScientificCalculator : ISimpleCalculator
{...}

尽管服务可能已经为层级中最底层的接口公开了一个单独的终结点,客户端仍然可以将它看作是相同地址的不同终结点,每个终结点对应契约层级的不同层:

  <client>
    <endpoint name = "SimpleEndpoint"
    address = "http://localhost:8001/MyCalculator/"
    binding = "basicHttpBinding"
    contract = "ISimpleCalculator"
/>
    <endpoint name = "ScientificEndpoint"
    address = "http://localhost:8001/MyCalculator/"
    binding = "basicHttpBinding"
    contract = "IScientificCalculator"
/>
  </client>

现在,客户端可以编写如下代理,充分地利用契约层级的优势:

SimpleCalculatorClient proxy1 = new SimpleCalculatorClient();
proxy1.Add(1,2);
proxy1.Close();
ScientificCalculatorClient proxy2 = new ScientificCalculatorClient();
proxy2.Add(3,4);
proxy2.Multiply(5,6);
proxy2.Close();

例 2-5 对代理的分解,解除了契约中每一层级之间的依赖,实现了契约层级的解耦。在客户端代码中,凡是希望使用 IScientificCalculator引用的,都可以指派为IScientificCalculator 类型的引用:

void UseCalculator(ISimpleCalculator calculator)
{...}
ISimpleCalculator proxy1 = new SimpleCalculatorClient();
ISimpleCalculator proxy2 = new ScientificCalculatorClient();
IScientificCalculator proxy3 = new ScientificCalculatorClient();
SimpleCalculatorClient proxy4 = new SimpleCalculatorClient();
ScientificCalculatorClient proxy5 = new ScientificCalculatorClient();
UseCalculator(proxy1);
UseCalculator(proxy2);
UseCalculator(proxy3);
UseCalculator(proxy4);
UseCalculator(proxy5);

但是,代理之间并不存在 IS-A 关系。即使 IScientificCalculator 接口派生自ISimpleCalculator 接口,也不能认为代理类 ScientificCalculatorClient 就是SimpleCalculatorClient类型。此外,我们必须为子契约重复实现代理中的基契约。调整的办法是使用所谓的代理链( Proxy Chaining)技术,如例 2-6 所示。
例 2-6:代理链

    public partial class SimpleCalculatorClient : ClientBase<IScientificCalculator>,
    ISimpleCalculator
    {
        public int Add(int arg1, int arg2)
        {
            return Channel.Add(arg1, arg2);
        }
        //代理的其余内容
    }
    public partial class ScientificCalculatorClient : SimpleCalculatorClient,
    IScientificCalculator
    {
        public int Multiply(int arg1, int arg2)
        {
            return Channel.Multiply(arg1, arg2);
        }
        //代理的其余内容
    }

只有实现了最顶层的基契约的代理直接继承于ClientBase<T>,提供的类型参数则为最底层的子接口类型。所有的其他代理则直接继承于它们的上一级代理,同时实现各自的契约接口。代理链为代理建立了 IS-A 关系,保证了代码的重用。在客户端代码中,凡是希望使用SimpleCalculatorClient引用的,都可以指派为 ScientificCalculatorClient类型的引用:

void UseCalculator(SimpleCalculatorClient calculator)
{...}
SimpleCalculatorClient proxy1 = new SimpleCalculatorClient();
SimpleCalculatorClient proxy2 = new ScientificCalculatorClient();
ScientificCalculatorClient proxy3 = new ScientificCalculatorClient();
UseCalculator(proxy1);
UseCalculator(proxy2);
UseCalculator(proxy3);

服务契约的分解与设计
如果不考虑语法因素,我们应该如何设计服务契约?如何知道服务契约中应该定义哪些操作?每个契约又应该包含多少操作?解决这些问题与 WCF 技术并无太大关系,更多地属于抽象的面向服务分析与设计的范畴。如何将系统分解为服务,以及如何剖析契约方法,并不在本书讨论范围之内。不过,本节仍然给出了一些建议,以指导开发者更好地设计服务契约。

契约分解
一个服务契约是逻辑相关的操作的组合。所谓的“逻辑相关”通常指特定的领域逻辑。我们可以将服务契约想象成实体的不同表现。一旦识别(在需求分析之后)出实体支持的所有操作,就需要将它们分配给契约。这称为服务契约的分解( Service Contract Factoring)。分解服务契约时,通常需要考虑可重用元素( Reusable Element)。在面向服务的应用程序中,一个可重用的基本单元就是服务契约。那么,系统的其他实体能否重用这些被分解出的服务契约?实体对象的哪些职责能够被分解出来,哪些职责又能被其他实体所调用?

让我们考虑一个具体而又简单的实例。假定我们希望对一个狗的服务建模。需求说明狗能叫能吃,拥有一个兽医诊所的注册号,可以对它注射疫苗。我们可以定义一个 IDog服务契约,并让不同的服务如 PoodleService(狮子狗)和 GermanShepherdService(德国牧羊犬)实现 IDog 契约:

[ServiceContract]
interface IDog
{
    [OperationContract]
    void Fetch();
    [OperationContract]
    void Bark();
    [OperationContract]
    long GetVetClinicNumber();
    [OperationContract]
    void Vaccinate();
}
class PoodleService : IDog
{...}
class GermanShepherdService : IDog
{...}

然而, IDog 服务契约的定义并没有体现职责分离的原则。虽然这些操作都是狗所应具有的,但是 Fetch()和 Bark()方法与 IDog 服务契约的逻辑关联性,远远强于GetVetClinicNumber()和Vaccinate()方法。Fetch()和Bark()体现了狗的本性,与它的日常生活有关,属于实例化的犬类实体的职责。 GetVetClinicNumber()和Vaccinate()则体现了不同的特性,它们与兽医诊所的宠物记录有关。 一个最佳方案是将 GetVetClinicNumber()和 Vaccinate()操作分解出来,形成一个单独的 IPet 契约:

    [ServiceContract]
    interface IPet
    {
        [OperationContract]
        long GetVetClinicNumber();
        [OperationContract]
        void Vaccinate();
    }
    [ServiceContract]
    interface IDog
    {
        [OperationContract]
        void Fetch();
        [OperationContract]
        void Bark();
    }

由于宠物的职责不依赖于犬类实体,因此其他实体(例如猫)可以重用以及实现 IPet
服务契约:

[ServiceContract]
interface ICat
{
    [OperationContract]
    void Purr();
    [OperationContract]
    void CatchMouse();
}
class PoodleService : IDog,IPet
{...}
class SiameseService : ICat,IPet
{...}

契约的分解实现了应用程序中诊所管理职责与实际服务(狗或者猫)之间的解耦。将操作分解为单独的接口,是服务设计中常见的做法,它能够降低操作之间的逻辑关系。但是,有时候在几个不相关的契约中会找到相同的操作,这些操作与它们各自的契约存在一定的逻辑关系。例如,猫和狗这两种动物都会脱毛,都能够哺育后代。从逻辑上讲,脱毛与犬吠一样,都属于狗的服务操作;同时它又与猫叫一样,属于猫的服务操作。此时,我们将服务契约分解为契约层级的方式,而不是单独的契约:

[ServiceContract]
interface IMammal
{
    [OperationContract]
    void ShedFur();
    [OperationContract]
    void Lactate();
}
[ServiceContract]
interface IDog : IMammal
{...}
[ServiceContract]
interface ICat : IMammal
{...}

分解准则
显而易见,合理的契约分解可以实现深度特化、松散耦合、精细调整以及契约的重用。这些优势有助于改善整个系统。总的来说,契约分解的目的就是使契约包含的操作尽可能少。
设计面向服务的系统时,需要平衡两个影响系统的因素。一个是实现服务契约的代价,一个则是将服务契约合并或集成为一个高内聚应用程序的代价。

如果我们定义了太多的细粒度服务契约,虽然它们易于实现,但集成它们的代价未免太高。另一方面,如果我们仅定义了几个复杂而又庞大的服务契约,虽然集成的代价可能会降低,但却制约了契约的实现。

实现契约的代价与服务契约的规模并非线性的关系,当契约的规模增加两倍时,复杂度会陡增至四到六倍。与之相似,集成契约的代价与服务契约的数量同样不是线性关系,因为参与的服务数与它们之间关联点的数目不是线形的。

对于任何一个系统,实现契约所付出的代价,包括设计服务以及维护服务的代价,等于上述两个因素的总和(实现的代价与集成的代价)。 图 2-1的一个区域显示了最小代价与服务契约规模和数量之间的关系。一个设计良好的系统,服务的个数与规模应该恰如其分,遵循平衡的“中庸之道”,力求达到“增之一分则太多(大),减之一分则太少(小)”的标准。

由于契约分解与使用的服务技术无关,对于职责分离以及大规模应用程序的架构设计,我们只能根据自己或他人的经验,总结出关于服务契约分解的规则和方法,与读者分享。

首先,我们应该避免设计只具有一个操作的服务契约。一个服务契约体现了实体的特征,如果服务只有一个操作,则过于单调,没有实际的意义。此时,就应该检查它是否使用了太多的参数?它的粒度是否过粗,因此需要分解为多个操作?是否需要将该操作转移到已有的服务契约中?

服务契约成员的最佳数量(根据经验总结,仅代表本人观点)应介于 3 到 5 之间。如果设计的服务契约包含了多个操作,例如 6 到 9 个,仍然可能工作良好。但是,我们需要判断这些操作会否因为过度分解而需要合并。如果服务契约定义了 12 个甚至更多的操作,毫无疑问,我们需要将这些操作分解到单独的服务契约中,或者为它们建立契约层级。 开发者在制订 WCF编码规范时, 应该指定一个上限值(例如 20)。 无论在何种情况,都不能超过该值。

另一个原则是关于准属性操作( Property-Like Operation)的使用,例如:
  [OperationContract]
  long GetVetClinicNumber();

我们应该避免定义这样的操作。服务契约允许客户端在调用抽象操作时,不用关心具体的实现细节。准属性操作由于无法封装状态的管理,因此在封装性的表现上差强人意。在服务端,我们可以封装读写变量值的业务逻辑,但在理想状态下,我们却不应该干涉客户端对属性的使用。客户端应该只负责调用操作,而由服务去管理服务对象的状态。这种交互方式应该被表示为 DoSomething()样式,例如 Vaccinate()方法。服务如何实现该方法,是否需要设置诊所号,都不是客户端需要考虑的内容。

需要注意的是,这些分解原则,包括经验法则与通用规律,只能作为帮助开发者核算和评估特定设计的工具。它不能替代领域专家的意见与经验。“实践出真知”,应用这些指导原则时,需要做出合理的判断,甚至提出质问。

契约查询
有时候,客户端需要通过编程方式验证一个特定的终结点(通过地址进行识别)是否支持一个特定的契约。设想有这样一个应用程序,终端用户在安装时(甚至在运行时)指定或配置应用程序,用以使用服务并与服务交互。如果服务不支持所需的契约,应用程序就会向用户发出警告,提示配置的地址是无效的,询问是否更正地址或替换地址。例如,第 10 章使用的证书管理器应用程序( Credentials Manager Application)就具备这样的特征:用户需要为应用程序提供管理账户成员与角色的安全证书服务的地址。在验证了地址支持所需的服务契约之后,证书管理器只允许用户选择有效的地址。

编程处理元数据
为了支持这一功能,应用程序需要获取服务终结点的元数据,查看是否存在至少一个终结点支持请求的契约。正如第 1 章阐释的那样,如果元数据交换终结点是服务支持的,或者基于HTTP-GET协议,那么元数据在这个终结点中就是可用的。 当我们使用 HTTP-GET 协议时,元数据交换的地址就是 HTTP-GET 地址(通常,服务的基地址以?wsdl为后缀) 。为了简化对返回元数据的解析工作, WCF 提供了几个辅助类,位于System.ServiceModel.Description 命名空间,如例 2-7 所示。
例 2-7:支持元数据处理的类型

public enum MetadataExchangeClientMode
{
    MetadataExchange,
    HttpGet
}
class MetadataSet : ...
{...}
public class ServiceEndpointCollection : Collection<ServiceEndpoint>
{...}
public class MetadataExchangeClient
{
    public MetadataExchangeClient();
    public MetadataExchangeClient(Binding mexBinding);
    public MetadataSet GetMetadata(Uri address,MetadataExchangeClientMode mode);
    //更多成员
}
public abstract class MetadataImporter
{
    public abstract ServiceEndpointCollection ImportAllEndpoints();
    //更多成员
}
public class WsdlImporter : MetadataImporter
{
    public WsdlImporter(MetadataSet metadata);
    //更多成员
}
public class ServiceEndpoint
{
    public EndpointAddress Address
    {get;set;}
    public Binding Binding
    {get;set;}
    public ContractDescription Contract
    {get;}
    //更多成员
}
public class ContractDescription
{
    public string Name
    {get;set;}
    public string Namespace
    {get;set;}
    //更多成员
}

MetadataExchangeClient能够使用与元数据交换关联的绑定,该元数据交换保存在应用程序的配置文件中。我们也可以将初始化后的绑定实例传递给 MetadataExchangeClient的构造函数。传递的绑定实例包含一些自定义值,例如容量。如果返回的元数据超过默认的接收消息大小时,为了接收更大的消息,就可以设置容量值。MetadataExchangeClient 的 GetMetadata()方法接收一个终结点地址实例,它封装了元数据交换地址以及一个枚举值,指定了访问的方式。方法返回的元数据放在一个 MetadataSet 实例中。我们不需要直接操作 MetadataSet 类型,而是创建MetadataImporter类的子类实例,例如 WsdlImporter,将原来的元数据传递给它的构造函数,然后调用 ImportAllEndpoints()方法,获取在元数据中查找到的所有终结点的集合。终结点以 ServiceEndpoint 类型方式表示。

ServiceEndpoint 定义了 ContractDescription 类型属性 Contract。 ContractDescription类定义了契约的名称与命名空间。
使用HTTP-GET时,为了判断配置的基地址是否支持特定的契约, 通过刚才描述的步骤就能够生成终结点的集合。遍历集合的每一个终结点,比较请求契约中 ContractDescription 的 Name 和 Namespace 属性值,如例 2-8 所示。
例 2-8:查询契约的地址

        bool contractSupported = false;
        string mexAddress = "...?WSDL";
        MetadataExchangeClient MEXClient = new MetadataExchangeClient(new Uri(mexAddress),
        MetadataExchangeClientMode.HttpGet);
        MetadataSet metadata = MEXClient.GetMetadata();
        MetadataImporter importer = new WsdlImporter(metadata);
        ServiceEndpointCollection endpoints = importer.ImportAllEndpoints();
        foreach(ServiceEndpoint endpoint in endpoints)
        {
            if(endpoint.Contract.Namespace == "MyNamespace" &&
                endpoint.Contract.Name == "IMyContract")
            {
                contractSupported = true;
                break;
            }
        }

注意: 第 1 章提到的元数据浏览器工具采用的步骤与例 2-8 获取服务终结点的步骤相似。如果给定一个基于 HTTP 的地址,工具会同时尝试使用 HTTP-GET和基于 HTTP 的元数据交换终结点。元数据浏览器也能够使用基于 TCP 或 IPC 的元数据交换终结点获取元数据。工具的大量实现都是用于处理元数据,以及显示元数据的内容,毕竟, WCF 提供的类很难获取和解析元数据。

MetadataHelper 类
我们将例 2-8 所示的步骤封装到了设计的通用静态工具类 MetadataHelper()的
QueryContract()方法中:

public static class MetadataHelper
{
  public static bool QueryContract(string mexAddress,Type contractType);
  public static bool QueryContract(string mexAddress,string contractNamespace,string contractName);
// 更多成员
}

可以为MetadataHelper类提供我们希望查询的契约类型, 或者提供该契约的名称与命名空间:
  string address = "...";
  bool contractSupported = MetadataHelper.QueryContract(address,typeof(IMyContract));
至于 QueryContract()方法中的元数据交换地址 mexAddress,我们可以提供带HTTP-GET 地址的 MetadataHelper 类,也可以提供基于 HTTP、 HTTPS、 TCP 或者IPC 的元数据交换终结点地址。例 2-9 演示了 MetadataHelper.QueryContract()方法的实现,它省略了错误处理的代码。
例 2-9: MetadataHelper.QueryContract()的实现

    public static class MetadataHelper
    {
        const int MessageMultiplier = 5;
        static ServiceEndpointCollection QueryMexEndpoint(string mexAddress,
        BindingElement bindingElement)
        {
            CustomBinding binding = new CustomBinding(bindingElement);
            MetadataExchangeClient MEXClient = new MetadataExchangeClient(binding);
            MetadataSet metadata = MEXClient.GetMetadata
            (new EndpointAddress(mexAddress));
            MetadataImporter importer = new WsdlImporter(metadata);
            return importer.ImportAllEndpoints();
        }
        public static ServiceEndpoint[] GetEndpoints(string mexAddress)
        {
        /* 一些错误处理 */
        Uri address = new Uri(mexAddress);
        ServiceEndpointCollection endpoints = null;
        if(address.Scheme == "net.tcp")
        {
        TcpTransportBindingElement tcpBindingElement =
        new TcpTransportBindingElement();
        tcpBindingElement.MaxReceivedMessageSize *= MessageMultiplier;
        endpoints = QueryMexEndpoint(mexAddress,tcpBindingElement);
        }
        if(address.Scheme == "net.pipe")
        {...}
        if(address.Scheme == "http") // 判断是否为 HTTP-GET
        {...}
        if(address.Scheme == "https") // 判断是否为 HTTPS-GET
        {...}
        return Collection.ToArray(endpoints);
        }
        public static bool QueryContract(string mexAddress, Type contractType)
        {
            if (contractType.IsInterface == false)
            {
                Debug.Assert(false, contractType + " is not an interface");
                return false;
            }
            object[] attributes = contractType.GetCustomAttributes(
            typeof(ServiceContractAttribute), false);
            if (attributes.Length == 0)
            {
                Debug.Assert(false, "Interface " + contractType +
                " does not have the ServiceContractAttribute");
                return false;
            }
            ServiceContractAttribute attribute = attributes[0] as
            ServiceContractAttribute;
            if (attribute.Name == null)
            {
                attribute.Name = contractType.ToString();
            }
            if (attribute.Namespace == null)
            {
                attribute.Namespace = "http://tempuri.org/";
            }
            return QueryContract(mexAddress, attribute.Namespace, attribute.Name);
        }
        public static bool QueryContract(string mexAddress, string contractNamespace, string contractName)
        {
            if (String.IsNullOrEmpty(contractNamespace))
            {
                Debug.Assert(false, "Empty namespace");
                return false;
            }
            if (String.IsNullOrEmpty(contractName))
            {
                Debug.Assert(false, "Empty name");
                return false;
            }
            try
            {
                ServiceEndpoint[] endpoints = GetEndpoints(mexAddress);
                foreach (ServiceEndpoint endpoint in endpoints)
                {
                    if (endpoint.Contract.Namespace == contractNamespace &&
                    endpoint.Contract.Name == contractName)
                    {
                        return true;
                    }
                }
            }
            catch
            { }
            return false;
        }
    }

在例 2-9 中, GetEndpoints()方法对元数据交换地址的样式进行了解析。根据找到的传输样式(例如 TCP), GetEndpoints()方法创建了一个需要使用的绑定元素,这样就可以设置它的 MaxReceivedMessageSize 属性值:

public abstract class TransportBindingElement : BindingElement
{
    public virtual long MaxReceivedMessageSize
    {get;set;}
}
public abstract class ConnectionOrientedTransportBindingElement :TransportBindingElement,...
{...}
public class TcpTransportBindingElement : ConnectionOrientedTransportBindingElement
{...}

MaxReceiveMessageSize的默认值为 64K。它适用于简单的服务。如果服务包含多个终结点,终结点又使用了复杂类型,就会生成更大的消息。此时,调用MetadataExchangeClient.GetMetadata()方法就会失败。 根据经验, 大多数情况下最合适的倍数因子是 5。接着, GetEndpoints()调用了 QueryMexEndpoint()私有方法,以获取元数据。 QueryMexEndpoint()接收元数据交换终结点的地址以及要使用的绑定元素。使用绑定元素是为了创建定制绑定,并将它提供给 MetadataExchangeClient实例。 MetadataExchangeClient实例能够获取元数据,返回终结点集合。但是, GetEndpoints()方法返回的终结点集合不是 ServiceEndpointCollection类型,而是使用了我们设计的 Collection 辅助类,返回了一个终结点数
组。
接收Type参数的QueryContract()方法首先会验证传入的Type类型是否是接口类型,如果是,则判断该接口是否标记了 ServiceContract 特性。因为 ServiceContract特性可以为契约的请求类型指定名称和命名空间的别名, QueryContract()使用这些值查询符合条件的契约。如果没有指定别名, QueryContract()方法则使用类型的名字与默认的命名空间http://tempuri.org,然后调用另一个重载版本的QueryContract()方法,它能够操作契约的名称和命名空间。该版本的 QueryContract()方法调用了GetEndpoints()方法,以获得终结点数组,然后遍历该数组。如果找到至少一个终结
点支持该契约,则返回 true。不管出现何种错误, QueryContract()方法都会返回false。
例 2-10 介绍了 MetadataHelper类定义的额外的查询元数据的方法。
例 2-10: MetadataHelper 类

public static class MetadataHelper
{
    public static ServiceEndpoint[] GetEndpoints(string mexAddress);
    public static string[] GetAddresses(Type bindingType,string mexAddress,
    Type contractType);
    public static string[] GetAddresses(string mexAddress,Type contractType);
    public static string[] GetAddresses(Type bindingType,string mexAddress,
    string contractNamespace,string contractName)
    where B: Binding;
    public static string[] GetAddresses(string mexAddress,string contractNamespace,
    string contractName);
    public static string[] GetContracts(Type bindingType,string mexAddress);
    public static string[] GetContracts(string mexAddress);
    public static string[] GetOperations(string mexAddress,Type contractType);
    public static string[] GetOperations(string mexAddress,
    string contractNamespace,
    string contractName);
    public static bool QueryContract(string mexAddress,Type contractType);
    public static bool QueryContract(string mexAddress,
    string contractNamespace,string contractName);
    //更多成员
}

在管理程序和管理工具中,或者在对契约进行设置时,往往需要这些强大而有效的功能。它们的实现都是基于对终结点数组的处理,终结点数组则是通过GetEndpoints()方法返回的。
GetAddresses()方法返回的终结点地址,要么支持一个特定的契约,要么就是使用特定绑定的终结点地址。
相似的, GetContracts()方法返回的所有契约,要么被所有终结点支持,要么就是使用了特定绑定的所有终结点支持的契约。最后, GetOperations()方法会返回一个特定契约的所有操作。

摘自:《WCF服务编程》Juval Louml著    张逸 徐宁 译

转载请注明出处:

作者:JesseLZJ
出处:http://jesselzj.cnblogs.com

时间: 2024-10-25 13:40:55

WCF服务编程 读书笔记——第2章 服务契约的相关文章

WCF服务编程 读书笔记——第1章 WCF基础(1)

第1章 WCF基础 本章主要介绍WCF的基本概念.构建模块以及WCF体系架构,以指导读者构建一个简单的WCF服务.从本章的内容中,我们可以了解到WCF的基本术语,包括地址(Address).绑定(Binding).契约(Contract)和终结点(Endpoint):了解如何托管服务,如何编写客户端代码:了解WCF的相关主题,诸如进程内托管(In-Proc Hosting)以及可靠性的实现.即使你已经熟知WCF的基本概念,仍然建议你快速浏览本章的内容,它不仅能够巩固你的已有知识,而且本章介绍的一

WCF服务编程 读书笔记——第1章 WCF基础(2)

续:第1章 WCF基础(1) 元数据交换 服务有两种方案可以发布自己的元数据.一种是基于HTTP-GET协议提供元数据, 另一种则是后面将要讨论的使用专门的终结点的方式.WCF能够为服务自动提供基于HTTPGET的元数据,但需要显式地添加服务行为( Behavior)以支持这一功能.本书后面的章节会介绍行为的相关知识.现在,我们只需要知道行为属于服务的本地特性,例如是否需要基于HTTP-GET交换元数据, 就是一种服务行为.我们可以通过编程方式或管理方式添加行为.在例 1 - 10 演示的宿主应

Windows核心编程读书笔记-第六章线程基础

1.相较于线程,进程所使用的系统资源更多.其原因在于地址空间.为一个进程创建一个虚拟的地址空间需要大量系统资源.线程只有一个内核对象和一个栈. 2.线程的入口函数 DWORD WINAPI ThreadFunc(PVOID pvParam){ DWORD dwResult = 0; ... return(dwResult); } 线程函数的几点说明 线程函数可以任意命名. 线程函数只有一个参数,而其意义由我们(而非操作系统)来定义.因此,我们不必担心ANSI/Unicode问题. 线程函数必须返

Windows核心编程读书笔记-第四章进程

1.进程组成 一个内核对象,操作系统用它来管理进程. 一个地址空间,其中包含所有可执行文件或DLL模块的代码和数据.此外,它还包含动态内存分配,比如线程堆栈和堆的分配. 2.一个进程可以有多个线程,所有线程都在进程的地址空间中"同时"执行代码.每个进程至少要有一个线程来执行进程地址空间包含的代码. 3.用Microsoft Visual Studio来创建一个应用程序项目时,集开发环境会设置各种链接器开关,使链接器将子系统的正确类型嵌入最终生成的可执行文件.对于CUI程序,这个链接器开

《Unix环境高级编程》读书笔记 第7章-进程环境

1. main函数 int main( int argc, char *argv[] ); argc是命令行参数的数目,包括程序名在内 argv是指向参数的各个指针所构成的数组,即指针数组 当内核执行C程序时(使用exec函数),在调用main前先调用一个特殊的启动例程.可执行程序文件将此启动例程指定为程序的起始地址——这是由连接器设置的,而连接器则是由C编译器调用.启动例程从内核取得命令行参数和环境变量值,然后按上述方式调用main函数做好安排. 2. 进程终止 有8种方式使进程终止,其中5种

《Unix环境高级编程》读书笔记 第3章-文件I/O

1. 引言 Unix系统的大多数文件I/O只需用到5个函数:open.read.write.lseek以及close 本章描述的函数经常被称为不带缓冲的I/O.术语不带缓冲指的是在用户的进程中对其不会自动缓冲,每个read和write都调用内核中的一个系统调用.但是,所有磁盘I/O都要经过内核的块缓存区(也称为内核的缓冲区高速缓存).唯一例外的是对原始磁盘设备的I/O. 2. 文件描述符 对于内核而言,所有打开的文件都通过文件描述符引用.文件描述符是一个非负整数,其变化范围是0~OPEN_MAX

Java编程思想第四版读书笔记——第十三章 字符串

Java编程思想第四版读书笔记--第十三章 字符串 字符串的操作是计算机程序设计中最常见的行为. 关键词: StringBuilder ,StringBuffer,toString(),format转换,正则表达式, 1.不可变String String对象时不可变的.每当把String对象作为方法的参数时,都会复制一份引用.(其实就是对函数中参数列表中参数的操作不会影响外面的原参数) 如下: import static net.mindview.util.Print.*; public cla

C++Windows核心编程读书笔记

转自:http://www.makaidong.com/%E5%8D%9A%E5%AE%A2%E5%9B%AD%E6%96%87/71405.shtml "C++Windows核心编程读书笔记": 关键词:c++windows 核心 编程 读书笔记 这篇笔记是我在读<windows核心编程>第5版时做的记录和总结(部分章节是第4版的书),没有摘抄原句,包含了很多我个人的思考和对实现的推断,因此不少条款和windows实际机制可能有出入,但应该是合理的.开头几章由于我追求简洁

MySQL Cookbook读书笔记第5章

1,字符串属性 查看系统拥有那些字符集: 若需要来自多种语言存放到同一列中,会考虑Unicode字符集(utf8或ucs2),只有它能表示多语言的字符 有些字符集支持多字节,有些只包含单字节,判断是否支持多字节的方法是对比Length()h和char_length函数的返回值来判定字符串中是否有多字节.例如使用ucs2的字节长度为6,字符数目为3. 另外虽然Unicode字符集utf8包含多字节字符,但是一个具体的字符串有可能只包含单字节字 非二进制字符串另一特征是collation,决定字符集