/// <summary> /// Modifies RegistrationEntries which have [ExcelMapArrayFunction], /// converting IEnumerable parameters to and from Excel Ranges (i.e. object[,]). /// This allows idiomatic .NET functions (which use sequences and lists) to be used as UDFs. /// /// Supports the use of Excel Array formulae where a UDF returns an enumerable. /// /// 1-dimensional Excel arrays are mapped automatically to/from IEnumerable. /// 2-dimensional Excel arrays can be mapped to a single function parameter with /// [ExcelMapPropertiesToColumnHeaders]. /// </summary> public static IEnumerable <ExcelFunctionRegistration> ProcessMapArrayFunctions( this IEnumerable <ExcelFunctionRegistration> registrations, ParameterConversionConfiguration config = null) { foreach (var reg in registrations) { if (!(reg.FunctionAttribute is ExcelMapArrayFunctionAttribute)) { // Not considered at all yield return(reg); continue; } try { var inputShimParameters = reg.FunctionLambda.Parameters.ZipSameLengths(reg.ParameterRegistrations, (p, r) => new ShimParameter(p.Type, r, config)).ToList(); var resultShimParameter = new ShimParameter(reg.FunctionLambda.ReturnType, reg.ReturnRegistration, config); // create the shim function as a lambda, using reflection LambdaExpression shim = MakeObjectArrayShim( reg.FunctionLambda, inputShimParameters, resultShimParameter); // create a description of the function, with a list of the output fields string functionDescription = "Returns " + resultShimParameter.HelpString; // create a description of each parameter, with a list of the input fields var parameterDescriptions = inputShimParameters.Select(shimParameter => "Input " + shimParameter.HelpString).ToArray(); // all ok so far - modify the registration reg.FunctionLambda = shim; if (String.IsNullOrEmpty(reg.FunctionAttribute.Description)) { reg.FunctionAttribute.Description = functionDescription; } for (int param = 0; param != reg.ParameterRegistrations.Count; ++param) { if (String.IsNullOrEmpty(reg.ParameterRegistrations[param].ArgumentAttribute.Description)) { reg.ParameterRegistrations[param].ArgumentAttribute.Description = parameterDescriptions[param]; } } } catch { // failed to shim, just pass on the original } yield return(reg); } }
/// <summary> /// Function which creates a shim for a target method. /// The target method is expected to take 1 or more enumerables of various types, and return a single enumerable of another type. /// The shim is a lambda expression which takes 1 or more object[,] parameters, and returns a single object[,] /// The first row of each array defines the field names, which are mapped to the public properties of the /// input and return types. /// </summary> /// <returns></returns> private static LambdaExpression MakeObjectArrayShim( LambdaExpression targetMethod, IList <ShimParameter> inputShimParameters, ShimParameter resultShimParameter) { var nParams = targetMethod.Parameters.Count; var compiledTargetMethod = targetMethod.Compile(); // create a delegate, object*n -> object // (simpler, but probably slower, alternative to building it all out of Expressions) ParamsDelegate shimDelegate = inputObjectArray => { try { if (inputObjectArray.GetLength(0) != nParams) { throw new InvalidOperationException( $"Expected {nParams} params, received {inputObjectArray.GetLength(0)}"); } var targetMethodInputs = new object[nParams]; for (int i = 0; i != nParams; ++i) { try { targetMethodInputs[i] = inputShimParameters[i].ConvertShimToTarget(inputObjectArray[i]); } catch (Exception e) { throw new InvalidOperationException($"Failed to convert parameter {i + 1}: {e.Message}"); } } var targetMethodResult = compiledTargetMethod.DynamicInvoke(targetMethodInputs); return(resultShimParameter.ConvertTargetToShim(targetMethodResult)); } catch (Exception e) { return(new object[, ] { { ExcelError.ExcelErrorValue }, { e.Message } }); } }; // convert the delegate back to a LambdaExpression var args = targetMethod.Parameters.Select(param => Expression.Parameter(typeof(object))).ToList(); var paramsParam = Expression.NewArrayInit(typeof(object), args); var closure = Expression.Constant(shimDelegate.Target); var call = Expression.Call(closure, shimDelegate.Method, paramsParam); return(Expression.Lambda(call, args)); }