Telerik blogs

In a real world scenario the loss of connection to the primary database server due to various reasons is a danger that your application may need to handle. Telerik Data Access can help you do that. Today we start a series of blog posts which will expand upon the concepts shown in our previous post to show you how to implement a SmartContext class which can help you with such scenario.

The concept behind the SmartContext class is that it should allow you to work with the database in a manner very similar to how a normal instance of your model`s context would. Additionally it will have logic to automatically cache data in a backup storage and seamlessly switch to using this backup storage in case the connection to the primary database server is lost.

In the first part of this series we will see how to persist the changes made by CRUD operations in a backup storage so that they would be readily available in case of a connection failure.

When Should Entities be Backed Up

One question we must answer before we continue is what data do we need to back up? The answer would be highly specific to the concrete scenario which the application covers, it depends on when we can expect a connection loss and other conditions.

For the current implementation we will assume that a connection loss can occur at any given time. Therefore, to continue working even when the primary database server is not available, we would need to back up all data that is being retrieved from the database and push any inserts, updates and deletes to the backup storage. This way the backup storage will always contain the entities with which the application is currently working.

Core Members of the SmartCotnext Class

To start the implementation of the SmartCotnext class we would need two instances of the model`s context – one to work with the primary storage and one for the backup storage. Additionally we would need a context factory to instantiate those objects for us.

public class SmartContext : IDisposable
{
        private FluentModel primaryContext;
        private FluentModel backupContext;
        private ContextFactory contextFactory;
}

Backing up Entities

In order to save a retrieved, inserted or updated entity to the backup storage we would need to attach it to the backupContext object. To this end you would need to detach it from the primaryContext object, create a deep clone of the detached entity and then attach it to the backupContext. This process is a bit different for newly added entities as they cannot be detached from their managing context. Consider the generic SaveToBackupStorage method:

private void SaveToBackupStorage<T>(IEnumerable<T> entities, FetchStrategy fetchStrategy = null)
{
    foreach (T entity in entities)
    {
        if (OpenAccessContext.PersistenceState.GetState(entity) == ObjectState.New)
        {
            T clone = DeepClone<T>(entity);
            this.backupContext.AttachCopy<T>(clone);
        }
        else
        {
            this.AttachToBackupContext<T>(entity, fetchStrategy);
        }
    }
    this.backupContext.SaveChanges(); }

As the name suggest this method can save a number of entities of the given type to the backup context. It also takes a FetchStrategy as an argument to be able to back up entire graphs of objects. The conditional statement checks whether the entity that should be backed up is new and handles it accordingly.

As entities with ObjectState New cannot be detached from their managing context, we only need to deep clone them and attach them to the backupContext.

Entities which are not new are backed up with the AttachToBackupContext method. Notice that when detaching the object, the CreateDetachedCopy method is called with a FetchStrategy as a second parameter. This is done in order to enable the backup of graphs of related objects.

private void AttachToBackupContext<T>(T entity, FetchStrategy fetchStrategy = null)
{
    T detachedEntity = this.primaryContext.CreateDetachedCopy(entity, fetchStrategy);
    T cloneEntity = DeepClone<T>(detachedEntity);
    this.backupContext.AttachCopy(cloneEntity);
}

These will be the methods used further in the implementation of the SmartContext class to back up the required entities.

Handling Back up of Retrieved Entities

To facilitate the retrieval and backup of all entities we will create a generic GetMultiple method which retrieves and backups a number of entities with the specific type:

public IEnumerable<T> GetMultiple<T>(Expression<Func<T, bool>> condition = null, FetchStrategy fetchStrategy = null)
{
    this.primaryContext.FetchStrategy = fetchStrategy;
    var query = this.primaryContext.GetAll<T>();
    if (condition != null)
    {
        query = query.Where(condition);
    }
    IList<T> entities = entities = query.ToList();
    this.primaryContext.FetchStrategy = null;
    if (this.primaryContext.Mode == ContextMode.OnlineMode)
    {
        this.SaveToBackupStorage<T>(entities, fetchStrategy);
    }
    return entities; }

The method takes as arguments a condition which determines which entities should be retrieved and a fetch strategy in case any related objects need to be loaded as well. In this scenario the handling of the backup is pretty straight forward – the retrieved entities are just passed to the SaveToBackupStorage method along with the FetchStrategy object used to load them.

A consideration you must keep in mind is that to backup navigation properties, they must be loaded together with the retrieved entity by passing a fetch strategy to the GetMultiple method.

Note that in the provided sample the SmartContext class also has a GetSingle method which is based on the same concepts.

Handling Back up of Newly Added and Updated Entities.

Generally to add entities to a context, the Add method is called. This is also be true for the SmartContext as we have an Add method which does nothing more than adding the passed entity to the primaryContext object:

public void Add(Object entity)
{
    this.primaryContext.Add(entity);
}

At first glance this might seem as a good place to add logic to back up the newly added entity. But what about updated entities? As entities are managed by their respective context, updates are automatically handled – there is no method which we can duplicate and add in it logic to handle the backup.

There is however one common point for both insert and update operations – the SaveChanges method is called after them in order to persist the changes to the database. This is where we will back up both inserts and updates.

public void SaveChanges()
{
    if (this.primaryContext.HasChanges)
    {
        if (this.primaryContext.Mode == ContextMode.OnlineMode)
        {
            ContextChanges contextChanges = this.primaryContext.GetChanges();
            IList<User> userInserts = contextChanges.GetInserts<User>();
            IList<User> userUpdates = contextChanges.GetUpdates<User>();
            IList<Group> groupInserts = contextChanges.GetInserts<Group>();
            IList<Group> groupUpdates = contextChanges.GetUpdates<Group>();
            this.SaveToBackupStorage<User>(userInserts);
            this.SaveToBackupStorage<User>(userUpdates);
            this.SaveToBackupStorage<Group>(groupInserts);
            this.SaveToBackupStorage<Group>(groupUpdates);
        }
        this.primaryContext.SaveChanges();
    }
    if (this.primaryContext.Mode == ContextMode.OnlineMode)
    {
        this.backupContext.SaveChanges();
    }
}

The Context API provides means to retrieve the present pending changes from a context object. Will use the GetChanges method to get a ContextChanges object and retrieve from it the entities which need to be inserted or updated in the backup storage.

Handling Back up of Deletes

To backup deletes operations you need to do two things – delete the entity from the backup storage and backup the delete operation itself. The second part is required because at a later point you will probably need to push those delete operations from the backup storage to the primary one.

To enable the backup of the delete operations we will create an Artificial Type called DeleteOperation.  It will be used to save in the backup storage the Id and Type of the entity which is to be deleted.

The backup of the deletes will be done in the Delete method of the SmartContext:

public void Delete(Object entity)
{
    if (this.primaryContext.Mode == ContextMode.OnlineMode)
    {
        object detachedEntityToDelete = this.primaryContext.CreateDetachedCopy(entity);
        object entityClone = DeepClone(entity);
        object attachedObject = this.backupContext.AttachCopy(entityClone);
        this.backupContext.Delete(attachedObject);
        this.BackupDeleteOperation(entity);
    }
    this.primaryContext.Delete(entity);
}

The process of deleting the entity from the backup storage again involves detaching the entity from the primaryContext object, cloning the detached entity and attaching it to the backupContext prior to executing the delete operation. After this the BackupDeleteOperation method is called:

private void BackupDeleteOperation<T>(T objectToDelte)
{
    ObjectKey objectToDeleteObjectKey = this.primaryContext.CreateObjectKey(objectToDelte);
    int entityToDeleteId = (int)objectToDeleteObjectKey.ObjectKeyValues.FirstOrDefault().Value;
    string entityToDeleteType = objectToDeleteObjectKey.TypeName.Split('.')[1];
    object entityToDelete = this.backupContext.CreateInstance(DeleteOperationDefinition.DeleteOperationFullTypeName);
    this.backupContext.Add(entityToDelete);
    entityToDelete.SetFieldValue(DeleteOperationDefinition.EntityToDeleteId, entityToDeleteId);
    entityToDelete.SetFieldValue(DeleteOperationDefinition.EntityToDeleteType, entityToDeleteType); }

The method uses the ObjectKey API to get the Id and Type of the passed entity regardless of its concrete type. Then using the Artificial API, the Id and Type are persisted in the backup storage as a DeleteOperation.

Note that in the FluentModelMetadataSource class, the mapping configuration of the DeleteOperation artificial type is only added when the an instance of the context is created to work with the backup storage. This initialization is done by the ContextFactory class.

Using the SmartContext Class

Using the SmartContext class is very close as to how you would use a normal model context. The only difference is that retrieving the entities is done through the GetMultiple and GetSingle methods instead of having a property for each persistent type. The following code snippet is an example of how you can use the SmartContext class:

using (SmartContext sCtx = new SmartContext())
{
    //retrieve an User with the given condition
    //doing this will also backup the retrieved user
    User usr = sCtx.GetSingle<User>((u) => u.UserId == 3);

    //update the name of the user
    usr.Name = "New Name";
    //upon calling SaveChanges the user will be updated
    //in both the primary and the backup storage
    sCtx.SaveChanges();
}

Now the SmartContext class is ready to automatically cache the entities with which your application works in a backup storage.You can find the sample project with the implementation of the SmartContext class and unit tests illustrating its behavior here.

Stay tuned as next time we will extend the SmartContext class to seamlessly switch between using the primary and backup storage.

Download Data Access

About the Author

Kristian Nikolov

Kristian Nikolov is Support Officer.

Comments

Comments are disabled in preview mode.