Genie - An Efficient Way to Access Data

Almost all the time we need a database to run our apps. most of the time, these databases are relational databases. as everyone knows we must use SQL to access these databases. writing an SQL query for every database call is a pain and no one does that these days. instead, we use something called ORMs (Object Relational Mappers). we can use an ORM to access databases without writing SQL or worrying about mapping relations to objects. there are a lot of ORM implementations for every popular language.

Genie is a simple code generator which generates an ORM with all essential functionalities for you. You can use Genie to create a dot Net class library and use it as a layer between your data source and application. usually, this type of implementation is called a Data Access Layer as it provides an API to access a database. You can write your database logic in this layer. and the code generated by Genie will help you to do that.

Installing Genie

Genie is a free and open source software. you can download the latest release for Linux/ Windows or Mac from here

After downloading, extract the zip to some folder and add the path to the folder to the PATH global variable. for Linux users make the GenieCLI file executable by running the command chmod +x GenieCLI

Setting Up

To use Genie on a database you should create a configuration file. this configuration file specifies all the configuration that Genie needs to generate the code. For this demonstration, I will use a MySql database called bestappdata

{
  "connectionString":
    "Server=localhost;Database=bestappdata;UID=root;Password=password",
  "projectPath": "/home/rusith/Documents/Projects/BestAppEver/BackEnd/BestAppEver.DAL",
  "baseNamespace": "BestAppEver.DAL",
  "ProjectFile": "BestAppEver.DAL.csproj",
  "dbms": "mysql",
  "schema": "animus",
  "abstractModelsLocation": "./../BestAppEver/Models/Data",
  "abstractModelsNamespace": "BestAppEver.Models.Data"
}

This configuration file should be named as genieSettings.json and can put anywhere in your project. but its good to put this file inside the target library’s location. Genie uses this to fetch database metadata and generate the code. the projectPath is the full path to the project folder

Below is my folder structure for this demonstration. I have put my genieSettings.json file inside the BestAppEver.DAL folder.

├── BestAppEver
│   ├── Modules
├── BestAppEver.API
│   ├── Controllers
├── BestAppEver.BL
│   ├── Services
├── BestAppEver.DAL
│   ├── Modules
│   └── Providers
└── BestAppEver.UnitTests
|   ├── Providers
├── bin
│   └── Debug
│       └── netstandard2.0
├── Dapper
├── Infrastructure
│   ├── Actions
│   │   ├── Abstract
│   │   └── Concrete
│   ├── Collections
│   │   ├── Abstract
│   │   └── Concrete
│   ├── Enum
│   ├── Filters
│   │   ├── Abstract
│   │   └── Concrete
│   ├── Interfaces
│   ├── Models
│   │   ├── Abstract
│   │   │   └── Context
│   │   └── Concrete
│   │       └── Context
│   └── Repositories
├── Modules
├── obj
│   └── Debug
│       └── netstandard2.0
└── Providers

Generating the Code

go to the directory where the genieSettings.json file exists, run GenieCLI executable by executing GenieCLI in the terminal. we can do this because we have put Genie executable’s folder path to the path variable. otherwise you have to specify full path to the GenieCLI executable which is in the zip file you have downloaded.

After generating , the BestAppEver.DAL folder will have two new root folders. Will look like below

├── Dapper
├── Infrastructure
│   ├── Actions
│   │   ├── Abstract
│   │   └── Concrete
│   ├── Collections
│   │   ├── Abstract
│   │   └── Concrete
│   ├── Enum
│   ├── Filters
│   │   ├── Abstract
│   │   └── Concrete
│   ├── Interfaces
│   ├── Models
│   │   ├── Abstract
│   │   │   └── Context
│   │   └── Concrete
│   │       └── Context
│   └── Repositories
├── Modules
└── Providers

Writing Code

In order to continue, you need one thing. you have to implement the IConnectionStringProvider in order to user the DBContext. create a class for IConnectionStringProvider and pass an instance to the DBContext when ever needed.

using BestAppEver.DAL.Infrastructure.Interfaces;

namespace BestAppEver.DAL
{
  public class ConnectionProvider : IConnectionStringProvider
  {
    public string GetConnectionString()
    {
      return "Server=localhost;Database=bestappdata;UID=root;Password=password"; // Should be taken from a configuration file 
    }
  }
}

You should initialize DBContext only once .you should keep this instance in a global scope. for this demonstration i use .Net core DI to setup my dependencies.

using BestAppEver.Injection;
using BestAppEver.DAL.Infrastructure.Interfaces;
using BestAppEver.DAL.Infrastructure;

namespace BestAppEver.DAL.Modules
{
  public class DataAccessModule : IModule
  {
    public void Bootstrap(IDependencyStore store)
    {
      store.Singleton<IConnectionStringProvider, ConnectionProvider>();
      store.Singleton<IDBContext, DBContext>();
    }
  }
}

I use providers to wrap my database code.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using BestAppEver.DAL.Infrastructure.Interfaces;
using BestAppEver.DAL.Infrastructure.Models.Concrete;
using BestAppEver.Models.Data;
using BestAppEver.Providers.Data;

namespace BestAppEver.DAL.Providers
{
  public class UserProvider : IUserProvider
  {
    private readonly IDBContext _context;
    public UserProvider(IDBContext context)
    {
      _context = context;
    }

    public async Task Add(IUser user)
    {
      var unit = _context.Unit();
      unit.UserRepository.Add(user);
      await unit.CommitAsync();
    }

    public async Task<IUser> Get(int id)
    {
      var unit = _context.Unit();
      return await unit.UserRepository.GetByKeyAsync(id);
    }

    public async IEnumerable<IUser> GetAll(int skip, int take, string serchText)
    {
      var getter = _context.Unit().UserRepository.Get();
      if (!string.IsNullOrWhiteSpace(serchText))
      {
        getter.Where.FirstName.Contains(serchText)
          .Or.LastName.Contains(serchText);
      }

      getter.Skip(skip).Take(take);

      return await getter.QueryAsync();
    }

    public async Task<IUser> GetAsync(int id)
    {
      return await _context.Unit().UserRepository.GetByKeyAsync(id);
    }

    public async Task<IUser> getByEmail(string email)
    {
      return await _context.Unit().UserRepository.Get()
        .Where.Email.EqualsTo(email).Filter().FirstOrDefaultAsync();
    }

    public async Task Update(IUser user)
    {
      await (user  as User).DatabaseUnitOfWork.CommitAsync();
    }
  }
}

This could have problems with some databases as this is in development and has only tested in Windows 10, Mac OSX Sierra, Ubuntu 16.04. You can contribute to the code through GitHub



Copyright 2019 Shanaka Rusith