/// <summary>
        /// Creates a new instance of the indicated type, populating it with the dictionary.
        /// Can use any constructor of the indicated type, provided that there are keys in the
        /// dictionary that correspond (case sensitive) to the names of the constructor parameters.
        /// </summary>
        /// <param name="source">The source of values.</param>
        /// <param name="type">The type to create.</param>
        /// <param name="mappedType">
        /// GraphType for matching dictionary keys with <paramref name="type"/> property names.
        /// GraphType contains information about this matching in Metadata property.
        /// In case of configuring field as Field(x => x.FName).Name("FirstName") source dictionary
        /// will have 'FirstName' key but its value should be set to 'FName' property of created object.
        /// </param>
        public static object ToObject(this IDictionary <string, object> source, Type type, IGraphType mappedType = null)
        {
            // Given Field(x => x.FName).Name("FirstName") and key == "FirstName" returns "FName"
            string GetPropertyName(string key, out FieldType field)
            {
                var complexType = mappedType.GetNamedType() as IComplexGraphType;

                // type may not contain mapping information
                field = complexType?.GetField(key);
                return(field?.GetMetadata(ComplexGraphType <object> .ORIGINAL_EXPRESSION_PROPERTY_NAME, key) ?? key);
            }

            // Returns keys from source that match constructor signature
            string[] MatchSourceKeys(ParameterInfo[] parameters)
            {
                // parameterless constructors are the most common use case
                if (parameters.Length == 0)
                {
                    return(Array.Empty <string>());
                }

                // otherwise we have to iterate over the parameters - worse performance but this is rather rare case
                List <string> keys = null;

                return(parameters.All(p => source.Any(keyValue =>
                {
                    bool matched = string.Equals(GetPropertyName(keyValue.Key, out var _), p.Name, StringComparison.InvariantCultureIgnoreCase);
                    if (matched)
                    {
                        (keys ??= new List <string>()).Add(keyValue.Key);
                    }
                    return matched;
                }))
                    ? keys.ToArray()
                    : null);
            }

            if (source == null)
            {
                throw new ArgumentNullException(nameof(source));
            }

            // force sourceType to be IDictionary<string, object>
            if (ValueConverter.TryConvertTo(source, type, out object result, typeof(IDictionary <string, object>)))
            {
                return(result);
            }

            // attempt to use the most specific constructor sorting in decreasing order of parameters number
            var ctorCandidates = _types.GetOrAdd(type, t => t.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).OrderByDescending(ctor => ctor.GetParameters().Length).ToArray());

            ConstructorInfo targetCtor = null;

            ParameterInfo[] ctorParameters = null;
            string[]        matchedKeys    = null;

            foreach (var ctor in ctorCandidates)
            {
                var parameters = ctor.GetParameters();
                matchedKeys = MatchSourceKeys(parameters);
                if (matchedKeys != null)
                {
                    targetCtor     = ctor;
                    ctorParameters = parameters;
                    break;
                }
            }

            if (targetCtor == null)
            {
                throw new ArgumentException($"Type '{type}' does not contain a constructor that could be used for current input arguments.", nameof(type));
            }

            object[] ctorArguments = ctorParameters.Length == 0 ? Array.Empty <object>() : new object[ctorParameters.Length];

            for (int i = 0; i < ctorParameters.Length; ++i)
            {
                object arg = GetPropertyValue(source[matchedKeys[i]], ctorParameters[i].ParameterType);
                ctorArguments[i] = arg;
            }

            object obj = targetCtor.Invoke(ctorArguments);

            foreach (var item in source)
            {
                // these parameters have already been used in the constructor, no need to set property
                if (matchedKeys.Length > 0 && matchedKeys.Any(k => k == item.Key))
                {
                    continue;
                }

                string       propertyName = GetPropertyName(item.Key, out var field);
                PropertyInfo propertyInfo = null;

                try
                {
                    propertyInfo = type.GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
                }
                catch (AmbiguousMatchException)
                {
                    propertyInfo = type.GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
                }

                if (propertyInfo?.CanWrite == true)
                {
                    object value = GetPropertyValue(item.Value, propertyInfo.PropertyType, field?.ResolvedType);
                    propertyInfo.SetValue(obj, value, null);   //issue: this works even if propertyInfo is ValueType and value is null
                }
            }

            return(obj);
        }
Example #2
0
 private static object ConvertValue(object value, Type targetType)
 {
     return(ValueConverter.ConvertTo(value, targetType));
 }
        /// <summary>
        /// Converts the indicated value into a type that is compatible with fieldType.
        /// </summary>
        /// <param name="propertyValue">The value to be converted.</param>
        /// <param name="fieldType">The desired type.</param>
        /// <param name="mappedType">
        /// GraphType for matching dictionary keys with <paramref name="type"/> property names.
        /// GraphType contains information about this matching in Metadata property.
        /// In case of configuring field as Field(x => x.FName).Name("FirstName") source dictionary
        /// will have 'FirstName' key but its value should be set to 'FName' property of created object.
        /// </param>
        /// <remarks>There is special handling for strings, IEnumerable&lt;T&gt;, Nullable&lt;T&gt;, and Enum.</remarks>
        public static object GetPropertyValue(this object propertyValue, Type fieldType, IGraphType mappedType = null)
        {
            // Short-circuit conversion if the property value already of the right type
            if (propertyValue == null || fieldType == typeof(object) || fieldType.IsInstanceOfType(propertyValue))
            {
                return(propertyValue);
            }

            if (ValueConverter.TryConvertTo(propertyValue, fieldType, out object result))
            {
                return(result);
            }

            var enumerableInterface = fieldType.Name == "IEnumerable`1"
              ? fieldType
              : fieldType.GetInterface("IEnumerable`1");

            if (fieldType != typeof(string) && enumerableInterface != null)
            {
                IList newCollection;
                var   elementType              = enumerableInterface.GetGenericArguments()[0];
                var   underlyingType           = Nullable.GetUnderlyingType(elementType) ?? elementType;
                bool  fieldTypeImplementsIList = fieldType.GetInterface("IList") != null;

                var propertyValueAsIList = propertyValue as IList;

                // Custom container
                if (fieldTypeImplementsIList && !fieldType.IsArray)
                {
                    newCollection = ( IList )Activator.CreateInstance(fieldType);
                }
                // Array of known size is created immediately
                else if (fieldType.IsArray && propertyValueAsIList != null)
                {
                    newCollection = Array.CreateInstance(elementType, propertyValueAsIList.Count);
                }
                // List<T>
                else
                {
                    var genericListType = typeof(List <>).MakeGenericType(elementType);
                    newCollection = ( IList )Activator.CreateInstance(genericListType);
                }

                if (!(propertyValue is IEnumerable valueList))
                {
                    return(newCollection);
                }

                // Array of known size is populated in-place
                if (fieldType.IsArray && propertyValueAsIList != null)
                {
                    for (int i = 0; i < propertyValueAsIList.Count; ++i)
                    {
                        object listItem = propertyValueAsIList[i];
                        newCollection[i] = listItem == null ? null : GetPropertyValue(listItem, underlyingType, mappedType);
                    }
                }
                // Array of unknown size is created only after populating list
                else
                {
                    foreach (object listItem in valueList)
                    {
                        _ = newCollection.Add(listItem == null ? null : GetPropertyValue(listItem, underlyingType, mappedType));
                    }

                    if (fieldType.IsArray)
                    {
                        newCollection = (( dynamic )newCollection).ToArray();
                    }
                }

                return(newCollection);
            }

            object value = propertyValue;

            var nullableFieldType = Nullable.GetUnderlyingType(fieldType);

            // if this is a nullable type and the value is null, return null
            if (nullableFieldType != null && value == null)
            {
                return(null);
            }

            if (nullableFieldType != null)
            {
                fieldType = nullableFieldType;
            }

            if (propertyValue is IDictionary <string, object> objects)
            {
                return(ToObject(objects, fieldType, mappedType));
            }

            if (fieldType.IsEnum)
            {
                if (value == null)
                {
                    string[] enumNames = Enum.GetNames(fieldType);
                    value = enumNames[0];
                }

                if (!IsDefinedEnumValue(fieldType, value))
                {
                    throw new ExecutionError($"Unknown value '{value}' for enum '{fieldType.Name}'.");
                }

                string str = value.ToString();
                value = Enum.Parse(fieldType, str, true);
            }

            return(ValueConverter.ConvertTo(value, fieldType));
        }