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.
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.
[ServiceContract(CallbackContract=typeof(IRemoteSideCommunicationContract))]
public interface IRemoteSideCommunicationContract
{
[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.
public interface IRemoteOperationHandler
{
RemoteRequest CreateRequest(RemoteOperationDescriptor rso);
RemoteResponse ExecuteRequest(RemoteRequest request, Type callableTypeAttribute, Type callableFuncAttribute);
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.
public interface IRemoteSideCommunicationHandler
{
TResult ExecuteOnRemoteSide<TResult>(RemoteSideIDType remoteSideID, RemoteOperationDescriptor rso);
void ExecuteOnRemoteSide(RemoteSideIDType remoteSideID, RemoteOperationDescriptor rso);
RemoteSideIDType CurrentRemoteSideID { get; }
event EventHandler<RemoteSideConnectedEventArgs> RemoteSideConnected;
event EventHandler<RemoteSideDisconnectedEventArgs> RemoteSideDisconnected;
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
).
public interface IRemoteSide
{
event EventHandler Closed;
event EventHandler Faulted;
RemoteCommunicationState State { get; }
event EventHandler StateChanged;
void Open();
void Close();
void Abort();
IRemoteSideCommunicationContract GetCurrentRemoteSideCommunicationContract();
}
To instantiate an IRemoteSide
interface implementation we use the factory pattern defined in the IRemoteSideFactory
interface.
public interface IRemoteSideFactory
{
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);
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)
{
throw new RemoteSideFaultedException(ex.Message, ex);
}
catch (EndpointNotFoundException ex)
{
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;
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);
var endpointAddress = new EndpointAddress(cm.ClientServiceAddress);
var clientServiceInstance = diContainer.GetLazyBoundInstance<IRemoteSideCommunicationHandler>().Value;
var instanceContext = new InstanceContext((IRemoteSideCommunicationContract)clientServiceInstance);
var cscc = new WCFServiceClient(instanceContext, binding, endpointAddress);
clientServiceInstance.AssignRemoteSide(cscc);
WCFHelper.ApplyWCFEndpointLimits(cscc.Endpoint, cm.ClientServiceMaxItemsInObjectGraph);
ret = cscc;
return ret;
}
}
This diagram sums up the main interfaces and classes.
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.
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