1
0
mirror of https://github.com/bitwarden/server synced 2026-01-14 14:33:51 +00:00

Create DataTableBuilder (#4608)

* Add DataTableBuilder Using Expressions

* Format

* Unwrap Underlying Enum Type

* Formatting
This commit is contained in:
Justin Baur
2024-09-05 20:44:45 -04:00
committed by GitHub
parent ec2522de8b
commit 329eef82cd
6 changed files with 386 additions and 41 deletions

View File

@@ -1,4 +1,8 @@
using System.Data;
using System.Collections.Frozen;
using System.Data;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using System.Reflection;
using Bit.Core.Entities;
using Bit.Core.Models.Data;
using Dapper;
@@ -7,8 +11,148 @@ using Dapper;
namespace Bit.Infrastructure.Dapper;
/// <summary>
/// Provides a way to build a <see cref="DataTable"/> based on the properties of <see cref="T"/>.
/// </summary>
/// <typeparam name="T"></typeparam>
public class DataTableBuilder<T>
{
private readonly FrozenDictionary<string, (Type Type, Func<T, object?> Getter)> _columnBuilders;
/// <summary>
/// Creates a new instance of <see cref="DataTableBuilder{T}"/>.
/// </summary>
/// <example>
/// <code>
/// new DataTableBuilder<MyObject>(
/// [
/// i => i.Id,
/// i => i.Name,
/// ]
/// );
/// </code>
/// </example>
/// <param name="columnExpressions"></param>
/// <exception cref="ArgumentException"></exception>
public DataTableBuilder(Expression<Func<T, object?>>[] columnExpressions)
{
ArgumentNullException.ThrowIfNull(columnExpressions);
ArgumentOutOfRangeException.ThrowIfZero(columnExpressions.Length);
var columnBuilders = new Dictionary<string, (Type Type, Func<T, object?>)>(columnExpressions.Length);
for (var i = 0; i < columnExpressions.Length; i++)
{
var columnExpression = columnExpressions[i];
if (!TryGetPropertyInfo(columnExpression, out var propertyInfo))
{
throw new ArgumentException($"Could not determine the property info from the given expression '{columnExpression}'.");
}
// Unwrap possible Nullable<T>
var type = Nullable.GetUnderlyingType(propertyInfo.PropertyType) ?? propertyInfo.PropertyType;
// This needs to be after unwrapping the `Nullable` since enums can be nullable
if (type.IsEnum)
{
// Get the backing type of the enum
type = Enum.GetUnderlyingType(type);
}
if (!columnBuilders.TryAdd(propertyInfo.Name, (type, columnExpression.Compile())))
{
throw new ArgumentException($"Property with name '{propertyInfo.Name}' was already added, properties can only be added once.");
}
}
_columnBuilders = columnBuilders.ToFrozenDictionary();
}
private static bool TryGetPropertyInfo(Expression<Func<T, object?>> columnExpression, [MaybeNullWhen(false)] out PropertyInfo property)
{
property = null;
// Reference type properties
// i => i.Data
if (columnExpression.Body is MemberExpression { Member: PropertyInfo referencePropertyInfo })
{
property = referencePropertyInfo;
return true;
}
// Value type properties will implicitly box into the object so
// we need to look past the Convert expression
// i => (System.Object?)i.Id
if (
columnExpression.Body is UnaryExpression
{
NodeType: ExpressionType.Convert,
Operand: MemberExpression { Member: PropertyInfo valuePropertyInfo },
}
)
{
// This could be an implicit cast from the property into our return type object?
property = valuePropertyInfo;
return true;
}
// Other possible expression bodies here
return false;
}
public DataTable Build(IEnumerable<T> source)
{
ArgumentNullException.ThrowIfNull(source);
var table = new DataTable();
foreach (var (name, (type, _)) in _columnBuilders)
{
table.Columns.Add(new DataColumn(name, type));
}
foreach (var entity in source)
{
var row = table.NewRow();
foreach (var (name, (_, getter)) in _columnBuilders)
{
var value = getter(entity);
if (value is null)
{
row[name] = DBNull.Value;
}
else
{
row[name] = value;
}
}
table.Rows.Add(row);
}
return table;
}
}
public static class DapperHelpers
{
private static readonly DataTableBuilder<OrganizationSponsorship> _organizationSponsorshipTableBuilder = new(
[
os => os.Id,
os => os.SponsoringOrganizationId,
os => os.SponsoringOrganizationUserId,
os => os.SponsoredOrganizationId,
os => os.FriendlyName,
os => os.OfferedToEmail,
os => os.PlanSponsorshipType,
os => os.LastSyncDate,
os => os.ValidUntil,
os => os.ToDelete,
]
);
public static DataTable ToGuidIdArrayTVP(this IEnumerable<Guid> ids)
{
return ids.ToArrayTVP("GuidId");
@@ -63,24 +207,9 @@ public static class DapperHelpers
public static DataTable ToTvp(this IEnumerable<OrganizationSponsorship> organizationSponsorships)
{
var table = new DataTable();
var table = _organizationSponsorshipTableBuilder.Build(organizationSponsorships ?? []);
table.SetTypeName("[dbo].[OrganizationSponsorshipType]");
var columnData = new List<(string name, Type type, Func<OrganizationSponsorship, object?> getter)>
{
(nameof(OrganizationSponsorship.Id), typeof(Guid), ou => ou.Id),
(nameof(OrganizationSponsorship.SponsoringOrganizationId), typeof(Guid), ou => ou.SponsoringOrganizationId),
(nameof(OrganizationSponsorship.SponsoringOrganizationUserId), typeof(Guid), ou => ou.SponsoringOrganizationUserId),
(nameof(OrganizationSponsorship.SponsoredOrganizationId), typeof(Guid), ou => ou.SponsoredOrganizationId),
(nameof(OrganizationSponsorship.FriendlyName), typeof(string), ou => ou.FriendlyName),
(nameof(OrganizationSponsorship.OfferedToEmail), typeof(string), ou => ou.OfferedToEmail),
(nameof(OrganizationSponsorship.PlanSponsorshipType), typeof(byte), ou => ou.PlanSponsorshipType),
(nameof(OrganizationSponsorship.LastSyncDate), typeof(DateTime), ou => ou.LastSyncDate),
(nameof(OrganizationSponsorship.ValidUntil), typeof(DateTime), ou => ou.ValidUntil),
(nameof(OrganizationSponsorship.ToDelete), typeof(bool), ou => ou.ToDelete),
};
return organizationSponsorships.BuildTable(table, columnData);
return table;
}
public static DataTable BuildTable<T>(this IEnumerable<T> entities, DataTable table,