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

WCF 契约继承问题简单解释

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.43/5 (8投票s)

2014 年 6 月 9 日

CPOL

4分钟阅读

viewsIcon

33900

downloadIcon

149

如何在 WCF 契约中正确替换基类与子类?

引言

最近我遇到了一个问题,无法在 WCF 中将子类而不是基类传递。我搜索了一个主题,但没有找到合适的解决方案或解释。所以,我想,这是一个很好的文章主题来写。

背景

让我们简要描述一下我们目前的情况:我们有一个 WCF 应用程序,其中服务(又名 Factory)将信息发送到客户端(又名 Factory Client)。

首先,这是服务合同

    [ServiceContract]
    public interface IFactory
    {
        [OperationContract]
        string TryService(string echo);
 
        [OperationContract]
        Result GetResult();
    }

它包含两个方法,

  1. TryService 是一个虚拟方法,用于测试我们的 WCF 通信。
  2. GetResult 将创建一个 dictionary<string, BaseElement> 并将其发送回客户端。

其次,服务实现

    public class Factory : IFactory
    {
        public string TryService(string echo)
        {
            return echo;
        }
 
        public Result GetResult()
        {
            var baseElem = new BaseElement
            {
                BaseName = "BaseElement"
            };
 
            return new Result()
            {
                Elements = new Dictionary<string, BaseElement>
                {
                    {"1", baseElem}
                }
            };
        }
    }

我们的 BaseElement 类实现

    [DataContract]
    public class BaseElement
    {
        [DataMember]
        public string BaseName { get; set; }
    }

我们的服务配置

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.web>
    <compilation debug="true" />
  </system.web>
  <system.serviceModel>
    <services>
      <service name="ElementService.Factory">
        <endpoint address="/FactoryService" behaviorConfiguration=""
          binding="basicHttpBinding" bindingConfiguration="" contract="ElementService.IFactory" />
        <host>
          <baseAddresses>
            <add baseAddress="https:///services" />
          </baseAddresses>
        </host>
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior>
          <serviceMetadata httpGetEnabled="True"/>
          <serviceDebug includeExceptionDetailInFaults="False" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>

客户端实现

    class FactoryClient : IFactory
    {
        public FactoryClient()
        {
            var myBinding = new BasicHttpBinding();
            var endpoint = new EndpointAddress("https:///services/FactoryService");
            ChannelFactory = new ChannelFactory<IFactory>(myBinding, endpoint);
        }
 
        public string TryService(string echo)
        {
            var client = ChannelFactory.CreateChannel();
            var response = client.TryService(echo);
            ((ICommunicationObject)client).Close();
            return response;
        }
 
        public Result GetResult()
        {
            var client = ChannelFactory.CreateChannel();
            var response = client.GetResult();
            ((ICommunicationObject)client).Close();
            return response;
        }
 
        public ChannelFactory<IFactory> ChannelFactory { get; set; }
    }

我们的程序

    class Program
    {
        static void Main(string[] args)
        {
            FactoryClient factory = new FactoryClient();
            var tt = factory.TryService("Bill");
            Console.WriteLine("Service says: " + tt);
 
            var res = factory.GetResult();
            foreach (var item in res.Elements)
            {
                Console.WriteLine("Key :" + item.Key + " Value: " + item.Value);
            }
            Console.ReadLine();            
        }
    }

到目前为止,一切都应该正常工作,并且运行程序时预期的结果是

问题

现在,假设我们有一个新的类(又名 SuperElement),它继承自 BaseElement。

    [DataContract]
    public class SuperElement : BaseElement
    {
        [DataMember]
        public string SuperName { get; set; }
    }

在服务端的 GetResult 方法中返回 SuperElement 是完全没问题的。

        public Result GetResult()
        {
            var baseElem = new BaseElement
            {
                BaseName = "BaseElement"
            };
 
            var superElem = new SuperElement
            {
                SuperName = "SuperElement"
            };
 
            return new Result()
            {
                Elements = new Dictionary<string, BaseElement>
                {
                    {"1", baseElem},
                    {"2", superElem},
                }
            };
        }

但是,当我运行客户端并调用服务上的 GetResult 方法时,我们会得到一个类似这样的异常:

正在接收 https:///services/FactoryService 的 HTTP 响应时出错。原因可能是连接终结点服务未使用 HTTP 协议。这也可能是因为服务器忽略了 HTTP 请求的上下文(可能是由于服务已停用)。有关更多信息,请检查服务器日志。

那么,这到底发生了什么?

解释

让我们回顾一下继承。

    public class A
    {
    }

    public class B : A
    {
    }

对于基类 A,任何继承自 A 的子类 B 也被视为 A 类,但 A 类不一定是 B 类。

这是正确的,因为每当你创建一个子类的实例时,编译器首先将基类分配给对象的内存状态,然后添加子类部分来完成实例化,正如这张图所解释的。

当一个方法期望一个基类引用时,它接收一个子类引用,它会接受它,因为这也是一个基类引用。然而,WCF 的情况并非如此,因为对象是按值传递而不是按引用传递!

在多级应用程序中,每个层都可以自由地以自己的方式处理类,按值传递元素比按引用传递元素更好。此外,这种行为有利于应用程序的互操作性、异步调用和长工作流。

在我们的例子中,创建 Dictionary<string, BaseElement> 后,服务会将其序列化以在 WCF 消息中发送。收到 WCF 消息后,客户端会尝试使用服务合同作为指导图进行反序列化。如服务合同中所述,对象应该是 Dictionary<string, BaseElement>,但客户端发现另一个对象(即 SuperElement)。因为它没有预期到这种类型,所以客户端会引发一个异常!

解决方案

随着 .Net Framework 3.0 的推出,WCF 创建了一个名为 KnownType 的属性来解决这类问题。让我们看看如何使用它,然后再解释它的作用。

    [DataContract]
    [KnownType(typeof(SuperElement))]
    public class BaseElement
    {
        [DataMember]
        public string BaseName { get; set; }
    }

使用 KnownType 属性,我们告诉服务/客户端,如果你发现 SuperElement 实例而不是 BaseElement 实例,那么这是完全没问题的,你也可以序列化/反序列化它。神奇的是,序列化/反序列化会根据子类类型(SuperElement)而不是基类(BaseElement)进行。

KnownType 属性会影响所有操作和合同,从而允许接受子类而不是基类。此外,它还允许客户端通过在元数据中包含子类来将子类传递给服务。

注意

你必须记住三点:

  • 你必须始终包含完整的继承层次结构才能启用子类替换。添加子类不会添加其基类!
  • KnownType 属性会降低应用程序的性能。当你在应用程序中到处进行替换时,这种成本会变得相当可观。在这种情况下,你可能需要重新审视你的应用程序架构。
  • 如果拥有许多基类/子类是不可避免的,微软提供了其他替代方案,例如:
    • ServiceKnowType 属性
    • 在应用程序配置文件 System.runtime.serialization 部分编写要替换的类的列表。
    • .Net 4.0 提供了最佳解决方案:DataContractResolver 是一个强大的工具,它可以拦截序列化/反序列化过程,让你通过自己的代码来改变行为。

希望这篇文章对您有所帮助。

致谢

所有功劳都归于 MSDN 上一篇关于这个主题的文章。我强烈建议阅读关于 DataContractResolver 的部分。

© . All rights reserved.