using System.Linq.Expressions; using System.Reflection; #nullable disable namespace Tabletop.Core { /// /// Superfast deep copier class, which uses Expression trees. /// public static class DeepCopyByExpressionTrees { private static readonly object _isStructTypeToDeepCopyDictionaryLocker = new(); private static Dictionary _isStructTypeToDeepCopyDictionary = []; private static readonly object _compiledCopyFunctionsDictionaryLocker = new(); private static Dictionary, object>> _compiledCopyFunctionsDictionary = []; private static readonly Type _objectType = typeof(object); private static readonly Type _objectDictionaryType = typeof(Dictionary); /// /// Creates a deep copy of an object. /// /// Object type. /// Object to copy. /// Dictionary of already copied objects (Keys: original objects, Values: their copies). /// public static T DeepCopyByExpressionTree(this T original, Dictionary copiedReferencesDict = null) { return (T)DeepCopyByExpressionTreeObj(original, false, copiedReferencesDict ?? new Dictionary(new ReferenceEqualityComparer())); } private static object DeepCopyByExpressionTreeObj(object original, bool forceDeepCopy, Dictionary copiedReferencesDict) { if (original == null) { return null; } var type = original.GetType(); if (IsDelegate(type)) { return null; } if (!forceDeepCopy && !IsTypeToDeepCopy(type)) { return original; } if (copiedReferencesDict.TryGetValue(original, out object alreadyCopiedObject)) { return alreadyCopiedObject; } if (type == _objectType) { return new object(); } var compiledCopyFunction = GetOrCreateCompiledLambdaCopyFunction(type); object copy = compiledCopyFunction(original, copiedReferencesDict); return copy; } private static Func, object> GetOrCreateCompiledLambdaCopyFunction(Type type) { // The following structure ensures that multiple threads can use the dictionary // even while dictionary is locked and being updated by other thread. // That is why we do not modify the old dictionary instance but // we replace it with a new instance everytime. if (!_compiledCopyFunctionsDictionary.TryGetValue(type, out Func, object> compiledCopyFunction)) { lock (_compiledCopyFunctionsDictionaryLocker) { if (!_compiledCopyFunctionsDictionary.TryGetValue(type, out compiledCopyFunction)) { var uncompiledCopyFunction = CreateCompiledLambdaCopyFunctionForType(type); compiledCopyFunction = uncompiledCopyFunction.Compile(); var dictionaryCopy = _compiledCopyFunctionsDictionary.ToDictionary(pair => pair.Key, pair => pair.Value); dictionaryCopy.Add(type, compiledCopyFunction); _compiledCopyFunctionsDictionary = dictionaryCopy; } } } return compiledCopyFunction; } private static Expression, object>> CreateCompiledLambdaCopyFunctionForType(Type type) { ///// INITIALIZATION OF EXPRESSIONS AND VARIABLES InitializeExpressions(type, out ParameterExpression inputParameter, out ParameterExpression inputDictionary, out ParameterExpression outputVariable, out ParameterExpression boxingVariable, out LabelTarget endLabel, out List variables, out List expressions); ///// RETURN NULL IF ORIGINAL IS NULL IfNullThenReturnNullExpression(inputParameter, endLabel, expressions); ///// MEMBERWISE CLONE ORIGINAL OBJECT MemberwiseCloneInputToOutputExpression(type, inputParameter, outputVariable, expressions); ///// STORE COPIED OBJECT TO REFERENCES DICTIONARY if (IsClassOtherThanString(type)) { StoreReferencesIntoDictionaryExpression(inputParameter, inputDictionary, outputVariable, expressions); } ///// COPY ALL NONVALUE OR NONPRIMITIVE FIELDS FieldsCopyExpressions(type, inputParameter, inputDictionary, outputVariable, boxingVariable, expressions); ///// COPY ELEMENTS OF ARRAY if (IsArray(type) && IsTypeToDeepCopy(type.GetElementType())) { CreateArrayCopyLoopExpression(type, inputParameter, inputDictionary, outputVariable, variables, expressions); } ///// COMBINE ALL EXPRESSIONS INTO LAMBDA FUNCTION var lambda = CombineAllIntoLambdaFunctionExpression(inputParameter, inputDictionary, outputVariable, endLabel, variables, expressions); return lambda; } private static void InitializeExpressions(Type type, out ParameterExpression inputParameter, out ParameterExpression inputDictionary, out ParameterExpression outputVariable, out ParameterExpression boxingVariable, out LabelTarget endLabel, out List variables, out List expressions) { inputParameter = Expression.Parameter(_objectType); inputDictionary = Expression.Parameter(_objectDictionaryType); outputVariable = Expression.Variable(type); boxingVariable = Expression.Variable(_objectType); endLabel = Expression.Label(); variables = []; expressions = []; variables.Add(outputVariable); variables.Add(boxingVariable); } private static void IfNullThenReturnNullExpression(ParameterExpression inputParameter, LabelTarget endLabel, List expressions) { ///// Intended code: ///// ///// if (input == null) ///// { ///// return null; ///// } var ifNullThenReturnNullExpression = Expression.IfThen( Expression.Equal( inputParameter, Expression.Constant(null, _objectType)), Expression.Return(endLabel)); expressions.Add(ifNullThenReturnNullExpression); } private static void MemberwiseCloneInputToOutputExpression( Type type, ParameterExpression inputParameter, ParameterExpression outputVariable, List expressions) { ///// Intended code: ///// ///// var output = ()input.MemberwiseClone(); var memberwiseCloneMethod = _objectType.GetMethod("MemberwiseClone", BindingFlags.NonPublic | BindingFlags.Instance); var memberwiseCloneInputExpression = Expression.Assign( outputVariable, Expression.Convert( Expression.Call( inputParameter, memberwiseCloneMethod), type)); expressions.Add(memberwiseCloneInputExpression); } private static void StoreReferencesIntoDictionaryExpression(ParameterExpression inputParameter, ParameterExpression inputDictionary, ParameterExpression outputVariable, List expressions) { ///// Intended code: ///// ///// inputDictionary[(Object)input] = (Object)output; var storeReferencesExpression = Expression.Assign( Expression.Property( inputDictionary, _objectDictionaryType.GetProperty("Item"), inputParameter), Expression.Convert(outputVariable, _objectType)); expressions.Add(storeReferencesExpression); } private static Expression, object>> CombineAllIntoLambdaFunctionExpression( ParameterExpression inputParameter, ParameterExpression inputDictionary, ParameterExpression outputVariable, LabelTarget endLabel, List variables, List expressions) { expressions.Add(Expression.Label(endLabel)); expressions.Add(Expression.Convert(outputVariable, _objectType)); var finalBody = Expression.Block(variables, expressions); var lambda = Expression.Lambda, object>>(finalBody, inputParameter, inputDictionary); return lambda; } private static void CreateArrayCopyLoopExpression(Type type, ParameterExpression inputParameter, ParameterExpression inputDictionary, ParameterExpression outputVariable, List variables, List expressions) { ///// Intended code: ///// ///// int i1, i2, ..., in; ///// ///// int length1 = inputarray.GetLength(0); ///// i1 = 0; ///// while (true) ///// { ///// if (i1 >= length1) ///// { ///// goto ENDLABELFORLOOP1; ///// } ///// int length2 = inputarray.GetLength(1); ///// i2 = 0; ///// while (true) ///// { ///// if (i2 >= length2) ///// { ///// goto ENDLABELFORLOOP2; ///// } ///// ... ///// ... ///// ... ///// int lengthn = inputarray.GetLength(n); ///// in = 0; ///// while (true) ///// { ///// if (in >= lengthn) ///// { ///// goto ENDLABELFORLOOPn; ///// } ///// outputarray[i1, i2, ..., in] ///// = ()DeepCopyByExpressionTreeObj( ///// (Object)inputarray[i1, i2, ..., in]) ///// in++; ///// } ///// ENDLABELFORLOOPn: ///// ... ///// ... ///// ... ///// i2++; ///// } ///// ENDLABELFORLOOP2: ///// i1++; ///// } ///// ENDLABELFORLOOP1: var rank = type.GetArrayRank(); var indices = GenerateIndices(rank); variables.AddRange(indices); var elementType = type.GetElementType(); var assignExpression = ArrayFieldToArrayFieldAssignExpression(inputParameter, inputDictionary, outputVariable, elementType, type, indices); Expression forExpression = assignExpression; for (int dimension = 0; dimension < rank; dimension++) { var indexVariable = indices[dimension]; forExpression = LoopIntoLoopExpression(inputParameter, indexVariable, forExpression, dimension); } expressions.Add(forExpression); } private static List GenerateIndices(int arrayRank) { ///// Intended code: ///// ///// int i1, i2, ..., in; var indices = new List(); for (int i = 0; i < arrayRank; i++) { var indexVariable = Expression.Variable(typeof(int)); indices.Add(indexVariable); } return indices; } private static BinaryExpression ArrayFieldToArrayFieldAssignExpression( ParameterExpression inputParameter, ParameterExpression inputDictionary, ParameterExpression outputVariable, Type elementType, Type arrayType, List indices) { ///// Intended code: ///// ///// outputarray[i1, i2, ..., in] ///// = ()DeepCopyByExpressionTreeObj( ///// (Object)inputarray[i1, i2, ..., in]); var indexTo = Expression.ArrayAccess(outputVariable, indices); var indexFrom = Expression.ArrayIndex(Expression.Convert(inputParameter, arrayType), indices); var forceDeepCopy = elementType != _objectType; var rightSide = Expression.Convert( Expression.Call( _deepCopyByExpressionTreeObjMethod, Expression.Convert(indexFrom, _objectType), Expression.Constant(forceDeepCopy, typeof(bool)), inputDictionary), elementType); var assignExpression = Expression.Assign(indexTo, rightSide); return assignExpression; } private static BlockExpression LoopIntoLoopExpression( ParameterExpression inputParameter, ParameterExpression indexVariable, Expression loopToEncapsulate, int dimension) { ///// Intended code: ///// ///// int length = inputarray.GetLength(dimension); ///// i = 0; ///// while (true) ///// { ///// if (i >= length) ///// { ///// goto ENDLABELFORLOOP; ///// } ///// loopToEncapsulate; ///// i++; ///// } ///// ENDLABELFORLOOP: var lengthVariable = Expression.Variable(typeof(int)); var endLabelForThisLoop = Expression.Label(); var newLoop = Expression.Loop( Expression.Block( [], Expression.IfThen( Expression.GreaterThanOrEqual(indexVariable, lengthVariable), Expression.Break(endLabelForThisLoop)), loopToEncapsulate, Expression.PostIncrementAssign(indexVariable)), endLabelForThisLoop); var lengthAssignment = GetLengthForDimensionExpression(lengthVariable, inputParameter, dimension); var indexAssignment = Expression.Assign(indexVariable, Expression.Constant(0)); return Expression.Block( [lengthVariable], lengthAssignment, indexAssignment, newLoop); } private static BinaryExpression GetLengthForDimensionExpression( ParameterExpression lengthVariable, ParameterExpression inputParameter, int i) { ///// Intended code: ///// ///// length = ((Array)input).GetLength(i); var getLengthMethod = typeof(Array).GetMethod("GetLength", BindingFlags.Public | BindingFlags.Instance); var dimensionConstant = Expression.Constant(i); return Expression.Assign( lengthVariable, Expression.Call( Expression.Convert(inputParameter, typeof(Array)), getLengthMethod, [dimensionConstant])); } private static void FieldsCopyExpressions(Type type, ParameterExpression inputParameter, ParameterExpression inputDictionary, ParameterExpression outputVariable, ParameterExpression boxingVariable, List expressions) { var fields = GetAllRelevantFields(type); var readonlyFields = fields.Where(f => f.IsInitOnly).ToList(); var writableFields = fields.Where(f => !f.IsInitOnly).ToList(); ///// READONLY FIELDS COPY (with boxing) bool shouldUseBoxing = readonlyFields.Count != 0; if (shouldUseBoxing) { var boxingExpression = Expression.Assign(boxingVariable, Expression.Convert(outputVariable, _objectType)); expressions.Add(boxingExpression); } foreach (var field in readonlyFields) { if (IsDelegate(field.FieldType)) { ReadonlyFieldToNullExpression(field, boxingVariable, expressions); } else { ReadonlyFieldCopyExpression(type, field, inputParameter, inputDictionary, boxingVariable, expressions); } } if (shouldUseBoxing) { var unboxingExpression = Expression.Assign(outputVariable, Expression.Convert(boxingVariable, type)); expressions.Add(unboxingExpression); } ///// NOT-READONLY FIELDS COPY foreach (var field in writableFields) { if (IsDelegate(field.FieldType)) { WritableFieldToNullExpression(field, outputVariable, expressions); } else { WritableFieldCopyExpression(type, field, inputParameter, inputDictionary, outputVariable, expressions); } } } private static FieldInfo[] GetAllRelevantFields(Type type, bool forceAllFields = false) { var fieldsList = new List(); var typeCache = type; while (typeCache != null) { fieldsList.AddRange( typeCache .GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy) .Where(field => forceAllFields || IsTypeToDeepCopy(field.FieldType))); typeCache = typeCache.BaseType; } return [.. fieldsList]; } private static FieldInfo[] GetAllFields(Type type) { return GetAllRelevantFields(type, forceAllFields: true); } private static readonly Type _fieldInfoType = typeof(FieldInfo); private static readonly MethodInfo _setValueMethod = _fieldInfoType.GetMethod("SetValue", [_objectType, _objectType]); private static void ReadonlyFieldToNullExpression(FieldInfo field, ParameterExpression boxingVariable, List expressions) { // This option must be implemented by Reflection because of the following: // https://visualstudio.uservoice.com/forums/121579-visual-studio-2015/suggestions/2727812-allow-expression-assign-to-set-readonly-struct-f ///// Intended code: ///// ///// fieldInfo.SetValue(boxing, null); var fieldToNullExpression = Expression.Call( Expression.Constant(field), _setValueMethod, boxingVariable, Expression.Constant(null, field.FieldType)); expressions.Add(fieldToNullExpression); } private static readonly Type _thisType = typeof(DeepCopyByExpressionTrees); private static readonly MethodInfo _deepCopyByExpressionTreeObjMethod = _thisType.GetMethod("DeepCopyByExpressionTreeObj", BindingFlags.NonPublic | BindingFlags.Static); private static void ReadonlyFieldCopyExpression(Type type, FieldInfo field, ParameterExpression inputParameter, ParameterExpression inputDictionary, ParameterExpression boxingVariable, List expressions) { // This option must be implemented by Reflection (SetValueMethod) because of the following: // https://visualstudio.uservoice.com/forums/121579-visual-studio-2015/suggestions/2727812-allow-expression-assign-to-set-readonly-struct-f ///// Intended code: ///// ///// fieldInfo.SetValue(boxing, DeepCopyByExpressionTreeObj((Object)(()input).)) var fieldFrom = Expression.Field(Expression.Convert(inputParameter, type), field); var forceDeepCopy = field.FieldType != _objectType; var fieldDeepCopyExpression = Expression.Call( Expression.Constant(field, _fieldInfoType), _setValueMethod, boxingVariable, Expression.Call( _deepCopyByExpressionTreeObjMethod, Expression.Convert(fieldFrom, _objectType), Expression.Constant(forceDeepCopy, typeof(bool)), inputDictionary)); expressions.Add(fieldDeepCopyExpression); } private static void WritableFieldToNullExpression(FieldInfo field, ParameterExpression outputVariable, List expressions) { ///// Intended code: ///// ///// output. = ()null; var fieldTo = Expression.Field(outputVariable, field); var fieldToNullExpression = Expression.Assign( fieldTo, Expression.Constant(null, field.FieldType)); expressions.Add(fieldToNullExpression); } private static void WritableFieldCopyExpression(Type type, FieldInfo field, ParameterExpression inputParameter, ParameterExpression inputDictionary, ParameterExpression outputVariable, List expressions) { ///// Intended code: ///// ///// output. = ()DeepCopyByExpressionTreeObj((Object)(()input).); var fieldFrom = Expression.Field(Expression.Convert(inputParameter, type), field); var fieldType = field.FieldType; var fieldTo = Expression.Field(outputVariable, field); var forceDeepCopy = field.FieldType != _objectType; var fieldDeepCopyExpression = Expression.Assign( fieldTo, Expression.Convert( Expression.Call( _deepCopyByExpressionTreeObjMethod, Expression.Convert(fieldFrom, _objectType), Expression.Constant(forceDeepCopy, typeof(bool)), inputDictionary), fieldType)); expressions.Add(fieldDeepCopyExpression); } private static bool IsArray(Type type) { return type.IsArray; } private static bool IsDelegate(Type type) { return typeof(Delegate).IsAssignableFrom(type); } private static bool IsTypeToDeepCopy(Type type) { return IsClassOtherThanString(type) || IsStructWhichNeedsDeepCopy(type); } private static bool IsClassOtherThanString(Type type) { return !type.IsValueType && type != typeof(string); } private static bool IsStructWhichNeedsDeepCopy(Type type) { // The following structure ensures that multiple threads can use the dictionary // even while dictionary is locked and being updated by other thread. // That is why we do not modify the old dictionary instance but // we replace it with a new instance everytime. if (!_isStructTypeToDeepCopyDictionary.TryGetValue(type, out bool isStructTypeToDeepCopy)) { lock (_isStructTypeToDeepCopyDictionaryLocker) { if (!_isStructTypeToDeepCopyDictionary.TryGetValue(type, out isStructTypeToDeepCopy)) { isStructTypeToDeepCopy = IsStructWhichNeedsDeepCopy_NoDictionaryUsed(type); var newDictionary = _isStructTypeToDeepCopyDictionary.ToDictionary(pair => pair.Key, pair => pair.Value); newDictionary[type] = isStructTypeToDeepCopy; _isStructTypeToDeepCopyDictionary = newDictionary; } } } return isStructTypeToDeepCopy; } private static bool IsStructWhichNeedsDeepCopy_NoDictionaryUsed(Type type) { return IsStructOtherThanBasicValueTypes(type) && HasInItsHierarchyFieldsWithClasses(type); } private static bool IsStructOtherThanBasicValueTypes(Type type) { return type.IsValueType && !type.IsPrimitive && !type.IsEnum && type != typeof(decimal); } private static bool HasInItsHierarchyFieldsWithClasses(Type type, HashSet alreadyCheckedTypes = null) { alreadyCheckedTypes ??= []; alreadyCheckedTypes.Add(type); var allFields = GetAllFields(type); var allFieldTypes = allFields.Select(f => f.FieldType).Distinct().ToList(); var hasFieldsWithClasses = allFieldTypes.Any(IsClassOtherThanString); if (hasFieldsWithClasses) { return true; } var notBasicStructsTypes = allFieldTypes.Where(IsStructOtherThanBasicValueTypes).ToList(); var typesToCheck = notBasicStructsTypes.Where(t => !alreadyCheckedTypes.Contains(t)).ToList(); foreach (var typeToCheck in typesToCheck) { if (HasInItsHierarchyFieldsWithClasses(typeToCheck, alreadyCheckedTypes)) { return true; } } return false; } public class ReferenceEqualityComparer : EqualityComparer { public override bool Equals(object x, object y) { return ReferenceEquals(x, y); } public override int GetHashCode(object obj) { if (obj == null) { return 0; } return obj.GetHashCode(); } } } }