高效的数据管理在软件开发中至关重要。本文介绍了一种使用 Repository 和 Unit of Work 模式进行数据访问的简化方法,并通过 Dapper 和 Dapper.Contrib 的强大功能进行了增强。这些工具简化了数据操作,确保代码既可维护又高性能。我们将指导您设置通用存储库和工作单元,使您的数据层强大而高效。
此类用作所有实体模型的基础,确保一致性并减少冗余。
这是简单而强大的: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; }
}
}
通用存储库将应用程序的数据层抽象化,提供一种干净的模块化方式来访问数据。它是实现存储库模式的关键部分,可确保应用程序的其余部分通过一组明确定义的操作与数据层进行交互。
我们是这样定义的: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 以其高性能和易用性而闻名,它提供了执行 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>
在复杂的应用程序中,确保数据操作的一致性和可靠性至关重要。这就是工作单元模式发挥作用的地方。它管理一组相关的数据库操作,确保它们要么全部成功,要么全部回滚,从而保持数据完整性。
下面介绍我们在应用程序中实现该类的方式: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
通过封装事务管理并确保所有操作都是同一数据库事务的一部分,它提供了一种可靠且可维护的方式来处理复杂的数据操作。UnitOfWork
以一致且可管理的方式创建实例至关重要,尤其是在应用程序复杂性不断增加的情况下。它抽象了实例的创建,确保每个单元都正确实例化了其所有依赖项。此模式增强了代码的可维护性,并遵循依赖关系反转原则。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);
}
}
}
要点:
以下是在 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...
}
虽然服务层是处理业务逻辑的理想选择,但在此示例中,我们直接在控制器中使用 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();
}
}
}
在此控制器中:
此示例有效地演示了使用工作单元模式在 ASP.NET Core 控制器中的事务管理和基本 CRUD 操作。虽然理想情况下,这些操作应该在服务层中处理,但这种直接方法有助于清楚地演示这些概念。
为了确保我们的通用存储库和工作单元实现的可靠性和正确性,单元测试至关重要。在我们的测试环境中,我们使用 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
这种方法可确保我们的单元测试针对可预测且隔离的数据库运行,使我们能够准确评估存储库和工作单元实现的行为。
为了确保我们实现的可靠性和正确性,我们创建了一套单元测试,用于提高可读性和清晰度。每个测试都旨在验证不同 CRUD 操作的功能。以下是测试的细分:FluentAssertionsGenericRepositoryUnitOfWork
这些测试使用 用于设置本地数据库实例,确保每个测试都以干净、可预测的状态运行。下面是测试的快照: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 中设置可靠的数据访问层。我们涵盖了:
虽然为了简单起见,我们直接与控制器中的数据层进行交互,但请记住将业务逻辑封装在实际应用程序的服务层中。
源代码获取:公众号回复消息【code:19454
】