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.
Curso C# (C Sharp) - Introdução ao ASP.NET Core
Conhecer o cursoCriando 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:
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.
Curso C# (C Sharp) Básico
Conhecer o cursoO 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.