Skip to content

Commit 329eef8

Browse files
authored
Create DataTableBuilder (bitwarden#4608)
* Add DataTableBuilder Using Expressions * Format * Unwrap Underlying Enum Type * Formatting
1 parent ec2522d commit 329eef8

File tree

6 files changed

+386
-41
lines changed

6 files changed

+386
-41
lines changed

bitwarden-server.sln

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
1+
22
Microsoft Visual Studio Solution File, Format Version 12.00
33
# Visual Studio Version 16
44
VisualStudioVersion = 16.0.29102.190
@@ -124,6 +124,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventsProcessor.Test", "tes
124124
EndProject
125125
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Notifications.Test", "test\Notifications.Test\Notifications.Test.csproj", "{90D85D8F-5577-4570-A96E-5A2E185F0F6F}"
126126
EndProject
127+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure.Dapper.Test", "test\Infrastructure.Dapper.Test\Infrastructure.Dapper.Test.csproj", "{4A725DB3-BE4F-4C23-9087-82D0610D67AF}"
128+
EndProject
127129
Global
128130
GlobalSection(SolutionConfigurationPlatforms) = preSolution
129131
Debug|Any CPU = Debug|Any CPU
@@ -308,6 +310,10 @@ Global
308310
{90D85D8F-5577-4570-A96E-5A2E185F0F6F}.Debug|Any CPU.Build.0 = Debug|Any CPU
309311
{90D85D8F-5577-4570-A96E-5A2E185F0F6F}.Release|Any CPU.ActiveCfg = Release|Any CPU
310312
{90D85D8F-5577-4570-A96E-5A2E185F0F6F}.Release|Any CPU.Build.0 = Release|Any CPU
313+
{4A725DB3-BE4F-4C23-9087-82D0610D67AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
314+
{4A725DB3-BE4F-4C23-9087-82D0610D67AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
315+
{4A725DB3-BE4F-4C23-9087-82D0610D67AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
316+
{4A725DB3-BE4F-4C23-9087-82D0610D67AF}.Release|Any CPU.Build.0 = Release|Any CPU
311317
EndGlobalSection
312318
GlobalSection(SolutionProperties) = preSolution
313319
HideSolutionNode = FALSE
@@ -357,6 +363,7 @@ Global
357363
{916AFD8C-30AF-49B6-A5C9-28CA1B5D9298} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
358364
{81673EFB-7134-4B4B-A32F-1EA05F0EF3CE} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
359365
{90D85D8F-5577-4570-A96E-5A2E185F0F6F} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
366+
{4A725DB3-BE4F-4C23-9087-82D0610D67AF} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
360367
EndGlobalSection
361368
GlobalSection(ExtensibilityGlobals) = postSolution
362369
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}

src/Infrastructure.Dapper/DapperHelpers.cs

+147-18
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
using System.Data;
1+
using System.Collections.Frozen;
2+
using System.Data;
3+
using System.Diagnostics.CodeAnalysis;
4+
using System.Linq.Expressions;
5+
using System.Reflection;
26
using Bit.Core.Entities;
37
using Bit.Core.Models.Data;
48
using Dapper;
@@ -7,8 +11,148 @@
711

812
namespace Bit.Infrastructure.Dapper;
913

14+
/// <summary>
15+
/// Provides a way to build a <see cref="DataTable"/> based on the properties of <see cref="T"/>.
16+
/// </summary>
17+
/// <typeparam name="T"></typeparam>
18+
public class DataTableBuilder<T>
19+
{
20+
private readonly FrozenDictionary<string, (Type Type, Func<T, object?> Getter)> _columnBuilders;
21+
22+
/// <summary>
23+
/// Creates a new instance of <see cref="DataTableBuilder{T}"/>.
24+
/// </summary>
25+
/// <example>
26+
/// <code>
27+
/// new DataTableBuilder<MyObject>(
28+
/// [
29+
/// i => i.Id,
30+
/// i => i.Name,
31+
/// ]
32+
/// );
33+
/// </code>
34+
/// </example>
35+
/// <param name="columnExpressions"></param>
36+
/// <exception cref="ArgumentException"></exception>
37+
public DataTableBuilder(Expression<Func<T, object?>>[] columnExpressions)
38+
{
39+
ArgumentNullException.ThrowIfNull(columnExpressions);
40+
ArgumentOutOfRangeException.ThrowIfZero(columnExpressions.Length);
41+
42+
var columnBuilders = new Dictionary<string, (Type Type, Func<T, object?>)>(columnExpressions.Length);
43+
44+
for (var i = 0; i < columnExpressions.Length; i++)
45+
{
46+
var columnExpression = columnExpressions[i];
47+
48+
if (!TryGetPropertyInfo(columnExpression, out var propertyInfo))
49+
{
50+
throw new ArgumentException($"Could not determine the property info from the given expression '{columnExpression}'.");
51+
}
52+
53+
// Unwrap possible Nullable<T>
54+
var type = Nullable.GetUnderlyingType(propertyInfo.PropertyType) ?? propertyInfo.PropertyType;
55+
56+
// This needs to be after unwrapping the `Nullable` since enums can be nullable
57+
if (type.IsEnum)
58+
{
59+
// Get the backing type of the enum
60+
type = Enum.GetUnderlyingType(type);
61+
}
62+
63+
if (!columnBuilders.TryAdd(propertyInfo.Name, (type, columnExpression.Compile())))
64+
{
65+
throw new ArgumentException($"Property with name '{propertyInfo.Name}' was already added, properties can only be added once.");
66+
}
67+
}
68+
69+
_columnBuilders = columnBuilders.ToFrozenDictionary();
70+
}
71+
72+
private static bool TryGetPropertyInfo(Expression<Func<T, object?>> columnExpression, [MaybeNullWhen(false)] out PropertyInfo property)
73+
{
74+
property = null;
75+
76+
// Reference type properties
77+
// i => i.Data
78+
if (columnExpression.Body is MemberExpression { Member: PropertyInfo referencePropertyInfo })
79+
{
80+
property = referencePropertyInfo;
81+
return true;
82+
}
83+
84+
// Value type properties will implicitly box into the object so
85+
// we need to look past the Convert expression
86+
// i => (System.Object?)i.Id
87+
if (
88+
columnExpression.Body is UnaryExpression
89+
{
90+
NodeType: ExpressionType.Convert,
91+
Operand: MemberExpression { Member: PropertyInfo valuePropertyInfo },
92+
}
93+
)
94+
{
95+
// This could be an implicit cast from the property into our return type object?
96+
property = valuePropertyInfo;
97+
return true;
98+
}
99+
100+
// Other possible expression bodies here
101+
return false;
102+
}
103+
104+
public DataTable Build(IEnumerable<T> source)
105+
{
106+
ArgumentNullException.ThrowIfNull(source);
107+
108+
var table = new DataTable();
109+
110+
foreach (var (name, (type, _)) in _columnBuilders)
111+
{
112+
table.Columns.Add(new DataColumn(name, type));
113+
}
114+
115+
foreach (var entity in source)
116+
{
117+
var row = table.NewRow();
118+
119+
foreach (var (name, (_, getter)) in _columnBuilders)
120+
{
121+
var value = getter(entity);
122+
if (value is null)
123+
{
124+
row[name] = DBNull.Value;
125+
}
126+
else
127+
{
128+
row[name] = value;
129+
}
130+
}
131+
132+
table.Rows.Add(row);
133+
}
134+
135+
return table;
136+
}
137+
}
138+
10139
public static class DapperHelpers
11140
{
141+
private static readonly DataTableBuilder<OrganizationSponsorship> _organizationSponsorshipTableBuilder = new(
142+
[
143+
os => os.Id,
144+
os => os.SponsoringOrganizationId,
145+
os => os.SponsoringOrganizationUserId,
146+
os => os.SponsoredOrganizationId,
147+
os => os.FriendlyName,
148+
os => os.OfferedToEmail,
149+
os => os.PlanSponsorshipType,
150+
os => os.LastSyncDate,
151+
os => os.ValidUntil,
152+
os => os.ToDelete,
153+
]
154+
);
155+
12156
public static DataTable ToGuidIdArrayTVP(this IEnumerable<Guid> ids)
13157
{
14158
return ids.ToArrayTVP("GuidId");
@@ -63,24 +207,9 @@ public static DataTable ToArrayTVP(this IEnumerable<CollectionAccessSelection> v
63207

64208
public static DataTable ToTvp(this IEnumerable<OrganizationSponsorship> organizationSponsorships)
65209
{
66-
var table = new DataTable();
210+
var table = _organizationSponsorshipTableBuilder.Build(organizationSponsorships ?? []);
67211
table.SetTypeName("[dbo].[OrganizationSponsorshipType]");
68-
69-
var columnData = new List<(string name, Type type, Func<OrganizationSponsorship, object?> getter)>
70-
{
71-
(nameof(OrganizationSponsorship.Id), typeof(Guid), ou => ou.Id),
72-
(nameof(OrganizationSponsorship.SponsoringOrganizationId), typeof(Guid), ou => ou.SponsoringOrganizationId),
73-
(nameof(OrganizationSponsorship.SponsoringOrganizationUserId), typeof(Guid), ou => ou.SponsoringOrganizationUserId),
74-
(nameof(OrganizationSponsorship.SponsoredOrganizationId), typeof(Guid), ou => ou.SponsoredOrganizationId),
75-
(nameof(OrganizationSponsorship.FriendlyName), typeof(string), ou => ou.FriendlyName),
76-
(nameof(OrganizationSponsorship.OfferedToEmail), typeof(string), ou => ou.OfferedToEmail),
77-
(nameof(OrganizationSponsorship.PlanSponsorshipType), typeof(byte), ou => ou.PlanSponsorshipType),
78-
(nameof(OrganizationSponsorship.LastSyncDate), typeof(DateTime), ou => ou.LastSyncDate),
79-
(nameof(OrganizationSponsorship.ValidUntil), typeof(DateTime), ou => ou.ValidUntil),
80-
(nameof(OrganizationSponsorship.ToDelete), typeof(bool), ou => ou.ToDelete),
81-
};
82-
83-
return organizationSponsorships.BuildTable(table, columnData);
212+
return table;
84213
}
85214

86215
public static DataTable BuildTable<T>(this IEnumerable<T> entities, DataTable table,

src/Infrastructure.Dapper/Tools/Helpers/SendHelpers.cs

+21-22
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,26 @@ namespace Bit.Infrastructure.Dapper.Tools.Helpers;
88
/// </summary>
99
public static class SendHelpers
1010
{
11+
private static readonly DataTableBuilder<Send> _sendTableBuilder = new(
12+
[
13+
s => s.Id,
14+
s => s.UserId,
15+
s => s.OrganizationId,
16+
s => s.Type,
17+
s => s.Data,
18+
s => s.Key,
19+
s => s.Password,
20+
s => s.MaxAccessCount,
21+
s => s.AccessCount,
22+
s => s.CreationDate,
23+
s => s.RevisionDate,
24+
s => s.ExpirationDate,
25+
s => s.DeletionDate,
26+
s => s.Disabled,
27+
s => s.HideEmail,
28+
]
29+
);
30+
1131
/// <summary>
1232
/// Converts an IEnumerable of Sends to a DataTable
1333
/// </summary>
@@ -16,27 +36,6 @@ public static class SendHelpers
1636
/// <returns>A data table matching the schema of dbo.Send containing one row mapped from the items in <see cref="Send"/>s</returns>
1737
public static DataTable ToDataTable(this IEnumerable<Send> sends)
1838
{
19-
var sendsTable = new DataTable();
20-
21-
var columnData = new List<(string name, Type type, Func<Send, object> getter)>
22-
{
23-
(nameof(Send.Id), typeof(Guid), c => c.Id),
24-
(nameof(Send.UserId), typeof(Guid), c => c.UserId),
25-
(nameof(Send.OrganizationId), typeof(Guid), c => c.OrganizationId),
26-
(nameof(Send.Type), typeof(short), c => c.Type),
27-
(nameof(Send.Data), typeof(string), c => c.Data),
28-
(nameof(Send.Key), typeof(string), c => c.Key),
29-
(nameof(Send.Password), typeof(string), c => c.Password),
30-
(nameof(Send.MaxAccessCount), typeof(int), c => c.MaxAccessCount),
31-
(nameof(Send.AccessCount), typeof(int), c => c.AccessCount),
32-
(nameof(Send.CreationDate), typeof(DateTime), c => c.CreationDate),
33-
(nameof(Send.RevisionDate), typeof(DateTime), c => c.RevisionDate),
34-
(nameof(Send.ExpirationDate), typeof(DateTime), c => c.ExpirationDate),
35-
(nameof(Send.DeletionDate), typeof(DateTime), c => c.DeletionDate),
36-
(nameof(Send.Disabled), typeof(bool), c => c.Disabled),
37-
(nameof(Send.HideEmail), typeof(bool), c => c.HideEmail),
38-
};
39-
40-
return sends.BuildTable(sendsTable, columnData);
39+
return _sendTableBuilder.Build(sends ?? []);
4140
}
4241
}

0 commit comments

Comments
 (0)