// 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
{
///
/// Allows building a transactional MongoDB update of several fields, and applying those to an in-memory object. Useful for transactional updates.
///
/// The type of document to update
public class TransactionBuilder where TDocument : class
{
///
/// Information about an update to a field
///
class FieldUpdate
{
///
/// The update definition
///
public UpdateDefinition _update;
///
/// Applies an update to the given object
///
public Action _apply;
///
/// Constructor
///
/// The update definition
/// Applies the update to a given object
public FieldUpdate(UpdateDefinition update, Action apply)
{
_update = update;
_apply = apply;
}
}
///
/// Helper class for determining the indexer for a particular type
///
/// The type to find the indexer for
static class Reflection
{
///
/// Caches the indexer for this type
///
public static PropertyInfo Indexer { get; } = GetIndexerProperty();
///
/// Finds the indexer property from a type
///
/// The property info
static PropertyInfo GetIndexerProperty()
{
foreach (PropertyInfo property in typeof(T).GetProperties())
{
if (property.GetIndexParameters().Length > 0)
{
return property;
}
}
return null!;
}
}
///
/// List of field updates
///
readonly List _fieldUpdates = new List();
///
/// Whether the transaction is currenty empty
///
public bool IsEmpty => _fieldUpdates.Count == 0;
///
/// Adds a setting to this transaction
///
/// Type of the field to be updated
/// The expresion defining the field to update
/// New value for the field
public void Set(Expression> expr, TField value)
{
UpdateDefinition update = Builders.Update.Set(expr, value);
void Apply(object target) => Assign(target, expr, value);
_fieldUpdates.Add(new FieldUpdate(update, Apply));
}
///
/// Adds an update to remove a field
///
/// The expresion defining the field to update
public void Unset(Expression> expr)
{
UpdateDefinition update = Builders.Update.Unset(expr);
void Apply(object target) => Unassign(target, expr.Body);
_fieldUpdates.Add(new FieldUpdate(update, Apply));
}
///
/// Updates a property dictionary with a set of adds and removes
///
/// Lambda expression defining a field to update
/// List of updates
public void UpdateDictionary(Expression>> expr, IEnumerable> updates) where TKey : class where TValue : class
{
PropertyInfo indexerProperty = Reflection>.Indexer;
foreach (KeyValuePair update in updates)
{
MethodCallExpression index = Expression.Call(expr.Body, indexerProperty.GetMethod!, new[] { Expression.Constant(update.Key) });
if (update.Value == null)
{
Expression> indexExpression = Expression.Lambda>(index, expr.Parameters[0]);
Unset(indexExpression);
}
else
{
Expression> indexExpression = Expression.Lambda>(index, expr.Parameters[0]);
Set(indexExpression, update.Value);
}
}
}
///
/// Applies this transaction to the given update
///
/// The object to update
public void ApplyTo(TDocument target)
{
foreach (FieldUpdate setting in _fieldUpdates)
{
setting._apply(target);
}
}
///
/// Assign a value to a field
///
/// The target object to be updated
/// Lambda indicating the field to update
/// New value for the field
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();
}
}
///
/// Removes an entry from a dictionary
///
/// The target object to be updated
/// Body of the expression indicating the field to update
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();
}
}
///
/// Evaluates an expression
///
/// The expression to evaluate
/// Parameter to the unary lambda expression
/// Value of the expression
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();
}
}
///
/// Converts this transaction to an update definition
///
public UpdateDefinition ToUpdateDefinition()
{
return Builders.Update.Combine(_fieldUpdates.Select(x => x._update));
}
}
}