消息检查器

MessageInspectors 示例演示如何实现和配置客户端和服务消息检查器。

消息检查器是一个扩展性对象,可以在服务模型的客户端运行时和调度运行时中进行编程或通过配置使用,并且能够在消息接收后或发送前对消息进行检查和更改。

此示例实现一个基本的客户端和服务消息验证机制,该机制针对一组可配置的 XML 架构文档验证传入消息。 请注意,本示例并不验证每个操作的消息。 这是一种有意的简化。

消息检查器

客户端消息检查器实现 IClientMessageInspector 接口和服务消息检查器实现 IDispatchMessageInspector 接口。 这些实现可以组合成单个类,形成适用于双方的消息检查器。 此示例实现这样的组合消息检查器。 检查器是使用一组架构构造的,这些架构用于对传入和传出的消息进行验证,并允许开发人员指定是否验证传入或传出的消息,以及检查器是在调度模式还是客户端模式下运行,这会影响本主题后面讨论的错误处理。

public class SchemaValidationMessageInspector : IClientMessageInspector, IDispatchMessageInspector
{
    XmlSchemaSet schemaSet;
    bool validateRequest;
    bool validateReply;
    bool isClientSide;
    [ThreadStatic]
    bool isRequest;

    public SchemaValidationMessageInspector(XmlSchemaSet schemaSet,
         bool validateRequest, bool validateReply, bool isClientSide)
    {
        this.schemaSet = schemaSet;
        this.validateReply = validateReply;
        this.validateRequest = validateRequest;
        this.isClientSide = isClientSide;
    }

任何服务(调度程序)消息检查器都必须实现这两 IDispatchMessageInspector 种方法 AfterReceiveRequestBeforeSendReply(Message, Object)

当通道堆栈接收到消息、处理消息并将消息分配给服务后且在将消息反序列化并调度到操作之前,调度程序将调用 AfterReceiveRequest。 如果传入消息已加密,则消息在到达消息检查器时已解密。 该方法获取 request 作为引用参数传递的消息,该参数允许根据需要检查、作或替换消息。 返回值可以是任何对象,并用作在服务返回对当前消息的答复时传递给 BeforeSendReply 的关联状态对象。 在此示例中, AfterReceiveRequest 将消息的检查(验证)委托给专用本地方法 ValidateMessageBody ,并且不返回相关状态对象。 此方法可确保不会将无效消息传递到服务中。

object IDispatchMessageInspector.AfterReceiveRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel, System.ServiceModel.InstanceContext instanceContext)
{
    if (validateRequest)
    {
        // inspect the message. If a validation error occurs,
        // the thrown fault exception bubbles up.
        ValidateMessageBody(ref request, true);
    }
    return null;
}

BeforeSendReply(Message, Object) 每当已准备好将回复发送回客户端或处理单向消息时调用。 这允许对称地调用所需的扩展,而不管 MEP 如何。 与该 AfterReceiveRequest消息一样,消息作为引用参数传递,可以检查、修改或替换消息。 此示例中执行的消息的验证再次委托给 ValidMessageBody 该方法,但在这种情况下,验证错误的处理略有不同。

如果服务上发生验证错误,ValidateMessageBody 方法将抛出由 FaultException 派生的异常。 在 AfterReceiveRequest 中,这些异常可以放入服务模型基础结构中,自动转换为 SOAP 错误,并中继到客户端。 在BeforeSendReply中,FaultException异常不应置于基础设施中,因为服务抛出的故障异常在消息检查器被调用之前已经发生转换。 因此,以下实现捕获已知 ReplyValidationFault 异常,并将回复消息替换为显式错误消息。 此方法可确保服务实现不会返回无效消息。

void IDispatchMessageInspector.BeforeSendReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
{
    if (validateReply)
    {
        // Inspect the reply, catch a possible validation error
        try
        {
            ValidateMessageBody(ref reply, false);
        }
        catch (ReplyValidationFault fault)
        {
            // if a validation error occurred, the message is replaced
            // with the validation fault.
            reply = Message.CreateMessage(reply.Version,
                    fault.CreateMessageFault(), reply.Headers.Action);
        }
    }

客户端消息检查器非常相似。 必须从 IClientMessageInspector 中实现的两种方法是 AfterReceiveReplyBeforeSendRequest

BeforeSendRequest 当消息由客户端应用程序或操作格式化程序撰写时将被调用。 与调度程序消息检查器一样,只需检查或完全替换该消息。 在本示例中,检查器向同一本地 ValidateMessageBody 帮助器方法进行委托,该方法也可用于调度消息检查器。

客户端和服务验证(如构造函数中指定的)之间的行为差异是,客户端验证会引发本地异常,这些异常因本地发生,而不是由于服务故障而放入用户代码中。 一般规则是服务调度程序检查器引发错误,而客户端检查器引发异常。

BeforeSendRequest 实现可确保不会向服务发送无效消息。

object IClientMessageInspector.BeforeSendRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel)
{
    if (validateRequest)
    {
        ValidateMessageBody(ref request, true);
    }
    return null;
}

AfterReceiveReply 实现可确保不会将从服务接收的无效消息中继到客户端用户代码。

void IClientMessageInspector.AfterReceiveReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
{
    if (validateReply)
    {
        ValidateMessageBody(ref reply, false);
    }
}

此特定消息检查器的核心是 ValidateMessageBody 方法。 为了执行工作,检查器会在所传递消息的正文内容子树周围包装一个验证 XmlReader。 将用消息检查器包含的一组架构填充该读取器,并会将验证回调设置为引用与此方法并排定义的 InspectionValidationHandler 的委托。 为了执行验证,将随后读取消息并后台处理到受内存流支持的 XmlDictionaryWriter 中。 如果在进程中出现验证错误或警告,则会调用回调方法。

如果未发生错误,则会构造一条新消息,复制原始消息的属性和标头。然后,在内存流中使用经过验证的信息集,并将其包装在 XmlDictionaryReader 中,添加到替换消息中。

void ValidateMessageBody(ref System.ServiceModel.Channels.Message message, bool isRequest)
{
    if (!message.IsFault)
    {
        XmlDictionaryReaderQuotas quotas =
                new XmlDictionaryReaderQuotas();
        XmlReader bodyReader =
            message.GetReaderAtBodyContents().ReadSubtree();
        XmlReaderSettings wrapperSettings =
                              new XmlReaderSettings();
        wrapperSettings.CloseInput = true;
        wrapperSettings.Schemas = schemaSet;
        wrapperSettings.ValidationFlags =
                                XmlSchemaValidationFlags.None;
        wrapperSettings.ValidationType = ValidationType.Schema;
        wrapperSettings.ValidationEventHandler += new
           ValidationEventHandler(InspectionValidationHandler);
        XmlReader wrappedReader = XmlReader.Create(bodyReader,
                                            wrapperSettings);

        // pull body into a memory backed writer to validate
        this.isRequest = isRequest;
        MemoryStream memStream = new MemoryStream();
        XmlDictionaryWriter xdw =
              XmlDictionaryWriter.CreateBinaryWriter(memStream);
        xdw.WriteNode(wrappedReader, false);
        xdw.Flush(); memStream.Position = 0;
        XmlDictionaryReader xdr =
        XmlDictionaryReader.CreateBinaryReader(memStream, quotas);

        // reconstruct the message with the validated body
        Message replacedMessage =
            Message.CreateMessage(message.Version, null, xdr);
        replacedMessage.Headers.CopyHeadersFrom(message.Headers);
        replacedMessage.Properties.CopyProperties(message.Properties);
        message = replacedMessage;
    }
}

每当发生架构验证错误或警告时,验证 InspectionValidationHandler 就会调用 XmlReader 方法。 以下实现仅处理错误并忽略所有警告。

初步考虑时,似乎可以使用消息检查器将验证 XmlReader 注入消息中,并在处理消息时实现验证,而无需缓冲消息。 但是,这意味着此回调会在服务模型基础结构或用户代码中引发验证异常,因为检测到无效的 XML 节点,从而导致不可预知的行为。 缓冲方法完全保护用户代码免受无效消息的防护。

如前所述,处理程序引发的异常在客户端和服务之间有所不同。 在服务中,异常派生自 FaultException客户端上的异常是常规自定义异常。

        void InspectionValidationHandler(object sender, ValidationEventArgs e)
{
    if (e.Severity == XmlSeverityType.Error)
    {
        // We are treating client and service side validation errors
        // differently here. Client side errors cause exceptions
        // and are thrown straight up to the user code. Service side
        // validations cause faults.
        if (isClientSide)
        {
            if (isRequest)
            {
                throw new RequestClientValidationException(e.Message);
            }
            else
            {
                throw new ReplyClientValidationException(e.Message);
            }
        }
        else
        {
            if (isRequest)
            {
                // this fault is caught by the ServiceModel
                // infrastructure and turned into a fault reply.
                throw new RequestValidationFault(e.Message);
             }
             else
             {
                // this fault is caught and turned into a fault message
                // in BeforeSendReply in this class
                throw new ReplyValidationFault(e.Message);
              }
          }
      }
    }

行为

消息检查器是客户端运行时或调度运行时的扩展。 此类扩展是使用 行为配置的。 行为是一个类,它通过更改默认配置或向其添加扩展(如消息检查器)来更改服务模型运行时的行为。

以下 SchemaValidationBehavior 类是用于将此示例的消息检查器添加到客户端或调度运行时的行为。 在这两种情况下,实施方法相当简单。 在ApplyClientBehaviorApplyDispatchBehavior中,消息检查器会被创建并添加到相应运行时的MessageInspectors集合中。

public class SchemaValidationBehavior : IEndpointBehavior
{
    XmlSchemaSet schemaSet;
    bool validateRequest;
    bool validateReply;

    public SchemaValidationBehavior(XmlSchemaSet schemaSet, bool
                           inspectRequest, bool inspectReply)
    {
        this.schemaSet = schemaSet;
        this.validateReply = inspectReply;
        this.validateRequest = inspectRequest;
    }
    #region IEndpointBehavior Members

    public void AddBindingParameters(ServiceEndpoint endpoint,
       System.ServiceModel.Channels.BindingParameterCollection
                                            bindingParameters)
    {
    }

    public void ApplyClientBehavior(ServiceEndpoint endpoint,
            System.ServiceModel.Dispatcher.ClientRuntime clientRuntime)
    {
        SchemaValidationMessageInspector inspector =
           new SchemaValidationMessageInspector(schemaSet,
                      validateRequest, validateReply, true);
            clientRuntime.MessageInspectors.Add(inspector);
    }

    public void ApplyDispatchBehavior(ServiceEndpoint endpoint,
         System.ServiceModel.Dispatcher.EndpointDispatcher
                                          endpointDispatcher)
    {
        SchemaValidationMessageInspector inspector =
           new SchemaValidationMessageInspector(schemaSet,
                        validateRequest, validateReply, false);
   endpointDispatcher.DispatchRuntime.MessageInspectors.Add(inspector);
    }

   public void Validate(ServiceEndpoint endpoint)
   {
   }

    #endregion
}

注释

此特定行为不能充当属性,因此不能声明性地添加到服务类型的合同类型中。 这是一个设计决策,因为架构集合不能加载在属性声明中,并引用此属性中的额外配置位置(例如应用程序设置),这意味着创建与服务模型配置其余部分不一致的配置元素。 因此,只能通过代码和服务模型配置扩展来强制添加此行为。

通过配置添加消息检查器

若要在应用程序配置文件中的终结点上配置自定义行为,服务模型要求实现者创建由派生自的类表示的配置BehaviorExtensionElement。 然后,必须将此扩展添加到服务模型的配置部分,以便扩展,如本节中讨论的以下扩展所示。

<system.serviceModel>
…
   <extensions>
      <behaviorExtensions>
        <add name="schemaValidator" type="Microsoft.ServiceModel.Samples.SchemaValidationBehaviorExtensionElement, MessageInspectors, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
      </behaviorExtensions>
    </extensions>
…
</system.serviceModel>

可以在应用程序或 ASP.NET 配置文件中添加扩展,这是最常见的选择,也可以在计算机配置文件中添加扩展。

将扩展添加到配置范围时,可以将该行为添加到行为配置中,如以下代码所示。 行为配置是可根据需要应用于多个终结点的可重用元素。 由于此处要配置的特定行为实现 IEndpointBehavior,因此它仅在配置文件的相应配置部分中有效。

<system.serviceModel>
   <behaviors>
      …
     <endpointBehaviors>
        <behavior name="HelloServiceEndpointBehavior">
          <schemaValidator validateRequest="True" validateReply="True">
            <schemas>
              <add location="messages.xsd" />
            </schemas>
          </schemaValidator>
        </behavior>
      </endpointBehaviors>
      …
    </behaviors>
</system.serviceModel>

<schemaValidator>配置消息检查器的元素由SchemaValidationBehaviorExtensionElement类提供支持。 该类公开两个名为 ValidateRequestValidateReply 的布尔公共属性。 这两个属性均使用 ConfigurationPropertyAttribute 进行标记。 此属性构成代码属性与在前面的 XML 配置元素上可以看到的 XML 属性之间的链接。 该类还有一个属性 Schemas,该属性不仅标有 ConfigurationCollectionAttribute,而且属于类型 SchemaCollection。这一属性在此示例中也包括,但为简洁起见,在本文档中被省略。 该属性与集合及集合元素类 SchemaConfigElement 一起支持之前配置代码片段中的 <schemas> 元素,并允许将多个架构添加到验证集。

重写的 CreateBehavior 方法会在运行时为生成客户端或终结点而计算配置数据时将配置数据转换为行为对象。

public class SchemaValidationBehaviorExtensionElement : BehaviorExtensionElement
{
    public SchemaValidationBehaviorExtensionElement()
    {
    }

    public override Type BehaviorType
    {
        get
        {
            return typeof(SchemaValidationBehavior);
        }
    }

    protected override object CreateBehavior()
    {
        XmlSchemaSet schemaSet = new XmlSchemaSet();
        foreach (SchemaConfigElement schemaCfg in this.Schemas)
        {
            Uri baseSchema = new
                Uri(AppDomain.CurrentDomain.BaseDirectory);
            string location = new
                Uri(baseSchema,schemaCfg.Location).ToString();
            XmlSchema schema =
                XmlSchema.Read(new XmlTextReader(location), null);
            schemaSet.Add(schema);
        }
     return new
     SchemaValidationBehavior(schemaSet,ValidateRequest,ValidateReply);
    }

[ConfigurationProperty("validateRequest",DefaultValue=false,IsRequired=false)]
public bool ValidateRequest
{
    get { return (bool)base["validateRequest"]; }
    set { base["validateRequest"] = value; }
}

[ConfigurationProperty("validateReply", DefaultValue = false, IsRequired = false)]
        public bool ValidateReply
        {
            get { return (bool)base["validateReply"]; }
            set { base["validateReply"] = value; }
        }

     //Declare the Schema collection property.
     //Note: the "IsDefaultCollection = false" instructs
     //.NET Framework to build a nested section of
     //the kind <Schema> ...</Schema>.
    [ConfigurationProperty("schemas", IsDefaultCollection = true)]
    [ConfigurationCollection(typeof(SchemasCollection),
        AddItemName = "add",
        ClearItemsName = "clear",
        RemoveItemName = "remove")]
    public SchemasCollection Schemas
    {
        get
        {
            SchemasCollection SchemasCollection =
            (SchemasCollection)base["schemas"];
            return SchemasCollection;
        }
    }
}

强制添加消息检查器

除了通过属性(此示例中不受上述原因支持)和配置之外,可以使用命令性代码轻松地将行为添加到客户端和服务运行时。 在此示例中,这是在客户端应用程序中完成的,用于测试客户端消息检查器。 该 GenericClient 类派生自 ClientBase<TChannel>该类,它将终结点配置公开给用户代码。 在隐式打开客户端之前,可以更改终结点配置,例如,添加行为,如以下代码所示。 在服务上添加行为在很大程度上等同于此处所示的客户端技术,必须在打开服务主机之前执行。

try
{
    Console.WriteLine("*** Call 'Hello' with generic client, with client behavior");
    GenericClient client = new GenericClient();

    // Configure client programmatically, adding behavior
    XmlSchema schema = XmlSchema.Read(new StreamReader("messages.xsd"),
                                                          null);
    XmlSchemaSet schemaSet = new XmlSchemaSet();
    schemaSet.Add(schema);
    client.Endpoint.Behaviors.Add(new
                SchemaValidationBehavior(schemaSet, true, true));

    Console.WriteLine("--- Sending valid client request:");
    GenericCallValid(client, helloAction);
    Console.WriteLine("--- Sending invalid client request:");
    GenericCallInvalid(client, helloAction);

    client.Close();
}
catch (Exception e)
{
    DumpException(e);
}

设置、生成和运行示例

  1. 确保已为 Windows Communication Foundation 示例 执行One-Time 安装过程。

  2. 要生成解决方案,请按照生成 Windows Communication Foundation 示例中的说明进行操作。

  3. 若要在单台计算机或跨计算机配置中运行示例,请按照 运行 Windows Communication Foundation 示例中的说明进行操作。