在服务和客户端之间共享 WCF 集合类型






4.75/5 (9投票s)
在 WCF 服务和客户端之间共享类型,包括多个程序集和集合类型。
引言
在本文中,我将演示一个在 Windows Communication Foundation (WCF) 中共享服务和客户端类型,包括多个程序集中的类型和自定义集合类型的示例。
背景
决定在 WCF 服务和客户端之间共享类型可能存在争议。面向服务架构(SOA)的纯粹主义者会告诉你,你不应该在服务和客户端之间共享类型;你只应该共享契约。因此,你应该仔细考虑在客户端和服务之间共享类型的决定。在我正在开发的一个应用程序中,我同时控制着客户端和服务,并且在可预见的未来都会如此。当我开始实现时,很明显,为了只共享契约,我将不得不复制(或以某种方式提取)相当多的代码,所以我决定在此项目中共享类型本身。
在我决定共享类型之后,一个将我引向正确方向的资源是 The Code Project 上 Mike Robsiki 的一篇文章,标题为《Sharing Types Between WCF Service and Client》。它展示了使用 SvcUtil.exe 生成使用共享类型的代理的基本语法,但它没有涵盖诸如使用来自多个程序集的类型或集合类型等主题。本文将解决这些附加主题。
本文假定您对 WCF 有基本了解。有关 WCF 的其他 CodeProject 文章,请查看此链接。
源代码概述
本文的源代码包含一个 Visual Studio 2005 解决方案,其中有四个项目:一个服务 (ShareTypes.Service),一个客户端 (ShareTypes.Client),以及两个通用的类库 (ShareTypes.Common1, ShareTypes.Common2)。通用类库包含在服务和客户端之间共享的类。服务和客户端项目都是控制台应用程序。客户端和服务两者的配置在此示例中被硬编码。
共享类型
有两个程序集包含将在服务和客户端之间共享的类型。ShareTypes.Common1 程序集定义了三个类。
using System.Collections.ObjectModel;
using System.Runtime.Serialization;
namespace ShareTypes.Common {
[DataContract]
public class SharedType1 {
public SharedType1() {
Item = new SharedCollection1();
ItemWithContractNotShared = new CollectionWithContractNotShared();
}
[DataMember]
public SharedCollection1 Item;
[DataMember]
public CollectionWithContractNotShared ItemWithContractNotShared;
}
[CollectionDataContract]
public class SharedCollection1:Collection<string /> {
public void AddArray( string[] toAdd ) {
foreach ( string s in toAdd ) {
Add( s );
}
}
}
[CollectionDataContract]
public class CollectionWithContractNotShared: Collection<string /> {
public void AddArray( string[] toAdd ) {
foreach ( string s in toAdd ) {
Add( s );
}
}
}
}
SharedType1
类被 DataContract
属性装饰,并且其字段被 DataMember
属性标记。每个字段都是一个集合。SharedCollection1
旨在在客户端和服务器之间共享,而 CollectionWithContractNotShared
将不会在客户端和服务之间共享。为了方便起见,在此示例中,我使用了公共字段而不是私有字段和相应的公共属性。这不推荐用于生产应用程序。
请注意,这些集合被 CollectionDataContract
属性标记,而不是 DataContract
属性。这对于在服务和客户端之间共享集合类型是必要的(但不是充分的),并且会影响生成的代理,如下文所示。
ShareTypes.Common2 程序集定义了两个类,这两个类都将在客户端和服务之间共享。
using System.Collections.ObjectModel;
using System.Runtime.Serialization;
namespace ShareTypes.Common {
[DataContract]
public class SharedType2 {
public SharedType2() {
Item = new SharedCollection2< int >();
}
[DataMember]
public SharedCollection2< int > Item;
}
[CollectionDataContract]
public class SharedCollection2< T >: Collection< T > {
public void AddArray( T[] toAdd ) {
foreach ( T o in toAdd ) {
Add( o );
}
}
}
}
Service
服务本身非常简单。每个方法都接受一个具有公共字段的集合类型的参数,并返回一个来自作为参数传递的实例的集合。每个方法都会回显作为参数传递的集合值。这是服务接口和实现。
[ServiceContract]
public interface IService {
[OperationContract]
SharedCollection1 UseSharedTypes1( SharedType1 sharedType );
[OperationContract]
SharedCollection2< int > UseSharedTypes2( SharedType2 sharedType );
[OperationContract]
CollectionWithContractNotShared
UseCollectionWithContractNotShared( SharedType1 sharedType );
[OperationContract]
NonSharedCollection UseNonSharedTypes( NonSharedType nonSharedType );
}
public class Service: IService {
public SharedCollection1 UseSharedTypes1( SharedType1 sharedType ) {
return sharedType.Item;
}
public SharedCollection2<int> UseSharedTypes2( SharedType2 sharedType ) {
return sharedType.Item;
}
public CollectionWithContractNotShared
UseCollectionWithContractNotShared( SharedType1 sharedType ) {
return sharedType.ItemWithContractNotShared;
}
public NonSharedCollection UseNonSharedTypes( NonSharedType nonSharedType ) {
return nonSharedType.Item;
}
}
服务项目还定义了一些不在客户端和服务之间共享的类型。
[DataContract]
public class NonSharedType {
public NonSharedType() {
Item = new NonSharedCollection();
}
[DataMember]
public NonSharedCollection Item;
}
public class NonSharedCollection: Collection<int> {}
托管服务
以下代码用于托管服务。为了使 SvcUtil.exe 能够为我们的服务创建代理,我们需要启用元数据交换。要启用元数据交换,我们需要将 ServiceMetadataBehavior
添加到服务中并添加一个服务终结点。(请注意,我们使用了 MetadataExchangeBindings
类上的静态 CreateMexTcpBinding
方法来创建元数据交换的绑定。存在用于创建各种协议绑定的方法。)
public static void Main() {
Uri baseAddress = new Uri( "net.tcp://:9042/Service" );
Uri mexAddress = new Uri( "mex", UriKind.Relative );
using ( ServiceHost serviceHost =
new ServiceHost( typeof( Service ), baseAddress ) ) {
NetTcpBinding binding = new NetTcpBinding();
serviceHost.AddServiceEndpoint( typeof ( IService ), binding, baseAddress );
// Add metadata exchange behavior to the service
serviceHost.Description.Behaviors.Add( new ServiceMetadataBehavior() );
// Add a service Endpoint for the metadata exchange
serviceHost.AddServiceEndpoint( typeof ( IMetadataExchange ),
MetadataExchangeBindings.CreateMexTcpBinding(), mexAddress );
// Run the service
serviceHost.Open();
Console.WriteLine( "Service started. Press enter to terminate service." );
Console.ReadLine();
serviceHost.Close();
}
}
客户端
SvcUtil.exe 命令行
在编写客户端代码之前,我们必须为服务生成一个代理类。要共享服务和客户端之间的类型,您必须使用 SvcUtil.exe 命令行工具生成代理,而不是在 Visual Studio 中添加服务引用。(在 Visual Studio 的 Orcas 版本中,将在 IDE 中提供更多 SvcUtil.exe 选项,这可能不再是必需的)。下面显示了生成客户端代理所需的命令行。它包含在客户端项目源代码的 UpdateServiceReference.bat 中(在批处理文件中,它在同一行,但在此处换行以便于阅读)。
"c:\Program Files\Microsoft SDKs\Windows\v6.0\Bin\SvcUtil"
/language:cs
/out:ServiceClient.cs
/namespace:*,ShareTypes.Client
/noconfig
/reference:..\ShareTypes.Common1\bin\Debug\ShareTypes.Common1.dll
/reference:..\ShareTypes.Common2\bin\Debug\ShareTypes.Common2.dll
/collectionType:ShareTypes.Common.SharedCollection1
/collectionType:ShareTypes.Common.SharedCollection2`1
net.tcp://:9042/Service/mex
与在客户端和服务之间共享类型相关的选项是 reference 和 collectionType 开关。reference 开关(短形式:/r)指示 SvcUtil.exe 直接使用引用程序集中的类型,而不是生成客户端版本的类型;基本上,这可以防止 SvcUtil.exe 为这些程序集中的类型生成代码。您可以多次指定 reference 开关。对于不是集合的类型,这就是共享类型在客户端和服务之间所需的所有操作。
对于集合类型,需要在客户端和服务之间共享类型方面额外进行一步,即添加 collectionType 开关(短形式:/ct)。collectionType 开关的每一次出现都标识一个将在客户端和服务之间共享的集合类型。collectionType 开关必须标识在 reference 开关指定的程序集内定义的类型。
对于 SharedCollection2
的 collectionType 开关中的表达式,后面跟着一个反引号(`)和一个 1。对于泛型类型,您必须指定泛型参数的数量,后面跟着一个反引号和一个泛型参数的数量(对于 SharedCollection2
是一个)。请勿将反引号(`)与单引号(')混淆。(在我的键盘上,反引号在波浪号(~)下方)。
如果一个集合被 CollectionDataContract
属性标记,并且位于 reference 开关中指定的程序集中,但**没有**在 collectionType 开关中标识,那么它将不会在客户端和服务之间共享。ShareTypes.Common1 程序集中的 CollectionWithContractNotShared
类演示了这一点。它被 CollectionDataContract
属性标记,但没有在 collectionType 开关中明确提及。因为它不在 collectionType 开关中,所以为其生成了一个客户端类型的版本。
以下代码片段显示了生成的客户端代理的一部分,为清晰起见省略了属性。
public interface IService
{
ShareTypes.Common.SharedCollection1 UseSharedTypes1(
ShareTypes.Common.SharedType1 sharedType);
ShareTypes.Common.SharedCollection2<int> UseSharedTypes2(
ShareTypes.Common.SharedType2 sharedType);
ShareTypes.Client.CollectionWithContractNotShared
UseCollectionWithContractNotShared(ShareTypes.Common.SharedType1 sharedType);
int[] UseNonSharedTypes(ShareTypes.Client.NonSharedType nonSharedType);
}
对于使用共享类型的两个方法,它们的返回值来自 ShareTypes.Common
命名空间。对于返回 CollectionWithContractNotShared
的方法,它引用了上面描述的在客户端生成的类。最后一个方法 UseNonSharedTypes
说明了当服务返回集合时的默认行为,即输出结果为数组。因此,使用 CollectionWithContractNotShared
上的 CollectionDataContract
属性会强制生成一个派生自 List<T>
的客户端类。
public class CollectionWithContractNotShared : System.Collections.Generic.List<string />
{
}
有关数据合同中集合类型的深入讨论,请参阅 MSDN 文章:数据合同中的集合类型。
客户端代码
调用服务的客户端代码如下(穿插注释)。从通用程序集中共享的所有类型都以 Common
作为前缀,以标识通用命名空间。前两个方法调用演示了共享类型的使用,其中参数和返回值类型都以 Common
作为前缀。
private static void Main() {
EndpointAddress endpointAddress =
new EndpointAddress( "net.tcp://:9042/Service" );
NetTcpBinding binding = new NetTcpBinding();
using ( ServiceClient proxy = new ServiceClient( binding, endpointAddress ) ) {
Common.SharedType1 sharedType1 = new Common.SharedType1();
sharedType1.Item.AddArray( new string[] { "1", "2", "3", "4", "5" } );
Console.WriteLine(
string.Format( "Calling UseSharedTypes1 with collection values {0}",
GetValues( sharedType1.Item ) ) );
Common.SharedCollection1 sharedCollection1 = proxy.UseSharedTypes1( sharedType1 );
Console.WriteLine( string.Format( "Returned UseSharedTypes1 values {0}",
GetValues( sharedCollection1 ) ) );
Console.WriteLine( "" );
Common.SharedType2 sharedType2 = new Common.SharedType2();
sharedType2.Item.AddArray( new int[] { 3, 2, 1 } ) ;
Console.WriteLine(
string.Format( "Calling UseSharedTypes2 with collection values {0}",
GetValues( sharedType2.Item ) ) );
Common.SharedCollection2<int> sharedCollection2 = proxy.UseSharedTypes2( sharedType2 );
Console.WriteLine( string.Format( "Returned UseSharedTypes2 values {0}",
GetValues( sharedCollection2 ) ) );
Console.WriteLine( "" );
第三个方法调用返回 CollectionWithContractNotShared
的一个实例。它不使用通用类型作为返回值;它使用一个在客户端生成的类型。在这种情况下,我们仍然可以使用 SharedType1
实例上的 AddArray
方法来添加进入方法的值,但 AddArray
方法在返回的集合上不可用,因为它引用的是生成的代理类型(没有 Common
前缀)。
sharedType1 = new Common.SharedType1();
sharedType1.ItemWithContractNotShared.AddArray( new string[] { "1", "2" } );
Console.WriteLine( string.Format(
"Calling UseCollectionWithContractNotShared with collection values {0}",
GetValues( sharedType1.ItemWithContractNotShared ) ) );
CollectionWithContractNotShared collectionWithContractNotShared =
proxy.UseCollectionWithContractNotShared( sharedType1 );
Console.WriteLine( string.Format(
"Returned UseCollectionWithContractNotShared values {0}",
GetValues( collectionWithContractNotShared ) ) );
Console.WriteLine( "" );
最后一个方法调用显示了对于未在服务和客户端之间共享的类型的标准行为。此方法返回一个整数数组。由于引用的 NonSharedType
是在客户端生成的,因此 Item
集合属性被生成为一个整数数组。
NonSharedType nonSharedType = new NonSharedType();
nonSharedType.Item = new int[] { 5, 4, 3, 2, 1 };
Console.WriteLine(
string.Format( "Calling UseNonSharedTypes with collection values {0}",
GetValues( nonSharedType.Item ) ) );
int[] collectionNonShared = proxy.UseNonSharedTypes( nonSharedType );
Console.WriteLine( string.Format( "Returned UseNonSharedTypes values {0}",
GetValues( collectionNonShared ) ) );
Console.WriteLine( "" );
Console.WriteLine( "Press enter to continue." );
Console.ReadLine();
}
}
以下屏幕截图代表了运行客户端程序的结果。在运行客户端(或创建客户端代理的批处理文件)之前,您必须先启动服务应用程序。
设计注意事项
正如我在文章开头提到的,在 WCF 服务和客户端之间共享类型可能存在争议,许多人对此皱眉。然而,正如您从客户端共享类型的代码中看到的,共享类型的决定完全由客户端控制。如果您在调用 SvcUtil.exe 时省略 reference 和 collectionType 开关,您将不再与服务共享类型。这里有两个重要的观点:
- WCF 服务接口或实现中没有任何特殊内容可以允许与客户端共享类型。
- 因为您在服务和客户端之间共享类型,并不意味着您必须在该服务和所有客户端之间共享类型。因此,您可以有一个 .NET 客户端共享类型,而其他客户端不共享类型。
因此,如果您想在服务和客户端之间共享类型,请尽管去做,风险很小,只要您在服务中不对类型将被共享的客户端做任何特殊处理即可。您还应该将要在服务和客户端之间共享的类型隔离到单独的程序集中,并确保这些程序集的依赖项尽可能少(例如,这些程序集可能不应包含数据访问)。否则,您可能会发现客户端需要不必要的程序集。
历史
- 2007 年 9 月 4 日 - 初次发布。