WCF 契约继承问题简单解释






4.43/5 (8投票s)
如何在 WCF 契约中正确替换基类与子类?
引言
最近我遇到了一个问题,无法在 WCF 中将子类而不是基类传递。我搜索了一个主题,但没有找到合适的解决方案或解释。所以,我想,这是一个很好的文章主题来写。
背景
让我们简要描述一下我们目前的情况:我们有一个 WCF 应用程序,其中服务(又名 Factory)将信息发送到客户端(又名 Factory Client)。
首先,这是服务合同
[ServiceContract]
public interface IFactory
{
[OperationContract]
string TryService(string echo);
[OperationContract]
Result GetResult();
}
它包含两个方法,
- TryService 是一个虚拟方法,用于测试我们的 WCF 通信。
- 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 的部分。