C#中使用Dapper实现具有单元测试的通用存储库

作者:微信公众号:【架构师老卢】
5-4 9:36
136

高效的数据管理在软件开发中至关重要。本文介绍了一种使用 Repository 和 Unit of Work 模式进行数据访问的简化方法,并通过 Dapper 和 Dapper.Contrib 的强大功能进行了增强。这些工具简化了数据操作,确保代码既可维护又高性能。我们将指导您设置通用存储库和工作单元,使您的数据层强大而高效。

1. 奠定基础:BaseEntity 和实体

此类用作所有实体模型的基础,确保一致性并减少冗余。

这是简单而强大的:BaseEntity

namespace GenericRepository.Data.Entity  
{  
    public class BaseEntity  
    {  
        public int Id { get; set; }  
        public DateTime CreatedDate { get; set; }  
        public DateTime? UpdatedDate { get; set; }  
    }  
}

在此基础上,我们定义了表示应用程序中各种数据模型的特定实体。这些实体中的每一个都继承自 ,确保它们携带基本属性。BaseEntity

YourEntity1:

namespace GenericRepository.Data.Entity  
{  
    public class YourEntity1 : BaseEntity  
    {  
        public string Prop1 { get; set; }  
        public string Prop2 { get; set; }  
    }  
}

YourEntity2:

namespace GenericRepository.Data.Entity  
{  
    public class YourEntity2 : BaseEntity  
    {  
        public string Prop1 { get; set; }  
    }  
}

2. 定义存储库:GenericRepository

通用存储库将应用程序的数据层抽象化,提供一种干净的模块化方式来访问数据。它是实现存储库模式的关键部分,可确保应用程序的其余部分通过一组明确定义的操作与数据层进行交互。

我们是这样定义的:IGenericRepository<TEntity>

namespace GenericRepository.Data  
{  
    public interface IGenericRepository<TEntity> where TEntity : BaseEntity  
    {  
        Task<IEnumerable<TEntity>> GetAllAsync();  
        Task<TEntity> GetByIdAsync(int id);  
        Task InsertAsync(TEntity entity);  
        Task UpdateAsync(TEntity entityToUpdate);  
        Task DeleteAsync(int id);  
    }  
}

使用 Dapper.Contrib 实现存储库

定义 之后,下一步就是实现它。Dapper 以其高性能和易用性而闻名,它提供了执行 SQL 命令和将结果映射到对象的基本功能。Dapper.Contrib 是一个官方扩展,它添加了额外的功能,包括为基本 CRUD 操作自动生成 SQL,这与我们的存储库模式完全一致。IGenericRepository<TEntity>

以下是我们如何实现:GenericRepository<TEntity>

using Dapper.Contrib.Extensions;  
using GenericRepository.Data.Entity;  
using System.Data;  
  
namespace GenericRepository.Data  
{  
    public class GenericRepository<TEntity> : IGenericRepository<TEntity> where TEntity : BaseEntity  
    {  
        private readonly IDbConnection _dbConnection;  
        private readonly IDbTransaction _dbTransaction;  
  
  
        public GenericRepository(IDbTransaction dbTransaction)  
        {  
            _dbConnection = dbTransaction.Connection??default!;  
            _dbTransaction = dbTransaction;  
            SqlMapperExtensions.TableNameMapper = (type) => type.Name;  
        }  
  
        public async Task<IEnumerable<TEntity>> GetAllAsync()  
        {  
            return await _dbConnection.GetAllAsync<TEntity>(_dbTransaction);  
        }  
  
        public async Task<TEntity> GetByIdAsync(int id)  
        {  
            return await _dbConnection.GetAsync<TEntity>(id, _dbTransaction);  
        }  
  
        public async Task InsertAsync(TEntity entity)  
        {  
            entity.CreatedDate = DateTime.UtcNow;  
            entity.UpdatedDate = null;  
            await _dbConnection.InsertAsync(entity, _dbTransaction);  
        }  
  
        public async Task UpdateAsync(TEntity entityToUpdate)  
        {  
            entityToUpdate.UpdatedDate = DateTime.UtcNow;  
            await _dbConnection.UpdateAsync<TEntity>(entityToUpdate, _dbTransaction);  
        }  
  
        public async Task DeleteAsync(int id)  
        {  
            var entity = await GetByIdAsync(id);  
            await _dbConnection.DeleteAsync(entity, _dbTransaction);  
        }  
    }  
  
}

Dapper.Contrib 用于将实体类直接映射到表名,假设类名与表名匹配的约定。SqlMapperExtensions.TableNameMapper

自定义通用存储库

可以根据您的特定需求进行定制。有关高级功能或自定义项,请参阅 Dapper.Contrib 文档。这是一份用于增强存储库功能的综合指南。GenericRepository<TEntity>

3. 管理事务:UnitOfWork

在复杂的应用程序中,确保数据操作的一致性和可靠性至关重要。这就是工作单元模式发挥作用的地方。它管理一组相关的数据库操作,确保它们要么全部成功,要么全部回滚,从而保持数据完整性。

下面介绍我们在应用程序中实现该类的方式:UnitOfWork

using GenericRepository.Data.Entity;  
  
namespace GenericRepository.Data  
{  
    public interface IUnitOfWork : IDisposable  
    {  
        void Commit();  
        void Rollback();  
        IGenericRepository<T> Repository<T>() where T : BaseEntity;  
    }  
}

using GenericRepository.Data.Entity;  
using System.Collections.Concurrent;  
using System.Data;  
  
namespace GenericRepository.Data  
{  
    public class UnitOfWork : IUnitOfWork  
    {  
        private readonly IDbConnection _dbConnection;  
        private IDbTransaction _dbTransaction { get; set; }  
  
        private readonly ConcurrentDictionary<Type, object> _repositories;  
        private bool _disposed = false;  
        public UnitOfWork(  
 IDbConnection dbConnection)  
        {  
            _repositories = new ConcurrentDictionary<Type, object>();  
            _dbConnection = dbConnection;  
            _dbConnection.Open();  
            _dbTransaction = _dbConnection.BeginTransaction();  
  
        }  
  
        public IGenericRepository<T> Repository<T>() where T : BaseEntity  
        {  
            return _repositories.GetOrAdd(typeof(T), _ => new GenericRepository<T>(_dbTransaction)) as IGenericRepository<T> ?? default!;  
        }  
        public void Commit()  
        {  
            try  
            {  
                _dbTransaction.Commit();  
            }  
            catch  
            {  
                _dbTransaction.Rollback();  
                throw;  
            }  
            finally  
            {  
                _dbTransaction.Dispose();  
                _repositories.Clear();  
                _dbConnection?.Close();  
                _dbConnection?.Dispose();  
            }  
        }  
  
        public void Rollback()  
        {  
            try  
            {  
                _dbTransaction.Rollback();  
            }  
            catch  
            {  
                throw;  
            }  
            finally  
            {  
                _dbTransaction.Dispose();  
                _repositories.Clear();  
                _dbConnection?.Close();  
                _dbConnection?.Dispose();  
            }  
        }  
        public void Dispose()  
        {  
            Dispose(true);  
            GC.SuppressFinalize(this);  
        }  
  
        protected virtual void Dispose(bool disposing)  
        {  
            if (!_disposed)  
            {  
                if (disposing)  
                {  
                    // Dispose managed resources.  
                    _dbTransaction?.Dispose();  
                    _dbConnection?.Close();  
                    _dbConnection?.Dispose();  
                }  
  
                // Dispose unmanaged resources.  
  
                _disposed = true;  
            }  
        }  
    }  
  
}

关键方面:UnitOfWork

  • 数据库连接管理:它保存和管理 ,确保所有操作都使用相同的连接。打开连接和开始事务是确保后续操作是同一上下文的一部分的重要步骤。IDbConnection
  • 存储库缓存:该字段是保存存储库实例的线程安全集合。这可确保每种类型的实体在 的生命周期中都有一个存储库实例,从而促进一致性。_repositoriesUnitOfWork
  • 提交和回滚:这些方法是工作单元模式的核心。 尝试保存事务期间所做的所有更改。如果发生错误,则调用以还原所有更改,从而保持数据的完整性。CommitRollback
  • 资源管理:用于确保正确清理的实现。这对于避免资源泄漏至关重要,尤其是在数据库连接和事务的上下文中。IDisposable

通过封装事务管理并确保所有操作都是同一数据库事务的一部分,它提供了一种可靠且可维护的方式来处理复杂的数据操作。UnitOfWork

4. 简化 UnitOfWork 的创建:UnitOfWorkFactory

以一致且可管理的方式创建实例至关重要,尤其是在应用程序复杂性不断增加的情况下。它抽象了实例的创建,确保每个单元都正确实例化了其所有依赖项。此模式增强了代码的可维护性,并遵循依赖关系反转原则。UnitOfWorkUnitOfWorkFactoryUnitOfWork

以下是 :UnitOfWorkFactory

using System.Data.SqlClient;  
  
namespace GenericRepository.Data  
{  
    public interface IUnitOfWorkFactory  
    {  
        IUnitOfWork CreateUnitOfWork();  
    }  
    public class UnitOfWorkFactory : IUnitOfWorkFactory  
    {  
        private readonly string _connectionString;  
  
        public UnitOfWorkFactory(string connectionString)  
        {  
            _connectionString = connectionString;  
        }  
  
        public IUnitOfWork CreateUnitOfWork()  
        {  
            var connection = new SqlConnection(_connectionString);  
            return new UnitOfWork(connection);  
        }  
    }  
}

要点:

  • 创建逻辑的封装:工厂保存用于创建的逻辑,封装详细信息,例如如何打开数据库连接以及如何实例化数据库连接。UnitOfWorkUnitOfWork
  • 连接字符串管理:它存储连接字符串,这是创建数据库连接的关键信息。这允许集中管理数据库连接设置。
  • 简化 UnitOfWork 用法:通过注入任何需要 的 ,可以将代码与创建细节分离,从而更易于管理、测试和更改。IUnitOfWorkFactoryUnitOfWorkUnitOfWork

5. 与依赖注入集成

以下是在 or 中注册 and its dependencies 的方法:UnitOfWorkFactoryStartup.csProgram.cs

public void ConfigureServices(IServiceCollection services)  
{  
    // Other service registrations...  
  
    var connectionString = Configuration.GetConnectionString("DefaultConnection");  
    services.AddTransient<IUnitOfWorkFactory>(_ => new UnitOfWorkFactory(connectionString));  
  
    // Other service registrations...  
}

6. 在 ASP.NET 内核控制器中使用

虽然服务层是处理业务逻辑的理想选择,但在此示例中,我们直接在控制器中使用 and。这使我们的示例保持重点并易于遵循。UnitOfWorkUnitOfWorkFactory

以下是使用以下方法管理控制器中的数据的方法:UnitOfWork

using GenericRepository.Data;  
using GenericRepository.Data.Entity;  
using Microsoft.AspNetCore.Mvc;  
  
namespace GenericRepository.Controllers  
{  
    [ApiController]  
    [Route("[controller]")]  
    public class YourEntity2Controller : ControllerBase  
    {  
  
        private readonly ILogger<YourEntity2Controller> _logger;  
        private IUnitOfWorkFactory _unitOfWorkFactory;  
        public YourEntity2Controller(ILogger<YourEntity2Controller> logger, IUnitOfWorkFactory unitOfWorkFactory)  
        {  
            _logger = logger;  
            _unitOfWorkFactory = unitOfWorkFactory;  
        }  
  
        [HttpGet(Name = "GetYourEntity2")]  
        public async Task<IEnumerable<YourEntity2>> Get()  
        {  
            using var uow = _unitOfWorkFactory.CreateUnitOfWork();  
            var insertTask1 = uow.Repository<YourEntity2>().InsertAsync(new YourEntity2() { Prop1 = DateTime.Now.ToString(), });  
            var insertTask2 = uow.Repository<YourEntity2>().InsertAsync(new YourEntity2() { Prop1 = DateTime.Now.ToString(), });  
            var insertTask3 = uow.Repository<YourEntity2>().InsertAsync(new YourEntity2() { Prop1 = DateTime.Now.ToString(), });  
            await Task.WhenAll(insertTask1, insertTask2, insertTask3);  
            uow.Commit();  
  
            using var uow1 = _unitOfWorkFactory.CreateUnitOfWork();  
            var itemList = await uow1.Repository\<YourEntity2>().GetAllAsync();  
            var item = itemList.First();  
            item.Prop1 = "updated";  
            await uow1.Repository<YourEntity2>().UpdateAsync(item);  
            uow1.Commit();  
  
            using var uow2 = _unitOfWorkFactory.CreateUnitOfWork();  
            return await uow2.Repository<YourEntity2>().GetAllAsync();  
        }  
    }  
}

在此控制器中:

  1. 插入操作:异步插入三个实体。使用 of 可确保在提交事务之前完成所有插入操作。YourEntity2Task.WhenAll
  2. 更新操作:检索第一个实体,更新其属性并提交事务。YourEntity2Prop1
  3. 检索操作:检索 GET 请求要返回的所有实体。YourEntity2

此示例有效地演示了使用工作单元模式在 ASP.NET Core 控制器中的事务管理和基本 CRUD 操作。虽然理想情况下,这些操作应该在服务层中处理,但这种直接方法有助于清楚地演示这些概念。

7. 对通用存储库和工作单元进行单元测试

为了确保我们的通用存储库和工作单元实现的可靠性和正确性,单元测试至关重要。在我们的测试环境中,我们使用 SQL Server 的轻量级版本,非常适合测试数据访问逻辑,而无需完整的 SQL Server 实例。SqlLocalDb

下面是用于测试的数据库上下文的类:ContextFixture

using Dapper;  
using MartinCostello.SqlLocalDb;  
using System.Data;  
  
namespace GenericRepository.Data.Test  
{  
    public class ContextFixture : IDisposable  
    {  
        public IDbConnection dbConnection;  
        private ISqlLocalDbInstanceInfo instance;  
        public ContextFixture()  
        {  
            using var localDB = new SqlLocalDbApi();  
  
            instance = localDB.GetOrCreateInstance("Test");  
            ISqlLocalDbInstanceManager manager = instance.Manage();  
  
            if (!instance.IsRunning)  
            {  
                manager.Start();  
            }  
  
            using var connection1 = instance.CreateConnection();  
            DatabaseInitializer.Migrate(connection1);  
  
            using var connection2 = instance.CreateConnection();  
            connection2.Open();  
            connection2.Execute(@"  
 delete from [dbo].[YourEntity1]  
 delete from [dbo].[YourEntity2]  
 ");  
            connection2.Dispose();  
            dbConnection = instance.CreateConnection();  
        }  
        public IDbConnection GetDbConnection()  
        {  
            return instance.CreateConnection();  
        }  
  
        public void Dispose()  
        {  
            dbConnection.Dispose();  
        }  
    }  
}

在课堂上:ContextFixture

  • 我们初始化 SQL LocalDB 的新实例进行测试。
  • 设置和清理数据库,以确保每个测试的启动状态一致。
  • 我们提供了一种方法来检索新连接以用于我们的测试。GetDbConnection

这种方法可确保我们的单元测试针对可预测且隔离的数据库运行,使我们能够准确评估存储库和工作单元实现的行为。

确保可靠性:对实现进行单元测试

为了确保我们实现的可靠性和正确性,我们创建了一套单元测试,用于提高可读性和清晰度。每个测试都旨在验证不同 CRUD 操作的功能。以下是测试的细分:FluentAssertionsGenericRepositoryUnitOfWork

  1. GetAllAsyncTest:验证是否检索所有记录,包括新插入的记录。GetAllAsync
  2. GetByIdAsyncTest:确保按 ID 正确提取记录。GetByIdAsync
  3. InsertAsyncTest:正确添加新记录的测试,并且该记录在插入后可检索。InsertAsync
  4. UpdateAsyncTest:确认是否准确更新了记录,并且更改仍然存在。UpdateAsync
  5. DeleteAsyncTest:检查是否有效地删除了记录,并且删除后无法检索该记录。DeleteAsync

这些测试使用 用于设置本地数据库实例,确保每个测试都以干净、可预测的状态运行。下面是测试的快照:ContextFixture

using Dapper;
using FluentAssertions;
using GenericRepository.Data.Entity;

namespace GenericRepository.Data.Test
{
    public class UnitOfWorkGenericRepositoryTest : ContextFixture
    {
        [Fact]
        public async void GetAllAsyncTest()
        {
            dbConnection.Open();
            dbConnection.Execute(@"insert into [dbo].[YourEntity1] select 'GetAllAsyncTest','p2','2024/1/1',null");
            dbConnection.Close();

            using var unitOfWork = new UnitOfWork(GetDbConnection());
            var data = await unitOfWork.Repository<YourEntity1>().GetAllAsync();
            var item = data.FirstOrDefault(i => i.Prop1 == "GetAllAsyncTest");
            item.Should().NotBe(null);
        }

        [Fact]
        public async void GetByIdAsyncTest()
        {
            dbConnection.Open();
            dbConnection.Execute(@"insert into [dbo].[YourEntity1] select 'p1','p2','2024/1/1',null");
            dbConnection.Close();

            using var unitOfWork = new UnitOfWork(GetDbConnection());
            var list = await unitOfWork.Repository<YourEntity1>().GetAllAsync();
            var data = await unitOfWork.Repository<YourEntity1>().GetByIdAsync(list.First().Id);
            data.Should().NotBeNull(null);
        }

        [Fact]
        public async void InsertAsyncTest()
        {
            using var unitOfWork = new UnitOfWork(GetDbConnection());
            var item = new YourEntity1() { Prop1 = "p1", Prop2 = "p2" };
            await unitOfWork.Repository<YourEntity1>().InsertAsync(item);
            unitOfWork.Commit();

            using var unitOfWork1 = new UnitOfWork(GetDbConnection());
            var data = await unitOfWork1.Repository<YourEntity1>().GetByIdAsync(item.Id);
            data.Should().NotBeNull(null);
        }

        [Fact]
        public async void UpdateAsyncTest()
        {
            using var unitOfWork = new UnitOfWork(GetDbConnection());
            var item = new YourEntity1() { Prop1 = "p1", Prop2 = "p2" };
            await unitOfWork.Repository<YourEntity1>().InsertAsync(item);
            unitOfWork.Commit();

            using var unitOfWork1 = new UnitOfWork(GetDbConnection());
            var updateKey = "updated xyz";
            item.Prop1 = updateKey;
            await unitOfWork1.Repository<YourEntity1>().UpdateAsync(item);
            unitOfWork1.Commit();

            using var unitOfWork2 = new UnitOfWork(GetDbConnection());
            var data = await unitOfWork2.Repository<YourEntity1>().GetByIdAsync(item.Id);
            data.Prop1.Should().Be(updateKey);
        }

        [Fact]
        public async void DeleteAsyncTest()
        {
            using var unitOfWork = new UnitOfWork(GetDbConnection());
            var item = new YourEntity1() { Prop1 = "p1", Prop2 = "p2" };
            await unitOfWork.Repository<YourEntity1>().InsertAsync(item);
            unitOfWork.Commit();

            using var unitOfWork1 = new UnitOfWork(GetDbConnection());
            await unitOfWork1.Repository<YourEntity1>().DeleteAsync(item.Id);
            unitOfWork1.Commit();

            using var unitOfWork2 = new UnitOfWork(GetDbConnection());
            var data = await unitOfWork2.Repository<YourEntity1>().GetByIdAsync(item.Id);
            data.Should().Be(null);
        }
    }
}

在本指南中,我们介绍了如何使用 Dapper 和 Dapper.Contrib 在 .NET 中设置可靠的数据访问层。我们涵盖了:

  1. BaseEntity:为我们的实体建立一致的基础。
  2. 通用存储库:使用 .IGenericRepository<TEntity>
  3. Dapper.Contrib:简化存储库实现中的 CRUD 操作。
  4. 工作单元:使用 . 确保事务完整性。UnitOfWork
  5. 集成:演示 ASP.NET Core 控制器的实际用法。

虽然为了简单起见,我们直接与控制器中的数据层进行交互,但请记住将业务逻辑封装在实际应用程序的服务层中。

源代码获取:公众号回复消息【code:19454

相关代码下载地址
重要提示!:取消关注公众号后将无法再启用回复功能,不支持解封!
第一步:微信扫码关键公众号“架构师老卢”
第二步:在公众号聊天框发送code:19454,如:code:19454 获取下载地址
第三步:恭喜你,快去下载你想要的资源吧
相关留言评论
昵称:
邮箱:
阅读排行