Web API with complex array parameters - c#

Need help on this one. I have a WebAPI who can receive multiple ids as parameters. The user can call the API using 2 route:
First route:
api/{controller}/{action}/{ids}
ex: http://localhost/api/{controller}/{action}/id1,id2,[...],idN
Method signature
public HttpResponseMessage MyFunction(
string action,
IList<string> values)
Second route:
"api/{controller}/{values}"
ex: http://localhost/api/{controller}/id1;type1,id2;type2,[...],idN;typeN
public HttpResponseMessage MyFunction(
IList<KeyValuePair<string, string>> ids)
Now I need to pass a new parameter to the 2 existing route. The problem is this parameter is optional and tightly associated with the id value. I made some attempt like a method with KeyValuePair into KeyValuePair parameter but its results in some conflict between routes.
What I need is something like that :
ex: http://localhost/api/{controller}/{action}/id1;param1,id2;param2,[...],idN;paramN
http://localhost/api/{controller}/id1;type1;param1,id2;type2;param2,[...],idN;typeN;paramN

You might be able to deal with it by accepting an array:
public HttpResponseMessage MyFunction(
string action,
string[] values)
Mapping the route as:
api/{controller}/{action}
And using the query string to supply values:
GET http://server/api/Controller?values=1&values=2&values=3

Assumption: You are actually doing some command with the data.
If your payload to the server is getting more complex than a simple route can handle, consider using a POST http verb and send it to the server as JSON instead of mangling the uri to shoehorn it in as a GET.
Different assumption: You are doing a complex fetch and GET is idiomatically correct for a RESTFUL service.
Use a querystring, per the answer posted by #TrevorPilley

Looks like a good scenario for a custom model binder. You can handle your incoming data and detect it your self and pass it to your own type to use in your controller. No need to fight with the built in types.
See here.
From the page (to keep the answer on SO):
Model Binders
A more flexible option than a type converter is to create a custom
model binder. With a model binder, you have access to things like the
HTTP request, the action description, and the raw values from the
route data.
To create a model binder, implement the IModelBinder interface. This
interface defines a single method, BindModel:
bool BindModel(HttpActionContext actionContext, ModelBindingContext
bindingContext);
Here is a model binder for GeoPoint objects.
public class GeoPointModelBinder : IModelBinder {
// List of known locations.
private static ConcurrentDictionary<string, GeoPoint> _locations
= new ConcurrentDictionary<string, GeoPoint>(StringComparer.OrdinalIgnoreCase);
static GeoPointModelBinder()
{
_locations["redmond"] = new GeoPoint() { Latitude = 47.67856, Longitude = -122.131 };
_locations["paris"] = new GeoPoint() { Latitude = 48.856930, Longitude = 2.3412 };
_locations["tokyo"] = new GeoPoint() { Latitude = 35.683208, Longitude = 139.80894 };
}
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(GeoPoint))
{
return false;
}
ValueProviderResult val = bindingContext.ValueProvider.GetValue(
bindingContext.ModelName);
if (val == null)
{
return false;
}
string key = val.RawValue as string;
if (key == null)
{
bindingContext.ModelState.AddModelError(
bindingContext.ModelName, "Wrong value type");
return false;
}
GeoPoint result;
if (_locations.TryGetValue(key, out result) || GeoPoint.TryParse(key, out result))
{
bindingContext.Model = result;
return true;
}
bindingContext.ModelState.AddModelError(
bindingContext.ModelName, "Cannot convert value to Location");
return false;
} } A model binder gets raw input values from a value provider. This design separates two distinct functions:
The value provider takes the HTTP request and populates a dictionary
of key-value pairs. The model binder uses this dictionary to populate
the model. The default value provider in Web API gets values from the
route data and the query string. For example, if the URI is
http://localhost/api/values/1?location=48,-122, the value provider
creates the following key-value pairs:
id = "1" location = "48,122" (I'm assuming the default route template,
which is "api/{controller}/{id}".)
The name of the parameter to bind is stored in the
ModelBindingContext.ModelName property. The model binder looks for a
key with this value in the dictionary. If the value exists and can be
converted into a GeoPoint, the model binder assigns the bound value to
the ModelBindingContext.Model property.
Notice that the model binder is not limited to a simple type
conversion. In this example, the model binder first looks in a table
of known locations, and if that fails, it uses type conversion.
Setting the Model Binder
There are several ways to set a model binder. First, you can add a
[ModelBinder] attribute to the parameter.
public HttpResponseMessage
Get([ModelBinder(typeof(GeoPointModelBinder))] GeoPoint location)
You
can also add a [ModelBinder] attribute to the type. Web API will use
the specified model binder for all parameters of that type.
[ModelBinder(typeof(GeoPointModelBinder))] public class GeoPoint {
// .... }

I found a solution.
First, I created a class to override the
KeyValuePair<string, string>
type to add a third element (I know it's not really a pair!). I could have use Tuple type also:
public sealed class KeyValuePair<TKey, TValue1, TValue2>
: IEquatable<KeyValuePair<TKey, TValue1, TValue2>>
To use this type with parameter, I create an
ActionFilterAttribute
to split (";") the value from the url and create a KeyValuePair (third element is optional)
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (actionContext.ActionArguments.ContainsKey(ParameterName))
{
var keyValuePairs = /* function to split parameters */;
actionContext.ActionArguments[ParameterName] =
keyValuePairs.Select(
x => x.Split(new[] { "," }, StringSplitOptions.None))
.Select(x => new KeyValuePair<string, string, string>(x[0], x[1], x.Length == 3 ? x[2] : string.Empty))
.ToList();
}
}
And finally, I add the action attribute filter to the controller route and change the parameter type:
"api/{controller}/{values}"
ex: http://localhost/api/{controller}/id1;type1;param1,id2;type2,[...],idN;typeN;param3
[MyCustomFilter("ids")]
public HttpResponseMessage MyFunction(
IList<KeyValuePair<string, string, string>> ids)
I could use some url parsing technique, but the ActionFilterAttribute is great and the code is not a mess finally!

Related

Sanitising parameters in ASP.Net Core 2.2

I have a seemingly rather specific problem. My top-level controller and models for complex parameters are auto-generated (Nswag). Some of the model consists of enums.
I have parameters (in query or body) which have to contain backslashes. The values of these in the auto-generated enums automatically have backslashes replaced with underscores. To make model validation work, I have to somehow catch parameters binding with these enums and change them before binding occurs therefore.
For example, given a query
?param=A\B
(or a body with param="a\b") and the Enum:
public enum SomeEnum
{
[System.Runtime.Serialization.EnumMember(Value = #"A\B")]
A_B = 0
}
Model validation fails because A\B isn't found in the enum, naturally.
I have tried filters, custom model binders etc. and custom model binding seems to be the best place as it can be made to apply at precisely the point of binding that specific model. Now, the problem is that I need to modify the incoming parameter and bind to a modified version with underscores. I can't for the life of me find out how to do this. I implemented a custom IModelBinder class, which is called properly but ModelBindingResult.Success(model) doesn't alter what is bound to.
Just to be clear, this has nothing to do with URL encoding or binding to collections etc. This is all working fine.
I essentially need to modify parameters being bound with a specific Enum so that they match the auto-generated enum properties. Any ideas much appreciated.
It seems that a custom binder is the correct way to do it, when you code it properly ...
Below is the binder class that works nicely. SSASPropertyNameBinder is the enum whose values can contain backslashes. This class is mostly boiler plate from the MS ASP.Net Core docs on custom model binders - the interesting bit is at the end.
public class SSASPropertyNameBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var modelName = bindingContext.ModelName;
// Try to fetch the value of the argument by name
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
if (valueProviderResult == ValueProviderResult.None)
{
return Task.CompletedTask;
}
var value = valueProviderResult.FirstValue;
// Check if the argument value is null or empty
if (string.IsNullOrEmpty(value))
{
return Task.CompletedTask;
}
ValueProviderResult newValueProviderResult = new ValueProviderResult(valueProviderResult.FirstValue.Replace(#"\", "_"));
bindingContext.ModelState.SetModelValue(modelName, newValueProviderResult);
SSASServerPropertyName spn;
// Check if a valid SSAS property
if (Enum.TryParse<SSASServerPropertyName>(newValueProviderResult.FirstValue, out spn))
{
bindingContext.Result = ModelBindingResult.Success(spn);
}
else
{
bindingContext.ModelState.TryAddModelError(modelName, $"Invalid SSAS Property: {valueProviderResult.FirstValue}");
}
return Task.CompletedTask;
}
}

How to get optional parameters in mvc action C#

There is an TestAction in HomeController as below:
public ActionResult TestAction(string a = "a", int b = 2, int c = 1)
{
var DATA = Request.Params;//Can't get param neither a nor b
return View();
}
If my visit link is "/Home/TestAction?c=23". Then DATA will be {c=23}, but not contain a and b.
Does there any way to get these two params to make DATA like {a="a", b=2, c=23} by visit link "/Home/TestAction?c=23". (These params are different in different page, so can't hard-code).
You can do it by passing the params in the url as following. The model binder will read the query string and pass these parameter values to the action method.
/Home/TestAction?a=valuea&b=valueb
You can also use the route data to pass the value. An appropriate route will need to be defined to do that.
Take all the paramters as nullable
int?,
string accepts null by default.
So after changes your method will look like
public ActionResult TestAction(string a, int? b, int? c)
{
//Check conditions for null here
return View();
}

ASP NET MVC OutputCache VaryByParam complex objects

This is what I have:
[OutputCache(Duration = 3600, VaryByParam = "model")]
public object Hrs(ReportFilterModel model) {
var result = GetFromDatabase(model);
return result;
}
I want it to cache a new result for each different model. At the moment it is caching the first result and even when the model changes, it returns the same result.
I even tried to override ToString and GetHashCode methods for ReportFilterModel. Actually I have about more properties I want to use for generating unique HashCode or String.
public override string ToString() {
return SiteId.ToString();
}
public override int GetHashCode() {
return SiteId;
}
Any suggestions, how can I get complex objects working with OutputCache?
The VaryByParam value from MSDN: A semicolon-separated list of strings that correspond to query-string values for the GET method, or to parameter values for the POST method.
If you want to vary the output cache by all parameter values, set the attribute to an asterisk (*).
An alternative approach is to make a subclass of the OutputCacheAttribute and user reflection to create the VaryByParam String. Something like this:
public class OutputCacheComplex : OutputCacheAttribute
{
public OutputCacheComplex(Type type)
{
PropertyInfo[] properties = type.GetProperties();
VaryByParam = string.Join(";", properties.Select(p => p.Name).ToList());
Duration = 3600;
}
}
And in the Controller:
[OutputCacheComplex(typeof (ReportFilterModel))]
For more info:
How do I use VaryByParam with multiple parameters?
https://msdn.microsoft.com/en-us/library/system.web.mvc.outputcacheattribute.varybyparam(v=vs.118).aspx

Is it possible to have one parameter of type object in a controller's action accepting int, date, float or string from json

I have a collection of of key-value pairs, where the value part can be of any of the types (C#) string, DatetimeOffset, long, float, boolean. The key-value pair is an alternate key of a class named Component:
public class Component {
public long Id { get; set;}
public string Key { get; set; }
// another properties here ...
public object Value { get; set;}
}
The user enters the value part in an input bound with knockoutjs to an observable, and I expect to find the corresponding component instance given the key and that value with this controller action method (MVC4):
public JsonResult GetComponent(string compKey, object compValue)
{
var comp = Database.FindComponentValue(compKey, compValue);
return this.Json(comp, JsonRequestBehavior.AllowGet);
}
which I invoke in a function of the knockout view model in this way:
self.findComponent = function (component) {
// component is ko.observable() generated on-the-fly with ko.mapping.fromJS()
//var compdata = ko.mapping.toJS(component);
$.get("MyController/GetComponent", {
compClasskey: component.FullCode(),
compValue: component.Value()
},
self.validateComponent
);
};
validateComponent is a function that shows an icon OK if the component is found.
Now, if component.Value() has a string value, MyController.GetComponent receives an array of string with the value in the first position (compValue[0]). But declaring compValue parameter as string works:
public JsonResult GetProductComponent(string compClassKey, string compValue) { ... }
But it leads to me to declare the method like this in order to be able to accept the other types:
public JsonResult GetProductComponent(string compClassKey, string compValueString, DatetimeOffset compValueDateTimeOffset, bool? compValueBoolean, long compValueLong) {
...
}
Another approach is to be compValue of type string and its corresponding type in another parameter also of type string.
Is this the solution or is it possible to have only one parameter of type object and I am making a mistake I am not seeing?
Your basic issue is that you are pushing the data payload as items on the querystring, not as a JSON payload. In this scenario the QueryString value provider will be responsible for populating the method's parameters.
All querystring key values are treated as an array by the provider. If you specify a data type and the key's value has 1 entry then the provider will flatten the array to a single value.
In your case you specify an object datatype, so the provider gives you the array of 1 entry.
To fix this you need to switch from $.get to $.ajax, supply a few extra parameters and force the data payload to be a JSON string.
$.ajax({
url: "MyController/GetComponent",
type: 'POST',
contentType: 'application/json',
dataType: 'JSON'
data: JSON.stringify(
{
compClasskey: component.FullCode(),
compValue: component.Value()
})
});
The MVC application should now use the JSON value provider ( as the contentType is now changed to application/json ) instead of the QueryString provider and the compValue parameter will now set as you expect.
As an aside: IMHO you should NEVER request a JSON document via a GET request, it is still an attack vector, unless you can 100% guarantee IE10+, Chrome 27+ or Firefox 21+.
JSON Hijacking -- Phil HAACK
SO -- Is JSON Hijacking still possible
All values are transported from client to server in the request body, this is a "text" stream and can be seen as a large string.
MVC reads the method's arguments and types, and tries to convert the (string) values from the http request to the type of the argument
If your argument is defined as an object, it does not know where to convert the incomming string values to. So that is what you are not seeing ;-)
To solve this you indeed need to provide information about the type of the value. having to arguments (compClassKey and compValue) is a solution.
Alternative you can provide a model class
public JsonResult GetProductComponent( ComponentValue value )
where ComponentValue is
public class ComponentValue
{
public DateTime? DateValue {get;set;}
public string TextValue {get;set;}
public decimal? DecimalValue {get;set;}
}
and than post a JSON object
{
DecimalValue:1
}

Passing arguments to Web API model binder

I am trying to create Web API model binder that will bind URL parameters sent by a grid component of a javascript framework.
Grid sends URL parameters indicating standard page, pageSize, and JSON formatted sorters, filters, and groupers.
The URL string looks like this:
http://localhost/api/inventory?page=1&start=0&limit=10sort=[{"property":"partName","direction":"desc"},{"property":"partStatus","direction":"asc"}]&group=[{"property":"count","direction":"asc"}]
The model in question is Inventory which has simple, Count (int) property and a reference, Part (Part) peoperty (which in turn has Name, Status).
The view model/dto is flattened (InventoryViewModel .Count, .PartName, .PartStatus, etc, etc.)
I use Dynamic Expression Api then to query domain model, map the result to view model and send it back as JSON.
During model binding I need to build the expressions by examining model and view model that are being used.
In order to keep model binder reusable, how can I pass/specify model and view model types being used?
I need this in order to build valid sort,filter,and grouping expsessions
Note: I don't want to pass these as part of the grid url params!
One idea I had was to make StoreRequest generic (e.g. StoreRequest) but I am not sure if or how model binder would work.
Sample API Controller
// 1. model binder is used to transform URL params into StoreRequest. Is there a way to "pass" types of model & view model to it?
public HttpResponseMessage Get(StoreRequest storeRequest)
{
int total;
// 2. domain entites are then queried using StoreRequest properties and Dynamic Expression API (e.g. "Order By Part.Name DESC, Part.Status ASC")
var inventoryItems = _inventoryService.GetAll(storeRequest.Page, out total, storeRequest.PageSize, storeRequest.SortExpression);
// 3. model is then mapped to view model/dto
var inventoryDto = _mapper.MapToDto(inventoryItems);
// 4. response is created and view model is wrapped into grid friendly JSON
var response = Request.CreateResponse(HttpStatusCode.OK, inventoryDto.ToGridResult(total));
response.Content.Headers.Expires = DateTimeOffset.UtcNow.AddMinutes(5);
return response;
}
StoreRequestModelBinder
public class StoreRequestModelBinder : IModelBinder
{
private static readonly ILog Logger = LogManager.GetCurrentClassLogger();
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
Logger.Debug(m => m("Testing model binder for type: {0}", bindingContext.ModelType));
if (bindingContext.ModelType != typeof(StoreRequest))
{
return false;
}
var storeRequest = new StoreRequest();
// ----------------------------------------------------------------
int page;
if (TryGetValue(bindingContext, StoreRequest.PageParameter, out page))
{
storeRequest.Page = page;
}
// ----------------------------------------------------------------
int pageSize;
if (TryGetValue(bindingContext, StoreRequest.PageSizeParameter, out pageSize))
{
storeRequest.PageSize = pageSize;
}
// ----------------------------------------------------------------
string sort;
if (TryGetValue(bindingContext, StoreRequest.SortParameter, out sort))
{
try
{
storeRequest.Sorters = JsonConvert.DeserializeObject<List<Sorter>>(sort);
// TODO: build sort expression using model and viewModel types
}
catch(Exception e)
{
Logger.Warn(m=>m("Unable to parse sort parameter: \"{0}\"", sort), e);
}
}
// ----------------------------------------------------------------
bindingContext.Model = storeRequest;
return true;
}
private bool TryGetValue<T>(ModelBindingContext bindingContext, string key, out T result)
{
var valueProviderResult = bindingContext.ValueProvider.GetValue(key);
if (valueProviderResult == null)
{
result = default(T);
return false;
}
result = (T)valueProviderResult.ConvertTo(typeof(T));
return true;
}
}
just change your controller signature like
public HttpResponseMessage Get([ModelBinder(typeof(StoreRequestModelBinder)]StoreRequest storeRequest)
Regards

Categories

Resources