Simplify Your Storage in Xamarin with SimpleStorage

While developing a mobile application, it’s important to consider the data storage architecture that best suits the needs of the app. In many cases, especially if the app is data-intensive, a database is the best option. I have had a great experience using the SQLite.NET package for Xamarin, in particular. However, there are some cases where a database is not the best option for data storage. For example, a simple key-value store is often a great solution for small pieces of data, such as user preferences and app settings, that can be easily retrieved by a unique key.

Introduction to SimpleStorage

SimpleStorage is a cross-platform implementation of a key-value data store that uses each platform’s native Preferences API. For iOS, this means NSUserDefaults, and for Android, this means SharedPreferences.

SimpleStorage provides a simple, synchronous API. It makes it easy to store arbitrary key-value pairs of data across both iOS and Android. SimpleStorage also utilizes a binary serialization method that allows objects to be serialized to disk and deserialized when read back. This makes it simple to store arbitrary objects, whether built-in (e.g., DateTime) or custom. I recommend SimpleStorage as a reliable, simple, and easily-integrated solution for your Xamarin project.

Some key extensions

While using SimpleStorage in my latest project, I noticed that it was missing a few key features. These included the ability to:

  • Retrieve all keys in a given storage group
  • Retrieve all storage groups
  • Retrieve a full storage group in a single shot

In particular, the inability to retrieve all keys was a show stopper for me. I decided to implement this functionality as an extension to SimpleStorage.

Desired interfaces

I wanted to keep a similar interface to the real SimpleStorage in my wrapper class. Since we are working in C#, I decided to create an interface called IPersistentStorage to define the API of this module. Using an interface also allows us to implement another version of these extensions to work with Unit Tests (we’ll see this later).


namespace PersistentStorage
{
  public interface IPersistentStorage
  {
    IEnumerable<string> Groups { get;  }
    IEnumerable<string> KeysForGroup(string group);

    string Get(string group, string key, string defaultValue);
    string Get(string group, string key);
    T Get<T>(string group, string key, T defaultValue) where T : new();
    T Get<T>(string group, string key) where T : new();
    IEnumerable<T> GetAll<T>(string group, string key) where T : new();
    IEnumerable<string> GetAll(string group, string key);

    void Set<T>(string group, string key, T value) where T : new();
    void Set(string group, string key, string value);
    void SetAll<T>(string group, string key, IEnumerable<T> value) where T : new();
    void SetAll(string group, string key, IEnumerable<string> value);

    void Delete(string group, string key);

    Dictionary<string, T> GetGroup<T>(string group) where T : new();
    Dictionary<string, string> GetGroup(string group);

    bool ContainsKey(string group, string key);
  }
}

This interface is similar to the SimpleStorage API, except for a few differences. The first difference is the Groups property. This API will return an IEnumerable of all the storage group names. The next difference is the KeysForGroup method, which will return a list of keys for a given storage group. Finally, the last big difference is the GetGroup API, which returns a dictionary of all key-value pairs for a given storage group. This is a really nifty utility method in some situations.

Extensions implementation

The backbone of the implementation is based on a private dictionary structure, which is used internally to store the groups and their keys. This is implemented as a ConcurrentDictionary to allow for thread-safe manipulation of the structure. Below is the class definition and constructor:


public class SimpleStorageExt : IPersistentStorage
{
  ConcurrentDictionary<string, List<string>> groupMappings = new ConcurrentDictionary<string, List<string>>();

  public SimpleStorage()
  {
    var grp = PerpetualEngine.Storage.SimpleStorage.EditGroup("schema");
    var groups = grp.Get<string[]>("groups");
    if (groups == null) {
      Console.WriteLine("Groups was null when initializing SimpleStorage");
    } else if (groups.Any()) {
      foreach (var storageGroup in groups) {
        var keyStorage = PerpetualEngine.Storage.SimpleStorage.EditGroup($"{storageGroup}Keys");
        var keys = keyStorage.Get<string[]>("keys");
        groupMappings.TryAdd(storageGroup, keys.ToList());
      }
    }
  }
  // ... more to come
}

The constructor initializes the groupMappings dictionary based on the stored schema of the key value store. The schema is updated as keys are added and removed, which you will see in a bit. The schema group stores a list of the storage groups. Then, each storage group will have a corresponding *Keys storage group, which holds all the keys associated with that group. With both of these predefined data structures, we can reconstruct our storage groups and their corresponding keys.

Now that we have the basic architecture defined, the next piece is the implementation of the individual interfaces. In some cases, the implementation of the wrapped method is simple: just a pass-through to the SimpleStorage equivalent. For example, the Get method is defined below:


public string Get(string group, string key)
{
  var grp = PerpetualEngine.Storage.SimpleStorage.EditGroup(group);
  return grp.Get(key);
}

In the SimpleStorage API, you can only perform Get/Set/Delete operations within the context of a SimpleStorage object (returned from EditGroup). This wrapper API just allows the caller to pass in the group and key in the same method call, which I think is a nice abstraction.

However, the tricky part to this implementation is keeping tracking of the groups and keys that are being stored. Take the Set method, for example:


public void Set<T>(string group, string key, T value) where T : new()
{
  TryAddGroupAndKey(group, key);
  var grp = PerpetualEngine.Storage.SimpleStorage.EditGroup(group);
  grp.Put<T>(key, value);
}

You’ll notice the TryAddGroupAndKey method call, which basically takes the group and key combination and tries to add a new group and/or key to the store. The TryAddGroupAndKey method is defined below:


void TryAddGroupAndKey(string group, string key)
{
  var existingKeys = new List<string>();
  if (groupMappings.TryGetValue(group, out existingKeys)) {
    if (!existingKeys.Contains(key) && key != null) {
      existingKeys.Add(key);
      PerpetualEngine.Storage.SimpleStorage.EditGroup($"{group}Keys").Put("keys", existingKeys.ToArray());
    }
  } else {
    var newKeys = new List<string> { key };
    groupMappings.TryAdd(group, newKeys);

    PerpetualEngine.Storage.SimpleStorage.EditGroup("schema").Put("groups", groupMappings.Select(g => g.Key).ToArray());
    PerpetualEngine.Storage.SimpleStorage.EditGroup($"{group}Keys").Put("keys", newKeys.ToArray());
  }
}

Besides updating the groupMappings dictionary, we also need to update the Keys for that group. If this is a new group, then we need to add it to the schema store. You can imagine a very similar (but opposite) implementation for TryRemoveGroupAndKey which is used in the Delete method. Every time the dictionary is mutated, we update persisted schema accordingly, which allows us to maintain a proper storage schema at all times. The entire implementation of PersistentStorage can be found here.

What About Tests?

I also wrote an implementation of IPersistentStorage to be used for testing, called TestStorage. You can find this implementation here as well. This implementation is not backed by SimpleStorage at all, but it keeps a local dictionary of groups, keys, and values in RAM. It has all the same interfaces and functionality as the “real” version, but you typically don’t need to worry about actually saving the data while unit testing.

So, that’s it! I had a fun time extending SimpleStorage’s capabilities, and I hope you learned a thing or two about how to store simple key-value-pair data in Xamarin/C#.

Conversation
  • Jeff says:

    How would you compare this with Akavache which is a key/value store layer over SQLite?

    https://github.com/akavache/Akavache

    • Matt Rozema Matt Rozema says:

      Hi Jeff,

      Thanks for asking. Akavache is also a great key-value storage utility, and I can see many cases where this library would be very useful. It has a lot of great utilities for caching network responses and invalidating time-sensitive material.

      That being said, I would say that Akavache would be overkill for a simple application. You don’t need SQLite and all the overhead it brings if you are just trying to store simple key-value pairs of data. This is where SimpleStorage excels – it is very light-weight and easy to use. The synchronous API of SimpleStorage is also handy for some situations, and SimpleStorage also exposes an asynchronous API as well. Unless you are already using SQLite in your app, there is no need to introduce that dependency just for this sort of storage.

      In summary, I would ay that Akavache is a “beefed up” version of SimpleStorage. The core API is pretty similar. If your app requires offline data caching or data invalidations, then Akavache would be a great option, but for simple key-value storage (e.g. user preferences that are just stored and retrieved at run time), then SimpleStorage would be my recommendation.

      Thanks for the question! I’d be curious to know your opinion.

  • Jeff says:

    I think your summary is spot on when you say “If your app requires offline data caching or data invalidations, then Akavache would be a great option, but for simple key-value storage (e.g. user preferences that are just stored and retrieved at run time), then SimpleStorage would be my recommendation.”

    I would only add that in my experience, user expectations for offline capabilities are growing and will continue to do so.

    So if anyone asks “what should I use” the answer is of course, as always, “it depends.” :)

  • Comments are closed.