Skip to content

Commit 1de75aa

Browse files
committed
Add support for projecting entities by multiple IDs
1 parent d07c310 commit 1de75aa

5 files changed

Lines changed: 233 additions & 3 deletions

File tree

src/Dommel/Cache.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ internal enum QueryCacheType
99
GetByMultipleIds,
1010
GetAll,
1111
Project,
12+
ProjectByMultipleIds,
1213
ProjectAll,
1314
Count,
1415
Insert,

src/Dommel/Project.cs

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.Data;
44
using System.Linq;
5+
using System.Text;
56
using System.Threading.Tasks;
67
using Dapper;
78

@@ -44,8 +45,13 @@ internal static string BuildProjectById(ISqlBuilder sqlBuilder, Type type, objec
4445
var cacheKey = new QueryCacheKey(QueryCacheType.Project, sqlBuilder, type);
4546
if (!QueryCache.TryGetValue(cacheKey, out var sql))
4647
{
47-
var keyProperty = Resolvers.KeyProperties(type).Single().Property;
48-
var keyColumnName = Resolvers.Column(keyProperty, sqlBuilder);
48+
var keyProperties = Resolvers.KeyProperties(type);
49+
if (keyProperties.Length > 1)
50+
{
51+
throw new InvalidOperationException($"Entity {type.Name} contains more than one key property." +
52+
"Use the Project<T> overload which supports passing multiple IDs.");
53+
}
54+
var keyColumnName = Resolvers.Column(keyProperties[0].Property, sqlBuilder);
4955

5056
sql = BuildProjectAllQuery(sqlBuilder, type);
5157
sql += $" where {keyColumnName} = @Id";
@@ -58,6 +64,104 @@ internal static string BuildProjectById(ISqlBuilder sqlBuilder, Type type, objec
5864
return sql;
5965
}
6066

67+
/// <summary>
68+
/// Retrieves the entity of type <typeparamref name="TEntity"/> with the specified id.
69+
/// </summary>
70+
/// <typeparam name="TEntity">The type of the entity.</typeparam>
71+
/// <param name="connection">The connection to the database. This can either be open or closed.</param>
72+
/// <param name="ids">The id of the entity in the database.</param>
73+
/// <returns>The entity with the corresponding id.</returns>
74+
public static TEntity? Project<TEntity>(this IDbConnection connection, params object[] ids) where TEntity : class
75+
=> Project<TEntity>(connection, ids, transaction: null);
76+
77+
/// <summary>
78+
/// Retrieves the entity of type <typeparamref name="TEntity"/> with the specified id.
79+
/// </summary>
80+
/// <typeparam name="TEntity">The type of the entity.</typeparam>
81+
/// <param name="connection">The connection to the database. This can either be open or closed.</param>
82+
/// <param name="ids">The id of the entity in the database.</param>
83+
/// <param name="transaction">Optional transaction for the command.</param>
84+
/// <returns>The entity with the corresponding id.</returns>
85+
public static TEntity? Project<TEntity>(this IDbConnection connection, object[] ids, IDbTransaction? transaction = null) where TEntity : class
86+
{
87+
if (ids.Length == 1)
88+
{
89+
return Project<TEntity>(connection, ids[0], transaction);
90+
}
91+
92+
var sql = BuildProjectByIds(GetSqlBuilder(connection), typeof(TEntity), ids, out var parameters);
93+
LogQuery<TEntity>(sql);
94+
return connection.QueryFirstOrDefault<TEntity>(sql, parameters, transaction);
95+
}
96+
97+
/// <summary>
98+
/// Retrieves the entity of type <typeparamref name="TEntity"/> with the specified id.
99+
/// </summary>
100+
/// <typeparam name="TEntity">The type of the entity.</typeparam>
101+
/// <param name="connection">The connection to the database. This can either be open or closed.</param>
102+
/// <param name="ids">The id of the entity in the database.</param>
103+
/// <returns>The entity with the corresponding id.</returns>
104+
public static Task<TEntity?> ProjectAsync<TEntity>(this IDbConnection connection, params object[] ids) where TEntity : class
105+
=> ProjectAsync<TEntity>(connection, ids, transaction: null);
106+
107+
/// <summary>
108+
/// Retrieves the entity of type <typeparamref name="TEntity"/> with the specified id.
109+
/// </summary>
110+
/// <typeparam name="TEntity">The type of the entity.</typeparam>
111+
/// <param name="connection">The connection to the database. This can either be open or closed.</param>
112+
/// <param name="ids">The id of the entity in the database.</param>
113+
/// <param name="transaction">Optional transaction for the command.</param>
114+
/// <returns>The entity with the corresponding id.</returns>
115+
public static async Task<TEntity?> ProjectAsync<TEntity>(this IDbConnection connection, object[] ids, IDbTransaction? transaction = null) where TEntity : class
116+
{
117+
if (ids.Length == 1)
118+
{
119+
return await ProjectAsync<TEntity>(connection, ids[0], transaction);
120+
}
121+
122+
var sql = BuildProjectByIds(GetSqlBuilder(connection), typeof(TEntity), ids, out var parameters);
123+
LogQuery<TEntity>(sql);
124+
return await connection.QueryFirstOrDefaultAsync<TEntity>(sql, parameters, transaction);
125+
}
126+
127+
internal static string BuildProjectByIds(ISqlBuilder sqlBuilder, Type type, object[] ids, out DynamicParameters parameters)
128+
{
129+
var cacheKey = new QueryCacheKey(QueryCacheType.ProjectByMultipleIds, sqlBuilder, type);
130+
if (!QueryCache.TryGetValue(cacheKey, out var sql))
131+
{
132+
var keyProperties = Resolvers.KeyProperties(type);
133+
var keyColumnNames = keyProperties.Select(p => Resolvers.Column(p.Property, sqlBuilder)).ToArray();
134+
if (keyColumnNames.Length != ids.Length)
135+
{
136+
throw new InvalidOperationException($"Number of key columns ({keyColumnNames.Length}) of type {type.Name} does not match with the number of specified IDs ({ids.Length}).");
137+
}
138+
139+
var sb = new StringBuilder(BuildProjectAllQuery(sqlBuilder, type)).Append(" where");
140+
var i = 0;
141+
foreach (var keyColumnName in keyColumnNames)
142+
{
143+
if (i != 0)
144+
{
145+
sb.Append(" and");
146+
}
147+
148+
sb.Append(' ').Append(keyColumnName).Append($" = {sqlBuilder.PrefixParameter("Id")}").Append(i);
149+
i++;
150+
}
151+
152+
sql = sb.ToString();
153+
QueryCache.TryAdd(cacheKey, sql);
154+
}
155+
156+
parameters = new DynamicParameters();
157+
for (var i = 0; i < ids.Length; i++)
158+
{
159+
parameters.Add("Id" + i, ids[i]);
160+
}
161+
162+
return sql;
163+
}
164+
61165
/// <summary>
62166
/// Retrieves all the entities of type <typeparamref name="TEntity"/>.
63167
/// </summary>

test/Dommel.IntegrationTests/ProjectTests.cs

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.ComponentModel.DataAnnotations;
1+
using System;
2+
using System.ComponentModel.DataAnnotations;
23
using System.ComponentModel.DataAnnotations.Schema;
34
using System.Threading.Tasks;
45
using Xunit;
@@ -30,6 +31,88 @@ public async Task ProjectAsync(DatabaseDriver database)
3031
Assert.NotNull(p.Name);
3132
}
3233

34+
[Theory]
35+
[ClassData(typeof(DatabaseTestData))]
36+
public void Project_ParamsOverload(DatabaseDriver database)
37+
{
38+
using var con = database.GetConnection();
39+
var p = con.Project<ProductSmall>(new object[] { 1 });
40+
Assert.NotNull(p);
41+
Assert.Equal(1, p!.ProductId);
42+
Assert.False(string.IsNullOrEmpty(p.Name));
43+
}
44+
45+
[Theory]
46+
[ClassData(typeof(DatabaseTestData))]
47+
public async Task ProjectAsync_ParamsOverload(DatabaseDriver database)
48+
{
49+
using var con = database.GetConnection();
50+
var p = await con.ProjectAsync<ProductSmall>(new object[] { 1 });
51+
Assert.NotNull(p);
52+
Assert.Equal(1, p!.ProductId);
53+
Assert.False(string.IsNullOrEmpty(p.Name));
54+
}
55+
56+
[Theory]
57+
[ClassData(typeof(DatabaseTestData))]
58+
public void Project_ThrowsWhenCompositeKey(DatabaseDriver database)
59+
{
60+
using var con = database.GetConnection();
61+
var ex = Assert.Throws<InvalidOperationException>(() => con.Project<ProjectedProductsCategories>(1));
62+
Assert.Equal("Entity ProjectedProductsCategories contains more than one key property.Use the Project<T> overload which supports passing multiple IDs.", ex.Message);
63+
}
64+
65+
[Theory]
66+
[ClassData(typeof(DatabaseTestData))]
67+
public async Task ProjectAsync_ThrowsWhenCompositeKey(DatabaseDriver database)
68+
{
69+
using var con = database.GetConnection();
70+
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => con.ProjectAsync<ProjectedProductsCategories>(1));
71+
Assert.Equal("Entity ProjectedProductsCategories contains more than one key property.Use the Project<T> overload which supports passing multiple IDs.", ex.Message);
72+
}
73+
74+
[Theory]
75+
[ClassData(typeof(DatabaseTestData))]
76+
public void Project_CompositeKey(DatabaseDriver database)
77+
{
78+
using var con = database.GetConnection();
79+
var p = con.Project<ProjectedProductsCategories>(3, 1);
80+
Assert.NotNull(p);
81+
Assert.Equal(3, p!.Prod_id);
82+
Assert.Equal(1, p.CategoryId);
83+
}
84+
85+
[Theory]
86+
[ClassData(typeof(DatabaseTestData))]
87+
public async Task ProjectAsync_CompositeKey(DatabaseDriver database)
88+
{
89+
using var con = database.GetConnection();
90+
var p = await con.ProjectAsync<ProjectedProductsCategories>(3, 1);
91+
Assert.NotNull(p);
92+
Assert.Equal(3, p!.Prod_id);
93+
Assert.Equal(1, p.CategoryId);
94+
}
95+
96+
[Theory]
97+
[ClassData(typeof(DatabaseTestData))]
98+
public void Project_ThrowsWhenCompositeKeyArgumentsDontMatch(DatabaseDriver database)
99+
{
100+
DommelMapper.QueryCache.Clear();
101+
using var con = database.GetConnection();
102+
var ex = Assert.Throws<InvalidOperationException>(() => con.Project<ProjectedProductsCategories>(1, 2, 3));
103+
Assert.Equal("Number of key columns (2) of type ProjectedProductsCategories does not match with the number of specified IDs (3).", ex.Message);
104+
}
105+
106+
[Theory]
107+
[ClassData(typeof(DatabaseTestData))]
108+
public async Task ProjectAsync_ThrowsWhenCompositeKeyArgumentsDontMatch(DatabaseDriver database)
109+
{
110+
DommelMapper.QueryCache.Clear();
111+
using var con = database.GetConnection();
112+
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => con.ProjectAsync<ProjectedProductsCategories>(1, 2, 3));
113+
Assert.Equal("Number of key columns (2) of type ProjectedProductsCategories does not match with the number of specified IDs (3).", ex.Message);
114+
}
115+
33116
[Theory]
34117
[ClassData(typeof(DatabaseTestData))]
35118
public void ProjectAll(DatabaseDriver database)
@@ -95,4 +178,17 @@ public class ProductSmall
95178
[Column("FullName")]
96179
public string? Name { get; set; }
97180
}
181+
182+
[Table("ProductsCategories")]
183+
public class ProjectedProductsCategories
184+
{
185+
[Key]
186+
[DatabaseGenerated(DatabaseGeneratedOption.None)]
187+
[Column("ProductId")]
188+
public int Prod_id { get; set; }
189+
190+
[Key]
191+
[DatabaseGenerated(DatabaseGeneratedOption.None)]
192+
public int CategoryId { get; set; }
193+
}
98194
}

test/Dommel.Tests/CacheTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public class CacheTests
99
[InlineData(QueryCacheType.GetByMultipleIds)]
1010
[InlineData(QueryCacheType.GetAll)]
1111
[InlineData(QueryCacheType.Project)]
12+
[InlineData(QueryCacheType.ProjectByMultipleIds)]
1213
[InlineData(QueryCacheType.ProjectAll)]
1314
[InlineData(QueryCacheType.Count)]
1415
[InlineData(QueryCacheType.Insert)]

test/Dommel.Tests/ProjectTests.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
using System;
2+
using System.ComponentModel.DataAnnotations.Schema;
3+
using System.ComponentModel.DataAnnotations;
4+
using System.Linq;
25
using Xunit;
36
using static Dommel.DommelMapper;
47

@@ -16,6 +19,17 @@ public void ProjectById()
1619
Assert.NotNull(parameters);
1720
}
1821

22+
[Fact]
23+
public void ProjectByIds()
24+
{
25+
var sql = BuildProjectByIds(SqlBuilder, typeof(ProjectedProductsCategories), new object[] { 4, 2 }, out var parameters);
26+
Assert.Equal("select [ProductId], [CategoryId], [FullName] from [ProjectedProductsCategories] where [ProjectedProductsCategories].[ProductId] = @Id0 and [ProjectedProductsCategories].[CategoryId] = @Id1", sql);
27+
Assert.NotNull(parameters);
28+
Assert.Equal(2, parameters.ParameterNames.Count());
29+
Assert.Equal(4, parameters.Get<int>("Id0"));
30+
Assert.Equal(2, parameters.Get<int>("Id1"));
31+
}
32+
1933
[Fact]
2034
public void ProjectAll()
2135
{
@@ -38,4 +52,18 @@ public class ProjectedFoo
3852

3953
public DateTime? DateUpdated { get; set; }
4054
}
55+
56+
public class ProjectedProductsCategories
57+
{
58+
[Key]
59+
[DatabaseGenerated(DatabaseGeneratedOption.None)]
60+
public int ProductId { get; set; }
61+
62+
[Key]
63+
[DatabaseGenerated(DatabaseGeneratedOption.None)]
64+
public int CategoryId { get; set; }
65+
66+
[Column("FullName")]
67+
public string? Name { get; set; }
68+
}
4169
}

0 commit comments

Comments
 (0)