mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 15:42:48 -05:00
Create DataTableBuilder (#4608)
* Add DataTableBuilder Using Expressions * Format * Unwrap Underlying Enum Type * Formatting
This commit is contained in:
@ -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,
|
||||
|
@ -8,6 +8,26 @@ namespace Bit.Infrastructure.Dapper.Tools.Helpers;
|
||||
/// </summary>
|
||||
public static class SendHelpers
|
||||
{
|
||||
private static readonly DataTableBuilder<Send> _sendTableBuilder = new(
|
||||
[
|
||||
s => s.Id,
|
||||
s => s.UserId,
|
||||
s => s.OrganizationId,
|
||||
s => s.Type,
|
||||
s => s.Data,
|
||||
s => s.Key,
|
||||
s => s.Password,
|
||||
s => s.MaxAccessCount,
|
||||
s => s.AccessCount,
|
||||
s => s.CreationDate,
|
||||
s => s.RevisionDate,
|
||||
s => s.ExpirationDate,
|
||||
s => s.DeletionDate,
|
||||
s => s.Disabled,
|
||||
s => s.HideEmail,
|
||||
]
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Converts an IEnumerable of Sends to a DataTable
|
||||
/// </summary>
|
||||
@ -16,27 +36,6 @@ public static class SendHelpers
|
||||
/// <returns>A data table matching the schema of dbo.Send containing one row mapped from the items in <see cref="Send"/>s</returns>
|
||||
public static DataTable ToDataTable(this IEnumerable<Send> sends)
|
||||
{
|
||||
var sendsTable = new DataTable();
|
||||
|
||||
var columnData = new List<(string name, Type type, Func<Send, object> getter)>
|
||||
{
|
||||
(nameof(Send.Id), typeof(Guid), c => c.Id),
|
||||
(nameof(Send.UserId), typeof(Guid), c => c.UserId),
|
||||
(nameof(Send.OrganizationId), typeof(Guid), c => c.OrganizationId),
|
||||
(nameof(Send.Type), typeof(short), c => c.Type),
|
||||
(nameof(Send.Data), typeof(string), c => c.Data),
|
||||
(nameof(Send.Key), typeof(string), c => c.Key),
|
||||
(nameof(Send.Password), typeof(string), c => c.Password),
|
||||
(nameof(Send.MaxAccessCount), typeof(int), c => c.MaxAccessCount),
|
||||
(nameof(Send.AccessCount), typeof(int), c => c.AccessCount),
|
||||
(nameof(Send.CreationDate), typeof(DateTime), c => c.CreationDate),
|
||||
(nameof(Send.RevisionDate), typeof(DateTime), c => c.RevisionDate),
|
||||
(nameof(Send.ExpirationDate), typeof(DateTime), c => c.ExpirationDate),
|
||||
(nameof(Send.DeletionDate), typeof(DateTime), c => c.DeletionDate),
|
||||
(nameof(Send.Disabled), typeof(bool), c => c.Disabled),
|
||||
(nameof(Send.HideEmail), typeof(bool), c => c.HideEmail),
|
||||
};
|
||||
|
||||
return sends.BuildTable(sendsTable, columnData);
|
||||
return _sendTableBuilder.Build(sends ?? []);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user