Saturday, 14 July 2012

Securing WCF Services exposed on Azure Service Bus Namespace - Part 4


In previous posts, we have created Service Bus namespace and configured the Access Control Service for exposing and consuming WCF Service using service identities. In this post, we will see the service application which will be exposed to public using Service Bus endpoints and secured by Access Control Service.

The service project in our example exposes Customer details from Northwind database. So make sure to have the same database setup in the SQL Server for running this code.

This service project is same as done in the following post. So for any reference you can refer that post too.
http://www.dotnettwitter.com/2012/01/implementing-azure-appfabric-service_11.html

The below steps explains for creating WCF Service Application.

Step 1: Open Visual Studio and create a new project using File -> Project (Ctrl + Shift + N). Select the WCF -> WCF Service Application from Installed Template and press OK.

Step 2: Once the solution created successfully, the project contains the following default service file.

For implementing our example, I am planning to create three projects.
  1. DNT.RelayServiceBus – Contains the WCF service files and the configuration settings. This project does not contain the implementation of the services, will contain only the svc files and the configuration settings such as the endpoints, bindings and the behaviors.
  2. DNT.Entity – Contains the entity class files.
  3. DNT.Services – Contains the implementation of all the services created in DNT.RelayServiceBus project.
Step 3: Delete the existing service files such as IService1.cs, Service1.svc and Service1.svc.cs from the DNT.RelayServiceBus project. We will be adding required service in future steps.
Step 4: Add a new project in the solution (File -> Add -> New Project) and select the project type as Class Library. Name the project as DNT.Services.
Step 5: Add a new project in the same way by selecting Class Library as project type. Name the project as DNT.Entity.
Now we added three projects in the solution as defined in Step 2. The solution looks like as below.
Step 6: Right click the service project and select Add -> New Item (Ctrl + Shift + A). Select the Web from Installed Template and WCF Service. Name the file name as CustomerService.svc.
Now Visual Studio will be adding three files in the project ICustomerService.cs, CustomerService.svc and CustomerService.svc.cs.

Step 7: As we are planning to add the service implementation in another project DNT.Services. So delete the ICustomerService.cs and CustomerService.svc.cs. So the project will look like below.
Step 8: Now we will add entity class for this service. So right click the DNT.Entity project and select Add -> New Item (Ctrl + Shift + A). Enter the file name as Customer.cs (or change the existing class Class1.cs to Customer.cs, Visual Studio will change the class name automatically).
Step 9: Change the Customer class with the following source code.
[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; }
}
You required to add System.Runtime.Serialization reference to the project and refer the same to the class file.

Step 10: Now we required to add the implementation of the service in the DNT.Services project. Before adding the implementation, we required to add an exception class for handling exceptions in the service.

Right click the project and add class file DNTFaultException in the project. Change the DNTFaultException class with the following source code.
[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);
    }
}
You required to add the following reference to the project and the class.
using System.Runtime.Serialization;
using System.Collections;
Step 11: We need to add an interface for the service. So right click the project and add a class file ICustomerContract.cs.

Change the ICustomerContract interface source with the following source.
/// <summary>
/// Interface for defining the Customer Service functionalities.
/// </summary>
[ServiceContract]
public interface ICustomerContract
{
    /// <summary>
    /// Method to get all the Customers
    /// </summary>
    /// <returns>List of Customers</returns>
    [OperationContract]
    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]
    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]
    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]
    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]
    int Delete(string customerID);
}
You required to add following references in the project and also in the file header.
using System.ServiceModel;
using DNT.Entity;
Step 12: Add a new class file CustomerContract.cs, which implements the ICustomerContract interface.
[ServiceBehavior]
public class CustomerContract : ICustomerContract
{

    /// <summary>
    /// Method to get all the Customers
    /// </summary>
    /// <returns>List of Customers</returns>
    public IList<Customer> GetAll()
    {
        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";
                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>
    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>
    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>
    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>
    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);
        }
    }
}
Add the following reference in the project and in the class header.
using DNT.Entity;
using System.Data;
using System.ServiceModel;
Step 13: For exposing service to public discoverable using Service Bus, we required to add a custom class MyServiceRegistrySettingsElement by extending BehaviorExtensionElement in the project.

So add a class file MyServiceRegistrySettingsElement.cs in the project and change the source code as below.
public class MyServiceRegistrySettingsElement : BehaviorExtensionElement
{
    private const string displayNameId = "displayName";
    private const string discoveryModeId = "discoveryMode";

    public override Type BehaviorType
    {
        get { return typeof(ServiceRegistrySettings); }
    }

    protected override object CreateBehavior()
    {
        return new ServiceRegistrySettings()
        {
            DiscoveryMode = this.DiscoveryMode,
        };
    }

    [ConfigurationProperty(discoveryModeId, DefaultValue = DiscoveryType.Private)]
    public DiscoveryType DiscoveryMode
    {
        get { return (DiscoveryType)this[discoveryModeId]; }
        set { this[discoveryModeId] = value; }
    }

    [ConfigurationProperty(displayNameId)]
    public string DisplayName
    {
        get { return (string)this[displayNameId]; }
        set { this[displayNameId] = value; }
    }
}
Add the below required reference in the project and the class file.
using System.ServiceModel.Configuration;
using Microsoft.ServiceBus;
using System.Configuration;
Step 14: Open the CustomerService.svc file in the DNT.RelayServiceBus project and change the script as below.
<%@ ServiceHost Service="DNT.Services.CustomerContract" Factory="System.ServiceModel.Activation.ServiceHostFactory" %>
Step 15: We need to do make some modification in the Web.Config of the service project for exposing the services to public thro' Service Bus endpoints. So change the system.serviceModel node as below.
<system.serviceModel>
  <services>
    <clear />
    <service behaviorConfiguration="MyServiceTypeBehavior" name="DNT.Services.CustomerContract">
      <endpoint address="http://localhost/DNT.RelayServiceBus/CustomerService.svc/"
                binding="basicHttpBinding"
                bindingConfiguration="BasicHttpConfig"
                name="Basic"
                contract="DNT.Services.ICustomerContract" />

      <endpoint address="https://dntsales.servicebus.windows.net/Http/Order/Test/V0101/"
                behaviorConfiguration="sharedSecretClientCredentials"
                binding="basicHttpRelayBinding"
                bindingConfiguration="HttpRelayEndpointConfig"
                name="RelayEndpoint"
                contract="DNT.Services.ICustomerContract" />

      <endpoint address="sb://dntsales.servicebus.windows.net/Tcp/Order/Test/V0101/"
                behaviorConfiguration="sharedSecretClientCredentials"
                binding="netTcpRelayBinding"
                bindingConfiguration="NetTcpRelayEndpointConfig"
                name="RelayEndpoint"
                contract="DNT.Services.ICustomerContract" />
    </service>
  </services>
  <bindings>
    <basicHttpBinding>
      <binding name="BasicHttpConfig" />
    </basicHttpBinding>
    <basicHttpRelayBinding>
      <binding name="HttpRelayEndpointConfig">
        <security relayClientAuthenticationType="RelayAccessToken" />
      </binding>
    </basicHttpRelayBinding>
    <netTcpRelayBinding>
      <binding name="NetTcpRelayEndpointConfig">
        <security relayClientAuthenticationType="RelayAccessToken" />
      </binding>
    </netTcpRelayBinding>
  </bindings>
  <behaviors>
    <endpointBehaviors>
      <behavior name="sharedSecretClientCredentials">
        <transportClientEndpointBehavior credentialType="SharedSecret">
          <clientCredentials>
            <sharedSecret issuerName="raja" issuerSecret="K5GIOAyluK5W4E2+As+AnMIOPV1mKWoNw9ggNAmGSR4=" />
          </clientCredentials>
        </transportClientEndpointBehavior>
        <ServiceRegistrySettings discoveryMode="Public" />
      </behavior>
    </endpointBehaviors>
    <serviceBehaviors>
      <behavior name="MyServiceTypeBehavior">
        <serviceMetadata httpGetEnabled="true" />
        <serviceDebug includeExceptionDetailInFaults="false" />
      </behavior>
    </serviceBehaviors>
  </behaviors>
  <extensions>
    <behaviorExtensions>
      <add name="transportClientEndpointBehavior" type="Microsoft.ServiceBus.Configuration.TransportClientEndpointBehaviorElement, Microsoft.ServiceBus, Version=1.6.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
      <add name="ServiceRegistrySettings" type="DNT.Services.MyServiceRegistrySettingsElement, DNT.Services, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
    </behaviorExtensions>
    <bindingExtensions>
      <add name="basicHttpRelayBinding" type="Microsoft.ServiceBus.Configuration.BasicHttpRelayBindingCollectionElement, Microsoft.ServiceBus, Version=1.6.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
      <add name="netTcpRelayBinding" type="Microsoft.ServiceBus.Configuration.NetTcpRelayBindingCollectionElement, Microsoft.ServiceBus, Version=1.6.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    </bindingExtensions>
  </extensions>
</system.serviceModel>
As defined in previous post, here we had referred issuerName as raja. This is the service identity we created with Listen access rights in the Access Control Service portal.

Step 16: Finally change the DNT.Services and DNT.Entity output path to service project bin folder. So open the property of DNT.Services and DNT.Entity projects and select the Build tab. Change the output dorectory to ..\DNT.RelayServiceBus\bin\.
Step 17: Change the database connection string in the Web.Config.
<connectionStrings>
  <add connectionString="Data Source=thirumalai-note;Initial Catalog=Northwind;Trusted_Connection=True;" name="SQLConnection" />
</connectionStrings>
Step 18: To expose the services in the application to the public, the service application should run under IIS server. So get the Properties of the service project and select the Web tab. Select the Use Local IIS Web server option and press Create Virtual Directory.
Visual Studio will create a Virtual Directory under Default Web Site with the project source path as the path of the Virtual Directory.

Step 19: To expose the WCF Service application to public using Service Bus, we required to run the service under IIS instead of Visual Studio Development Server. This is because the IIS provides some features which are required exposing to public.

So, right click the WCF project and select Properties. The Visual Studio will show Properties page. Select the Web tab on the properties page.

Select the Use Local IIS Web server and press Create Virtual Directory button. The Visual Studio will create a virtual directory under Default Web Site and show The virtual directory was created successfully message.

Step 20: Run the project and verify the browser output. The output in the browser will look as below.

Step 21: Once the service runs successfully, it connects the Service Bus and creates a secure bi-directional communication using relay binding. The Service can be accessible by public using Service bus endpoints configured.

To know how the Service Bus endpoints are exposed, you can browse the base Service Bus endpoint from the browser. The browser will show the next level hierarchies in a list. By drilldown to that level, it shows the next level till end.

Please refer the following posts Step 15 and Step 16 to know more about how to browse the Service Bus endpoints.

http://www.dotnettwitter.com/2012/01/implementing-azure-appfabric-service_11.html

Possible Errors

You may get some error when running the project. You can verify the following url under Possible Errors for finding the solution.
http://www.dotnettwitter.com/2012/01/implementing-azure-appfabric-service_11.html

Configuring Auto-Start for WCF Service
Once the Service executed, it creates a secure connection with Service Bus and it needs to be remain connected as the consumer can connect the service at any time. But in some case, the server or IIS might restart which needs the service application to again run for creating connection to service bus.

To avoid this manual work, Windows Server AppFabric provides a nice feature call Auto-Start will helps to run the site once the IIS starts and create an environment for consuming the service.

To know more about how to configure IIS Auto-Start for the service application, please refer the following post.
http://www.dotnettwitter.com/2012/01/implementing-azure-appfabric-service_20.html

download the service application source code here.


0 Responses to “Securing WCF Services exposed on Azure Service Bus Namespace - Part 4”

Post a Comment