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);
}
Related
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.
I have seen this answer describing ASP.NET support for keyless (not valueless) parameters, like http://some.url?param1¶m2, and confirmed them to be viewable on Request.QueryString like:
var values = this.Request.QueryString.GetValues(null);
values.Any(o => o == "param1");
This is fine and dandy but now I want to generate urls like this. My first intuition was to use the RouteValueDictionary: routeValues parameter of Url.Action with null as a key:
#{
var dict = new RouteValueDictionary();
dict.Add(null, "param1");
dict.Add(null, "param2");
}
Very link, amaze
But apparently C# forbids nulls as dictionary keys because of reasons.
I have also tried the empty string as the key, but it results in a query string like: ?=param1,=param2 which contains 2 more equal signs that I want it to.
Of course I can string manipulate the heck out of my URL and add the ¶m1 part to the query string, but I was hoping for a concise solution.
You want to add the key values, but leaving the value null isn't allowed.
RouteValueDictionary ignores empty values
You could add a value like 1 for instance, but you lose your fine and dandy solution.
#{
var dict = new RouteValueDictionary();
dict.Add("param1",1);
}
Very link, amaze
For another solution you will have to write some custom code.
Since there's no built-in helper for this why don't you roll your own:
public static class UrlHelperExtensions
{
public static string MyAction(this UrlHelper urlHelper, string actionName, IList<string> parameters)
{
string url = urlHelper.Action(actionName);
if (parameters == null || !parameters.Any())
{
return url;
}
return string.Format("{0}?{1}", url, string.Join("&", parameters));
}
}
and then:
#{
var parameters = new List<string>();
parameters.Add("param1");
parameters.Add("param2");
}
#Url.MyAction("ActionName", parameters)
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.
I followed this example:
ASP.NET MVC - Pass array object as a route value within Html.ActionLink(...)
But, my Action is always called with null. What am I doing wrong?
foreach (OrderDetail od in order.OrderDetails)
{
rvd.Add("key" + count++, productID);
rvd.Add("key" + count++, productName);
}
#Html.ActionLink(linkText, "Renew", "Orders", rvd, new Dictionary<string, object>())
The query string is correctly generated, like ?key0=dog&key1=cat&key2=fish..., but I get a null parameter in my Action below:
public ActionResult Renew(RouteValueDictionary rvd)
{
// 'rvd' is null here!
}
Please note: I don't know the number of parameters in advance.
The query string is correctly generated, like ?key0=dog&key1=cat&key2=fish...
No, this is not a correct url. A correct url would have looked like this:
?%5B0%5D.Key=123&%5B0%5D.Value=dog&%5B1%5D.Key=456&%5B1%5D.Value=cat...
which would have mapped to:
public ActionResult Renew(Dictionary<int, string> rvd)
{
...
}
You could write a custom ActionLink to generate this url:
public static class LinkExtensions
{
public static IHtmlString MyActionLink(
this HtmlHelper html,
string linkText,
string actionName,
string controllerName,
IDictionary<string, string> parameters
)
{
var a = new TagBuilder("a");
var urlHelper = new UrlHelper(html.ViewContext.RequestContext);
var query = string.Join("&", parameters.Select((x, i) => string.Format("[{0}].Key={1}&[{0}].Value={2}", i, urlHelper.Encode(x.Key), urlHelper.Encode(x.Value))));
var url = string.Format(
"{0}?{1}",
urlHelper.Action(actionName, controllerName, null, html.ViewContext.HttpContext.Request.Url.Scheme),
query
);
a.Attributes["href"] = url;
a.SetInnerText(linkText);
return new HtmlString(a.ToString());
}
}
which you could use like this in your view:
#Html.MyActionLink(
linkText,
"Renew",
"Orders",
order.OrderDetails.ToDictionary(x => x.ProductID.ToString(), x => x.ProductName)
)
You can read more about the correct wire format for binding to various collections in this blog post.
I imagine what is happening is you are expecting the model binder to bind your array to a RouteValueDictionary, but the model binder doesn't know that key0=dog&key1=cat&key2=fish is supposed to be a dictionary. I would recommend changing your code to accept a string array. To do this, your query string needs to look something like this: ?rvd=dog&rvd=cat&rvd=fish
And your Action...
public ActionResult Renew(string[] rvd)
{
// 'rvd' is no longer null here!
}
The important part is rvd is the parameter name in your action, as well as the name of each element in the querystring: ?rvd=dog&rvd=cat&rvd=fish. If you really want to use a dictionary instead of a string array, then your querystring should look like this: ?rvd[0]=dog&rvd[1]=cat&rvd[2]=fish, giving each item an array index, but you would probably have to change your parameter from RouteValueDictionary to Dictionary<string,string>, I'm not quite sure. More info here. EDIT: See Darin's comment about binding to a dictionary, as I believe his is correct.
You may have to write your own extension for Html.ActionLink that accepts an array (or whatever OrderDetails is) and creates the querystring as an array. This looks like a pretty good starting place.
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());