Using LiteDB in Xamarin / Xamarin.Forms

Table of Contents

Overview

LiteDB can be used in a similar way as SQLite. The data base needs no server and has no integrated user / rights management. The documents can be stored both as generic BsonDocument types, but also as DAO classes. Most queries can be expressed via LINQ.

var connection = new ConnectionString
{
  Filename = "./data/my_db.db",
  Connection = ConnectionType.Direct
};
using(var db = new LiteDatabase(connection)) {
  var myCollection = db.GetCollection<MyModel>();
  var newModel = new MyModel { Name ="Max Musternamm", Age = 33,
    Address = new MyAddress {Street = "Mustergasse", City = "London" }};
  myCollection.Insert(newModel);
  // or myCollection.Upsert(newModel);

  var fromLondon = myCollection.Query()
    .Where(w => w.Address.City == "London")
    .OrderByDescending(o => o.Age)
    .ToArray();
}

Initialization

To be able to use LiteDB, an object for controlling the connection ConnectionString must be created. This contains the path to the database file as well as the mode how this file should be accessed (Direct, Shared). In addition, it is also possible to encrypt the database (Password property in the ConnectionString).

The database instance receives the configuration for the connection and works with it.

var connection = new ConnectionString
{
  Filename = "./data/my_db.db",
  Connection = ConnectionType.Direct,
  Password = "SuperSecret"
};
var db = new LiteDatabase(connection);

Modes

Currently two modes are supported for the connection.

  • Shared: This allows multiple database instances to access the same database file. This option is currently not available for mobile operating systems (see also Issue #1618).
  • Direct: Only one instance is allowed to access the database file (but also from different threads). Before a new instance can access it, the previous one must be closed cleanly with Dispose. If another instance accesses it, the connection is closed with an exception.

Queries

The data can now be written into the collections and read out again very easily. For the queries SQL syntax can be used on untyped collection as well as LINQ on typed collections. Most possibilities from LINQ are offered, but not all. For this reason all queries should be tested with real data. More about queries and built-in functions can be found in the official documentation.

// Add unique index
var myCollection = db.GetCollection<MyModel>();
myCollection.EnsureIndex(i => i.Number, true);

// Add data
var model = new MyModel {
  Number = 25,
  Name = "My Model",
  Price = 22.99m
};
myCollection.Insert(model);

// Query
var priceList = myCollection.Query()
  .Where(w => w.Price > 20m)
  .OrderByDescending(o => o.Price)
  .Select(w => s.Price)
  .ToArray();

// Update (ID necessary)
var model2 = myCollection.Query()
  .Where(w => w.Number == 100)
  .Single();
model2.Price = 98.33m;
myCollection.Update(model2);

// Or (insert / update)
myCollection.Upsert(model2);

Dependency Injection

I really like to use the DI (Dependency Injection) pattern in development to make my code more testable. In Xamarin, I use the Prism.Forms library for this purpose.

LiteDB is very nicely designed in this regard and provides the appropriate interfaces for all the necessary services, such as ILiteDatabase for the database and ILiteCollection<TModel> for accessing the documents. With Xamarin, the database must be registered as a singleton for this purpose (Direct mode). The collections should be registered as transient (have no IDisposable interface).

/* App.xaml.cs */
private static ILiteDatabase? _liteDatabase;

protected override void RegisterTypes(IContainerRegistry containerRegistry) {
  // Register database
  RegisterDatabase(containerRegistry);
}

private void RegisterDatabase(IContainerRegistry containerRegistry) {
  // Registration of the data type serialization,
  // if these do not correspond to the "default",
  // here from NetUnits units
  BsonMapper.Global.RegisterType(
    mass => new BsonDocument(new Dictionary<string, BsonValue> {
      { 
        nameof(Mass.Unit),
        new BsonValue((int)mass.Unit)
      },
      {
        nameof(Mass.Value),
        new BsonValue(mass.Value)
      } 
    }),
    doc => Mass.From(
      doc[nameof(Mass.Value)].AsDouble,
      (MassUnit)doc[nameof(Mass.Unit)].AsInt32)
  );
  // Register database as singleton
  containerRegistry.RegisterInstance(_liteDatabase
    ??= CreateDatabase(containerRegistry));
}

private ILiteDatabase CreateDatabase(IContainerRegistry containerRegistry) {
  // Path to database file
  var resolver = containerRegistry.GetContainer();
  var fileSystem = resolver.Resolve<IFileSystem>();
  var dbPath = Path.Combine(fileSystem.AppDataDirectory, "databases");
  if (!Directory.Exists(dbPath)) {
    Directory.CreateDirectory(dbPath);
  }

  var dbFileName = Path.Combine(dbPath, "mydb.ldb");
  var connection = new ConnectionString {
    Filename = dbFileName,
    Connection = ConnectionType.Direct
  };
  
  // Register connection
  containerRegistry.RegisterInstance(connection);
  var db = new LiteDatabase(connection) {CheckpointSize = 100};
  
  // Set indezes and register collections
  var col1 = db.GetCollection<MyModel>();
  col1.EnsureIndex(i => i.Timestamp);
  col1.EnsureIndex(i => i.Localtion.City);
  containerRegistry.Register<ILiteCollection<MyModel>>(t =>
    t.Resolve<ILiteDatabase>().GetCollection<MyModel>());

  return db;
}

With the registration I also add the definition of the indexes to the collections, so that I have all the definitions for the database in one place. In the ViewModels I can now use the collections as dependencies and resolve them by DI.

public sealed class StorageService : IStorageService {
  private readonly ILiteCollection<MyModel> _myRepo;
  
  public StorageService(ILiteCollection<MyModel> myRepo) {
    _myRepo = myRepo;
  }
}

Without Dependency Injection (DI) with Lazy

Without DI, the private static instance _liteDatabase can be implemented as a property with Lazy<> initializer, so that you can access the database from the ViewModels using this property.

/* App.xaml.cs */

// Thread-safe singleton implementation with Lazy
private static Lazy<ILiteDatabase> _db =
  new Lazy<ILiteDatabase>(CreateDatabase, LazyThreadSafetyMode.PublicationOnly);

private static ILiteDatabase CreateDatabase() {
  // Initialize database
  ...  
}

// Static property for access
public static ILiteDatabase DB => _db.Value;

The access is now done via the static property in the App class.

public sealed class MyViewModel : ObservableModel {
  private readonly ILiteCollection<MyModel> _myRepo_;
  
  public MyViewModel() {
    _myRepo = App.DB.GetCollection<MyModel>();
  }
}

Known issues

Xamarin unfortunately does not provide a way to detect that the app has been terminated (not just moved to background). However, this is the limitation of mobile systems, which can terminate an app in the background at any time without warning.

Due to this behavior, the instance (singleton) is not properly removed from the DI (Dispose is not called). When the app is restarted, the database is re-registered, but access fails because only one instance is allowed in Direct mode.

I work around this problem by defining a static variable in App.xaml.cs for the database and checking it during registration. I have not yet found a better solution to this.

If there is a better way to do this, please comment below.

comments powered by Disqus

Related