asp.net mvc controller arguments a?b1,c2... how to parse variable number? - asp.net-mvc

If a URL, in asp.net mvc, has args that are not catered for in the index/show method signature of a controller, is it possible to use something like a params string[] args in the signature to gather them up. Or what is a good way to do this when the arg is a list of separated values (ie not name/value pairs)?
We have users, ultimately, creating urls with a variable number of arguments & need to parse them.
This is the code we have at the moment, but we can't help thinking there's a better way without splitting the string ourselves:
var url = Request.RawUrl.Split('?');
if (url.Length > 1)
{
var queryString = url[1];
var queryStringArgs = queryString.Split('&');
var queryStringMembers = from arg in queryStringArgs
let c = arg.Split('=').Length == 1
where c
select arg;
ViewBag.QueryStringMembers = queryStringMembers.ToJson();
}
*Append: these args dont have name=value, it's just a list of values.
Request.QueryString doesn't seem to help us, as it treats these query string args differently because they are not name=value, they are just value. So it puts them in a Request.QueryString[null] key as comma separated

First things first what you have is a malformed URL. So you are totally on your own parsing it. You may also expect it to fail any time. Everything that is part of the querystring, i.e. following the first ?, must be url encoded, you should not have multiple ?.
This being said you could write a custom model binder:
public class CustomModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var tokens = controllerContext.HttpContext.Request.RawUrl.Split('?');
if (tokens.Length > 1)
{
return tokens.Skip(1).ToArray();
}
return null;
}
}
and then:
public ActionResult Index([ModelBinder(typeof(CustomModelBinder))]string[] args)
{
return View();
}

Request.QueryString.AllKeys will contain a string array of the argument names sent in on the QueryString. Is that what you're looking for?

Related

AttributeRouting - Correct way of Getting the Action Name from RouteData

I have recently started using attribute routing for my action methods and am struggling in getting the action name (and/or id) from the RouteData. Below is an example of how I use the attrubues like so:
[Route( "Edit/{id:int}" )]
public ActionResult Edit( int id )
{
...
}
Previously I used the following method as an extension of RouteData to retrieve the value
public static string GetActionName( this RouteData routeData )
{
return routeData.Values[ "Action" ] as string;
}
This used to return the action name (in this example "edit"). Now this alway returns null. An inspection of the RouteData dictionary shows that these values seem to no longer be placed in the root of the array but rather in an item keyed "[0] = "MS_DirectRouteMatches".
I am able to access this value in the following manner but due to my limited understanding of how these values are populated I am concerned that, for e.g. routes where there are more than one match, this code will fall over in some cases.
public static string GetActionName( this RouteData routeData )
{
if( routeData.Values.ContainsKey( "MS_DirectRouteMatches" ) )
routeData = ( ( IList<RouteData> )routeData.Values[ "MS_DirectRouteMatches" ] )[ 0 ];
return routeData.Values[ "Action" ] as string;
}
What is the correct way to access the RouteData values populated by AttributeRouting?
Your solution will work in all but one case. It is possible that a request will not match any route, so you should ensure the action key exists before you return it, or you may get an exception.
public static string GetActionName( this RouteData routeData )
{
if( routeData.Values.ContainsKey( "MS_DirectRouteMatches" ) )
routeData = ( ( IEnumerable<RouteData> )routeData.Values["MS_DirectRouteMatches"] )[0];
return routeData.Values.ContainsKey("action") ?
routeData.Values["action"] as string :
string.Empty;
}
That one case typically passes the request to IIS so it can return the 404 page (or a custom one if configured), and this basically just covers the custom 404 page case.
I would also change IList to be IEnumerable, since you are adding nothing to the list in the method.
There is no such thing as a request that matches more than one route (0 or 1 route are the only possibilities). In the case where there is no match, MS_DirectRouteMatches will not exist. In the case where it does exist, there is 1 value in the collection.
Unfortunately, there aren't really any guarantees that the number of values in the collection won't change to more than one in a future version, but for now that holds true. But since there isn't a more robust way to determine an action name than this, it is what we are stuck with until the next breaking change.

Ninject Call Method to Return Constructor Argument

I'm trying to bind a service and specifying a constructor argument using Ninject in our application. The constructor argument is a value that can be pulled from the query string or a cookie. The code we currently have is something like this
kernel.Bind<SomeService>()
.ToSelf()
.InRequestScope()
.WithConstructorArgument("someID", ctx =>
// Try to get it from the posted form values
System.Web.HttpContext.Current.Request.Form["someID"] != null ?
long.Parse(System.Web.HttpContext.Current.Request.Form["someID"]) :
// Try to get it from the query string
System.Web.HttpContext.Current.Request.QueryString["someID"] != null ?
long.Parse(System.Web.HttpContext.Current.Request.QueryString["someID"])
: 0);
This works but is pretty ugly. I realize there are other ways of accomplishing this such as passing in the Form value or the QueryString value as a parameter, but we like having it defined in the Binding. What we would ideally like to do is something like this:
kernel.Bind<SomeService>()
.ToSelf()
.InRequestScope()
.WithConstructorArgument("someID", ctx => GetSomeID());
From what I can tell, this is not possible. Is there another way to break out the constructor argument injection logic into another method so we don't have to nested one line if statements?
I'd suggest binding the dependency on the Query String / HTTP form via an interface. This approach seems more in line with the dependency injection pattern (de-coupling code from specific implementations and classes).
public interface IParameters
{
string SomeID { get; }
}
public class ParametersFromHttpContext
{
IQueryString _queryString;
IRequestForm _requestForm;
public ParametersFromHttpContext(IQueryString queryString, IRequestForm requestForm)
{
_queryString = queryString;
_requestForm = requestForm;
}
public string SomeID
{
get
{
return
// Try to get it from the posted form values
_requestForm["someID"] != null ?
long.Parse(_requestForm["someID"]) :
// Try to get it from the query string
_queryString["someID"] != null ?
long.Parse(_queryString["someID"])
: 0;
}
}
}
Now logic you want can be contained in the binding, without the need to reference HttpContext in the kernel.
kernel.Bind<IParameters>().To<ParametersFromHttpContext>();

How to get GET parameters with ASP.NET MVC ApiController

I feel a bit absurd asking this but I can't find a way to get parameters for a get request at
/api/foo?sort=name for instance.
In the ApiController class, I gave a public string Get(). Putting Get(string sort) makes /api/foo a bad request. Request instance in the ApiController is of type System.Net.Http.HttpRequestMessage. It doesn't have a QueryString or Parameters property or anything.
The ApiController is designed to work without the HttpContext object (making it portable, and allowing it to be hosted outside of IIS).
You can still access the query string parameters, but it is done through the following property:
Request.GetQueryNameValuePairs()
Here's an example loop through all the values:
foreach (var parameter in Request.GetQueryNameValuePairs())
{
var key = parameter.Key;
var value = parameter.Value;
}
You could just use
HttpContext.Current.Request.QueryString
Here's an example that gets the querystring q from the request and uses it to query accounts:
var q = Request.GetQueryNameValuePairs().Where(nv => nv.Key =="q").Select(nv => nv.Value).FirstOrDefault();
if (q != null && q != string.Empty)
{
var result = accounts.Where(a=>a.Name.ToLower().StartsWith(q.ToLower()));
return result;
}
else
{
throw new Exception("Please specify a search query");
}
This can be called then like this:
url/api/Accounts?q=p
Get all querystring name/value pairs into a variable:
IEnumerable<KeyValuePair<string, string>> queryString = request.GetQueryNameValuePairs();
Then extract a specified querystring parameter
string value = queryString.Where(nv => nv.Key == "parameterNameGoesHere").Select(nv => nv.Value).FirstOrDefault();
You can also use the following
var value = request.GetQueryNameValuePairs().Where(m => m.Key == "paramName").SingleOrDefault().Value;
if we have a proper model for that request
for example
public class JustModel
{
public int Id {get;set;}
public int Age {gets;set;}
}
and query like this
/api/foo?id=1&Age=10
You could just use [FromUri] attribute
For example
public IHttpActionResult GetAge([FromUri] JustModel model){}
You're trying to build an OData webservice? If so, just return an IQueryable, and the Web API will do the rest.
Adding a default value does the job:
public string Get(string sort="")

How to Unit Test JsonResult and Collections in MSTest

I am very new to unit testing even though i have been coding for a very long time. I want to make this a part of my way of development. I run into blocks on how to unit test things like a collection. I generally have my jQuery script calling ASP.Net Server side methods to get data and populate tables and the like. They look like
Get_*Noun*()
which generally returns a JsonResult. Any ideas on what and how to test these using Unit tests using MSTest?
You should be able to test this just like anything else, provided you can extract the values from the JsonResult. Here's a helper that will do that for you:
private T GetValueFromJsonResult<T>(JsonResult jsonResult, string propertyName)
{
var property =
jsonResult.Data.GetType().GetProperties()
.Where(p => string.Compare(p.Name, propertyName) == 0)
.FirstOrDefault();
if (null == property)
throw new ArgumentException("propertyName not found", "propertyName");
return (T)property.GetValue(jsonResult.Data, null);
}
Then call your controller as usual, and test the result using that helper.
var jsonResult = yourController.YourAction(params);
bool testValue = GetValueFromJsonResult<bool>(jsonResult, "PropertyName");
Assert.IsFalse(testValue);
(I am using NUnit syntax, but MSUnit shouldn't be far off)
You could test your JsonResult like this:
var json = Get_JsonResult()
dynamic data = json.Data;
Assert.AreEqual("value", data.MyValue)
Then in the project that contains the code to be tested, edit AssemblyInfo.cs file to allow the testing assembly access to the anonymous type:
[assembly: InternalsVisibleTo("Tests")]
This is so the dynamic can determine the type of anonymous object being returned from the json.Data value;
This is the best blog I've found on this subject.
My favorite was the 4th approach using dynamics. Note that it requires you to ensure that the internals are visible to your test project using [assembly:InternalsVisibleTo("TestProject")] which I find is a reasonably good idea in general.
[TestMethod]
public void IndexTestWithDynamic()
{
//arrange
HomeController controller = new HomeController();
//act
var result = controller.Index() as JsonResult;
//assert
dynamic data = result.Data;
Assert.AreEqual(3, data.Count);
Assert.IsTrue(data.Success);
Assert.AreEqual("Adam", data.People[0].Name);
}
You could use PrivateObject to do this.
var jsonResult = yourController.YourAction(params);
var success = (bool)(new PrivateObject(jsonResult.Data, "success")).Target;
Assert.IsTrue(success);
var errors = (IEnumerable<string>)(new PrivateObject(jsonResult.Data, "errors")).Target;
Assert.IsTrue(!errors.Any());
It's uses reflection similar to David Ruttka's answer, however it'll save you a few key strokes.
See http://msdn.microsoft.com/en-us/library/microsoft.visualstudio.testtools.unittesting.privateobject.aspx for more info.
Here's a small extension to easily convert a Json ActionResult into the object it represents.
using System.Web.Mvc;
public static class WebExtensions
{
public static T ToJson<T>(this ActionResult actionResult)
{
var jsonResult = (JsonResult)actionResult;
return (T)jsonResult.Data;
}
}
With this, your 'act' in the test becomes smaller:
var myModel = myController.Action().ToJson<MyViewModel>();
My suggestion would be to create a model for the data returned and then cast the result into that model. That way you can verify:
the structure is correct
the data within the model is correct
// Assert
var result = action
.AssertResultIs<JsonResult>();
var model = (UIDSearchResults)result.Data;
Assert.IsTrue(model.IsValid);
Assert.AreEqual("ABC", model.UIDType);
Assert.IsNull(model.CodeID);
Assert.AreEqual(4, model.PossibleCodes.Count());

How to set decimal separators in ASP.NET MVC controllers?

I'm working with the NerdDinner application trying to teach myself ASP.NET MVC. However, I have stumbled upon a problem with globalization, where my server presents floating point numbers with a comma as the decimal separator, but Virtual Earth map requires them with dots, which causes some problems.
I have already solved the issue with the mapping JavaScript in my views, but if I now try to post an edited dinner entry with dots as decimal separators the controller fails (throwing InvalidOperationException) when updating the model (in the UpdateModel() metod). I feel like I must set the proper culture somewhere in the controller as well, I tried it in OnActionExecuting() but that didn't help.
I have just revisited the issue in a real project and finally found a working solution. Proper solution is to have a custom model binder for the type decimal (and decimal? if you're using them):
using System.Globalization;
using System.Web.Mvc;
public class DecimalModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
object result = null;
// Don't do this here!
// It might do bindingContext.ModelState.AddModelError
// and there is no RemoveModelError!
//
// result = base.BindModel(controllerContext, bindingContext);
string modelName = bindingContext.ModelName;
string attemptedValue = bindingContext.ValueProvider.GetValue(modelName)?.AttemptedValue;
// in decimal? binding attemptedValue can be Null
if (attemptedValue != null)
{
// Depending on CultureInfo, the NumberDecimalSeparator can be "," or "."
// Both "." and "," should be accepted, but aren't.
string wantedSeperator = NumberFormatInfo.CurrentInfo.NumberDecimalSeparator;
string alternateSeperator = (wantedSeperator == "," ? "." : ",");
if (attemptedValue.IndexOf(wantedSeperator, StringComparison.Ordinal) == -1
&& attemptedValue.IndexOf(alternateSeperator, StringComparison.Ordinal) != -1)
{
attemptedValue = attemptedValue.Replace(alternateSeperator, wantedSeperator);
}
try
{
if (bindingContext.ModelMetadata.IsNullableValueType && string.IsNullOrWhiteSpace(attemptedValue))
{
return null;
}
result = decimal.Parse(attemptedValue, NumberStyles.Any);
}
catch (FormatException e)
{
bindingContext.ModelState.AddModelError(modelName, e);
}
}
return result;
}
}
Then in Global.asax.cs in Application_Start():
ModelBinders.Binders.Add(typeof(decimal), new DecimalModelBinder());
ModelBinders.Binders.Add(typeof(decimal?), new DecimalModelBinder());
Note that code is not mine, I actually found it at Kristof Neirynck's blog here. I just edited a few lines and am adding the binder for a specific data type, not replacing the default binder.
Set this in your web.config
<system.web>
<globalization uiCulture="en" culture="en-US" />
You appear to be using a server that is setup with a language that uses comma's instead of decimal places. You can adjust the culture to one that uses the comma's in a way that your application is designed, such as en-US.
Can you parse the text using the invariant culture - sorry, I don't have the NerdDinner code in fornt of me, but if you are passing in dot-separated decimals than the parsing should be OK if you tell it to use the invariant culture. E.g.
float i = float.Parse("0.1", CultureInfo.InvariantCulture);
Edit. I suspect that this is a bug in the NerdDinner code by the way, along the same lines as your previous problem.
I have a different take on this, you might like it. What I don't like about the accepted answer is it doesn't check for other characters. I know there will be a case where the currency symbol will be in the box because my user doesn't know better. So yeah I can check in javascript to remove it, but what if for some reason javascript isn't on? Then extra characters might get through. Or if someone tries to spam you passing unknown characters through... who knows! So I decided to use a regex. It's a bit slower, tiny fraction slower - for my case it was 1,000,000 iterations of the regex took just under 3 seconds, while around 1 second to do a string replace on a coma and period. But seeing as I don't know what characters might come through, then I am happy for this slightest of performance hits.
public class DecimalModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
string modelName = bindingContext.ModelName;
string attemptedValue =
bindingContext.ValueProvider.GetValue(modelName).AttemptedValue;
if (bindingContext.ModelMetadata.IsNullableValueType
&& string.IsNullOrWhiteSpace(attemptedValue))
{
return null;
}
if (string.IsNullOrWhiteSpace(attemptedValue))
{
return decimal.Zero;
}
decimal value = decimal.Zero;
Regex digitsOnly = new Regex(#"[^\d]", RegexOptions.Compiled);
var numbersOnly = digitsOnly.Replace(attemptedValue, "");
if (!string.IsNullOrWhiteSpace(numbersOnly))
{
var numbers = Convert.ToDecimal(numbersOnly);
value = (numbers / 100m);
return value;
}
else
{
if (bindingContext.ModelMetadata.IsNullableValueType)
{
return null;
}
}
return value;
}
}
Basically, remove all characters that are not digits, for a string that isn't empty. Convert to decimal. Divide by 100. Return result.
Works for me.

Resources