WCF as a Replacement for COM

06 July 2011

Microsoft presents Windows Communication Foundation (WCF) as a new and complete solution for creating distributed computing solutions of all kinds. Microsoft clearly intends WCF to supersede COM+ (Enterprise Services), but doesn't make clear if this also includes COM, which is still very widely used for inter-application and 'plug-in' type program interfaces.

In this post I'll review just how realistic it is to view WCF as a replacement for COM in relation to inter-application communication on a single machine. We'll create the following:

  • A COM component
    The COM component will which provides two simple methods and be consumed by a .NET WPF client which will evaluate the performance of various calls
  • A .NET assembly
    The assembly will implement the equivalent functionality of the COM component
  • A WCF equivalent of the COM component
    The WCF service will expose two endpoints: one using a named pipe binding, the other a TCP binding
  • A WPF-based test client
    The test client will consume the COM component, the .NET assembly and the WCF service

Creating the COM component

  1. Create a .NET Class Library project using Visual Studio 2010
  2. This project will be a .NET assembly that exposes its public interface as a COM object
  3. Create a class for the COM object – notice that the IntArray() method returns an array of random integers of a parameterized size:
  4. public class COMClass
    {
        public COMClass() { }
    
        public string Hello()
        {
            return "Hello from COM";
        }
    
        public int[] IntArray(int size)
        {
            if(size < 1)
                size = 1;
    
            Random r = new Random();
            int[] tmp = new int[size];
            for (int i = 0; i < size; i++)
                tmp[i] = r.Next();
    
            return tmp;
        }
    }
    
  5. Right-click the project file in Solution Explorer and select Properties
  6. Select the Build tab and make sure Register for COM interop is checked:
  7. Build the project and then register the assembly:

      REGASM COMMClassLibrary.dll

Creating the .NET Assembly

  1. This is a standard .NET Class Library project. Simply create a new project and add the code for the class shown in the COM example above
  2. Build the project – we'll add a reference to it in our client test app

Creating the WCF Services and Host

  1. Here we'll create a WCF service that is configured to provide two endpoints, one via TCP, the other via a named pipe. We'll also create a simple console-based host for the service
  2. Create a new Console project and then add a new WCF Service to the project
  3. Rename the interface and class files as required (I called mine ICOMPerformanceService and COMPerformanceService)
  4. Define the interface as follows:
  5. 
    [ServiceContract]
    public interface ICOMPerformanceService
    {
        [OperationContract]
        string Hello();
    
        [OperationContract]
        int[] IntArray(int size);
    }
    
  6. Implement the interface:
  7. public class COMPerfService : ICOMPerformanceService
    {
        public string Hello()
        {
            return "Hello from WCF";
        }
    
        public int[] IntArray(int size)
        {
            if (size < 1)
                size = 1;
    
            Random r = new Random();
            int[] tmp = new int[size];
            for (int i = 0; i < size; i++)
                tmp[i] = r.Next();
    
            return tmp;
        }
    }
    
  8. To create the service host, modify Program.cs:
  9. class Program
    {
        static void Main(string[] args)
        {
            ServiceHost host = new ServiceHost(typeof(COMPerfService));
            host.Open();
    
            Console.WriteLine("Service running. [Enter] to close");
            Console.ReadLine();
    
            host.Close();
        }
    }
    
  10. Finally, modify the app.config file to create two endpoints for the service.
    Notice that we name both of the endpoints. This is so that our client app will be able to specify which endpoint (for a specific binding) to use :
  11. <?xml version="1.0" encoding="utf-8" ?>
    <configuration>
      <system.serviceModel>
        <services>
          <service name="COMPerformanceConsoleApp.COMPerfService">
            <endpoint name="COMPerfServiceTcp" 
                      address="" binding="netTcpBinding" 
                      contract="COMPerformanceConsoleApp.ICOMPerformanceService" />
            <endpoint name="COMPerfServicePipe" 
                      address="" binding="netNamedPipeBinding" 
                      contract="COMPerformanceConsoleApp.ICOMPerformanceService" />
            <host>
              <baseAddresses>
                <add baseAddress="net.tcp://localhost" />
                <add baseAddress="net.pipe://localhost" />
                <add baseAddress="http://localhost:9008"/>
              </baseAddresses>
            </host>
          </service>
        </services>
        <behaviors>
          <serviceBehaviors>
            <behavior>
              <serviceMetadata httpGetEnabled="true" />
            </behavior>
          </serviceBehaviors>
        </behaviors>
      </system.serviceModel>
    </configuration>
    
  12. Build and run the project – leave the service running so that you can add a reference to it when creating the client app in the next stage

Creating the WPF Test Client

  1. Start a new instance of Visual Studio and create a new WPF project
  2. Add a reference (a normal assembly reference to the .NET assembly created previously) by browsing to the required assembly file
  3. In the same way as the previous step, add a reference to the COM component.
    Because the COM interface is exposed via a .NET assembly, you can't add the reference via the COM tab:
  4. If you attempt to add a COM reference in this way you'll you get an error
  5. Make sure the WCF service created previously is running, then add a service reference to it
  6. Create a simple interface:
  7. The modify the MainWindow.cs code as follows. Notice that we can vary the number of consecutive calls that are made to the IntArray() method in each of the services (COM, .NET and WCF):
  8. public partial class MainWindow : Window
    {
        COMClassLibrary.COMClass comClass;
        ServiceRef.COMPerformanceServiceClient wcfServiceTcp;
        ServiceRef.COMPerformanceServiceClient wcfServicePipe;
        DotNetClassLibrary.Class1 dotNetClass;
    
        public MainWindow()
        {
            InitializeComponent();
    
            comClass = new COMClassLibrary.COMClass();
                dotNetClass = new DotNetClassLibrary.Class1();
    
                // Notice that we specify particular endpoints (bindings) 
                wcfServiceTcp = new 
                  ServiceRef.COMPerformanceServiceClient("COMPerfServiceTcp");
                wcfServicePipe = new 
                  ServiceRef.COMPerformanceServiceClient("COMPerfServicePipe");
         }
    
         // Start Button click handler
         private void button2_Click(object sender, RoutedEventArgs e)
         {
             listBox1.Items.Add("Results:");
    
             TimeSpan tsCOM = DoCalls(comClass);
             listBox1.Items.Add("  COM = " + 
               tsCOM.Milliseconds.ToString() + " millisecs");
    
             TimeSpan tsTCP = DoCalls(wcfServiceTcp);
             listBox1.Items.Add("  TCP = " + 
               tsTCP.Milliseconds.ToString() + " millisecs");
    
             TimeSpan tsPipe = DoCalls(wcfServicePipe);
             listBox1.Items.Add("  Pipe = " + 
               tsPipe.Milliseconds.ToString() + " millisecs");
    
             TimeSpan tsDotNet = DoCalls(dotNetClass);
             listBox1.Items.Add("  .NET = " + 
               tsDotNet.Milliseconds.ToString() + " millisecs");
        }
    
        private TimeSpan DoCalls(dynamic service)
        {
            int nCalls = int.Parse(textBox1.Text);
            DateTime dt1 = DateTime.Now;
            int[] j;
    
            // Call the service method nCalls times
            for (int i = 0; i < nCalls; i++)
                j = service.IntArray(10);
    
            DateTime dt2 = DateTime.Now;
            return dt2.Subtract(dt1);
        }
    
        private void button1_Click(object sender, RoutedEventArgs e)
        {
            listBox1.Items.Clear();
        }
    }
    
  9. Run the application and click Start – you should see something like the following:
  10. Running the test a number of times and averaging the data I built up the following table and graph of results (all times are in milliseconds):

Conclusions

As can be seen from the above results, calls to the WCF service (both named pipes and TCP) incurred a very significantly higher overhead than for calls to the COM object and .NET assembly. Now, this simple and unrealistic test is bound to show that calls to WCF services incur greater overheads than simple calls to COM components or .NET assemblies.

If performance is at the top of your list of requirements, using a WCF-based architecture is clearly not the right choice. However, WCF was designed to address a wide-range of architectural scenarios and provides benefits other than sheer performance, including flexibility, loose-coupling, topology agnosticism (i.e. the same solution will work cross-process and cross-machine), a standardized approach to security and authentication, etc.

As Microsoft's Dave Cliffe commented in an article on WCF performance, "...there is almost always a fundamental trade-off between performance and flexibility, i.e. there is often a cost for abstraction."

And that's the key: abstraction.

I've had the "abstraction verses hand-crafted performance" argument on many occasions in the last 25 years (Assembler verses C, Win SDK verses MFC, sockets verses web services, etc.). In my experience, unless there's an overriding reason not to, abstraction from detail is the best way to go. But it's a judgement call that needs to be made on a case-by-case basis.

Note that I did also do some testing to see if IIS/AppFabric hosting would produce better results, but, as you can see from the table and graph below, performance (at least for the single-user scenario I was using) is broadly comparable: