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; }
}
Related
My apologies for the vast amount of code, but it is necessary for the context of the problem. I am faced with an interesting dilemma that I have not been able to solve. I am trying to access information from model called Repository. Repository contains nested classes and lists, and looks like this:
{
public User User { get; set; }
}
public class User
{
public PinnedItems PinnedItems { get; set; }
}
public class PinnedItems
{
public List<Nodes> Nodes { get; set; }
}
public class Nodes
{
public string Name { get; set; }
public string Description { get; set; }
public string Url { get; set; }
public RepositoryTopics RepositoryTopics { get; set; }
}
public class RepositoryTopics
{
public List<TopicNodes> Nodes { get; set; }
}
public class TopicNodes
{
public Topic Topic { get; set; }
}
public class Topic
{
public string Name { get; set; }
}
I have the following method within a web api controller. It is responsible for grabbing my github repositories using graphql. This method looks like this:
{
var request = new GraphQLHttpRequest
{
Query = #"query($username: String!){
user(login: $username) {
pinnedItems(first: 6, types: REPOSITORY) {
nodes {
... on Repository {
name
description
url
repositoryTopics(first:6){
nodes{
topic{
name
}
}
}
}
}
}
}
}
",
Variables = new
{
username = _configuration.GetSection("GithubUserName").Value
}
};
var graphQlResponse = await CreateClient().SendQueryAsync<Repository>(request);
var repo = new Repository
{
User = graphQlResponse.Data.User
};
return Ok(repo);
}
repo is of type Repository.
This is an example piece of JSON that comes back from testing the controller in swagger.
"pinnedItems": {
"nodes": [
{
"name": "personal-website",
"description": "My personal website",
"url": "https://github.com/personal-website",
"repositoryTopics": {
"nodes": [
{
"topic": {
"name": "blazor-webassembly"
}
},
{
"topic": {
"name": "web-api"
}
},
{
"topic": {
"name": "contentful-api"
}
},
{
"topic": {
"name": "contentful"
}
}
]
}
}
I am accessing the code in my blazor component with the following:
Repository SoftwareRepos = new Repository();
protected async override Task OnInitializedAsync()
{
SoftwareRepos = await graphQLquery.GetRepositories();
}
}
And some example code such as this gets me the list of projects as a name.
#foreach(var name in SoftwareRepos.User.PinnedItems.Nodes.Select(x => x.Name).ToArray())
{
#name
}
PRINTS OUT: name, name, name, name
Ideally I would want something that looks like this:
Project One, Description, URL, html, css, react, javascript (a list of tags)
I am having trouble trying to construct LINQ queries to access this nested information (particularly repositoryTopic -> TopicNodes -> Nodes -> Topics -> Name.
I am seeking advice on how to approach this situation, or maybe some alternative solutions to what I am doing as I suspect I am a little out of my depth here. I am using graphql.client to send and retrieve information from github.
first thing to do is to deserialize that JSON into a class structure that it represents.
public class GitResponse{
public Node[] PinnedItems {get;set;}
}
public class Node{
public string Name {get;set};
public string Description {get;set;}
....
}
etc. Once this is done the rest is easy , you just walk that tree
deserialize with
System.Text.Json.Serailizer.Deserialize<GitResponse>(json);
We have a Web API written in DotNet Core 3.1.402 (I am new to DotNet Core and WebAPI).
We use SqlKata for Database processing.
We have an Account model that has AccountID, AccountName, AccountNumber, etc.
We would like to get an Account by different attributes, for ex: by AccountID, by AccountName, by AccountNumber.
How can we do that so that we don't need a separate HttpGet for each attribute (so we don't have to repeat the same code for different attributes) ?
This is our HttpGet in the AccountsController to get the account by AccountID
public class AccountsController : ControllerBase
{
private readonly IAccountRepository _accountRepository;
[HttpGet("{AccountID}")]
public Account GetAccount(int AccountID)
{
var result = _accountRepository.GetAccount(AccountID);
return result;
}
This is the code in the AccountRepository.cs
public Account GetAccount(int accountID)
{
var result = _db.Query("MyAccountTable").Where("AccountID", accountID).FirstOrDefault<Account>();
return result;
}
This is the Account class
namespace MyApi.Models
{
public class Account
{
public string AccountID { get; set; }
public string AccountName { get; set; }
public string AccountNumber { get; set; }
// other attributes
}
}
Thank you.
Doing it with GET can be a pain, there are ways to pass on the path/query arrays and complex objects but are ugly, the best you can do is to use POST instead of GET and pass an object with the filters that you want.
//In the controller...
[HttpPost]
public Account GetAccount([FromBody]Filter[] DesiredFilters)
{
var result = _accountRepository.GetAccount(DesiredFilters);
return result;
}
//Somewhere else, in a shared model...
public class Filter
{
public string PropertyName { get; set; }
public string Value { get; set; }
}
//In the repository...
public Account GetAccount(Filter[] Filters)
{
var query = _db.Query("MyAccountTable");
foreach(var filter in Filters)
query = query.Where(filter.PropertyName, filter.Value);
return query.FirstOrDefault<Account>();
}
Now you can send a JSON array on the request body with any filters that you want, per example:
[
{ "PropertyName": "AccountID", "Value": "3" },
{ "PropertyName": "AccountName", "Value": "Whatever" }
]
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'm developing a basic Web API project for education purposes, and I am having trouble with my EF Model relationships. I have 2 models. Message and MessageBoard.
public class Message
{
public long Id { get; set; }
public string Text { get; set; }
public string User { get; set; }
public DateTime PostedDate { get; set; }
public long MessageBoardId { get; set; }
[ForeignKey("MessageBoardId")]
public MessageBoard MessageBoard { get; set; }
}
public class MessageBoard
{
public long Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public ICollection<Message> Messages { get; set; }
}
I've setup my DBContext and created a migration to configure the database. I generated two Web API controllers using EF Scaffolding. The migration appears to correctly detect the relationship between the two models:
modelBuilder.Entity("Asp.net_Core_Web_Api.Models.Message", b =>
{
b.HasOne("Asp.net_Core_Web_Api.Models.MessageBoard", "MessageBoard")
.WithMany("Messages")
.HasForeignKey("MessageBoardId")
.OnDelete(DeleteBehavior.Cascade);
});
But, when I create a MessageBoard and then create a Message with the ID of the MessageBoard, they don't appear to link correctly. In PostMan, I am doing the following:
1) Post a new MessageBoard
POST - https://localhost:44384/api/MessageBoards/
Body - Raw - Json
{
"Name":"Test Board",
"Description":"A Message board for testing purposes."
}
Returns
{
"id": 4,
"name": "Test Board",
"description": "A Message board for testing purposes.",
"messages": null
}
2) Post a new Message
POST - https://localhost:44384/api/Messages
Body - Raw - JSON
{
"Text":"Posting my first message!",
"User":"Jesse",
"PostedDate":"1/1/2019",
"MessageBoardId":4
}
Returns
{
"id": 2,
"text": "Posting my first message!",
"user": "Jesse",
"postedDate": "2019-01-01T00:00:00",
"messageBoardId": 4,
"messageBoard": null
}
I would expect that the messageBoard would not be null, and it would instead return the JSON for the messageBoard that was previously created. If I change to a GET method, it is also null. Why is it null?
EDIT: Here are my controllers. I removed actions except for GET and POST.
[Route("api/[controller]")]
[ApiController]
public class MessageBoardsController : ControllerBase
{
private readonly MessageBoardContext _context;
public MessageBoardsController(MessageBoardContext context)
{
_context = context;
}
// GET: api/MessageBoards/5
[HttpGet("{id}")]
public async Task<ActionResult<MessageBoard>> GetMessageBoard(long id)
{
var messageBoard = await _context.MessageBoards.FindAsync(id);
if (messageBoard == null)
{
return NotFound();
}
return messageBoard;
}
// POST: api/MessageBoards
[HttpPost]
public async Task<ActionResult<MessageBoard>> PostMessageBoard(MessageBoard messageBoard)
{
_context.MessageBoards.Add(messageBoard);
await _context.SaveChangesAsync();
return CreatedAtAction("GetMessageBoard", new { id = messageBoard.Id }, messageBoard);
}
}
[Route("api/[controller]")]
[ApiController]
public class MessagesController : ControllerBase
{
private readonly MessageBoardContext _context;
public MessagesController(MessageBoardContext context)
{
_context = context;
}
// GET: api/Messages/5
[HttpGet("{id}")]
public async Task<ActionResult<Message>> GetMessage(long id)
{
var message = await _context.Messages.FindAsync(id);
if (message == null)
{
return NotFound();
}
return message;
}
// POST: api/Messages
[HttpPost]
public async Task<ActionResult<Message>> PostMessage(Message message)
{
_context.Messages.Add(message);
await _context.SaveChangesAsync();
return CreatedAtAction("GetMessage", new { id = message.Id }, message);
}
}
You need to load related data
So for example, for your MessageBoard GET - // GET: api/MessageBoards/5
Change from:
var messageBoard = await _context.MessageBoards.FindAsync(id);
To
var messageBoard = await _context.MessageBoards
.Include(i=>i.Messages)
.FirstOrDefaultAsync(i => i.Id == id);
I would expect that the messageBoard would not be null, and it would instead return the JSON for the messageBoard that was previously created. If I change to a GET method, it is also null. Why is it null?
This is because you are returning the newly created message, here only MessageBoadId is exists, not MessageBoad object. So you have to load the related MessageBoad from database using Include for newly created message.
Your PostMessage method should be as follows:
// POST: api/Messages
[HttpPost]
public async Task<ActionResult<Message>> PostMessage(Message message)
{
_context.Messages.Add(message);
await _context.SaveChangesAsync();
var message = await _context.Messages
.Include(i=>i.MessageBoard)
.FirstOrDefaultAsync(i => i.Id == message.Id);
return Json(message);
}
You've given the the application output and what the database looks like, but not the middle bit on how it saves/retrieves the data.
Without knowing what's going on in the middle, my best stab in the dark is that you've neither set your lazy loading correctly nor used Include to include the MessageBoard entity.
More info here on what they are.
I'm adding an API controller to my MVC application to retun JSON data
In my application I have a class called Album:
public class Album
{
public int Id { get; set; }
public string AlbumName { get; set; }
public int YearReleased { get; set; }
public string AlbumInfo { get; set; }
public string imgAlbumCover { get; set; }
}
My database contains a table of several Album objects
I created an API controller to return this list of Albums in Json format.
I added the following code to WebApiConfig.cs to get JSON back instead of XML:
using System.Net.Http.Headers;
GlobalConfiguration.Configuration.Formatters
.JsonFormatter.MediaTypeMappings.Add
(new System.Net.Http.Formatting.RequestHeaderMapping("Accept",
"text/html",
StringComparison.InvariantCultureIgnoreCase,
true,
"application/json"));
When I do an Albums API call in the browser, returned is a list of Album objects in JSON format.
Instead of returning the list of Albums, I'd like to retun a RootObject that has 1 property called Albums, where Albums is a list of Album objects. Is there a way of doing this in the controller? I don't want to have to create a new RootObject class.
Below is the code for my API controller:
namespace Music.Controllers.API
{
public class AlbumsController : ApiController
{
private MusicContext db;
public AlbumsController()
{
db = new MusicContext();
}
public IEnumerable<Album> GetAlbums()
{
return (db.Albums.ToList());
}
}
}
Then create a viewmodel as such and return the same like
public class AlbumListResponseModel
{
public IEnumerable<Album> Albums { get; set; }
}
public async Task<IActionResult> GetAlbums()
{
AlbumListResponseModel model = new AlbumListResponseModel
{
Albums = db.Albums;
}
return OK(model);
}
If you are using WEB API 2.0 then consider using IActionResult rather
Change the GetAlbums return type to HttpResponseMessage and change the return statement as
return Request.CreateResponse(HttpStatusCode.OK, new {Albums = db.Albums.ToList() });
That's way you don't need to create a new class.
Full Code :
namespace Music.Controllers.API
{
public class AlbumsController : ApiController
{
private MusicContext db;
public AlbumsController()
{
db = new MusicContext();
}
public HttpResponseMessage GetAlbums()
{
return Request.CreateResponse(HttpStatusCode.OK, new {Albums = db.Albums.ToList() });
}
}
}