Currently I am doing an API call via jQuery, my question is, is there away to do this call in C# or away to convert the results of an API call to an ASP.NET List of Model Objects?
Here is my Model
public class TeamStatsClass
{
public int id { get; set; }
public string name { get; set; }
public string league { get; set; }
public string division { get; set; }
}
And here is my current ajax call
$.ajax({
url: "https://statsapi.web.nhl.com/api/v1/teams?sportId=1",
success: function (data) {
for (var team of data.teams) {
console.log(team.name);
}
}
});
UPDATE
I changed my classes to look like so:
public class StatsTeamsClass
{
public IEnumerable<Teams> teams { get; set; }
public string copyright { get; set; }
}
public class Division
{
public int id { get; set; }
public string name { get; set; }
public string link { get; set; }
}
public class Teams
{
public int id { get; set; }
public string name { get; set; }
public string link { get; set; }
public League league { get; set; }
public Division division { get; set; }
}
and created this method which indeeds puts the results in model object:
public async System.Threading.Tasks.Task<StatsTeamsClass> GetTeams()
{
HttpClient Http = new HttpClient();
var json = await Http.GetStringAsync("https://statsapi.web.nhl.com/api/v1/teams?sportId=1");
StatsTeamsClass teams = JsonConvert.DeserializeObject<StatsTeamsClass>(json);
return teams;
}
But when I try to call this method in another controller, it just hangs there, no error, no nothing, I am assuming it will just time out after a while
public class HomeController : Controller
{
APIController webService = new APIController();
public ActionResult Index()
{
var item = webService.GetTeams().Result.teams;
return View();
}
}
(GetTeams() is inside the controller APIController)
So what would be the proper way to A. get the results of an API in object model and then call those results?
The controller action needs to be made async as well to avoid mixing async-await and blocking calls like .Result or .Wait() that could potentially cause deadlocks.
Reference Async/Await - Best Practices in Asynchronous Programming
public class HomeController : Controller {
APIController webService = new APIController();
public async Task<ActionResult> Index() {
var model = await webService.GetTeams();
var teams = model.teams;
return View();
}
}
Assuming APIController is an actual ApiContoller
public class APIController : ApiController {
//Your original code
public async Task<StatsTeamsClass> GetTeams() {
HttpClient Http = new HttpClient();
var json = await Http.GetStringAsync("https://statsapi.web.nhl.com/api/v1/teams?sportId=1");
StatsTeamsClass teams = JsonConvert.DeserializeObject<StatsTeamsClass>(json);
return teams;
}
//...
}
I would suggest not calling APIController directly like that from the HomeController and instead extract the GetTeams() method out into a reusable service
public class WebService {
static Lazy<HttpClient> http = new Lazy<HttpClient>();
public async Task<T> GetAsync<T>(string url) {
var json = await http.Value.GetStringAsync(url);
return JsonConvert.DeserializeObject<T>(json);
}
public Task<StatsTeamsClass> GetTeamsAsync() {
var url = "https://statsapi.web.nhl.com/api/v1/teams?sportId=1";
return GetAsync<StatsTeamsClass>(url);
}
}
Reference You're using HttpClient wrong
that can be properly used in HomeController
public class HomeController : Controller {
public async Task<ActionResult> Index() {
// Ideally web service should be injected but that topic
// is outside of the scope of the question at the moment.
var webService = new WebService();
var model = await webService.GetTeamsAsync();
var teams = model.teams;
//...
return View(teams);
}
}
The assumption here is that the project is a mixed Asp.Net MVC and Web Api 2+
Index.cshtml
#model IEnumerable<Teams>
#{
ViewBag.Title = "Teams";
}
#if(Model != null && Model.Count() > 0) {
#foreach (var #team in Model) {
<p>#team.name</p>
}
}
Yes, the equivalent in C# would be to use HttpClient. You're best off creating a static instance of the class that you reuse for a particular kind of repeated call:
private static readonly HttpClient Http = new HttpClient();
and then used it from an async method using Newtonsoft.Json like this:
var json = await Http.GetStringAsync("https://statsapi.web.nhl.com/api/v1/teams?sportId=1");
You can then parse this string of JSON into a model class like this:
var model = JsonConvert.DeserializeObject<TeamStatsClass>(json);
As the question is answered by #Daniel above just want to add couple of more points here The json you are getting cannot be directly casted to TeamStatsClass you might have to introduce another base class as teams is the collection in the json you are getting.
Im posting it here to get a clearer view
public class ResponseBaseClass
{
public IEnumerable<TeamStatsClass> teams { get; set; }
public string copyright { get; set; }
}
public class TeamStatsClass
{
public int id { get; set; }
public string name { get; set; }
public Division division { get; set; }
}
public class Division
{
public int id { get; set; }
public string name { get; set; }
public string nameShort { get; set; }
public string link { get; set; }
}
HttpClient Http = new HttpClient();
var json = await Http.GetStringAsync("https://statsapi.web.nhl.com/api/v1/teams?sportId=1");
var model = JsonConvert.DeserializeObject<ResponseBaseClass>(json);
var yourTeamModelObj = model.teams;
Related
I am practicing with web api. My goal is to create a Get endpoint, which receive data from an external api, then return a different result. external api link: https://www.themealdb.com/api/json/v1/1/search.php?f=a, The external api data looks like:
{
"meals": [
{
"idMeal": "52768",
"strMeal": "Apple Frangipan Tart",
"strDrinkAlternate": null,
"strCategory": "Dessert",
.....
},
{
"idMeal": "52893",
"strMeal": "Apple & Blackberry Crumble",
....
}
]
}
I want my endpoint provide a different result like the following:
[
{
"idMeal": "52768",
"strMeal": "Apple Frangipan Tart",
"ingredients": ["Apple", "sugar"...]
},
{
"idMeal": "52893",
"strMeal": "Apple & Blackberry Crumble",
"ingredients": ["Apple", "sugar"...]
}
]
The following code is what I attempted so far, It's working, but the moment I changed property ingredient1 from public to private, that ingredient in list will become null, also, there are so many ingredients, some of them are null by default, I don't want to add them if they are null, how can I fix these two issues? Thanks a lot
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Mvc;
using RestSharp;
namespace testAPI.Controllers;
public class Content
{
[JsonPropertyName("meals")]
public List<Meal> Meals { get; set; }
}
public class Meal
{
[JsonPropertyName("idMeal")]
public string MealId { get; set; }
[JsonPropertyName("strMeal")]
public string Name { get; set; }
[JsonPropertyName("strIngredient1")]
public string Ingredient1 { get; set; }
[JsonPropertyName("strIngredient2")]
public string Ingredient2 { get; set; }
[JsonPropertyName("strIngredient20")]
public string Ingredient20 { get; set; }
public List<string> Ingredients
{
get { return new List<string>(){Ingredient1, Ingredient2, Ingredient20};}
}
}
[ApiController]
[Route("api/[controller]")]
public class DishesController : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetAllRecipes()
{
var client = new RestClient($"https://www.themealdb.com/api/json/v1/1/search.php?s=");
var request = new RestRequest();
var response = await client.ExecuteAsync(request);
var mealList = JsonSerializer.Deserialize<Content>(response.Content);
return Ok(mealList.Meals);
}
}
To address the problems one at a time...
the moment I changed property ingredient1 from public to private, that ingredient in list will become null
Changing the access modifier affects both deserialization and serialization, so this cannot be used to only stop it from serializing the property. You should split the data models up into what you want to receive and what you want to expose/return.
there are so many ingredients, some of them are null by default, I don't want to add them if they are null
Addition to splitting up the data models you can handle this when mapping from one model to the other.
The following code should fix both issues:
namespace TheMealDb.Models
{
// These are the models you receive from TheMealDb
// JSON converted to classes with https://json2csharp.com/
public class Root
{
public List<Meal> meals { get; set; }
}
public class Meal
{
public string idMeal { get; set; }
public string strMeal { get; set; }
public string strIngredient1 { get; set; }
public string strIngredient2 { get; set; }
public string strIngredient3 { get; set; }
// Other properties removed for brevity...
}
}
namespace Internal.Models
{
// This is the model you want to return from your controller action
public class Meal
{
[JsonPropertyName("id")] // No need to use the same name as from themealdb
public string Id { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("ingredients")]
public List<string> Ingredients { get; set; }
}
}
Now, to fetch, map and return the data in your controller action:
[HttpGet]
public async Task<IActionResult> GetAllRecipes()
{
var client = new RestClient($"https://www.themealdb.com/api/json/v1/1/search.php?s=");
var request = new RestRequest();
var response = await client.ExecuteAsync(request);
// Deserialize to the "TheMealDb" models
var mealList = JsonSerializer.Deserialize<TheMealDb.Models.Root>(response.Content);
// Map to your own models
var myMealList = mealDbList.meals?.Select(MapToInternal);
return Ok(myMealList);
}
// Map "TheMealDb" model to your own model
private Internal.Models.Meal MapToInternal(TheMealDb.Models.Meal externalMeal)
{
return new Internal.Models.Meal
{
Id = externalMeal.idMeal,
Name = externalMeal.strMeal,
Ingredients = new []
{
externalMeal.strIngredient1,
externalMeal.strIngredient2,
externalMeal.strIngredient3,
// ...
}
// Remove empty/null ingredients
.Where(ingr => !string.IsNullOrEmpty(ingr))
.ToList()
};
}
See the code in action.
I am using this class (representing goal) as model:
public class Goal
{
public int Id { get; set; }
public int UserId { get; set; }
public String Name { get; set; }
public List<GoalDay> Days { get; set; } = new List<GoalDay>();
public bool IsAllDayGenerated { get; set; }
public DateTime StartDateGenerated { get; set; }
public TimeSpan LengthTimeGenerated { get; set; }
public int TotalValue { get; set; }
public string Unit { get; set; }
}
Method in service sending data:
public async Task AddGoal(Goal goal)
{
var result = await _http.PostAsJsonAsync<Goal>("api/goal", goal);
if (result.StatusCode != System.Net.HttpStatusCode.OK)
{
_toastService.ShowError(await result.Content.ReadAsStringAsync());
}
else
{
_toastService.ShowSuccess(await result.Content.ReadAsStringAsync());
}
Method in controller receiving data:
[HttpPost]
public async Task<IActionResult> CreateGoal(Goal goal)
{
var user = await _utilityService.GetUser();
goal.UserId = user.Id;
_context.Goals.Add(goal);
await _context.SaveChangesAsync();
return Ok("Goal added days:" + goal.Days.Count);
}
When I send data using HttpPost to server value saved in TimeSpan (in variable LengthTimeGenerated ) is there. In the upper part of picture can be seen printscreen of network traffic.
But afterward in controller data are gone. In the lower part of the picture can be seen printscreen of model zero value of the same variable in controller.
Looks like the classic case of the camel case changes in the System.Text.Json compared to Newtownsoft. (System.Text.Json is case sensitive by default)
Try adding serialization option to use camel case.
private readonly JsonSerializerOptions _jsonSerializerOptions = new() { PropertyNameCaseInsensitive = true };
// usage
public async Task AddGoal(Goal goal)
{
var result = await _http.PostAsJsonAsync<Goal>("api/goal", goal, _jsonSerializerOptions);
//... etc
}
Other example of usage:
var myParsedObject = await JsonSerializer.DeserializeAsync<SomeResponseType>(await response.Content.ReadAsStreamAsync(), _jsonSerializerOptions);
I'm developing an api in net core.
I've done a post function in which I send an object containing multiple parameters and a list within another list.
When I'm debugging the code the function is called correctly but I find that the second list always arrives null.
The rest of the data arrives at you correctly. I have done different tests with other objects and everything works correctly.
It is this case in which the list within another the second one arrives null.
My code:
example request input
{
"Name": "TestName",
"Related1":
[{
"id1": "TestNameRelated1",
"Related2":
[{
"id2": "TestNameRelated2"
}]
}]
}
[HttpPost]
public resultExample Test([FromBody]TestClass test)
{
//do something
}
[DataContract]
public class TestClass
{
[DataMember]
public string Name { get; set; }
[DataMember]
public List<TestClassArray> Related1 { get; set; }
}
[DataContract]
public class TestClassArray
{
[DataMember]
public string id1 { get; set; }
[DataMember]
public List<TestClassArray2> Related2 { get; set; }
}
[DataContract]
public class TestClassArray2
{
[DataMember]
public string id2 { get; set; }
}
This api was previously made in .NET framework 4.8 and this case worked correctly.
Now I'm passing the api to .Net5.
Could it be that in .Net5 it is not allowed to pass lists within other lists?
Do you have to enable some kind of configuration to be able to do this now?
You need use class/DTO with constructor like shown below and you should be good to go. I have uploaded this sample API app's code working with .net5.0 on my GitHub here.
public class TestClass
{
public TestClass()
{
Related1 = new List<TestClassArray>();
}
public string Name { get; set; }
public List<TestClassArray> Related1 { get; set; }
}
public class TestClassArray
{
public TestClassArray()
{
Related2 = new List<TestClassArray2>();
}
public string id1 { get; set; }
public List<TestClassArray2> Related2 { get; set; }
}
public class TestClassArray2
{
public string id2 { get; set; }
}
public class ResultExample
{
public string StatusCode { get; set; }
public string Message { get; set; }
}
Controller Post Method
[HttpPost]
[ProducesResponseType(typeof(ResultExample), 200)]
public ResultExample Post([FromBody] TestClass test)
{
ResultExample testResult = new ResultExample();
TestClass test2 = new TestClass();
TestClassArray testClassArray = new TestClassArray();
TestClassArray2 testClassArray2 = new TestClassArray2();
test2.Name = test.Name;
foreach (var item in test.Related1)
{
foreach (var item2 in item.Related2)
{
testClassArray2.id2 = item2.id2;
}
testClassArray.Related2.Add(testClassArray2);
}
test2.Related1.Add(testClassArray);
Console.WriteLine(test2);
testResult.Message = "New Result added successfullly....";
testResult.StatusCode = "201";
return testResult;
}
Swagger Input Sample Payload
Post Controller Result
Response of Sample input payload,(You can change it to default 201 response code as well)
I had a similar issue.
API method shows List was null
In my case a date field was not well formatted
So I use SimpleDateFormat on Android Studio with a correct datetime format
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss",Locale.US);
item.setDate(dateFormat.format(calendar.getTime()));
and works fine
I am using ASP.NET Core 5.0 and I have Users Controller with Register method, which receives UserRegisterInputModel. The problem is that all responses from my API are in specific format, but the ApiController auto-validates the input model and returns BadResponse in another format.
This is my abstract response model
public abstract class ResponseModel
{
public ResponseModel(bool successfull, int statusCode)
{
this.Successfull = successfull;
this.StatusCode = statusCode;
this.ErrorMessages = new List<string>();
}
public bool Successfull { get; set; }
public int StatusCode { get; set; }
public List<string> ErrorMessages { get; set; }
public object Data { get; set; }
}
And this is my BadResponseModel
public class BadResponseModel : ResponseModel
{
public BadResponseModel()
: base(false, 400)
{
}
}
This is part of my Register method in Users Controller.
[HttpPost]
public async Task<IActionResult> Register(UserRegisterInputModel input)
{
if (!ModelState.IsValid)
{
return Json(new BadResponseModel()
{
ErrorMessages = new List<string>()
{
"Invalid register information"
}
});
}
ApiController functionality auto-validates my model and the return statement for BadResponseModel is never reached. Is there any way of stopping auto-validation or changing the default response from ApiController validation ?
You can disable automatic model state validation by using "ApiBehaviourOptions.SuppressModelStateInvalidFilter" property:
https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.apibehavioroptions.suppressmodelstateinvalidfilter?view=aspnetcore-5.0
So a very basic example of usage would be like that (ConfigureServices method in Startup.cs):
services.Configure<ApiBehaviorOptions>(opt => { opt.SuppressModelStateInvalidFilter = true; });
I'm creating a static class with static methods for helping the controllers to do their job. When build the application I get the following error:
Error 40 'System.Web.Mvc.Controller.Content(string)' is inaccessible due to its protection level"
Any idea how to solve this problem?
Notes:
It's a c# mvc aplication
public static ActionResult GetAlbumJSON(AlbumVO album)
{
return Controller.Content(
JsonConvert.SerializeObject(new
{
max_car = #ABookClient.maxCharsProjecName,
trans_img = #ABookClient.Transparent_Image,
show_description = #ABookClient.Show_Product_Description,
product_type = "Album",
obj = CreateObjAlbumVO(album),
})
);
}
Content method is protected internal, so you can't use it outside of controller.
Controller.Content Method. Most probably your static class violates SRP principle. Let him do his job (initializing, serializing,...) and controller - controller's job - return result to the client.
protected internal ContentResult Content(string content)
It would look smth like:
public static class MyHelper
{
public static object GetAlbum(AlbumVO album)
{
return new
{
max_car = #ABookClient.maxCharsProjecName,
trans_img = #ABookClient.Transparent_Image,
show_description = #ABookClient.Show_Product_Description,
product_type = "Album",
obj = CreateObjAlbumVO(album),
};
}
}
public class AlbumController : Controller
{
public ActionResult GetAlbums(int id)
{
var album = Context.GetAlbum(id);
var convertedResult = MyHelper.GetAlbum(album);
return Json(convertedResult);
}
}
Also I'd advice to take a look at AutoMapper for creating client response objects
I think this is valid case for a view-model for a JSON result since you do want a separation between the Domain model and the data sent back to the client. Using a view model also gives you a proper place to put this mapping between the domain model and the view (the JSON) so you don't need to delegate to a helper class.
public class AlbumModel
{
[JsonProperty(PropertyName = "max_car")]
public int MaxChars { get; private set; }
[JsonProperty(PropertyName = "trans_img")]
public string TransparentImage { get; private set; }
[JsonProperty(PropertyName = "product_type")]
public string ProductType { get; private set; }
[JsonProperty(PropertyName = "obj")]
public AlbumInfo Object { get; private set; }
[JsonProperty(PropertyName = "show_description")]
public bool ShowProductDescription { get; private set; }
public AlbumModel(AlbumVO album)
{
MaxChars = album.maxCharsProjecName;
TransparentImage = album.Transparent_Image;
ShowProductDescription = album.Show_Product_Description;
ProductType = "Album";
Object = new AlbumInfo(album);
}
}
The AlbumInfo class provides additional mappings for your JSON result, which becomes the "obj" property sent back to the client.
public class AlbumInfo
{
// ... define properties here
public AlbumInfo(AlbumVO album)
{
// ... map properties here
}
}
And your controller becomes nice and clean:
public class AlbumController : Conrtoller
{
public ActionResult GetAlbums(int id)
{
var album = Context.GetAlbum(id);
var model = new AlbumModel(album);
return Json(model);
}
}