Service-to-Service Interactions

Applies to: Windows Communication Foundation

Published: June 2011

Author: Bill Collette

Referenced Image

This topic contains the following sections.

  • Introduction
  • Data Mapping
  • Service Invocation
  • Service Granularity
  • Transferring Large Data Sets
  • Conclusion
  • Additional Resources

Introduction

When you build WCF services, you must consider how they will interact with each other. When you add a service reference to your project, will you reuse existing types in referenced assemblies? When you build a service-oriented architecture, will it contain several atomic services that contain a few operations, or will it contain large, expansive services that have many operations? Considering these questions in advance will help you to build an effective architecture and interaction strategy.

Data Mapping

Data mapping between services can be cumbersome if there are interactions with several different services. If one service invokes another service, one or all of the data elements that have been passed to the first service may also need to be passed to the other services. An example of such a data element is a customer entity that is passed in from the client to an order service. Before the order can be processed, data is required from a shipping service, which, in turn, needs the same customer information as the order service.

In this situation, many of the data elements that the order service needs are similar to the ones needed by the shipping service, and must be mapped. One possibility to consider when several services share the same data elements is to centralize your types into a common project. When you generate a proxy to one of the services, you can select the option to reuse types in referenced assemblies. This option can minimize the amount of data mapping that you must do when there are entities that are shared between services.

The following illustration shows the Reuse types in referenced assemblies check box.

Referenced Image

Another possibility is to use or develop a utility that relies on .NET reflection. Such a utility can reduce the overhead of manually mapping one entity to another. However, .NET reflection is not always the best choice because of its impact on run-time performance. AutoMapper found here http://automapper.codeplex.com/ is one such utility that you can use for your data mapping. Another alternative is manual data mapping. If you choose this approach, it is a good practice to build unit tests that ensure that the mapping is correct for each data element.

Service Invocation

It is important to manage invocations consistently when you invoke a service from a service. For example, if the service being invoked ends up in a faulted state, it is preferable to abort the channel rather than to close it, which can cause another exception.

This is especially important if your WCF service is configured to run as a singleton, which means that one instance of the service is shared across all callers. Closing and aborting channels appropriately ensures that callers do not receive unexpected errors.

Invocations Without Proxies

If you control the services that interact with each other, you may want to centralize your contracts into a separate project. The calling service can then reference these contract projects, which allows you to invoke the services without a proxy. his means that there are no proxies to regenerate when you change a service. In other words, centralization can decrease development time, and prevent later problems with stale proxies. A drawback to this approach is that it blurs the separation between two layers. A proxy provides an operational boundary. It is a contract that the caller and the service have agreed to. No proxy eliminates some overhead, but introduces more coupling. Weigh the advantages and the tradeoffs in terms of your own situation.

The following code shows a class named ClientProxy that contains some extension methods that are inherited from the ClientBase class in the System.ServiceModel namespace. This code allows you to handle service interactions in a consistent manner. It is a good practice to include it whenever you make a service invocation. Rather than duplicating the code throughout the service, use the extension methods to invoke the service.

using System;
using System.ServiceModel;

namespace WcfService1
{
    public class ClientProxy<TServiceClientInterface> 
        where TServiceClientInterface : class
    {
        //This class will be used internally, inheriting from ClientBase 
        class Client : ClientBase<TServiceClientInterface>
        {
            public TServiceClientInterface Service
            {
                get { return Channel; }
            }
        }

        //This extenstion method accomodates methods without a return value
        public void InvokeService(Action<TServiceClientInterface> action)
        {
            var client = new Client();

            try
            {
                action(client.Service);
                client.Close();
            }
            catch (Exception)
            {
                client.Abort();
                throw;
            }
        }

        //This extension method accomodates methods with a return value
        public TResult InvokeService<TResult>(Func<TServiceClientInterface, TResult> function)
        {
            var client = new Client();
            try
            {
                TResult result = function(client.Service);
                client.Close();
                return result;
            }
            catch (Exception)
            {
                client.Abort();
                throw;
            }
        }
    }
}

The following code shows how to use the ClientProxy utility class to invoke operations on contracts from your services.

namespace ConsoleApplication1
{
    class Program
    {
        private readonly static ClientProxy<WcfService1.Contracts.IService1> _myService;

        private static ClientProxy<WcfService1.Contracts.IService1> MyService
        {
            get { return _myService; }
        }
        static void Main(string[] args)
        {            
            MyService.InvokeService(client => client.GetData(1));           
        }
    }
}

These methods not only invoke the service, but they close the channel, and can abort the invocation if an exception occurs. Addionally, you can refer to MSDN for using the ChannelFactory class at https://msdn.microsoft.com/en-us/library/ms751400.aspx. This article contains a sample that demonstrates using the ChannelFactory class instead of generating a client for your service.

Service Granularity

Service granularity presents a complex problem to designers of WCF services. The "divide and conqueror" approach is a common solution. It splits a service up into separate components that each performs operations that are the same or related. In addition, each service either can be designed to have several lightweight operations that will increase performance, or a few chunky operations that minimize the number of invocations.

Chatty Services vs. Chunky Services

Generally, services are designed to be either chatty or chunky. A chatty service returns data with a simple structure, and exposes many finely-grained operations. A chunky service defines coarse-grained operations, and returns data in a complex, hierarchal structure. To return the requested data, chatty services require more calls than chunky services do. However, chatty services allow the client to request only data that is relevant. Chunky services return larger data sets, much of which may be extraneous. In many cases, these large data sets can introduce latency into your service.

Transferring Large Data Sets

WCF can process messages in one of two modes. By default, WCF processes messages in buffered mode. In buffered mode, WCF loads the entire message into memory before it is sent or after it is received. In other words, the entire message must be delivered before the receiver can read it. While buffered mode is efficient in most cases, buffering large data sets consumes large amounts of system resources, and the receiver must wait until it receives the entire message. This delay can cause performance issues on the server or the client.

In streaming mode, the information is available to the receiver before the sender delivers the entire message. Streaming mode increases performance on very large data sets because they can be processed as they are received. Streaming is available on all standard bindings except for the NetMsmqBinding and the MsmqIntegrationBinding.

If you stream large data sets, keep in mind that the service is constrained by the WCF default limits. You should increase the maxReceivedMessageSize value in your endpoint’s binding configuration.

For detailed instructions on how to enable streaming mode, see "How to: Enable Streaming" on MSDN at https://msdn.microsoft.com/en-us/library/ms789010.aspx. Additionally, you can refer to https://msdn.microsoft.com/en-us/library/ms733742.aspx about streaming large data.

Conclusion

When you build services in an environment that has many service-to-service interactions, consider how they will interact. If you have control of the services, you can share contracts and types to minimize data mapping. If you rely on proxies, you can use a utility such as AutoMapper to help you perform the mapping, but be aware of the performance implications. Alternatively, you can map your entities manually, but make sure your unit tests check for the correct data flow between the services. Finally, think about how your clients will interact with your service. Weigh the tradeoffs and benefits of many chatty calls versus a few chunky calls.

Additional Resources

For more information, see the following resource:

Previous article: Things to Consider When Designing a WCF Contract

Continue on to the next article: Things to Consider When Implementing a Load Balancer with WCF