internal static IEnumerable <(MethodInfo MethodInfo, object[] Args)> BindMethodInfos(IEnumerable <MethodInfo> methodInfos, object[] args)
 {
     return(methodInfos
            .Select(
                m => (
                    MethodInfo: m,
                    Args: BindingHelper.TryBindParameters(m, args, out var argsOut) ?
                    argsOut : null
                    )
                ).Where(x => x.Args != null));
 }
 /// <summary>
 /// Attempts to convert the given value to a different type. Some of this
 /// conversion is specific to ARM Templates (e.g. string -> char), and
 /// some of it is a (very) limited version of what the C# compiler does.
 /// </summary>
 /// <param name="originalValue"></param>
 /// <param name="desiredType"></param>
 /// <param name="convertedValue"></param>
 /// <returns></returns>
 internal static bool TryConvertValue(object originalValue, Type desiredType, out object convertedValue)
 {
     if (originalValue == null)
     {
         convertedValue = originalValue;
         return(true);
     }
     if (desiredType.IsAssignableFrom(originalValue.GetType()))
     {
         // it's already the desired type, so no conversion necessary
         convertedValue = originalValue;
         return(true);
     }
     if (desiredType == typeof(object))
     {
         // value types return false for IsAssignablFrom()
         // but we can still box them into objects
         convertedValue = originalValue;
         return(true);
     }
     // ARM templates don't support a char type - everything is a string,
     // so if a parameter is a char and the argument is a string consisting
     // of a single char then we'll convert it to a char
     if (desiredType == typeof(char))
     {
         if ((originalValue is string stringArg) && (stringArg.Length == 1))
         {
             convertedValue = stringArg[0];
             return(true);
         }
     }
     // can we cast arrays? e.g. object[] -> string[]
     // see https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/covariance-contravariance/
     if (originalValue.GetType().IsArray&& desiredType.IsArray)
     {
         if (BindingHelper.TryConvertArray((Array)originalValue, desiredType.GetElementType(), out var convertedArray))
         {
             convertedValue = convertedArray;
             return(true);
         }
     }
     convertedValue = null;
     return(false);
 }
        /// <summary>
        /// A poor-mans's contravariance / covariance implementation. Given an
        /// array of one type of element, TryConvertArray attempts to convert
        /// it into an array of another compatible type.
        ///
        /// For example, given a string[] we can convert it into an object[].
        /// And, given an object[] where all the items are strings, we can
        /// convert it into a string[].
        /// </summary>
        /// <param name="originalArray"></param>
        /// <param name="elementType"></param>
        /// <param name="convertedArray"></param>
        /// <returns></returns>
        /// <remarks>
        /// See https://docs.microsoft.com/en-us/dotnet/standard/generics/covariance-and-contravariance#:~:text=Covariance%20and%20contravariance%20are%20terms,assigning%20and%20using%20generic%20types.
        /// </remarks>
        internal static bool TryConvertArray(Array originalArray, Type elementType, out object convertedArray)
        {
            var tmpArray = Array.CreateInstance(elementType, originalArray.Length);

            for (var index = 0; index < originalArray.Length; index++)
            {
                if (BindingHelper.TryConvertValue(originalArray.GetValue(index), elementType, out var convertedValue))
                {
                    tmpArray.SetValue(convertedValue, index);
                }
                else
                {
                    convertedArray = null;
                    return(false);
                }
            }
            convertedArray = tmpArray;
            return(true);
        }
        /// <summary>
        /// Tries to prepare a list of arguments so that they can be used as parameters to
        /// invoke a MethodInfo with. Returns true if the arguments can be used with the
        /// MethodInfo, otherwise returns false.
        /// </summary>
        /// <param name="methodInfo"></param>
        /// <param name="argsIn"></param>
        /// <param name="argsOut"></param>
        /// <returns>
        /// Given a MethodInfo and a list of arguments, this function checks if the arguments satisfy the
        /// type signature of the method and returns true if they do, otherwise returns false.
        ///
        /// If the arguments can be modified to match the type signature of the method then the argsOut
        /// parameter will contain the modified arguments. If the arguments do not need to be modified
        /// then argsOut will contain a copy of the original argsIn.
        ///
        /// Modifications that are applied if appropriate are:
        ///
        /// + Adding a parameter's default value (if it has one) where an argument is missing
        /// + Casting the value of arguments to match the type of a parameter where a cast makes sense
        /// + Converting multiple "params" argument values into a single array-valued argument
        /// + Converting array arguments where the element type needs to be different
        ///
        /// </returns>
        internal static bool TryBindParameters(MethodInfo methodInfo, object[] argsIn, out object[] argsOut)
        {
            if (methodInfo == null)
            {
                throw new ArgumentNullException(nameof(methodInfo));
            }

            var parameters = methodInfo.GetParameters();
            var newArgs    = new List <object>(argsIn);

            var parameterIndex = 0;

            while (parameterIndex < parameters.Length)
            {
                var parameter = parameters[parameterIndex];

                // check if this is a "params" parameter with a dynamic number of arguments
                var paramsAttribute = parameter.GetCustomAttribute(typeof(ParamArrayAttribute));
                if (paramsAttribute != null)
                {
                    if (parameterIndex != (parameters.Length - 1))
                    {
                        throw new InvalidOperationException("params must be the last parameter.");
                    }

                    // convert the params array
                    if (
                        !BindingHelper.TryConvertArray(
                            argsIn.Skip(parameterIndex).ToArray(),
                            parameter.ParameterType.GetElementType(),
                            out var convertedArray
                            )
                        )
                    {
                        argsOut = null;
                        return(false);
                    }

                    // replace the multiple "params" args with the params array
                    newArgs = newArgs.Take(parameterIndex).ToList();
                    newArgs.Add(convertedArray);
                }
                else if (newArgs.Count <= parameterIndex)
                {
                    // if there's no argument for this parameter we can
                    // see if it has a default value and use that
                    if (parameter.HasDefaultValue)
                    {
                        newArgs.Add(parameter.DefaultValue);
                    }
                    else
                    {
                        argsOut = null;
                        return(false);
                    }
                }
                else if (BindingHelper.TryConvertValue(newArgs[parameterIndex], parameter.ParameterType, out var convertedArg))
                {
                    // try to convert any parameters that don't quite match the required type
                    newArgs[parameterIndex] = convertedArg;
                }
                else
                {
                    // we couldn't convert the argument type to match the parameter type
                    argsOut = null;
                    return(false);
                }

                parameterIndex++;
            }

            // we've processed all the method parameters and arguments, but
            // do we have the same number of each now? (i.e. were there the
            // right number of arguments in the attempted call to the mathod?)
            if (newArgs.Count != parameters.Length)
            {
                argsOut = null;
                return(false);
            }

            // everything look ok
            argsOut = newArgs.ToArray();
            return(true);
        }