Designing a software service interaction using WSE 3.0

You know, a software service, a piece of software that can be invoked from just one client to thousands of clients simultaneously, and it works like a charm the same (if its service-level agreement says so).

 

A key part of a software service is the actual code designed for the agreed level of concurrency –the service core; this core is usually packaged as a component or other kind of reusable mechanism so that multiples façades can expose endpoints via particular communication channels, protocols and formats providing pathways that –ultimately– service clients draw on to reach the actual service core.

 

Another key part of a software service is the networking protocols that represent different channels to the service core, e.g. HTTP, TCP, MSMQ, SMTP, etc; and, among a number of other key things, there is the format, e.g. SOAP.

 

Each service request ultimately comes to an operating system process hosting the software service which will reply back a proper service response. This host process could be IIS, a Window service, a console application, a Windows Forms application, or the like.

 

For instance, a Windows service process hosting service endpoints using WSE 3.0 could invoke the following method from its OnStart method:

 

private void RegisterWebService()

{

  Uri url = new Uri("soap.tcp://" + System.Net.Dns.GetHostName() + "/ConfigService");

  EndpointReference endpoint = new EndpointReference(url);

  SoapReceivers.Add(endpoint, typeof(ConfigService));

  Log.Write("WebService ConfigService listening for messages at " + url);

}

 

ConfigService type is the service class type (that is, its implementation –you know, type concept refers to the interface whereas class concept refers to the implementation, and ‘class type’ because it is a type that happens to be a class) – and looks like:

 

using System;

using System.Collections.Generic;

using System.Web.Services;

using System.Web.Services.Protocols;

using processor;

[WebService(Namespace = shared.ConfigServiceEndPoint.Namespace)]

[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]

public class ConfigService : WebService

{

  [WebMethod]

  public List<shared.PCDescription> ReturnComputers()

  {

    try

    {

      return ComputerOperation.ReturnComputers();

    }

    catch (Exception err)

    {

      Log.Write(err.Message);

      throw new SoapException(

      "SORRY, UNAVAILABLE SERVICE",

      new XmlQualifiedName("ConfigService"));

    }

  }

}

 

As you can see, this service class type is a .NET Framework 2.0 WebService-derived class, which happens to be hosted in a process other than IIS. The implementation of WS-Security specification in WSE 3.0 among some other WS-* specifications make this hosting mechanism approachable for enterprise scenarios where IIS cannot be used.

 

This service class type is just a façade to the service core, in this case the ComputerOperation.ReturnComputers() static method; which is a realization of the processor design pattern (a.k.a. unit of work, business object, transaction script, business service); that is to say, the service core is a processor. The service core is a procedure that handles a single service request in a scalable way and produces its proper service response. For doing that, the service core could coordinate any number of other processors and hence it is prepared to be coordinated by any number of other processors. At some point in the coordinated work a data object is reached which sole responsibility is to guard the access to a data structure and its persistent medium (SQL table or the like). There is no other way to reach that data structure but through its corresponding data object. A key point for processors is to protect all the coordinated work with a single transaction with ACID properties, which is initiated by the first processor that receives the service request from outside. The receiving processor is hence called the root object of the transaction and the only one knowing the result of all other coordinated processors and therefore who knows if the whole transaction is to be committed or rolled back. By coordinated work I mean following the same rules about participating in an existing transactional context or creating one in the case of the transaction root object, exception handling, logging, etc.

 

If you were present at object methods wars of the 90’s most likely you recognize this kind of module organization as a wicked manifestation of functional decomposition, specially wicked on large-scale systems where abstractions depends on concrete details (and not the other way around) and little changes –mainly in data structures– cause massive recompilations and the propagation of changes all over the system decreasing its general stability and triggering effects on unrelated sections of the system for apparent no reason. This is true, only if you are unaware of dependency management principles and blindly ignore the real forces of coupling and cohesion in software structures. A mechanism to lessen such wickedness is by some kind of late-binding processor dispatcher. The processor design pattern is sound, simple and scalable, which is a much better alternative in comparison with convoluted, over-engineered and non-scalable distributed objects approaches where there is no clue of object identity other than physical object identity.

 

A basic processor procedure implementation looks like this:

 

namespace processor

{

  using System;

  using System.Collections.Generic;

  using System.Data;

  using System.Data.SqlClient;

  using dataaccess;

  public class ComputerOperation

  {

    public static List<shared.PCDescription> ReturnComputers(TransactionalContext tx)

    {

      return ComputerStore.ReturnComputers(tx);

    }

    public static List<shared.PCDescription> ReturnComputers()

    {

      TransactionalContext tx = null;

      try

      {

        tx = TransactionalContext.Create();

        List<shared.PCDescription> result = ReturnComputers(tx);

        tx.Transaction.Commit();

        return result;

      }

      catch (SqlException err)

      {

        Log.Write(err.Message);

        if (tx != null && tx.Connection.State == ConnectionState.Open)

          tx.Transaction.Rollback();

        throw;

      }

      finally

      {

        if (tx != null && tx.Connection.State == ConnectionState.Open)

         tx.Connection.Close();

      }

    }

  }

}

 

namespace dataaccess

{

  using System.Collections.Generic;

  using System.Data;

  using System.Data.SqlClient;

  using System.Configuration.

  using shared;

  public class TransactionalContext

  {

    private TransactionalContext() { }

    public static TransactionalContext Create()

    {

      TransactionalContext result = new TransactionalContext();

      result.Connection = new SqlConnection(ConfigurationManager.AppSettings["dbcon"]);

      result.Connection.Open();

      result.Transaction = result.Connection.BeginTransaction();

      return result;

    }

    public SqlConnection Connection;

    public SqlTransaction Transaction;

  }

  public class ComputerStore

  {

    public static List<shared.PCDescription> ReturnComputers(TransactionalContext tx)

    {

      SqlCommand cmd = tx.Connection.CreateCommand();

      cmd.Transaction = tx.Transaction;

      cmd.CommandType = CommandType.StoredProcedure;

      cmd.CommandText = "Computer_Select";

      using (SqlDataReader reader = cmd.ExecuteReader())

      {

        List<shared.PCDescription> result = new List<shared.PCDescription>();

        while (reader.Read())

        {

          shared.PCDescription pc = new shared.PCDescription();

          pc.Name = reader["PCNetworkID"] as string;

          result.Add(pc);

        }

        return result;

      }

    }

  }

}

 

A namespace called ‘shared’ has been used so far and it holds the common data structures and constants shared between the service and its .NET clients. Here is it:

 

namespace shared

{

  using System.Collections;

  public class ConfigServiceEndPoint

  {

    public const string Namespace = "https://wsemsg.net";

    public class Contract

    {

      public const string ReturnComputers = Namespace + "/ReturnComputers";

    }

  }

  public class PCDescription

  {

    public PCDescription() { }

    public PCDescription(string id) { Name = id; }

    public string Name;

    public string Domain;

    public string IP;

    public string Mask;

  }

  [System.Xml.Serialization.XmlType(TypeName = "PCCollection")]

  public class PCDescriptionCollection : CollectionBase

  {

    public PCDescriptionCollection() { }

    public PCDescriptionCollection(IEnumerable items)

    {

      foreach (PCDescription pc in items)

  Add(pc);

    }

    public void Add(PCDescription pc)

    {

      InnerList.Add(pc);

    }

    public PCDescription this[int index]

    {

      get { return InnerList[index] as PCDescription; }

    }

  }

}

 

Now a client for our service, which usually employs a service proxy, that looks like:

 

namespace webservice_proxy

{

  using System.Configuration;

  using System.Collections.Generic;

  using Microsoft.Web.Services3;

  using System.Web.Services;

  using System.Web.Services.Protocols;

  [WebServiceBinding(Namespace = shared.ConfigServiceEndPoint.Namespace)]

  public class ConfigServiceProxy : WebServicesClientProtocol

  {

    public ConfigServiceProxy()

    {

      Url = ConfigurationManager.AppSettings["webservice"];

// webservice -> "soap.tcp://server/ConfigService";

    }

    [SoapDocumentMethod(shared.ConfigServiceEndPoint.Contract.ReturnComputers)]

    public List<shared.PCDescription> ReturnComputers()

    {

      object[] results = this.Invoke("ReturnComputers", new object[] { });

      return ((List<shared.PCDescription>)(results[0]));

    }

  }

}

 

And the client code:

namespace client

{

  using System;

  using System.Collections.Generic;

  using System.Web.Services.Protocols;

  using Microsoft.Web.Services3;

  public class client

  {

  public void use_service()

    {

      try

      {

        webservice_proxy.ConfigServiceProxy config;

        config = new webservice_proxy.ConfigServiceProxy();

        List<shared.PCDescription> PCs = config.ReturnComputers();

        //process response

      }

      catch (AsynchronousOperationException err)

      {

        Console.WriteLine("The service can not be contacted due the following problem:{0} and {1}", err.Message, err.InnerException.Message);

      }

      catch (SoapHeaderException err)

  {

        Console.WriteLine("The service has reported an internal error:{0}. {1}", err.Actor, err);

      }

      catch (SoapException err)

      {

        Console.WriteLine("\nSoapException:{0}\nCode:{1}", err.Message, err.Code.Name);

      }

      catch (Exception err)

      {

        Console.WriteLine("UN-IDENTIFIED EXCEPTION:{0}", err);

        throw;

      }

    }

  }

}