Dapper

Dapper y Entity Framework Core en .NET 6

En este artículo, mostraré como usar Dapper y Entity Framework Core juntos en la misma aplicación.

Introducción a Dapper

¿Qué es Dapper?

Es un marco de mapeo de objetos simple o un Micro-ORM que nos ayuda a mapear los datos del resultado de una consulta SQL a una clase .NET de manera eficiente. Extiende a IDbConnection y simplifica la configuración, la ejecución y proporciona métodos de extensión útiles para consultar nuestra base de datos.

Al usar Dapper podemos escribir sentencias SQL como si lo hiciéramos en SQL Server. Es más, Dapper tiene un gran rendimiento porque no traduce las consultas que escribimos en .NET a SQL.

Además, admite múltiples proveedores de bases de datos.

Dapper vs Entity Framework Core

EF Core tiene muchas características que debe tener un ORM, mientras que Dapper no tiene muchas de estas características. Es por esto que está mal compararlos. Aún así…

Dapper es un Micro ORM sencillo que tiene características mínimas. Dapper es muy rápido, esto no significa que Entity Framework Core sea más lento. Con cada actualización de EF Core, el rendimiento también parece mejorar. Dapper es el paraíso para aquellos a quienes todavía les gusta trabajar con consultas en lugar de LINQ con EF Core.

Es por ello que Dapper es increíble para manejar consultas complejas que tienen uniones múltiples y una lógica realmente grande.

Y EF Core es excelente para la generación de clases, el seguimiento de objetos, el mapeo de varias clases anidadas y mucho más. Por lo general, se trata de rendimiento y características cuando se habla de estos 2 ORMs.

Por lo tanto, es posible usar los dos ORMs en el mismo proyecto, aprovechando lo mejor de cada uno. Si tenemos consultas complejas y creemos que el uso de Dapper mejorará el rendimiento, esta es una opción.

Veamos el código👇

En el código fuente de este artículo se puede encontrar cómo Dapper se puede integrar fácilmente con Entity Framework Core, veremos cómo trabajar con estos dos marcos juntos en la misma aplicación. He creado una pequeña Web API en ASP.NET Core, he añadido repositorios y ejemplos de uso. También una transacción combinando los dos ORMs.

  • Instalaciones necesarias para usar «Dapper»:
Install-Package Dapper
Install-Package Microsoft.Data.SqlClient
  • Instalaciones necesarias para usar «Entity Framework Core» en nuestro proyecto:
Install-Package Microsoft.EntityFrameworkCore
Install-Package Microsoft.EntityFrameworkCore.Relational
Install-Package Microsoft.EntityFrameworkCore.SqlServer
Install-Package Microsoft.EntityFrameworkCore.Tools
  • Usaremos base de datos SQL Server Express LocalDB, para ello lanzamos el siguiente comando que nos creará la base de datos y añadirá datos de pruebas al ejecutar el proyecto API:
Add-migration Initial
Update-database

Configuración de Entity Framework Core

  • En primer lugar, el acceso a datos se hace mediante un modelo que se compone de clases de entidad y un objeto de contexto que representa una sesión con la base de datos. Este objeto de contexto permite consultar y guardar datos DbContext:
public class ApplicationContext : DbContext, IApplicationContext
{
    public DbSet<User> Users => Set<User>();
    public DbSet<Post> Posts => Set<Post>();
    public DbSet<Comment> Comments => Set<Comment>();
    public DbSet<PostDetail> PostDetails => Set<PostDetail>();

    // Representa una conexión abierta a un origen de datos y
    // lo implementan proveedores de datos .NET que acceden a bases de datos relacionales.
    public IDbConnection Connection => Database.GetDbConnection();

    public ApplicationContext(DbContextOptions<ApplicationContext> options) : base(options) { }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());

        base.OnModelCreating(builder);
    }

    public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken())
    {
        return await base.SaveChangesAsync(cancellationToken);
    }
}
  • En segundo lugar, interfaz IReadRepositoryBase que implementa repositorio de solo lectura en EF Core:
public interface IReadRepositoryBase<TEntity> where TEntity : class
{
    IQueryable<TEntity> GetAll(bool asNoTracking = true);

    IQueryable<TEntity> GetAllBySpec(Expression<Func<TEntity, bool>> predicate, bool asNoTracking = true);

    Task<TEntity?> GetByIdAsync<TId>(TId id, CancellationToken cancellationToken = default) where TId : notnull;

    Task<TEntity?> GetBySpecAsync<Spec>(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);

    Task<ICollection<TEntity>> ListAsync(CancellationToken cancellationToken = default);

    Task<ICollection<TEntity>> ListAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);

    Task<int> CountAsync(CancellationToken cancellationToken = default);

    Task<int> CountAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);

    Task<bool> AnyAsync(CancellationToken cancellationToken = default);

    Task<bool> AnyAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);

    IQueryable<TEntity> GetAllIncluding(params Expression<Func<TEntity, object>>[] includeProperties);
}
  • En tercer lugar, interfaz IRepositoryBase que implementa repositorio de lectura y escritura en EF Core:
public interface IRepositoryBase<TEntity> : IReadRepositoryBase<TEntity> where TEntity : class
{
    IUnitOfWork UnitOfWork { get; }

    TEntity Add(TEntity entity);

    Task<TEntity> AddAsync(TEntity entity, CancellationToken cancellationToken = default);

    ICollection<TEntity> AddRange(ICollection<TEntity> entities);

    Task<int> AddRangeAsync(ICollection<TEntity> entities, CancellationToken cancellationToken = default);

    void Delete(TEntity entity);

    Task<int> DeleteAsync(TEntity entity, CancellationToken cancellationToken = default);

    void DeleteRange(ICollection<TEntity> entities);

    Task<int> DeleteRangeAsync(ICollection<TEntity> entities, CancellationToken cancellationToken = default);

    void Update(TEntity entity);

    Task<int> UpdateAsync(TEntity entity, CancellationToken cancellationToken = default);
}

Configuración de Dapper

Para trabajar con Dapper, los únicos requisitos son un «DbConnection», el texto SQL y algunos parámetros opcionales, como un «DbTransaction», tiempo de espera del comando, parámetros de consulta, etc. 

  • En primer lugar, clase de lectura, aquí no hay ningún vínculo con ningún objeto DBContext (Entity Framework Core) porque realmente no tiene sentido compartir las conexiones entre Entity Framework Core y Dapper cuando lee los datos. En la implementación de lectura, estamos trabajando directamente con el objeto IDbConnection, como SqlConnection, con una cadena de conexión conocida. Para poder ejecutar las consultas, lo inicializamos en el constructor usando la cadena de conexión:
public class ApplicationReadDbConnection : IApplicationReadDbConnection, IDisposable
{
    private readonly IDbConnection connection;

    public ApplicationReadDbConnection(IConfiguration configuration)
    {
        connection = new SqlConnection(configuration.GetConnectionString("DefaultConnection"));
    }

    public async Task<IReadOnlyList<T>> QueryAsync<T>(string sql, object? param = null, IDbTransaction? transaction = null, CancellationToken cancellationToken = default)
    {
        return (await connection.QueryAsync<T>(sql, param, transaction)).AsList();
    }

    public async Task<IEnumerable<TResult>> QueryMapAsync<T1, T2, TResult>(string sql, Func<T1, T2, TResult> map, object? param = null, IDbTransaction? transaction = null, string splitOn = "Id", CancellationToken cancellationToken = default)
    {
        return await connection.QueryAsync(sql, map, param, transaction, true, splitOn);
    }

    public async Task<IEnumerable<TResult>> QueryMapAsync<T1, T2, T3, TResult>(string sql, Func<T1, T2, T3, TResult> map, object? param = null, IDbTransaction? transaction = null, string splitOn = "Id", CancellationToken cancellationToken = default)
    {
        return await connection.QueryAsync(sql, map, param, transaction, true, splitOn);
    }

    public async Task<T> QueryFirstOrDefaultAsync<T>(string sql, object? param = null, IDbTransaction? transaction = null, CancellationToken cancellationToken = default)
    {
        return await connection.QueryFirstOrDefaultAsync<T>(sql, param, transaction);
    }

    public async Task<T> QuerySingleAsync<T>(string sql, object? param = null, IDbTransaction? transaction = null, CancellationToken cancellationToken = default)
    {
        return await connection.QuerySingleAsync<T>(sql, param, transaction);
    }

    public void Dispose()
    {
        connection.Dispose();
    }
}
  • En segundo lugar, clase de lectura y escritura, el caso de uso de compartir la conexión entra en escena cuando hay escritura de datos involucrada. En la implementación de escritura estamos reutilizando el objeto de contexto para ejecutar consultas y comandos con la ayuda de Dapper. Podemos ver que estamos inyectando el IApplicationDbContext que pertenece a Entity Framework en el Constructor. Así es cómo podemos compartir la conexión y la transacción. Usando la conexión del contexto, realizamos las operaciones de lectura y escritura usando Dapper. Veamos cómo se implementa:
public class ApplicationWriteDbConnection : IApplicationWriteDbConnection
{
    private readonly IApplicationContext context;

    public ApplicationWriteDbConnection(IApplicationContext context)
    {
        this.context = context;
    }

    public async Task<int> ExecuteAsync(string sql, object? param = null, IDbTransaction? transaction = null, CancellationToken cancellationToken = default)
    {
        return await context.Connection.ExecuteAsync(sql, param, transaction);
    }

    public async Task<IReadOnlyList<T>> QueryAsync<T>(string sql, object? param = null, IDbTransaction? transaction = null, CancellationToken cancellationToken = default)
    {
        return (await context.Connection.QueryAsync<T>(sql, param, transaction)).AsList();
    }

    public async Task<IEnumerable<TResult>> QueryMapAsync<T1, T2, TResult>(string sql, Func<T1, T2, TResult> map, object? param = null, IDbTransaction? transaction = null, string splitOn = "Id", CancellationToken cancellationToken = default)
    {
        return await context.Connection.QueryAsync(sql, map, param, transaction, true, splitOn);
    }

    public async Task<IEnumerable<TResult>> QueryMapAsync<T1, T2, T3, TResult>(string sql, Func<T1, T2, T3, TResult> map, object? param = null, IDbTransaction? transaction = null, string splitOn = "Id", CancellationToken cancellationToken = default)
    {
        return await context.Connection.QueryAsync(sql, map, param, transaction, true, splitOn);
    }

    public async Task<T> QueryFirstOrDefaultAsync<T>(string sql, object? param = null, IDbTransaction? transaction = null, CancellationToken cancellationToken = default)
    {
        return await context.Connection.QueryFirstOrDefaultAsync<T>(sql, param, transaction);
    }

    public async Task<T> QuerySingleAsync<T>(string sql, object? param = null, IDbTransaction? transaction = null, CancellationToken cancellationToken = default)
    {
        return await context.Connection.QuerySingleAsync<T>(sql, param, transaction);
    }
}

Veamos ahora un repositorio donde usamos Dapper y EF Core

En este repositorio podemos hacer uso tanto de Dapper como EF Core, según necesidades. Muestro alguno ejemplos de usos, como consultas con relaciones uno a uno, una a muchos… También una misma transacción que implementa Dapper y EF Core. Si algo falla se revierten todos los cambios, tantos los realizados con Dapper como con EF Core.

public class PostRepository : BaseRepository<Post>, IPostRepository
{
    private readonly ApplicationContext _dbContext;
    private readonly IApplicationReadDbConnection _readDbConnection;
    private readonly IApplicationWriteDbConnection _writeDbConnection;

    public PostRepository(ApplicationContext dbContext, IApplicationReadDbConnection readDbConnection, IApplicationWriteDbConnection writeDbConnection) :
        base(dbContext)
    {
        _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
        _readDbConnection = readDbConnection ?? throw new ArgumentNullException(nameof(readDbConnection));
        _writeDbConnection = writeDbConnection ?? throw new ArgumentNullException(nameof(writeDbConnection));
    }

    // One-to-one
    public async Task<Post?> GetRelationOneToOneAsync(int id)
    {
        var result = await _readDbConnection.QueryMapAsync<Post, PostDetail, Post>(
            sql: "SELECT p.Id, p.UserId, p.Title, p.Body, pd.Created, pd.LastModified FROM Posts p INNER JOIN PostDetails pd ON p.Id = pd.PostId Where p.Id = @Id;",
            map: (post, detail) =>
            {
                post.Detail = detail;
                return post;
            },
            param: new { id },
            splitOn: "Created");

        return result.FirstOrDefault();
    }

    //One-to-many
    public async Task<Post?> GetRelationOneToManyAsync(int id)
    {
        var postMap = new Dictionary<int, Post>();

        var result = await _readDbConnection.QueryMapAsync<Post, Comment, Post>(
            sql: "SELECT p.Id, p.UserId, p.Title, p.Body, c.Id, c.PostId, c.Email, c.Name, c.Body FROM Posts p INNER JOIN Comments c ON p.Id = c.PostId Where p.Id = @Id;",
            map: (post, comment) =>
            {
                comment.PostId = post.Id; //non-reference back link

                //check if this order has been seen already
                if (postMap.TryGetValue(post.Id, out Post? existingPost))
                    post = existingPost;
                else
                    postMap.Add(post.Id, post);

                post.Comments.Add(comment);
                return post;
            },
            param: new { id },
            splitOn: "Id");

        return result.FirstOrDefault();
    }

    //Multi mapping
    public async Task<Post?> GetMultiMappingAsync(int id)
    {
        var postMap = new Dictionary<int, Post>();

        var result = await _readDbConnection.QueryMapAsync<Post, Comment, PostDetail, Post>(
            sql: "SELECT p.Id, p.UserId, p.Title, p.Body, " +
            "c.Id, c.PostId, c.Email, c.Name, c.Body, " +
            "pd.Created " +
            "FROM Posts p " +
            "INNER JOIN Comments c " +
            "ON p.Id = c.PostId " +
            "INNER JOIN PostDetails pd " +
            "ON p.Id = pd.PostId " +
            "Where p.Id = @Id;",
            map: (post, comment, detail) =>
            {
                if (post.Detail is null)
                    post.Detail = detail;

                comment.PostId = post.Id; //non-reference back link

                //check if this order has been seen already
                if (postMap.TryGetValue(post.Id, out Post? existingPost))
                    post = existingPost;
                else
                    postMap.Add(post.Id, post);

                post.Comments.Add(comment);
                return post;
            },
            param: new { id },
            splitOn: "Id,Created");

        return result.FirstOrDefault();
    }

    public async Task<IReadOnlyList<Post>> SearchPostByText(string text)
    {
        return await _readDbConnection.QueryAsync<Post>(
            sql: "SELECT * FROM Posts WHERE title LIKE @Text or body LIKE @Text",
            param: new { Text = $"%{ text.Trim() }%" });
    }

    /* Transaction Dapper and EF Core */
    public async Task SampleTransaction()
    {
        _dbContext.Connection.Open();

        using var transaction = _dbContext.Connection.BeginTransaction();

        try
        {
            // TRANSACTION
            _dbContext.Database.UseTransaction(transaction as DbTransaction);

            // add user with EF Core
            var user = new User { Name = "Ervin Howell", Email = "Julianne.OConner@kory.org", Username = "Clementine", Address = new Address("Douglas Extension", "McKenziehaven", "McKenziehaven", "Germany", "59590-4157") };
            await _dbContext.Users.AddAsync(user);
            await _dbContext.SaveChangesAsync();

            // add post with Dapper
            var postId = await _writeDbConnection.QuerySingleAsync<int>(
                sql: $"insert into Posts(UserId, Title, Body) values (@User, @Title, @Body);SELECT CAST(SCOPE_IDENTITY() as int)",
                param: new { User = 1, Title = "ullam et saepe reiciendis voluptatem", Body = "nsit amet autem assumenda provident rerum culpa" },
                transaction: transaction
                );

            if (postId == 0) throw new Exception("error post id");

            // add detail with EF Core
            var detail = new PostDetail { PostId = postId, Created = DateTime.Now };
            await _dbContext.PostDetails.AddAsync(detail);
            await _dbContext.SaveChangesAsync();

            // add comments with Dapper
            var count = await _writeDbConnection.ExecuteAsync(
                sql: @"insert into Comments(PostId, Email, Name, Body) values (@PostId, @Email, @Name, @Body)",
                param: new Comment[] {
                        new Comment { PostId = postId, Email = "Shanna@melissa.tv", Name = "sunt aut facere repellat provident", Body = "occaecati excepturi optio reprehenderit" },
                        new Comment { PostId = postId, Email = "Clementine Bauch", Name = "ea molestias quasi exercitationem", Body = "doloribus vel accusantium quis pariatur" }
                },
                transaction: transaction
              );

            if (count != 2) throw new Exception("error adding posts");

            transaction.Commit();
            // COMMIT
        }
        catch (Exception ex)
        {
            transaction.Rollback();
            throw;
        }
        finally
        {
            _dbContext.Connection.Close();
        }
    }
}

Eso es todo por el momento, seguiré actualizando y agregando contenido en este post. Espero que este artículo le haya dado una buena idea sobre cómo integrar fácilmente Dapper con Entity Framework Core, ya sea para optimizar las rutas críticas o para solucionar las limitaciones.

Código de ejemplo

Dapper y Entity Framework Core

Leer más

Dapper

Entity Framework Core