Utilizando o hybrid ORM RepoDb em uma aplicação ASP.NET Core

No ambiente .NET, quando uma aplicação necessita persistir dados, geralmente fará uso de algum framework ORM. Se for algo simples tende a optar por um micro-ORM, como o Dapper e se for complexo, a opção é um “full-ORM”, como o Entity Framework. Mas quando a aplicação estiver no meio termo? É neste ponto que entra o hybrid ORM RepoDb.

RepoDb

O RepoDb é um framework ORM open-source que tem por objetivo sanar a brecha que há entre um micro-ORM e um macro-ORM (full-ORM). Fornecendo recursos que permitem o desenvolvedor alterar de forma simples entre operações básicas e avançadas.

Além de fornecer as operações CRUD padrão (criação, leitura, alteração e exclusão), também disponibiliza recursos avançados como: 2nd-Layer Cache, Tracing, Repositories e operações em lote (Batch/Bulk). Permitindo que seja utilizado tanto em um banco de dados simples quanto nos mais complexos.

Criado por Michael Pendon, o RepoDb se define como a melhor alternativa de ORM para o Dapper e o Entity Framework e procura ter a mesma adoção de ambos. Para ajudá-lo nisso, vamos conhecer este framework através de um exemplo.

C# (C Sharp) - Introdução ao ASP.NET Core
Curso de C# (C Sharp) - Introdução ao ASP.NET Core
CONHEÇA O CURSO

Criando a aplicação

Como estou utilizando o Visual Studio Code, criarei a aplicação por linha de comando:

dotnet new mvc -n AspNetCoreRepodb

No momento da criação deste artigo, o RepoDb suporta os seguintes banco de dados:

  • SqlServer: RepoDb.SqlServer;
  • SqLite: RepoDb.SqLite;
  • MySql: RepoDb.MySql;
  • PostgreSql: RepoDb.PostgreSql.

Para o exemplo deste artigo irei utilizar o SQLite, desta forma é necessário adicionar a dependência abaixo:

dotnet add package RepoDb.SqLite

Não se esqueça de aplicar o restore no projeto:

dotnet restore

Com isso já podemos começar a nossa configuração do RepoDb.

Criando o model e tabela

Para este exemplo será utilizada a entidade abaixo:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Quantity { get; set; }
    public double Price { get; set; }
}

Como o RepoDb não realiza este procedimento, também é necessário criar a tabela desta entidade:

CREATE TABLE IF NOT EXISTS [Product]
(
    Id INTEGER PRIMARY KEY AUTOINCREMENT,
    Name TEXT,
    Quantity INTEGER,
    Price REAL
);

Agora podemos configurar o acesso ao banco.

Configurando o acesso ao banco de dados

Assim como o Dapper e diferente do Entity Framework, o RepoDb não fornece uma classe de configuração, o banco deve ser acessado via SqlConnection. Desta forma, para organizar o nosso código, irei implementar o padrão repository:

public interface IRepository<T>
{
    string ConnectionString { get; }
    void Add(T item);
    void Remove(int id);
    void Update(T item);
    T FindByID(int id);
    IEnumerable<T> FindAll();
}

Esta interface será implementada pela classe ProductRepository:

public class ProductRepository : IRepository<Product>
{
    private string _connectionString;
    public string ConnectionString => _connectionString;

    public ProductRepository(IConfiguration configuration)
    {
        _connectionString = configuration.GetValue<string>("DBInfo:ConnectionString");
        RepoDb.SqLiteBootstrap.Initialize();
    } 

    public void Add(Product item)
    {
        using (var dbConnection = new SQLiteConnection(ConnectionString))
        {
            var id = dbConnection.Insert<Product, int>(item);
        }
    }

    public IEnumerable<Product> FindAll()
    {
        using (var dbConnection = new SQLiteConnection(ConnectionString))
        {
            return dbConnection.ExecuteQuery<Product>("SELECT * FROM Product");
        }
    }

    public Product FindByID(int id)
    {
        using (var dbConnection = new SQLiteConnection(ConnectionString))
        {
            return dbConnection.Query<Product>(e => e.Id == id).FirstOrDefault();
        }
    }

    public void Remove(int id)
    {
        using (var dbConnection = new SQLiteConnection(ConnectionString))
        {
            dbConnection.Delete<Product>(id);
            //Também poderia ser
            // dbConnection.Delete<Product>(e => e.Id = id);
        }
    }

    public void Update(Product item)
    {
        using (var dbConnection = new SQLiteConnection(ConnectionString))
        {
            dbConnection.Merge(item);
        }
    }
}

Note que no construtor da classe é chamado o bootstrapper do RepoDb para o SQLite:

public ProductRepository(IConfiguration configuration)
{
    _connectionString = configuration.GetValue<string>("DBInfo:ConnectionString");
    RepoDb.SqLiteBootstrap.Initialize();
} 

Isso é necessário para configurar o DataMapping da biblioteca para este banco de dados.

Caso trabalhe apenas com SQL RAW, como no exemplo do método FindAll:

public IEnumerable<Product> FindAll()
{
    using (var dbConnection = new SQLiteConnection(ConnectionString))
    {
        return dbConnection.ExecuteQuery<Product>("SELECT * FROM Product");
    }
}

Não é necessário utilizar o bootstrapper, pois esta é a forma mais simples de fazer uso da biblioteca, o que o torna muito semelhante ao Dapper. Mas o seu principal poder é visto quando definimos as ações via Fluent:

public void Add(Product item)
{
    using (var dbConnection = new SQLiteConnection(ConnectionString))
    {
        var id = dbConnection.Insert<Product, int>(item);
    }
}

E para o Fluent funcionar, é necessário que o RepoDb seja “inicializado” para o banco de dados em questão.

Além das ações implementadas nesta classe, também é possível realizar uma ação em lote, como a inserção:

using (var dbConnection = new SQLiteConnection(ConnectionString))
{
    dbConnection.InsertAll<Product>(products, batchSize: 100);
}

Note que é informado no parâmetro batchSize a quantidade de registros serão salvos. Após serem salvos, os id gerados serão atribuídos ao itens da lista.

Com o repositório criado, podemos definir o controller e as views para testar a nossa conexão.

Testando o RepoDb

Para testar, criaremos o controller abaixo:

public class ProductController : Controller
{
    private readonly IRepository<Product> productRepository;

    public ProductController(IRepository<Product> repository) 
        => productRepository = repository;

    // GET: Products
    public ActionResult Index()
    {
        return View(productRepository.FindAll().ToList());
    }

    // GET: Products/Details/5
    public ActionResult Details(int? id)
    {
        if (id == null)
        {
            return StatusCode(StatusCodes.Status404NotFound);
        }
        Product product = productRepository.FindByID(id.Value);
        if (product == null)
        {
            return StatusCode(StatusCodes.Status404NotFound);
        }
        return View(product);
    }

    // GET: Products/Create
    public ActionResult Create()
    {
        return View();
    }

    // POST: Products/Create
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create([Bind("Id,Name,Quantity,Price")] Product product)
    {
        if (ModelState.IsValid)
        {
            productRepository.Add(product);
            return RedirectToAction("Index");
        }

        return View(product);
    }

    // GET: Products/Edit/5
    public ActionResult Edit(int? id)
    {
        if (id == null)
        {
            return StatusCode(StatusCodes.Status400BadRequest);
        }
        Product product = productRepository.FindByID(id.Value);
        if (product == null)
        {
            return StatusCode(StatusCodes.Status404NotFound);
        }
        return View(product);
    }

    // POST: Products/Edit/5
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Edit([Bind("Id,Name,Quantity,Price")] Product product)
    {
        if (ModelState.IsValid)
        {
            productRepository.Update(product);
            return RedirectToAction("Index");
        }
        return View(product);
    }

    // GET: Products/Delete/5
    public ActionResult Delete(int? id)
    {
        if (id == null)
        {
            return StatusCode(StatusCodes.Status400BadRequest);
        }
        Product product = productRepository.FindByID(id.Value);
        if (product == null)
        {
            return StatusCode(StatusCodes.Status404NotFound);
        }
        return View(product);
    }

    // POST: Products/Delete/5
    [HttpPost, ActionName("Delete")]
    [ValidateAntiForgeryToken]
    public ActionResult DeleteConfirmed(int id)
    {
        productRepository.Remove(id);
        return RedirectToAction("Index");
    }
}

Note que o repositório é recebido por parâmetro no construtor:

public ProductController(IRepository<Product> repository) 
    => productRepository = repository;

Por causa disso, vamos adicioná-lo via injeção de dependência:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    services.AddTransient<IRepository<Product>, ProductRepository>();
}

Ao definir as views, poderemos ver o sistema funcionando:

Tabela exibido dois produtos: Teclado e Mouse; com os preços 123,40 e 49,9; e as quantidades 10 e 20

Implementando o padrão Repository com o RepoDb

No nosso exemplo acima, o padrão repository foi implementado “manualmente”. Entretanto, o RepoDb já fornece uma implementação deste padrão. Para utilizá-lo basta herdar a classe BaseRepository, onde deve ser informado a entidade e o tipo de conexão.

Para que a nossa aplicação não necessite de muitas alterações, além desta classe, basta manter a interface IRepository que definimos:

public class ProductRepository : BaseRepository<Product, SQLiteConnection>, IRepository<Product>
{
    public ProductRepository(IConfiguration configuration) : base(configuration.GetValue<string>("DBInfo:ConnectionString"))
    {
        RepoDb.SqLiteBootstrap.Initialize();
    } 

    public void Add(Product item)
    {
        Insert<int>(item);
    }

    public IEnumerable<Product> FindAll()
    {
        return QueryAll();
    }

    public Product FindByID(int id)
    {
        return Query(id).FirstOrDefault();
    }

    public void Remove(int id)
    {
        Delete(id);
    }

    public void Update(Product item)
    {
        Update(item);
    }
}

Note que o código da nossa classe ficou mais “limpo”. Caso execute a aplicação novamente, verá que ela continuará funcionando da mesma forma que antes. Desta forma, quando utilizar esta biblioteca, recomendo fazer uso desta classe BaseRepository. Ela não chega no nível da DbContext do Entity Framework, mas assim como ela, facilita o acesso ao banco.

C# (C Sharp) Básico
Curso de C# (C Sharp) Básico
CONHEÇA O CURSO

O RepoDb ainda está crescendo, mas é uma clara boa alternativa quando não necessitar de algo robusto como o Entity e não queira algo muito simples como o Dapper. Portanto, recomendo que nestas situações dê uma chance para este framework, você notará as suas vantagens.

Ah, o código desta aplicação pode ser visto no meu Github.

Até a próxima.

Deixe seu comentário

Instrutor, nerd, cinéfilo e desenvolvedor nas horas vagas. Graduado em Ciências da Computação pela Universidade Metodista de São Paulo.

© 2004 - 2019 TreinaWeb Tecnologia LTDA - CNPJ: 06.156.637/0001-58 Av. Paulista, 1765, Conj 71 e 72 - Bela Vista - São Paulo - SP - 01311-200