How to do it: Best Design for WCF Web Services

Description Article
Table of Contents
Summary

Creating a WCF project through Visual Studio code implies too detailed code at both service configuration and definition levels. The same is true when developing the consumer client. This detail is often confusing and sometimes unnecessary.

Depending on the application the design of a WCF-system based assembly separation is recommended in order to maximize reusability and maintainability.

 

Visual Studio Solution for the best design of WCF service

Description

Why assembly separation ?

Data contracts and service contracts in a separated assembly:
In case of ,NET consumers, the contracts can be shared by them.

Services definition in a separated assembly:
Allow separation of the definition of services and hosts

Service host in a separated assembly:
Allow separation of the definition of services and hosts.

Client proxies in a separated assembly:
Allow more than one client application to use these services without code duplication

Why create manually ?

Service host’s web.config contains extraneous information:
Metadata endpoints it’s only required if planned to be used.

Avoid adding a service reference:
Service and data contracts are duplicated in case of a .NET consumer.
The developer does not have full control of the namespace name and the adding process creates bloated configuration.

 How to do it ?

Contract level

In case of a .NET Consummer when adding a service reference, Visual Studio will create a copy of the service and data contracts.
Using a separated assembly this can be used both on the service definition and service consummer sides.

Note: The option to have a single assembly or have multiple assemblies should be based on some logical contract grouping defined some how by the type of application under development

Service Definition level

In a multilayered system assembly separation provides the basis for reusability scenarios and promotes decoupling.
The service projects will reference only the contracts they need, and the services implement only those
referenced contracts.

Note: A service can provide implementations for multiple contracts, but this no way means that a consuming client must be exposed to all implementations. This will be set at configuration file indicating the contract type to consider:
This could be a scenario where some operations (contract A) are private and should not be exposed out the intranet. Other operations (contract B) are public and exposed over the internet.
This is defined at configuration level by inidicating the <contract> property on node <endpoint>.

Host level

Decoupling the services from the host allow services hosting in different types of hosts and change at any time (e.g Initially a service can be hosted as a IIS application and later be self-hosted). Service definition code is not altered
Hosts will find the service configuration information in either a web.config file or an app.config configuration file.
The configuration file should only contain the information required to expose the service(s): At a minimum, the configuration information containing <service> entries with one endpoint per contract.

If metadata excange is required (e.g. to obtain the WSDL) then a service behaviour configuration and a mex endpoint must also be added to the host configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <services>
      <service name="WCF.wcfTestServiceImpl" behaviorConfiguration="ServiceBehavior">
        <endpoint
          address="net.tcp://localhost:52503/wcfTestServiceImpl"
          binding="netTcpBinding"
          contract="WCF.IwcfTestServiceJSON" />
        <endpoint
          address="mex"
          binding="mexHttpBinding"
          contract="IMetadataExchange" />
        <host>
          <baseAddresses>
            <add baseAddress="http://localhost:8002/training/wcf/app/testservice1"/>
          </baseAddresses>
        </host>
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="ServiceBehavior" >
          <serviceMetadata httpGetEnabled="true" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>


Client/Consummer level (.NET clients only)

The “Add Service Reference” creates the proxy class to access a service’s implementation but it is the most blame feature for generating a configuration file filled with unrequired extra-information.

The proxy class is easily build manually.
Keeping proxies in a separated assembly provides the ability to use the proxies from more than one client application.

Client Proxies (.NET clients only)

A client application uses client proxy classes to connect to a service and use its operations.
Manually creating the client proxies classes, no copies of data and service contracts are required and these will be reusable for different applications

The client proxy can be create on two different manners:

  • OPTION 1: Proxy class inherits from generic ClientBase class parameterized with the interface to which the client should be able to access  Client configuration , which will reside at the client application level. The proxy class is can only instantiate one service contract interface.
  • OPTION 2: Proxy class using ChannelFactory class. The ChannelFactory class creates a channel at runtime. When instantiated it passes the target service contract and then call the CreateChannel method.

The end result is a channel class returned as the type of the service contract interface initially passed and the service contract’s operations can be called normally.

The proxy class can then be instantiated for one or multiple service contract interfaces.

Client Applications

Any client application can access any of the service contracts by referencing the appropriate client proxy assembly and instantiating its proxy classes.

The client applications requires only a reference to the proxy assembly, and also some configuration information.

The proxies are designed to access specific service contracts but the configuration defines where those contracts can be found:

This is achieved by configuring an endpoint definition per contract in the <clients> section:

<system.serviceModel>
  <client>
    <endpoint
      address="http://localhost:8002/training/wcf/app/testservice1"/>
      binding="netTcpBinding"
      contract="WCF.IwcfTestServiceJSON" />
    <endpoint
	  address="http://localhost:5992/training/wcf/app/testservice1"
      binding="wsHttpBinding"
      contract="WCF.IwcfTestServiceXML" />
  </client>
</system.serviceModel>

 

Operations
Project "Contracts"
How to do it

File “Common.Data.cs”

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Runtime.Serialization;

namespace WCF
{
    [DataContract]
    public class Category
    {
        [DataMember]
        public int Id { get; set; }
        [DataMember]
        public string Name { get; set; }
        [DataMember]
        public string Description { get; set; }
    }
}

File “IwcfTestServiceJSON.cs”

using System.ServiceModel;
using System.Collections.Generic;

namespace WCF
{
    /// <summary>
    /// Summary description for wcfTestService service contract (output as JSON)
    /// </summary>
    [ServiceContract(Namespace = "http://www.training.com")]
    public interface IwcfTestServiceJSON
    {
        [OperationContract]
        Category getCategoryWithDataContractAsJSON(string pId);

        [OperationContract]
        List<Category> getCategoriesWithDataContractAsJSON();

        [OperationContract]
        int addCategory(string pName);
    }
}

File “IwcfTestServiceXML.cs”

using System.ServiceModel;
using System.Collections.Generic;

namespace WCF
{
    /// <summary>
    /// Summary description for wcfTestService service contract (output as XML)
    /// </summary>
    [ServiceContract(Namespace = "http://www.training.com")]
    public interface IwcfTestServiceXML
    {
        [OperationContract]
        Category getCategoryWithDataContractAsXML(string pId);

        [OperationContract]
        List<Category> getCategoriesWithDataContractAsXML();

        [OperationContract]
        int addCategoryWithDataContractAsXML(Category pCategory);
    }
}
Project "Services"
How to do it

File “wcfTestServiceImpl.cs”

using System.Collections.Generic;
using System;

namespace WCF
{
    /// <summary>
    /// Summary description for wcfTestService service contract implementation
    /// </summary>
    public class wcfTestServiceImpl : IwcfTestServiceXML, IwcfTestServiceJSON
    {

        #region "IwcfTestServiceXML Members"

        public Category getCategoryWithDataContractAsXML(string pId)
        {
            //TODO: Put code here to retrieve category...
            return loadCategoryWithDataContract(Convert.ToInt32(pId));
        }

        public List<Category> getCategoriesWithDataContractAsXML()
        {
            //TODO: Put code here to retrieve category names...
            return loadCategoriesWithDataContract();
        }

        public int addCategoryWithDataContractAsXML(Category pCategory)
        {
            //TODO: Put code here to save category defintion
            return 10;
        }

        #endregion

        #region "IwcfTestServiceJON Members"

        public string getCategoryAsJSON(string pID)
        {
            //TODO: Put code here to retrieve category name...
            return loadCategory(Convert.ToInt32(pID));
        }

        public Category getCategoryWithDataContractAsJSON(string pId)
        {
            //TODO: Put code here to retrieve category...
            return loadCategoryWithDataContract(Convert.ToInt32(pId));
        }

        public List<Category> getCategoriesWithDataContractAsJSON()
        {
            //TODO: Put code here to retrieve category names...
            return loadCategoriesWithDataContract();
        }

        #endregion

        #region "IwcfTestServiceXML ãnd IwcfTestServiceJSON Members"

        public int addCategory(string pName)
        {
            //TODO: Put code here to save category name
            return 10;
        }

        private string loadCategory(int pId)
        {
            return "This the category with name for ID " + pId.ToString();
        }

        private List<string> loadCategoryNames()
        {
            return new List<string>() { "Category #1", "Category #2" };
        }

        private Category loadCategoryWithDataContract(int pId)
        {
            return new Category { Id = 1, Name = string.Format("category #{0}", pId), Description = string.Format("test category #{0}", pId) };
        }

        private List<Category> loadCategoriesWithDataContract()
        {
            return new List<Category>() {
                            new Category { Id=1, Name="category #1", Description= "test category #1"},
                            new Category { Id=2, Name="category #2", Description= "test category #2"} };
        }

        #endregion

    }
}
Project "Proxies"
How to do it

File “TestServiceProxyUsingChannelFactory.cs”

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;

namespace WCF
{
    public class TestServiceProxyUsingChannelFactory : IwcfTestServiceXML
    {
        IwcfTestServiceXML _categoryChannel = null;

        public TestServiceProxyUsingChannelFactory()
        {
            IwcfTestServiceXML _categoryChannel = new ChannelFactory<IwcfTestServiceXML>().CreateChannel();
        }

        #region IwcfTestServiceXML Members

        public Category getCategoryWithDataContractAsXML(string pId)
        {
            IwcfTestServiceXML category = new ChannelFactory<IwcfTestServiceXML>().CreateChannel();

            return _categoryChannel.getCategoryWithDataContractAsXML(pId);
        }

        public List<Category> getCategoriesWithDataContractAsXML()
        {
            return _categoryChannel.getCategoriesWithDataContractAsXML();
        }

        #endregion
    }

}

File “TestServiceProxyUsingClientBase.cs”

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;

namespace WCF
{
    public class TestServiceProxyUsingClientBase : ClientBase<IwcfTestServiceXML>, IwcfTestServiceXML
    {
        #region IwcfTestServiceXML Members

        public Category getCategoryWithDataContractAsXML(string pId)
        {
            return Channel.getCategoryWithDataContractAsXML(pId);
        }

        public List<Category> getCategoriesWithDataContractAsXML()
        {
            return Channel.getCategoriesWithDataContractAsXML();
        }

        #endregion
    }

}
Project "selfHost"
How to do it

File “Program.cs”

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using WCF;

namespace WCF.seflHost
{
    class Program
    {
        static void Main(string[] args)
    {
       ServiceHost host =
          new ServiceHost(typeof(wcfTestServiceImpl));

       host.Open();

       Console.WriteLine("Test console host initiated: Press <enter> to close host application");

       Console.ReadLine();

   }    }
}

File “App.config”

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <services>
      <service name="WCF.wcfTestServiceImpl" behaviorConfiguration="ServiceBehavior">
        <endpoint
          address="net.tcp://localhost:52503/wcfTestServiceImpl"
          binding="netTcpBinding"
          contract="WCF.IwcfTestServiceJSON" />
        <endpoint contract="IMetadataExchange" binding="mexHttpBinding" address="mex" />
        <host>
          <baseAddresses>
            <add baseAddress="http://localhost:8002/training/wcf/app/testservice1"/>
          </baseAddresses>
        </host>
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="ServiceBehavior" >
          <serviceMetadata httpGetEnabled="true" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>
Project "wcfIIS70WebHost"
How to do it

File “wcfTestServiceImpl.svc”

 <%@ ServiceHost Service="WCF.wcfTestServiceImpl" %>

File “web.config”

<system.serviceModel>
    <services>
      <service name=
         "WCF.wcfTestServiceImpl">
        <endpoint
          address=""
          binding="wsHttpBinding"
          contract="WCF.IwcfTestServiceXML" />
      </service>
    </services>
  </system.serviceModel>
References

[1]  Video describing how to build a bad and good WCF service
[2] Article about WCF the Manual Way, the bad and the Right Way