Monday 28 May 2012

Hosting WCF Services by extending SeviceHostingFactory

There are many ways to host WCF services, such as using IIS, WAS, Windows Server AppFabric, Windows Service. Following is the msdn url for describing the same – http://msdn.microsoft.com/en-us/library/ms729846.aspx.

Even thou we have multiple ways to deploy the services, we need some custom functionalities required be taken while exposing the services.

This post explains hosting WCF services using ServiceHostFactory with an example. This implementation entirely based on the following msdn article - http://msdn.microsoft.com/en-us/library/aa702697.aspx

The following points explain the use of exposing the services using ServiceHostFactory:
  1. It helps the project development to adopt a transparent and agile development process.
  2. It helps to automate the process of building software according to accepted patterns and predefined standards.
  3. It helps by creating a template for exposing services easier. So creating a new service on existing application or creating a new application and adding services became easier.
  4. It helps customizing the software factory code at one place and the effect takes place for all other services.
  5. It helps to fetch configuration values from various resources while the service hosted and assign to the service object.
For implementing this concept, I have two projects the service project and the client project.

The Service Application
There are three projects build with the Service Solution which is mentioned below.
  1. DNT.WCFHost – Is the Service project which has the .svc files and Web.Config file.
  2. DNT.Services – Is a component which contains the logic for all the Services such as ServiceContract and ServiceBehavior.
  3. DNT.Entity – Is the entity project which has business entity classes.
Note:
  1. This implementation refers the Northwind database from the backend. So make sure to setup the same in the SQL Server for running the code.
  2. I am planning to host a Customer data with this code which actually does the CRUD operations.
DNT.WCFHost
This project only contains the .svc files without code behind files and Web.Config file. So, when adding a new service file we can delete the .svc.cs file and interface file. The service markup must be pointing to Factory class.

For Ex: A Customer Service will have the following markup.
<%@ ServiceHost Service="CustomerServiceRoot" Factory="DNT.Services.DNTWcfServiceHostFactory" %>
The Web.Config will have different configuration nodes such as configSections, ServiceNameList, connectionStrings and Service.serviceModel.
<?xml version="1.0"?>
<configuration>
  <configSections>
    <section name="ServiceNameList" type="System.Configuration.NameValueSectionHandler"/>
  </configSections>
  
  <ServiceNameList>
    <add key="CustomerServiceRoot" value="DNT.Services.CustomerService, DNT.Services"/>
  </ServiceNameList>
  
  <connectionStrings>
    <add connectionString="Data Source=THIRUMALAI-NOTE;Initial Catalog=Northwind;Trusted_Connection=True;" name="SQLConnection"/>
  </connectionStrings>
  
  <system.web>
    <compilation debug="true" targetFramework="4.0" />
    <authentication mode="Windows"/>
    <pages controlRenderingCompatibilityVersion="3.5" clientIDMode="AutoID"/>
  </system.web>

  <system.serviceModel>
    <serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
    <services>
      <service behaviorConfiguration="WcfServices" name="DNT.Services.CustomerService">
        <endpoint address="" binding="wsHttpBinding" bindingConfiguration="wsHttpBinding_WcfServices" contract="DNT.Services.ICustomerService"/>
        <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
      </service>
    </services>
    
    <behaviors>
      <serviceBehaviors>
        <behavior name="WcfServices">
          <serviceDebug includeExceptionDetailInFaults="true" />
          <serviceMetadata httpGetEnabled="true" />
          <serviceThrottling maxConcurrentCalls="100" maxConcurrentSessions="100" />
        </behavior>
        <behavior name="">
          <serviceMetadata httpGetEnabled="true" />
          <serviceDebug includeExceptionDetailInFaults="false" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
    
    <bindings>
      <wsHttpBinding>
        <binding name="wsHttpBinding_WcfServices" transactionFlow="true" maxReceivedMessageSize="2147483647">
          <readerQuotas maxDepth="32" maxStringContentLength="2147483647" maxArrayLength="2147483647" maxBytesPerRead="2147483647" maxNameTableCharCount="2147483647"/>
        </binding>
      </wsHttpBinding>
    </bindings>
  </system.serviceModel>
</configuration>
This configuration has some highlighted script which required to be added when adding a new service.

So to add a new service –
  1. Add a new .svc file (Right click the Project and click Add New Item. Select WCF Service)
  2. Delete .svc.cs and interface files.
  3. Change the market as defined before
  4. Add additional configuration in the Web.Config file.
DNT.Servces
This project contains the logic for all the services such as ServiceContract and ServiceBehavior. It contains the following files –
  1. DNTWcfServiceHostFactory.cs – This is a factory class which contains logic for hosting the service using ServiceHostFactory.
  2. DNTFaultException.cs – This is an exception class which contains the exception data when passing to the client.
  3. ICustomerService.cs – This is the interface class which contains the ServiceContract for Customer Service.
  4. CustomerService.cs – This is a ServiceBehavior class which contains the logic for Customer Service.
DNTWcfServiceHostFactory
This class extents ServiceHostFactory class. This class takes the responsibility for hosting the services which are requested.
public class DNTWcfServiceHostFactory : ServiceHostFactory
{
    public DNTWcfServiceHostFactory()
    {
    }

    public override ServiceHostBase CreateServiceHost(string service, Uri[] baseAddresses)
    {
        return CreateServiceHost(service, baseAddresses, true);
    }

    public ServiceHost CreateServiceHost(string service)
    {
        return CreateServiceHost(service, null, false);
    }

    private static ServiceHost CreateServiceHost(string service, Uri[] baseAddresses, bool useURL)
    {
        try
        {
            string serviceTypeName = null;
            Type serviceType = null;
            ConstructorInfo serviceCtor = null;

            // Get the list of Services and it is code behind files
            IDictionary<string, string> ServiceNameList = new Dictionary<string, string>();

            // Get the ServiceNameList from Web.Config.
            // ServiceNameList is a node defined in Service Web.Config file which has service name and the code behind file (Namespace and class)
            // For Ex: <add key="CustomerServiceRoot" value="DNT.Services.CustomerService, DNT.Services"/>
            NameValueCollection ServiceNameListConfig = (NameValueCollection)ConfigurationManager.GetSection("ServiceNameList");
            if (ServiceNameListConfig != null)
            {
                // Assign the list of service value to ServiceNameList
                foreach (string key in ServiceNameListConfig.Keys)
                    ServiceNameList.Add(key, ServiceNameListConfig[key]);

                serviceTypeName = ServiceNameList[service] as string;
                serviceType = Type.GetType(serviceTypeName.Trim());

                // Get the constructor of the service object
                serviceCtor = serviceType.GetConstructor(new Type[0]);
                if (serviceCtor == null) // If constructor not available
                {
                    throw new Exception("Cannot create type," + serviceTypeName + ". Type must have zero param constructor.");
                }

                // create the host
                ServiceHost serviceHost = new ServiceHost(serviceCtor.Invoke(new object[0]), baseAddresses);

                return serviceHost;
            }
            else
                return null;
        }
        catch (Exception e)
        {
            throw e;
        }
    }
}
Note: Required to add assembly System.ServiceModel.Activation for ServiceHostFactory class.
DNTFaultException
This class handles the exceptions of the services.
[Serializable]
[DataContract]
public class DNTFaultException
{
    private ICollection data;
    private string message;
    private string stackTrace;

    [DataMember]
    public ICollection Data
    {
        get
        {
            return data;
        }
        set
        {
            data = value;
        }
    }

    [DataMember]
    public string Message
    {
        get
        {
            return message;
        }
        set
        {
            message = value;
        }
    }

    [DataMember]
    public string StackTrace
    {
        get
        {
            return stackTrace;
        }
        set
        {
            stackTrace = value;
        }
    }

    public DNTFaultException(ICollection data, string message, string stackTrace)
    {
        Data = data;
        Message = message;
        StackTrace = stackTrace;
    }

    public DNTFaultException()
    {
        // Do nothing
    }

    public static DNTFaultException CreateFromException(Exception exception)
    {
        return new DNTFaultException(exception.Data, exception.Message, exception.StackTrace);
    }
}
The source code for ICustomerService class
/// <summary>
/// Interface for defining the Customer Service functionalities.
/// </summary>
[ServiceContract(SessionMode = SessionMode.Required)]
public interface ICustomerService
{
    /// <summary>
    /// Method to get all the Customers
    /// </summary>
    /// <returns>List of Customers</returns>
    [OperationContract]
    [TransactionFlow(TransactionFlowOption.Allowed)]
    [FaultContract(typeof(DNTFaultException))]
    IList<Customer> GetAll();

    /// <summary>
    /// Method to get a particular Customer based on Customer Id
    /// </summary>
    /// <param name="customerID">Customer Id</param>
    /// <returns>Customer Object</returns>
    [OperationContract]
    [TransactionFlow(TransactionFlowOption.Allowed)]
    [FaultContract(typeof(DNTFaultException))]
    Customer Get(string customerID);

    /// <summary>
    /// Method to create a new Customer record in the system
    /// </summary>
    /// <param name="customer">Customer object</param>
    /// <returns>No of Row affected</returns>
    [OperationContract]
    [TransactionFlow(TransactionFlowOption.Allowed)]
    [FaultContract(typeof(DNTFaultException))]
    int Create(Customer customer);

    /// <summary>
    /// Method to Update existing Customer in the System
    /// </summary>
    /// <param name="customer">Customer object</param>
    /// <returns>No of Row affected</returns>
    [OperationContract]
    [TransactionFlow(TransactionFlowOption.Allowed)]
    [FaultContract(typeof(DNTFaultException))]
    int Update(Customer customer);

    /// <summary>
    /// Method to Delete an existing Customer from the system
    /// </summary>
    /// <param name="customerID">Customer Id</param>
    /// <returns>No of Row affected</returns>
    [OperationContract]
    [TransactionFlow(TransactionFlowOption.Allowed)]
    [FaultContract(typeof(DNTFaultException))]
    int Delete(string customerID);
}
The source code for CustomerService class
[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple,
                    InstanceContextMode = InstanceContextMode.Single)]
public class CustomerService : ICustomerService
{

    /// <summary>
    /// Method to get all the Customers
    /// </summary>
    /// <returns>List of Customers</returns>
    [OperationBehavior(TransactionScopeRequired = false)]
    public IList<Customer> GetAll()
    {
        try
        {
            // Opening the connection
            using (SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLConnection"].ConnectionString))
            {
                string strSQL = "Select CustomerID, CompanyName, ContactName, ContactTitle, Address, City, Region, PostalCode, Country, Phone, Fax From Customers";
                SqlCommand command = new SqlCommand(strSQL, connection);

                connection.Open();
                SqlDataReader dr = command.ExecuteReader(CommandBehavior.CloseConnection);

                IList<Customer> customerList = new List<Customer>();
                while (dr.Read())
                {
                    Customer customer = new Customer();
                    customer.CustomerID = dr["CustomerID"].ToString();
                    customer.CompanyName = dr["CompanyName"].ToString();
                    customer.ContactName = dr["ContactName"].ToString();
                    customer.ContactTitle = dr["ContactTitle"].ToString();
                    customer.Address = dr["Address"].ToString();
                    customer.City = dr["City"].ToString();
                    customer.Region = dr["Region"].ToString();
                    customer.PostalCode = dr["PostalCode"].ToString();
                    customer.Country = dr["Country"].ToString();
                    customer.Phone = dr["Phone"].ToString();
                    customer.Fax = dr["Fax"].ToString();

                    customerList.Add(customer);
                }
                return customerList;
            }
        }
        catch (Exception ex)
        {
            // Log the error

            // Rethrow
            throw new FaultException<DNTFaultException>(DNTFaultException.CreateFromException(ex), ex.Message);
        }
    }

    /// <summary>
    /// Method to get a particular Customer based on Customer Id
    /// </summary>
    /// <param name="customerID">Customer Id</param>
    /// <returns>Customer Object</returns>
    [OperationBehavior(TransactionScopeRequired = false)]
    public Customer Get(string customerID)
    {
        try
        {
            using (SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLConnection"].ConnectionString))
            {
                string strSQL = "Select CustomerID, CompanyName, ContactName, ContactTitle, Address, City, Region, PostalCode, Country, Phone, Fax From Customers " +
                                "Where CustomerID = '" + customerID + "'";
                SqlCommand command = new SqlCommand(strSQL, connection);

                connection.Open();
                SqlDataReader dr = command.ExecuteReader(CommandBehavior.CloseConnection);

                Customer customer = new Customer();
                while (dr.Read()) // If more then one record for a customer, it will return last one
                {
                    customer.CustomerID = dr["CustomerID"].ToString();
                    customer.CompanyName = dr["CompanyName"].ToString();
                    customer.ContactName = dr["ContactName"].ToString();
                    customer.ContactTitle = dr["ContactTitle"].ToString();
                    customer.Address = dr["Address"].ToString();
                    customer.City = dr["City"].ToString();
                    customer.Region = dr["Region"].ToString();
                    customer.PostalCode = dr["PostalCode"].ToString();
                    customer.Country = dr["Country"].ToString();
                    customer.Phone = dr["Phone"].ToString();
                    customer.Fax = dr["Fax"].ToString();
                }
                return customer;
            }
        }
        catch (Exception ex)
        {
            // Log the error

            // Rethrow
            throw new FaultException<DNTFaultException>(DNTFaultException.CreateFromException(ex), ex.Message);
        }
    }

    /// <summary>
    /// Method to create a new Customer record in the system
    /// </summary>
    /// <param name="customer">Customer object</param>
    /// <returns>No of Row affected</returns>
    [OperationBehavior(TransactionScopeRequired = false)]
    public int Create(Customer customer)
    {
        try
        {
            using (SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLConnection"].ConnectionString))
            {
                string strSQL = "Insert into Customers (CustomerID, CompanyName, ContactName, ContactTitle, Address, City, Region, Country, Phone, Fax) " +
                                "values ('" + customer.CustomerID.Replace("'", "") + "', '" + customer.CompanyName.Replace("'", "''") + "', '" +
                                                customer.ContactName.Replace("'", "''") + "', '" + customer.ContactTitle.Replace("'", "''") + "', '" +
                                                customer.Address.Replace("'", "''") + "', '" + customer.City.Replace("'", "''") + "', '" +
                                                customer.Region.Replace("'", "''") + "', '" + customer.Country.Replace("'", "''") + "', '" +
                                                customer.Phone.Replace("'", "''") + "', '" + customer.Fax.Replace("'", "''") + "')";

                SqlCommand command = new SqlCommand(strSQL, connection);

                connection.Open();

                return command.ExecuteNonQuery();
            }
        }
        catch (Exception ex)
        {
            // Log the error

            // Rethrow
            throw new FaultException<DNTFaultException>(DNTFaultException.CreateFromException(ex), ex.Message);
        }
    }

    /// <summary>
    /// Method to Update existing Customer in the System
    /// </summary>
    /// <param name="customer">Customer object</param>
    /// <returns>No of Row affected</returns>
    [OperationBehavior(TransactionScopeRequired = false)]
    public int Update(Customer customer)
    {
        try
        {
            using (SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLConnection"].ConnectionString))
            {
                string strSQL = "Update Customers Set " +
                                " CompanyName = '" + customer.CompanyName.Replace("'", "''") + "'," +
                                " ContactName = '" + customer.ContactName.Replace("'", "''") + "'," +
                                " ContactTitle = '" + customer.ContactTitle.Replace("'", "''") + "'," +
                                " Address = '" + customer.Address.Replace("'", "''") + "'," +
                                " City = '" + customer.City.Replace("'", "''") + "'," +
                                " Region = '" + customer.Region.Replace("'", "''") + "'," +
                                " Country = '" + customer.Country.Replace("'", "''") + "'," +
                                " Phone = '" + customer.Phone.Replace("'", "''") + "'," +
                                " Fax = '" + customer.Fax.Replace("'", "''") + "'" +
                                " Where CustomerID = '" + customer.CustomerID + "'";

                SqlCommand command = new SqlCommand(strSQL, connection);

                connection.Open();

                return command.ExecuteNonQuery();
            }
        }
        catch (Exception ex)
        {
            // Log the error

            // Rethrow
            throw new FaultException<DNTFaultException>(DNTFaultException.CreateFromException(ex), ex.Message);
        }
    }

    /// <summary>
    /// Method to Delete an existing Customer from the system
    /// </summary>
    /// <param name="customer">Customer object</param>
    /// <returns>No of Row affected</returns>
    [OperationBehavior(TransactionScopeRequired = false)]
    public int Delete(string customerID)
    {
        try
        {
            Customer customer = Get(customerID);
            using (SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLConnection"].ConnectionString))
            {
                string strSQL = "Delete from Customers Where CustomerID = '" + customer.CustomerID + "'";

                SqlCommand command = new SqlCommand(strSQL, connection);

                connection.Open();

                return command.ExecuteNonQuery();
            }
        }
        catch (Exception ex)
        {
            // Log the error

            // Rethrow
            throw new FaultException<DNTFaultException>(DNTFaultException.CreateFromException(ex), ex.Message);
        }
    }
}
DNT.Entity
This project class contains the entity class file. Below is the Customer class code used for CustomerService.
[DataContract]
public class Customer
{
    [DataMember]
    public string CustomerID { get; set; }

    [DataMember]
    public string CompanyName { get; set; }

    [DataMember]
    public string ContactName { get; set; }

    [DataMember]
    public string ContactTitle { get; set; }

    [DataMember]
    public string Address { get; set; }

    [DataMember]
    public string City { get; set; }

    [DataMember]
    public string Region { get; set; }

    [DataMember]
    public string PostalCode { get; set; }

    [DataMember]
    public string Country { get; set; }

    [DataMember]
    public string Phone { get; set; }

    [DataMember]
    public string Fax { get; set; }
}
Before running this code, you can publish or setup with IIS server (It also runs fine with Visual Studio Development Server).

Run the project and get the url of the CustomerService service.

download the working example of the source code in C# here.
The next post explains the client application which consume this service.

0 Responses to “Hosting WCF Services by extending SeviceHostingFactory”

Post a Comment