Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Remote Operation Layer

5.00/5 (17 votes)
14 Apr 2014CPOL9 min read 21.3K   299  
Service layer which hides the concrete technology details and doesn’t need to change when a new feature is implemented.

Introduction

This article shows how we create a service layer which is able to communicate with a remote side, can work together with different remoting technologies, hides details of the technology and doesn’t need to be changed when a new feature is implemented. Why is it good for us?

It is useful to hide the concrete technology because there are lot of reasons when you have to change it during a project. For example once we faced performance problems in the Windows Communication Foundation and decided to write our own socket communication. With this architecture, changes like this can be done easliy without changing anything in other components.

The service contract is something that describes methods which are exposed and can be called from a remote side. The contract can be for example an interface which contains methods or a WSDL file. When a new feature is added to a project, new methods will appear in the service contract. We want to avoid these changes and create a service contract which won't change during the lifetime of a project.

We will show a concrete usage of the architecture where the communication technology is WCF.

Background

Our service architecture uses method, event and state names similar to WCF conventions. So it is useful to be familiar with the .NET ServiceModel class library. Especially to understand the concrete WCF implementation.

Using the code

We define interfaces and classes to wrap all the communication functions. This way we hide all the concrete details of the current remoting technology. The high level architecture shows that on both sides the business layers use the Remote Operation Layer to initiate operations on remote side. The two Remote Operation Layers communicate with each other with the configured technology.

Image 1

Remote operation descriptor

The RemoteOperationDescriptor class represents a remote operation which is sent through the network and contains information about what should be executed on the remote side. A remote operation can be described with three properties: type of the interface, name of the method to be invoked on that interface and the arguments of that method. Based on the interface type on the remote side an implementation of the interface is retrived from the DI container. On this instance the given method can be invoked with the given parameters.

[Serializable]
public class RemoteOperationDescriptor
{
    public RemoteOperationDescriptor(string interfaceType, string methodName, params object[] paramters)
    {
        if (interfaceType == null)
        {
            throw new ArgumentNullException("interfaceType");
        }

        if (methodName == null)
        {
            throw new ArgumentNullException("methodName");
        }

        InterfaceType = interfaceType;
        MethodName = methodName;
        Parameters = paramters;
    }

    public string InterfaceType
    {
        get;
        set;
    }
        
    public string MethodName
    {
        get;
        set;
    }
    public object[] Parameters
    {
        get;
        set;
    }
}  

Custom attributes

For security reasons the remote callable interfaces and methods are marked with an attribute. RemoteCallableTypeAttribute indicating that they can be accessed remotely. Similarly the RemoteCallableFuncAttribute attribute indicates the methods which are callable from the remote side. Before each execution it is checked whether the interface and the method given in the RemoteOperationDescriptor are marked with these attributes or not. If not then an exception is thrown.

[AttributeUsage(AttributeTargets.Interface, AllowMultiple = false, Inherited = false)]
public class RemoteCallableTypeAttribute : Attribute
{
    public bool RegisterInDIContainer { get; set; }

    public RemoteCallableTypeAttribute() : this(false)
    {
    }

    public RemoteCallableTypeAttribute(bool registerInDIContainer)
    {
        this.RegisterInDIContainer = registerInDIContainer;
    }
} 
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public class RemoteCallableFuncAttribute : Attribute
{
}  

Request and response classes

The RemoteOperationDescriptor class is wrapped in a request object. A request object is an XML serializable object which is sent to the remote side. This request object is responsible for serializing the RemoteOperationDescriptor in XML format. We also have an XML serializable response object which only contains an object typed return value. We define a common base class for the requests and responses which contains an identifier. The identifier's type is RemoteSideIDType and it is a unique value that identifies the caller. You can see the class hiararchy below.

[Serializable]
public abstract class RemoteRequestResponseBase : IXmlSerializable, SocketCommunication.ISocketXmlSerializable
{
    public XmlSchema GetSchema()
    {
        return null;
    }

    public RemoteSideIDType RemoteID = null;

    #region IXmlSerializable Members
    ...
    #endregion


    #region ISocketXmlSerializable Members
    ...
    #endregion
} 
[Serializable]
public class RemoteRequest : RemoteRequestBase
{
    internal RemoteOperationDescriptor ExecuteOnRemoteSideOperation = null;

    #region IXmlSerializable Members
    ....
    #endregion
} 
[Serializable]
public class RemoteResponse : RemoteResponseBase
{
    public object ReturnValue;

    #region IXmlSerializable Members
    ....
    #endregion
} 

Initiate remote call

The IRemoteSideCommunicationContract interface describes the contract between the remote sides. It has only one method, called ExecuteOnRemoteSide, which gets a RemoteRequest argument and returns a RemoteResponse. This interface should be implemented if a new remoting library is used. For example we have two default implementations, one for WCF and one for our own socket-based communication framework.

 /// <summary>
/// Interface which contains the remote callable methods
/// </summary>
[ServiceContract(CallbackContract=typeof(IRemoteSideCommunicationContract))]
public interface IRemoteSideCommunicationContract
{
    /// <summary>
    /// Remote callable method which handles all the requests from the remote side
    /// </summary>
    [OperationContract]
    RemoteResponse ExecuteRequest(RemoteRequest request);
}  

RemoteOperationHandler is a helper class that helps us construct a new RemoteRequest object from a RemoteOperationDescriptor. It also contains the logic how a newly arrived RemoteRequest is processed and executed in ExecuteRequest method. First it searches for the interface type and method described in the request. Then it checks the attributes of the interface and the method and if they are remote callable then it invokes the method with the parameters given in the request. After the invocation it wraps the return value in a RemoteResponse object. If an exception occures in the method invocation, it catches the exception and puts the exception object in the return value property. RemoteOperationHandler has a method to handle RemoteResponse objects. The method checks the response object value and returns it. If the method finds an exception in the return value then it throws that exception. This is a limitation in this service layer: a remote operation cannot have Exception as a return type beacause on the calling side it wouldn't be possible to decide if it is a return value or a raised exception.

/// <summary>
/// Interface which describe methods to generate request from RemoteSideOperation and handle responses.
/// </summary>
public interface IRemoteOperationHandler
{
    /// <summary>
    /// Creates the request object from the RemoteSideOperation
    /// </summary>
    /// <param name="rso">Describes the request parameters</param>
    /// <returns>Created request object</returns>
    RemoteRequest CreateRequest(RemoteOperationDescriptor rso);

    /// <summary>
    /// Executes the request on the remote side.
    /// </summary>
    /// <param name="request">Request</param>
    /// <param name="callableTypeAttribute"></param>
    /// <param name="callableFuncAttribute"></param>
    /// <returns>Response</returns>
    RemoteResponse ExecuteRequest(RemoteRequest request, Type callableTypeAttribute, Type callableFuncAttribute);

    /// <summary>
    /// Handles the response of the remote side request
    /// </summary>
    /// <param name="resp">Response object</param>
    /// <returns>Value stored in response</returns>
    object HandleResponse(RemoteResponse resp);
}  

The IRemoteSideCommunicationHandler defines a contract to initiate calls to the remote side and handles the connection. The interface defines the following members:

The two ExecuteOnRemoteSide methods are responsible for initiating a new request. Both methods get a RemoteOperationDescriptor which contains the details of the call and a RemoteSideIDType identifying which remote side gets this request.

The CurrentRemoteSideID returns the local side's identifier.

The RemoteSideConnected and RemoteSideDisconnected events are raised when a new remote side connected or disconnected.

The AssignRemoteSide method is used to assign the current IRemoteSide object with the communication handler. The IRemoteSide interface represents the endpoint of a communication and defines methods for an abstract communication endpoint.

/// <summary>
/// Describes a communicator object to start remote calls and handles states
/// </summary>
public interface IRemoteSideCommunicationHandler
{
    /// <summary>
    /// Executes a request on the remote side and gives the return back
    /// </summary>
    /// <typeparam name="TResult">Return type of the remote call</typeparam>
    /// <param name="remoteSideID">Remote client ID</param>
    /// <param name="rso">The descriptor of the operation</param>
    /// <returns>Result</returns>
    TResult ExecuteOnRemoteSide<TResult>(RemoteSideIDType remoteSideID, RemoteOperationDescriptor rso);

    /// <summary>
    /// Executes a request on the remote side
    /// </summary>
    /// <param name="remoteSideID">Remote client ID</param>
    /// <param name="rso">The descriptor of the operation</param>
    void ExecuteOnRemoteSide(RemoteSideIDType remoteSideID, RemoteOperationDescriptor rso);

    /// <summary>
    /// The id of the remote client whos request is executed currently
    /// </summary>
    RemoteSideIDType CurrentRemoteSideID { get; }

    /// <summary>
    /// Remote client connected event
    /// </summary>
    event EventHandler<RemoteSideConnectedEventArgs> RemoteSideConnected;

    /// <summary>
    /// Remote client disconnected event
    /// </summary>
    event EventHandler<RemoteSideDisconnectedEventArgs> RemoteSideDisconnected;

    /// <summary>
    /// Assign the concrete ClientServiceHost
    /// </summary>
    /// <param name="remoteSide">Service host</param>
    void AssignRemoteSide(IRemoteSide remoteSide);
}  

The IRemoteSide interface defines methods for creating and closing communication (Open, Close, Abort) and evetns for these methods (Opened, Closed). It also contains state information and a state changed event. Lastly, it can return the contract object it uses to initiate remote calls (GetCurrentRemoteSideCommunicationContract).

/// <summary>
/// Represent a remote side communication endpoint
/// </summary>
public interface IRemoteSide
{
    /// <summary>
    /// Raised after the communication is closed
    /// </summary>
    event EventHandler Closed;
    /// <summary>
    /// Raised after the communication is faulted
    /// </summary>
    event EventHandler Faulted;
    /// <summary>
    /// State of the communication
    /// </summary>
    RemoteCommunicationState State { get; }
    /// <summary>
    /// Raised after the state changed
    /// </summary>
    event EventHandler StateChanged;
    /// <summary>
    /// Open the communication
    /// </summary>
    void Open();
    /// <summary>
    /// Close the communication 
    /// </summary>
    void Close();
    /// <summary>
    /// Abort the communication
    /// </summary>
    void Abort();
    /// <summary>
    /// Concrete communication contract implementation
    /// </summary>
    /// <returns></returns>
    IRemoteSideCommunicationContract GetCurrentRemoteSideCommunicationContract();
}  

To instantiate an IRemoteSide interface implementation we use the factory pattern defined in the IRemoteSideFactory interface.

/// <summary>
/// Factory which creates a new IRemoteSide
/// </summary>
public interface IRemoteSideFactory
{
    /// <summary>
    /// Creates a new remote side instance
    /// </summary>
    /// <returns>New instance</returns>
    IRemoteSide CreateInstance();
}   

Now let's see our implementations for these interfaces. The RemoteSideCommunicator class implements the IRemoteSide interface and IRemoteSideCommunicationContract interace too. Therefor this class is responsible for all communication with the remote side. It handles and executes calls from the the remote side and initiates new calls to the remote side. The constructor gets an IRemoteOperationHandler implementation to handle the incoming calls. A communicator can handle multiple remote sides distinguished by the RemoteSideID.

The class also has some events to raise when a remote side connects or a known remote side disconnected. This is a base class and provides some virtual methods to override. With these methods we can modify or wrap all the remote calls. For example we use session information about a client and we use these virtual methods to create, manage and drop sessions during the communication.

 public class RemoteSideCommunicator : IRemoteSideCommunicationHandler, IRemoteSideCommunicationContract
{
    protected IRemoteOperationHandler remoteOperationHandler = null;
    protected IRemoteSide remoteSide = null;

    public RemoteSideCommunicator(IRemoteOperationHandler remoteOperationHandler)
    {
    ....
    }

    public RemoteSideCommunicator(IRemoteOperationHandler remoteOperationHandler, Tuple<RemoteSideIDType, IRemoteSideCommunicationContract> alreadyKnownRemoteSide) :this(remoteOperationHandler)
    {
    ....
    }

    public RemoteSideIDType CurrentRemoteSideID
    {
        get
        {
        ....
        }
    }

    protected virtual void NewRemoteSideConnected(RemoteSideIDType remoteSideID)
    {
    }

    protected virtual void KnownRemoteSideRequest(RemoteSideIDType remoteSideID)
    {
    }

    protected virtual void RemoteSideReconnected(RemoteSideIDType remoteSideID)
    {
    }

    protected virtual void RemoteRequestStarted(RemoteSideIDType remoteSideID, RemoteRequest request)
    {
    }

    protected virtual void RemoteRequestFinished(RemoteSideIDType remoteSideID, RemoteRequest request)
    {
    }

    public virtual RemoteResponse ExecuteRequest(RemoteRequest request)
    {
        ....
        IRemoteSideCommunicationContract currentContract = wrapper.GetCurrentRemoteSideCommunicationContract();

        try
        {
            try
            {
                ....
                RemoteRequestStarted(remoteID, request);

                if (communicationContractsByRemoteSideIDDict.TryGetValue(remoteID, out lastKnownContract))
                {
                    ....
                    KnownRemoteSideRequest(remoteID);
                }
                else
                {
                    ....
                    NewRemoteSideConnected(remoteID);
                }
            }
            finally
            {
            }
            ....
            if (isNewRemoteSide)
            {
                OnRemoteSideConnected(remoteID);
            }
            ret = remoteOperationHandler.ExecuteRequest(request, typeof(RemoteCallableTypeAttribute), typeof(RemoteCallableFuncAttribute));
        }
        finally
        {
            RemoteRequestFinished(remoteID, request);
        }
        ....        
    }

    protected virtual void RemoteSideFaulted(RemoteSideIDType remoteID)
    {
    }

    private void Channel_Faulted(object sender, EventArgs e)
    {
        ....
        RemoteSideFaulted(remoteSideID);
        ....
    }


    public event EventHandler<RemoteSideConnectedEventArgs> RemoteSideConnected;
    public event EventHandler<RemoteSideDisconnectedEventArgs> RemoteSideDisconnected;

    public TResult ExecuteOnRemoteSide<TResult>(RemoteSideIDType remoteSideID, RemoteOperationDescriptor rso)
    {
        ....
        TResult ret = (TResult)ExecuteOnRemoteSideInternal(remoteSideID, rso);
        ....
    }

    public void ExecuteOnRemoteSide(RemoteSideIDType remoteSideID, RemoteOperationDescriptor rso)
    {
        ....
        ExecuteOnRemoteSideInternal(remoteSideID, rso);
    }

    protected virtual void RequestToRemoteSideStarted(RemoteSideIDType remoteSideID, RemoteOperationDescriptor rso)
    {
    }

    protected virtual void RequestToRemoteSideFinished(RemoteSideIDType remoteSideID, RemoteOperationDescriptor rso)
    {
    }

    protected virtual object ExecuteOnRemoteSideInternal(RemoteSideIDType remoteSideID, RemoteOperationDescriptor rso)
    {
        ....
        RemoteRequest req = remoteOperationHandler.CreateRequest(rso);
        req.RemoteID = remoteSideID;

        RemoteResponse resp = null;
        try
        {
            RequestToRemoteSideStarted(remoteSideID, rso);
            resp = contract.ExecuteRequest(req);
        }
        finally
        {
            RequestToRemoteSideFinished(remoteSideID, rso);
        }
        ret = remoteOperationHandler.HandleResponse(resp);
        ....
    }
}    

WCF implementation

Any communicaton technology you want to use with this architecture needs to implement the following interfaces: IRemoteSide, IRemoteSideCommunicationContract and IRemoteSideFactory. For instance in our project we implement two different communicational layers: one based on WCF and the other one is our own socket-based communication framework. In this section we will introduce you the WCF based implementation.

In this implementation we create a server side and a client side WCF component. For the server side we implement the IRemoteSide and IRemoteSideCommunicationContract interfaces in the WCFServiceHost class which is inherited from the System.ServiceModel.ServiceHost . The ServiceHost contains the methods and events for opening and closing the communication. We only need to implement the state handling and returning the contract instance.

public class WCFServiceHost : ServiceHost, IRemoteSide, IRemoteSideCommunicationContract
{
    private IWCFConfigManager cm = null;
    private IDIContainer diContainer = null;
    private IRemoteSideCommunicationContract clientServiceContractInstance = null;

    public WCFServiceHost(IDIContainer diContainer, IRemoteSideCommunicationContract clientServiceContractInstance, Uri clientServiceAddress)
        : base(clientServiceContractInstance, clientServiceAddress)
    {
        this.diContainer = diContainer;
        this.clientServiceContractInstance = clientServiceContractInstance;
        cm = diContainer.GetLazyBoundInstance<IWCFConfigManager>();

        ApplyConfiguration();

        this.AddServiceEndpoint(
            ServiceMetadataBehavior.MexContractName, 
            MetadataExchangeBindings.CreateMexTcpBinding(),
            String.Concat(new Uri(cm.Value.ClientServiceAddress).OriginalString, "/mex"));

        this.Closed += new EventHandler(clientServiceHost_StateChanged);
        this.Closing += new EventHandler(clientServiceHost_StateChanged);
        this.Faulted += new EventHandler(clientServiceHost_StateChanged);
        this.Opened += new EventHandler(clientServiceHost_StateChanged);
        this.Opening += new EventHandler(clientServiceHost_StateChanged);
    }

    private void clientServiceHost_StateChanged(object sender, EventArgs e)
    {
        Log.Debug("ClientServiceHost.State changed: {0}", this.State.ToString());
        OnStateChanged();
    }

    private Binding CreateBinding()
    {
        NetTcpBinding binding = new NetTcpBinding();

        WCFHelper.ApplyWCFBindingLimits(
            binding,
            cm.Value.ClientServiceMaxSizeInBytes,
            cm.Value.ClientServiceTimeoutInSecs);
        binding.Security.Mode = SecurityMode.None;
        binding.Security.Transport.ClientCredentialType = TcpClientCredentialType.None;

        return binding;
    }

    protected override void ApplyConfiguration()
    {
        if (cm != null)
        {
            ServiceDebugBehavior debug = new ServiceDebugBehavior();
            debug.IncludeExceptionDetailInFaults = true;
            this.Description.Behaviors.Add(debug);

            ServiceThrottlingBehavior throttling = new ServiceThrottlingBehavior();
            throttling.MaxConcurrentCalls = cm.Value.ClientServiceMaxConcurrentCalls;
            throttling.MaxConcurrentInstances = cm.Value.ClientServiceMaxConcurrentInstances;
            throttling.MaxConcurrentSessions = cm.Value.ClientServiceMaxConcurrentSessions;
            this.Description.Behaviors.Add(throttling);

            Binding binding = CreateBinding();
            ServiceEndpoint ep = this.AddServiceEndpoint(typeof(IRemoteSideCommunicationContract), binding, String.Empty);
            EndpointAddress epa = new EndpointAddress(ep.Address.Uri, EndpointIdentity.CreateDnsIdentity("localhost"), ep.Address.Headers);
            ep.Address = epa;

            WCFHelper.ApplyWCFEndpointLimits(ep, cm.Value.ClientServiceMaxItemsInObjectGraph);

            ServiceMetadataBehavior mb = new ServiceMetadataBehavior();
            this.Description.Behaviors.Add(mb);

            ServiceBehaviorAttribute sba = (ServiceBehaviorAttribute)(from a in this.Description.Behaviors
                                                                        where a.GetType() == typeof(ServiceBehaviorAttribute)
                                                                        select a).Single();

            sba.InstanceContextMode = InstanceContextMode.Single;
            sba.ConcurrencyMode = ConcurrencyMode.Multiple;
            sba.UseSynchronizationContext = false;
        }
    }

    public IRemoteSideCommunicationContract GetCurrentRemoteSideCommunicationContract()
    {
        IRemoteSideCommunicationContract ret = null;

        var cc = OperationContext.Current;
        if (cc != null)
        {
            ret = cc.GetCallbackChannel<IRemoteSideCommunicationContract>();
        }

        return ret;
    }

    public new RemoteCommunicationState State
    {
        get;
        private set;
    }

    public event EventHandler StateChanged;

    private void OnStateChanged()
    {
        if (this.StateChanged != null)
        {
            StateChanged(this, EventArgs.Empty);
        }
    }

    public RemoteResponse ExecuteRequest(RemoteRequest request)
    {
        RemoteResponse ret;
        ret = clientServiceContractInstance.ExecuteRequest(request);
        return ret;
    }
} 

The IRemoteSideFactory is implemented in the WCFServiceHostFactory class. This class can create new instance of the WCFServiceHost and assign it to the current communicator.

public class WCFServiceHostFactory : IRemoteSideFactory
{
    private IDIContainer diContainer = null;
    public WCFServiceHostFactory(IDIContainer diContainer)
    {
        this.diContainer = diContainer;
    }

    public IRemoteSide CreateInstance()
    {
        IRemoteSide ret = null;

        var cm = diContainer.GetLazyBoundInstance<IWCFConfigManager>().Value;
        var clientServiceInstance = diContainer.GetLazyBoundInstance<IRemoteSideCommunicationHandler>().Value;
        ret = new WCFServiceHost(diContainer, (IRemoteSideCommunicationContract)clientServiceInstance, new Uri(cm.ClientServiceAddress));
        clientServiceInstance.AssignRemoteSide(ret);

        return ret;
    }
}  

Both classes use an IWCFConfigManager interface which describes properties of the WCF communication channel. There is no default implementation for this interfaces because it depends on the application where we use this library. If you have config files you should create a class that implements this interface and retrives the concrete values from the config file or config manager. Now we don't define all the properties of a WCF channel here, just what we really want to allow to be changed from the configuration file. If it becomes necessary it is really easy to extend this interface with more properties.

public interface IWCFConfigManager
{
    string ClientServiceAddress { get; }
    int ClientServiceMaxItemsInObjectGraph { get; }
    int ClientServiceMaxSizeInBytes { get; }
    int ClientServiceMaxConcurrentCalls { get; }
    int ClientServiceMaxConcurrentInstances { get; }
    int ClientServiceMaxConcurrentSessions { get; }
    int ClientServiceTimeoutInSecs { get; }
}   

For example we store these settings in the config file and the config file values are accessible through a config manager.

internal class WCFConfigManager : IWCFConfigManager
{
    private IConfigManager configManagerInstance = DIContainer.Instance.GetLazyBoundInstance<IConfigManager>();

    private int? clientServiceMaxItemsInObjectGraph = null;
    public int ClientServiceMaxItemsInObjectGraph
    {
        get
        {
            if (!clientServiceMaxItemsInObjectGraph.HasValue)
            {
                clientServiceMaxItemsInObjectGraph = configManagerInstance.Value.GetValue<int?>("WCFClientServiceMaxItemsInObjectGraph", true, 65535);
            }
            return clientServiceMaxItemsInObjectGraph.Value;
        }
    }

    ....
}

The WCF client implementation inherits from the <a href="http://msdn.microsoft.com/en-us/library/ms576169(v=vs.110).aspx" target="_blank">DuplexClientBase</a> generic class. The channel type is our communication contract interface IRemoteSideCommunicationContract. This client implements also the IRemoteSide and the IRemoteSideCommunicationContract interface. So it can initiate remote calls and handle calls from the remote side. It uses the base.Channel to send requests to the server side.

public class WCFServiceClient : DuplexClientBase<IRemoteSideCommunicationContract>, IRemoteSide, IRemoteSideCommunicationContract
{
    public WCFServiceClient(InstanceContext instanceContext, Binding binding, EndpointAddress remoteAddress) :
        base(instanceContext, binding, remoteAddress)
    {
        ((ICommunicationObject)base.Channel).Closed += new EventHandler(ClientServiceContractClient_StateChanged);
        ((ICommunicationObject)base.Channel).Closing += new EventHandler(ClientServiceContractClient_StateChanged);
        ((ICommunicationObject)base.Channel).Faulted += new EventHandler(ClientServiceContractClient_StateChanged);
        ((ICommunicationObject)base.Channel).Opened += new EventHandler(ClientServiceContractClient_StateChanged);
        ((ICommunicationObject)base.Channel).Opening += new EventHandler(ClientServiceContractClient_StateChanged);
    }

    public event EventHandler StateChanged;

    public new RemoteCommunicationState State
    {
        get
        {
            RemoteCommunicationState ret = (RemoteCommunicationState)Enum.Parse(typeof(RemoteCommunicationState), base.State.ToString());
            return ret;
        }
    }

    private void OnStateChanged()
    {
        if (this.StateChanged != null)
        {
            StateChanged(this, EventArgs.Empty);
        }
    }

    private void ClientServiceContractClient_StateChanged(object sender, EventArgs e)
    {
        Log.Debug("WCFServiceClient.State changed: {0}", ((ICommunicationObject)base.Channel).State.ToString());
        OnStateChanged();
    }


    public RemoteResponse ExecuteRequest(RemoteRequest oRequest)
    {
        RemoteResponse ret = null;
        try
        {
            try
            {
                ret = base.Channel.ExecuteRequest(oRequest);
            }
            catch (FaultException<ExceptionDetail> ex)
            {
                Log.Error(ex);

                // extract & throw original exception from Fault contract
                Exception originalException = null;
                try
                {
                    originalException = (Exception)Activator.CreateInstance(Type.GetType(ex.Detail.Type), BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, null, new object[] { ex.Message, ex }, null);
                }
                catch
                {
                    ExceptionHelper.RethrowExceptionPreservingStackTrace(ex);
                }
                throw originalException;
            }
        }
        catch (CommunicationObjectFaultedException ex)
        {
            // wrap WCF specific exception
            throw new RemoteSideFaultedException(ex.Message, ex);
        }
        catch (EndpointNotFoundException ex)
        {
            // wrap WCF specific exception
            throw new RemoteSideUnreachableException(ex.Message, ex);
        }
        return ret;
    }


    public event EventHandler Closed
    {
        add
        {
            ((ICommunicationObject)this).Closed += value;
        }
        remove 
        {
            ((ICommunicationObject)this).Closed -= value;
        }
    }

    public event EventHandler Faulted
    {
        add
        {
            ((ICommunicationObject)this).Faulted += value;
        }
        remove
        {
            ((ICommunicationObject)this).Faulted -= value;
        }
    }        
    public IRemoteSideCommunicationContract GetCurrentRemoteSideCommunicationContract()
    {
        IRemoteSideCommunicationContract ret = this;
        return ret;
    }
}  

The client side also has its own factory implementation, WCFServiceClientFactory.

public class WCFServiceClientFactory : IRemoteSideFactory
{
    private IDIContainer diContainer = null;
    public WCFServiceClientFactory(IDIContainer diContainer)
    {
        this.diContainer = diContainer;
    }

    public IRemoteSide CreateInstance()
    {
        IRemoteSide ret = null;

        // init binding
        var binding = new NetTcpBinding();
        binding.Security.Mode = SecurityMode.None;
        binding.Security.Transport.ClientCredentialType = TcpClientCredentialType.None;

        var cm = diContainer.GetLazyBoundInstance<IWCFConfigManager>().Value;
        WCFHelper.ApplyWCFBindingLimits(
            binding,
            cm.ClientServiceMaxSizeInBytes,
            cm.ClientServiceTimeoutInSecs);

        // init endpoint address
        var endpointAddress = new EndpointAddress(cm.ClientServiceAddress);

        // create context
        var clientServiceInstance = diContainer.GetLazyBoundInstance<IRemoteSideCommunicationHandler>().Value;
        var instanceContext = new InstanceContext((IRemoteSideCommunicationContract)clientServiceInstance);

        // create client
        var cscc = new WCFServiceClient(instanceContext, binding, endpointAddress);
        clientServiceInstance.AssignRemoteSide(cscc);

        // init client
        WCFHelper.ApplyWCFEndpointLimits(cscc.Endpoint, cm.ClientServiceMaxItemsInObjectGraph);

        ret = cscc;

        return ret;
    }
}  

This diagram sums up the main interfaces and classes.

Image 2

Using the class library

Now let's take a look how this library can be used as a service layer in a project. When a project uses WCF as the communication layer, the only task is to register the appropriate classes in the DI container. In the server side you need to register the WCFServiceHostFactory class as the IRemoteSideFactory implementation. You need to implement the WCFConfigManager that retrieves the config parameters from the application config store. The RemoteSideCommunicator handles the request from the clients so you need to register an instance of this class in the DI container as the IRemoteSideCommunicationHandler implementation and as IRemoteSideCommunicationContract implementation as well. At this point you can create a new IRemoteSide instance with the factory and open the communication. Since the factory assigns the created IRemoteSide with the RemoteSideCommunicator, now it is ready to be used everywhere in the application to initiate new remote calls.

You need to do exactly the same on the client side, but register the WCFServiceClientFactory as the IRemoteSideFactory.

The sequence diagram shows how a new communication object is initialized in the client side and what happens when a remote call is initiated.

Image 3

Points of Interest

This architecture provides a solution to create a service layer where the communication technology is completly wrapped. This approach becomes extremly handy when the communication technology needs to be replaced during the project lifetime. If you have special or different requirements for the communication layer you can easily hook in a new implementation with only a few lines of code and without any effect on the other layers of the application.

I will show you a conrete example how we combine our dynamic proxy generator and this service layer to create an architecture which totally hides the service layer so you don't event need to bother if you call a function locally or remotely.

History

  • 14th April, 2014: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)