C#:为什么你会选择 RepoDb 而不是 Dapper(ORM)?
由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!
介绍
在本页中,我们将分享RepoDb与Dapper 的区别以及它们各自的独特之处。我们尽力在大多数方面进行一对一的比较。希望本页内容能帮助您作为开发者,有充分的理由选择RepoDb作为您的微型 ORM 。
“我是一名开源贡献者,今天想和大家分享我的成果。我为此付出了很多努力,旨在改进 .NET 中的数据访问。我恳请大家支持这个库。希望大家能够分享、撰写博客并使用它。”
本教程的所有内容均由我(作者本人)撰写。与我们对RepoDb的了解相比,我们对Dapper 的了解还不够深入。因此,如果您认为本页面内容偏向RepoDb ,请立即提出您的意见或建议。
在我们开始之前
以下示例中我们使用的编程语言和数据库提供商分别是C#和SQL Server。
这两个库都是用于.NET的ORM框架。它们都轻量级、快速且高效。Dapper是一个功能齐全的微型 ORM,而RepoDb是一个混合型 ORM。
为了避免比较结果出现偏差,我们将不讨论RepoDb中存在而Dapper中不存在的功能(例如:缓存、跟踪、查询提示、可扩展性、语句构建器和存储库)(反之亦然)。此外,比较结果也不包含两者的任何其他扩展库(例如:RepoDb.SqLite、RepoDb.MySql、RepoDb.PostgreSql、Dapper.Contrib、DapperExtensions、Dapper.SqlBuilder等)。
表格
假设我们有以下数据库表。
CREATE TABLE [dbo].[Customer]
(
[Id] BIGINT IDENTITY(1,1)
, [Name] NVARCHAR(128) NOT NULL
, [Address] NVARCHAR(MAX)
, CONSTRAINT [PK_Customer] PRIMARY KEY CLUSTERED ([Id] ASC )
)
ON [PRIMARY];
GO
CREATE TABLE [dbo].[Product]
(
[Id] BIGINT IDENTITY(1,1)
, [Name] NVARCHAR(128) NOT NULL
, [Price] Decimal(18,2)
, CONSTRAINT [PK_Product] PRIMARY KEY CLUSTERED ([Id] ASC )
)
ON [PRIMARY];
GO
CREATE TABLE [dbo].[Order]
(
[Id] BIGINT IDENTITY(1,1)
, [ProductId] BIGINT NOT NULL
, [CustomerId] BIGINT
, [OrderDateUtc] DATETIME(5)
, [Quantity] INT
, CONSTRAINT [PK_Order] PRIMARY KEY CLUSTERED ([Id] ASC )
)
ON [PRIMARY];
GO
模型
假设我们有以下类模型。
public class Customer
{
public long Id { get; set; }
public string Name { get; set; }
public string Address { get; set; }
}
public class Product
{
public long Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
public class Order
{
public long Id { get; set; }
public long ProductId { get; set; }
public long CustomerId { get; set; }
public int Quantity { get; set; }
public DateTime OrderDateUtc{ get; set; }
}
CRUD 基本区别
查询多行
潇洒:
-
询问:
using (var connection = new SqlConnection(ConnectionString)) { var customers = connection.Query<Customer>("SELECT * FROM [dbo].[Customer];"); }
RepoDb:
-
原始 SQL:
using (var connection = new SqlConnection(ConnectionString)) { var customers = connection.ExecuteQuery<Customer>("SELECT * FROM [dbo].[Customer];"); } -
流利:
using (var connection = new SqlConnection(ConnectionString)) { var customers = connection.QueryAll<Customer>(); }
查询单个记录
潇洒:
-
询问
using (var connection = new SqlConnection(ConnectionString)) { var customer = connection.Query<Customer>("SELECT * FROM [dbo].[Customer] WHERE (Id = @Id);", new { Id = 10045 }).FirstOrDefault(); }
RepoDb:
-
原始 SQL:
using (var connection = new SqlConnection(ConnectionString)) { var customer = connection.ExecuteQuery<Customer>("SELECT * FROM [dbo].[Customer] WHERE (Id = @Id);", new { Id = 10045 }).FirstOrDefault(); } -
流利:
using (var connection = new SqlConnection(ConnectionString)) { var customer = connection.Query<Customer>(e => e.Id == 10045).FirstOrDefault(); }
插入记录
潇洒:
-
执行:
默认情况下,它返回受影响的行数。
using (var connection = new SqlConnection(ConnectionString)) { var customer = new Customer { Name = "John Doe", Address = "New York" }; var affectedRows = connection.Execute("INSERT INTO [dbo].[Customer] (Name, Address) VALUES (@Name, @Address);", customer); } -
询问:
返回标识值。
using (var connection = new SqlConnection(ConnectionString)) { var customer = new Customer { Name = "John Doe", Address = "New York" }; var id = connection.Query<long>("INSERT INTO [dbo].[Customer] (Name, Address) VALUES (@Name, @Address); SELECT CONVERT(BIGINT, SCOPE_IDENTITY());", customer).Single(); }
RepoDb:
-
原始 SQL:
using (var connection = new SqlConnection(ConnectionString)) { var customer = new Customer { Name = "John Doe", Address = "New York" }; var id = connection.ExecuteScalar<long>("INSERT INTO [dbo].[Customer] (Name, Address) VALUES (@Name, @Address); SELECT CONVERT(BIGINT, SCOPE_IDENTITY());", customer); } -
流利:
using (var connection = new SqlConnection(ConnectionString)) { var customer = new Customer { Name = "John Doe", Address = "New York" }; var id = (long)connection.Insert<Customer>(customer); // or connection.Insert<Customer, long>(customer); }
更新记录
潇洒:
-
执行:
using (var connection = new SqlConnection(ConnectionString)) { var affectedRows = connection.Execute("UPDATE [dbo].[Customer] SET Name = @Name, Address = @Address WHERE Id = @Id;", new { Id = 10045, Name = "John Doe", Address = "New York" }); }
RepoDb:
-
原始 SQL:
using (var connection = new SqlConnection(ConnectionString)) { var affectedRows = connection.ExecuteScalar<int>("UPDATE [dbo].[Customer] SET Name = @Name, Address = @Address WHERE Id = @Id;", new { Id = 10045, Name = "John Doe", Address = "New York" }); } -
流利:
using (var connection = new SqlConnection(ConnectionString)) { var customer = new Customer { Id = 10045, Name = "John Doe", Address = "New York" }; var affectedRows = connection.Update<Customer>(customer); }
删除记录
潇洒:
-
执行:
using (var connection = new SqlConnection(ConnectionString)) { var affectedRows = connection.Execute("DELETE FROM [dbo].[Customer] WHERE Id = @Id;", new { Id = 10045 }); }
RepoDb:
-
原始 SQL:
using (var connection = new SqlConnection(ConnectionString)) { var affectedRows = connection.ExecuteScalar<int>("DELETE FROM [dbo].[Customer] WHERE Id = @Id;", new { Id = 10045 }); } -
流利:
using (var connection = new SqlConnection(ConnectionString)) { var affectedRows = connection.Delete<Customer>(10045); }
提前通话的区别
查询父节点及其子节点
假设我们已经在Customer类中添加了Orders(类型为 IEnumerable<Order>)属性。
-
顾客
public class Customer { public long Id { get; set; } public string Name { get; set; } public string Address { get; set; } public IEnumerable<Order> Orders { get; set; } } -
命令
public class Order { public long Id { get; set; } public long ProductId { get; set; } public long CustomerId { get; set; } public int Quantity { get; set; } public DateTime OrderDateUtc{ get; set; } }
潇洒:
-
询问:
using (var connection = new SqlConnection(ConnectionString)) { var sql = "SELECT C.Id, C.Name, C.Address, O.ProductId, O.Quantity, O.OrderDateUtc FROM [dbo].[Customer] C INNER JOIN [dbo].[Order] O ON O.CustomerId = C.Id WHERE C.Id = @Id;"; var customers = connection.Query<Customer, Order, Customer>(sql, (customer, order) => { customer.Orders = customer.Orders ?? new List<Order>(); customer.Orders.Add(order); return customer; }, new { Id = 10045 }); } -
查询多个:
using (var connection = new SqlConnection(ConnectionString)) { var sql = "SELECT * FROM [dbo].[Customer] WHERE Id = @CustomerId; SELECT * FROM [dbo].[Order] WHERE CustomerId = @CustomerId;"; using (var result = connection.QueryMultiple(sql, new { CustomerId = 10045 })) { var customer = result.Read<Customer>().First(); var orders = result.Read<Order>().ToList(); } }
RepoDb:
目前我们有意暂不支持JOIN功能。我们在“通过 QueryMultiple 和 ExecuteQueryMultiple 处理多个结果集”页面中对此进行了说明。此外,我们也在常见问题解答中提供了相关答案。
不过,这项功能的支持工作即将启动。我们目前正在根据社区的反馈进行一项投票调查,探讨如何实现这项功能。您可以在这里查看讨论内容,我们也期待听到您的意见!
这一点毋庸置疑。最理想的方法是在数据库中执行真正的INNER JOIN操作,就像Dapper所做的那样!
然而,在RepoDb中还有另一种方法可以实现这一点。它可以通过多查询来实现,多查询可以在一次调用中执行多个打包的 SELECT 语句。
-
原始 SQL:
using (var connection = new SqlConnection(ConnectionString)) { var sql = "SELECT * FROM [dbo].[Customer] WHERE Id = @CustomerId; SELECT * FROM [dbo].[Order] WHERE CustomerId = @CustomerId;"; var extractor = connection.ExecuteQueryMultiple(sql, new { CustomerId = 10045 }); var customer = extractor.Extract<Customer>().FirstOrDefault(); var orders = extractor.Extract<Order>().AsList(); customer.Orders = orders; } -
流利:
using (var connection = new SqlConnection(ConnectionString)) { var customerId = 10045; var tuple = connection.QueryMultiple<Customer, Order>(customer => customer.Id == customerId, order => order.CustomerId == customerId); var customer = tuple.Item1.FirstOrDefault(); var orders = tuple.Item2.AsList(); customer.Orders = orders; }
查询多个父母及其子女
与上一节内容几乎相同。
-
询问:
var customers = new List<Customer>(); using (var connection = new SqlConnection(ConnectionString)) { var sql = "SELECT C.Id, C.Name, C.Address, O.ProductId, O.Quantity, O.OrderDateUtc FROM [dbo].[Customer] C INNER JOIN [dbo].[Order] O ON O.CustomerId = C.Id;"; var customers = connection.Query<Customer, Order, Customer>(sql, (customer, order) => { customer = customers.Where(e => e.Id == customer.Id).FirstOrDefault() ?? customer; customer.Orders = customer.Orders ?? new List<Order>(); customer.Orders.Add(order); return customer; }); }注意:这种黑客技术是在开发者端进行的,而不是嵌入在库内部的。
-
查询多个:
using (var connection = new SqlConnection(ConnectionString)) { var sql = "SELECT * FROM [dbo].[Customer]; SELECT * FROM [dbo].[Order];"; using (var result = connection.QueryMultiple(sql, new { CustomerId = 10045 })) { var customers = result.Read<Customer>().ToList(); var orders = result.Read<Order>().ToList(); customers.ForEach(customer => customer.Orders = orders.Where(o => o.CustomerId == customer.Id).ToList()); // Client memory processing } }
RepoDb:
-
原始 SQL:
using (var connection = new SqlConnection(ConnectionString)) { var extractor = connection.ExecuteQueryMultiple("SELECT * FROM [dbo].[Customer]; SELECT * FROM [dbo].[Order];"); var customers = extractor.Extract<Customer>().AsList(); var orders = extractor.Extract<Order>().AsList(); customers.ForEach(customer => customer.Orders = orders.Where(o => o.CustomerId == customer.Id).AsList()); // Client memory processing } -
流利:
using (var connection = new SqlConnection(ConnectionString)) { var customerId = 10045; var tuple = connection.QueryMultiple<Customer, Order>(customer => customer.Id == customerId, order => order.CustomerId == customerId); var customers = tuple.Item1.FirstOrDefault(); var orders = tuple.Item2.AsList(); customers.ForEach(customer => customer.Orders = orders.Where(o => o.CustomerId == customer.Id).AsList()); // Client memory processing }
插入多行
潇洒:
-
询问:
using (var connection = new SqlConnection(ConnectionString)) { var customers = GenerateCustomers(1000); var identities = connection.Query<long>("INSERT INTO [dbo].[Customer] (Name, Address) VALUES (@Name, @Address); SELECT CONVERT(BIGINT, SCOPE_IDENTITY());", customers); }实际上,这一点我不太明白:
- 它是否会创建隐式事务?如果其中一行数据出错会怎样?
- 它是否遍历列表并多次调用DbCommand.Execute ?
请在这里纠正我,以便我立即更新此页面。
RepoDb:
-
批量操作:
using (var connection = new SqlConnection(ConnectionString)) { var customers = GenerateCustomers(1000); var affectedRows = connection.InsertAll<Customer>(customers); }可以通过向batchSize参数传递一个值来批量执行上述操作。
注意:您可以指定要针对的列。此外,标识值会自动设置回实体。
-
批量操作:
using (var connection = new SqlConnection(ConnectionString)) { var customers = GenerateCustomers(1000); var affectedRows = connection.BulkInsert<Customer>(customers); }可以通过向batchSize参数传递一个值来批量执行上述操作。
注:仅供参考。此操作使用ADO.NET的SqlBulkCopy方法。由于这是真正的批量操作,因此不应将其性能与Dapper 的性能进行比较。与Dapper(多条插入)和RepoDb(InsertAll)操作相比,此操作速度极快。
合并多行
潇洒:
-
询问:
using (var connection = new SqlConnection(ConnectionString)) { var sql = @"MERGE [dbo].[Customer] AS T USING (SELECT @Name, @Address) AS S ON S.Id = T.Id WHEN NOT MATCH THEN INSERT INTO ( Name , Address ) VALUES ( S.Name , S. Address) WHEN MATCHED THEN UPDATE SET Name = S.Name , Address = S.Address OUTPUT INSERTED.Id AS Result;"; var customers = GenerateCustomers(1000); var identities = connection.Query<long>(sql, customers); }这里我的问题和上一节一样。
RepoDb:
-
流利:
using (var connection = new SqlConnection(ConnectionString)) { var customers = GenerateCustomers(1000); var affectedRows = connection.MergeAll<Customer>(customers); }可以通过向batchSize参数传递一个值来批量执行上述操作。
注意:您可以设置限定字段。此外,对于新插入的记录,标识值会自动设置回实体。
更新多行
潇洒:
-
询问:
using (var connection = new SqlConnection(ConnectionString)) { var customers = GenerateCustomers(1000); var affectedRows = connection.Execute("UPDATE [dbo].[Customer] SET Name = @Name, Address = @Address WHERE Id = @Id;", customers); }
RepoDb:
-
流利:
using (var connection = new SqlConnection(ConnectionString)) { var customers = GenerateCustomers(1000); var affectedRows = connection.UpdateAll<Customer>(customers); }可以通过向batchSize参数传递一个值来批量执行上述操作。
注意:您可以设置限定符字段。
批量插入多行
潇洒:
-
ADO.NET:
using (var connection = new SqlConnection(ConnectionString)) { var customers = GenerateCustomers(1000); var table = ConvertToTable(customers); using (var sqlBulkCopy = new SqlBulkCopy(connection, options, transaction)) { sqlBulkCopy.DestinationTableName = "Customer"; sqlBulkCopy.WriteToServer(table); } }注意:您也可以传递DbDataReader的实例(而不是DataTable)。
RepoDb:
-
流利:
using (var connection = new SqlConnection(ConnectionString)) { var customers = GenerateCustomers(1000); var affectedRows = connection.BulkInsert<Customer>(customers); }注意:您也可以传递DbDataReader的实例。
-
流利(目标明确):
using (var connection = new SqlConnection(ConnectionString)) { var customers = GenerateCustomers(1000); var affectedRows = connection.BulkInsert("[dbo].[Customer]", customers); }
按批次查询行
潇洒:
-
询问:
using (var connection = new SqlConnection(ConnectionString)) { var sql = @"WITH CTE AS ( SELECT TOP (@Rows) ROW_NUMBER() OVER(ORDER BY Name ASC) AS RowNumber FROM [dbo].[Customer] WHERE (Address = @Address) ) SELECT Id , Name , Address FROM CTE WHERE RowNumber BETWEEN @From AND (@From + @Rows);"; using (var connection = new SqlConnection(ConnectionString)) { var customers = connection.Query<Customer>(sql, new { From = 0, Rows = 100, Address = "New York" }); } }注:您也可以使用 ( LIMIT ) 关键字来执行此操作。这取决于您的个人喜好。
RepoDb:
-
流利:
using (var connection = new SqlConnection(ConnectionString)) { var customers = connection.BatchQuery<Customer>(e => e.Address == "New York", page: 0, rowsPerBatch: 100, orderBy: OrderField.Parse(new { Name = Order.Ascending })); }
从不同数据库复制记录
潇洒:
-
询问:
using (var sourceConnection = new SqlConnection(SourceConnectionString)) { var customers = sourceConnection.Query<Customer>("SELECT * FROM [dbo].[Customer];"); using (var destinationConnection = new SqlConnection(DestinationConnectionString)) { var identities = destinationConnection.Query<long>("INSERT INTO [dbo].[Customer] (Name, Address) VALUES (@Name, @Address); SELECT CONVERT(BIGINT, SCOPE_IDENTITY());", customers); } }
RepoDb:
-
Fluent(全部插入):
using (var sourceConnection = new SqlConnection(SourceConnectionString)) { var customers = sourceConnection.QueryAll<Customer>(); using (var destinationConnection = new SqlConnection(DestinationConnectionString)) { var affectedRows = destinationConnection.InsertAll<Customer>(customers); } } -
Fluent(批量插入):
using (var sourceConnection = new SqlConnection(SourceConnectionString)) { var customers = sourceConnection.QueryAll<Customer>(); using (var destinationConnection = new SqlConnection(DestinationConnectionString)) { var affectedRows = destinationConnection.BulkInsert<Customer>(customers); } } -
流畅(流媒体):
这是处理大型数据集的最佳且推荐的方法。我们不会在客户端应用程序中将数据作为类对象传递。
using (var sourceConnection = new SqlConnection(SourceConnectionString)) { using (var reader = sourceConnection.ExecuteReader("SELECT * FROM [dbo].[Customer];")) { using (var destinationConnection = new SqlConnection(DestinationConnectionString)) { var affectedRows = destinationConnection.BulkInsert<Customer>(reader); } } }注意:请检查排序规则约束。这是ADO.NET 的特性。
参数传递
潇洒:
-
动态的:
Query<T>(sql, new { Id = 10045 });它始终是相等运算。您可以通过SQL 语句控制查询。
-
动态参数:
var parameters = new DynamicParameters(); parameters.Add("Name", "John Doe"); parameters.Add("Address", "New York"); Query<T>(sql, parameters);
RepoDb:
-
动态的:
Query<T>(new { Id = 10045 });与Dapper类似,它始终指的是相等运算。您可以通过SQL 语句控制查询。
-
LINQ表达式:
Query<T>(e => e.Id == 10045); -
查询字段:
Query<T>(new QueryField("Id", 10045)); -
查询字段或查询组:
var queryFields = new[] { new QueryField("Name", "John Doe") new QueryField("Address", "New York") }; Query<T>(queryFields); // or Query<T>(new QueryGroup(queryFields));
参数数组
潇洒:
-
询问:
using (var connection = new SqlConnection(ConnectionString)) { var addresses = new [] { "New York", "Washington" }; var customers = connection.Query<Customer>("SELECT * FROM [dbo].[Customer] WHERE Address IN (@Addresses);", new { Addresses = addresses }); }
RepoDb:
-
执行查询:
using (var connection = new SqlConnection(ConnectionString)) { var addresses = new [] { "New York", "Washington" }; var customers = connection.ExecuteQuery<Customer>("SELECT * FROM [dbo].[Customer] WHERE Address IN (@Addresses);", new { Addresses = addresses }); }如需进一步说明,您可以访问我们的文档。
-
询问:
using (var connection = new SqlConnection(ConnectionString)) { var addresses = new [] { "New York", "Washington" }; var customers = connection.Query<Customer>(e => addresses.Contains(e => e.Address)); }
表达式树
- Dapper 不支持Linq 表达式,仅支持动态和动态参数。
- RepoDb 支持Linq 表达式、动态和查询对象。
注意: Dapper.DynamicParameters只是RepoDb.QueryObjects的一个子集。QueryObjects具有更强大的功能,可以进一步支持Linq表达式。
请查阅这两份文档。
支持的数据库
潇洒:
支持所有关系数据库管理系统(RDBMS)数据提供程序。
RepoDb:
- Raw-SQL 支持所有关系数据库管理系统 (RDBMS) 数据提供程序。
- Fluent 调用仅支持SQL Server、SqLite、MySql和PostgreSql。
性能和效率
我们只参考社区认可的 ORM 基准测试工具之一,即RawDataAccessBencher。
.NET Core:
以下是我们根据官方执行结果得出的观察结果。官方结果可在此处查看。
表现:
- RepoDb 是获取集合记录时速度最快的 ORM,无论是原始 SQL调用还是Fluent调用。
- Dapper 和 RepoDb 在获取单条记录时的速度相同。
- 在获取单个记录时,Dapper 比 RepoDb 的Fluent调用速度更快。
效率:
- RepoDb 是获取集合记录时效率最高的 ORM,支持原始 SQL和Fluent调用。
- 在获取单条记录时,Dapper 比 RepoDb 效率高得多。
.NET Framework:
RepoDb 是获取集合记录和单个记录速度最快、效率最高的ORM 。官方结果可在此处查看。
质量
潇洒:
Dapper 自 2012 年以来一直在运行,并被StackOverflow.com使用。它拥有庞大的用户群,并得到了社区的大力支持。
RepoDb:
我们尽力为每个场景编写了一个测试用例,目前已交付了数千个测试用例(约 6500 个),包括单元测试和集成测试。我们希望您也能帮忙审阅一下。
以下是我们的测试套件链接。
- 核心单元测试
- 核心集成测试
- SQL Server 单元测试
- SQL Server 集成测试
- SQLite单元测试
- SQLite集成测试
- MySQL单元测试
- MySQL集成测试
- PostgreSQL 单元测试
- PostgreSQL 集成测试
- RepoDb.SqlServer.BulkOperations 集成测试
我们(或者说作为作者的我)一直被质疑软件质量并不取决于测试的数量。然而,我们坚信,投入大量精力编写测试能够增强库用户(即.NET社区)的信心。实际上,如果有人向我们提交PR ,这有助于我们避免对已正常运行的功能进行手动修改;它还能防止库出现任何意料之外的错误。
质量结论:
两者都质量很高,但Dapper比RepoDb成熟得多。这一点我们并不否认!
图书馆支持
潇洒:
经过验证,并得到 .NET 社区的大力支持;由StackOverflow.com资助。
RepoDb:
该项目由一人独立完成,未获得任何机构的资助或赞助。目前正处于起步阶段,希望得到 .NET 社区的更多支持。
许可和合法性
两者均采用Apache-2.0许可证。
免责声明:
我们并非法律专家,但提供咨询服务。如果RepoDb遇到任何版权或商标方面的纠纷,目前尚未解决。
总体结论
我们希望您能再次考虑并访问这个图书馆。它相比最初的样子已经有了很大的改进。
简单
Dapper 虽然轻量级,但会将你带入代码开发的复杂层面。编写原始 SQL 语句总是很繁琐,而且由于它对编译器不友好,维护起来也很困难。此外,为了完成必要的任务,你还需要实现一些必要的功能。
RepoDb 是一个非常易于使用的 ORM,它具有足够多的功能集供您使用。
表现
如果性能是唯一考虑因素,那么 RepoDb 比 Dapper 快,这足以成为选择该库的理由。
RepoDb 是 .NET 中最快的 ORM。这一说法得到了社区认可的 ORM 性能测试工具RawDataAccessBencher的官方运行结果的支持。
效率
RepoDb 比 Dapper 更高效(与性能部分所述相同)。
经验
使用 RepoDb 开发代码更加轻松快捷。它拥有丰富的功能集,可立即使用(例如:二层缓存、流畅方法)。它将帮助您作为开发人员以更快、更简洁的方式交付更多代码。
特征
在 RepoDb 中,微型 ORM 框架内具备必要的功能,这将对您的开发大有帮助。
批量操作、属性处理器、二级缓存、表达式树、多重查询和内联提示等功能是最常用的功能。而Dapper的主要痛点在于,这些功能在Dapper中缺失。
感谢您阅读本文。原文链接在此。
我们恳请您支持此代码库和解决方案。您为我们的GitHub页面点赞对我们意义重大。
文章来源:https://dev.to/mikependon/c-what-will-make-you-choose-repodb-over-dapper-orm-3eb8