I am new to designing API's, and I have a situation in ASP.NET MVC.
In my domain system, I have different concepts, such as an Invoice. I want to create a REST API, where it is possible to:
Create
Update
Delete
Select (based on different elements)
But, for instance, when creating a new object, I need a big set of parameters (see an example viewmodel below).
If I expect a path such as this:
POST - /api/invoice/create
How would I go around and accept form data?
My best guess is to make an APIController, and then accept the InvoiceViewModel as the only parameter. As it is an API Controller, I assume it accepts JSON by default.
Then I have the following question(s):
In jQuery, how would I build a JSON object to "satisfy" this InvoiceViewModel?
Is this the best way to handle more complex products?
InvoiceViewModel:
public class InvoiceViewModel
{
public int Id { get; set; }
public string Comment { get; set; }
public InvoiceAddressViewModel CompanyInfo { get; set; }
public InvoiceAddressViewModel ReceiverInfo { get; set; }
public DateTime DateCreated { get; set; }
public List<InvoiceLineViewModel> Lines { get; set; }
public decimal Total { get; set; }
public decimal VatTotal { get; set; }
public decimal VatPercentage { get; set; }
public decimal TotalExVat { get; set; }
public InvoiceViewModel()
{
this.Lines = new List<InvoiceLineViewModel>();
}
}
public class InvoiceAddressViewModel
{
public string Name { get; set; }
public string Address { get; set; }
public string Company { get; set; }
public string VatNumber { get; set; }
public string Country { get; set; }
public string ZipCode { get; set; }
public string City { get; set; }
}
public class InvoiceLineViewModel
{
public string Title { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
}
There is a default JsonValueProviderFactory for the Asp.net-mvc 3 framework, as long as your JSON data posted to the Action matches the Model it should bind the data correctly.
So something like this:
var requestData = {
Id: "1",
Comment: "The quick brown fox",
CompanyInfo: {
Name: "etc."
}
};
$.ajax({
url: '/api/invoice/create',
type: 'POST',
data: JSON.stringify(requestData),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
error: function (xhr) {
alert('Error: ' + xhr.statusText);
},
success: function (result) {
alert('success');
}
});
Should work just fine, all the data should be bound correctly, just match the JSON property names with the Model names.(they must match)
The best way to get your model to match is to simply serialize your Model in your view to JSON, then you know it is correct, and manipulate it to your hearts content.
var data = set(#Html.Raw(new JavaScriptSerializer().Serialize(Model)));
As for complexity, the sky is the limit you may (very unlikely) run into some scenarios where the default JsonValueProviderFactory for .net is not good enough, but if you do I would be suprised.
Just as a side note, this whole use-case works wonderfully with http://knockoutjs.com/.
Have fun.
Related
I am currently trying to learn to work with API systems using C# .net core 3 and Newtonsoft.
The following call to Steam API is what I am using
for specific game details. For example http://store.steampowered.com/api/appdetails?appids=72850
This returns JSON similar to this ( I have cut it down for simplicity )
{
"72850": {
"success": true,
"data": {
"type": "game",
"name": "The Elder Scrolls V: Skyrim",
"steam_appid": 72850,
"required_age": 0,
"is_free": false
}
}
}
Each return has the unique ID as the root in this case 72850 and I am at a loss on how to map this into an object class so I can process this data. The "data" element is what I am really interested in but as a beginner, I am at a loss.
This API indexes its response using the internal Identifier of the Item requested.
This is a common scenario and it's also a quite efficient method to organize objects based on an Indexer, which can then be used to store or retrieve these objects, from a database, for example.
A common way to deserialize JSON object indexed like this, is to use a Dictionary, where the Key is Indexer and the Value the RootObject of the class structure (the Model) that further describes the JSON properties.
Some notes on the current JSON:
The API looks like it's built to represent the JSON on a HTML document, since the internal strings are formatted ready for presentation on a HTML page. This can be less useful when used elsewhere and can also create a problem when deserializing.
I've added a trivial clean-up, replacing what can cause a problem for sure:
json = json.Replace(#"\/", "/").Replace(#"\t", "");
I've added some more properties and classes to those presented in the question: it may be useful to see when a JsonProperty attribute is needed and when is it's not. For example: the [JsonProperty("type")] attribute is added to the public string GameType { get; set; } property, since Type is a keyword that may be misinterpreted, as is Name etc.
Json.Net is not case sensitive, so the JSON property background can be assigned to a .Net property public Uri Background { get; set; } without problem.
A couple of WebSites that provide a free service to format, validate and convert JSON object to a class model:
JsonFormatter - Formatting, validation
QuickType - Multi-language Class Model generator
Download the JSON using the WebClient.DownloadString() method, clean up the JSON and deserialize:
var steamUri = new Uri("https://store.steampowered.com/api/appdetails?appids=72850")
string json = new WebClient(steamUri).DownloadString();
json = json.Replace(#"\/", "/").Replace(#"\t", "");
var steamObj = JsonConvert.DeserializeObject<Dictionary<long, SteamApps.SteamAppDetails>>(json);
Class structure:
public class SteamApps
{
public class SteamAppDetails
{
public bool Success { get; set; }
public Data Data { get; set; }
}
public class Data
{
[JsonProperty("type")]
public string GameType { get; set; }
[JsonProperty("name")]
public string GameName { get; set; }
[JsonProperty("steam_appid")]
public long SteamAppid { get; set; }
[JsonProperty("required_age")]
public long RequiredAge { get; set; }
[JsonProperty("is_free")]
public bool IsFree { get; set; }
[JsonProperty("short_description")]
public string ShortDescription { get; set; }
[JsonProperty("supported_languages")]
public string Languages { get; set; }
[JsonProperty("header_image")]
public string HeaderImage { get; set; }
public string WebSite { get; set; }
[JsonProperty("price_overview")]
public PriceOverview PriceOverview { get; set; }
public Dictionary<string, bool> Platforms { get; set; }
public List<Screenshot> Screenshots { get; set; }
public Uri Background { get; set; }
public List<Category> Categories { get; set; }
}
public class PriceOverview
{
public string Currency { get; set; }
public long Initial { get; set; }
public long Final { get; set; }
[JsonProperty("discount_percent")]
public decimal DiscountPercent { get; set; }
[JsonProperty("initial_formatted")]
public string InitialFormatted { get; set; }
[JsonProperty("final_formatted")]
public string FinalFormatted { get; set; }
}
public partial class Screenshot
{
[JsonProperty("id")]
public long Id { get; set; }
[JsonProperty("path_thumbnail")]
public string PathThumbnail { get; set; }
[JsonProperty("path_full")]
public string PathFull { get; set; }
}
public partial class Category
{
[JsonProperty("id")]
public long Id { get; set; }
[JsonProperty("description")]
public string Description { get; set; }
}
}
Since you only need the "Data" element from the json, it is fairly simple using Newtonsoft. First make a class with all the fields that the Data element contains as shown below:
public class Data
{
public string Type { get; set; }
public string Name { get; set; }
public long Steam_AppId { get; set; }
public int Required_Age { get; set; }
public bool Is_Free { get; set; }
}
Now in order to map the json response, which I'm assuming is stored in a string at the moment, you have to Deserialize it to map to your C# class. And you can do that very easily:
Edit: A more elegant solution which avoids all the string manipulation nuisance
//You already have this but I created it in order to test
string jsonResult = "{ \"72850\": " +
"{ \"success\": true, \"data\": " +
"{ \"type\": \"game\", \"name\": \"The Elder Scrolls V: Skyrim\", " +
"\"steam_appid\": 72850, \"required_age\": 0, \"is_free\": false } }";
//JObject is a class in Newtonsoft library for handling json objects
JObject jObject = JObject.Parse(jsonResult);
//Since you're sending a request to the api, either you already have the id
//"72850" or can extract it easily from uri. This line gets data's value
//by 1st searching for key = "72850" and then within that a key = "data"
JToken dataToken = jObject["72850"]["data"];
Data data = dataToken.ToObject<Data>();
Reference: https://www.newtonsoft.com/json/help/html/SerializingJSONFragments.htm
Older solution
//getting the value portion of data element/key
string jsonData = "{" + jsonResult.Substring(jsonResult.IndexOf("\"type"));
//removing the extra } from the end
jsonData = jsonData.TrimEnd('}');
//map the json string to a C# object
var dataObj = JsonConvert.DeserializeObject<Data>(jsonData);
So now you'll see the json values mapped to your Data object which in this case is dataObj. Feel free to ask questions if anything's not clear. Cheers!
I have an entity that consists of ApllicationUser as a foreign Key:
public class Trades
{
public int ID { get; set; }
public double Price { get; set; }
public double Volume { get; set; }
public string InstrumentName { get; set; }
public ApplicationUser User { get; set; }
}
Then i try to post a trade using AJAX.
var tradeData = {
"Price": 1,
"Volume": 1,
"InstrumentName": "instrumentName",
"User": "#User.Identity.Name"
};
$.ajax({
url: "/api/TradesAPI/",
method: "POST",
data: JSON.stringify(tradeData),
contentType: 'application/json; charset=utf-8',
dataType: 'json',
success: function () {
alert('executed!');
},
error: function (error) {
alert("Transacion has not been executed");
}
});
Unfortunetely ApplicationUser is not being serialised to ApplicationUSer as it is posted a string. What have I done wrong? Thanks for any help in advance.
If we have a class "A" that contains a reference to "B" and this "B" has also a reference to "A" (even if not direct), the proccess of serialization doesn't work. It's like an "infinite loop".
I suggest you change the ApplicationUser(type of User) to string and use it to get the model (on code-behind), if you need it.
If you need to get the user from within the controller, use the User property of Controller.
If you need it from the view, would populate what you specifically need in the ViewData, or you could just call User.
Ex. #User.Identity.Name
Thank you Indrit Kello. I changed my model so I have
public class Trades
{
public int ID { get; set; }
public double Price { get; set; }
public double Volume { get; set; }
public string InstrumentName { get; set; }
public string UserId { get; set; }
}
I decided to take info about user on server in API controller insted of taking data about User in a View.
public void CreateTradeAPI(Trades trade)
{
trade.UserID =User.Identity.Name;
_context.UsersTrades.Add(trade);
_context.SaveChanges();
}
And I have what i wanted :)
I'm currently working on a mobile app that communicates with an MVC4 API.
The problem I just noticed is that it seems to be unable to parse nested objects for some reason.
Example:
I'm POSTing the following data towards the url (http://localhost/DoSomething):
{
"id":610,
"dynamic":[
{
"fieldId":2756,
"fieldValue":""
},
{
"fieldId":2757,
"fieldValue":""
}
],
"person":{
"name":"test",
"age":"123",
"dateOfBirth":"test",
"groups":[
{
"groupId":1182
},
{
"groupId":1311
},
{
"groupId":673
}
]
}
}
Knowing that MVC will try to serialize it against the provided models, I have created the following model for the request:
public class PersonRequest : RequestBase
{
public class Field
{
public int fieldId { get; set; }
public string fieldValue { get; set; }
}
public class Group
{
public int groupId { get; set; }
}
public class Person
{
public string name { get; set; }
public string age { get; set; }
public string dateOfBirth { get; set; }
public IEnumerable<Group> groups { get; set; }
}
public int id { get; set; }
public IEnumerable<Field> dynamic { get; set; }
public Person person { get; set; }
}
In order to handle the input I have created the following route (which works):
routes.MapRoute(
name: "PersonRequest",
url: "DoSomething",
defaults: new { controller = "Person", action = "Generate" }
);
And my actual routing method:
[HttpPost]
public ActionResult Generate(PersonRequest request)
{
return Json(request, JsonRequestBehavior.AllowGet);
}
The response is however:
{"id":610,"person":null,"dynamic":null}
After searching for a possible solution, people said you would have to use IEnumerable for such situations instead of a List. Sadly, this didn't seem to be working for me.
Just some extra info:
I could always use JSON.stringify on the clientside on the dataobject person and dynamic, and eventually deserialize it myself on the backend (as shown in this topic: parse Json text to C# object in asp mvc 4), but there has to be a better workaround for this problem.
Change: Changed groups to dynamic in the resulting json.
Solved: https://stackoverflow.com/a/29349804/2076351
Why are you using IEnumerable<>, the query is fired at the end,
use List<> and check.
public class PersonRequest : RequestBase
{
public class Field
{
public int fieldId { get; set; }
public string fieldValue { get; set; }
}
public class Group
{
public int groupId { get; set; }
}
public class Person
{
public string name { get; set; }
public string age { get; set; }
public string dateOfBirth { get; set; }
public List<Group> groups { get; set; }
}
public int id { get; set; }
public List<Field> dynamic { get; set; }
public Person person { get; set; }
}
Solved.
Seems that my backend was properly setup. The fix was the way the data was sent towards the backend.
If you are sending a nested object towards the backend, to be parsed. You'll have to do two things:
Use JSON.stringify on the entire data send to the data
Use a proper content-type, e.g.:
xhr.open("POST", url);
xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8");
xhr.setRequestHeader("hash", params.hash);
xhr.send(JSON.stringify(params.data));
So I have a ViewModel:
public class PrelimViewModel
{
public int? PrelimId { get; set; }
public int JobId { get; set; }
public string Code { get; set; }
public string Description { get; set; }
public string Comment { get; set; }
public string Unit { get; set; }
public int? Qty { get; set; }
public decimal? BidPrice { get; set; }
public bool Accepted { get; set; }
public int? OriginalPrelimId { get; set; }
public string Option { get; set; }
public List<RefCodeViewModel> Codes { get; set; }
public List<UnitViewModel> Units { get; set; }
public List<OptionLetterViewModel> Options { get; set; }
}
A GetPrelim controller method that returns List<PrelimViewModel>
A ko.mapper of the List of PrelimViewModel client side:
viewModel = ko.mapping.fromJS({ Prelims: data });
ko.applyBindings(viewModel);
Do some work, ready to save:
function savePrelims(elem) {
var $form = $(elem).parents('form');
$.ajax({
url: $form.attr('action'),
type: "POST",
data: ko.toJSON(viewModel),
datatype: "json",
contentType: "application/json charset=utf-8",
success: function(data) { toastr.success('Options Saved!'); },
error: function(data) { }
});
}
And I cannot get my MVC Method to parse the JSON:
public void AddPrelims(List<PrelimViewModel> Prelims)
You have wrapped your list into the Prelims property in your KO viewmodel, but on the server side you are expecting just a list not with an object which has the list in its Prelims property.
So to fix this you just need to send the list in your ajax request:
data: ko.toJSON(viewModel.Prelims()),
However you don't necessary need to wrap your list if you won't have any additional properties on your viewmodel, because you can just do:
viewModel = ko.mapping.fromJS(data);
ko.applyBindings(viewModel);
And then in your view you can bind to $data which is refering the current viewmodel which will be your array:
<div data-bind="foreach: $data">
...
</div>
And in this case you don't have to change your ajax call and data: ko.toJSON(viewModel), should work fine.
However this foreach: $data is kinda strange and it is not the best solution so you are probably better if you stick to your original approach with ko.mapping.fromJS({ Prelims: data }); and sending the correct data back to your controller.
I have a form that is flexible. Meaning that depending on what selection you make in a dropdownlist the fields will be different. Also, the controller action that is called will also change. I am trying to get this to work with just a simple example, but I can't find how to submit data to the controller and have the controller map it correctly to a defined class.
Clarification: When a user creates a new question that has only one choice this is the form/controller that they are using. However, when they create a question with multiple choices I would like to use the same form/controller. The error i am getting is that the object is null. Which i think means that whenever the data is being passed to the controller, it is not being properly mapped into the object. How can i map the data explicitly into my defined object? Or should i do this whole thing differently?
Here is the controller:
[HttpPost]
public ActionResult CreateSimpleQuestion(SimpleQuestion question)
{
if (ModelState.IsValid)
{
question.question.is_counted = true;
question.question.DateCreated = DateTime.Now;
db.Questions.Add(question.question);
db.QuestionChoices.Add(question.choices[0]);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(question);
}
Here is the class:
[Serializable]
public class SimpleQuestion
{
public Question question { get; set; }
public QuestionChoices[] choices { get; set; }
}
Here is the script that is calling the controller action:
<script type="text/javascript">
$("form").on("submit", function (event) {
event.preventDefault();
var data = $('form').serialize();
console.log(data);
$.post('/Question/CreateSimpleQuestion/', data);
});
</script>
This is the serialized data:
QuestionTitle=faketitle&Keywords=fakekeywords&Description=fakedescription&Comments=fakecomments&QuestionType=Simple&DisplayText=fakequestiontext&OrderNumber=fakeorder
And in case you need the specifics of the model:
public class Question
{
public int QuestionId { get; set; }
public string QuestionTitle { get; set; }
public DateTime DateCreated { get; set; }
public string QuestionType { get; set; }
public string Keywords { get; set; }
public bool is_counted { get; set; }
public int? ParentId { get; set; }
[Column(TypeName = "ntext")]
[MaxLength]
public string Description { get; set; }
[Column(TypeName = "ntext")]
[MaxLength]
public string Comments { get; set; }
//These define a one to many relationship
public virtual ICollection<TeamQuestionRoster> TeamQuestionRosters { get; set; }
public virtual ICollection<Response> Responses { get; set; }
public virtual ICollection<QuestionChoices> QuestionChoices { get; set; }
}
public class QuestionChoices
{
public int QuestionChoicesId { get; set; }
public string DisplayText { get; set; }
public int OrderNumber { get; set; }
public bool is_correct { get; set; }
//These are the FK properties
public int QuestionId { get; set; }
//This defines the FK Relationships
public virtual Question Question { get; set; }
//These define a one to many relationship
public virtual ICollection<ResponseDetails> ResponsDetails { get; set; }
}
I think you might be having a issue with your media type. Try posting JSON like this:
$.post('/Question/CreateSimpleQuestion', data, function() { /* success callback */ }, 'application/json');
EDIT:
The $.post shorthand method might be expecting 'json' rather than 'application/json'. I typically use $.ajax instead.
TAKE 2:
Based the JSON you posted, I can see that your data is not being serialized properly. You're getting name/value pairs instead of actual JSON objects. Your JSON data should like this:
{
"QuestionId" : 0,
"QuestionTitle" : "My Title",
"Description": "My Description"
}
Here's another SO post explaining how to convert the jQuery serialize results to an appropriate JSON object: Convert form data to JavaScript object with jQuery
Hope that helps :)