private static MergerConditions[] GetMergerPropertyMap(Type type)
        {
            if (type.GetCustomAttribute(typeof(MergableAttribute), true) == null)
            {
                throw new ObjectGraphMergerException($"Object type {type.Name} is not marked with the Mergable attribute.");
            }

            MergerConditions[] result = null;
            if (!knownMergerPropertyMaps.TryGetValue(type, out result))
            {
                // Generate the map for this type and cache it
                List <MergerConditions> conditions = new List <MergerConditions>();
                var mergableProperties             = type.GetProperties().Where(p => p.CanRead && p.CanWrite);
                foreach (var prop in mergableProperties)
                {
                    var policyAttrib = prop.GetCustomAttribute <MergePolicyAttribute>(true);
                    var policy       = policyAttrib?.Policy ?? MergePolicy.Default;
                    if (policy != MergePolicy.Ignore)
                    {
                        var condition = new MergerConditions {
                            Policy = policy
                        };
                        condition.EquivalentValues = ParseEquivalentValues(policyAttrib?.EquivalentValues);
                        if (typeof(IEnumerable).IsAssignableFrom(prop.PropertyType) && prop.PropertyType != typeof(string))
                        {
                            condition.IsCollection = true;
                            // Determine the class type contained by this collection
                            var collectionType = prop.PropertyType;
                            if (collectionType.IsGenericType)
                            {
                                condition.CollectionInnerType = collectionType.GenericTypeArguments.First();
                            }
                            else
                            {
                                condition.CollectionInnerType = collectionType.GetElementType();
                            }
                            condition.CollectionIdentifierPropertyName = condition.CollectionInnerType?.GetCustomAttribute <MergableAttribute>(true)?.CollectionIdentifier;
                            if (!string.IsNullOrEmpty(condition.CollectionIdentifierPropertyName))
                            {
                                condition.CollectionIdentifierProperty = condition.CollectionInnerType?.GetProperty(condition.CollectionIdentifierPropertyName);
                            }
                            condition.CollapseSingleItemMatchingName = condition.CollectionInnerType?.GetCustomAttribute <MergableAttribute>(true)?.CollapseSingleItemMatchingProperty;
                            if (!string.IsNullOrEmpty(condition.CollapseSingleItemMatchingName))
                            {
                                condition.CollapseSingleObjectMatchingProperty = condition.CollectionInnerType?.GetProperty(condition.CollapseSingleItemMatchingName);
                            }
                        }
                        condition.IsSimpleType = IsSimple(prop.PropertyType);
                        condition.Property     = prop;
                        conditions.Add(condition);
                    }
                }
                result = conditions.ToArray();
                knownMergerPropertyMaps.Add(type, result);
            }
            return(result);
        }
        private object CalculateMergedValueForMapping(MergerConditions mapping, IEnumerable <object> values)
        {
            if (!mapping.IsSimpleType)
            {
                throw new ObjectGraphMergerException("Attempted to calculate merged value for a non-value type map.");
            }

            switch (mapping.Policy)
            {
            case MergePolicy.EqualOrNull:
            case MergePolicy.Default:
            {
                // See if we need to do any mapping of values to their replacements.
                if (mapping.EquivalentValues.Any())
                {
                    object[] knownValues = values.ToArray();
                    for (int i = 0; i < knownValues.Length; i++)
                    {
                        object replacementValue;
                        if (mapping.EquivalentValues.TryGetValue(knownValues[i], out replacementValue))
                        {
                            knownValues[i] = replacementValue;
                        }
                    }

                    // Replace the enumerable collection with our modified version
                    values = knownValues;
                }

                object knownValue = values.FirstOrDefault(x => x != null);
                if (values.Any(x => x != null && !x.Equals(knownValue)))
                {
                    if (IsSimple(knownValue.GetType()))
                    {
                        Console.WriteLine($"Unable to merge values for {mapping.Property.DeclaringType.Name}.{mapping.Property.Name} because values are not equal or null. Values: {values.Select(x => x.ToString()).ComponentsJoinedByString(",")}.");
                        return($"MERGE INCONSISTENCY: {values.Select(x => x.ToString()).ComponentsJoinedByString(",")}");
                    }
                    else
                    {
                        throw new InvalidOperationException($"Unable to merge values for {mapping.Property.DeclaringType.Name}.{mapping.Property.Name} because values are not equal or null. Values: {values.Select(x => x.ToString()).ComponentsJoinedByString(",")}.");
                    }
                }
                return(knownValue);
            }

            case MergePolicy.PreferGreaterValue:
                return(values.OrderByDescending(x => x).FirstOrDefault());

            case MergePolicy.PreferLesserValue:
                return(values.OrderBy(x => x).FirstOrDefault());

            default:
                throw new NotImplementedException($"Unsupported merge policy value: {mapping.Policy}.");
            }
        }
        private IEnumerable <object> DedupeMembersInCollection(MergerConditions mapping, IEnumerable <NodeToMerge> members)
        {
            if (!members.Any())
            {
                return(new object[0]);
            }

            Dictionary <object, object> equivelentKeyValues = new Dictionary <object, object>();
            var policyAttributeOnIdentifier = mapping?.CollectionIdentifierProperty?.GetCustomAttribute <MergePolicyAttribute>();

            if (null != policyAttributeOnIdentifier)
            {
                equivelentKeyValues = ParseEquivalentValues(policyAttributeOnIdentifier.EquivalentValues);
            }

            if (null == mapping.CollectionIdentifierProperty)
            {
                throw new ObjectGraphMergerException($"Missing a collection identifier for class {mapping.CollectionInnerType.Name} referenced from {mapping.Property.DeclaringType.Name}.{mapping.Property.Name}.");
            }

            Dictionary <string, List <NodeToMerge> > uniqueMembers = new Dictionary <string, List <NodeToMerge> >();

            // First pass, organize everything by the collapse single object matching property, if it exists
            Dictionary <string, List <NodeToMerge> > matchingMembers = new Dictionary <string, List <NodeToMerge> >();

            foreach (var obj in members)
            {
                if (mapping.CollapseSingleObjectMatchingProperty != null)
                {
                    string collapsingKey = (string)mapping.CollapseSingleObjectMatchingProperty.GetValue(obj.Node);
                    if (!string.IsNullOrEmpty(collapsingKey))
                    {
                        matchingMembers.AddToList(collapsingKey, obj);
                    }
                }
                else
                {
                    string key = (string)mapping.CollectionIdentifierProperty.GetValue(obj.Node);
                    object newKey;
                    if (equivelentKeyValues != null && equivelentKeyValues.TryGetValue(key, out newKey))
                    {
                        key = (string)newKey;
                    }
                    matchingMembers.AddToList(key, obj);
                }
            }

            foreach (var match in matchingMembers)
            {
                // Special case, the same object is defined in two different sources only once.
                if (match.Value.Count == 2 && match.Value[0].Source != match.Value[1].Source)
                {
                    // Single instance of a member, with different sources, means we should treat them as a single mergable member
                    uniqueMembers.AddToList(match.Key, match.Value[0], match.Value[1]);
                }
                else
                {
                    // Reindex these nodes using their element identifier instead of the collapsing element identifier
                    foreach (var matchedNode in match.Value)
                    {
                        string key = (string)mapping.CollectionIdentifierProperty.GetValue(matchedNode.Node);
                        uniqueMembers.AddToList(key, matchedNode);
                    }
                }
            }

            // Merge any elements where a duplicate key exists
            foreach (var key in uniqueMembers)
            {
                if (key.Value.Count > 1)
                {
                    var mergedNode = MergeNodes(key.Value.ToArray());
                    key.Value.Clear();
                    key.Value.Add(new NodeToMerge {
                        Source = "merger", Node = mergedNode
                    });
                }
            }

            // Return the single value from each key member
            return(from m in uniqueMembers select m.Value.Single().Node);
        }