WCF and Client Proxies

Common Considerations for Proxy Generation

This column explores how to build Web services with WCF in.NET 3.0 and 3.5. In this installment, I ll talk about proxy generation techniques. If you have questions about migrating your existing ASMX or WSE Web services to WCF, or questions regarding WCF as it relates to Web services, please send them to [email protected]!

When developers think about WCF, they primarily concern themselves with the server side of the equation: how to design, build, and deploy reliable and scalable services for an enterprise system. Certainly this is the starring role for WCF; but where there are services, there must also be clients consuming those services. There are also design considerations for the client that impact your development process, available tools, and general coding practices as related to WCF proxy classes. In this article, I ll discuss the proxy generation results achieved with Visual Studio 2008 (Beta 2), discuss the pros and cons of using generated or hand-rolled proxies, and discuss approaches for sharing service metadata.

 

Proxy Generation with Visual Studio 2008

You can generate a proxy to consume a service using the command-line tool SvcUtil (svcutil.exe) or using Add Service Reference from Solution Explorer in Visual Studio. SvcUtil is a fully featured proxy generator that supports the following key features:

  • Generating proxies from a WS-MetadataExchange endpoint, or from a static WSDL document.
  • Generating asynchronous proxies to improve perceived performance for client applications.
  • Generating configuration files that describe each service endpoint and the correct protocols for a proxy to reach each endpoint.
  • Generating proxies that reference types from pre-existing assemblies.
  • Controlling the collection type preferences proxies use for client coding against arrays and dictionaries.

In addition to these key features, you can generate a service type with associated data contracts from an existing WSDL document, control the choice of serializer, merge configuration files, and a few other tasks.

Command-line generation of proxies is important in environments that rely on scripts to generate and update proxy code, and is necessary when the desired results can t be accomplished using Visual Studio. The WCF Orcas extension for Visual Studio 2005 supports generating a basic proxy using Add Service Reference but it doesn t give you control over SvcUtil options during the process. Visual Studio 2008 includes the latest WCF tooling for proxy generation via Add Service Reference, providing a way to reach several key SvcUtil options (listed above) from a friendly dialog box.

Figure 1 illustrates the first dialog box you see when you invoke Add Service Reference from Solution Explorer. As before, you specify the metadata exchange endpoint for the service (metadata exchange must be enabled for this to work) and provide a name for the reference, which becomes part of the generated CLR namespace. Unlike before, you can now discover services within the current solution, and view information about the service endpoints before adding the reference.


Figure 1: The Add Service Reference dialog box in Visual Studio 2008 (Beta 2).

From the Advanced button in this dialog box you can access additional features, as shown in Figure 2. This includes control over the visibility of classes generated, asynchronous proxy generation, array and dictionary types used by the proxy, leveraging existing assemblies for data contract type definitions, and other features, such as creating message contracts and generating a traditional .NET 2.0 Web reference instead of a service reference.


Figure 2: The Service Reference Settings dialog box in Visual Studio 2008 (Beta 2).

Several of these features make a big difference to client developers, removing the need to go to the command line and use SvcUtil. Likely, the most convenient of these new features is the ability to leverage pre-existing assemblies and reuse the same data contracts instead of generating copies at the client. I ll talk more about this scenario shortly. It is also very convenient to be able to control collection and dictionary types, particularly if you want to enable data binding with controls in the client UI with BindingList. Another very common need is to create asynchronous operations to simplify multithreading at the client to improve perceived performance. Having these features built-in to the Add Service Reference dialogs is a real time saver, as it is always a pain to move out of the development environment to generate proxies with the command line.

 

Proxy Generation vs. ChannelFactory

Whether you generate proxies from the command line, or through the Visual Studio IDE, you are using SvcUtil. By default, several things are created from proxy generation: a configuration file with settings to invoke each service endpoint, a proxy wrapper class that encapsulates calls to each service operation, a copy of the service contract, and a copy of all data contracts, message contracts, and fault contracts necessary to support calls to and from service operations.

Consider the service contract and data contract in Figure 3. The resulting code (not including endpoint configuration) from proxy generation is shown in Listing One.

[ServiceContract(Namespace = "http://www.thatindigogirl.com/

 samples/VS2008")]

public interface IMessagingService

{

  [OperationContract]

 void SendMessage(MessageInfo message);

}

[DataContract(Namespace = "http://schemas.thatindigogirl.com/

 samples/VS2008")]

public class MessageInfo

{

  [DataMember(IsRequired=true, Order=0)]

 public Guid Id { get; set; }

  [DataMember(IsRequired = true, Order = 1)]

 public string Subject { get; set; }

  [DataMember(IsRequired = true, Order = 2)]

 public string Message { get; set; }

}

Figure 3: Service contract and data contract for the MessagingService.

The benefits of generating proxies using SvcUtil include:

  • All the necessary metadata required to communicate with the service is generated for you.
  • Data contracts implement IExtensibleDataObject for version tolerance and INotifyPropertyChanged to support client-side data binding.
  • A class (proxy) that wraps the underlying client communication channel is created for you, thus you need only scope the proxy and begin calling service operations with it.
  • The proxy provides access to other underlying channel properties, such as SessionId and Open and Close methods.
  • This model decouples clients and services so clients and services can be versioned separately. Changes to the service contract or related data contracts will not impact client compilation, but may affect the client at run time if changes are not backward compatible.
  • Endpoint configuration necessary to initialize the proxy is automatically generated.

The drawbacks of generating proxies with SvcUtil include:

  • The code is generally more cluttered. Data contracts include property notification features you may not use. Service contracts expand several OperationContractAttribute values that could be inferred (Action, ReplyAction), and add ProtectionLevel settings even if the service doesn t require it. Both have superfluous debugging and code generation attributes.
  • Although generated data contracts will serialize properly to communicate with the service, the object model may not match that of the service type definition because it is derived from the WSDL schema. Programmers working on both sides must learn two object models as a result.
  • Generating proxies from WSDL decouples service and client development. If you own both sides, that could mean clients aren t notified at compile time of a change they should be updating their code to reflect in the service or data contracts. They ll discover the problem at run time only if backward compatibility is an issue. If you want to keep both sides in sync, this may not be ideal.
  • If you add code to the generated proxy to catch certain types of exceptions after each call, and recreate the inner channel wrapped by the proxy in case of a faulted channel (or, any other code that can t be provided in a partial class definition), regenerating the proxy will erase this custom code.

As an alternative to generating the proxy with SvcUtil, you can construct the channel directly using ChannelFactory (DuplexChannelFactory where callbacks are involved). Thus, the client coding experience would change from using the generated proxy like this:

ServiceReference.MessagingServiceClient proxy =

 new MessagingServiceClient("wsHttp");

MessageInfo msg = new MessageInfo { Id = Guid.NewGuid(),

 Subject = string.Format("Message {0}", ++m_counter),

 Message = string.Format("This is message {0}", m_counter) };

proxy.SendMessage(msg);

 

to creating the channel by hand like this:

 

ChannelFactory factory =

 new ChannelFactory("wsHttp");

IMessagingService proxy = factory.CreateChannel();

MessageInfo msg = new MessageInfo { Id = Guid.NewGuid(),

 Subject = string.Format("Message {0}", ++m_counter),

 Message = string.Format("This is message {0}", m_counter) };

proxy.SendMessage(msg); 

But, this also implies that the metadata from Figure 3 is somehow available to the client application, and there was a way to generate the client endpoint configuration necessary to initialize the channel. When you own both sides, this may be done by sharing metadata assemblies between client and service developers, and communicating endpoint requirements in some other way rather than generating them with SvcUtil.

The benefits of this approach include:

  • Less overall clutter in the client code.
  • More control over proxy wrapper code needed for the client to handle faulted channels and other customizations.
  • Clients and services share assemblies, thus are both immediately updated when changes are made.
  • Developers work with the same object model at the client and service for metadata types.

The drawbacks are:

  • This raw approach to creating the channel factory and channel may not be perceived as friendly as the generated proxy.
  • You don t have access to channel properties such as SessionId (from IContextChannel) or Open and Close methods (from ICommunicationObject) without casting explicitly to those interfaces first. The generated proxy base class (ClientBase or DuplexClientBase) makes these properties directly available.

As a general rule, when you own both sides you ll likely find it more useful to share metadata assemblies between clients and services (as I ll discuss shortly). But, when you want versioning between clients and services to be independent, or if you are consuming services you don t own, proxy generation makes it the best approach to creating the initial configuration, metadata, and proxy wrapper necessary to communicate with the service.

 

Sharing Metadata

Clients require service metadata to build a channel to communicate with services. That metadata includes definitions for the service contract and any related data contracts, message contracts, fault contracts, or otherwise serializable types leveraged by service operations. You can generate this metadata entirely with SvcUtil, generate parts of it while sharing some metadata assemblies, or take the approach of sharing all metadata assemblies between clients and services. In this section, I ll discuss these approaches.

Figure 4 illustrates the result of proxy generation as I ve discussed thus far. In this case, the client relies solely on WSDL to generate copies of the service contract, and any other contracts (including data contracts). In addition, a proxy class is created to simplify client coding efforts.


Figure 4: Proxy generation using SvcUtil by default doesn t share assemblies.

You can also use SvcUtil to generate the proxy, while sharing metadata libraries for your data contracts with the service. This hybrid result is illustrated in Figure 5, where the proxy and service contract are still generated, but data contract assemblies are shared. Prior to Visual Studio 2008 you could only achieve this using SvcUtil with the /r option:

svcutil /d:[output path] /o:proxy.cs

 /r:Messaging.Entities.dll http://localhost:8000

Now, Visual Studio 2008 will automatically reuse types from referenced assemblies (as shown by the settings in Figure 2). That means all you must do is reference the data contract assemblies you want to share (such as Messaging.Entities), prior to generating the service reference.

NOTE: Even if you reference assemblies that contain message contracts or service contracts you want to share, SvcUtil will only look for data contracts and other serializable types that are used by service operations or fault contracts.


Figure 5: Proxy generation using SvcUtil sharing data contract assemblies.

If you want to achieve a result where client and service developers both work with the same object model for all metadata including service contracts, message contracts, data contracts, and other serializable types you won t use SvcUtil to generate your client proxy. Instead, you ll use ChannelFactory or DuplexChannelFactory as appropriate, and share all metadata assemblies. Figure 6 illustrates this result.


Figure 6: Sharing metadata assemblies between client and service.

NOTE: In general, I recommend you break up your projects so you have separate assemblies for service contracts, message contracts, fault contracts, and data contracts or other serializable entities. This makes it easier to share assemblies down the road, without sharing service logic with clients.

 

Conclusion

Proxy generation techniques have definitely improved in Visual Studio 2008, making it easier to pass custom options to the SvcUtil proxy generation process using a simple dialog box. This definitely streamlines proxy generation efforts for common scenarios such as customization of collection and dictionary types, creating asynchronous proxies, or sharing data contract libraries. In the latter case, it is most likely that you own both sides: the client and service. This often means you want to share more than just data contract assemblies, but also those that contain metadata for service contracts and message contracts (to keep developers working with a consistent object model). In this case, you can skip proxy generation altogether and create the channel directly losing direct access to a few additional properties that the generated proxy provided.

Regardless of your approach, you ll most likely want to create your own client proxy wrappers that can encapsulate common exception handling needs and recovery from faulted channels. I ll discuss this technique in the next installment of this column. Until then, enjoy!

Code for this article is available at http://www.dasblonde.net/downloads/aspprodec07.zip. The code is based on Visual Studio 2008 (Beta 2).

 

Michele Leroux Bustamante is Chief Architect of IDesign Inc., Microsoft Regional Director for San Diego, Microsoft MVP for Connected Systems, and a BEA Technical Director. At IDesign Michele provides training, mentoring, and high-end architecture consulting services focusing on Web services, scalable and secure architecture design for .NET, federated security scenarios, Web services, interoperability, and globalization architecture. She is a member of the International .NET Speakers Association (INETA), a frequent conference presenter, conference chair for SD West, and is frequently published in several major technology journals. Michele is also on the board of directors for IASA (International Association of Software Architects), and a Program Advisor to UCSD Extension. Her latest book is Learning WCF (O Reilly, 2007); see her book blog at http://www.thatindigogirl.com. Reach her at mailto:[email protected] or visit http://www.idesign.net and her main blog at http://www.dasblonde.net.

Begin Listing One Generated service contract, data contract, and proxy wrapper for MessagingService from Figure 3

namespace WinClient.ServiceReference {

 using System.Runtime.Serialization;

 using System;

  [System.Diagnostics.DebuggerStepThroughAttribute()]

  [System.CodeDom.Compiler.GeneratedCodeAttribute(

   "System.Runtime.Serialization", "3.0.0.0")]

  [System.Runtime.Serialization.DataContractAttribute(

    Name="MessageInfo", Namespace=

    "http://schemas.thatindigogirl.com/samples/VS2008")]

  [System.SerializableAttribute()]

 public partial class MessageInfo : object,

    System.Runtime.Serialization.IExtensibleDataObject,

    System.ComponentModel.INotifyPropertyChanged {

      [System.NonSerializedAttribute()]

     private System.Runtime.Serialization.

       ExtensionDataObject extensionDataField;

     private System.Guid IdField;

     private string SubjectField;

     private string MessageField;

      [global::System.ComponentModel.BrowsableAttribute(

        false)]

     public System.Runtime.Serialization.ExtensionDataObject

        ExtensionData {

         get {

             return this.extensionDataField;

         }

         set {

             this.extensionDataField = value;

         }

     }

      [System.Runtime.Serialization.DataMemberAttribute(

        IsRequired=true)]

     public System.Guid Id {

         get {

             return this.IdField;

         }

         set {

             if ((this.IdField.Equals(value) != true)) {

                 this.IdField = value;

                 this.RaisePropertyChanged("Id");

             }

         }

     }

      [System.Runtime.Serialization.DataMemberAttribute(

        IsRequired=true)]

     public string Subject {

         get {

             return this.SubjectField;

         }

         set {

             if ((object.ReferenceEquals(this.SubjectField,

                  value) != true)) {this.SubjectField = value;

                   this.RaisePropertyChanged("Subject");

             }

         }

     }

      [System.Runtime.Serialization.DataMemberAttribute(

        IsRequired=true, Order=2)]

     public string Message {

         get {

             return this.MessageField;

         }

         set {

             if ((object.ReferenceEquals(this.MessageField,

                  value) != true)) {this.MessageField = value;

                   this.RaisePropertyChanged("Message");

             }

         }

     }

     public event System.ComponentModel.

       PropertyChangedEventHandler PropertyChanged;

     protected void RaisePropertyChanged(

       string propertyName) {System.ComponentModel.

       PropertyChangedEventHandler propertyChanged =

       this.PropertyChanged;

         if ((propertyChanged != null)) {

               propertyChanged(this, new System.

              ComponentModel.PropertyChangedEventArgs

               (propertyName));

         }

     }

 }

  [System.CodeDom.Compiler.GeneratedCodeAttribute(

   "System.ServiceModel", "3.0.0.0")]

  [System.ServiceModel.ServiceContractAttribute(Namespace=

   "http://www.thatindigogirl.com/samples/VS2008",

   ConfigurationName="ServiceReference.IMessagingService")]

 public interface IMessagingService {

      [System.ServiceModel.OperationContractAttribute(

        Action="http://www.thatindigogirl.com/samples/

        VS2008/IMessagingService/SendMessage", ReplyAction=

        "http://www.thatindigogirl.com/samples/VS2008/

        IMessagingService/SendMessageRespons" + "e")]

     void SendMessage(WinClient.ServiceReference.

       MessageInfo message);

 }

  [System.CodeDom.Compiler.GeneratedCodeAttribute(

    "System.ServiceModel", "3.0.0.0")]

 public interface IMessagingServiceChannel :

   WinClient.ServiceReference.IMessagingService,

    System.ServiceModel.IClientChannel {

 }

  [System.Diagnostics.DebuggerStepThroughAttribute()]

  [System.CodeDom.Compiler.GeneratedCodeAttribute(

    "System.ServiceModel", "3.0.0.0")]

 public partial class MessagingServiceClient :

   System.ServiceModel.ClientBase, WinClient.

  ServiceReference.IMessagingService {

     public MessagingServiceClient() {

     }

     public MessagingServiceClient(string

       endpointConfigurationName) : base(

       endpointConfigurationName) {

     }

     public MessagingServiceClient(string

       endpointConfigurationName, string remoteAddress) :

       base(endpointConfigurationName, remoteAddress) {

     }

     public MessagingServiceClient(string

       endpointConfigurationName, System.ServiceModel.

       EndpointAddress remoteAddress) :

       base(endpointConfigurationName, remoteAddress) {

     }

     public MessagingServiceClient(System.ServiceModel.

       Channels.Binding binding, System.ServiceModel.

       EndpointAddress remoteAddress) : base(binding,

       remoteAddress) {

     }

     public void SendMessage(WinClient.ServiceReference.

       MessageInfo message) {

         base.Channel.SendMessage(message);

     }

 }

}

End Listing One

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish