Telerik blogs

Last time we implemented a SmartContext class which automatically caches the data with which we are working in a secondary database. In Part 2 you will see an example of how to facilitate logic for switching between the primary and backup storage.

The concept is that in case the connection to the primary storage is lost, the SmartContext should seamlessly switch to using the backup storage where all data with which your application has worked up to this point is cached. Then while working with the backup storage, if the primary storage becomes available again the SmartContext should automatically switch to using it.

Switching from Primary to Backup Storage

The actual process of switching to the backup storage is fairly straight-forward. When the SmartContext is using the primary storage, the primaryContext object works with it while the backupContext works with the backup storage. In case a switch is needed, the primaryContext object needs to work with the backup storage while the backupContext will not be necessary. The switch is done with the SwitchToOfflineStorage method:

private void SwitchToOfflineStorage()
{
    this.primaryContext.Dispose();
    this.primaryContext = this.backupContext;
    this.backupContext = null;
    this.StartOnlineConnectionChecker();
}

In order to work with the backup storage we are using the same instance of OpenAccessContext which up to this point was used only for caching. This will allow us to push changes pending at the time of the switch to the backup storage and persist them with less effort.

When to Switch to the Backup Storage

Generally when a connection to the used database server is not available Telerik Data Access methods such as SaveChanges, Delete and execution of LINQ queries will result in an exception being thrown. The exception type can vary based on the method which threw the exception and the reason due to which it was thrown.

In order to switch from using the primary storage to the backup one, the SmartContext will need to intercept and evaluate these exceptions. As the exception type can vary depending on the scenario it would be good to have an extension point for specifying which exceptions should cause a switch. To this end we will add a Func<Exception, bool> delegate in our SmartContext class:

private Func<Exception, bool> isConnectionUnavailableEvaluator;

The method referenced by this delegate will be invoked on the required places in order to evaluate the whether the thrown exceptions are of the expected type.

In our sample application we are locking a table of the primary storage in order to simulate that the database is not available. Therefore, the isConnectionUnavailableEvaluator delegate will by default correspond to the DefaultConnectionEvaluator method which intercepts LockNotGrantedException and OptimisticVerificationException:

private static bool DefaultConnectionEvaluator(Exception ex)
{
    if (ex is LockNotGrantedException || ex is OptimisticVerificationException)
    {
        return true;
    }
    return false;
}

In addition to intercepting one of those exceptions, the switch to backup storage should occur only when the SmartContext uses the primary storage. This check is handled in the methods where a switch can happen.

Switch on Retrieval

When retrieving data through an OpenAccessContext object, an exception would be thrown when executing the LINQ query should the used database is not available. For example in the GetMultiple method of the SmartContext this happens when the ToList method is called for the used LINQ query. The switch happens by catching any Exception, evaluating it with the isConnectionUnavailableEvaluator and checking whether the primaryContext object is in OnlineMode (uses the primary storage). 

try
{
    entities = query.ToList();
}
catch (Exception ex)
{
    if (this.isConnectionUnavailableEvaluator(ex) && this.primaryContext.Mode == ContextMode.OnlineMode)
    {
        this.SwitchToOfflineStorage();
        return this.GetMultiple<T>(condition, fetchStrategy);
    } else
    {
        throw;
    }
}

Note that after the switch is made by calling SwitchToOfflineStorage, the GetMultiple method gets recalled in order to retrieve the required data from the backup storage.

The GetSingle method switches using the same logic but the exception is thrown by the FirstOrDefault extension method.

Switch on Insert and Update

Similarly to how backing up works for Inserts an Updates, switching for those operations also happens in one central place – in the SaveChanges method of the SmartContext. When attempting to push new entries or updates to existing ones to the database using an OpenAccessContext object, an exception will be thrown by the SaveChanges method.

Again to switch we need to catch the Exception thrown from SaveChanges of the primaryContext object, evaluate it and check whether the primaryContext works with the primary storage.

try
{
    this.primaryContext.SaveChanges();
}
catch (Exception ex)
{
    if (this.isConnectionUnavailableEvaluator(ex) && this.primaryContext.Mode == ContextMode.OnlineMode)
    {
        this.SwitchToOfflineStorage();
    } else
    {
        throw;
    }
}
Note that as at this moment inserts and updates have already been cached in the backup storage, there is no need to recall any methods.

Switch on Delete

In the Delete method of the SmartContext, the exception that we need to catch and evaluate before switching to using the backup storage is thrown by the Delete method executed over the primaryContext object:

try
{
    this.primaryContext.Delete(entity);
}
catch (Exception ex)
{
    if (this.isConnectionUnavailableEvaluator(ex) && this.primaryContext.Mode == ContextMode.OnlineMode)
    {
        this.SwitchToOfflineStorage();
    } else
    {
        throw;
    }
}

Recalling any methods will not be necessary as the objects are already marked for deletion in the context which works with the backup storage.

Note that another change in the Delete method of the SmartContext is that backup of the delete operations now happens regardless whether the primary or backup storage is used by the primaryContext object.

Switching from Backup to Primary Storage

You may have noticed StartOnlineConnectionChecker called at the end of the SwitchToOfflineStorage method. This method is used to initialize a System.Threading.Timers Timer which would periodically check whether the primary storage has become available again:

private Timer connectionChecker;
private void StartOnlineConnectionChecker()
{
    TimerCallback callback = new TimerCallback(this.CheckOnlineStorageAvailability);
    this.connectionChecker = new Timer(callback, null, 10000, 10000);
}

The method which does the actual checking is CheckOnlineStorageAvailability. It executes a query to the database and if the query succeeds it switches the SmartContext to using the primary storage:

private void CheckOnlineStorageAvailability(object StateObj)
{
    lock (this.operationsLock)
    {
        contextFactory.SetContextMode(ContextMode.OnlineMode);
        using (FluentModel onlineContxt = contextFactory.GetContext())
        {
            bool exceptionIsThrown = false;
            try
            {
                User usr = onlineContxt.Users.FirstOrDefault();
            }
            catch (Exception)
            {
                exceptionIsThrown = true;
            }
            if (exceptionIsThrown == false)
            {
                this.SwitchToOnlineStorage();
            }
        }
    }
}

The switch is facilitated in the SwitchToOnlineStorage method. There, the timer is disposed and the primaryContext is set to work with the primary storage while the backupContext will once again be used for caching in the backup storage.

private void SwitchToOnlineStorage()
{
    this.connectionChecker.Dispose();
    this.connectionChecker = null;
    this.contextFactory.SetContextMode(ContextMode.OnlineMode);
    FluentModel onlineContext = this.contextFactory.GetContext();
    this.backupContext = this.primaryContext;
    this.primaryContext = onlineContext;
    this.backupContext.SaveChanges();
    if (this.offlineToOnlineSynchronizer != null)
    {
        offlineToOnlineSynchronizer.Invoke(this.backupContext, this.primaryContext);
    }
}

Note that since the check for the online storage and the actual switch are done on a separate thread, we added locking in the following methods of the SmartContext: GetSingle, GetMultiple, Add, Delete, SaveChanges, ClearChanges, CheckOnlineStorageAvailability and Dispose.

Pushing Delete Operations to the Primary Storage

When switching from using the backup storage to the primary one you will probably need to push changes that have accumulated in the backup. Syncing two databases is a highly complex task that is very dependent on the requirements of the present scenario. In our example we will show you a simple way that you can push saved delete operations to the primary storage.

To have a flexible way for pushing the changes in the backup storage to the primary storage we have defined an Action<OpenAccessContext, OpenAccessContext> delegate to the SmartContext. Using it you can pass methods with your custom sync logic to the SmartContext:

private Action<OpenAccessContext, OpenAccessContext> offlineToOnlineSynchronizer;

By default the offlineToOnlineSynchronizer delegate corresponds to DefaultOfflineToOnlineSynchronizer method:

private static void DefaultOfflineToOnlineSynchronizer(OpenAccessContext backupContext, OpenAccessContext primaryContext)
{
    var deleteOperations = backupContext.GetAll(DeleteOperationDefinition.DeleteOperationFullTypeName).Cast<object>().ToList();
    foreach (var deleteOperation in deleteOperations)
    {
        string entityToDeleteType = deleteOperation.FieldValue<string>(DeleteOperationDefinition.EntityToDeleteType);
        int entityToDeleteId = deleteOperation.FieldValue<int>(DeleteOperationDefinition.EntityToDeleteId);
        ObjectKey entityToDeleteObjectKey = new ObjectKey(entityToDeleteType, entityToDeleteId);
        object entityToDelete = null;
        if (primaryContext.TryGetObjectByKey(entityToDeleteObjectKey, out entityToDelete))
        {
            primaryContext.Delete(entityToDelete);
        }
    }
    primaryContext.SaveChanges();
  backupContext.GetAll(DeleteOperationDefinition.DeleteOperationFullTypeName).DeleteAll();
}

The method takes as arguments two instances of OpenAccessContext – one which works with the primary storage and one for the backup. Using the Artificial API, the DeleteOperation objects are retrieved from the backup storage. Then using the Type and Id from each DeleteOperation, we create an ObjectKey for the respective object which should be deleted. With the ObjectKey API we can retrieve the object which needs to be deleted from the primary storage and mark it for deletion. Note that at the end the delete operations are all cleared from the backup storage as they have already been pushed to the primary storage and are no longer needed.

With this our SmartContext is complete and able to both automatically backup the data with which your application works and seamlessly switch between using the primary and backup storage. Check out our full sample illustrating the implementation and behavior of the SmartContext.

Download Data Access

About the Author

Kristian Nikolov

Kristian Nikolov is Support Officer.

Comments

Comments are disabled in preview mode.