Files
UnrealEngine/Engine/Source/Programs/Shared/EpicGames.MongoDB/TransactionBuilder.cs
2025-05-18 13:04:45 +08:00

301 lines
10 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using MongoDB.Driver;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
namespace EpicGames.MongoDB
{
/// <summary>
/// Allows building a transactional MongoDB update of several fields, and applying those to an in-memory object. Useful for transactional updates.
/// </summary>
/// <typeparam name="TDocument">The type of document to update</typeparam>
public class TransactionBuilder<TDocument> where TDocument : class
{
/// <summary>
/// Information about an update to a field
/// </summary>
class FieldUpdate
{
/// <summary>
/// The update definition
/// </summary>
public UpdateDefinition<TDocument> _update;
/// <summary>
/// Applies an update to the given object
/// </summary>
public Action<object> _apply;
/// <summary>
/// Constructor
/// </summary>
/// <param name="update">The update definition</param>
/// <param name="apply">Applies the update to a given object</param>
public FieldUpdate(UpdateDefinition<TDocument> update, Action<object> apply)
{
_update = update;
_apply = apply;
}
}
/// <summary>
/// Helper class for determining the indexer for a particular type
/// </summary>
/// <typeparam name="T">The type to find the indexer for</typeparam>
static class Reflection<T>
{
/// <summary>
/// Caches the indexer for this type
/// </summary>
public static PropertyInfo Indexer { get; } = GetIndexerProperty();
/// <summary>
/// Finds the indexer property from a type
/// </summary>
/// <returns>The property info</returns>
static PropertyInfo GetIndexerProperty()
{
foreach (PropertyInfo property in typeof(T).GetProperties())
{
if (property.GetIndexParameters().Length > 0)
{
return property;
}
}
return null!;
}
}
/// <summary>
/// List of field updates
/// </summary>
readonly List<FieldUpdate> _fieldUpdates = new List<FieldUpdate>();
/// <summary>
/// Whether the transaction is currenty empty
/// </summary>
public bool IsEmpty => _fieldUpdates.Count == 0;
/// <summary>
/// Adds a setting to this transaction
/// </summary>
/// <typeparam name="TField">Type of the field to be updated</typeparam>
/// <param name="expr">The expresion defining the field to update</param>
/// <param name="value">New value for the field</param>
public void Set<TField>(Expression<Func<TDocument, TField>> expr, TField value)
{
UpdateDefinition<TDocument> update = Builders<TDocument>.Update.Set(expr, value);
void Apply(object target) => Assign(target, expr, value);
_fieldUpdates.Add(new FieldUpdate(update, Apply));
}
/// <summary>
/// Adds an update to remove a field
/// </summary>
/// <param name="expr">The expresion defining the field to update</param>
public void Unset(Expression<Func<TDocument, object>> expr)
{
UpdateDefinition<TDocument> update = Builders<TDocument>.Update.Unset(expr);
void Apply(object target) => Unassign(target, expr.Body);
_fieldUpdates.Add(new FieldUpdate(update, Apply));
}
/// <summary>
/// Updates a property dictionary with a set of adds and removes
/// </summary>
/// <param name="expr">Lambda expression defining a field to update</param>
/// <param name="updates">List of updates</param>
public void UpdateDictionary<TKey, TValue>(Expression<Func<TDocument, Dictionary<TKey, TValue>>> expr, IEnumerable<KeyValuePair<TKey, TValue?>> updates) where TKey : class where TValue : class
{
PropertyInfo indexerProperty = Reflection<Dictionary<TKey, TValue>>.Indexer;
foreach (KeyValuePair<TKey, TValue?> update in updates)
{
MethodCallExpression index = Expression.Call(expr.Body, indexerProperty.GetMethod!, new[] { Expression.Constant(update.Key) });
if (update.Value == null)
{
Expression<Func<TDocument, object>> indexExpression = Expression.Lambda<Func<TDocument, object>>(index, expr.Parameters[0]);
Unset(indexExpression);
}
else
{
Expression<Func<TDocument, TValue>> indexExpression = Expression.Lambda<Func<TDocument, TValue>>(index, expr.Parameters[0]);
Set(indexExpression, update.Value);
}
}
}
/// <summary>
/// Applies this transaction to the given update
/// </summary>
/// <param name="target">The object to update</param>
public void ApplyTo(TDocument target)
{
foreach (FieldUpdate setting in _fieldUpdates)
{
setting._apply(target);
}
}
/// <summary>
/// Assign a value to a field
/// </summary>
/// <param name="target">The target object to be updated</param>
/// <param name="fieldExpression">Lambda indicating the field to update</param>
/// <param name="value">New value for the field</param>
static void Assign(object? target, LambdaExpression fieldExpression, object? value)
{
Expression body = fieldExpression.Body;
if (body.NodeType == ExpressionType.MemberAccess)
{
MemberExpression memberExpression = (MemberExpression)body;
object? @object = Evaluate(memberExpression.Expression!, target);
PropertyInfo propertyInfo = (PropertyInfo)memberExpression.Member;
propertyInfo.SetValue(@object, value);
}
else if (body.NodeType == ExpressionType.ArrayIndex)
{
BinaryExpression binaryExpression = (BinaryExpression)body;
System.Collections.IList list = (System.Collections.IList)Evaluate(binaryExpression.Left, target)!;
int index = (int)Evaluate(binaryExpression.Right, target)!;
list[index] = value;
}
else if (body.NodeType == ExpressionType.Call)
{
MethodCallExpression callExpression = (MethodCallExpression)body;
System.Collections.IDictionary dictionary = (System.Collections.IDictionary)Evaluate(callExpression.Object!, target)!;
object? key = Evaluate(callExpression.Arguments[0], null);
dictionary[key!] = value;
}
else if (body.NodeType == ExpressionType.Index)
{
IndexExpression indexExpression = (IndexExpression)body;
object? @object = Evaluate(indexExpression.Object!, target);
object?[] arguments = new object?[indexExpression.Arguments.Count];
for (int idx = 0; idx < indexExpression.Arguments.Count; idx++)
{
arguments[idx] = Evaluate(indexExpression.Arguments[idx], null);
}
indexExpression.Indexer!.SetValue(@object, value, arguments);
}
else
{
throw new NotImplementedException();
}
}
/// <summary>
/// Removes an entry from a dictionary
/// </summary>
/// <param name="target">The target object to be updated</param>
/// <param name="body">Body of the expression indicating the field to update</param>
static void Unassign(object? target, Expression body)
{
if (body.NodeType == ExpressionType.MemberAccess)
{
MemberExpression memberExpression = (MemberExpression)body;
switch (memberExpression.Member.MemberType)
{
case MemberTypes.Field:
((FieldInfo)memberExpression.Member).SetValue(target, null);
break;
case MemberTypes.Property:
((PropertyInfo)memberExpression.Member).SetValue(target, null);
break;
default:
throw new NotImplementedException();
}
}
else if (body.NodeType == ExpressionType.Call)
{
MethodCallExpression callExpression = (MethodCallExpression)body;
System.Collections.IDictionary? dictionary = (System.Collections.IDictionary?)Evaluate(callExpression.Object!, target);
object? key = Evaluate(callExpression.Arguments[0], null);
dictionary!.Remove(key!);
}
else if (body.NodeType == ExpressionType.Index)
{
IndexExpression indexExpression = (IndexExpression)body;
System.Collections.IDictionary? dictionary = (System.Collections.IDictionary?)Evaluate(indexExpression.Object!, target);
object? key = Evaluate(indexExpression.Arguments[0], null);
dictionary!.Remove(key!);
}
else if (body.NodeType == ExpressionType.Convert)
{
UnaryExpression unaryExpression = (UnaryExpression)body;
Unassign(target, unaryExpression.Operand);
}
else
{
throw new NotImplementedException();
}
}
/// <summary>
/// Evaluates an expression
/// </summary>
/// <param name="expression">The expression to evaluate</param>
/// <param name="parameter">Parameter to the unary lambda expression</param>
/// <returns>Value of the expression</returns>
static object? Evaluate(Expression expression, object? parameter)
{
if (expression.NodeType == ExpressionType.Call)
{
MethodCallExpression callExpression = (MethodCallExpression)expression;
object? @object = Evaluate(callExpression.Object!, parameter);
object?[] arguments = callExpression.Arguments.Select(x => Evaluate(x, parameter)).ToArray();
return callExpression.Method.Invoke(@object, arguments);
}
else if (expression.NodeType == ExpressionType.Constant)
{
ConstantExpression constantExpression = (ConstantExpression)expression;
return constantExpression.Value;
}
else if (expression.NodeType == ExpressionType.MemberAccess)
{
MemberExpression memberExpression = (MemberExpression)expression;
object? target = Evaluate(memberExpression.Expression!, parameter);
MemberInfo member = memberExpression.Member;
switch (member.MemberType)
{
case MemberTypes.Property:
return ((PropertyInfo)member).GetValue(target);
case MemberTypes.Field:
return ((FieldInfo)member).GetValue(target);
default:
throw new NotImplementedException("Unsupported expression type");
}
}
else if (expression.NodeType == ExpressionType.ArrayIndex)
{
BinaryExpression binaryExpression = (BinaryExpression)expression;
System.Collections.IList list = (System.Collections.IList)Evaluate(binaryExpression.Left, parameter)!;
int index = (int)Evaluate(binaryExpression.Right, null)!;
return list[index];
}
else if (expression.NodeType == ExpressionType.Parameter)
{
return parameter;
}
else
{
throw new NotImplementedException();
}
}
/// <summary>
/// Converts this transaction to an update definition
/// </summary>
public UpdateDefinition<TDocument> ToUpdateDefinition()
{
return Builders<TDocument>.Update.Combine(_fieldUpdates.Select(x => x._update));
}
}
}