I am working on a ASP.NET Web Api and I'm having an issue figuring out how to display data to the api.
So to explain what I need to figure out. After accessing the objects from the Json file and converting JSON to a .NET type. I believe I should be able to display these on the api?
For example, whatever field I ask for, it should return the result sorted by that field.
Now I need to display this data in different ports. For example.
https://host:port/api/books returns all unsorted (Book1-Book13)
https://host:port/api/books/id returns all sorted by id (Book1-Book13)
https://host:port/api/books/id/1 returns all with id containing '1' sorted by id (Book1, Book10-13)
I have followed this tutorial by microsoft and the application seems to be working in regards to starting and using different ports.
This is the code I have set up in my controller class BooksController.cs
[Route("api/[controller]")]
[ApiController]
public class BooksController : ControllerBase
{
[HttpGet]
public ActionResult GetAllBooks()
{
string[] books = { "book1", "book2", "book3" };
if (!books.Any())
return NotFound();
return Ok(books);
}
[HttpGet("id/{id}")]
public string GetBookById(int id)
{
return $"book: {id}";
}
[HttpGet("author/{author}")]
public string GetBookByAuthor(string author)
{
return $"author: {author}";
}
This is my startup.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
GetAllBooks();
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddControllers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
public void GetAllBooks()
{
string _path = $"C:\\Users\\filip\\source\\repos\\NexerGroupApi\\NexerGroupApi\\books.json";
string jsonFromFile;
using (var reader = new StreamReader(_path))
{
jsonFromFile = reader.ReadToEnd();
}
var booksFromJsonFile = JsonConvert.DeserializeObject<List<NexerGroupApi.Book>>(jsonFromFile);
JArray bookArray = JArray.FromObject(booksFromJsonFile);
IList<Book> bookList= bookArray.Select(p => new Book
{
Id = (string)p["Id"],
Author = (string)p["Author"],
Title = (string)p["Title"],
Genre = (string)p["Genre"],
Price = (string)p["Price"],
Publish_date = (string)p["Publish_date"],
Description = (string)p["Description"]
}).ToList();
}
public class Book
{
public string Id { get; set; }
public string Author { get; set; }
public string Title { get; set; }
public string Genre { get; set; }
public string Price { get; set; }
public string Publish_date { get; set; }
public string Description { get; set; }
}
I wonder where I actually set up my GetAllBooks() method so that I can then access them in the BooksController.cs and be able to do api/books/bookNameFromJsonFile and show that specific object with all of it's contents such as id, author, title, genre, etc.
I know that my IList<Book> bookList in the class startup.cs does not work, for some reason every variable get's null value. Although not sure why.
As the error suggests, you have two routes that are ambiguous [HttpGet("{id}")] and [HttpGet("{author}")]
Given
When you hit api/books/1 the 1 could be the author or the bookid.
Consider changing the route of the author method to:
[HttpGet("author/{author}")]
[HttpGet("{id}")]
public string GetBookById(int id)
{
return $"book: {id}";
}
[HttpGet("author/{author}")]
public string GetBookByAuthor(string author)
{
return $"author: {author}";
}
So requests would route as follows:
api/books/1 would invoke GetBookById
api/books/author/jsmith would invoke GetBookByAuthor
Edit: Add list filtering:
To find a Book from the List<Book> then this can be done:
private readonly List<Book> _bookList; //populate this with books from json file
[HttpGet("{id}")]
public IActionResult GetBookById(int id)
{
var book = GetBookObjectById(id);
if(book is null) return NotFound();
return Ok(book);
}
private Book GetBookObjectById(int id)
{
return _bookList.FirstOrDefault(book => book.Id == id);
}
private Book GetBookObjectByAuthor(string author)
{
return _bookList.FirstOrDefault(book => book.Author == author);
}
Related
I got a small problem here. I got a course class and a User. I want to show all the Users inside a Course through the API.
the error i get,
'Object reference not set to an instance of an object.'
And this is my controller method,
var objList = _courseRepo.GetUsers(CourseId);
if (objList == null)
{
return NotFound();
}
var objToShow = new List<ViewCourseDetailsDTO>();
foreach (var obj in objList)
{
objToShow.Add(_mapper.Map<ViewCourseDetailsDTO>(obj));
}
return Ok(objToShow);
The Error i got is inside the Foreach-loop. It says that i need to create an object...
This is how my DTO classes looks like,
public class ViewCourseDetailsDTO
{
public int CourseId { get; set; }
public string CourseTitle { get; set; };
public ICollection<UserDTO>? Users { get; set; } = new List<UserDTO>();
}
And this one,
public class UserDTO
{
public string ID { get; set; }
public string UserName { get; set; }
public string Name { get; set; }
}
Do you think i have to break out the UserDTO somehow? Is it Therefore u think ?
if you want to see my CourseRepository than its here,
public ICollection<Course> GetUsers(int courseId)
{
return _db.Course.Where(c => c.CourseId == courseId).Include(a => a.Users).ToList();
}
Would be really grateful if you could help me out here.
Wohooo I found it, damnit!
On my controller, i forgot to put in mapper here,
public CourseController(ICourseRepository courseRepo, IMapper mapper)
{
_courseRepo = courseRepo;
_mapper = mapper;
}
I had injected it correct at the top but forgot to put it inside there ^
I'm trying to call the business endpoint of Yelp's GraphQL api with my asp.net core mvc application using GraphQLHttpClient. I have the api and bearer token configured in my client instance. I followed the query structure here using business as the endpoint and I just wanted Id and Name fields from the data. When I call SendQueryAsync(query), I get a GraphQL Error from the response. I'm not sure if I'm making an improper httprequest and/or my query is written wrong. I couldn't find any YouTube videos, stackoverflow questions, or github projects regarding consuming Yelp's GraphQL api using C#. Any help is appreciated. Thank you! Below is my source code and attached response.
[Update: Resolved Issued]
There were a collection of issues. Added additional required fields with variables to YelpGraphQL query for GraphQL request. More about query structure and variable declaration is explained in this thread. Overrided the casing of the fields (ty Neil). Fixed the responsetype class and added the missing classes (ty Neil). Added searchconsumer class to controller via dependency injection. Also I will post copied text of exceptions next time.
Classes
public class Business
{
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
}
public class Search
{
[JsonPropertyName("business")]
public List<Business> business { get; set; }
}
public class SearchResponseType
{
[JsonPropertyName("search")]
public Search Search { get; set; }
}
public interface ISearchConsumer
{
public Task<List<Business>> GetAllBusinesses();
}
public class SearchConsumer : ISearchConsumer
{
private readonly ApplicationDbContext _dbContext;
public SearchConsumer(ApplicationDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<List<Business>> GetAllBusinesses()
{
var authorization = _dbContext.Authorizations.FirstOrDefault().Token;
var _client = new GraphQLHttpClient("https://api.yelp.com/v3/graphql", new NewtonsoftJsonSerializer());
_client.HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authorization);
var query = new GraphQLRequest
{
Query = #"
query($termId: String $locationId: String){
search(term:$termId location:$locationId) {
business {
id
name
}
}
}",
Variables = new
{
termId = "burrito",
locationId = "san francisco"
}
};
var response = await _client.SendQueryAsync<SearchResponseType>(query);
var businesses = response.Data.Search.business;
return businesses;
}
}
Controllers
public class YelpGraphQLController : Controller
{
private readonly ISearchConsumer _consumer;
public YelpGraphQLController(ISearchConsumer consumer)
{
_consumer = consumer;
}
public IActionResult Index()
{
return View();
}
[HttpGet]
public async Task<IActionResult> Get()
{
var businesses = await _consumer.GetAllBusinesses();
return Ok(businesses);
}
}
Program
ConfigureServices(builder.Services);
void ConfigureServices(IServiceCollection services)
{
services.AddTransient<ISearchConsumer, SearchConsumer>();
}
YelpGraphQL Json Data Example
{
"data": {
"search": {
"business": [
{
"id": "wGl_DyNxSv8KUtYgiuLhmA",
"name": "Bi-Rite Creamery"
},
{
"id": "lJAGnYzku5zSaLnQ_T6_GQ",
"name": "Brenda's French Soul Food"
}
]
}
}
}
Debug GraphQL Error
I'm guessing that the deserialization isn't working because of the casing of the fields vs your class, which you can override like so:
public class Business
{
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
}
public class ResponseBusinessCollectionType
{
[JsonPropertyName("businesses")]
public List<Business> Businesses { get; set; }
}
My project is Application with Recipes (cooking) .NET Core 5.0
And i have problem with adding a new recipe (HttpPost) web api
On postman my response is:
"A possible object cycle was detected. This can either be due to a cycle or if the object depth is larger than the maximum allowed depth of 32. Consider using ReferenceHandler.Preserve on JsonSerializerOptions to support cycles."
When i'm creating a new recipe it should use recipeToCreateDto instead of Recipe - which contains all properties (circular referencing)
Could you help me how to make it working properly. How to map etc.
https://i.postimg.cc/Mphv7zRH/screen.png <- screen here
I'm using AutoMapper for mapping classes and Repository Pattern.
public class AppUser
{
public int Id { get; set; }
public string UserName { get; set; }
public ICollection<Recipe> Recipes {get; set;}
}
}
User has many recipes.
public class Recipe
{
public int Id { get; set; }
public string Name { get; set; }
public AppUser AppUser { get; set; }
public int AppUserId { get; set; }
}
Data Transfer Object
public class RecipeForCreateDto
{
[Required]
[StringLength(50, MinimumLength = 3, ErrorMessage = "You must specify name between 3 and 50 characters")]
public string Name { get; set; }
public int AppUserId { get; set; }
public int AuthorId { get; set; }
}
In my AutoMapperProfiles.cs
public class AutoMapperProfiles : Profile
{
public AutoMapperProfiles()
{
CreateMap<RecipeForCreateDto, Recipe>();
}
Recipe Interface
public interface IRecipeRepository
{
Task<Recipe> AddNewRecipe(Recipe recipe);
}
public class RecipeRepository : IRecipeRepository
{
private readonly DataContext _context;
private readonly IMapper _autoMapper;
public RecipeRepository(DataContext context, IMapper autoMapper)
{
_autoMapper = autoMapper;
_context = context;
}
public async Task<Recipe> AddNewRecipe(Recipe recipe)
{
await _context.Recipes.AddAsync(recipe);
await _context.SaveChangesAsync();
return recipe;
}
}
Users Controller:
User.GetUsername() is static method that is getting User's username.
[HttpPost("add-recipe")]
public async Task<ActionResult> AddNewRecipe(RecipeForCreateDto recipeForCreateDto)
{
var userFromRepo = await _userRepository.GetUserByUsernameAsync(User.GetUsername());
recipeForCreateDto.Name = recipeForCreateDto.Name.ToLower();
if (await _recipeRepository.RecipeExists(recipeForCreateDto.Name))
return BadRequest("Recipe with that name already exists!");
var recipeToCreate = _autoMapper.Map<Recipe>(recipeForCreateDto);
recipeToCreate.AppUserId = userFromRepo.Id;
var createdRecipe = await _recipeRepository.AddNewRecipe(recipeToCreate); // here is problem
var recipeToReturn = _autoMapper.Map<RecipeForDetailDto>(createdRecipe);
return CreatedAtRoute("GetRecipe", new { controller = "Recipes", id = createdRecipe.Id }, recipeToReturn);
}
"A possible object cycle was detected. This can either be due to a
cycle or if the object depth is larger than the maximum allowed depth
of 32. Consider using ReferenceHandler.Preserve on
JsonSerializerOptions to support cycles."
For this issue , you can add the following code in startup.cs ConfigureServices method:
services.AddControllersWithViews()
.AddNewtonsoftJson(options =>
options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore
);
[HttpPost("{recipeForCreateDto}")]
public async Task < ActionResult > AddNewRecipe([FromBody] RecipeForCreateDto recipeForCreateDto) {
var userFromRepo = await _userRepository.GetUserByUsernameAsync(User.GetUsername());
recipeForCreateDto.Name = recipeForCreateDto.Name.ToLower();
if (await _recipeRepository.RecipeExists(recipeForCreateDto.Name)) return BadRequest("Recipe with that name already exists!");
var recipeToCreate = _autoMapper.Map < Recipe > (recipeForCreateDto);
recipeToCreate.AppUserId = userFromRepo.Id;
var createdRecipe = await _recipeRepository.AddNewRecipe(recipeToCreate); // here is problem
var recipeToReturn = _autoMapper.Map < RecipeForDetailDto > (createdRecipe);
return CreatedAtRoute("GetRecipe", new {
controller = "Recipes",
id = createdRecipe.Id
},
recipeToReturn);
}
It is important to note that the default serializer in .Net is now from the System.Text.Json.Serialization namespace. This serializer actually does a great job of both serializing and deserializing circular references.
To turn this feature on place the following code in your Startup.cs:
services
.AddControllers()
.AddJsonOptions(c => c.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve);
Documented here: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-preserve-references
I have this problem I'm following the Api course on pluralsight and I've been trying to understand why when I pass an invalid Dto in a post request it doesn't get set to null.
Here is my Dto
public class AuthorCreateDto
{
public string Name { get; set; }
public string LastName { get; set; }
public int GenreId { get; set; }
public int CountryId { get; set; }
}
and action
[Route("")]
[HttpPost]
public ActionResult<AuthorDto> CreateAuthor([FromBody]AuthorCreateDto authorCreateDto)
{
if (authorCreateDto == null)
return BadRequest();
var author = Mapper.Map<Author>(authorCreateDto);
if (TryValidateModel(author))
return BadRequest();
var newAuthor = _authorService.CreateAuthor(author);
var newAuthorDto = Mapper.Map<AuthorDto>(newAuthor);
return CreatedAtRoute("GetAuthor", new { id = newAuthor.Id }, newAuthorDto);
}
so when I post an invalid json as
{
"epa": 2,
"wdawd": "awdawd"
}
authorCreateDto does not get set to null while on the course it does. Idk whats going on thank you
For Asp.Net Core, its built-in serializer is Newtonsoft.Json and for FromBody, it will use JsonInputFormatter to bind the request body to model.
By default, SerializerSettings.MissingMemberHandling is Ignore which return default value for the properties which is missing in the request body.
If you prefer null for authorCreateDto, you could configure it with Error by
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
.AddJsonOptions(options => {
options.SerializerSettings.MissingMemberHandling = Newtonsoft.Json.MissingMemberHandling.Error;
});
}
I am building WebApi2 project to expose some RESTful service. Let's say I have following model objects.
public class Person
{
public string Name { get; set; }
public DateTime? Birthdate { get; set; }
public int Status { get; set; }
public List<Account> Accounts { get; set; }
}
public class Account
{
public decimal Amount { get; set; }
public string Code { get; set; }
public DateTime Expiry { get; set; }
}
In my service I have to go to 2 different systems to retrieve data for Person and the account info of the Person. Obviously the service implementation looks like
[HttpGet]
[Route("Person/{id:int}")]
public IHttpActionResult Get(string id)
{
var person = new Person();
person = GetPersonFromSystemA(id);
if (person.Status == 2)
{
person.Accounts = GetPersonAccountsFromSystemB(id);
}
return this.Ok(person);
}
I cannot use EF at all in this case, so OData is very tricky.
I have some requirement that I need to provide the filtering capability to the service client. The client can decide which fields of the objects to return, it also means that if the client does not like to include Accounts info of the person I should skip the second call to system B to avoid entire child object.
I did some quick search but I could not find some similar solution yet. I know I can implement my own filtering syntax, and have all custom codes to use the filtering (by having lots of if/else).
I am looking for some ideas of more elegant solution.
Entity Framework is not required for building an OData Service. If you do not use OData, you will probably have to implement your own IQueryable which is what OData does out of the box.
Some sample code.
Model classes with some added properties
public class Person
{
[Key]
public String Id { get; set; }
[Required]
public string Name { get; set; }
public DateTime? Birthdate { get; set; }
public int Status { get; set; }
public List<Account> Accounts { get; set; }
}
public class Account
{
[Key]
public String Id { get; set; }
[Required]
public decimal Amount { get; set; }
public string Code { get; set; }
public DateTime Expiry { get; set; }
}
WebApiConfig.cs
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.MapODataServiceRoute("odata", null, GetEdmModel(), new DefaultODataBatchHandler(GlobalConfiguration.DefaultServer));
config.EnsureInitialized();
}
private static IEdmModel GetEdmModel()
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.Namespace = "YourNamespace";
builder.ContainerName = "DefaultContainer";
builder.EntitySet<Person>("People");
builder.EntitySet<Account>("Accounts");
var edmModel = builder.GetEdmModel();
return edmModel;
}
}
Controller method
[EnableQuery]
public class PeopleController : ODataController
{
public IHttpActionResult Get()
{
return Ok(SomeDataSource.Instance.People.AsQueryable());
}
}
You will need to include the Microsoft.AspNet.OData Nuget package.
Refer to the following for more guidance. It uses an in memory data source, but the concept is the same regardless.
http://www.odata.org/blog/how-to-use-web-api-odata-to-build-an-odata-v4-service-without-entity-framework/
When building a web api you would often want to filter your response and get only certain fields. You could do it in many ways, one of which as suggested above. Another way, you could approach it is using data shaping in your web api.
If you had a controller action as such:
public IHttpActionResult Get(string fields="all")
{
try
{
var results = _tripRepository.Get();
if (results == null)
return NotFound();
// Getting the fields is an expensive operation, so the default is all,
// in which case we will just return the results
if (!string.Equals(fields, "all", StringComparison.OrdinalIgnoreCase))
{
var shapedResults = results.Select(x => GetShapedObject(x, fields));
return Ok(shapedResults);
}
return Ok(results);
}
catch (Exception)
{
return InternalServerError();
}
}
And then your GetShapedData method can do the filtering as such:
public object GetShapedObject<TParameter>(TParameter entity, string fields)
{
if (string.IsNullOrEmpty(fields))
return entity;
Regex regex = new Regex(#"[^,()]+(\([^()]*\))?");
var requestedFields = regex.Matches(fields).Cast<Match>().Select(m => m.Value).Distinct();
ExpandoObject expando = new ExpandoObject();
foreach (var field in requestedFields)
{
if (field.Contains("("))
{
var navField = field.Substring(0, field.IndexOf('('));
IList navFieldValue = entity.GetType()
?.GetProperty(navField, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public)
?.GetValue(entity, null) as IList;
var regexMatch = Regex.Matches(field, #"\((.+?)\)");
if (regexMatch?.Count > 0)
{
var propertiesString = regexMatch[0].Value?.Replace("(", string.Empty).Replace(")", string.Empty);
if (!string.IsNullOrEmpty(propertiesString))
{
string[] navigationObjectProperties = propertiesString.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
List<object> list = new List<object>();
foreach (var item in navFieldValue)
{
list.Add(GetShapedObject(item, navigationObjectProperties));
}
((IDictionary<string, object>)expando).Add(navField, list);
}
}
}
else
{
var value = entity.GetType()
?.GetProperty(field, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public)
?.GetValue(entity, null);
((IDictionary<string, object>)expando).Add(field, value);
}
}
return expando;
}
Check my blog for a detailed post: https://jinishbhardwaj.wordpress.com/2016/12/03/web-api-supporting-data-shaping/