Remote Object Repository





5.00/5 (4 votes)
Repository layer which creates proxy class for an object which is implemented in a different component.
Introduction
This article introduces a client-server architecture where the communication between the two sides is completely hidden. In this architecture, when calling a method you don't even have to know if it is executed locally or remotely. Suppose that we have a Calc
class on the server side that implements ICalc
interface and contains the logic how to add or subtract two numbers. When we are about to add two numbers on the client side, we would like to use that implementation. With our solution, it is possible to instantiate a proxy class on client side which implements the ICalc
interface and when you invoke its Add
method, the call will be automatically forwarded to the server side where it will be evaluated. So it is transparent to the client that the real ICalc
implementation is on the server side and if we use DI container to get a new instance, it is absolutely hidden where it is implemented. For the caller, the proxy object seems like a common local instance so the property getters and setters, the event subscription and unsubscription and method invocations work as normal.
However, you always have to use the proxy classes carefully because the communication overhead can cause performance problems. To reduce this problem, we introduced journals to batch remote operations before sending them. Whenever a journal is opened, then no remote call would be started till the journal is closed. The remote calls are queued in the journal and when the journal is disposed, all the calls are sent at once for the remote side.
Background
We utilize two main components of our libraries to achieve our goals. The DynamicProxy helps us to generate fake implementations for interfaces and our remoting layer responsible for conducting the remote calls. This is covered in my first article.
We use our Remote Operation Layer to start remote calls between the remote object repositories in different components at different locations. This is explained in my second article.
Using the Code
The diagram below shows the high level architecture. The architecture is symmetric, each side can instantiate proxy classes what is implemented on the other side.
First, we create a class inherit from the abstract DynamicProxy
class. This RemoteObjectRepositoryDynamicProxy
class describes what happens when the proxy interface implementation class gets a call (see details in Dynamic Interface Implementation article). The main idea is when a call is received, this class does a remote call to execute the local call on the remote side where the real implementation is. But we try to keep the traffic low so we store the state of the remote object in local. So the proxy object can return property values without network traffic. To do this, we need to get the initial state of the object after it is created on the remote side and if the state is changed on the remote side, the local proxy object should be notified and get the new value.
This notification event is defined in the IRemoteCallable
interface which should be implemented by all proxy object. If the proxy object is IRemoteCallable
, then the RemoteObjectRepositoryDynamicProxy
class subscribes to this event and handles the event when a new value is coming.
/// <summary>
/// Interface for objects which can be call from the remote side
/// </summary>
public interface IRemoteCallable
{
/// <summary>
/// Raised when remote property changed
/// </summary>
event EventHandler<RemotePropertyChangedEventArgs> RemotePropertyChanged;
}
We lower the network traffic with journaling too. This means when a remote call is needed, then the proxy writes the operation to the journal if it is possible. The property sets and event subscriptions can be stored in journals but the method invocations is always sent to the remote side immediately.
The journal is handled by DynamicProxyJournalManager
. This class implements the related interface IDynamicProxyJournalManager
. It only contains one method.
/// <summary>
/// Journal interface to pile calls up
/// </summary>
public interface IDynamicProxyJournalManager
{
/// <summary>
/// Start new journal. While the journal is active no remote call
/// will be started just collect the calls.
/// When journal disposed all the collected calls will be sent.
/// </summary>
/// <returns>Journal as IDisposable
/// which can be disposed to finish the journal</returns>
IDisposable StartJournal();
}
The StartJournal
method creates a new journal if there is no current journal. Journals are created per thread or task. If a journal already exists for a thread, then that instance would be returned. The return value type is IDisposable
because the only important attribute of the journal from the callers point of view that it is disposable. So the start
method should always be used in using
blocks. The manager stores the reference numbers of a journal and if the last reference is disposed the journal, the manager executes only one remote call and passes all collected operations to the remote side. There, the operations are executed in a batch. The remote calls are initiated by the RemoteObjectRepository
.
The RemoteObjectRepository
manages the communication with remote object repository of the remote side. Because its task is symmetrical, it has some Local and Remote postfixed methods. For example, ExecuteBatchRequestRemote
responsible for calling the other side’s ExecuteBatchRequestLocal
methods.
The CreateObjectRemote
and CreateObjectLocal
pair are responsible for instantiating a new proxy object and a real object pair. The CreateObjectRemote
is registered in the DI container as the creation methods for the remotely implemented interfaces. The method first creates an id for the new object. This id will identify this new object on both sides and this id will be sent through the network. This method uses a DynamicProxyFactory
to create proxy objects. Register this object in the local remote object repository with the new id. Then, it creates a new RemoteOperation
which contains the information for the remote side to create the real instance with the same id. Then the operation is stored in the current journal. During the execution of the journal, the CreateObjectLocal
will be called on the remote side. This gets a new real instance from the DI container and registers it in the remote object repository with the given id. Then the repository subscribes for all the events of that object so it can notify the caller side when an event is fired. Finally, the initial state of the object is queried and returned. (The state of the object means the value of the properties).
private object CreateObjectRemote(Type interfaceType)
{
object ret = null;
if (CheckRemoteTypeAvailable(interfaceType))
{
RemoteObjectRepositoryId newId = RemoteObjectRepositoryId.NewId();
RemoteObjectRepositoryDynamicProxy p =
(RemoteObjectRepositoryDynamicProxy)factory.CreateDynamicProxy
(interfaceType, interfaceType, this, journalManager);
ret = p;
RegisterNewObject(interfaceType, ret,
newId, p.HandleEvent, p.InitializeHandler, p.DisposeInternal);
using (journalManager.StartJournal())
{
journalManager.AddJournalItem
(new RemoteOperation(RemoteOperationType.Initialize, ret, interfaceType, null));
}
}
else
{
throw new InvalidOperationException(string.Format
("The {0} típus not registered as remote callable!", interfaceType.FullName)); //LOCSTR
}
return ret;
}
public object CreateObjectLocal
(Type interfaceType, RemoteObjectRepositoryId newId, HashSet<string> propList)
{
object ret;
object obj = diContainer.GetLazyBoundInstance(interfaceType).Value;
RegisterNewObject(interfaceType, obj, newId);
SubscribeAllEventLocal(obj, interfaceType, newId);
ret = GetObjectCurrentValues(newId, interfaceType, propList);
return ret;
}
After the creation, we have an object in our hands which implements the requested interface, this is a proxy object stored in the local remote object repository with a RemoteObjectRepositoryId
, the remote side’s remote object repository contains an object which is a real implementation of the requested interface and has the same id. So you can use the proxy object like the real implementation of that interface.
public abstract class RemoteObjectRepository :
IRemoteObjectRepository, IRemoteObjectRepositoryInternal
{
private IDynamicInterfaceImplementor interfaceImplementor = null;
private DynamicProxyFactory<RemoteObjectRepositoryDynamicProxy> factory = null;
private IDIContainer diContainer = null;
private DynamicProxyJournalManager journalManager = null;
protected abstract int MyComponentID
{
get;
}
protected abstract TReturnType ExecuteOnRemoteSide
<TReturnType>(RemoteOperationDescriptor rso);
protected abstract void ExecuteOnRemoteSide(RemoteOperationDescriptor rso);
protected abstract IEnumerable<Type> GetTypesImplementedByThisSide();
}
The RemoteObjectRepository
is an abstract
class and has some abstract
members. There are two ExecuteOnRemoteSide
methods. These are responsible to manage the remote calls and should be implemented in each side. There is a MyComponentID
property which identify the remote object repository.
If an interface can be used from a remote location, it should be marked with RemoteObjectRepositorySupportedTypeAttribute
and this attribute needs a component id representing which side contains the real implementation. And this is why we need the last abstract
method, the GetTypesImplementedByThisSide
. This method returns the list of the interface types which have the real implementations in the current component. This method is called in SendRemoteTypesAvailable
which process this list and send it to the other side. When the other repository gets the list of the types, it registers these interface types in the DI container and sets the creation method of the proxy class to the above mentioned CreateObjectRemote
method.
As you can see in the downloadable sample code, it is really easy to set up an environment for this library. In the sample, I use the ICalc
interface which is attributed with RemoteObjectRepositorySupportedType
attribute with a parameter that indicates that this interface is implemented on server side (ComponentId.Server
). I have to implement the RemoteObjectRepository abstract
members on both project (ClientROR
, ServerROR
classes). The important thing is that the client side immediately calls SendRemoteTypesAvailable
and sends its classes and the server side calls this method when it gets the client side's types. The server side first sets up the communication objects and then creates the server-side RemoteObjectRepository
and waits for the call from client side. The client side also sets up the communication classes and its RemoteObjectRepository
. After the setup, you can get an ICalc
implementation at client side and call its methods. These calls will be executed at server side where the logic is. This is just a simple usage. You can try to use properties and events also or you can place classes on both sides or create a server remote object repository that supports multiple clients (as we did in our product).
For example, we use this library to control view models properties from server side. In our architecture, we have a server side that contains all the business logic and a client side which now uses WPF as presentation technology. We use the MVVM patterns so in the client side, there are a lot of viewmodel
classes which are binded to a WPF view. It would be very nice if we could set a property of the viewmodel
from server side which will affect the UI.
To achieve this, we use interfaces for the viewmodel
s. For example, we have a login viewmodel
which has an ILoginViewModel
interface. The attribute shows that it is implemented on client side. It has two properties (LoginName
and Password
) and one event (Login
).
[RemoteObjectRepositorySupportedType(DefaulRemoteObjectRepositoryComponent.Client)]
public interface ILoginViewModel
{
string LoginName { get; set; }
string Password { get; set; }
event EventHandler TryLogin;
}
Now, we should do the real implementation on the client side. We create a LoginViewModel
class which implements this interface. The implementation is a little bit tricky because it implements the interface explicitly and implicitly. The implicitly implemented properties are binded to the WPF controls and it raises the RemotePropertyChanged
event to notify the server side if the property value is changed. The explicit implementation of a property is called by the remote object repository and raises the PropertyChanged
event which informs the WPF view that a property value is changed so refresh the UI.
For the event, we create a related command. We bind this command on the view and the command implementation is firing the event. So for example, if a button is clicked on the UI, it executes the command which raises the event and the remote object repository will raise that event on the other side.
public class LoginViewModel : ILoginViewModel
{
private string loginName = null;
public string LoginName
{
get
{
return loginName;
}
set
{
loginName = value;
OnRemotePropertyChanged("LoginName", loginName);
}
}
string ILoginViewController.LoginName
{
get
{
return loginName;
}
set
{
loginName = value;
OnPropertyChanged("LoginName");
}
}
private string password = null;
public string Password
{
get
{
return password;
}
set
{
password = value;
OnRemotePropertyChanged("Password", loginName);
}
}
string ILoginViewController.Password
{
get
{
return password;
}
set
{
loginName = value;
OnPropertyChanged("Password");
}
}
public event EventHandler<RemotePropertyChangedEventArgs> RemotePropertyChanged;
private void OnRemotePropertyChanged(string prop, object value)
{
if(RemotePropertyChanged != null)
{
RemotePropertyChanged(this, new RemotePropertyChangedEventArgs(prop, value));
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string prop)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(prop));
}
}
public ICommand LoginCommand
{
get;
set;
}
private void LoginCommandImpl(object o)
{
if (Login != null)
{
Login(this, EventArgs.Empty);
}
}
public event EventHandler Login;
}
Because the interface is marked that the client side contains the implementation, the client side sends this type to the server side as a remotely implemented interface and the server's remote object repository will register the proxy creation method to the DI container. When we need a login user interface in a business process, then we retrieve a new instance of ILoginViewModel
from the DI on the server side and subscribe to the login event to do the business logic when the user performed a login. (And we don’t know anything about the design if the user should click on a button or press a key.) In the event handler, we get the value of the LoginName
and the Password
. These values already contain the value given by the user so there is no network traffic getting the property values. So the server side code looks like this:
IDynamicProxyJournalManager journal = DIContainer.Instance.GetInstance<IDynamicProxyJournalManager>();
using (journal.StartJournal())
{
ILoginViewController loginVM = DIContainer.Instance.GetInstance<ILoginViewController();
loginVM.Login += loginVM_Login;
}
private void loginVM_Login(object sender, EventArgs e)
{
ILoginViewController loginVM = sender as ILoginViewController;
string loginname = loginVM.LoginName;
string password = loginVM.Password;
businesslogic.DoLogin(loginname, password)
}
Points of Interest
Our client side viewmodel
classes have almost the same implementation and we try to prevent business logic in these classes, so we use TT script to generate these classes from the interface files. Create a DI container abstraction so everybody can use his own favourite implementation.
History
- 28th January, 2015: Initial version