Building GraphQL APIs with ASP.NET Core
Github Repositories
The Building GraphQL APIs with ASP.NET Core Pluralsight course explains how to use an API that doesn't use REST, but contains just one endpoint that has the ability to return data in a form that exactly suits the data-consuming application. This becomes a reality with GraphQL for .NET.
Table of contents
Description
Having an API that doesn't use REST, and contains just one endpoint which has the ability to return data in a form that exactly suits the data-consuming application is very powerful. After watching this practical course, Building GraphQL APIs with ASP.NET Core, you’ll be ready to implement GraphQL in your ASP.NET Core API and write queries against it. First, you will explore how using GraphQL has lots of benefits compared to REST or SOAP. Next, you will very quickly understand the basic principles of GraphQL and shortly after, you can start building your own GraphQL endpoint in ASP.NET Core. Finally, you will discover how schemas provide metadata to clients, how queries are written for data retrieval and mutations. By the end of this course, you will be amazed by the ability that your GraphQL endpoint has to actively contact the client when changes occur with subscriptions.
1 Course Overview
1.1 Course Overview
2 Starting with GraphQL
2.1 Introduction
2.2 The Power of GraphQL
2.3 What Is GraphQL?
2.4 Preparing a Project
Download exercise files
from PluralSight
- Unzip the
building-graphql-apis-aspdotnet-core.zip
document into a local folder.
- Copy the content from
02\demos
folder to the folder where we are going to work.
- Open the
CarvedRock.sln
document with VS2017 or newer or thebuilding-graphql-apis-aspdotnet-core
folder with Visual Studio Code.
- Modify the
appsettings.json
document to putlocalhost
for theServer
in the Connection String.
appsettings.json
{
"ConnectionStrings": {
"CarvedRock": "Server=localhost;Database=CarvedRock;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}
- Run the
dotnet ef database update
command to create the database
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/GraphQL/building-graphql-apis-aspdotnet-core
$ ls
CarvedRock.Api CarvedRock.sln CarvedRock.sln.DotSettings.user
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/GraphQL/building-graphql-apis-aspdotnet-core
$ cd CarvedRock.Api/
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/GraphQL/building-graphql-apis-aspdotnet-core/CarvedRock.Api
$ dotnet ef database update
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 2.1.4-rtm-31024 initialized 'CarvedRockDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (3,330ms) [Parameters=[], CommandType='Text', CommandTimeout='60']
CREATE DATABASE [CarvedRock];
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (1,306ms) [Parameters=[], CommandType='Text', CommandTimeout='60']
IF SERVERPROPERTY('EngineEdition') <> 5
BEGIN
ALTER DATABASE [CarvedRock] SET READ_COMMITTED_SNAPSHOT ON;
END;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (16ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE TABLE [__EFMigrationsHistory] (
[MigrationId] nvarchar(150) NOT NULL,
[ProductVersion] nvarchar(32) NOT NULL,
CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId])
);
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (39ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT OBJECT_ID(N'[__EFMigrationsHistory]');
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (3ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [MigrationId], [ProductVersion]
FROM [__EFMigrationsHistory]
ORDER BY [MigrationId];
Applying migration '20181108113631_Products'.
info: Microsoft.EntityFrameworkCore.Migrations[20402]
Applying migration '20181108113631_Products'.
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (46ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE TABLE [Products] (
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(100) NULL,
[Type] int NOT NULL,
[Description] nvarchar(max) NULL,
[Price] decimal(18, 2) NOT NULL,
[Stock] int NOT NULL,
[Rating] int NOT NULL,
[IntroducedAt] datetimeoffset NOT NULL,
[PhotoFileName] nvarchar(100) NULL,
CONSTRAINT [PK_Products] PRIMARY KEY ([Id])
);
Done.
- Run the application with
dotnet build
and thedotnet run
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/GraphQL/building-graphql-apis-aspdotnet-core
$ dotnet build CarvedRock.Api
Microsoft (R) Build Engine version 15.9.20+g88f5fadfbe for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.
Restore completed in 244.97 ms for C:\Work\Training\Pre\GraphQL\building-graphql-apis-aspdotnet-core\CarvedRock.Api\CarvedRock.Api.csproj.
CarvedRock.Api -> C:\Work\Training\Pre\GraphQL\building-graphql-apis-aspdotnet-core\CarvedRock.Api\bin\Debug\netcoreapp2.1\CarvedRock.Api.dll
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:02.71
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/GraphQL/building-graphql-apis-aspdotnet-core
$ dotnet run --project CarvedRock.Api/CarvedRock.Api.csproj
info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[0]
User profile is available. Using 'C:\Users\juan.pablo.perez\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and Windows DPAPI to encrypt keys at rest.
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 2.1.4-rtm-31024 initialized 'CarvedRockDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (6ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT CASE
WHEN EXISTS (
SELECT 1
FROM [Products] AS [p])
THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT)
END
Hosting environment: Development
Content root path: C:\Work\Training\Pre\GraphQL\building-graphql-apis-aspdotnet-core\CarvedRock.Api
Now listening on: https://localhost:5001
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.
- Browse to https://localhost:5001/ui/playground
- Initial code:
Data/Entities/Product.cs
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace CarvedRock.Api.Data.Entities
{
public class Product
{
public int Id { get; set; }
[StringLength(100)]
public string Name { get; set; }
public ProductType Type { get; set; }
public string Description { get; set; }
[Column(TypeName = "decimal(18,2)")]
public decimal Price { get; set; }
public int Stock { get; set; }
public int Rating { get; set; }
public DateTimeOffset IntroducedAt { get; set; }
[StringLength(100)]
public string PhotoFileName { get; set; }
}
}
Data/Migrations/20181108113631_Products.cs
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
namespace CarvedRock.Api.Data.Migrations
{
public partial class Products : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Products",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
Name = table.Column<string>(maxLength: 100, nullable: true),
Type = table.Column<int>(nullable: false),
Description = table.Column<string>(nullable: true),
Price = table.Column<decimal>(nullable: false),
Stock = table.Column<int>(nullable: false),
Rating = table.Column<int>(nullable: false),
IntroducedAt = table.Column<DateTimeOffset>(nullable: false),
PhotoFileName = table.Column<string>(maxLength: 100, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Products", x => x.Id);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Products");
}
}
}
Data/Migrations/20181108113631_Products.Designer.cs
// <auto-generated />
using System;
using CarvedRock.Api.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace CarvedRock.Api.Data.Migrations
{
[DbContext(typeof(CarvedRockDbContext))]
partial class CarvedRockDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.1.4-rtm-31024")
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
modelBuilder.Entity("CarvedRock.Api.Data.Entities.Product", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("Description");
b.Property<DateTimeOffset>("IntroducedAt");
b.Property<string>("Name")
.HasMaxLength(100);
b.Property<string>("PhotoFileName")
.HasMaxLength(100);
b.Property<decimal>("Price");
b.Property<int>("Rating");
b.Property<int>("Stock");
b.Property<int>("Type");
b.HasKey("Id");
b.ToTable("Products");
});
#pragma warning restore 612, 618
}
}
}
Data/CarvedRockDbContext.cs
using CarvedRock.Api.Data.Entities;
using Microsoft.EntityFrameworkCore;
namespace CarvedRock.Api.Data
{
public class CarvedRockDbContext: DbContext
{
public CarvedRockDbContext(DbContextOptions<CarvedRockDbContext> options): base(options)
{
}
public DbSet<Product> Products { get; set; }
}
}
Data/CarvedRockDbContext.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CarvedRock.Api.Data.Entities;
namespace CarvedRock.Api.Data
{
public static class InitialData
{
public static void Seed(this CarvedRockDbContext dbContext)
{
if (!dbContext.Products.Any())
{
dbContext.Products.Add(new Product
{
Name = "Mountain Walkers",
Description = "Use these sturdy shoes to pass any mountain range with ease.",
Price = 219.5m,
Rating = 4,
Type = ProductType.Boots,
Stock = 12,
PhotoFileName = "shutterstock_66842440.jpg",
IntroducedAt = DateTimeOffset.Now.AddMonths(-1)
});
dbContext.Products.Add(new Product
{
Name = "Army Slippers",
Description = "For your everyday marches in the army.",
Price = 125.9m,
Rating = 3,
Type = ProductType.Boots,
Stock = 56,
PhotoFileName = "shutterstock_222721876.jpg",
IntroducedAt = DateTimeOffset.Now.AddMonths(-1)
});
dbContext.Products.Add(new Product
{
Name = "Backpack Deluxe",
Description = "This backpack can survive any tornado.",
Price = 199.99m,
Rating = 5,
Type = ProductType.ClimbingGear,
Stock = 66,
PhotoFileName = "shutterstock_6170527.jpg",
IntroducedAt = DateTimeOffset.Now.AddMonths(-1)
});
dbContext.Products.Add(new Product
{
Name = "Climbing Kit",
Description = "Anything you need to climb the mount Everest.",
Price = 299.5m,
Rating = 5,
Type = ProductType.ClimbingGear,
Stock = 3,
PhotoFileName = "shutterstock_48040747.jpg",
IntroducedAt = DateTimeOffset.Now.AddMonths(-1)
});
dbContext.Products.Add(new Product
{
Name = "Blue Racer",
Description = "Simply the fastest kayak on earth and beyond for 2 persons.",
Price = 350m,
Rating = 5,
Type = ProductType.Kayaks,
Stock = 8,
PhotoFileName = "shutterstock_441989509.jpg",
IntroducedAt = DateTimeOffset.Now.AddMonths(-1)
});
dbContext.Products.Add(new Product
{
Name = "Orange Demon",
Description = "One person kayak with hyper boost button.",
Price = 450m,
Rating = 2,
Type = ProductType.Kayaks,
Stock = 1,
PhotoFileName = "shutterstock_495259978.jpg",
IntroducedAt = DateTimeOffset.Now.AddMonths(-1)
});
dbContext.SaveChanges();
}
}
}
}
Data/ProductType.cs
namespace CarvedRock.Api.Data
{
public enum ProductType
{
Boots,
ClimbingGear,
Kayaks
}
}
Repositories/ProductRepository.cs
using System.Collections.Generic;
using System.Threading.Tasks;
using CarvedRock.Api.Data;
using CarvedRock.Api.Data.Entities;
using Microsoft.EntityFrameworkCore;
namespace CarvedRock.Api.Repositories
{
public class ProductRepository
{
private readonly CarvedRockDbContext _dbContext;
public ProductRepository(CarvedRockDbContext dbContext)
{
_dbContext = dbContext;
}
public Task<List<Product>> GetAll()
{
return _dbContext.Products.ToListAsync();
}
}
}
CarvedRock.Api.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="GraphQL" Version="2.3.0" />
<PackageReference Include="GraphQL.Server.Transports.AspNetCore" Version="3.2.0" />
<PackageReference Include="GraphQL.Server.Ui.Playground" Version="3.2.0" />
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.1.2" PrivateAssets="All" />
</ItemGroup>
</Project>
Properties/launchSettings.json
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:53215",
"sslPort": 44332
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"CarvedRock.Api": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "ui/playground",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}
}
}
Program.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace CarvedRock.Api
{
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
}
Startup,cs
using CarvedRock.Api.Data;
using CarvedRock.Api.GraphQL;
using CarvedRock.Api.Repositories;
using GraphQL;
using GraphQL.Server;
using GraphQL.Server.Ui.Playground;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace CarvedRock.Api
{
public class Startup
{
private readonly IConfiguration _config;
private readonly IHostingEnvironment _env;
public Startup(IConfiguration config, IHostingEnvironment env)
{
_config = config;
_env = env;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddDbContext<CarvedRockDbContext>(options =>
options.UseSqlServer(_config["ConnectionStrings:CarvedRock"]));
services.AddScoped<ProductRepository>();
services.AddScoped<IDependencyResolver>(s => new FuncDependencyResolver(s.GetRequiredService));
services.AddScoped<CarvedRockSchema>();
services.AddGraphQL(o => { o.ExposeExceptions = false; })
.AddGraphTypes(ServiceLifetime.Scoped);
}
public void Configure(IApplicationBuilder app, CarvedRockDbContext dbContext)
{
app.UseGraphQL<CarvedRockSchema>();
app.UseGraphQLPlayground(new GraphQLPlaygroundOptions());
dbContext.Seed();
}
}
}
2.5 Writing a Schema
- The GraphQL for .NET is going to be used.
- We can find more about it on GraphQL .NET documentation and GraphQL Examples
- The
GraphQL
folder with theTypes/ProductType.cs
,Types/ProductTypeEnum.cs
,CarvedRockQuery.cs
andCavedRockSchema.cs
documents must be created.
GraphQL/Types/ProductType.cs
using CarvedRock.Api.Data.Entities;
using GraphQL.Types;
namespace CarvedRock.Api.GraphQL.Types
{
public class ProductType: ObjectGraphType<Product>
{
public ProductType()
{
Field(t => t.Id);
Field(t => t.Name).Description("The name of the product");
Field(t => t.Description);
Field(t => t.IntroducedAt).Description("When the product was first introduced in the catalog");
Field(t => t.PhotoFileName).Description("The file name of the photo so the client can render it");
Field(t => t.Price);
Field(t => t.Rating).Description("The (max 5) star customer rating");
Field(t => t.Stock);
Field<ProductTypeEnumType>("Type", "The type of product");
}
}
}
GraphQL/Types/ProductTypeEnum.cs
using GraphQL.Types;
namespace CarvedRock.Api.GraphQL.Types
{
public class ProductTypeEnumType: EnumerationGraphType<Data.ProductType>
{
public ProductTypeEnumType()
{
Name = "Type";
Description = "The type of product";
}
}
}
GraphQL/CarvedRockQuery.cs
using CarvedRock.Api.GraphQL.Types;
using CarvedRock.Api.Repositories;
using GraphQL.Types;
namespace CarvedRock.Api.GraphQL
{
public class CarvedRockQuery: ObjectGraphType
{
public CarvedRockQuery(ProductRepository productRepository)
{
Field<ListGraphType<ProductType>>(
"products",
resolve: context => productRepository.GetAll()
);
}
}
}
GraphQL/CavedRockSchema.cs
using GraphQL;
using GraphQL.Types;
namespace CarvedRock.Api.GraphQL
{
public class CarvedRockSchema: Schema
{
public CarvedRockSchema(IDependencyResolver resolver): base(resolver)
{
Query = resolver.Resolve<CarvedRockQuery>();
}
}
}
2.6 Configuring ASP.NET Core
- Modify the
Startup.cs
document to configure the use of GraphQL.
Startup.cs
using CarvedRock.Api.Data;
using CarvedRock.Api.GraphQL;
using CarvedRock.Api.Repositories;
using GraphQL;
using GraphQL.Server;
using GraphQL.Server.Ui.Playground;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace CarvedRock.Api
{
public class Startup
{
private readonly IConfiguration _config;
private readonly IHostingEnvironment _env;
public Startup(IConfiguration config, IHostingEnvironment env)
{
_config = config;
_env = env;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<CarvedRockDbContext>(options =>
options.UseSqlServer(_config["ConnectionStrings:CarvedRock"]));
services.AddScoped<ProductRepository>();
services.AddScoped<IDependencyResolver>(s => new FuncDependencyResolver(s.GetRequiredService));
services.AddScoped<CarvedRockSchema>();
services.AddGraphQL(o => { o.ExposeExceptions = false; })
.AddGraphTypes(ServiceLifetime.Scoped);
}
public void Configure(IApplicationBuilder app, CarvedRockDbContext dbContext)
{
app.UseGraphQL<CarvedRockSchema>();
app.UseGraphQLPlayground(new GraphQLPlaygroundOptions());
dbContext.Seed();
}
}
}
It is used to add the GraphQL service
services.AddScoped<IDependencyResolver>(s => new FuncDependencyResolver(s.GetRequiredService));
It is used to add the Schema
services.AddScoped<CarvedRockSchema>();
It is used to register all the types that GraphQL uses
services.AddGraphQL(o => { o.ExposeExceptions = false; })
.AddGraphTypes(ServiceLifetime.Scoped);
It is used to add the GraphQL middleware for the schema
app.UseGraphQL<CarvedRockSchema>();
It is used to add the Playground UI
app.UseGraphQLPlayground(new GraphQLPlaygroundOptions());
2.7 Querying the API
Request
{
products {
name
description
}
}
Response
{
"data": {
"products": [
{
"name": "Mountain Walkers",
"description": "Use these sturdy shoes to pass any mountain range with ease."
},
{
"name": "Army Slippers",
"description": "For your everyday marches in the army."
},
{
"name": "Backpack Deluxe",
"description": "This backpack can survive any tornado."
},
{
"name": "Climbing Kit",
"description": "Anything you need to climb the mount Everest."
},
{
"name": "Blue Racer",
"description": "Simply the fastest kayak on earth and beyond for 2 persons."
},
{
"name": "Orange Demon",
"description": "One person kayak with hyper boost button."
}
]
}
}
Request
{
__schema {
types {
name
}
}
}
Response
{
"data": {
"__schema": {
"types": [
{
"name": "String"
},
{
"name": "Boolean"
},
{
"name": "Float"
},
{
"name": "Int"
},
{
"name": "ID"
},
{
"name": "Date"
},
{
"name": "DateTime"
},
{
"name": "DateTimeOffset"
},
{
"name": "Seconds"
},
{
"name": "Milliseconds"
},
{
"name": "Decimal"
},
{
"name": "__Schema"
},
{
"name": "__Type"
},
{
"name": "__TypeKind"
},
{
"name": "__Field"
},
{
"name": "__InputValue"
},
{
"name": "__EnumValue"
},
{
"name": "__Directive"
},
{
"name": "__DirectiveLocation"
},
{
"name": "CarvedRockQuery"
},
{
"name": "ProductType"
},
{
"name": "Type"
}
]
}
}
}
2.8 Summary
2
3 Creating a GraphQL API
3.1 Introduction
3.2 Adding Scalar Types
GraphQL/Types/ProductTypeEnum.cs
using GraphQL.Types;
namespace CarvedRock.Api.GraphQL.Types
{
public class ProductTypeEnumType: EnumerationGraphType<Data.ProductType>
{
public ProductTypeEnumType()
{
Name = "Type";
Description = "The type of product";
}
}
}
GraphQL/Types/ProductType.cs
using CarvedRock.Api.Data.Entities;
using GraphQL.Types;
namespace CarvedRock.Api.GraphQL.Types
{
public class ProductType: ObjectGraphType<Product>
{
public ProductType()
{
Field(t => t.Id);
Field(t => t.Name).Description("The name of the product");
Field(t => t.Description);
Field(t => t.IntroducedAt).Description("When the product was first introduced in the catalog");
Field(t => t.PhotoFileName).Description("The file name of the photo so the client can render it");
Field(t => t.Price);
Field(t => t.Rating).Description("The (max 5) star customer rating");
Field(t => t.Stock);
Field<ProductTypeEnumType>("Type", "The type of product");
}
}
}
Request
{
products {
id
name
description
introducedAt
photoFileName
price
rating
type
}
}
Response
{
"data": {
"products": [
{
"id": 1,
"name": "Mountain Walkers",
"description": "Use these sturdy shoes to pass any mountain range with ease.",
"introducedAt": "2019-01-23T05:56:20.0892442+00:00",
"photoFileName": "shutterstock_66842440.jpg",
"price": 219.5,
"rating": 4,
"type": "BOOTS"
},
{
"id": 2,
"name": "Army Slippers",
"description": "For your everyday marches in the army.",
"introducedAt": "2019-01-23T05:56:20.1826394+00:00",
"photoFileName": "shutterstock_222721876.jpg",
"price": 125.9,
"rating": 3,
"type": "BOOTS"
},
{
"id": 3,
"name": "Backpack Deluxe",
"description": "This backpack can survive any tornado.",
"introducedAt": "2019-01-23T05:56:20.183893+00:00",
"photoFileName": "shutterstock_6170527.jpg",
"price": 199.99,
"rating": 5,
"type": "CLIMBING_GEAR"
},
{
"id": 4,
"name": "Climbing Kit",
"description": "Anything you need to climb the mount Everest.",
"introducedAt": "2019-01-23T05:56:20.1839969+00:00",
"photoFileName": "shutterstock_48040747.jpg",
"price": 299.5,
"rating": 5,
"type": "CLIMBING_GEAR"
},
{
"id": 5,
"name": "Blue Racer",
"description": "Simply the fastest kayak on earth and beyond for 2 persons.",
"introducedAt": "2019-01-23T05:56:20.1840446+00:00",
"photoFileName": "shutterstock_441989509.jpg",
"price": 350,
"rating": 5,
"type": "KAYAKS"
},
{
"id": 6,
"name": "Orange Demon",
"description": "One person kayak with hyper boost button.",
"introducedAt": "2019-01-23T05:56:20.1840819+00:00",
"photoFileName": "shutterstock_495259978.jpg",
"price": 450,
"rating": 2,
"type": "KAYAKS"
}
]
}
}
3.3 Adding Complex Types
- We need to change the
ProductType
enum name toProductTypeEnum
Data/ProductType.cs
namespace CarvedRock.Api.Data
{
public enum ProductTypeEnum
{
Boots,
ClimbingGear,
Kayaks
}
}
- We need to add the
Data/Entities/ProductReview.cs
document to add the .ProductReview
entity.
Data/Entities/ProductReview.cs
using System.ComponentModel.DataAnnotations;
namespace CarvedRock.Api.Data.Entities
{
public class ProductReview
{
public int Id { get; set; }
public int ProductId { get; set; }
public Product Product { get; set; }
[StringLength(200), Required]
public string Title { get; set; }
public string Review { get; set; }
}
}
- We need to update the
Data/Entities/Product.cs
andData/Migrations/CarvedRockDbContext.cs
documents to include the new entity.
Data/Entities/Product.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace CarvedRock.Api.Data.Entities
{
public class Product
{
public int Id { get; set; }
[StringLength(100)]
public string Name { get; set; }
public ProductTypeEnum Type { get; set; }
public string Description { get; set; }
[Column(TypeName = "decimal(18,2)")]
public decimal Price { get; set; }
public int Stock { get; set; }
public int Rating { get; set; }
public DateTimeOffset IntroducedAt { get; set; }
[StringLength(100)]
public string PhotoFileName { get; set; }
public List<ProductReview> ProductReviews { get; set; }
}
}
Data/CarvedRockDbContext.cs
using CarvedRock.Api.Data.Entities;
using Microsoft.EntityFrameworkCore;
namespace CarvedRock.Api.Data
{
public class CarvedRockDbContext: DbContext
{
public CarvedRockDbContext(DbContextOptions<CarvedRockDbContext> options): base(options)
{
}
public DbSet<Product> Products { get; set; }
public DbSet<ProductReview> ProductReviews { get; set; }
}
}
- We need to update the
InitialData.cs
to include data for the newProductReviews
field.
Data/InitialData.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CarvedRock.Api.Data.Entities;
namespace CarvedRock.Api.Data
{
public static class InitialData
{
public static void Seed(this CarvedRockDbContext dbContext)
{
if (!dbContext.Products.Any())
{
dbContext.Products.Add(new Product
{
Name = "Mountain Walkers",
Description = "Use these sturdy shoes to pass any mountain range with ease.",
Price = 219.5m,
Rating = 4,
Type = ProductTypeEnum.Boots,
Stock = 12,
PhotoFileName = "shutterstock_66842440.jpg",
IntroducedAt = DateTimeOffset.Now.AddMonths(-1),
ProductReviews = new List<ProductReview>
{
new ProductReview
{
Title = "Crossed the Himalayas",
Review = "First released almost 30 years ago, the Titan 26078 is a classic work boot. It’s also one of Timberland’s all time best sellers."
},
new ProductReview
{
Title = "Comfortable",
Review = "One of the most comfortable hiking boots available, each pair comes complete with the Power Fit Comfort System, designed to offer maximum support at key areas of your feet."
}
}
});
dbContext.Products.Add(new Product
{
Name = "Army Slippers",
Description = "For your everyday marches in the army.",
Price = 125.9m,
Rating = 3,
Type = ProductTypeEnum.Boots,
Stock = 56,
PhotoFileName = "shutterstock_222721876.jpg",
IntroducedAt = DateTimeOffset.Now.AddMonths(-1),
ProductReviews = new List<ProductReview>
{
new ProductReview
{
Title = "Indestructible",
Review = "They have absolutely no break in period and can literally be worn to work the day that you get them."
},
new ProductReview
{
Title = "Safety toe",
Review = "The safety toe is made from an aluminium alloy which offers all the protection of steel but half the weight. The soles are also oil, abrasion and slip resistant."
}
}
});
dbContext.Products.Add(new Product
{
Name = "Backpack Deluxe",
Description = "This backpack can survive any tornado.",
Price = 199.99m,
Rating = 5,
Type = ProductTypeEnum.ClimbingGear,
Stock = 66,
PhotoFileName = "shutterstock_6170527.jpg",
IntroducedAt = DateTimeOffset.Now.AddMonths(-1),
ProductReviews = new List<ProductReview>
{
new ProductReview
{
Title = "Feels like canvas",
Review = "The Better Backpack is made from 100% recycled plastic but looks and feels like canvas. We were sent the grey bag with the tan leather accents and silver zippers. I’ve personally always liked tan leather paired with the color grey and appreciate the feel of the leather pull tabs and handles at the top of the bag. Additionally the inside is navy blue with a diagonal stitch pattern which gives it an air of sophistication."
}
}
});
dbContext.Products.Add(new Product
{
Name = "Climbing Kit",
Description = "Anything you need to climb the mount Everest.",
Price = 299.5m,
Rating = 5,
Type = ProductTypeEnum.ClimbingGear,
Stock = 3,
PhotoFileName = "shutterstock_48040747.jpg",
IntroducedAt = DateTimeOffset.Now.AddMonths(-1)
});
dbContext.Products.Add(new Product
{
Name = "Blue Racer",
Description = "Simply the fastest kayak on earth and beyond for 2 persons.",
Price = 350m,
Rating = 5,
Type = ProductTypeEnum.Kayaks,
Stock = 8,
PhotoFileName = "shutterstock_441989509.jpg",
IntroducedAt = DateTimeOffset.Now.AddMonths(-1),
ProductReviews = new List<ProductReview>
{
new ProductReview
{
Title = "So this is my 1st ever...",
Review = "So this is my 1st ever kayak and my 1st experience paddling a kayak. I struggled with whether I should spend more money to buy a \"Fishing\" kayak, or even a \"higher end\" kayak because my fear was paddling around a bathtub on the lake. I have to say, I love this kayak for me. It doesn\'t have a lot of the bells and whistles that some of the pricier kayaks have but, I\'m not disappointed.\r\n\r\nIt\'s pretty bare bones aside from some dry storage areas and some bungees. It does have a bungee to hold your paddle on the side. I feel it paddles very easy. It goes exactly where I want it to, I didn\'t struggle to keep it on course. A stiff wind can knock you off track but it was very sturdy and I feel like it would take a lot to tip it over. I stayed fairly dry aside from the dripping off the paddle but its a sit on top so thats to be expected."
},
new ProductReview
{
Title = "Great for all genders",
Review = "I am a fit 5'7 woman 140lbs and I take my 30# dog along with no troubles. I have fished with it by using a bucket strapped in the back with my bungee cords. It holds my rods and small tackle tray, bug spray etc so I can get to it all easily. I can even get it up on the roof of my jeep alone. "
},
new ProductReview
{
Title = "Happy",
Review = "Very happy with my purchase and I recommend this to anyone who doesn't want to spend over 600$ on a kayak but also doesn't want a cheap ole hunk of plastic. "
}
}
});
dbContext.Products.Add(new Product
{
Name = "Orange Demon",
Description = "One person kayak with hyper boost button.",
Price = 450m,
Rating = 2,
Type = ProductTypeEnum.Kayaks,
Stock = 1,
PhotoFileName = "shutterstock_495259978.jpg",
IntroducedAt = DateTimeOffset.Now.AddMonths(-1),
ProductReviews = new List<ProductReview>
{
new ProductReview {
Title = "Hobie took the paddle out",
Review = "Hobie took the paddle out of the hands of the masses, and changed kayak fishing forever. Ive heavily fished the \'Revo\' since 2011 and feel its about perfect for my use. As a serious cyclist its a natural fit, and it can cover huge distances. The torque the Mirage drive generates in rough conditions is often overlooked. I feel it could easily pull any propeller driven kayak backwards in a tug-o-war. I recently weighed my bare hull at 69#s, and that\'s not too shabby for ANY pedal yak. I still fish from paddle yaks, but I spend more time with the rods in my hands when on the Revo."
}
}
});
dbContext.SaveChanges();
}
}
}
}
- We need to add the
Repositories/ProductReviewRepository.cs
document.
Repositories/ProductReviewRepository.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CarvedRock.Api.Data;
using CarvedRock.Api.Data.Entities;
using Microsoft.EntityFrameworkCore;
namespace CarvedRock.Api.Repositories
{
public class ProductReviewRepository
{
private readonly CarvedRockDbContext _dbContext;
public ProductReviewRepository(CarvedRockDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<IEnumerable<ProductReview>> GetForProduct(int productId)
{
return await _dbContext.ProductReviews.Where(pr => pr.ProductId == productId).ToListAsync();
}
}
}
- We have to add the
ProductReviewRepository
class in theStartup.cs
document.
using CarvedRock.Api.Data;
using CarvedRock.Api.GraphQL;
using CarvedRock.Api.Repositories;
using GraphQL;
using GraphQL.Server;
using GraphQL.Server.Ui.Playground;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace CarvedRock.Api
{
public class Startup
{
private readonly IConfiguration _config;
private readonly IHostingEnvironment _env;
public Startup(IConfiguration config, IHostingEnvironment env)
{
_config = config;
_env = env;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<CarvedRockDbContext>(options =>
options.UseSqlServer(_config["ConnectionStrings:CarvedRock"]));
services.AddScoped<ProductRepository>();
services.AddScoped<ProductReviewRepository>();
services.AddScoped<IDependencyResolver>(s => new FuncDependencyResolver(s.GetRequiredService));
services.AddScoped<CarvedRockSchema>();
services.AddGraphQL(o => { o.ExposeExceptions = false; })
.AddGraphTypes(ServiceLifetime.Scoped);
}
public void Configure(IApplicationBuilder app, CarvedRockDbContext dbContext)
{
app.UseGraphQL<CarvedRockSchema>();
app.UseGraphQLPlayground(new GraphQLPlaygroundOptions());
dbContext.Seed();
}
}
}
- We also need to add the
GraphQl/Types/ProductReviewType.cs
with theGraphQL ProductReviewType
type that we have to add to theGraphQl/Types/ProductReviewType.cs
document to update theGraphQL ProductType
type.
GraphQl/Types/ProductReviewType.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CarvedRock.Api.Data.Entities;
using GraphQL.Types;
namespace CarvedRock.Api.GraphQL.Types
{
public class ProductReviewType: ObjectGraphType<ProductReview>
{
public ProductReviewType()
{
Field(t => t.Id);
Field(t => t.Title);
Field(t => t.Review);
}
}
}
GraphQl/Types/ProductType.cs
using CarvedRock.Api.Data.Entities;
using CarvedRock.Api.Repositories;
using GraphQL.DataLoader;
using GraphQL.Types;
namespace CarvedRock.Api.GraphQL.Types
{
public class ProductType : ObjectGraphType<Product>
{
public ProductType(ProductReviewRepository reviewRepository)
{
Field(t => t.Id);
Field(t => t.Name);
Field(t => t.Description);
Field(t => t.IntroducedAt).Description("When the product was first introduced in the catalog");
Field(t => t.PhotoFileName).Description("The file name of the photo so the client can render it");
Field(t => t.Price);
Field(t => t.Rating).Description("The (max 5) star customer rating");
Field(t => t.Stock);
Field<ProductTypeEnumType>("Type", "The type of product");
Field<ListGraphType<ProductReviewType>>(
"reviews",
resolve: context => reviewRepository.GetForProduct(context.Source.Id)
);
}
}
}
- We need to drop the database and recreate it.
uan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/GraphQL/building-graphql-apis-aspdotnet-core/CarvedRock.Api
$ dotnet ef database drop
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 2.1.4-rtm-31024 initialized 'CarvedRockDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None
Are you sure you want to drop the database 'CarvedRock' on server 'localhost'? (y/N)
y
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 2.1.4-rtm-31024 initialized 'CarvedRockDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None
Dropping database 'CarvedRock'.
Database 'CarvedRock' did not exist, no action was taken.
It was removed manually before executing it.
The
Data\Migrations
folder should be removed to recreate it.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/GraphQL/building-graphql-apis-aspdotnet-core/CarvedRock.Api
$ dotnet ef migrations add ProductReview
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 2.1.4-rtm-31024 initialized 'CarvedRockDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None
Done. To undo this action, use 'ef migrations remove'
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/GraphQL/building-graphql-apis-aspdotnet-core/CarvedRock.Api
$ dotnet ef database update
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 2.1.4-rtm-31024 initialized 'CarvedRockDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (8ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT OBJECT_ID(N'[__EFMigrationsHistory]');
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT OBJECT_ID(N'[__EFMigrationsHistory]');
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [MigrationId], [ProductVersion]
FROM [__EFMigrationsHistory]
ORDER BY [MigrationId];
info: Microsoft.EntityFrameworkCore.Migrations[20402]
Applying migration '20190223103418_ProductReview'.
Applying migration '20190223103418_ProductReview'.
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE TABLE [Products] (
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(100) NULL,
[Type] int NOT NULL,
[Description] nvarchar(max) NULL,
[Price] decimal(18,2) NOT NULL,
[Stock] int NOT NULL,
[Rating] int NOT NULL,
[IntroducedAt] datetimeoffset NOT NULL,
[PhotoFileName] nvarchar(100) NULL,
CONSTRAINT [PK_Products] PRIMARY KEY ([Id])
);
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (4ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE TABLE [ProductReviews] (
[Id] int NOT NULL IDENTITY,
[ProductId] int NOT NULL,
[Title] nvarchar(200) NOT NULL,
[Review] nvarchar(max) NULL,
CONSTRAINT [PK_ProductReviews] PRIMARY KEY ([Id]),
CONSTRAINT [FK_ProductReviews_Products_ProductId] FOREIGN KEY ([ProductId]) REFERENCES [Products] ([Id]) ON DELETE CASCADE
);
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (3ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE INDEX [IX_ProductReviews_ProductId] ON [ProductReviews] ([ProductId]);
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (4ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20190223103418_ProductReview', N'2.1.4-rtm-31024');
Done.
- Build the solution
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/GraphQL/building-graphql-apis-aspdotnet-core/CarvedRock.Api
$ dotnet build
Microsoft (R) Build Engine version 15.9.20+g88f5fadfbe for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.
Restore completed in 159.19 ms for C:\Work\Training\Pre\GraphQL\building-graphql-apis-aspdotnet-core\CarvedRock.Api\CarvedRock.Api.csproj.
CarvedRock.Api -> C:\Work\Training\Pre\GraphQL\building-graphql-apis-aspdotnet-core\CarvedRock.Api\bin\Debug\netcoreapp2.1\CarvedRock.Api.dll
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:01.86
- Run the solution
$ dotnet run
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 2.1.4-rtm-31024 initialized 'CarvedRockDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (8ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT CASE
WHEN EXISTS (
SELECT 1
FROM [Products] AS [p])
THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT)
END
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (28ms) [Parameters=[@p0='?' (Size = 4000), @p1='?' (DbType = DateTimeOffset), @p2='?' (Size = 100), @p3='?' (Size = 100), @p4='?' (DbType = Decimal), @p5='?' (DbType = Int32), @p6='?' (DbType = Int32), @p7='?' (DbType = Int32), @p8='?' (Size = 4000), @p9='?' (DbType = DateTimeOffset), @p10='?' (Size = 100), @p11='?' (Size = 100), @p12='?' (DbType = Decimal), @p13='?' (DbType = Int32), @p14='?' (DbType = Int32), @p15='?' (DbType = Int32), @p16='?' (Size = 4000), @p17='?' (DbType = DateTimeOffset), @p18='?' (Size = 100), @p19='?' (Size = 100), @p20='?' (DbType = Decimal), @p21='?' (DbType = Int32), @p22='?' (DbType = Int32), @p23='?' (DbType = Int32), @p24='?' (Size = 4000), @p25='?' (DbType = DateTimeOffset), @p26='?' (Size = 100), @p27='?' (Size = 100), @p28='?' (DbType = Decimal), @p29='?' (DbType = Int32), @p30='?' (DbType = Int32), @p31='?' (DbType = Int32), @p32='?' (Size = 4000), @p33='?' (DbType = DateTimeOffset), @p34='?' (Size = 100), @p35='?' (Size = 100), @p36='?' (DbType = Decimal), @p37='?' (DbType = Int32), @p38='?' (DbType = Int32), @p39='?' (DbType = Int32), @p40='?' (Size = 4000), @p41='?' (DbType = DateTimeOffset), @p42='?' (Size = 100), @p43='?' (Size = 100), @p44='?' (DbType = Decimal), @p45='?' (DbType = Int32), @p46='?' (DbType = Int32), @p47='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DECLARE @inserted0 TABLE ([Id] int, [_Position] [int]);
MERGE [Products] USING (
VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7, 0),
(@p8, @p9, @p10, @p11, @p12, @p13, @p14, @p15, 1),
(@p16, @p17, @p18, @p19, @p20, @p21, @p22, @p23, 2),
(@p24, @p25, @p26, @p27, @p28, @p29, @p30, @p31, 3),
(@p32, @p33, @p34, @p35, @p36, @p37, @p38, @p39, 4),
(@p40, @p41, @p42, @p43, @p44, @p45, @p46, @p47, 5)) AS i ([Description], [IntroducedAt], [Name], [PhotoFileName], [Price], [Rating], [Stock], [Type], _Position) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([Description], [IntroducedAt], [Name], [PhotoFileName], [Price], [Rating], [Stock], [Type])
VALUES (i.[Description], i.[IntroducedAt], i.[Name], i.[PhotoFileName], i.[Price], i.[Rating], i.[Stock], i.[Type])
OUTPUT INSERTED.[Id], i._Position
INTO @inserted0;
SELECT [t].[Id] FROM [Products] t
INNER JOIN @inserted0 i ON ([t].[Id] = [i].[Id])
ORDER BY [i].[_Position];
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (15ms) [Parameters=[@p48='?' (DbType = Int32), @p49='?' (Size = 4000), @p50='?' (Size = 200), @p51='?' (DbType = Int32), @p52='?' (Size = 4000), @p53='?' (Size = 200), @p54='?' (DbType = Int32), @p55='?' (Size = 4000), @p56='?' (Size = 200), @p57='?' (DbType = Int32), @p58='?' (Size = 4000), @p59='?' (Size = 200), @p60='?' (DbType = Int32), @p61='?' (Size = 4000), @p62='?' (Size = 200), @p63='?' (DbType = Int32), @p64='?' (Size = 4000), @p65='?' (Size = 200), @p66='?' (DbType = Int32), @p67='?' (Size = 4000), @p68='?' (Size = 200), @p69='?' (DbType = Int32), @p70='?' (Size = 4000), @p71='?' (Size = 200), @p72='?' (DbType = Int32), @p73='?' (Size = 4000), @p74='?' (Size = 200)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DECLARE @inserted0 TABLE ([Id] int, [_Position] [int]);
MERGE [ProductReviews] USING (
VALUES (@p48, @p49, @p50, 0),
(@p51, @p52, @p53, 1),
(@p54, @p55, @p56, 2),
(@p57, @p58, @p59, 3),
(@p60, @p61, @p62, 4),
(@p63, @p64, @p65, 5),
(@p66, @p67, @p68, 6),
(@p69, @p70, @p71, 7),
(@p72, @p73, @p74, 8)) AS i ([ProductId], [Review], [Title], _Position) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([ProductId], [Review], [Title])
VALUES (i.[ProductId], i.[Review], i.[Title])
OUTPUT INSERTED.[Id], i._Position
INTO @inserted0;
SELECT [t].[Id] FROM [ProductReviews] t
INNER JOIN @inserted0 i ON ([t].[Id] = [i].[Id])
ORDER BY [i].[_Position];
Hosting environment: Development
Content root path: C:\Work\Training\Pre\GraphQL\building-graphql-apis-aspdotnet-core\CarvedRock.Api
Now listening on: https://localhost:5001
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.
Request
{
products {
name
reviews {
title
}
}
}
Response
{
"data": {
"products": [
{
"name": "Mountain Walkers",
"reviews": [
{
"title": "Crossed the Himalayas"
},
{
"title": "Comfortable"
}
]
},
{
"name": "Army Slippers",
"reviews": [
{
"title": "Indestructible"
},
{
"title": "Safety toe"
}
]
},
{
"name": "Backpack Deluxe",
"reviews": [
{
"title": "Feels like canvas"
}
]
},
{
"name": "Climbing Kit",
"reviews": []
},
{
"name": "Blue Racer",
"reviews": [
{
"title": "So this is my 1st ever..."
},
{
"title": "Great for all genders"
},
{
"title": "Happy"
}
]
},
{
"name": "Orange Demon",
"reviews": [
{
"title": "Hobie took the paddle out"
}
]
}
]
}
}
- There are many queries sent to the database
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 GET https://localhost:5001/ui/playground
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 36.758ms 200 text/html
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 POST https://localhost:5001/graphql application/json 1550
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 GET https://localhost:5001/graphql
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 124.4756ms 200 application/json
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 871.0606ms 200 application/json
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 POST https://localhost:5001/graphql application/json 131
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 2.1.4-rtm-31024 initialized 'CarvedRockDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (16ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Description], [p].[IntroducedAt], [p].[Name], [p].[PhotoFileName], [p].[Price], [p].[Rating], [p].[Stock], [p].[Type]
FROM [Products] AS [p]
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (4ms) [Parameters=[@__productId_0='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
SELECT [pr].[Id], [pr].[ProductId], [pr].[Review], [pr].[Title]
FROM [ProductReviews] AS [pr]
WHERE [pr].[ProductId] = @__productId_0
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (1ms) [Parameters=[@__productId_0='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
SELECT [pr].[Id], [pr].[ProductId], [pr].[Review], [pr].[Title]
FROM [ProductReviews] AS [pr]
WHERE [pr].[ProductId] = @__productId_0
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (1ms) [Parameters=[@__productId_0='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
SELECT [pr].[Id], [pr].[ProductId], [pr].[Review], [pr].[Title]
FROM [ProductReviews] AS [pr]
WHERE [pr].[ProductId] = @__productId_0
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (0ms) [Parameters=[@__productId_0='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
SELECT [pr].[Id], [pr].[ProductId], [pr].[Review], [pr].[Title]
FROM [ProductReviews] AS [pr]
WHERE [pr].[ProductId] = @__productId_0
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (1ms) [Parameters=[@__productId_0='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
SELECT [pr].[Id], [pr].[ProductId], [pr].[Review], [pr].[Title]
FROM [ProductReviews] AS [pr]
WHERE [pr].[ProductId] = @__productId_0
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (1ms) [Parameters=[@__productId_0='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
SELECT [pr].[Id], [pr].[ProductId], [pr].[Review], [pr].[Title]
FROM [ProductReviews] AS [pr]
WHERE [pr].[ProductId] = @__productId_0
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 247.9088ms 200 application/json
- That is not very efficient. It has to be modified using a DataLoader.
3.4 DataLoader
- In order to use the data loader we need to modify the
Startup.cs
document.
Startup.cs
using CarvedRock.Api.Data;
using CarvedRock.Api.GraphQL;
using CarvedRock.Api.Repositories;
using GraphQL;
using GraphQL.Server;
using GraphQL.Server.Ui.Playground;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace CarvedRock.Api
{
public class Startup
{
private readonly IConfiguration _config;
private readonly IHostingEnvironment _env;
public Startup(IConfiguration config, IHostingEnvironment env)
{
_config = config;
_env = env;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<CarvedRockDbContext>(options =>
options.UseSqlServer(_config["ConnectionStrings:CarvedRock"]));
services.AddScoped<ProductRepository>();
services.AddScoped<ProductReviewRepository>();
services.AddScoped<IDependencyResolver>(s => new FuncDependencyResolver(s.GetRequiredService));
services.AddScoped<CarvedRockSchema>();
services.AddGraphQL(o => { o.ExposeExceptions = false; })
.AddGraphTypes(ServiceLifetime.Scoped)
.AddDataLoader();
}
public void Configure(IApplicationBuilder app, CarvedRockDbContext dbContext)
{
app.UseGraphQL<CarvedRockSchema>();
app.UseGraphQLPlayground(new GraphQLPlaygroundOptions());
dbContext.Seed();
}
}
}
- Then we can inject the DataLoader in the
ProductType
GraphQL type.
GraphQL/Types/ProductType.cs
using CarvedRock.Api.Data.Entities;
using CarvedRock.Api.Repositories;
using GraphQL.DataLoader;
using GraphQL.Types;
namespace CarvedRock.Api.GraphQL.Types
{
public class ProductType: ObjectGraphType<Product>
{
public ProductType(ProductReviewRepository reviewRepository, IDataLoaderContextAccessor dataLoaderAccessor)
{
Field(t => t.Id);
Field(t => t.Name);
Field(t => t.Description);
Field(t => t.IntroducedAt).Description("When the product was first introduced in the catalog");
Field(t => t.PhotoFileName).Description("The file name of the photo so the client can render it");
Field(t => t.Price);
Field(t => t.Rating).Description("The (max 5) star customer rating");
Field(t => t.Stock);
Field<ProductTypeEnumType>("Type", "The type of product");
Field<ListGraphType<ProductReviewType>>(
"reviews",
resolve: context =>
{
var loader =
dataLoaderAccessor.Context.GetOrAddCollectionBatchLoader<int, ProductReview>(
"GetReviewsByProductId", reviewRepository.GetForProducts);
return loader.LoadAsync(context.Source.Id);
});
}
}
}
- We need to modify the
Repositories/ProductReviewRepository.cs
document to add theGetForProducts
method.
Repositories/ProductReviewRepository.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CarvedRock.Api.Data;
using CarvedRock.Api.Data.Entities;
using Microsoft.EntityFrameworkCore;
namespace CarvedRock.Api.Repositories
{
public class ProductReviewRepository
{
private readonly CarvedRockDbContext _dbContext;
public ProductReviewRepository(CarvedRockDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<IEnumerable<ProductReview>> GetForProduct(int productId)
{
return await _dbContext.ProductReviews.Where(pr => pr.ProductId == productId).ToListAsync();
}
public async Task<ILookup<int, ProductReview>> GetForProducts(IEnumerable<int> productIds)
{
var reviews = await _dbContext.ProductReviews.Where(pr => productIds.Contains(pr.ProductId)).ToListAsync();
return reviews.ToLookup(r => r.ProductId);
}
}
}
- We need to restart the application.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/GraphQL/building-graphql-apis-aspdotnet-core/CarvedRock.Api
$ dotnet run
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 2.1.4-rtm-31024 initialized 'CarvedRockDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (9ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT CASE
WHEN EXISTS (
SELECT 1
FROM [Products] AS [p])
THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT)
END
Hosting environment: Development
Content root path: C:\Work\Training\Pre\GraphQL\building-graphql-apis-aspdotnet-core\CarvedRock.Api
Now listening on: https://localhost:5001
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.
- We have to run the same query as before.
Request
{
products {
name
reviews {
title
}
}
}
Response
{
"data": {
"products": [
{
"name": "Mountain Walkers",
"reviews": [
{
"title": "Crossed the Himalayas"
},
{
"title": "Comfortable"
}
]
},
{
"name": "Army Slippers",
"reviews": [
{
"title": "Indestructible"
},
{
"title": "Safety toe"
}
]
},
{
"name": "Backpack Deluxe",
"reviews": [
{
"title": "Feels like canvas"
}
]
},
{
"name": "Climbing Kit",
"reviews": []
},
{
"name": "Blue Racer",
"reviews": [
{
"title": "So this is my 1st ever..."
},
{
"title": "Great for all genders"
},
{
"title": "Happy"
}
]
},
{
"name": "Orange Demon",
"reviews": [
{
"title": "Hobie took the paddle out"
}
]
}
]
}
}
- But now the access to the database is only twice, one query for the products and another query for the rewies.
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 POST https://localhost:5001/graphql application/json 131
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 2.1.4-rtm-31024 initialized 'CarvedRockDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (13ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Description], [p].[IntroducedAt], [p].[Name], [p].[PhotoFileName], [p].[Price], [p].[Rating], [p].[Stock], [p].[Type]
FROM [Products] AS [p]
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (3ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [pr].[Id], [pr].[ProductId], [pr].[Review], [pr].[Title]
FROM [ProductReviews] AS [pr]
WHERE [pr].[ProductId] IN (1, 2, 3, 4, 5, 6)
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 643.1114ms 200 application/json
3.5 Arguments
- We need to modify the
ProductRepository
class to include the newGetOne
method.
Repositories/ProductRepository.cs
using System.Collections.Generic;
using System.Threading.Tasks;
using CarvedRock.Api.Data;
using CarvedRock.Api.Data.Entities;
using Microsoft.EntityFrameworkCore;
namespace CarvedRock.Api.Repositories
{
public class ProductRepository
{
private readonly CarvedRockDbContext _dbContext;
public ProductRepository(CarvedRockDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<IEnumerable<Product>> GetAll()
{
return await _dbContext.Products.ToListAsync();
}
public Task<Product> GetOne(int id)
{
return _dbContext.Products.SingleAsync(p => p.Id == id);
}
}
}
- We need to modify the
CarvedRockQuery
class to include a new query to return just one product with the help of an argument.
GraphQL/CarvedRockQuery.cs
using CarvedRock.Api.GraphQL.Types;
using CarvedRock.Api.Repositories;
using GraphQL.Types;
namespace CarvedRock.Api.GraphQL
{
public class CarvedRockQuery: ObjectGraphType
{
public CarvedRockQuery(ProductRepository productRepository)
{
Field<ListGraphType<ProductType>>(
"products",
resolve: context => productRepository.GetAll()
);
Field<ProductType>(
"product",
arguments: new QueryArguments(new QueryArgument<NonNullGraphType<IdGraphType>>
{Name = "id"}),
resolve: context =>
{
var id = context.GetArgument<int>("id");
return productRepository.GetOne(id);
}
);
}
}
}
- Run the application again
$ dotnet run
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 2.1.4-rtm-31024 initialized 'CarvedRockDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (9ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT CASE
WHEN EXISTS (
SELECT 1
FROM [Products] AS [p])
THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT)
END
Hosting environment: Development
Content root path: C:\Work\Training\Pre\GraphQL\building-graphql-apis-aspdotnet-core\CarvedRock.Api
Now listening on: https://localhost:5001
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.
- We can run a new Query.
Request
{
product(id: 3) {
id
name
reviews {
title
}
}
}
Response
{
"data": {
"product": {
"id": 3,
"name": "Backpack Deluxe",
"reviews": [
{
"title": "Feels like canvas"
}
]
}
}
}
3.6 Authorization
We should need to modify the
Startup.cs
document to include the use of theAuthorizationService
. We cannot use theAuthorize
attribute withGraphQL
so we need to implement this service.We have to add
AddUserContextBuilder
to get the User from the context.
- Then we can modify the
ProductType
GraphQL type to get the User Context and the Claims from it. We could manage the authorization with it.
3.7 Interfaces
3.8 Summary
4. Consuming a GraphQL API
4.1 Introduction
4.2 Sending a Query via an HTTP Request
- We can use
Postmnan
to send an Http Request
Request using GET
GET /graphql?query={products{id name price rating photoFileName}} HTTP/1.1
Host: localhost:5000
Cache-Control: no-cache
Response
{
"data": {
"products": [
{
"id": 1,
"name": "Mountain Walkers",
"price": 219.5,
"rating": 4,
"photoFileName": "shutterstock_66842440.jpg"
},
{
"id": 2,
"name": "Army Slippers",
"price": 125.9,
"rating": 3,
"photoFileName": "shutterstock_222721876.jpg"
},
{
"id": 3,
"name": "Backpack Deluxe",
"price": 199.99,
"rating": 5,
"photoFileName": "shutterstock_6170527.jpg"
},
{
"id": 4,
"name": "Climbing Kit",
"price": 299.5,
"rating": 5,
"photoFileName": "shutterstock_48040747.jpg"
},
{
"id": 5,
"name": "Blue Racer",
"price": 350,
"rating": 5,
"photoFileName": "shutterstock_441989509.jpg"
},
{
"id": 6,
"name": "Orange Demon",
"price": 450,
"rating": 2,
"photoFileName": "shutterstock_495259978.jpg"
}
]
}
}
Request using POST
POST /graphql HTTP/1.1
Host: localhost:5000
Content-Type: application/json
Cache-Control: no-cache
Postman-Token: eeaecabf-aa1d-c825-14c8-ab6545d512ee
{
"query": "{products {id name price rating photoFileName} }"
}
Response
{
"data": {
"products": [
{
"id": 1,
"name": "Mountain Walkers",
"price": 219.5,
"rating": 4,
"photoFileName": "shutterstock_66842440.jpg"
},
{
"id": 2,
"name": "Army Slippers",
"price": 125.9,
"rating": 3,
"photoFileName": "shutterstock_222721876.jpg"
},
{
"id": 3,
"name": "Backpack Deluxe",
"price": 199.99,
"rating": 5,
"photoFileName": "shutterstock_6170527.jpg"
},
{
"id": 4,
"name": "Climbing Kit",
"price": 299.5,
"rating": 5,
"photoFileName": "shutterstock_48040747.jpg"
},
{
"id": 5,
"name": "Blue Racer",
"price": 350,
"rating": 5,
"photoFileName": "shutterstock_441989509.jpg"
},
{
"id": 6,
"name": "Orange Demon",
"price": 450,
"rating": 2,
"photoFileName": "shutterstock_495259978.jpg"
}
]
}
}
- Notice the response is wrapped in a
data
json property.
4.3 Aliases
- We can manipulate the response using
aliases
for both types and fields.
Request
{
result: products {
id
name
price
rating
photo: photoFileName
}
}
Response
{
"data": {
"result": [
{
"id": 1,
"name": "Mountain Walkers",
"price": 219.5,
"rating": 4,
"photo": "shutterstock_66842440.jpg"
},
{
"id": 2,
"name": "Army Slippers",
"price": 125.9,
"rating": 3,
"photo": "shutterstock_222721876.jpg"
},
{
"id": 3,
"name": "Backpack Deluxe",
"price": 199.99,
"rating": 5,
"photo": "shutterstock_6170527.jpg"
},
{
"id": 4,
"name": "Climbing Kit",
"price": 299.5,
"rating": 5,
"photo": "shutterstock_48040747.jpg"
},
{
"id": 5,
"name": "Blue Racer",
"price": 350,
"rating": 5,
"photo": "shutterstock_441989509.jpg"
},
{
"id": 6,
"name": "Orange Demon",
"price": 450,
"rating": 2,
"photo": "shutterstock_495259978.jpg"
}
]
}
}
- We can also send multiple queries in one go. But it doesn't work if we don't use aliases for each query.
Request
{
product(id: 1) {
id
name
price
stock
}
product(id: 2) {
id
name
price
stock
}
}
Response
{
"data": {
"product": {
"id": 1,
"name": "Mountain Walkers",
"price": 219.5,
"stock": 12
}
}
}
Request
{
p1: product(id: 1) {
id
name
price
stock
}
p2: product(id: 2) {
id
name
price
stock
}
}
Response
{
"data": {
"p1": {
"id": 1,
"name": "Mountain Walkers",
"price": 219.5,
"stock": 12
},
"p2": {
"id": 2,
"name": "Army Slippers",
"price": 125.9,
"stock": 56
}
}
}
4.4 Fragments
- We can use
Fragments
as a base template of a query type that we want to execute many times.
Request
{
p1: product(id: 1) {
...comparisonFields
}
p2: product(id: 2) {
...comparisonFields
}
}
fragment comparisonFields on ProductType {
id
name
price
stock
}
Response
{
"data": {
"p1": {
"id": 1,
"name": "Mountain Walkers",
"price": 219.5,
"stock": 12
},
"p2": {
"id": 2,
"name": "Army Slippers",
"price": 125.9,
"stock": 56
}
}
}
4.5 Named Queries and Variables
- We can put a name to a query that doesn't affect the result
Request
query all {
products {
id
name
price
rating
photoFileName
}
}
Response
{
"data": {
"products": [
{
"id": 1,
"name": "Mountain Walkers",
"price": 219.5,
"rating": 4,
"photoFileName": "shutterstock_66842440.jpg"
},
{
"id": 2,
"name": "Army Slippers",
"price": 125.9,
"rating": 3,
"photoFileName": "shutterstock_222721876.jpg"
},
{
"id": 3,
"name": "Backpack Deluxe",
"price": 199.99,
"rating": 5,
"photoFileName": "shutterstock_6170527.jpg"
},
{
"id": 4,
"name": "Climbing Kit",
"price": 299.5,
"rating": 5,
"photoFileName": "shutterstock_48040747.jpg"
},
{
"id": 5,
"name": "Blue Racer",
"price": 350,
"rating": 5,
"photoFileName": "shutterstock_441989509.jpg"
},
{
"id": 6,
"name": "Orange Demon",
"price": 450,
"rating": 2,
"photoFileName": "shutterstock_495259978.jpg"
}
]
}
}
- We can add multiple queries in one go.
Request
query all {
products {
id
name
price
rating
photoFileName
}
}
query p1 {
product(id: 1) {
id
name
price
rating
photoFileName
}
}
- When we execute it, it asks which query we want to execute
- It also works when we make an Http request, but we have to indicate which one we want to execute using
operationName
.
request
POST /graphql HTTP/1.1
Host: localhost:5000
Content-Type: application/json
Cache-Control: no-cache
Postman-Token: 74977283-28e7-5aff-a235-ea850e70f552
{
"query": "
query all
{ products {id name price rating photoFileName}
}
query p1
{ product(id: 1) {id name price rating photoFileName}
}",
"operationName": "p1"
}
Response
{
"data": {
"product": {
"id": 1,
"name": "Mountain Walkers",
"price": 219.5,
"rating": 4,
"photoFileName": "shutterstock_66842440.jpg"
}
}
}
- We can also use variables indicating the Type.
Request
query all {
products {
id
name
price
rating
photoFileName
}
}
query product($productId: ID!) {
product(id: $productId) {
id
name
price
rating
photoFileName
}
}
variable
{
"productId": 1
}
response
{
"data": {
"product": {
"id": 1,
"name": "Mountain Walkers",
"price": 219.5,
"rating": 4,
"photoFileName": "shutterstock_66842440.jpg"
}
}
}
- We can also use variables when submiting an Http request
request
POST /graphql HTTP/1.1
Host: localhost:5000
Content-Type: application/json
Cache-Control: no-cache
Postman-Token: 4655e269-fe2e-8631-4e28-3ff13a4f3d42
{
"query": "
query all
{ products {id name price rating photoFileName}
}
query product($productId: ID!)
{ product(id: $productId) {id name price rating photoFileName}
}",
"operationName": "product",
"variables": { "productId": 1}
}
response
{
"data": {
"product": {
"id": 1,
"name": "Mountain Walkers",
"price": 219.5,
"rating": 4,
"photoFileName": "shutterstock_66842440.jpg"
}
}
}
4.6 Directives
Directives
re used as decision makers in the queries
Request
query all($showPrice: Boolean!) {
products {
id
name
price @include(if: $showPrice)
rating
photoFileName
}
}
variable
{
"showPrice": false
}
response
{
"data": {
"products": [
{
"id": 1,
"name": "Mountain Walkers",
"rating": 4,
"photoFileName": "shutterstock_66842440.jpg"
},
{
"id": 2,
"name": "Army Slippers",
"rating": 3,
"photoFileName": "shutterstock_222721876.jpg"
},
{
"id": 3,
"name": "Backpack Deluxe",
"rating": 5,
"photoFileName": "shutterstock_6170527.jpg"
},
{
"id": 4,
"name": "Climbing Kit",
"rating": 5,
"photoFileName": "shutterstock_48040747.jpg"
},
{
"id": 5,
"name": "Blue Racer",
"rating": 5,
"photoFileName": "shutterstock_441989509.jpg"
},
{
"id": 6,
"name": "Orange Demon",
"rating": 2,
"photoFileName": "shutterstock_495259978.jpg"
}
]
}
}
variable
{
"showPrice": true
}
response
{
"data": {
"products": [
{
"id": 1,
"name": "Mountain Walkers",
"price": 219.5,
"rating": 4,
"photoFileName": "shutterstock_66842440.jpg"
},
{
"id": 2,
"name": "Army Slippers",
"price": 125.9,
"rating": 3,
"photoFileName": "shutterstock_222721876.jpg"
},
{
"id": 3,
"name": "Backpack Deluxe",
"price": 199.99,
"rating": 5,
"photoFileName": "shutterstock_6170527.jpg"
},
{
"id": 4,
"name": "Climbing Kit",
"price": 299.5,
"rating": 5,
"photoFileName": "shutterstock_48040747.jpg"
},
{
"id": 5,
"name": "Blue Racer",
"price": 350,
"rating": 5,
"photoFileName": "shutterstock_441989509.jpg"
},
{
"id": 6,
"name": "Orange Demon",
"price": 450,
"rating": 2,
"photoFileName": "shutterstock_495259978.jpg"
}
]
}
}
Request
query all($showPrice: Boolean = false) {
products {
id
name
price @include(if: $showPrice)
rating
photoFileName
}
}
variable (empty)
response
{
"data": {
"products": [
{
"id": 1,
"name": "Mountain Walkers",
"rating": 4,
"photoFileName": "shutterstock_66842440.jpg"
},
{
"id": 2,
"name": "Army Slippers",
"rating": 3,
"photoFileName": "shutterstock_222721876.jpg"
},
{
"id": 3,
"name": "Backpack Deluxe",
"rating": 5,
"photoFileName": "shutterstock_6170527.jpg"
},
{
"id": 4,
"name": "Climbing Kit",
"rating": 5,
"photoFileName": "shutterstock_48040747.jpg"
},
{
"id": 5,
"name": "Blue Racer",
"rating": 5,
"photoFileName": "shutterstock_441989509.jpg"
},
{
"id": 6,
"name": "Orange Demon",
"rating": 2,
"photoFileName": "shutterstock_495259978.jpg"
}
]
}
}
Request
query all($showPrice: Boolean = false) {
products {
id
name
price @skip(if: $showPrice)
rating
photoFileName
}
}
variable (empty)
response
{
"data": {
"products": [
{
"id": 1,
"name": "Mountain Walkers",
"price": 219.5,
"rating": 4,
"photoFileName": "shutterstock_66842440.jpg"
},
{
"id": 2,
"name": "Army Slippers",
"price": 125.9,
"rating": 3,
"photoFileName": "shutterstock_222721876.jpg"
},
{
"id": 3,
"name": "Backpack Deluxe",
"price": 199.99,
"rating": 5,
"photoFileName": "shutterstock_6170527.jpg"
},
{
"id": 4,
"name": "Climbing Kit",
"price": 299.5,
"rating": 5,
"photoFileName": "shutterstock_48040747.jpg"
},
{
"id": 5,
"name": "Blue Racer",
"price": 350,
"rating": 5,
"photoFileName": "shutterstock_441989509.jpg"
},
{
"id": 6,
"name": "Orange Demon",
"price": 450,
"rating": 2,
"photoFileName": "shutterstock_495259978.jpg"
}
]
}
}
4.7 Errors
Request (with invalid i field)
query all($showPrice: Boolean = false) {
products {
i
name
price @skip(if: $showPrice)
rating
photoFileName
}
}
variable (empty)
response
{
"errors": [
{
"message": "Cannot query field \"i\" on type \"ProductType\".",
"locations": [
{
"line": 3,
"column": 5
}
],
"extensions": {
"code": "5.2.1"
}
}
]
}
The error message contains the
message
of the error, thelocations
where the error occurs and some metadata information asextensions
.There are the standard rules that the query shoul comply
- We can add additional rules
- We can find more information about the custom validations on Query Validation
4.8 An ASP.NET Core Client
- Copy the
CarvedRock.Web
project from the04
folder.
- The
CarvedRock.sln
must be updated to include the newproject
CarvedRock.sln
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27703.2035
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CarvedRock.Api", "CarvedRock.Api\CarvedRock.Api.csproj", "{81C7A112-94D5-40C7-B28A-2C2356E71B0E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarvedRock.Web", "CarvedRock.Web\CarvedRock.Web.csproj", "{2D1BE822-9681-44CF-B456-C9506A1260F1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{81C7A112-94D5-40C7-B28A-2C2356E71B0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{81C7A112-94D5-40C7-B28A-2C2356E71B0E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{81C7A112-94D5-40C7-B28A-2C2356E71B0E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{81C7A112-94D5-40C7-B28A-2C2356E71B0E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3596467E-00E0-4519-8FD6-11F3A4805E8D}
EndGlobalSection
EndGlobal
- The project just have the
Home
Controller.
Controllers/HomeController.cs
using System.Threading.Tasks;
using CarvedRock.Web.Clients;
using CarvedRock.Web.HttpClients;
using CarvedRock.Web.Models;
using Microsoft.AspNetCore.Mvc;
namespace CarvedRock.Web.Controllers
{
public class HomeController: Controller
{
private readonly ProductHttpClient _httpClient;
private readonly ProductGraphClient _productGraphClient;
public HomeController(ProductHttpClient httpClient,
ProductGraphClient productGraphClient)
{
_httpClient = httpClient;
_productGraphClient = productGraphClient;
}
public async Task<IActionResult> Index()
{
var responseModel = await _httpClient.GetProducts();
responseModel.ThrowErrors();
return View(responseModel.Data.Products);
}
public async Task<IActionResult> ProductDetail(int productId)
{
var product = await _productGraphClient.GetProduct(productId);
return View(product);
}
public IActionResult AddReview(int productId)
{
return View(new ProductReviewModel {ProductId = productId});
}
[HttpPost]
public async Task<IActionResult> AddReview(ProductReviewInputModel reviewModel)
{
await _productGraphClient.AddReview(reviewModel);
return RedirectToAction("ProductDetail", new {productId = reviewModel.ProductId});
}
}
}
- The
ProductHttpClient
class has been created to manage the client request from theIndex
action. TheGetProducts
method makes aGet request
using HttpClient class using aGrpahQL query
and usingJsonConvert
to deserialize the response using theProductContainer
model.
Models/ProductModel.cs
using System;
using System.Collections.Generic;
namespace CarvedRock.Web.Models
{
public class ProductModel
{
public int Id { get; set; }
public string Name { get; set; }
public ProductTypeEnum Type { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
public int Rating { get; set; }
public DateTimeOffset IntroducedAt { get; set; }
public string PhotoFileName { get; set; }
public List<ProductReviewModel> Reviews { get; set; }
}
}
Models/ErrorModel.cs
namespace CarvedRock.Web.Models
{
public class ErrorModel
{
public string Message { get; set; }
public string Code { get; set; }
}
}
- Apart from the
ProductsContainer
class, theModels/Response.cs
document has a genericReponse
class with theThrowErrors
method used to manage the response errors. It is called from theActions
on theHome
controllers.
Models/Response.cs
using System.Collections.Generic;
using System.Linq;
namespace CarvedRock.Web.Models
{
public class Response<T>
{
public T Data { get; set; }
public List<ErrorModel> Errors { get; set; }
public void ThrowErrors()
{
if (Errors != null && Errors.Any())
throw new GraphQlException(
$"Message: {Errors[0].Message} Code: {Errors[0].Code}");
}
}
public class ProductsContainer
{
public List<ProductModel> Products { get; set; }
}
}
Clients/ProductHttpClient.cs
using System.Net.Http;
using System.Threading.Tasks;
using CarvedRock.Web.Models;
using Newtonsoft.Json;
namespace CarvedRock.Web.HttpClients
{
public class ProductHttpClient
{
private readonly HttpClient _httpClient;
public ProductHttpClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<Response<ProductsContainer>> GetProducts()
{
var response = await _httpClient.GetAsync(@"?query=
{ products
{ id name price rating photoFileName }
}");
var stringResult = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<Response<ProductsContainer>>(stringResult);
}
}
}
- The
HttpClient
is configured in theStartup.cs
document setting theurl
using theappsettings.json
document with theUrl
of theApi GraphQL endpoint
.
appsettings.json
{
"CarvedRockApiUri": "https://localhost:5001/graphql"
}
Startup.cs
using System;
using CarvedRock.Web.Clients;
using CarvedRock.Web.HttpClients;
using GraphQL.Client;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace CarvedRock.Web
{
public class Startup
{
private readonly IConfiguration _config;
public Startup(IConfiguration config)
{
_config = config;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddSingleton(t => new GraphQLClient(_config["CarvedRockApiUri"]));
services.AddSingleton<ProductGraphClient>();
services.AddHttpClient<ProductHttpClient>(o => o.BaseAddress = new Uri(_config["CarvedRockApiUri"]));
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
}
}
4.9 Using GraphQL Client for .NET
- In order to use parameters, fragments, directives, generic errors we have to use the
GraphQL.Client Nuget
package.
CarvedRock.Web.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="GraphQL.Client" Version="1.0.3" />
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.1.2" PrivateAssets="All" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.1.1" />
</ItemGroup>
</Project>
- We have the
ProductGraphClient
class created to manage thequeries
with all the special attributes.
Clients/ProductGraphClient.cs
using System.Threading.Tasks;
using CarvedRock.Web.Models;
using GraphQL.Client;
using GraphQL.Common.Request;
using Newtonsoft.Json;
namespace CarvedRock.Web.Clients
{
public class ProductGraphClient
{
private readonly GraphQLClient _client;
public ProductGraphClient(GraphQLClient client)
{
_client = client;
}
public async Task<ProductModel> GetProduct(int id)
{
var query = new GraphQLRequest
{
Query = @"
query productQuery($productId: ID!)
{ product(id: $productId)
{ id name price rating photoFileName description stock introducedAt
reviews { title review }
}
}",
Variables = new {productId = id}
};
var response = await _client.PostAsync(query);
return response.GetDataFieldAs<ProductModel>("product");
}
public async Task<ProductReviewModel> AddReview(ProductReviewInputModel review)
{
var query = new GraphQLRequest
{
Query = @"
mutation($review: reviewInput!)
{
createReview(review: $review)
{
id
}
}",
Variables = new { review }
};
var response = await _client.PostAsync(query);
return response.GetDataFieldAs<ProductReviewModel>("createReview");
}
}
}
- The
GraphQL client
amnd the class created must be also registered in theStartup.cs
class.
Startup.cs
.
.
.
services.AddSingleton(t => new GraphQLClient(_config["CarvedRockApiUri"]));
services.AddSingleton<ProductGraphClient>();
.
.
.
- The
Home
controller has theProductDetail
action that uses theGraphQL client
.
Controllers/HomeController.cs
.
.
.
public async Task<IActionResult> ProductDetail(int productId)
{
var product = await _productGraphClient.GetProduct(productId);
return View(product);
}
.
.
.
4.10 Using Apollo Client for JavaScript
- The
CarvedRock.Api/GraphQL/Types/CarvedRockQuery.cs
document will be updated to accept reviews for a product.
CarvedRock.Api/GraphQL/Types/CarvedRockQuery.cs
using CarvedRock.Api.GraphQL.Types;
using CarvedRock.Api.Repositories;
using GraphQL.Types;
namespace CarvedRock.Api.GraphQL
{
public class CarvedRockQuery: ObjectGraphType
{
public CarvedRockQuery(ProductRepository productRepository, ProductReviewRepository reviewRepository)
{
Field<ListGraphType<ProductType>>(
"products",
resolve: context => productRepository.GetAll()
);
Field<ProductType>(
"product",
arguments: new QueryArguments(new QueryArgument<NonNullGraphType<IdGraphType>> {Name = "id"}),
resolve: context =>
{
var id = context.GetArgument<int>("id");
return productRepository.GetOne(id);
}
);
Field<ListGraphType<ProductReviewType>>(
"reviews",
arguments: new QueryArguments(new QueryArgument<NonNullGraphType<IdGraphType>> {Name = "productId"}),
resolve: context =>
{
var id = context.GetArgument<int>("productId");
return reviewRepository.GetForProduct(id);
});
}
}
}
- In the
Views/Home/ProductDetail.cshtml
will be included theApollo
client.
Views/Home/ProductDetail.cshtml
@model CarvedRock.Web.Models.ProductModel
<div class="row">
<div class="col-3">
<img height="150" src="/Images/@Model.PhotoFileName" alt="Product image" />
</div>
<div class="col-9">
<div class="row">
<div class="col-12">
<h3>@Model.Name</h3>
</div>
</div>
<div class="row mb-3">
<div class="col-12">
@Model.Description
</div>
</div>
<div class="row mb-4">
<div class="col-3">
In store since: @Model.IntroducedAt.Year
</div>
<div class="col-3">
Stock: @Model.Stock
</div>
<div class="col-3">
Stars: @Model.Rating
</div>
<div class="col-3">
Price: $@Model.Price
</div>
</div>
<h6>Reviews:</h6>
<ul></ul>
@*@foreach (var review in Model.Reviews)
{
<div class="row">
<div class="col-12"><h5>@review.Title</h5></div>
</div>
<div class="row mb-2">
<div class="col-12">@review.Review</div>
</div>
}*@
<div id="reviews"></div>
<ul></ul>
<a asp-action="AddReview" asp-route-productId="@Model.Id">Add a review</a>
</div>
</div>
<script src="https://unpkg.com/apollo-client-browser@1.9.0"></script>
<script src="~/reviews.js"></script>
<script>
renderReviews(@Model.Id);
</script>
- The
wwwroot/reviews.js
document well be created to make a call to thereviewsForProducts query
from JavaScript.
wwwroot/reviews.js
const apolloClient = new Apollo.lib.ApolloClient({
networkInterface: Apollo.lib.createNetworkInterface({
uri: "https://localhost:5001/graphql"
})
});
function renderReviews(productId) {
const query = Apollo.gql`
query reviewsForProducts($productId: ID!)
{
reviews(productId: $productId) {
title
review
}
}
`;
apolloClient
.query({
query: query,
variables: { productId: productId }
})
.then(result => {
const div = document.getElementById("reviews");
result.data.reviews.forEach(review => {
div.innerHTML += `
<div class="row">
<div class="col-12"><h5>${review.title}</h5></div>
</div>
<div class="row mb-2">
<div class="col-12">${review.review}</div>
</div>`;
});
});
}
- In order to be able to call it from JavaScript we will need to configure Cors in the
Api StartUp.cs
document.
StartUp.cs
using CarvedRock.Api.Data;
using CarvedRock.Api.GraphQL;
using CarvedRock.Api.Repositories;
using GraphQL;
using GraphQL.Server;
using GraphQL.Server.Ui.Playground;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace CarvedRock.Api
{
public class Startup
{
private readonly IConfiguration _config;
private readonly IHostingEnvironment _env;
public Startup(IConfiguration config, IHostingEnvironment env)
{
_config = config;
_env = env;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<CarvedRockDbContext>(options =>
options.UseSqlServer(_config["ConnectionStrings:CarvedRock"]));
services.AddScoped<ProductRepository>();
services.AddScoped<ProductReviewRepository>();
services.AddScoped<IDependencyResolver>(s => new FuncDependencyResolver(s.GetRequiredService));
services.AddScoped<CarvedRockSchema>();
services.AddSingleton<ReviewMessageService>();
services.AddGraphQL(o => { o.ExposeExceptions = _env.IsDevelopment(); })
.AddGraphTypes(ServiceLifetime.Scoped).AddUserContextBuilder(httpContext => httpContext.User)
.AddDataLoader()
.AddWebSockets();
services.AddCors();
}
public void Configure(IApplicationBuilder app, CarvedRockDbContext dbContext)
{
app.UseCors(builder =>
builder.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod());
app.UseWebSockets();
app.UseGraphQLWebSockets<CarvedRockSchema>("/graphql");
app.UseGraphQL<CarvedRockSchema>();
app.UseGraphQLPlayground(new GraphQLPlaygroundOptions());
dbContext.Seed();
}
}
}
4.11 Summary
- In order to test if the solution works we have to run the
Api
in one terminal.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/GraphQL/building-graphql-apis-aspdotnet-core/CarvedRock.Api
$ dotnet run
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 2.1.4-rtm-31024 initialized 'CarvedRockDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (17ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT CASE
WHEN EXISTS (
SELECT 1
FROM [Products] AS [p])
THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT)
END
Hosting environment: Development
Content root path: C:\Work\Training\Pre\GraphQL\building-graphql-apis-aspdotnet-core\CarvedRock.Api
Now listening on: https://localhost:5001
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 GET https://localhost:5001/graphql?query=%20%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%7B%20products%20%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B%20id%20name%20price%20rating%20photoFileName%20%7D%20%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 2.1.4-rtm-31024 initialized 'CarvedRockDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (27ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Description], [p].[IntroducedAt], [p].[Name], [p].[PhotoFileName], [p].[Price], [p].[Rating], [p].[Stock], [p].[Type]
FROM [Products] AS [p]
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 1573.4738ms 200 application/json
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 POST https://localhost:5001/graphql application/json; charset=utf-8 351
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 2.1.4-rtm-31024 initialized 'CarvedRockDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (145ms) [Parameters=[@__id_0='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
SELECT TOP(2) [p].[Id], [p].[Description], [p].[IntroducedAt], [p].[Name], [p].[PhotoFileName], [p].[Price], [p].[Rating], [p].[Stock], [p].[Type]
FROM [Products] AS [p]
WHERE [p].[Id] = @__id_0
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (29ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [pr].[Id], [pr].[ProductId], [pr].[Review], [pr].[Title]
FROM [ProductReviews] AS [pr]
WHERE [pr].[ProductId] IN (1)
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 619.8238ms 200 application/json
- We need to run the
Web
in another terminal
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/GraphQL/building-graphql-apis-aspdotnet-core/CarvedRock.Web
$ dotnet run
info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[0]
User profile is available. Using 'C:\Users\juan.pablo.perez\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and Windows DPAPI to encrypt keys at rest.
Hosting environment: Development
Content root path: C:\Work\Training\Pre\GraphQL\building-graphql-apis-aspdotnet-core\CarvedRock.Web
Now listening on: https://localhost:5002
Application started. Press Ctrl+C to shut down.
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 GET https://localhost:5002/
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
Route matched with {action = "Index", controller = "Home"}. Executing action CarvedRock.Web.Controllers.HomeController.Index (CarvedRock.Web)
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
Executing action method CarvedRock.Web.Controllers.HomeController.Index (CarvedRock.Web) - Validation state: Valid
info: System.Net.Http.HttpClient.ProductHttpClient.LogicalHandler[100]
Start processing HTTP request GET https://localhost:5001/graphql?query=
{ products
{ id name price rating photoFileName }
}
info: System.Net.Http.HttpClient.ProductHttpClient.ClientHandler[100]
Sending HTTP request GET https://localhost:5001/graphql?query=
{ products
{ id name price rating photoFileName }
}
info: System.Net.Http.HttpClient.ProductHttpClient.ClientHandler[101]
Received HTTP response after 2169.399ms - OK
info: System.Net.Http.HttpClient.ProductHttpClient.LogicalHandler[101]
End processing HTTP request after 2176.8739ms - OK
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
Executed action method CarvedRock.Web.Controllers.HomeController.Index (CarvedRock.Web), returned result Microsoft.AspNetCore.Mvc.ViewResult in 2375.792ms.
info: Microsoft.AspNetCore.Mvc.ViewFeatures.ViewResultExecutor[1]
Executing ViewResult, running view Index.
info: Microsoft.AspNetCore.Mvc.ViewFeatures.ViewResultExecutor[4]
Executed ViewResult - view Index executed in 286.2657ms.
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
Executed action CarvedRock.Web.Controllers.HomeController.Index (CarvedRock.Web) in 2752.2023ms
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 GET https://localhost:5002/Images/carved-rock-logo.png
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 GET https://localhost:5002/bootstrap.min.css
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 GET https://localhost:5002/Images/shutterstock_66842440.jpg
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 3348.038ms 200 text/html; charset=utf-8
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 GET https://localhost:5002/carvedrock.css
info: Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware[2]
Sending file. Request path: '/Images/carved-rock-logo.png'. Physical path: 'C:\Work\Training\Pre\GraphQL\building-graphql-apis-aspdotnet-core\CarvedRock.Web\wwwroot\Images\carved-rock-logo.png'
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 47.7353ms 200 image/png
info: Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware[2]
Sending file. Request path: '/carvedrock.css'. Physical path: 'C:\Work\Training\Pre\GraphQL\building-graphql-apis-aspdotnet-core\CarvedRock.Web\wwwroot\carvedrock.css'
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 33.9162ms 200 text/css
info: Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware[2]
Sending file. Request path: '/bootstrap.min.css'. Physical path: 'C:\Work\Training\Pre\GraphQL\building-graphql-apis-aspdotnet-core\CarvedRock.Web\wwwroot\bootstrap.min.css'
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 77.1757ms 200 text/css
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 GET https://localhost:5002/Images/shutterstock_222721876.jpg
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 GET https://localhost:5002/Images/shutterstock_6170527.jpg
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 GET https://localhost:5002/Images/shutterstock_48040747.jpg
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 GET https://localhost:5002/Images/shutterstock_441989509.jpg
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 GET https://localhost:5002/Images/shutterstock_495259978.jpg
info: Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware[2]
Sending file. Request path: '/Images/shutterstock_48040747.jpg'. Physical path: 'C:\Work\Training\Pre\GraphQL\building-graphql-apis-aspdotnet-core\CarvedRock.Web\wwwroot\Images\shutterstock_48040747.jpg'
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 58.3366ms 200 image/jpeg
info: Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware[2]
Sending file. Request path: '/Images/shutterstock_441989509.jpg'. Physical path: 'C:\Work\Training\Pre\GraphQL\building-graphql-apis-aspdotnet-core\CarvedRock.Web\wwwroot\Images\shutterstock_441989509.jpg'
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 92.3036ms 200 image/jpeg
info: Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware[2]
Sending file. Request path: '/Images/shutterstock_66842440.jpg'. Physical path: 'C:\Work\Training\Pre\GraphQL\building-graphql-apis-aspdotnet-core\CarvedRock.Web\wwwroot\Images\shutterstock_66842440.jpg'
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 3057.0157ms 200 image/jpeg
info: Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware[2]
Sending file. Request path: '/Images/shutterstock_6170527.jpg'. Physical path: 'C:\Work\Training\Pre\GraphQL\building-graphql-apis-aspdotnet-core\CarvedRock.Web\wwwroot\Images\shutterstock_6170527.jpg'
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 1806.8826ms 200 image/jpeg
info: Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware[2]
Sending file. Request path: '/Images/shutterstock_222721876.jpg'. Physical path: 'C:\Work\Training\Pre\GraphQL\building-graphql-apis-aspdotnet-core\CarvedRock.Web\wwwroot\Images\shutterstock_222721876.jpg'
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 1882.8486ms 200 image/jpeg
info: Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware[2]
Sending file. Request path: '/Images/shutterstock_495259978.jpg'. Physical path: 'C:\Work\Training\Pre\GraphQL\building-graphql-apis-aspdotnet-core\CarvedRock.Web\wwwroot\Images\shutterstock_495259978.jpg'
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 1824.2995ms 200 image/jpeg
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 GET https://localhost:5002/favicon.ico
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 2.2598ms 404
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 GET https://localhost:5002/Home/ProductDetail?productId=1
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
Route matched with {action = "ProductDetail", controller = "Home"}. Executing action CarvedRock.Web.Controllers.HomeController.ProductDetail (CarvedRock.Web)
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
Executing action method CarvedRock.Web.Controllers.HomeController.ProductDetail (CarvedRock.Web) with arguments (1) - Validation state: Valid
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
Executed action method CarvedRock.Web.Controllers.HomeController.ProductDetail (CarvedRock.Web), returned result Microsoft.AspNetCore.Mvc.ViewResult in
933.7869ms.
info: Microsoft.AspNetCore.Mvc.ViewFeatures.ViewResultExecutor[1]
Executing ViewResult, running view ProductDetail.
info: Microsoft.AspNetCore.Mvc.ViewFeatures.ViewResultExecutor[4]
Executed ViewResult - view ProductDetail executed in 23.6771ms.
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
Executed action CarvedRock.Web.Controllers.HomeController.ProductDetail (CarvedRock.Web) in 1012.1305ms
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 1054.1124ms 200 text/html; charset=utf-8
5. Mutating Data with GraphQL
5.1 Introduction
5.2 Enhancing the Schema
5.3 The Input Type
- We need to create the
GraphQL/Types/ProductReviewInputType.cs
document where thereviewInput
input is created. TheProductReviewInputType
derived fromInputObjectGraphType
.
GraphQL/Types/ProductReviewInputType.cs
using GraphQL.Types;
namespace CarvedRock.Api.GraphQL.Types
{
public class ProductReviewInputType: InputObjectGraphType
{
public ProductReviewInputType()
{
Name = "reviewInput";
Field<NonNullGraphType<StringGraphType>>("title");
Field<StringGraphType>("review");
Field<NonNullGraphType<IntGraphType>>("productId");
}
}
}
5.4 The Mutation
- We need to create
GraphQL/CarvedRockMutation.cs
document where theCarvedRockMutation
mutation is created. It derives from the sameObjectGraphType
as the queries do.
GraphQL/CarvedRockMutation.cs
using CarvedRock.Api.Data.Entities;
using CarvedRock.Api.GraphQL.Types;
using CarvedRock.Api.Repositories;
using GraphQL.Types;
namespace CarvedRock.Api.GraphQL
{
public class CarvedRockMutation : ObjectGraphType
{
public CarvedRockMutation(ProductReviewRepository reviewRepository)
{
FieldAsync<ProductReviewType>(
"createReview",
arguments: new QueryArguments(
new QueryArgument<NonNullGraphType<ProductReviewInputType>> {Name = "review"}),
resolve: async context =>
{
var review = context.GetArgument<ProductReview>("review");
return await context.TryAsyncResolve(
async c => await reviewRepository.AddReview(review));
});
}
}
}
- We need to update the
Repositories/ProductReviewRepository.cs
to include theAddReview
method.
Repositories/ProductReviewRepository.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CarvedRock.Api.Data;
using CarvedRock.Api.Data.Entities;
using Microsoft.EntityFrameworkCore;
namespace CarvedRock.Api.Repositories
{
public class ProductReviewRepository
{
private readonly CarvedRockDbContext _dbContext;
public ProductReviewRepository(CarvedRockDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<IEnumerable<ProductReview>> GetForProduct(int productId)
{
return await _dbContext.ProductReviews.Where(pr => pr.ProductId == productId).ToListAsync();
}
public async Task<ILookup<int, ProductReview>> GetForProducts(IEnumerable<int> productIds)
{
var reviews = await _dbContext.ProductReviews.Where(pr => productIds.Contains(pr.ProductId)).ToListAsync();
return reviews.ToLookup(r => r.ProductId);
}
public async Task<ProductReview> AddReview(ProductReview review)
{
_dbContext.ProductReviews.Add(review);
await _dbContext.SaveChangesAsync();
return review;
}
}
}
5.5 Adding a Mutation to the Schema
- We don't need to do any extra configuration in the
Startup.cs
document because we are already including all GraphQl types withAddGraphTypes
.
Startup.cs
using CarvedRock.Api.Data;
using CarvedRock.Api.GraphQL;
using CarvedRock.Api.Repositories;
using GraphQL;
using GraphQL.Server;
using GraphQL.Server.Ui.Playground;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace CarvedRock.Api
{
public class Startup
{
private readonly IConfiguration _config;
private readonly IHostingEnvironment _env;
public Startup(IConfiguration config, IHostingEnvironment env)
{
_config = config;
_env = env;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<CarvedRockDbContext>(options =>
options.UseSqlServer(_config["ConnectionStrings:CarvedRock"]));
services.AddScoped<ProductRepository>();
services.AddScoped<ProductReviewRepository>();
services.AddScoped<IDependencyResolver>(s => new FuncDependencyResolver(s.GetRequiredService));
services.AddScoped<CarvedRockSchema>();
services.AddGraphQL(o => { o.ExposeExceptions = false; })
.AddGraphTypes(ServiceLifetime.Scoped)
.AddDataLoader();
}
public void Configure(IApplicationBuilder app, CarvedRockDbContext dbContext)
{
app.UseGraphQL<CarvedRockSchema>();
app.UseGraphQLPlayground(new GraphQLPlaygroundOptions());
dbContext.Seed();
}
}
}
- We have to modify the
GraphQL/CarvedRockSchema.cs
document to icnlude the Mutation.
GraphQL/CarvedRockSchema.cs
using GraphQL;
using GraphQL.Types;
namespace CarvedRock.Api.GraphQL
{
public class CarvedRockSchema: Schema
{
public CarvedRockSchema(IDependencyResolver resolver): base(resolver)
{
Query = resolver.Resolve<CarvedRockQuery>();
Mutation = resolver.Resolve<CarvedRockMutation>();
}
}
}
5.6 Writing a Query That Uses a Mutation
Request
mutation($review: reviewInput!) {
createReview(review: $review) {
id
title
}
}
Variable
{
"review": {
"title": "This is awesome!",
"productId": 1
}
}
Response
{
"data": {
"createReview": {
"id": 10,
"title": "This is awesome!"
}
}
}
- If we omit the
id
an error is thrown
Variable
{
"review": {
"title": "This is awesome!"
}
}
Response
{
"errors": [
{
"message": "Variable '$review.productId' is invalid. Received a null input for a non-null field.",
"extensions": {
"code": "INVALID_VALUE"
}
}
]
}
5.7 Mutations in Clients
- The
Clients/ProductGraphClient.cs
document must be modified to include theAddReview
method used to make a call to the newcreateReview
mutation. We need theModels/ProductReviewInputModel.cs
document with theProductReviewInputModel
used to populated the request mutation properties. We also need theModels/ProductReviewModel.cs
used to populated the response mutation properties.
Models/ProductReviewInputModel.cs
namespace CarvedRock.Web.Models
{
public class ProductReviewInputModel
{
public int ProductId { get; set; }
public string Title { get; set; }
public string Review { get; set; }
}
}
Models/ProductReviewModel.cs
namespace CarvedRock.Web.Models
{
public class ProductReviewModel
{
public int Id { get; set; }
public int ProductId { get; set; }
public string Title { get; set; }
public string Review { get; set; }
}
}
Clients/ProductGraphClient.cs
using System.Threading.Tasks;
using CarvedRock.Web.Models;
using GraphQL.Client;
using GraphQL.Common.Request;
using Newtonsoft.Json;
namespace CarvedRock.Web.Clients
{
public class ProductGraphClient
{
private readonly GraphQLClient _client;
public ProductGraphClient(GraphQLClient client)
{
_client = client;
}
public async Task<ProductModel> GetProduct(int id)
{
var query = new GraphQLRequest
{
Query = @"
query productQuery($productId: ID!)
{ product(id: $productId)
{ id name price rating photoFileName description stock introducedAt
reviews { title review }
}
}",
Variables = new {productId = id}
};
var response = await _client.PostAsync(query);
return response.GetDataFieldAs<ProductModel>("product");
}
public async Task<ProductReviewModel> AddReview(ProductReviewInputModel review)
{
var query = new GraphQLRequest
{
Query = @"
mutation($review: reviewInput!)
{
createReview(review: $review)
{
id
}
}",
Variables = new { review }
};
var response = await _client.PostAsync(query);
return response.GetDataFieldAs<ProductReviewModel>("createReview");
}
}
}
- The
Home
controller has been updated to include the newAddReview
action.
Controllers/HomeController.cs
using System.Threading.Tasks;
using CarvedRock.Web.Clients;
using CarvedRock.Web.HttpClients;
using CarvedRock.Web.Models;
using Microsoft.AspNetCore.Mvc;
namespace CarvedRock.Web.Controllers
{
public class HomeController: Controller
{
private readonly ProductHttpClient _httpClient;
private readonly ProductGraphClient _productGraphClient;
public HomeController(ProductHttpClient httpClient,
ProductGraphClient productGraphClient)
{
_httpClient = httpClient;
_productGraphClient = productGraphClient;
}
public async Task<IActionResult> Index()
{
var responseModel = await _httpClient.GetProducts();
responseModel.ThrowErrors();
return View(responseModel.Data.Products);
}
public async Task<IActionResult> ProductDetail(int productId)
{
var product = await _productGraphClient.GetProduct(productId);
return View(product);
}
public IActionResult AddReview(int productId)
{
return View(new ProductReviewModel {ProductId = productId});
}
[HttpPost]
public async Task<IActionResult> AddReview(ProductReviewInputModel reviewModel)
{
await _productGraphClient.AddReview(reviewModel);
return RedirectToAction("ProductDetail", new {productId = reviewModel.ProductId});
}
}
}
- The
Views/Home/ProductDetail.cshtml
document has been updated to include a link to the newAddReview
action.
Views/Home/ProductDetail.cshtml
@model CarvedRock.Web.Models.ProductModel
<div class="row">
<div class="col-3">
<img height="150" src="/Images/@Model.PhotoFileName" alt="Product image" />
</div>
<div class="col-9">
<div class="row">
<div class="col-12">
<h3>@Model.Name</h3>
</div>
</div>
<div class="row mb-3">
<div class="col-12">
@Model.Description
</div>
</div>
<div class="row mb-4">
<div class="col-3">
In store since: @Model.IntroducedAt.Year
</div>
<div class="col-3">
Stock: @Model.Stock
</div>
<div class="col-3">
Stars: @Model.Rating
</div>
<div class="col-3">
Price: $@Model.Price
</div>
</div>
<h6>Reviews:</h6>
<ul></ul>
@foreach (var review in Model.Reviews)
{
<div class="row">
<div class="col-12"><h5>@review.Title</h5></div>
</div>
<div class="row mb-2">
<div class="col-12">@review.Review</div>
</div>
}
<ul></ul>
<a asp-action="AddReview" asp-route-productId="@Model.Id">Add a review</a>
</div>
</div>
- We can test now how the
Web
works.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/GraphQL/building-graphql-apis-aspdotnet-core/CarvedRock.Api (master)
$ dotnet run
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 2.1.4-rtm-31024 initialized 'CarvedRockDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (16ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT CASE
WHEN EXISTS (
SELECT 1
FROM [Products] AS [p])
THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT)
END
Hosting environment: Development
Content root path: C:\Work\Training\Pre\GraphQL\building-graphql-apis-aspdotnet-core\CarvedRock.Api
Now listening on: https://localhost:5001
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/GraphQL/building-graphql-apis-aspdotnet-core/CarvedRock.Web (master)
$ dotnet run
info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[0]
User profile is available. Using 'C:\Users\juan.pablo.perez\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and Windows DPAPI to encrypt keys at rest.
Hosting environment: Development
Content root path: C:\Work\Training\Pre\GraphQL\building-graphql-apis-aspdotnet-core\CarvedRock.Web
Now listening on: https://localhost:5002
Application started. Press Ctrl+C to shut down.
5.8 Summary
6. Working with Subscriptions
6.1 Introduction
6.2 Subscriptions Explained
6.3 Adding a Subscription to the Schema
- we need to add the new
GraphQL/Messaging
folder with theReviewAddedMessage.cs
andReviewMessageService.cs
documents.
GraphQL/Messaging/ReviewAddedMessage.cs
namespace CarvedRock.Api.GraphQL.Messaging
{
public class ReviewAddedMessage
{
public int ProductId { get; set; }
public string Title { get; set; }
}
}
- The
OnNext
method send the message to all subscribers.
GraphQL/Messaging/ReviewMessageService.cs
using CarvedRock.Api.Data.Entities;
using CarvedRock.Api.GraphQL.Messaging;
using System;
using System.Reactive.Linq;
using System.Reactive.Subjects;
namespace CarvedRock.Api
{
public class ReviewMessageService
{
private readonly ISubject<ReviewAddedMessage> _messageStream = new ReplaySubject<ReviewAddedMessage>(1);
public ReviewAddedMessage AddReviewAddedMessage(ProductReview review)
{
var message = new ReviewAddedMessage
{
ProductId = review.ProductId,
Title = review.Title
};
_messageStream.OnNext(message);
return message;
}
public IObservable<ReviewAddedMessage> GetMessages()
{
return _messageStream.AsObservable();
}
}
}
- Thew new
GraphQL/CarvedRockSubscription.cs
along with theGraphQL/Types/ReviewAddedMessageType.cs
documents have to be created.
GraphQL/Types/ReviewAddedMessageType.cs
using CarvedRock.Api.GraphQL.Messaging;
using GraphQL.Types;
namespace CarvedRock.Api.GraphQL.Types
{
public class ReviewAddedMessageType: ObjectGraphType<ReviewAddedMessage>
{
public ReviewAddedMessageType()
{
Field(t => t.ProductId);
Field(t => t.Title);
}
}
}
GraphQL/CarvedRockSubscription.cs
using CarvedRock.Api.GraphQL.Messaging;
using CarvedRock.Api.GraphQL.Types;
using GraphQL.Resolvers;
using GraphQL.Types;
namespace CarvedRock.Api.GraphQL
{
public class CarvedRockSubscription: ObjectGraphType
{
public CarvedRockSubscription(ReviewMessageService messageService)
{
Name = "Subscription";
AddField(new EventStreamFieldType
{
Name = "reviewAdded",
Type = typeof(ReviewAddedMessageType),
Resolver = new FuncFieldResolver<ReviewAddedMessage>(c => c.Source as ReviewAddedMessage),
Subscriber = new EventStreamResolver<ReviewAddedMessage>(c => messageService.GetMessages())
});
}
}
}
- We finally need to add the subscription to the schema modifying the
GraphQL/CarvedRockSchema.cs
document.
GraphQL/CarvedRockSchema.cs
using GraphQL;
using GraphQL.Types;
namespace CarvedRock.Api.GraphQL
{
public class CarvedRockSchema : Schema
{
public CarvedRockSchema(IDependencyResolver resolver) : base(resolver)
{
Query = resolver.Resolve<CarvedRockQuery>();
Mutation = resolver.Resolve<CarvedRockMutation>();
Subscription = resolver.Resolve<CarvedRockSubscription>();
}
}
}
6.4 Configuring ASP.NET Core
- We need to add the
GraphQL.Server.Transports.WebSockets
Nuget package.
GraphQL.Server.Transports.WebSockets
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="GraphQL" Version="2.3.0" />
<PackageReference Include="GraphQL.Server.Transports.AspNetCore" Version="3.2.0" />
<PackageReference Include="GraphQL.Server.Transports.WebSockets" Version="3.2.0" />
<PackageReference Include="GraphQL.Server.Ui.Playground" Version="3.2.0" />
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.1.2" PrivateAssets="All" />
</ItemGroup>
</Project>
- The
ReviewMessageService
class must be registered in theStartup.cs
document asSingleton
becase we need it keeps alive. Also, the the use ofWeb Sockets
must be added as well.
Startup.cs
using CarvedRock.Api.Data;
using CarvedRock.Api.GraphQL;
using CarvedRock.Api.Repositories;
using GraphQL;
using GraphQL.Server;
using GraphQL.Server.Ui.Playground;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace CarvedRock.Api
{
public class Startup
{
private readonly IConfiguration _config;
private readonly IHostingEnvironment _env;
public Startup(IConfiguration config, IHostingEnvironment env)
{
_config = config;
_env = env;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<CarvedRockDbContext>(options =>
options.UseSqlServer(_config["ConnectionStrings:CarvedRock"]));
services.AddScoped<ProductRepository>();
services.AddScoped<ProductReviewRepository>();
services.AddScoped<IDependencyResolver>(s => new FuncDependencyResolver(s.GetRequiredService));
services.AddScoped<CarvedRockSchema>();
services.AddSingleton<ReviewMessageService>();
services.AddGraphQL(o => { o.ExposeExceptions = _env.IsDevelopment(); })
.AddGraphTypes(ServiceLifetime.Scoped).AddUserContextBuilder(httpContext => httpContext.User)
.AddDataLoader()
.AddWebSockets();
services.AddCors();
}
public void Configure(IApplicationBuilder app, CarvedRockDbContext dbContext)
{
app.UseCors(builder =>
builder.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod());
app.UseWebSockets();
app.UseGraphQLWebSockets<CarvedRockSchema>("/graphql");
app.UseGraphQL<CarvedRockSchema>();
app.UseGraphQLPlayground(new GraphQLPlaygroundOptions());
dbContext.Seed();
}
}
}
- We need to modify the
GrpahQL/CarvedRockMutation.cs
document to add the call to theAddReviewAddedMessage
method when the review is created.
GrpahQL/CarvedRockMutation.cs
using CarvedRock.Api.Data.Entities;
using CarvedRock.Api.GraphQL.Types;
using CarvedRock.Api.Repositories;
using GraphQL.Types;
namespace CarvedRock.Api.GraphQL
{
public class CarvedRockMutation : ObjectGraphType
{
public CarvedRockMutation(ProductReviewRepository reviewRepository, ReviewMessageService messageService)
{
FieldAsync<ProductReviewType>(
"createReview",
arguments: new QueryArguments(
new QueryArgument<NonNullGraphType<ProductReviewInputType>> {Name = "review"}),
resolve: async context =>
{
var review = context.GetArgument<ProductReview>("review");
await reviewRepository.AddReview(review);
messageService.AddReviewAddedMessage(review);
return review;
});
}
}
}
6.5 Subscriptions in Clients
- Run the
API
project to ensure the subscription is working properly.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/GraphQL/building-graphql-apis-aspdotnet-core/CarvedRock.Api (master)
$ dotnet run
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 2.1.4-rtm-31024 initialized 'CarvedRockDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (75ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT CASE
WHEN EXISTS (
SELECT 1
FROM [Products] AS [p])
THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT)
END
Hosting environment: Development
Content root path: C:\Work\Training\Pre\GraphQL\building-graphql-apis-aspdotnet-core\CarvedRock.Api
Now listening on: https://localhost:5001
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.
- We need first to create a subscription query
Request
subscription {
reviewAdded {
productId
title
}
}
- When we run the
Playground UI
it waits inlistening
mode.
- We have to execute the
Web
project to make a revision.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/GraphQL/building-graphql-apis-aspdotnet-core/CarvedRock.Web (master)
$ dotnet run
info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[0]
User profile is available. Using 'C:\Users\juan.pablo.perez\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and Windows DPAPI to encrypt keys at rest.
Hosting environment: Development
Content root path: C:\Work\Training\Pre\GraphQL\building-graphql-apis-aspdotnet-core\CarvedRock.Web
Now listening on: https://localhost:5002
Application started. Press Ctrl+C to shut down.
Response
{
"data": {
"reviewAdded": {
"productId": 2,
"title": "I would never wear them."
}
}
}
- In order to use subscriptions in the client we need a
GraphQL.Client
2.0.0 or newer version
CarvedRock.Web.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="GraphQL.Client" Version="2.0.0-alpha.3" />
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.1.2" PrivateAssets="All" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.1.1" />
</ItemGroup>
</Project>
- We need to modify the
Clients/ProductGraphClient.cs
to subscribe to revision updates.
using System.Threading.Tasks;
using CarvedRock.Web.Models;
using GraphQL.Client;
using GraphQL.Common.Request;
using GraphQL.Common.Response;
using Newtonsoft.Json;
namespace CarvedRock.Web.Clients
{
public class ProductGraphClient
{
private readonly GraphQLClient _client;
public ProductGraphClient(GraphQLClient client)
{
_client = client;
}
public async Task<ProductModel> GetProduct(int id)
{
var query = new GraphQLRequest
{
Query = @"
query productQuery($productId: ID!)
{ product(id: $productId)
{ id name price rating photoFileName description stock introducedAt
reviews { title review }
}
}",
Variables = new {productId = id}
};
var response = await _client.PostAsync(query);
return response.GetDataFieldAs<ProductModel>("product");
}
public async Task AddReview(ProductReviewModel review)
{
var query = new GraphQLRequest
{
Query = @"
mutation($review: reviewInput!)
{
createReview(review: $review)
{
id
}
}",
Variables = new { review }
};
var response = await _client.PostAsync(query);
var reviewReturned = response.GetDataFieldAs<ProductReviewModel>("createReview");
}
public async Task SubscribeToUpdates()
{
var result = await _client.SendSubscribeAsync("subscription { reviewAdded { title productId } }");
result.OnReceive += Receive;
}
private void Receive(GraphQLResponse resp)
{
var review = resp.Data["reviewAdded"];
}
}
}
- We need to modify the
Home
controller to call theSubscribeToUpdates
method created previously.
Controllers/HomeController.cs
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/GraphQL/building-graphql-apis-aspdotnet-core/CarvedRock.Web (master)
$ dotnet run
Clients\ProductGraphClient.cs(15,35): warning CS0618: 'GraphQLClient' is obsolete: 'Use GraphQLHttpClient directly' [C:\Work\Training\Pre\GraphQL\building-graphql-apis-aspdotnet-core\CarvedRock.Web\CarvedRock.Web.csproj]
Clients\ProductGraphClient.cs(13,26): warning CS0618: 'GraphQLClient' is obsolete: 'Use GraphQLHttpClient directly' [C:\Work\Training\Pre\GraphQL\building-graphql-apis-aspdotnet-core\CarvedRock.Web\CarvedRock.Web.csproj]
Clients\ProductGraphClient.cs(57,32): warning CS0618: 'GraphQLHttpClient.SendSubscribeAsync(string, CancellationToken)' is obsolete: 'EXPERIMENTAL API' [C:\Work\Training\Pre\GraphQL\building-graphql-apis-aspdotnet-core\CarvedRock.Web\CarvedRock.Web.csproj]
Startup.cs(23,44): warning CS0618: 'GraphQLClient' is obsolete: 'Use GraphQLHttpClient directly' [C:\Work\Training\Pre\GraphQL\building-graphql-apis-aspdotnet-core\CarvedRock.Web\CarvedRock.Web.csproj]
- We need to modify
CarvedRock.Web/Startup.cs
to useGraphQLHttpClient
instead ofGraphQLClient
CarvedRock.Web/Startup.cs
using System;
using CarvedRock.Web.Clients;
using CarvedRock.Web.HttpClients;
using GraphQL.Client.Http;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace CarvedRock.Web
{
public class Startup
{
private readonly IConfiguration _config;
public Startup(IConfiguration config)
{
_config = config;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddSingleton(t => new GraphQLHttpClient(_config["CarvedRockApiUri"]));
services.AddSingleton<ProductGraphClient>();
services.AddHttpClient<ProductHttpClient>(o => o.BaseAddress = new Uri(_config["CarvedRockApiUri"]));
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
}
}
- We need to modify
CarvedRock.Web/Clients/ProductGraphClient.cs
to useGraphQLHttpClient
instead ofGraphQLClient
CarvedRock.Web/Clients/ProductGraphClient.cs
using System.Threading.Tasks;
using CarvedRock.Web.Models;
using GraphQL.Common.Request;
using GraphQL.Common.Response;
using GraphQL.Client.Http;
namespace CarvedRock.Web.Clients
{
public class ProductGraphClient
{
private readonly GraphQLHttpClient _client;
public ProductGraphClient(GraphQLHttpClient client)
{
_client = client;
}
public async Task<ProductModel> GetProduct(int id)
{
var query = new GraphQLRequest
{
Query = @"
query productQuery($productId: ID!)
{ product(id: $productId)
{ id name price rating photoFileName description stock introducedAt
reviews { title review }
}
}",
Variables = new { productId = id }
};
var response = await _client.SendQueryAsync(query);
return response.GetDataFieldAs<ProductModel>("product");
}
public async Task AddReview(ProductReviewInputModel review)
{
var query = new GraphQLRequest
{
Query = @"
mutation($review: reviewInput!)
{
createReview(review: $review)
{
id
}
}",
Variables = new { review }
};
var response = await _client.SendQueryAsync(query);
if (response != null)
{
var reviewReturned = response.GetDataFieldAs<ProductReviewModel>("createReview");
}
}
public async Task SubscribeToUpdates()
{
var result = await _client.SendSubscribeAsync("subscription { reviewAdded { title productId } }");
result.OnReceive += Receive;
}
private void Receive(GraphQLResponse resp)
{
var review = resp.Data["reviewAdded"];
}
}
}
- We need to modify
CarvedRock.Web/Controllers/HomeController.cs
to useGraphQLHttpClient
instead ofGraphQLClient
CarvedRock.Web/Controllers/HomeController.cs
using System.Threading.Tasks;
using CarvedRock.Web.Clients;
using CarvedRock.Web.HttpClients;
using CarvedRock.Web.Models;
using Microsoft.AspNetCore.Mvc;
namespace CarvedRock.Web.Controllers
{
public class HomeController : Controller
{
private readonly ProductHttpClient _httpClient;
private readonly ProductGraphClient _productGraphClient;
public HomeController(ProductHttpClient httpClient, ProductGraphClient productGraphClient)
{
_httpClient = httpClient;
_productGraphClient = productGraphClient;
}
public async Task<IActionResult> Index()
{
var responseModel = await _httpClient.GetProducts();
responseModel.ThrowErrors();
return View(responseModel.Data.Products);
}
public async Task<IActionResult> ProductDetail(int productId)
{
await _productGraphClient.SubscribeToUpdates();
var product = await _productGraphClient.GetProduct(productId);
return View(product);
}
public IActionResult AddReview(int productId)
{
return View(new ProductReviewModel { ProductId = productId });
}
[HttpPost]
public async Task<IActionResult> AddReview(ProductReviewInputModel reviewModel)
{
await _productGraphClient.AddReview(reviewModel);
return RedirectToAction("ProductDetail", new { productId = reviewModel.ProductId });
}
}
}
- In order to use Apollo we need:
- The
CarvedRock.Api/GraphQL/Types/CarvedRockQuery.cs
document must be updated to accept reviews for a product.
CarvedRock.Api/GraphQL/Types/CarvedRockQuery.cs
using CarvedRock.Api.GraphQL.Types;
using CarvedRock.Api.Repositories;
using GraphQL.Types;
namespace CarvedRock.Api.GraphQL
{
public class CarvedRockQuery : ObjectGraphType
{
public CarvedRockQuery(ProductRepository productRepository, ProductReviewRepository reviewRepository)
{
Field<ListGraphType<ProductType>>(
"products",
resolve: context => productRepository.GetAll()
);
Field<ProductType>(
"product",
arguments: new QueryArguments(new QueryArgument<NonNullGraphType<IdGraphType>>
{ Name = "id" }),
resolve: context =>
{
var id = context.GetArgument<int>("id");
return productRepository.GetOne(id);
}
);
Field<ListGraphType<ProductReviewType>>(
"reviews",
arguments: new QueryArguments(new QueryArgument<NonNullGraphType<IdGraphType>> {Name = "productId"}),
resolve: context =>
{
var id = context.GetArgument<int>("productId");
return reviewRepository.GetForProduct(id);
});
}
}
}
- In the
Views/Home/ProductDetail.cshtml
we need to include theApollo
client.
Views/Home/ProductDetail.cshtml
@model CarvedRock.Web.Models.ProductModel
<div class="row">
<div class="col-3">
<img height="150" src="/Images/@Model.PhotoFileName" alt="Product image" />
</div>
<div class="col-9">
<div class="row">
<div class="col-12">
<h3>@Model.Name</h3>
</div>
</div>
<div class="row mb-3">
<div class="col-12">
@Model.Description
</div>
</div>
<div class="row mb-4">
<div class="col-3">
In store since: @Model.IntroducedAt.Year
</div>
<div class="col-3">
Stock: @Model.Stock
</div>
<div class="col-3">
Stars: @Model.Rating
</div>
<div class="col-3">
Price: $@Model.Price
</div>
</div>
<h6>Reviews:</h6>
<ul></ul>
@*@foreach (var review in Model.Reviews)
{
<div class="row">
<div class="col-12"><h5>@review.Title</h5></div>
</div>
<div class="row mb-2">
<div class="col-12">@review.Review</div>
</div>
}*@
<div id="reviews"></div>
<ul></ul>
<a asp-action="AddReview" asp-route-productId="@Model.Id">Add a review</a>
</div>
</div>
<script src="https://unpkg.com/apollo-client-browser@1.9.0"></script>
<script src="~/reviews.js"></script>
<script>
renderReviews(@Model.Id);
</script>
- The
wwwroot/reviews.js
document must be created to make a call to thereviewsForProducts query
from JavaScript.
wwwroot/reviews.js
const apolloClient = new Apollo.lib.ApolloClient({
networkInterface: Apollo.lib.createNetworkInterface({
uri: "https://localhost:5001/graphql"
})
});
function renderReviews(productId) {
const query = Apollo.gql`
query reviewsForProducts($productId: ID!)
{
reviews(productId: $productId) {
title
review
}
}
`;
apolloClient
.query({
query: query,
variables: { productId: productId }
})
.then(result => {
const div = document.getElementById("reviews");
result.data.reviews.forEach(review => {
div.innerHTML += `
<div class="row">
<div class="col-12"><h5>${review.title}</h5></div>
</div>
<div class="row mb-2">
<div class="col-12">${review.review}</div>
</div>`;
});
});
}
- In order to be able to call it from JavaScript we need to configure Cors in the
Api StartUp.cs
document.
StartUp.cs
using CarvedRock.Api.Data;
using CarvedRock.Api.GraphQL;
using CarvedRock.Api.Repositories;
using GraphQL;
using GraphQL.Server;
using GraphQL.Server.Ui.Playground;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace CarvedRock.Api
{
public class Startup
{
private readonly IConfiguration _config;
private readonly IHostingEnvironment _env;
public Startup(IConfiguration config, IHostingEnvironment env)
{
_config = config;
_env = env;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<CarvedRockDbContext>(options =>
options.UseSqlServer(_config["ConnectionStrings:CarvedRock"]));
services.AddScoped<ProductRepository>();
services.AddScoped<ProductReviewRepository>();
services.AddScoped<IDependencyResolver>(s => new FuncDependencyResolver(s.GetRequiredService));
services.AddScoped<CarvedRockSchema>();
services.AddSingleton<ReviewMessageService>();
services.AddGraphQL(o => { o.ExposeExceptions = _env.IsDevelopment(); })
.AddGraphTypes(ServiceLifetime.Scoped).AddUserContextBuilder(httpContext => httpContext.User)
.AddDataLoader()
.AddWebSockets();
services.AddCors();
}
public void Configure(IApplicationBuilder app, CarvedRockDbContext dbContext)
{
app.UseCors(builder =>
builder.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod());
app.UseWebSockets();
app.UseGraphQLWebSockets<CarvedRockSchema>("/graphql");
app.UseGraphQL<CarvedRockSchema>();
app.UseGraphQLPlayground(new GraphQLPlaygroundOptions());
dbContext.Seed();
}
}
}
- We need to modify the
GraphQL/Types/ProductReviewInputType.cs
document to include the 'id` field.
GraphQL/Types/ProductReviewInputType.cs
using GraphQL.Types;
namespace CarvedRock.Api.GraphQL.Types
{
public class ProductReviewInputType: InputObjectGraphType
{
public ProductReviewInputType()
{
Name = "reviewInput";
Field<IdGraphType>("id");
Field<NonNullGraphType<StringGraphType>>("title");
Field<StringGraphType>("review");
Field<NonNullGraphType<IntGraphType>>("productId");
}
}
}