ASP .NET

Utilizando o Micro ORM Dapper em uma aplicação ASP.NET Core

Conheça o micro framework ORM Dapper.NET e aprenda a utilizá-lo em uma aplicação ASP.NET Core.

há 6 anos 8 meses

Formação Full-stack: Desenvolvedor ASP.NET
Conheça a formação em detalhes

Persistir informações da aplicação é um requisito básico de quase todos os sistemas desenvolvidos atualmente. E o meio mais utilizado para isso são os bancos de dados relacionais.

Porém, o processo de persistência de dados em um banco de dados relacional não é simples, são necessárias uma gama de configurações, além de certas adaptações para que os objetos sejam persistidos corretamente no banco.

Para facilitar este processo, foram criados os frameworks ORM (Object-Relational Mapping – mapeamento objeto relacional). Eles se encarregam do trabalho pesado, como o processo de conversão dos objetos da aplicação em informações compreendidas pelo banco de dados (códigos SQL), e abstraem o máximo do acesso ao banco para facilitar a persistência de dados. Com isso, o desenvolvedor pode focar no que é mais importante para a aplicação.

Só que, com a facilidade, esses frameworks ORM acabaram comprometendo um pouco da performance das aplicações. Para resolver este problema, criou-se os chamados micro frameworks ORM. Este tipo abstrai apenas o necessário e otimiza ao máximo. Um dos frameworks que se destacam nesta categoria é o Dapper.NET.

Criado pela equipe por atrás do Stack Overflow, este micro framework ORM foi criado visando rapidez, fácil integração e liberdade de configuração. Com poucas linhas de código é possível adicioná-lo em uma aplicação e já fazer uso de todo o seu poder.

Para se ter ideia da sua rapidez e das características que o caracterizam, podemos ver a comparação que a sua equipe realiza com outros frameworks ORM disponíveis para .NET:

C# (C Sharp) - TDD
Curso C# (C Sharp) - TDD
Conhecer o curso

Performance de uma pesquisa com 500 iterações - POCO serialization

Method Duration Remarks
Hand coded (using a SqlDataReader) 47ms
Dapper ExecuteMapperQuery 49ms
ServiceStack.OrmLite (QueryById) 50ms
PetaPoco 52ms Can be faster
BLToolkit 80ms
SubSonic CodingHorror 107ms
NHibernate SQL 104ms
Linq 2 SQL ExecuteQuery 181ms
Entity framework ExecuteStoreQuery 631ms

Performance de uma pesquisa com 500 iterações - dynamic serialization

Method Duration Remarks
Dapper ExecuteMapperQuery (dynamic) 48ms
Massive 52ms
Simple.Data 95ms

Performance de uma pesquisa com 500 iterações - typical usage

Method Duration Remarks
Linq 2 SQL CompiledQuery 81ms Not super typical involves complex code
NHibernate HQL 118ms
Linq 2 SQL 559ms
Entity framework 859ms
SubSonic ActiveRecord.SingleOrDefault 3619ms

Para exemplificar o seu uso, vamos ver um exemplo dele em uma aplicação ASP.NET Core.

Criando a aplicação

No terminal digite o código abaixo para criar uma aplicação chamada AspNetCoreDapper:

dotnet new mvc -n AspNetCoreDapper

Agora, adicione o pacote do Dappper:

dotnet add package Dapper

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

dotnet restore

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

Criando o model / entidade

Para este exemplo será utilizada a entidade abaixo:

using System;
using System.ComponentModel.DataAnnotations;

namespace AspNetCoreDapper.Models
{
    public class Product
    {
        public int Id { get; set; }

        [Required]
        public string Name { get; set; }
        public int Quantity { get; set; }

        [Required]
        public double Price { get; set; }
    }
}

Configurando o acesso ao banco com Dapper

Para este exemplo vou utilizar como banco de dados o SQLite, assim, antes de configurar o Dapper é necessário adicionar o pacote deste banco de dados na aplicação:

dotnet add package Microsoft.Data.Sqlite

Aplique o restore para iniciarmos o processo de configuração do banco.

Diferente de outros frameworks ORM, o Dapper não define uma classe de configuração. Assim, configuraremos o acesso dele na classe base dos repositórios:

namespace AspNetCoreDapper.Repositories
{
    public abstract class AbstractRepository<T>
    {
        private string _connectionString;
        protected string ConnectionString => _connectionString;
        public AbstractRepository(IConfiguration configuration){
            _connectionString = configuration.GetValue<string>("DBInfo:ConnectionString");

        }
        public abstract void Add(T item);
        public abstract void Remove(int id);
        public abstract void Update(T item);
        public abstract T FindByID(int id);
        public abstract IEnumerable<T> FindAll();
    }
} 

O Dapper também não cria o banco de dados de acordo com as entidades. Nele, caso o banco ainda não esteja criado, ele deve ser criado “na mão”. Que é o que iremos fazer na classe abaixo:

namespace AspNetCoreDapper.Db
{
    public class Seed
    {
        private static IDbConnection _dbConnection;

        private static void CreateDb(IConfiguration configuration)
        {
            var connectionString = configuration.GetValue<string>("DBInfo:ConnectionString");
            var dbFilePath = configuration.GetValue<string>("DBInfo:ConnectionString");
            if (!File.Exists(dbFilePath))
            {
                _dbConnection = new SqliteConnection(connectionString);
                _dbConnection.Open();

                // Create a Product table
                _dbConnection.Execute(@"
                    CREATE TABLE IF NOT EXISTS [Product] (
                        [Id] INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
                        [Name] NVARCHAR(128) NOT NULL,
                        [Quantity] INTEGER NULL,
                        [Price] NUMERIC NOT NULL
                    )");

                _dbConnection.Close();
            }
            
        }
    }
}

E ele deve ser chamado na classe AbstractRepository:

public AbstractRepository(IConfiguration configuration){
    _connectionString = configuration.GetValue<string>("DBInfo:ConnectionString");

    AspNetCoreDapper.Db.Seed.CreateDb(configuration);
}

Para finalizar a configuração de acesso Dapper, iremos definir o repositório abaixo:

namespace AspNetCoreDapper.Repositories
{
    public class ProductRepository: AbstractRepository<Product>
    {
        public ProductRepository(IConfiguration configuration): base(configuration) { }

        public override void Add(Product item)
        {
            using (IDbConnection dbConnection = new SqliteConnection(ConnectionString))
            {
                string sQuery = "INSERT INTO Product (Name, Quantity, Price)"
                                + " VALUES(@Name, @Quantity, @Price)";
                dbConnection.Open();
                dbConnection.Execute(sQuery, item);
            }
        }
        public override void Remove(int id)
        {
            using (IDbConnection dbConnection = new SqliteConnection(ConnectionString))
            {
                string sQuery = "DELETE FROM Product" 
                            + " WHERE Id = @Id";
                dbConnection.Open();
                dbConnection.Execute(sQuery, new { Id = id });
            }
        }
        public override void Update(Product item)
        {
            using (IDbConnection dbConnection = new SqliteConnection(ConnectionString))
            {
                string sQuery = "UPDATE Product SET Name = @Name,"
                            + " Quantity = @Quantity, Price= @Price" 
                            + " WHERE Id = @Id";
                dbConnection.Open();
                dbConnection.Query(sQuery, item);
            }
        }
        public override Product FindByID(int id)
        { 
            using (IDbConnection dbConnection = new SqliteConnection(ConnectionString))
            {
                string sQuery = "SELECT * FROM Product" 
                            + " WHERE Id = @Id";
                dbConnection.Open();
                return dbConnection.Query<Product>(sQuery, new { Id = id }).FirstOrDefault();
            }
        }
        public override IEnumerable<Product> FindAll()
        { 
            using (IDbConnection dbConnection = new SqliteConnection(ConnectionString))
            {
                dbConnection.Open();
                return dbConnection.Query<Product>("SELECT * FROM Product");
            }
        }
    }
}

Note que o Dapper realiza o processo de impedância, a transformação dos dados do banco para entidades da aplicação de forma quase transparente:

dbConnection.Query<Product>("SELECT * FROM Product");

Na busca basta informar a classe que mapeia os dados do banco que o framework se encarregará de transformar os resultados obtidos em objetos desta classe. Aqui é importante que os nomes das colunas da tabela sejam iguais aos nomes das propriedades da classe, pois o Dapper irá realizar a transformação por Reflection.

Este mesmo processo é aplicado quando um dado é enviado para o banco:

using (IDbConnection dbConnection = new SqliteConnection(ConnectionString))
{
    string sQuery = "INSERT INTO Product (Name, Quantity, Price)"
                    + " VALUES(@Name, @Quantity, @Price)";
    dbConnection.Open();
    dbConnection.Execute(sQuery, item);
}

Também repare que é necessário informar o código SQL, pois este é um recurso dos frameworks ORM tradicionais que o Dapper não implementa. Assim, é possível definir um código mais eficiente, obtendo ainda mais performance.

Testando o Dapper

Para testar o Dapper, crie um controller:

namespace AspNetCoreDapper.Repositories
{
    public class ProductController : Controller
    {
        private readonly ProductRepository productRepository;

        public ProductController(IConfiguration configuration){
            productRepository = new ProductRepository(configuration);
        }

        // 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
        // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
        // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
        [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");
        }
    }
}

Também crie as views as actions acima e então podemos ver o sistema funcionando:

Você pode fazer download dos sources dessa aplicação clicando aqui.

Até a próxima!

C# (C Sharp) Avançado
Curso C# (C Sharp) Avançado
Conhecer o curso

Autor(a) do artigo

Wladimilson M. Nascimento
Wladimilson M. Nascimento

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

Todos os artigos

Artigos relacionados Ver todos