// 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)); } } }