Introduction
In this article, I will illustrate an example of sharing types between services and clients in Windows Communication Foundation (WCF), including types in multiple assemblies and custom collection types.
Background
Making the decision to share types between a WCF service and clients can be controversial. Service Oriented Architecture (SOA) purists will tell you that you should never share types between services and clients; you should only share contracts. So, you should carefully consider the decision to share types between clients and services. In an application I am working on, I am controlling both the client and the service, and will for the foreseeable future. As I started implementation, it quickly became obvious that I would have to copy (or factor out in some way) a fair amount of code to support sharing only contracts, so I decided to share the types themselves on this project.
After I made the decision to share types, one of the resources that pointed me in the right direction was an article on The Code Project by Mike Robsiki titled Sharing Types Between WCF Service and Client. It shows the basic syntax for using SvcUtil.exe to generate a proxy using shared types, but it does not cover topics such as using types from multiple assemblies, or collection types. This article addresses these additional topics.
This article assumes you have a basic understanding of WCF. For other CodeProject articles regarding WCF, check out this link.
Source Code Overview
The source code for this article contains a Visual Studio 2005 solution with four projects: a service (ShareTypes.Service), client (ShareTypes.Client), and two common class libraries (ShareTypes.Common1, ShareTypes.Common2). The common libraries contain the classes that are shared between the service and the client. Both the service and the client projects are console applications. The configuration for both the client and service are hard-coded for this example.
Shared Types
There are two assemblies containing types to be shared between the service and the client. The ShareTypes.Common1 assembly defines three classes:
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 );
}
}
}
}
The SharedType1
class is decorated with the DataContract
attribute, and has fields marked with the DataMember
attribute. Each field is a collection. SharedCollection1
is intended to be shared between the client and server, while CollectionWithContractNotShared
will not be shared between the client and the service. For convenience, in this example, I have used public fields rather than private fields with corresponding public properties. This is not recommended for production applications.
Note that the collections are marked with the CollectionDataContract
attribute rather than the DataContract
attribute. This is necessary (but not sufficient) to share collection types between services and clients, and affects the generated proxy, as will be seen later.
The ShareTypes.Common2 assembly defines two classes, both of which are to be shared between the client and the service:
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
The service itself is very simple. Each method accepts one of the types with a collection as a public field, and returns a collection from the instance passed as a parameter. Each method echoes the collection values passed as a parameter. Here is the service interface and implementation:
[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;
}
}
The service project also defines some types that are not shared between the client and the service:
[DataContract]
public class NonSharedType {
public NonSharedType() {
Item = new NonSharedCollection();
}
[DataMember]
public NonSharedCollection Item;
}
public class NonSharedCollection: Collection<int> {}
Hosting the Service
The following code hosts the service. To enable SvcUtil.exe to be able to create a proxy for our service, we need to enable metadata exchange. To enable metadata exchanges, we need to add ServiceMetadataBehavior
to the service and add a service endpoint. (Note the use of the static CreateMexTcpBinding
method on the MetadataExchangeBindings
class to create a binding for metadata exchange. There are methods to create bindings for various protocols.)
public static void Main() {
Uri baseAddress = new Uri( "net.tcp://localhost: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 );
serviceHost.Description.Behaviors.Add( new ServiceMetadataBehavior() );
serviceHost.AddServiceEndpoint( typeof ( IMetadataExchange ),
MetadataExchangeBindings.CreateMexTcpBinding(), mexAddress );
serviceHost.Open();
Console.WriteLine( "Service started. Press enter to terminate service." );
Console.ReadLine();
serviceHost.Close();
}
}
Client
SvcUtil.exe Command Line
Before writing client code, we must generate a proxy class for the service. To share types between a service and client, you must generate the proxy using the SvcUtil.exe command line tool, rather than adding a service reference in Visual Studio. (In the Orcas version of Visual Studio, more SvcUtil.exe options will be available within the IDE and this may no longer be necessary). The following shows the command line required to generate the client proxy. It is included in the client project source in UpdateServiceReference.bat (in the batch file, it is all on one line, but is wrapped here for readability):
"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://localhost:9042/Service/mex
The options relevant to sharing types between the client and the service are the reference and collectionType switches. The reference switch (short form: /r) instructs SvcUtil.exe to use types in the referenced assembly directly, rather than generating client side versions of them; basically, this prevents SvcUtil.exe from generating code for the types in these assemblies. You can specify the reference switch multiple times. For types that are not collections, this is all you need to do to share types between clients and services.
For collection types, there is an additional step necessary to share types between the client and the service, which is adding the collectionType switch (short form: /ct). Each occurrence of the collectionType switch identifies a collection type that will be shared between the service and the client. The collectionType switch must identify a type that is defined within an assembly specified in a reference switch.
The expression in the collectionType switch for SharedCollection2
is followed by a backquote (`) and a 1. For generic types, you must specify the number of generic parameters with a backquote followed by the number of generic parameters (one for SharedCollection2
). Do not confuse the backquote (`) with the single quote ('). (On my keyboard, the backquote is under the tilde (~)).
If a collection is marked with the CollectionDataContract
attribute and is in an assembly identified in a reference switch, but is not identified in a collectionType switch, it will not be shared between the client and the service. The CollectionWithContractNotShared
class in the ShareTypes.Common1 assembly demonstrates this. It is marked with the CollectionDataContract
attribute, but is not mentioned explicitly in a collectionType switch. Because it is not in a collectionType switch, there is a client side type generated for it.
The following code excerpt shows a portion of the generated client proxy, with the attributes omitted for clarity:
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);
}
For the two methods using shared types, their return values are from the ShareTypes.Common
namespace. For the method returning a CollectionWithContractNotShared
, it is referencing the class generated on the client side as described above. The final method, UseNonSharedTypes
, illustrates the default behavior when a collection is returned from a service, which is to output the results as an array. So using the CollectionDataContract
attribute on the CollectionWithContractNotShared
forces a client side class to be generated deriving from List<T>
:
public class CollectionWithContractNotShared : System.Collections.Generic.List<string />
{
}
For an in-depth discussion of collection types in data contracts, refer to the MSDN article: Collection Types in Data Contracts.
Client Code
The client code to call the service follows (with interspersed comments). All types being shared from the common assemblies are prefixed with Common
to identify the common namespace. The first two method calls demonstrate the usage of shared types, with parameter and return value types prefixed by Common
.
private static void Main() {
EndpointAddress endpointAddress =
new EndpointAddress( "net.tcp://localhost: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( "" );
The third method call returns an instance of CollectionWithContractNotShared
. It does not use a common type as a return value; it uses one generated on the client side. In this case, we can still use the AddArray
method on the SharedType1
instance to add values going into the method, but the AddArray
method is not available on the returned collection because it is referencing the proxy generated type (not prefaced by 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( "" );
The last method call shows the standard behavior with types that are not shared between the service and the client. This method returns an array of integers. And since the NonSharedType
referenced was generated on the client side, the Item
collection property is generated as an array of integers.
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();
}
}
The following screenshot represents the results of running the client program. You must first start the service application before you can run the client (or the batch file to create the client proxy).
A Design Note
As I mentioned at the beginning of the article, sharing types between a WCF service and client can be controversial, and many people frown upon ever doing it. However, as you can see from the code to share types on the client side, the decision to share types is completely controlled by the client. If you omit the reference and collectionType switches in the call to SvcUtil.exe, you are no longer sharing types with the service. There are two important points here:
- There is nothing special in the WCF service interface or implementation to allow type sharing with the client.
- Because you share types between a service and a client, it does not mean you must share types between that service and all clients. So you can have one .NET client that shares types, and other clients that do not share types.
So if you want to share types between the service and a client, go ahead, there's little risk in doing so, as long as you do nothing special in the service that assumes types will be shared with the client. You should also isolate the types to be shared between the service and the client into separate assemblies, and ensure these assemblies have as few dependencies as possible (for example, these assemblies should probably not contain data access). Otherwise, you may find that you need unwanted assemblies on the client side.
History
- September 4, 2007 - Initial publication.
References