I have a C# Website and Web API combined with a Save-method in one of my API Controllers. This method only allows HttpPosts. What I want to send in the body is a List<int>, List<decimal> and long.
Since HttpPost methods in API Controllers only allow one parameter to work, I tried both a JObject and string as parameter. When I use string it's always null, when I use JObject it's not null but incorrectly copied from the body. Because these are null and incorrectly copied, the converting to the List<int>, List<decimal> and long are also not working.
Parameter as string:
[HttpPost]
[AllowAnonymous]
[ActionName("save")]
public bool SaveOrders([FromBody] string jsonData)
{
// Convert jsonData string to List<int>, List<decimal>, long
JObject json = JObject.Parse(jsonData);
List<int> productIds = JsonConvert.DeserializeObject(json["productIds"].ToString(), typeof(List<int>));
List<decimal> prices = JsonConvert.DeserializeObject(json["prices"].ToString(), typeof(List<decimal>));
long dateInTicks = JsonConvert.DeserializeObject(json["dateInTicks"].ToString(), typeof(long));
...
}
with POST-body:
"{
"productIds": "[20, 25]",
"prices": "[0.40, 7.40]",
"dateInTicks": "1402444800"
}"
When I debug this above, the parameter-string is always null.
Parameter as JObject:
[HttpPost]
[AllowAnonymous]
[ActionName("save")]
public bool SaveOrders([FromBody] JObject jsonData)
{
// Convert jsonData JObject to List<int>, List<decimal>, long
dynamic json = jsonData;
string sProductIds = (string)json["productIds"];
string sPrices = (string)json["prices"];
string sDateInTicks = (string)json["dateInTicks"];
List<int> productIds = JsonConvert.DeserializeObject(sProductIds, typeof(List<int>));
List<decimal> prices = JsonConvert.DeserializeObject(sPrices, typeof(List<decimal>));
long dateInTicks = JsonConvert.DeserializeObject(sDateInTicks, typeof(long));
...
}
with POST-body:
productIds: "[20, 25]",
prices: "[0.40, 7.40]",
dateInTicks: "1402444800"
When I debug this, the parameter-JObject is:
{
"productIds: \"": {
"20, 25]\",\nprices: \"": {
"0.40, 7.40]\",\ndateInTicks: \"1402444800\"": ""
}
}
}
and the sProductIds, sPrices and sDateInTicks are null.
I know I'm doing some things wrong, therefore this question, since I don't how I should change it.
Edit 1 (Rafal's suggestion):
In my config file I added one line:
// Only allow JSON response format
var json = config.Formatters.JsonFormatter;
json.SerializerSettings.PreserveReferencesHandling = Newtonsoft.Json.PreserveReferencesHandling.Objects;
config.Formatters.Remove(config.Formatters.XmlFormatter);
// Added the following line:
config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
And my method is changed:
[HttpPost]
[AllowAnonymous]
[ActionName("save")]
public bool SaveOrders([FromBody] Data jsonData)
{
if (jsonData != null)
{
if (jsonData.productIds != null && jsonData.prices != null)
{
return SavePrices(jsonData.productIds, jsonData.prices, jsonData.dateInTicks);
}
else
{
Console.WriteLine("One of the objects is null, so we can't continue.");
return false;
}
}
else
{
Console.WriteLine("The send data is null, so we can't continue.");
return false;
}
}
public class Data
{
public List<int> productIds { get; set; }
public List<decimal> prices { get; set; }
public long dateInTicks { get; set; }
}
But although the Data-parameter isn't null, both the Lists inside of it are and the long is 0 as well.
Edit 2:
With the FireFox' RESTClient Body:
"productIds":[20,25],"prices":[0.4,7.4],"dateInTicks":1402444800
Why is the JObject parameter this:
{
"\"productIds\":": {
"20,25],\"prices\":": {
"0.4,7.4],\"dateInTicks\":1402444800": ""
}
}
}
instead of this:
{
\"productIds\":[20,25],\"prices\":[0.4,7.4],\"dateInTicks\":1402444800
}
It automatically removes the first [ with the arrays and replaces it with :" {"..
Add a class:
public class Data
{
public List<int> productIds { get; set; }
public List<decimal> prices { get; set; }
public long dateToTicks { get; set; }
}
And change the signature of you API method to receive it as parameter.
Note that properties names are pascal case. I did that on purpose as default configuration of deserializer will not allow camel case. To handle your json and use 'normal' properties casing add:
config.Formatters.JsonFormatter.SerializerSettings.ContractResolver
= new CamelCasePropertyNamesContractResolver();
line in your site configuration file you will find it in App_Start folder for newer template or directly in Global.asax.cs.
The config is of type HttpConfiguration.
Further investigation leads me to conclusion that you have invalid (well not invalid as far as json validator is concerned but not appropriate to the task at hand) json content. If you post it as:
{
"productIds": [20, 25],
"prices": [0.40, 7.40],
"dateToTicks": "1402444800"
}
without any additional quotes then it will deserialize to Data class. If you need to retain it as is then you would need to deserialize is by hand as if you loose wraping quote then you have json object with 3 string properties not lists of values.
Related
I created a database with JSON columns in mysql.
API defined in Swagger.
Writing JSON to the database works without problems, but when reading, the value of the JSON field is shown as an escaped string and not JSON
Here is part of model:
/// <summary>
/// Gets or Sets Doc
/// </summary>
[DataMember(Name="doc")]
public Dictionary<string, string> Doc { get; set; }
I also tried with string type and Dictionary<string, object> but unsuccessful.
Get method is here:
public virtual IActionResult GetDataById([FromRoute][Required]int? dataId, [FromRoute][Required]string jsonNode)
{
if(_context.Data.Any( a => a.Id == dataId)) {
var dataSingle = _context.Data.SingleOrDefault( data => data.Id == dataId);
return StatusCode(200, dataSingle);
} else {
return StatusCode(404);
}
}
}
And resulting JSON resposne looks like this one:
{
"field1": "value1",
"field2": "value2",
"doc": "{\"key1\":\"value1\",\"key2\":\"value2\",\"key3\":{\"subkey3-1\":\"value3-1\",\"subkey3-2\":\"value3-2\"}}"
}
but correct JOSN should be linke this one:
{
"field1": "value1",
"field2": "value2",
"doc": {
"key1":"value1",
"key2":"value2",
"key3":{
"subkey3-1":"value3-1",
"subkey3-2":"value3-2"
}
}
}
If I try to return just "Doc" (JSON) field, response JSON is properly formatted.
I tried different serialization/deserialization but unsuccessful.
If I have public Dictionary<string, string> Doc { get; set; } in Model I got error:
fail: Microsoft.AspNetCore.Server.Kestrel[13]
Connection id "0HM1VN0F0I0IM", Request id "0HM1VN0F0I0IM:00000001": An unhandled exception was thrown by the application.
System.InvalidOperationException: The property 'Data.Doc' is of type 'Dictionary<string, string>' which is not supported by current database provider. Either change the property CLR type or ignore the property using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'
and for public string Doc { get; set; } in Model I got "doc" field value as escaped string as I mention above.
What would be the best way to go about this?
A few days later I found a resolution.
You need a one helper method that converts a string to object, exactly JObject if you use Json.NET - Newtonsoft.
public static JObject getJsonOutOfData (dynamic selectedData)
{
var data = JsonConvert.SerializeObject(selectedData);
var jsonData = (JObject)JsonConvert.DeserializeObject<JObject>(data);
if (selectedData.Doc != null) {
JObject doc = JObject.Parse(selectedData.Doc);
JObject docNode = JObject.Parse("{\"doc\":" + doc.ToString() + "}");
var jsonDoc = (JObject)JsonConvert.DeserializeObject<JObject>(docNode.ToString());
jsonData.Property("doc").Remove();
jsonData.Merge(jsonDoc, new JsonMergeSettings
{
MergeArrayHandling = MergeArrayHandling.Union
});
}
return jsonData;
}
and call it in the IActionResult method
public virtual IActionResult GetDataById([FromRoute][Required]int? accidentId, [FromRoute][Required]string jsonNode)
{
if (_context.Data.Any( a => a.Id == accidentId)) {
var accidentSingle = _context.Data.SingleOrDefault( accident => accident.Id == accidentId);
var result = getJsonOutOfData(accidentSingle);
return StatusCode(200, result.ToString());
} else {
return StatusCode(404);
}
}
In order to use unescaped JSON in POST/PUT requests, you should have a string type as a property type of JSON filed in the database model, and create an additional "request" model and set JSON property as "Object" type:
Database Model:
[DataMember(Name="doc")]
public string Doc { get; set; }
Request Model:
[DataMember(Name="doc")]
public Object Doc { get; set; }
For example Create model looks like:
public virtual IActionResult CreateData([FromBody]DataCreateRequest body)
{
var accidentObject = new Data() {
ColumnOne = body.ColumnOne,
ColumnTwo = body.ColumnTwo,
ColumnThree = body.ColumnThree,
Doc = (bodyDoc != null) ? body.Doc.ToString() : "{}"
};
_context.Data.Add(accidentObject);
_context.SaveChanges();
return StatusCode(200, getJsonOutOfData(accidentObject));
}
I'm doing C# JSON <-> PHP JSON for the first time.
Thought I'd get on an easy road but seems like I've hit the rock.
I'm fairly sure that JSON from Newtonsoft allows "[" character but not sure why i have this error instead?
Here's my C# code:
public class SystemJSON
{
public bool Status { get; set; }
public string Message { get; set; }
public string ResponseData { get; set; }
}
public static class SystemCall
{
public static String Post(string uri, NameValueCollection pairs)
{
byte[] response = null;
using (WebClient wc = new WebClient())
{
response = wc.UploadValues(uri, pairs);
}
return Encoding.Default.GetString(response);
}
}
string system_Response = SystemCall.Post("http://127.0.0.1:8080/edsa-NEFS%20(PHP)/api.php", new NameValueCollection()
{
{"do_work", Functions.Get_Department_List.ToString()},
{"api_data", null }
});
**SystemJSON systemJSON = JsonConvert.DeserializeObject<SystemJSON>(system_Response);** //<-- Error happens here.
if(systemJSON.Status == true)
{
//do stuff here
}else
{
MessageBox.Show(this, systemJSON.Message, this.Text, MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
And here's my PHP code:
<?php
// Load Request
$function_name = isset($_POST['do_work']) ? $_POST['do_work'] : '';
$api_data = isset($_POST['api_data']) ? $_POST['api_data'] : '';
// Validate Request
if (empty($function_name))
{
SystemResponse(false, 'Invalid Request');
}
if (!function_exists($function_name))
{
SystemResponse(false, 'API Method Not Implemented');
}
// Call API Method
call_user_func($function_name, $api_data);
/* Helper Function */
function SystemResponse($responseStatus, $responseMessage, $responseData = '')
{
exit(json_encode(array(
'Status' => $responseStatus,
'Message' => $responseMessage,
'ResponseData' => $responseData
)));
}
/* API Methods */
function Get_Department_List($api_data)
{
//Test ------------------------------------------START
$node = array();
$dept = array();
$responseData = array();
$dept['id'] = 1;
$dept['name'] = "General";
$dept['description'] = "Forms, Samples, Templates, Catalogs, etc";
$dept['status'] = 1;
array_push($node, $dept);
$dept['id'] = 2;
$dept['name'] = "Test";
$dept['description'] = "Testing";
$dept['status'] = 1;
array_push($node, $dept);
$responseData["dept"] = $dept;
SystemResponse(true, 'SUCCESS', $responseData);
//Test ------------------------------------------END
}
?>
And here's my error:
Newtonsoft.Json.JsonReaderException HResult=0x80131500
Message=Unexpected character encountered while parsing value: {. Path
'ResponseData', line 1, position 51.
The problem is that your C# SystemJSON class does not match the structure of the incoming JSON correctly.
ResponseData in your C# SystemJSON class is listed as a string but your PHP appears to be pushing out a complex object inside that property. You can't deserialise an object into a string - there is no way for the deserialiser to know how to translate the object structure into a suitable string, and anyway it's not generally a useful or logical thing to do. So instead it throws an error to say the object structure doesn't match.
The specific error you're seeing means the deserialiser is expecting a " to denote the start of a string but instead it's seeing { denoting the start of another object.
Why is this happening? Well, your PHP code will produce a JSON response which looks like this:
{
"Status": true,
"Message": "SUCCESS",
"ResponseData": {
"dept": {
"id": 2,
"name": "Test",
"description": "Testing",
"status": 1
}
}
}
Live demo here
As you can see, ResponseData contains an object, which has a "dept" which in turn is another object with four more properties.
To deserialise this properly, your SystemJSON class will need to be altered, and you'll also need two sub-classes to help it out:
public class SystemJSON
{
public bool Status { get; set; }
public string Message { get; set; }
public ResponseData ResponseData { get; set; }
}
public class ResponseData {
public Department dept {get; set; }
}
public class Department {
public string id {get; set; }
public string description {get; set; }
public int status {get; set; }
}
You will now be able to deserialise the JSON correctly. Here is a live demo of the deserialisation.
P.S the [ character appears to be irrelevant here...it's unclear why you referred to that in your question.
P.P.S. From looking at your PHP I'm guessing that you may be intending to return different data structures in ResponseData depending on which parameter was specified for do_work - i.e. depending on which PHP function is called. If so then you'll need to amend your C# accordingly so that it deserialises to a different concrete class depending on which API method it requests. Or you could possibly cheat and specify ResponseData as dynamic, which will then accept any data structure it received, albeit with the caveat that it's now effectively loosely-typed and so you lose certain benefits when compiling the code such as checking for valid usage of property names, data types etc.
I have a model class I created in angular 2 to track fields. This same class also exists as a model in my webapi project.
export class People {
Name: string;
Phone: string;
Date: date;}
export class Address {
street: string,
zip: string,
}
in my service I send this to my webapi controller
getPeopleData(peopleModel: People, addressmodel: Address)
{
let headers = new headers(...)
data = {
"p": peopleModel,
"a": addressModel
}
let body = JSON.stringify(data);
return this.http.post(url, body, {headers: headers})
.map((response: Response)=> ...
}
finally in my controller
public JArray GetPeopleData([FromBody]JObject models)
{
var modelPeople = models["p"].ToObject<People>();
var modelAddress = models["a"].ToObject<Address>();
}
modelPeople and modeAddress doesn't map. How can I get my model to map directly.
All I get are a bunch of null fields when there is a string of data in the JObject.
EDIT:
I created a container class that holds the objects of People and Address
public class Container
{
public People people {get; set;}
public Address addresss {get; set;}
}
I then passed in the object to the Container and my results are still null
public JArray GetPeopleData([FromBody]Container container)
{
var modelPeople = container.people;
var modelAddress = container.address;
}
both have all values of null.
I dont get it I feel like I am so close but something is missing
Hello it works for me
//This is a Gobal object
result = {
modelPeople: '',
modelAddress: ''
}
constructor( private _http: Http ) {
}
getJsonModel() {
promise = await this._http.get(URL).toPromise().then(
res => {
this.result= res.json();
}
);
}
Ideally I would like to have an URL in following format:
/api/categories/1,2,3...N/products
And this would return all products for the specified categories. Having one API call with multiple category IDs saves me several database calls, thus improves performance.
I can easily implement this in a following way.
public HttpResponseMessage GetProducts(string categoryIdsCsv)
{
// <1> Split and parse categoryIdsCsv
// <2> Get products
}
However, this doesn't look like a clean clean solution, and possibly breaking SRP principle. I also tried using ModelBinder, however it adds parameters to query string.
Questions:
Is there a clean way to implement such URL structure?
Or is there a different/better approach to retrieve all products for multiple categories?
Please let me know if you need any further clarification.
I've just found an answer to my question. Route attribute had missing parameter when using ModelBinder.
[Route("api/categories/{categoryIds}/products")]
public HttpResponseMessage GetProducts([ModelBinder(typeof(CategoryIdsModelBinder))] CategoryIds categoryIds)
{
// <2> Get products using categoryIds.Ids
}
And CategoryIds would be
public class CategoryIds
{
public List<int> Ids{ get; set; }
}
And CategoryIdsModelBinder would be
public class CategoryIdsModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(CategoryIds))
{
return false;
}
var val = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (val == null)
{
return false;
}
var key = val.RawValue as string;
if (key == null)
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Wrong value type");
return false;
}
var values = val.AttemptedValue.Split(',');
var ids = new List<int>();
foreach (var value in values)
{
int intValue;
int.TryParse(value.Trim(), out intValue);
if (intValue > 0)
{
ids.Add(intValue);
}
}
if (ids.Count > 0)
{
var result = new CategoryIds
{
Ids= ids
};
bindingContext.Model = result;
return true;
}
bindingContext.ModelState.AddModelError(
bindingContext.ModelName, "Cannot convert value to Location");
return false;
}
We can use Post methods
[RoutePrefix ( "api/categories" )]
public class TestController
{
[HttpPost]
[Route ( "getProducts" )]
public HttpResponseMessage GetProducts ( HttpRequestMessage request )
{
HttpResponseMessage message = null;
string input = string.Empty;
input = request.Content.ReadAsStringAsync ().Result;
var ids = Newtonsoft.Json.JsonConvert.DeserializeObject<List<string>> ( input );
}
}
Unfortunately Web API can not parse your data as array or as some kind of your custom object out of the box.
If you want to parse your url param as array you can try to do:
Write your own route constraint which will read and convert your param from string to array of ints/strings/whatever;
Write your custom type converter and use it with your data model;
write your value provider and also use it with your data model
Use parameter binding
Moreover you can always use query params which is never will break principles of REST :)
Please see more details about here and here
Hope that helps
I am calling a REST api that returns their data in the following format:
{
"facets": [
{
"M": 100
},
{
"F": 210
}
]
}
I am not sure how to define a C# class that maps to this JSON since the M/F property name could be anything. This is currently a facet for gender, but for something else like language it might be "English", "Spanish", "Japanese", etc. Ideally I would like something like a dictionary.
Where the keys can vary, use a dictionary to represent the object:
public class Criteria
{
public List<Dictionary<string, int>> facets { get; set; }
}
(If the dictionary value isn't always an int, use object instead.)
Fiddle: https://dotnetfiddle.net/IwyXby
This is how I use json.net to both serialize and deserialize:
public static bool SerializeStudentsFile(string fileStorageLoc)
{
var jsonStudents = JsonConvert.SerializeObject(StudentsList);
System.IO.File.WriteAllText(fileStorageLoc, jsonStudents);
return true;
}
public static List<Student> DeserializeStudentsFile()
{
List<Student> studentList;
if (!System.IO.File.Exists(STUDENTS_FILENAME))
{
var studentFile = System.IO.File.Create(STUDENTS_FILENAME);
studentFile.Close();
}
var studentContentsFile = System.IO.File.ReadAllText(STUDENTS_FILENAME);
var studentContentsFileDeserialized = JsonConvert.DeserializeObject<List<Student>>(studentContentsFile);
if (null != studentContentsFileDeserialized) return studentContentsFileDeserialized;
studentList = new List<Student>();
return studentList;
}