I've made a function which takes a MethodBase object and a dictionary of named parameters and matches the named parameters to the method and converts the data so that the method can be called with the correct parameters.
In order to convert the data I've been using
parameters[paramIndex] = Convert.ChangeType(item.Value, paramType);
However this will not work for MVC bound types as simply invoking a MethodBase object doesn't perform the data conversion via MVC bindings.
I can check whether the type to be converted has a binder by doing
if(ModelBinders.Binders.TryGetValue(paramType, out var binder)){...}
However I'm not sure then how to use the binder to convert the data. I've tried using this answer but there's no explanation of what the ModelBindingContext parameters actually are and how to use them for these purposes.
Context
Thought it might be wise to provide some context: I'm trying to do this as we perform server-side rendering of SVG and HTML DOMs (using Puppeteer/batik) but some of the DOMs have embedded images with links in them.
So before performing the render:
Find image URLs from DOM
Parsing the parameters and route out
Find the method to call in assembly via it's MVC route attribute
Call the method with the converted parameters to render the image
Convert the result to base64
Change the embedded image in DOM to use the base64 representation rather than a URL
Thanks in advance for your help.
The entire method definition is:
private static object[] MapParameters(MethodBase method, IDictionary<string, object> namedParameters)
{
var parms = method.GetParameters();
var paramNames = parms.Select(p => p.Name).ToArray();
var parameters = new object[paramNames.Length];
for (var i = 0; i < parameters.Length; ++i)
parameters[i] = Type.Missing;
foreach (var item in namedParameters)
{
var param = parms.First(parm => parm.Name == item.Key);
var paramType = Nullable.GetUnderlyingType(param.ParameterType) ?? param.ParameterType;
var paramName = item.Key;
var paramIndex = Array.IndexOf(paramNames, paramName);
if (ModelBinders.Binders.TryGetValue(paramType, out var binder))
{
// Use the binder to convert data
continue;
}
parameters[paramIndex] = Convert.ChangeType(item.Value, paramType);
}
return parameters;
}
To solve this you can just throw a dummy ControllerContext and ModelBindingContext at the IModelBinder.BindModel implementation as so:
private static object ConvertDataWithModelBinder(IModelBinder binder, object value)
{
const string modelName = "ValueToBeConverted";
var modelContext = new ModelBindingContext { ModelName = modelName };
var routeData = new RouteData { Values = { { modelName, value } } };
var controllerContext = new ControllerContext { RouteData = routeData };
return binder.BindModel(controllerContext, modelContext);
}
This assumes that the binder implementation looks at the ControllerContext.RouteData.Values object (I checked our model binders and they mostly do), however some of them also use the BindingContext.ValueProvider.GetValue method in order to get the value from the controller context.
You may there also need to add a dummy implementation of ValueProvider containing a single ValueProviderResult with the above ModelName and value.
Related
ASP.NET Core's UrlHelperExtensions is not that useful, because it only accepts object for route values, so I can't add/remove/merge route values. Before Core the equivalent in UrlHelper had an overload that took a RouteValueDictionary, which made it easy.
So I have a method that performs routing. It receives object routeValues, I want to add to it, and then pass to IUrlHelper.Action(...).
I can convert from object to RouteValueDictionary, but not back to anonymous object. Maybe there's another way?
public string GenerateOrderRoute(object routeValues = null)
{
var newRouteValues = //...I want to add/remove/merge to routeValues
// e.g. I want to add an area
return _urlHelper.Action("details", "orders", newRouteValues);
}
I found a way, but it uses dynamic, which I don't like. If you spot problems with this approach please let me know. And if there is a better/different way then please let me know.
public string GenerateOrderRoute(object routeValues = null)
{
// here I merge given route values with some of my own
var dict = (routeValues != null) ? new RouteValueDictionary(routeValues) : new RouteValueDictionary();
dict.Add("area", "commercial");
dict.Add("foo", "bar");
var expandoObject = new ExpandoObject();
var expandoDictionary = (IDictionary<string, object>)expandoObject;
foreach (var keyValuePair in dict)
{
expandoDictionary.Add(keyValuePair);
}
return _urlHelper.Action("details", "orders", expandoDictionary);
}
I am trying to pass a params object[] through a jQuery to a C# method. I'm using this to use the same method through jQuery calls, sending a string that would be the real method to call and the params object[] that is the parameters to this call, obviously the number of parameters is unknown since I don't know exactly what method would be call, here is the code on jQuery:
$('#selectComboBox').change(function () {
var data = {
'method': 'GetComboBoxValues',
'arguments': $.param({ Id: this.value })
};
LoadComboBox('/Get/GetJsonResult', data, $('#destionationComboBox'))
})
The LoadComboBox function is a simple function that I centered to populate comboboxes:
function LoadComboBox(url, data, select) {
select.empty();
$.getJSON(url, data, function (a) {
$(a).each(function () {
$(document.createElement('option')).prop('value',this.Value).text(this.Text).appendTo(select);
});
});
}
My C# code is below:
public string GetJsonResult(string method, params object[] arguments)
{
var methodInfo = this.GetType().GetMethod(method);
var l = methodInfo.Invoke(this, arguments);
return new JavaScriptSerializer().Serialize(l);
}
I get arguments as a object array and it is filled with a string Id=1 (with $('#selectComboBox').value being 1). I was not able to perform a Split('=') in a new array because if the real method (GetComboBoxValues) is not expecting a string (in this case is a INT) it would not be dynamically converted.
Do anyone has any tips or clues?
This was a really interesting question. It seems like your main issue is dynamically converting from an object array to a bunch of required parameter types of a dynamically selected method. In short, this can be done using methodInfo.GetParameters(); and using Convert.ChangeType to convert each of your arguments into the appropriate ParameterType. This is probably best seen in action, so I made a small Forms app that does this. Of course, this all makes a ton of assumptions that what is passed in will be "clean" so a lot of error handling is probably in order.
private void button1_Click(object sender, EventArgs e)
{
//mock up some dynamically passed in parameters
var testParams = new List<object>();
testParams.Add("1");
testParams.Add("Hello");
//the args I'm building up to pass to my dynamically chosen method
var myArgs = new List<object>();
//reflection to get the method
var methodInfo = this.GetType().GetMethod("test");
var methodParams = methodInfo.GetParameters();
//loop through teh dynamic parameters, change them to the type of the method parameters, add them to myArgs
var i = 0;
foreach (var p in methodParams)
{
myArgs.Add(Convert.ChangeType(testParams[i], p.ParameterType));
i++;
}
//invoke method
var ans = methodInfo.Invoke(this, myArgs.ToArray());
//display answer
MessageBox.Show((string)ans);
}
public string test(int i, string s)
{
return s + i.ToString();
}
As an aside, in my opinion, this leads to some crazy code that's tough to maintain (you're trying to do things with C# that it wasn't really meant to do). But you didn't really ask anyone's opinion, so I'll leave that as an aside.
Mike Bell lead me to the answer, his idea just needed some adjusments, commented below, the answer was on editing the GetJsonResult method, to this:
public string GetJsonResult(string method, params object[] arguments)
{
var methodInfo = this.GetType().GetMethod(method);
var methodParameters = methodInfo.GetParameters();
var parameters = new List<object>();
for (int i = 0; i < methodParameters.Length; i++)
{
// Here I'm getting the parameter name and value that was sent
// on the arguments array, we need to assume that every
// argument will come as 'parameterName=parameterValue'
var pName = arguments[i].ToString().Split('=')[0];
var pValue = arguments[i].ToString().Split('=')[1];
// This way I can get the exact type for the argument name that I'm sending.
var pInfo = methodParameters.First(x => x.Name == pName);
parameters.Add(Convert.ChangeType(pValue,
// This is needed because we may be sending a parameter that is Nullable.
Nullable.GetUnderlyingType(pInfo.ParameterType) ?? pInfo.ParameterType));
}
var l = methodInfo.Invoke(this, parameters.ToArray());
return new JavaScriptSerializer().Serialize(l);
}
I'm using the following method to instantiate an object via reflection
Activator.CreateInstance(Type type, params object[] parameters)
Where "parameters" is the list of parameters passed to the constructor at runtime.
However, I'd like this process to be more intuitive to other developers on the team and rather than passing object[] array of parameters, I would like them to pass an anonymous object, e.g.
// note, invalid code
Activator.CreateInstance(typeof(MyType), new { paramName1 = "abc", paramName2 = "xyz})
Since the framework method doesn't support it, does anyone have an example of the code that translates an anonymous object into an array? Note that the order of the parameters is important to the Activator.CreateInstance() method since that's how it does parameter matching. Obviously this is error prone, that's why I'd prefer to use an anonymous type.
Any suggestions are gladly appreciated.
Alec.
I wouldn't use Activator.CreateInstance for this. I'd use Type.GetConstructors() to get all the constructors, and then find one which has the same number of parameters as the anonymous type has properties, and with the same names. If there might be multiple such constructors with different types, you'll need to add extra logic to check that each parameter type is compatible with the relevant property type.
Something like:
public object CreateInstance(Type type, Object parameterMapping)
{
var ctors = type.GetConstructors();
var properties = parameterMapping.GetType().GetProperties();
foreach (var ctor in ctors)
{
var parameters = ctor.GetParameters();
if (parameters.Length != properties.Length)
{
continue;
}
object[] args = new object[parameters.Length];
bool success = true;
for (int i = 0; i < parameters.Length;
{
var property = parameterMapping.GetType().GetProperty(parameter.Name);
if (property == null)
{
success = false;
break;
}
// TODO: Check property type is appropriate too
args[i] = property.GetValue(parameterMapping, null);
}
if (!success)
{
continue;
}
return ctor.Invoke(args);
}
throw new ArgumentException("No suitable constructor found");
}
I use this ActionLink method in order to generate method.
LinkExtensions.ActionLink Method (HtmlHelper, String, String, Object)
Sot the 4th parameter is an Object which contains anonymous properties, used for routes.
It is possible to append/add automatically new anonymous properties to existing routeValues which is Object ?
If yes, how ?
Let's assume that I have a method:
public void Test( ref object currentRouteValues, string newValue)
{
if(!string.IsNullOrEmpty(newValue)){
// add a custom property here to currentRouteValues
// something like: (is wrong but I don't know how to proceed)
currentRouteValues = new { currentRouteValues, myCustoProperty = newValue };
}
}
How to do that automatically for above method ?
Thanks
I think this would answer your question.
Merging anonymous types
If you simply want to extract the data it would be something like this.
Object o = new { var1 = "the first var", var2 = "the second var" };
System.Type type = o.GetType();
foreach (var i in type.GetProperties())
{
Console.Write(i.GetValue(o));
}
But for merging, look at the link above.
We're currently using .NET 3.5 and part of our application uses dynamic invocation (using MethodBase.Invoke)
I am wondering if it is possible to mix in Named Parameters (in .NET 4) with dynamic invocation, to perform something similar to:
// Dictionary that holds parameter name --> object mapping
var parameters = new Dictionary<string, object>();
// Add parameters ....
// Invoke where each parameter will match the one from the method signature.
methodInfo.Invoke(obj, parameters);
Is there any API that allows this option out of the box? If not, is it possible to develop some solution to perform this?
EDIT:
Rethinking of this problem, it sounds similar to how the compiler may actually need to match method calls based on argument lists. Perhaps there's some Compiler API (or the new Roslyn project) that allows doing just this easily? (without coding it myself which may be prone to errors).
You can use code like this:
public static class ReflectionExtensions {
public static object InvokeWithNamedParameters(this MethodBase self, object obj, IDictionary<string, object> namedParameters) {
return self.Invoke(obj, MapParameters(self, namedParameters));
}
public static object[] MapParameters(MethodBase method, IDictionary<string, object> namedParameters)
{
string[] paramNames = method.GetParameters().Select(p => p.Name).ToArray();
object[] parameters = new object[paramNames.Length];
for (int i = 0; i < parameters.Length; ++i)
{
parameters[i] = Type.Missing;
}
foreach (var item in namedParameters)
{
var paramName = item.Key;
var paramIndex = Array.IndexOf(paramNames, paramName);
if (paramIndex >= 0)
{
parameters[paramIndex] = item.Value;
}
}
return parameters;
}
}
And then call it like this:
var parameters = new Dictionary<string, object>();
// Add parameters ...
methodInfo.InvokeWithNamedParameters(obj, parameters);
you can get your paramter names with the help of this article How can you get the names of method parameters? and then you can reorder them to invoke them as described here Reflection: How to Invoke Method with parameters
With .net4, I have an opensource framework ImpromptuInterface (found in nuget) that makes it easy to use the DLR apis for late invocation including named/optional parameters.
var result = Impromptu.InvokeMember(target, "MyMethod", parameters.Select(pair=> InvokeArg.Create(pair.Key, pair.Value)).Cast<object>().ToArray());