Introduction:
GraphQL is a query language for your API and a server-side runtime for executing queries by using a type system you define for your data. GraphQL can be integrated into any framework like ASP.NET, Java, NestJs, etc and it isn't tied to any specific database or storage engine and is instead backed by your existing code and data.
How GraphQL API Different From Rest API:
- GraphQL exposes a single end-point or route for the entire application, regardless of its responses or actions.
- HTTP-POST is the only Http verb recommended by the GraphQL.
- The client applications (consumers of API) can give instructions to GraphQL API about what type of properties to be returned in the response.
Building Blocks Of GraphQL API:
- The main building blocks of GraphQL API is Schemas and Types.
- A 'Schema' in GrpahQL API describes the functionality available to the clients connect to API. Schema mostly consists of GraphQL Object Types, Queries, Mutations, etc.
- Types likely to be called GraphQL Object Types. GraphQL Object Types represents the data source types return from API. GraphQL Object Types contains 'Fields' and 'Methods'. Fields are just properties in a class, 'Methods' are used to modify or conditionally change the field values based on client query.
Create ASP.NET Core Web API Project Template:
Let's start to learn about features in GraphQL, its implementation, and its integration into the Asp.Net Core application. Now create the Asp.Net Core Web API template.
GraphQL Nuget:
Open package manager console in VisualStudio to install GraphQL Nuget.
Install-Package GraphQL -Version 2.4.0
Create Data Source Model:
Data Source Model means which represents a Table class. So add the new folder as 'Models', to that folder add a new class as 'Player.cs'
public class Player { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public int CurrentAge { get; set; } public string Teams { get; set; } public string PlayingRole { get; set; } public string BattingStyle { get; set; } public string BowlingStyle { get; set; } }
Create A Data Repository And Register To Services:
A repository is a data access layer, where communication with the database goes. So add the new folder as 'Repositories', to that folder add two files like 'IPlayerRepository' and 'PlayerRepository'.
IPlayerRepository:
using System.Collections.Generic; using GraphQLSample.Models; namespace GraphQLSample.Repositories { public interface IPlayerRepository { List<Player> GetAll(); } }
PlayerRepository:
using System.Collections.Generic; using GraphQLSample.Models; namespace GraphQLSample.Repositories { public class PlayerRepository: IPlayerRepository { // later code will be updated // to get data from database public List<Player> GetAll() { return new List<Player> { new Player { Id=1, FirstName= "Rohit", LastName = "Sharma", CurrentAge = 32, PlayingRole = "Opener", BattingStyle = "Right-hand Bat", BowlingStyle="spin", Teams="India, Mumbai, Mumbai Indians" } // display purpose items hidden , while testing add more items }; } } }Here repository 'GetAll()' method returning all the players(later code will be updated to access players from database).
Register Repositories In ConfigurationServices Method In StartUp.cs:
services.AddScoped<IPlayerRepository, PlayerRepository>();By default, the Dotnet Core application configured with dependency injection, so the creation of an object to any service or repository classes taken care of by it. AddScoped() represents an object created per request.
ObjectGraphType:
- 'GraphQL.Types.ObjectGraphType' class represents Types in GraphQL which we discussed as one of the core concepts in GraphQL.
- GraphQL is not bound to any specific language or framework, so it can not understand CSharp POCO( Plain Old CLR Object ) classes directly.
- So to represent GraphQL Type, CSharp Classes need to inherit 'GraphQL.Types.ObjectGraphType'.
- After inheriting ObjectGraphType in the constructor we need to register our own class properties as 'Field' types which is understandable by GraphQL.
PlayerType.cs:
using GraphQL.Types; using GraphQLSample.Models; namespace GraphQLSample.GraphQLTypes { public class PlayerType:ObjectGraphType<Player> { public PlayerType() { Field(_ => _.Id); Field(_ => _.FirstName); // description is an self //explanatory extenstion method which is options Field(_ => _.LastName).Description("last name of a player"); Field(_ => _.CurrentAge); Field(_ => _.Teams); Field(_ => _.PlayingRole); Field(_ => _.BattingStyle); Field(_ => _.BowlingStyle); } } }
- Here we have a model 'Player' which CSharp POCO classes, which can't be understood by GraphQL directly.
- To make 'Player' to GraphQL Type, we have created a new class 'PlayerType' which inherits 'GraphQL.Types.ObjectGrpahType<T>'. Type took by ObjectGraphType is 'Player' to represent it as GraphQLType.
- All the properties of 'Player' class returning as 'Field' types which can be understood by GraphQL.
Schema:
- Schema is one of the main building blocks of GraphQL API.
- Schema mainly consists of Query, Mutation, Types, etc. Query in Schema is an ObjectGraphType, but it is like Root or Parent ObjectGraphType.
- Schema Query held all other ObjectGraphType of entire application, more or less it acts as a Data Source to clients consuming GraphQL API.
- Based on the client's request this Query in Schema serves data.
RootQuery.cs:
using GraphQL.Types; using GraphQLSample.Repositories; namespace GraphQLSample.GraphQLTypes { public class RootQuery:ObjectGraphType { public RootQuery(IPlayerRepository _playerRepository) { Field<ListGraphType<PlayerType>>("players", resolve: context => { return _playerRepository.GetAll(); }); } } }
- Here all players returning under field name 'players' and Field register with 'PlayerType'(ObjectGraphType).
- Similarly, all POCO classes which represent a table will have their individual ObjectGraphType(like PlayerType for Player class) and these all ObjectGraphType will be registered as Field type in RootQuery in our application.
services.AddScoped<RootQuery>();Now in the 'GraphQLTypes' folder add a new file and name as 'RootSchema.cs'. By default GraphQL Dotnet Library provides us 'Schema' and 'ISchema' entities, but we are creating custom schema like 'RootSchema' because to consume the benefits of Dependency Injection. Our 'RootSchema' inherits both 'GrpahQL.Types.Schema', 'GraphQL.Types.ISchema' to represent a GraphQL Schema.
RootSchema:
using GraphQL; using GraphQL.Types; namespace GraphQLSample.GraphQLTypes { public class RootSchema:Schema, ISchema { public RootSchema(IDependencyResolver resolver):base(resolver) { Query = resolver.Resolve<RootQuery>(); } } }GraphQL NuGet library comes with inbuild Dependency Resolver. 'Schema' class has 'GraphQL.IDependencyResolver' as property, so that it can resolve all ObjectGraphType it needed.
.
Register Schema and IDependencyResolver in method ConfigurationServices in Startup.cs:
services.AddScoped<IDependencyResolver>(_ => new FuncDependencyResolver(_.GetRequiredService)); services.AddScoped<ISchema, RootSchema>();'GraphQL.FuncDependencyResolver' implements 'GraphQL.IDependencyResolver'.
Create GraphQL API EndPoint:
Now create a new Web API Controller as 'GraphQLController' and the controller contains only one action method that supports Http-Post verb. In the model, folder add new file 'GraphQLQueryDto.cs' files that represent the post data payload entity.
GraphQLQueryDto.cs:
namespace GraphQLSample.Models { public class GraphQLQueryDto { public string Query { get; set; } } }Query property represents data to query the GraphQL API.
GraphQLController.cs:
[Route("graphql")] public class GraphQLController : Controller { private readonly ISchema _schema; private readonly IDocumentExecuter _executer; public GraphQLController(ISchema schema, IDocumentExecuter executer) { _schema = schema; _executer = executer; } [HttpPost] public async Task<IActionResult> Post([FromBody] GraphQLQueryDto query) { var result = await _executer.ExecuteAsync(_ => { _.Schema = _schema; _.Query = query.Query; }).ConfigureAwait(false); if(result.Errors?.Count > 0) { return Problem(detail: result.Errors.Select(_ => _.Message).FirstOrDefault(), statusCode:500); } return Ok(result.Data); } }
- Here [Route("graphql")] is route decorator, it says controller configured with Web API attribute routing.
- 'GraphQL.ISchema' is a Schema of GraphQL API, the instance of schema was created by constructor injection.
- 'GraphQL.IDocumentExecuter' has async GraphQL query executor. On successful execution of IDocumentExecuter returns data. On Error, execution returns an error message to the clients consuming GraphQL API.
services.AddSingleton<IDocumentExecuter, DocumentExecuter>();
Fields:
GraphQL is about asking for specific fields on objects on the server. Let's test API by requesting Fields.
Request Query:
query { players { id firstName } }Here GraphQL Query just looks like almost similar to JSON Object. 'query' is represented as root query. 'players', 'id', 'firsName' are registered Fields in ObjectGraphQLType as below:
// field registered in RootQuery.cs Field<ListGraphType<PlayerType>>("players", resolve: context => { // logic to server 'players' }); // field registered in PlayerType.cs Field(_ => _.Id); Field(_ => _.FirstName);
Now test API as below:
We got 500 error status code and error message shows 'PlyerType' has been not registered. I allowed this error purposefully, because to aware that every ObjectGraphQLType must be registered in Startup.cs
Resolve No Service Registered For ObjectGraphQLType:
Now register PlayerType in the ConfigureService method in Startup.cs:
services.AddSingleton<PlayerType>();
Now test API again:
Query Arguments To Filter Data :
In GraphQL we can pass an argument to filter the GraphQL API. Since in GraphQL data is coming based on Fields requested, it is possible to pass arguments to the fields to filter the data.
RootQuery.cs:
public class RootQuery:ObjectGraphType { public RootQuery(IPlayerRepository _playerRepository) { Field<ListGraphType<PlayerType>>("players", resolve: context => { return _playerRepository.GetAll(); }); Field<ListGraphType<PlayerType>>("filteredPlayers", arguments: new QueryArguments { new QueryArgument<StringGraphType> { Name = "firstName"} }, resolve: context => { string firstName = context.GetArgument<string>("firstName"); return _playerRepository.GetAll().Where(_ => _.FirstName.ToLower() == firstName.ToLower()).ToList(); }); } }
- Here we added a new Field with name 'filteredPlayers'.
- 'ListGraphType' is collection representation in GraphQL.
- Filed takes an 'arguments' as input parameter and its type is 'QueryArguments', here we capturing our argument like 'firstName' which is passed from the client to GraphQL API. Arguments that are captured here will be available from the context of Field.
- 'StringGraphType' is equivalent to 'string' type in dotnet.
- From 'resolve' which is the input parameter of Field, we are getting the value of our argument like 'firstName' from field context. Based on the argument value filter the data and send to the client.
query { filteredPlayers(firstName: "rohit") { id firstName } }Now test the GraphQL API:
Aliases:
We can observe that field name and API result object names are matching. By looking at the previous result object whose name is "filteredPlayers" is like more self-explanatory which might not look good in the JSON result object for clients consuming the GraphQL API. "Players" looks ideal name for the result JSON Object, but we can not use the "Players" name for argument query because of the normal query without argument using it. So this problem can be overcome by using Aliases names in the query fields. The server will return data by using the aliases' names.
Aliases Query:
query { players: filteredPlayers(firstName: "rohit") { id firstName } }
A comparison between two records in GraphQL API is very easy. Let's do a comparison using the below query
The query for comparison (without Fragments):
query { leftPlayer: filteredPlayers(firstName: "rohit") { id firstName lastName } rigthPlayer: filteredPlayers(firstName: "virat") { id firstName lastName } }Now test GraphQL API:
Here in GraphQL API, we are getting comparison data in a perfect way without doing any work at the server-side. But if we carefully observe queries constructed with duplicate fields, which will be tedious if the fields are a high number.
So to resolve these duplicate fields, GraphQL provided an option called Fragments where a set of similar fields will be grouped to a set.
The Query for comparison with Fragment:
query { leftComparison: filteredPlayers(firstName: "rohit") { ...props } rightComparison: filteredPlayers(firstName: "virat") { ...props } } fragment props on PlayerType { id firstName lastName }
- Here 'fragment' keyword represents GraphQL Fragment.
- 'props' defines the name of the fragment.
- 'pops on PlayerType' represents a fragment working on which type of ObjectGrphQLType(example from our sample PlayerType is an ObjectGraphQLType).
- A fragment was enclosed with set fields grouped.
- Inside query we can observe instead of mentioning fields, we have used fragment name prop which dynamically replaces fields at the time of query execution on the server.
Now test GraphQL API:
Accessing Data From Database:
We are going to create a DbContext, code-first approach with an existing database. Let's first install NuGet packages related to entity-framework as below.
Install-Package Microsoft.EntityFrameworkCore -Version 3.0.1
Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 3.0.1
Create a new folder name as 'Data', inside add the new file as SportsContext.cs as below:
using GraphQLSample.Models; using Microsoft.EntityFrameworkCore; namespace GraphQLSample.Data { public class SportsContext : DbContext { public SportsContext(DbContextOptions<SportsContext> options) : base(options) { } public DbSet<Player> Player { get; set; } } }Register SportsContext in ConfigureServices method in Startup.cs as below:
services.AddDbContext<SportsContext>(options => options.UseSqlServer(Configuration.GetConnectionString("SportsContext")));Add database connection string in "appSettings.json". Now update the 'PlayerRepository.cs' as below:
using System.Collections.Generic; using System.Linq; using GraphQLSample.Data; using GraphQLSample.Models; namespace GraphQLSample.Repositories { public class PlayerRepository: IPlayerRepository { private readonly SportsContext _sportsContext; public PlayerRepository(SportsContext context) { _sportsContext = context; } public List<Player> GetAll() { return _sportsContext.Player.ToList(); } } }GraphQL Query to test:
query { players { id firstName lastName } }Now test GraphQL API:
Mutations:
GraphQL Mutations represent changing the state of data at the server-side, which means to create or update or delete data. To perform mutation you need to have the root mutation object of type ObjectGraphType. Like RootQuery which holds all ObjectGraphType Fields to fetch the data, we need to create RootMutation which holds all ObjectGraphType Fields to create or update or delete the data.
Let's write logic to add a new item in 'PlayerRepository.cs' file as below:
public Player AddPalyer(Player player) { _sportsContext.Player.Add(player); _sportsContext.SaveChanges(); return player; }IPlayerRepositroy.cs:
Player AddPalyer(Player player);
To capture the data of the object posted from the client, we need to create GraphQL type which inherits 'GraphQL.Types.InputObjectGraphType<T>'. InputObjectGraphType looks similar to ObjectGraphType, but InputObjectGraphType to capture the data from client wherea as ObjectGraphType to serve the type of data to clients. Now in 'GraphQLTypes' folder add a new file as 'InputPlayerType.cs'
InputPlayerType.cs:
using GraphQL.Types; using GraphQLSample.Models; namespace GraphQLSample.GraphQLTypes { public class InputPlayerType:InputObjectGraphType<Player> { public InputPlayerType() { Name = "InputPlayerType"; Field(_ => _.Id); Field(_ => _.FirstName); // description is an self //explanatory extenstion method which is options Field(_ => _.LastName).Description("last name of a player"); Field(_ => _.CurrentAge); Field(_ => _.Teams); Field(_ => _.PlayingRole); Field(_ => _.BattingStyle); Field(_ => _.BowlingStyle); } } }Now Register InputPlayerType in ConfigureServices method in Startup.cs:
services.AddSingleton<InputPlayerType>();Now in 'GraphQLTypes' folder create a new file 'RootMutation.cs' as below:
using GraphQL.Types; using GraphQLSample.Models; using GraphQLSample.Repositories; namespace GraphQLSample.GraphQLTypes { public class RootMutation : ObjectGraphType { public RootMutation(IPlayerRepository playerRepository) { Field<PlayerType>( "addPlayer", arguments: new QueryArguments { new QueryArgument<InputPlayerType>(){ Name = "player" } }, resolve: context => { var player = context.GetArgument<Player>("player"); return playerRepository.AddPalyer(player); } ); } } }Now configure RootMutation in RootSchema.cs as below:
public RootSchema(IDependencyResolver resolver):base(resolver) { Query = resolver.Resolve<RootQuery>(); Mutation = resolver.Resolve<RootMutation>(); }Now Register RootMutation in ConfigureSevices method in Startup.cs as below:
services.AddScoped<RootMutation>();Now modify 'GraphQLQueryDto' as below
public class GraphQLQueryDto { public string Query { get; set; } public string Variables { get; set; } }Here 'Variables' property is like input data to 'Query' property. Now update 'GraphQLController' post method as below:
var result = await _executer.ExecuteAsync(_ => { _.Schema = _schema; _.Query = query.Query; _.Inputs = query.Variables?.ToInputs(); }).ConfigureAwait(false);The variable value was assigned to the "Inputs" type in DocumentExecuter, where this variable values passed as data to 'Query' variables at runtime.
The query for Mutation:
mutation($player: InputPlayerType) { addPlayer(player: $player) { id firstName } }
- mutation keyword to represent GraphQL Mutations.
- '$player' is a variable whose values will pass from GraphQL 'Variables'.
- 'InputPlayerType' is assigned a type of '$player', it should match with the name of the 'InputObjectGraphType' we declared in the above steps.
- 'addPlayer' represents Field name in 'RootMutation' as we mentioned above steps. 'player' argument of 'addplayer' Field.
{ "player":{ "id":0 "firstName": "Shikar", "lastName": "Dhawan", "currentAge": 30, "teams": "India", "playingRole": "Opening Batsman", "battingStyle": "Left-hander", "bowlingStyle": "N/A" } }Variable is a plain JSON Object.
Now test the API with GraphQL Mutations
Summary:
We have discussed the core feature of the GraphQL API. We learned how GraphQL API is different from REST API. We did a hands-on sample of integrating GraphQL API into the Asp.Net Core application.
Best tutorial I found so far - and nicely laid out. Thank-you.
ReplyDeleteJust having an issue with the final sample where you explain the mutation. The query arg in the controller is null no matter how I format it in postman.
I downloaded the code to make sure I followed the examples correctly and it appears I have.
Any ideas?
Hi DominionZA,
Deletepayload for mutation
{
"query":"mutation($player: InputPlayerType) { addPlayer(player:$player){id firstName} }",
"variables":"{\"playe\":{\"id\":0, \"firstName\":\"rahul\",\"lastName\":\"KL\",\"currentAge\":30,\"teams\":\"India\",\"playingRole\":\"Wicket Keeper\",\"battingStyle\":\"Left-hander\",\"bowlingStyle\":\"N/A\"}}"
}
How to integrate existing API using GraphQL/HotChocolate to fetch data from the existing API?
ReplyDelete