I need to pass a complex object representing a data filter to an action using GET which returns a filtered data set in a csv file.
The filter object is something like this on the client (much more complex in actuality, simplified for brevity):
var filter = {
Folders = [
{ Positive: true, Reference: { Id: 19, Name: "Container" } },
{ Positive: true, Reference: { Id: 37, Name: "Bullseye" } },
]
}
The corresponding server side classes look something like this:
public class MyFilter
{
public List<MyComparison> Folders { get; set; }
}
public class MyComparison
{
public bool Positive { get; set; }
public MyReference Reference { get; set; }
}
public class MyReference
{
public int Id { get; set; }
public string Name {get; set; }
}
My action looks like this:
[HttpGet]
public FileContentResult Export(MyFilter filter, string sort, bool sortAscending)
{
string data = GetCsvData(filter, sort, sortAscending);
return this.File(StrToByteArray(data), "text/csv", "Data.csv");
}
I have tried calling this action from javascript like this:
function exportFilter(aFilter) {
var params = { filter: aFilter, sort: "Name", sortAscending: true };
var destination = "MyController/Export?" + decodeURIComponent($.param(params));
document.location = destination;
}
Within the action, both the sort and sortAscending parameters are properly populated. The filter is an object of type MyFilter, but its Folders property is null.
Is ASP.NET MVC incapable of properly deserializing complex parameters in this way (ie in the context of a GET)? What is the correct way to address this problem?
The databinding algorithm in Asp.net MVC is not very good in deserializing complex inputs in .NET types. In the best scenario, you would get some strange results and/or slow performance compared to other dedicated solutions. In this case, Json.NET provides much better results at deserializing json data in .NET types, and its very fast too.
You should pass these filters like an ordinary string parameter and, inside the action, deserialize it using Json.NET. Something like this:
using Newtonsoft.Json;
public ActionResult MyAction(string myFilters)
{
var deserializedObject = JsonConvert.DeserializeObject(myFilters);
}
It can bind complex objects/parameters, the problem is the way the params are being sent. For example, you're sending:
http://localhost/Home/Export?filter[Folders][0][Positive]=true&filter[Folders][0][Reference][Id]=19&filter[Folders][0][Reference][Name]=Container&filter[Folders][1][Positive]=true&filter[Folders][1][Reference][Id]=37&filter[Folders][1][Reference][Name]=Bullseye&sort=Name&sortAscending=true
But the MVC model binder expects this format:
http://localhost/Home/Export?filter.Folders[0].Positive=true&filter.Folders[0].Reference.Id=19&filter.Folders[0].Reference.Name=Container&filter.Folders[1].Positive=true&filter.Folders[1].Reference.Id=37&filter.Folders[1].Reference.Name=Bullseye&sort=Name&sortAscending=true
I'm not sure of the easiest way to build a string matching that pattern from a javascript object though.
Related
I'm trying to pass a object which has multiple objects inside it as below
[Object, Object, Object, Object]
0
:
Object
ProductID
:
"50"
__proto__
:
Object
1
:
Object
BrandID
:
24
__proto__
:
Object
2
:
Object
BrandID
:
26
__proto__
:
Object
3
:
Object
BrandID
:
20
__proto__
:
Object
One of these objects has different key value pair than the others. How can I get capture this data from a Web Api controller. How should I modify my Model in the Web Api project.
It seems to me that the array you are trying to send to Web API contains different objects with different schemas. This approach is certainly error prone, and will not allow you to use ModelBinding properly.
Why don't you change the format of your object to something like this?
$scope.myObject = {
ProductID: 50,
BrandIDs: [24, 26, 20]
};
Using this kind of object you will be able to bind it to a strongly typed model in Web API.
public class MyModel {
public int ProductID { get; set; }
public List<int> BrandIDs { get; set; }
}
public IHttpActionResult Post(MyModel model) {
var productId = model.ProductID;
foreach(var brandId in model.BrandIDs) {
DoSomething(brandId);
}
return Ok();
}
You just need to create a class model that corresponds to you JSON and Web Api will automatically bind it. It seems that what you are passing is an array, so you can do something like that:
public void Execute(Model[] input)
{
}
....
public class Model
{
public int? ProductId {get;set;}
public int? BrandId {get;set;}
}
Or if you want one object with an array inside you can pass a class like that
public class ProductsContainer
{
public Product[] Products {get;set;}
}
I am creating a pdf using pdfsharp. I need to pass the chart legend data(name,color) to the pdfsharp controller. I am using a angular $http post, a ajax post would be fine as well. the error I am getting is
Request URL:http://localhost:46691/api/exportChartPdf/[object%20Object],[object%20Object],[object%20Object],[object%20Object],[object%20Object],[object%20Object],[object%20Object],[object%20Object],[object%20Object],[object%20Object],[object%20Object],[object%20Object],[object%20Object],[object%20Object],[object%20Object],[object%20Object]
do i need to some how pass it as a string? if so how would I parse it in the api controller back to the way I need it?
the objects i am trying to pass back
0: Object
color: "rgb(67,134,215)"
name: "Fleming Place - Miscellaneous Spec Builders"
__proto__: Object
1: Object
2: Object
3: Object
4: Object
javascript
var legendModel = $scope.seriesData.map(function (a) {
return {
color: a.color,
name: a.name
};
});
$http.post('/api/exportChartPdf/' + legendModel);
api controller
[HttpPost]
public PDF Post() // allow nullable parameter
{
try
{
You should be posting the object on the form body, not on the querystring. In addition, your Web API controller should receive a strongly typed object that mirrors the data that you're passing.
public class Data
{
public string Color { get; set; }
public string Name { get; set; }
}
[HttpPost]
public PDF Post([FromBody]List<Data> data)
{
...
}
And then simply post the object.
var legendModel = $scope.seriesData.map(function (a) {
return {
color: a.color,
name: a.name
};
});
$http.post('/api/exportChartPdf/', legendModel);
Looks like your controller action is missing parameter on it,
[HttpPost]
public PDF Post(SomeClass legendModel) // allow nullable parameter
{
try
{
SomeClass would contain color & name properties.
Then you should correct your $http.post call, pass 2nd param as data in JSON format.
$http.post('/api/exportChartPdf', { legendModel: legendModel);
I'm using a third party library and cant really change the way it posts data to my MVC 5 controller.
I cant figure out how to setup my model to receive the data.
The json is a follows...
{
"expiration":"2015-06-14T21:02:52.969Z",
"conditions":[
{"acl":"private"},
{"bucket":"anyoldbucket"},
{"Content-Type":"application/pdf"},
{"success_action_status":"200"},
{"key":"somekey"}
]
}
I tried setting up my model like this...
public class AwsSignatureRequestViewModel
{
public DateTime expiration { get; set; }
public Dictionary<string, string> conditions { get; set; }
}
The expiration date is correctly filled out, and I get the right number of conditions but the keys to the dictionary are numbers (indexes) and the values are null
Any suggestions?
If your model is strict, you will need to make objects for every sub objects.
But if you have a dynamic model, you can read the raw string from your request and parse it with Json.net.
public ActionResult Test(string model)
{
Request.InputStream.Seek(0, SeekOrigin.Begin);
string jsonData = new StreamReader(Request.InputStream).ReadToEnd();
var dynamicObject = JObject.Parse(jsonData);
...
}
dynamicObject will contains all of your json.
IINM, based on the data you're getting, your AwsSignatureRequestViewModel should look something like this:
public class AwsSignatureRequestViewModel
{
public DateTime expiration { get; set; }
public List<Dictionary<string, string>> conditions { get; set; }
}
conditions is an "array of objects".
The way you currently have your model, data would map to something like this (which isn't the case):
{
"expiration": "2015-06-14T21:02:52.969Z",
"conditions":
{
"acl": "private",
"bucket": "anyoldbucket",
....
},
.....
Hth...
I'm trying to figure out how to post an object from my form to a web api service. Within my controller I defined a model that I wanted to add input values to.
$scope.Label;
within my input fields I have them bound using ng-model such as:
<input type="checkbox" ng-model="label.isPublic" />
<input type="text" ng-model="label.labelName" required focus-me />
On the submission of the form these two fields a passed to my service and submitted to my WebApi
I have tried this submission in two ways:
function addLabel(label) {
var mylabel = encodeURIComponent(angular.toJson(label));
return $http.post('reportLibrary/createlabel/', { params: label }, {
}).then(function (response) {
return response.data;
});
};
and also as the following without declaring parameters
function addLabel(label) {
var mylabel = encodeURIComponent(angular.toJson(label));
return $http.post('reportLibrary/createlabel/', label , {
}).then(function (response) {
return response.data;
});
};
In the webAPI I have a method setup for the post
[Route ("reportLibrary/createlabel/")]
[HttpPost]
public DTOs.ReportLabel CreateLabel(DTOs.ReportLabel json)
{
DTOs.ReportLabel result = new DTOs.ReportLabel();
//.... do stuff
return result;
}
The ReportLabel (dto) is defined as follows:
public class ReportLabel
{
public Int64 LabelId { get; set; }
public string LabelName { get; set; }
public bool IsPublic { get; set; }
public IEnumerable<Report> Reports { get; set; }//placeholder?
}
The issue I have is when I post an object from my angular service it shows up as null within the API. If I change the type in the method to something like a JToken or JObject the values appear.
Can anyone help me understand why when I define the type that it is not passed across from angular?
thanks
It seems like you may be doing an extra step. You don't need to encode in json then pass in in json
return $http.post('reportLibrary/createlabel/', { LabelId: 101, LabelName: 'myname' }, {
then
public DTOs.ReportLabel CreateLabel([FromBody]ReportLabel reportLabel)
Take a look at the network values going by and you should see in debug tools or fiddler the actual posted values (form values).
I'm having a class like the following:
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
[DataContract()]
public class TestCol : List<Test> { }
[DataContract()]
public class MainTest
{
public TestCol Components { get; set; }
}
[DataContract()]
public class Test
{
public Test() { }
public String Name { get; set; }
}
And a webservice with the following webmethod like this:
[WebMethod]
public String Test(MainTest input)
{
String rtrn = String.Empty;
foreach (Test test in input.Components)
rtrn += test.Name;
return rtrn;
}
Which is called by AJAX with the following method:
var Test = {};
Test.Name = "Test";
var MainTest = {};
MainTest.Components = [];
MainTest.Components.push(Test);
$.ajax({
type: "POST",
url: "WebService/WSTest.asmx/Test",
contentType: "application/json; charset=utf-8",
dataType: "json",
data: JSON.stringify({
"input": MainTest
}),
success: function(data, textStatus) {
console.log("success");
},
error: function(XMLHttpRequest, textStatus, errorThrown) {
window.console && console.log && console.log(XMLHttpRequest.responseText + " || " + textStatus + " || " + errorThrown);
}
});
When executing the AJAX call, it will return errors. I found out that the error is with the typed class TestCol, which has no properties.
Now do I have found 2 solutions that require changes in the C# classes:
Remove the TestCol class and change the Components property to List<Test> datatype:
[DataContract()]
public class MainTest
{
public List<Test> Components { get; set; }
}
[DataContract()]
public class Test
{
public Test() { }
public String Name { get; set; }
}
Or add an extra property to the TestCol class and change the webmethod:
[DataContract()]
public class TestCol : List<Test>
{
public List<Test> Components { get; set; }
}
[DataContract()]
public class MainTest
{
public TestCol Components { get; set; }
}
[DataContract()]
public class Test
{
public Test() { }
public String Name { get; set; }
}
&
[WebMethod]
public String Test(MainTest input)
{
String rtrn = String.Empty;
foreach (Test test in input.Components.Components)
rtrn += test.Name;
return rtrn;
}
Both solutions require changes in the C# classes, which I prefer not to, as other code is depended on it. Does anyone know a solution for this problem?
Edit: I've uploaded a test solution, containing above code: http://jeroenvanwarmerdam.nl/content/temp/JSONtoClassWebservice.zip
So this solution changes the List to Object instead of Test. I hoped to change as little code as possible (i dislike having to do casts in foreach loops). The below code does so with two function additions and the previously mentioned inheritance change.
public class TestCol : List<object>
{
public new IEnumerator<Test> GetEnumerator()
{
return this.ConvertAll<Test>(
dict => ConvertDictionaryTo<Test>(
(Dictionary<string, object>) dict
)
).GetEnumerator();
}
private T ConvertDictionaryTo<T>(IDictionary<string, object> dictionary) where T : new()
{
Type type = typeof(T);
T ret = new T();
foreach (var keyValue in dictionary)
{
type.GetProperty(keyValue.Key).SetValue(ret, keyValue.Value, null);
}
return ret;
}
}
Convert function courtesy TurBas
Mapping object to dictionary and vice versa
JavaScriptSerializer serialization: IEnumerable -> JavaScript Array
When the JavaScriptSerializer is used, it automatically converts an IEnumerable (without IDictionary) type -- that covers List<> or anything derived from it -- into an array.
Deserialization: JavaScript Array -> IEnumerable -> Collection Object
Now, upon deserialization from JSON, the JavaScriptSerializer must take the array, create an IEnumerable, then create an object for the field by passing that IEnumerable into its constructor.
Constructing Collection object via Constructor
Now, for List<> you have a constructor overload that takes an IEnumerable. So if you put List<Test> as the type of your component it creates it fine.
Constructors not inherited
However, TestCol does NOT have such a constructor! The reason why it worked with List<Test> and not with TestCol (which derives from List<Test>) is that the only thing that is not inherited between classes are constructors!
Therefore, the JavaScriptSerializer does not have any way to construct a TestCol from an IEnumerable. So it fails silently.
Deserialize Array by Creating List, then Casting to Type
Now the JavaScriptSerializer may then attempt to create a List<Test> from this IEnumerable<Test>, and then try to cast it into a TestCol.
Possible Solution
Solution: Try putting in:
public TestCol () {} // Need this when you have another constructor
public TestCol (IEnumerable<Test> list) : base(list) {} // Constructor that takes an IEnumerable
public TestCol (IList<Test> list) : base(list) {} // Constructor that takes an IList
as your TestCol's constructors.
And if it still doesn't work, implement an explicit type cast from List<Test> to TestCol.
public static explicit operator TestCol(IList<Test> list) { return new TestCol(list); }
hmmm this didn't work in the web method?
foreach (Test test in input.Components.TestCol)
Re comment below, does this work then?
foreach (Test test in (List<Test>)input.Components.TestCol)
It should work because a class can be enumerated...
If you're expecting JSON, you'll need to return JSON.
Check with the System.Web.Script.Serialization.JavaScriptSerializer
http://msdn.microsoft.com/en-us/library/system.web.script.serialization.javascriptserializer.aspx
If you use ASMX services the JavaScriptSerializer will be responsible for the data conversion and not the DataContractJsonSerializer. So all DataContract attributes which you use will not work.
You are write that classes like public class TestCol : List<Test> { } are bad for the JavaScriptSerializer, but classes having List<Test> as the property (public class MainTest { public List<Test> Components { get; set; }}) have no problem.
So I suggest to simplify you code to the following. The classes used as the parameters can be defines as
public class Test {
public String Name { get; set; }
}
public class MainTest {
public List<Test> Components { get; set; }
}
The WebMethod Test will be
[WebMethod]
public String Test(MainTest input)
{
StringBuilder rtrn = new StringBuilder();
foreach (Test test in input.Components) {
rtrn.AppendLine (test.Name);
}
return rtrn.ToString ();
}
and the ajax call can be
var tests = {
Components: [
{Name:"Test1"},
{Name:"Test2"},
{Name:"Test3"}
]
};
$.ajax({
type: "POST",
url: "WebService1.asmx/Test",
contentType: "application/json; charset=utf-8",
dataType: "json",
data: JSON.stringify({
"input": tests
}),
success: function (data, textStatus) {
alert("success:\n" + data.d);
},
error: function (XMLHttpRequest, textStatus, errorThrown) {
alert(XMLHttpRequest.responseText+" || "+textStatus+" || "+errorThrown);
}
});
How you can see all will be very simple and it's work. For more details how you can send complex data I recommend you to read another answer and this.
You seem to be using ASMX (not WCF) because you have omitted [DataMember] attributes on all your public properties and still get serialized. WCF is "opt-in", so you shouldn't be seeing any serialization of any properly.
As a result, all [DataContract] attributes are useless.
ASMX defaults to the JavaScriptSerializer if you are using ScriptManger and outputing JSON. The JavaScriptSerializer is "opt-out" (which means that all public properties are automatically serialized unless marked with [ScriptIgnoreAttribute]).
The JavaScriptSerializer supports serializing List<>'s. You should not be having problems serializing your TestCol property because JavaScriptSerializer automatically supports serializing all types that implement IEnumerable (but not IDictionary) -- which includes List<> -- into JSON arrays.
Your error seems to be that the JavaScriptSerializer does not properly handle classes that inherit from List<> (or from a class implementing IEnumerable). In your first work-around, you eliminated the class that inherited from List<>. In your second work-around, you skipped all functionalities of the base class, but re-implemented the List<> in a property.
Your JSON post data currently looks like:
{ Components: [
{ Name:"foo" },
{ Name:"bar" },
:
] }
However, you have one extra level or redirection in the serializer (inheriting from List<Test> -> TestCol). It is possible that the serializer is looking for:
{ Components: {
Items: [
{ Name:"foo" },
{ Name:"bar" },
:
] }
}
because you are essentially serializing the "Items" property of List<>. So, your JSON post data is just feeding Test objects to the wrong place, and your TestCol Components property ends up empty.
I'd suggest that you add a web service method to output a test MainTest object to see what it serializes into. You'll probably find that it puts in an additional level.