c# .net core litedb pattern

Implement Repository Pattern By Using LiteDB

Matt 2020/07/23 10:43:43
44

LiteDB

What is LiteDB ? LiteDB is a small, fast and lightweight .NET NoSQL embedded database.

LiteDB is a serverless database delivered in a single small DLL (< 450kb) fully written in .NET C# managed code (compatible with .NET 4.5 and NETStandard 2.0). It's a NoSQL impletemention.

Repository Pattern

A repository performs the tasks of an intermediary between the domain model layers and data mapping, acting in a similar way to a set of domain objects in memory. Client objects declaratively build queries and send them to the repositories for answers.

Conceptually, a repository encapsulates a set of objects stored in the database and operations that can be performed on them, providing a way that is closer to the persistence layer.

Repositories, also support the purpose of separating, clearly and in one direction, the dependency between the working domain and the data allocation or mapping.

The Codes

No more words, read codes.

POCO, Domain models

public class Vendor
{
    /// <summary>
    /// integer as Primary Key
    /// </summary>
    public int Id { get; set; }
    /// <summary>
    /// vendor name
    /// </summary>
    public string Name { get; set; }
	
    ... add more fields we need
	
    /// <summary>
    /// created time (local time)
    /// </summary>
    public DateTime CreatedTime { get; set; }
    /// <summary>
    /// updated time (local time)
    /// </summary>
    public DateTime ModifiedTime { get; set; }
}

Repositories

Create a BaseRepository with a generic type, and define some basic, must have CRUD functions.

Here, a collection, we could treat it like a RMDB's table.

public abstract class BaseRepository<T> : IBaseRepository<T>
{
    public ILiteDatabase DB { get; }
    public ILiteCollection<T> Collection { get; }

    protected BaseRepository(ILiteDatabase db)
    {
        DB = db;
        Collection = db.GetCollection<T>();
    }

    public virtual T Create(T entity)
    {
        var newId = Collection.Insert(entity);
        return Collection.FindById(newId.AsInt32);
    }

    public virtual IEnumerable<T> All()
    {
        return Collection.FindAll();
    }

    public virtual T FindById(int id)
    {
        return Collection.FindById(id);
    }

    public virtual void Update(T entity)
    {
        Collection.Upsert(entity);
    }

    public virtual bool Delete(int id)
    {
        return Collection.Delete(id);
    }
}

public interface IBaseRepository<T>
{
    T Create(T data);
    IEnumerable<T> All();
    T FindById(int id);
    void Update(T entity);
    bool Delete(int id);
}

Create a vendor owned repository. Of course, more domains, more repositories to create.

public class VendorRepository : BaseRepository<Vendor>, IVendorRepository
{
    public VendorRepository(ILiteDatabase db)
        : base(db)
    { }

    public override Vendor Create(Vendor entity)
    {
        var now = DateTime.Now;
        entity.CreatedTime = now;
        entity.ModifiedTime = now;

        Collection.EnsureIndex(x => x.Id);

        return Collection.Find(o => o.Id == entity.Id)?.FirstOrDefault();
    }

    public Vendor GetByPermanentId(string permanentId)
    {
        return Collection.Find(o => o.PermanentId == permanentId)?.FirstOrDefault();
    }
	
    ... add more functions about vendor
}

public interface IVendorRepository : IBaseRepository<Vendor>
{
    Vendor GetByPermanentId(string permanentId);
    ... add more defination
}

Also, please don't forget to create a mock/fake vendor repository for unit testing.

public class FakeVendorRepository : BaseRepository<Vendor>, IVendorRepository
{
    public FakeVendorRepository(ILiteDatabase db)
        : base(db)
    { }

    public override Vendor Create(Vendor entity)
    {
        return new Vendor
        {
            VendorId = "vendor",
            Password = "123",
            IsEnabled = true
        };
    }

    public Vendor GetByPermanentId(string permanentId)
    {
        return new Vendor
        {
            VendorId = "vendor",
            Password = "123",
            IsEnabled = true
        };
    }
}

Business time: if you are interesting in Unit Tests, try the course, 【針對遺留代碼加入單元測試的藝術】202011 - 台北, build by a good friend. It will help you to enhance your global skill about Unit Tests.

More, 30天快速上手TDD

OO basic knowledge, 輕鬆學會物件導向(使用C#)2020年版第二梯

Try more courses, here.

Data Service

Build a data service containing those repositories.

public class DataService : IDisposable
{
    private readonly string _connStr;
    private ILiteDatabase _db;
    private bool _disposed;

    private IVendorRepository _vendorRepository;
    private IVendorRepository _fakeVendorRepository;
	
    ... add more repositories we need

    public DataService(string connStr)
    {
        if (string.IsNullOrEmpty(connStr))
            throw new ArgumentNullException("missing connection string");

        _connStr = connStr;
    }

    private ILiteDatabase DB
    {
        get {
            //if (_db is null)
            //{
            //    _db = new LiteDatabase(_connStr);
            //    return _db;
            //}
            //else
            //{
            //    return _db;
            //}
            return _db ??= new LiteDatabase(_connStr); // whole remarked block above, can be replaced by this one, it's a C# 8 convension
        }
    }

    public IVendorRepository VendorRepository
    {
        get { return _vendorRepository ??= new VendorRepository(DB); }
    }

    public IVendorRepository FakeVendorRepository
    {
        get { return _fakeVendorRepository ??= new FakeVendorRepository(DB); }
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    private void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                if (_db != null)
                    _db.Dispose();
            }
            _disposed = true;
        }
    }

    ~DataService()
    {
        Dispose(false);
    }
}

Using

Now, let's try to get vendors from data service, data service defined in every action

[HttpGet, Route("")]
public IActionResult Get()
{
    using var svc = new DataService("DatabaseConnection");
    var vendors = svc.VendorRepository.All();
    return Ok(vendors);
}

Or, data service can be defined in constructor,

private readonly string _databaseConnection;
private readonly DataService _dataService;

public VendorsController(IOptionsSnapshot<AppSettings> options)
{
    _databaseConnection = options?.Value?.DatabaseConnection;
    _dataService = new DataService(_databaseConnection);
}

[HttpGet, Route("")]
public IActionResult Get()
{
    var data = _dataService.VendorRepository.All();
    return Ok(data);
}

My strategy, because of the LiteDB limits, we should NOT keep database open to prevent from the database file being locked, when others call that api, it will throw exception.

For the reason, I would like to set my LiteDB connection string to "Filename={your_database_file_path};Password={your_password};Connection=shared;".

It's important to read the Connection String settings.

Try it.


References:

LiteDB

LiteDB wiki

NoSQL

Design the infrastructure persistence layer

Repository Pattern C#

Repository Pattern

【.NET】【Design Patterns】 無辜的 Repository Pattern

DapperUnitOfWork

【C#】Dapper UnitOfWork Repository 範例(使用Dapper.SimpleCRUD)

Matt