DEV Community

Cover image for GraphQL For .NET
Douglas Minnaar
Douglas Minnaar

Posted on

GraphQL For .NET

I have recently created a project on Github that demonstrates how to build a GraphQL API through the use of a number of examples. I created the project in order to help serve as a guide for .NET developers that are new to GraphQL and would like to learn how to get started. As a final step to this project, I also demonstrate how to create the required infrastructure in Microsoft Azure to host the GraphQL API.

 


 

Key Takeaways

 

  • Project Overview
  • Learn how to build a .NET 6 GraphQL API from scratch
  • Learn how to define GraphQL queries, and mutations
  • Learn how to provide validation and error handling
  • Add EntityFramework support both in-memory and SQLite database
  • Add support for query projection, filtering, sorting, and paging
  • Provision Azure App Service using Azure CLI, Azure, Powershell, and Azure Bicep

 


 

Project Overview

 

 

All examples are based on a fictitious company called MyView, that provides a service to view games and game ratings. It also allows reviewers to post and delete game reviews. Therefore, in the examples that follow, I demonstrate how to build a GraphQL API that provides the following capabilities:

 

  • List games
  • List games with associated reviews
  • Find a game by id
  • Post a game review
  • Delete a game review

 

The details of the provided examples are as follows:

 

  • 📘 Example 1  
    • Create empty API project
    • Add GraphQL Server support through the use of the ChilliCream Hot Chocolate nuget package
    • Create a games query
    • Create a games mutation
    • Add metadata to support schema documentation

 

  • 📘 Example 2  
    • Add global error handling
    • Add input validation

 

  • 📘 Example 3  
    • Add EntityFramework support with an in-memory database
    • Add support for query filtering, sorting, and paging
    • Add projection support
    • Add query type extensions to allow splitting queries into multiple classes

 

  • 📘 Example 4  
    • Change project to use SQlite instead of an in-memory database

 
Lastly, I provide the instructions on how to provision an Azure App Service and deploy the GraphQL API to it. The details are as follows:

 

  • Provision Azure App Service using Azure CLI
  • Provision Azure App Service using Azure Powershell
  • Provision Azure App Service using Azure Bicep
  • Deploy GraphQL API

 

📘 Find all the infrastructure as code here

 


 

Required Setup and Tools

 

It is recommended that the following tools and frameworks are installed before trying to run the examples:

 

All code in this example is developed with C# 10 using the latest cross-platform .NET 6 framework.

See the .NET 6 SDK official website for details on how to download and setup for your development environment.
 

Find more information on Visual Studio Code with relevant C# and .NET extensions.
 

Everything you need to install and configure your windows, linux, and macos environment
 

Provides Bicep language support
 

jq is a lightweight and flexible command-line JSON processor
 

A package of all the Visual Studio Code extensions that you will need to work with Azure
 

Tools for developing and running commands of the Azure CLI

 


 

Example 1

 

Example 1 demonstrates how to get started in terms of creating and running a GraphQL Server.

 

📘 The full example can be found on Github.

 

Step 1 - Create project

 

 # create empty solution dotnet new sln --name MyView # create basic webapi project using minimal api's dotnet new webapi --no-https --no-openapi --framework net6.0 --use-minimal-apis --name MyView.Api # add webapi project to solution dotnet sln ./MyView.sln add ./MyView.Api 
Enter fullscreen mode Exit fullscreen mode

 

Step 2 - Add GraphQL Capability to Web API

In this section, we turn the web application into a GraphQL API by installing the Chillicream Hot Chocolate GraphQL nuget package called HotChocolate.AspNetCore. This package contains the Hot Chocolate GraphQL query execution engine and query validation.

 # From the /MyView.Api project folder, type the following commands: # Add HotChocolate packages dotnet add package HotChocolate.AspNetCore --version 12.11.1 
Enter fullscreen mode Exit fullscreen mode

 

Step 3 - Add Types

 

We are building a GraphQL API to allow querying and managing game reviews. Therefore, to get started we need to create a Game and GameReview type. A few things to note about the types we create:

 

  • I append the suffix "Dto" to indicate that the type is a Data Transfer Object. I use this to make it explicitly clear as to the intent of the type.
  • The GraphQLName attribute is used to rename the type for public consumption. The types will be exposed as Game and GameReview as opposed to GameDto and GameReviewDto
  • The GraphQLDescription attribute is used to provide a description of the type that is used by the GraphQL server to provide more detailed schema information
  • The types are defined as a record type, but can be declared as classes. I have chosen to use the record type as it allows me to define data contracts that are immutable and support value based equality comparison (should I require it).

 

 [GraphQLDescription("Basic game information")] [GraphQLName("Game")] public sealed record GameDto { public GameDto() { Reviews = new List<GameReviewDto>(); } [GraphQLDescription("A unique game identifier")] public Guid GameId { get; set; } [GraphQLDescription("A brief description of game")] public string Summary { get; set; } = string.Empty; [GraphQLDescription("The name of the game")] public string Title { get; set; } = string.Empty; [GraphQLDescription("The date that game was released")] public DateTime ReleasedOn { get; set; } [GraphQLDescription("A list of game reviews")] public ICollection<GameReviewDto> Reviews { get; set; } = new List<GameReviewDto>(); } // GameReviewDto.cs [GraphQLDescription("Game review information")] [GraphQLName("GameReview")] public sealed record GameReviewDto { [GraphQLDescription("Game identifier")] public Guid GameId { get; set; } [GraphQLDescription("Reviewer identifier")] public Guid ReviewerId { get; set; } [GraphQLDescription("Game rating")] public int Rating { get; set; } [GraphQLDescription("A brief description of game experience")] public string Summary { get; set; } = string.Empty; } 
Enter fullscreen mode Exit fullscreen mode

 

Step 4 - Create First Query

One or more queries must be defined in order to support querying data.

 

For our first example we will create a query to support retrieving of game related (games, reviews) data. A few things to note are as follows:

 

  • At this stage we do not not have a database configured and will instead use in-memory data to demonstrate fetching of data.
  • I use the GraphQLDescription attribute to provide GraphQL schema documentation

 

 // In this example, we use GameData (in-memory list) to provide sample dummy game related data [GraphQLDescription("Query games")] public sealed class GamesQuery { [GraphQLDescription("Get list of games")] public IEnumerable<GameDto> GetGames() => GameData.Games; [GraphQLDescription("Find game by id")] public GameDto? FindGameById(Guid gameId) => GameData.Games.FirstOrDefault(game => game.GameId == gameId); } 
Enter fullscreen mode Exit fullscreen mode

 

Step 5 - Create First Mutation

 

We will add 2 operations. One to create, and one to remove a game review. A few things to note are as follows:

 

  • At this stage we do not not have a database configured and will instead use in-memory data to demonstrate saving of data.
  • The GraphQLDescription attribute is used to provide GraphQL schema documentation

 

 [GraphQLDescription("Manage games")] public sealed class GamesMutation { [GraphQLDescription("Submit a game review")] public GameReviewDto SubmitGameReview(GameReviewDto gameReview) { var game = GameData .Games .FirstOrDefault(game => game.GameId == gameReview.GameId) ?? throw new Exception("Game not found"); var gameReviewFromDb = game.Reviews.FirstOrDefault(review => review.GameId == gameReview.GameId && review.ReviewerId == gameReview.ReviewerId); if (gameReviewFromDb is null) { game.Reviews.Add(gameReview); } else { gameReviewFromDb.Rating = gameReview.Rating; gameReviewFromDb.Summary = gameReview.Summary; } return gameReview; } [GraphQLDescription("Remove a game review")] public GameReviewDto DeleteGameReview(Guid gameId, Guid reviewerId) { var game = GameData .Games .FirstOrDefault(game => game.GameId == gameId) ?? throw new Exception("Game not found"); var gameReviewFromDb = game .Reviews .FirstOrDefault(review => review.GameId == gameId && review.ReviewerId == reviewerId) ?? throw new Exception("Game review not found"); game.Reviews.Remove(gameReviewFromDb); return gameReviewFromDb; } } 
Enter fullscreen mode Exit fullscreen mode

 

Step 6 - Configure API with GraphQL services

 

We need to configure the API to use the ChilliCream Hotchocolate GraphQL services and middleware.

 

 builder .Services .AddGraphQLServer() // Adds a GraphQL server configuration to the DI .AddMutationType<GamesMutation>() // Add GraphQL root mutation type .AddQueryType<GamesQuery>() // Add GraphQL root query type .ModifyRequestOptions(options => { // allow exceptions to be included in response when in development options.IncludeExceptionDetails = builder.Environment.IsDevelopment(); }); 
Enter fullscreen mode Exit fullscreen mode

 

Step 7 - Map GraphQL Endpoint

 

 app.MapGraphQL("/"); // Adds a GraphQL endpoint to the endpoint configurations 
Enter fullscreen mode Exit fullscreen mode

 

Step 8 - Run GraphQL API

 

Run the Web API project by typing the following command:

 

 # From the /MyView.Api project folder, type the following commands: dotnet run 
Enter fullscreen mode Exit fullscreen mode

 

Screenshot

 

Schema Information

 

Selecting the Browse Schema options allows one to view the schema information for queries, mutations, and objects as can be seen by the following screen shots.

 

Objects Schema

 

object-schema

 

Queries Schema

 

query-schema

 

Mutations Schema

 

mutation-schema

 

Write GraphQL Queries

 

 # List all games and associated reviews: query listGames { games { gameId title releasedOn summary reviews { reviewerId rating summary } } } 
Enter fullscreen mode Exit fullscreen mode

 

 # Find a game (with reviews) by game id # Add the following JSON snippet to the variables section: { "gameId": "8f7e254f-a6ce-4f13-a44c-8f102a17f2f5" } # Write query to find a game (with reviews) query findGameById ($gameId: UUID!) { findGameById (gameId: $gameId) { gameId title reviews { reviewerId rating summary } } } 
Enter fullscreen mode Exit fullscreen mode

 

queries

 

Write Mutations

 

 # Submit a game review # Define the following JSON in the 'Variables' section: { "gameReview": { "gameId": "8f7e254f-a6ce-4f13-a44c-8f102a17f2f5", "reviewerId": "025fefe4-e2de-4765-9af3-f5dcd6d47458", "rating": 75, "summary": "Enim quidem enim. Eius aut velit voluptas." }, } # Write a mutation to submit a game review mutation submitGameReview($gameReview: GameReviewInput!) { submitGameReview(gameReview: $gameReview) { gameId reviewerId rating summary } } # Write a mutation to DELETE a game review # Define the following JSON in the 'variables' section: { "gameId": "8f7e254f-a6ce-4f13-a44c-8f102a17f2f5", "reviewerId": "025fefe4-e2de-4765-9af3-f5dcd6d47458" } # Write mutation to delete game review mutation deleteGameReview($gameId: UUID!, $reviewerId: UUID!) { deleteGameReview(gameId: $gameId, reviewerId: $reviewerId) { gameId reviewerId rating summary } } 
Enter fullscreen mode Exit fullscreen mode

 

mutations

 


 

Example 2

Example 2 demonstrates the following concepts:

 

  • Implement input validations (like validating a review that is submitted)
  • Implement global error handling

 

📘 The full example can be found on Github.

 

Implement Input Validation

 

Currently, we have no validation in place when submitting a GameReview. In this section, we are going to provide input validation through the use of the [Fluent Validation] nuget package.

 

Step 1 - Add Fluent Validation packages

 

 dotnet add package FluentValidation.AspNetCore --version 11.1.1 dotnet add package FluentValidation.DependencyInjectionExtensions --version 11.1.0 
Enter fullscreen mode Exit fullscreen mode

Step 2 - Create Validator

 

We need to define a class that will handle validation for the GameReview type. To do this, we create a GameReviewValidator as follows:

 

 public sealed class GameReviewValidator : AbstractValidator<GameReviewDto> { public GameReviewValidator() { RuleFor(e => e.GameId) .Must(gameId => GameData.Games.Any(game => game.GameId == gameId)) .WithMessage(e => $"A game having id '{e.GameId}' does not exist"); RuleFor(e => e.Rating) .LessThanOrEqualTo(100) .GreaterThanOrEqualTo(0); RuleFor(e => e.Summary) .NotNull() .NotEmpty() .MinimumLength(20) .MaximumLength(500); } } 
Enter fullscreen mode Exit fullscreen mode

 

Step 3 - Use GameReviewValidator

 

  • The GameReviewValidator is used in the GamesMutation class to validate the GameReviewDto.

  • We use constructor dependency injection to provide the GameReviewValidator

  • The code _validator.ValidateAndThrow(gameReview); will execute the validation rules defined for GameReviewDto and throw a validation exception if there are any validation failures.

 

 [GraphQLDescription("Manage games")] public sealed class GamesMutation { private readonly AbstractValidator<GameReviewDto> _validator; public GamesMutation(AbstractValidator<GameReviewDto> validator) { _validator = validator ?? throw new ArgumentNullException(nameof(validator)); } [GraphQLDescription("Submit a game review")] public GameReviewDto SubmitGameReview(GameReviewDto gameReview) { // use fluent validator _validator.ValidateAndThrow(gameReview); . . . } } 
Enter fullscreen mode Exit fullscreen mode

 

Step 4 - Add Validation Service

 

 // configure fluent validation for GameReviewDto builder.Services.AddTransient<AbstractValidator<GameReviewDto>, GameReviewValidator>(); 
Enter fullscreen mode Exit fullscreen mode

 

Configure Global Error Handling

 

We are throwing exceptions in a number of areas. Those exceptions will be allowed to bubble all the way to the client if allowed to. The following list highlights the areas where we are throwing exceptions along with the GraphQL response resultiing from the exception:

 

  • Validation  
 public GameReviewDto SubmitGameReview(GameReviewDto gameReview) { // use fluent validator _validator.ValidateAndThrow(gameReview); . . . } 
Enter fullscreen mode Exit fullscreen mode

 

 { "errors": [ { "message": "Unexpected Execution Error", "locations": [ { "line": 2, "column": 3 } ], "path": [ "submitGameReview" ], "extensions": { "message": "Validation failed: \r\n -- Rating: 'Rating' must be greater than or equal to '0'. Severity: Error\r\n -- Summary: 'Summary' must not be empty. Severity: Error\r\n -- Summary: The length of 'Summary' must be at least 20 characters. You entered 0 characters. Severity: Error", "stackTrace": " at FluentValidation.AbstractValidator`..." } } ] } 
Enter fullscreen mode Exit fullscreen mode

 

  • Delete Game  
 public GameReviewDto DeleteGameReview(Guid gameId, Guid reviewerId) { var game = GameData .Games .FirstOrDefault(game => game.GameId == gameId) ?? throw GameNotFoundException.WithGameId(gameId); . . . } 
Enter fullscreen mode Exit fullscreen mode

 

 { "errors": [ { "message": "Unexpected Execution Error", "locations": [ { "line": 11, "column": 3 } ], "path": [ "deleteGameReview" ], "extensions": { "message": "Exception of type 'MyView.Api.Games.GameNotFoundException' was thrown.", "stackTrace": " at MyView.Api.Games.GamesMutation.DeleteGameReview ..." } } ] } 
Enter fullscreen mode Exit fullscreen mode

 

Step 1 - Create Custom Error Filter

 

We create a new class called ServerErrorFilter that inherits from the IErrorFilter interface as follows:

 

 public sealed class ServerErrorFilter : IErrorFilter { private readonly ILogger _logger; private readonly IWebHostEnvironment _environment; public ServerErrorFilter(ILogger logger, IWebHostEnvironment environment) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _environment = environment ?? throw new ArgumentNullException(nameof(environment)); } public IError OnError(IError error) { _logger.LogError(error.Exception, error.Message); if (_environment.IsDevelopment()) return error; return ErrorBuilder .New() .SetMessage("An unexpected server fault occurred") .SetCode(ServerErrorCode.ServerFault) .SetPath(error.Path) .Build(); } } 
Enter fullscreen mode Exit fullscreen mode

 

Step 2 - Add ValidationException Handler

 

 public sealed class ServerErrorFilter : IErrorFilter { . . . public IError OnError(IError error) { if (error.Exception is ValidationException validationException) { _logger.LogError(validationException, "There is a validation error"); var errorBuilder = ErrorBuilder .New() .SetMessage("There is a validation error") .SetCode(ServerErrorCode.BadUserInput) .SetPath(error.Path); foreach (var validationFailure in validationException.Errors) { errorBuilder.SetExtension( $"{ServerErrorCode.BadUserInput}_{validationFailure.PropertyName.ToUpper()}", validationFailure.ErrorMessage); } return errorBuilder.Build(); } . . . } } 
Enter fullscreen mode Exit fullscreen mode

 

Step 3 - Add GameNotFoundException Handler

 

 public sealed class ServerErrorFilter : IErrorFilter { . . . public IError OnError(IError error) { . . . if (error.Exception is GameNotFoundException gameNotFoundException) { _logger.LogError(gameNotFoundException, "Game not found"); return ErrorBuilder .New() .SetMessage($"A game having id '{gameNotFoundException.GameId} could not found") .SetCode(ServerErrorCode.ResourceNotFound) .SetPath(error.Path) .SetExtension($"{ServerErrorCode.ResourceNotFound}_GAME_ID", gameNotFoundException.GameId) .Build(); } . . . } } 
Enter fullscreen mode Exit fullscreen mode

 

Step 4 - Add GameReviewNotFoundException Handler

 

 public sealed class ServerErrorFilter : IErrorFilter { . . . public IError OnError(IError error) { . . . if (error.Exception is GameReviewNotFoundException gameReviewNotFoundException) { _logger.LogError(gameReviewNotFoundException, "Game review not found"); return ErrorBuilder .New() .SetMessage($"A game review having game id '{gameReviewNotFoundException.GameId}' and reviewer id '{gameReviewNotFoundException.ReviewerId}' could not found") .SetCode(ServerErrorCode.ResourceNotFound) .SetPath(error.Path) .SetExtension($"{ServerErrorCode.ResourceNotFound}_GAME_ID", gameReviewNotFoundException.GameId) .SetExtension($"{ServerErrorCode.ResourceNotFound}_REVIEWER_ID", gameReviewNotFoundException.ReviewerId) .Build(); } . . . } } 
Enter fullscreen mode Exit fullscreen mode

 

Step 5 - Configure GraphQL Service to support Error Filter

 

 builder .Services .AddGraphQLServer() // Add global error handling .AddErrorFilter(provider => { return new ServerErrorFilter( provider.GetRequiredService<ILogger<ServerErrorFilter>>(), builder.Environment); }) . . . .ModifyRequestOptions(options => { options.IncludeExceptionDetails = builder.Environment.IsDevelopment(); }); 
Enter fullscreen mode Exit fullscreen mode

 

Step 6 - Test Error Handling

 

  • Validation  
 { "errors": [ { "message": "There is a validation error", "path": [ "submitGameReview" ], "extensions": { "code": "BAD_USER_INPUT", "BAD_USER_INPUT_RATING": "'Rating' must be greater than or equal to '0'.", "BAD_USER_INPUT_SUMMARY": "The length of 'Summary' must be at least 20 characters. You entered 0 characters." } } ] } 
Enter fullscreen mode Exit fullscreen mode

 

  • GameNotFoundException  
 { "errors": [ { "message": "A game having id '8f7e254f-a6ce-4f13-a44c-8f102a17f2f4 could not found", "path": [ "deleteGameReview" ], "extensions": { "code": "RESOURCE_NOT_FOUND", "RESOURCE_NOT_FOUND_GAME_ID": "8f7e254f-a6ce-4f13-a44c-8f102a17f2f4" } } ] } 
Enter fullscreen mode Exit fullscreen mode

 

  • GameReviewNotFoundException  
 { "errors": [ { "message": "A game review having game id '8f7e254f-a6ce-4f13-a44c-8f102a17f2f5' and reviewer id '8b019864-4af2-4606-88b5-13e5eb62ff4e' could not found", "path": [ "deleteGameReview" ], "extensions": { "code": "RESOURCE_NOT_FOUND", "RESOURCE_NOT_FOUND_GAME_ID": "8f7e254f-a6ce-4f13-a44c-8f102a17f2f5", "RESOURCE_NOT_FOUND_REVIEWER_ID": "8b019864-4af2-4606-88b5-13e5eb62ff4e" } } ] } 
Enter fullscreen mode Exit fullscreen mode

 


 

Example 3

 

For Example 3, we extend Example 2 to cover the following topics:

 

  • Add EntityFramework support with in-memory database
  • Add support for query projection, filtering, sorting, and paging
  • Add query type extensions to allow splitting queries into multiple classes
  • Add database seeding (seed with fake data)

 

Add EntityFramework Support

 

  • Add required nuget packages
  • Create the following entities that will be used to help represent the data stored in our database
    • Game
    • GameReview
    • Reviewer
  • Create a context that will serve as our Repository/UnitOfWork called AppDbContext
  • Configure data services

 

Add EntityFramework Packages

 

 # add required EntityFramework packages dotnet add package Microsoft.EntityFrameworkCore.Design --version 6.0.6 dotnet add package Microsoft.EntityFrameworkCore.InMemory --version 6.0.6 # add HotChocolate package providing EntityFramework support dotnet add package HotChocolate.Data.EntityFramework --version 12.11.1 
Enter fullscreen mode Exit fullscreen mode

 

Define Entities

 

 public class Game { public Guid Id { get; set; } public string Title { get; set; } = string.Empty; public string Summary { get; set; } = string.Empty; public DateTime ReleasedOn { get; set; } public ICollection<GameReview> Reviews { get; set; } = new HashSet<GameReview>(); } public sealed class GameReview { public Guid GameId { get; set; } public Game? Game { get; set; } public Guid ReviewerId { get; set; } public Reviewer? Reviewer { get; set; } public int Rating { get; set; } public string Summary { get; set; } = string.Empty; public DateTime ReviewedOn { get; set; } } public class Reviewer { public Guid Id { get; set; } public string Name { get; set; } = string.Empty; public string Email { get; set; } = string.Empty; public string Username { get; set; } = string.Empty; public string Picture { get; set; } = string.Empty; public ICollection<GameReview> GameReviews { get; set; } = new HashSet<GameReview>(); } 
Enter fullscreen mode Exit fullscreen mode

 

Create AppDbContext

 

 public sealed class AppDbContext : DbContext { public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { } public DbSet<Game> Games { get; set; } = null!; public DbSet<GameReview> Reviews { get; set; } = null!; public DbSet<Reviewer> Reviewers { get; set; } = null!; protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Game>(game => { ... }); modelBuilder.Entity<Reviewer>(reviewer => { ... }); modelBuilder.Entity<GameReview>(gameReview => { ... }); base.OnModelCreating(modelBuilder); } } 
Enter fullscreen mode Exit fullscreen mode

 

Configure Data Services

 

 var builder = WebApplication.CreateBuilder(args); // configure in-memory database builder .Services .AddDbContextFactory<AppDbContext>(options => { options.UseInMemoryDatabase("myview"); options.EnableDetailedErrors(builder.Environment.IsDevelopment()); options.EnableSensitiveDataLogging(builder.Environment.IsDevelopment()); }); builder .Services .AddScoped<AppDbContext>(provider => provider .GetRequiredService<IDbContextFactory<AppDbContext>>() .CreateDbContext()); 
Enter fullscreen mode Exit fullscreen mode

 

Update GamesQuery

 

 [GraphQLDescription("Get list of games")] public IQueryable<GameDto> GetGames([Service] AppDbContext context) { return context .Games .AsNoTracking() .TagWith($"{nameof(GamesQuery)}::{nameof(GetGames)}") .OrderByDescending(game => game.ReleasedOn) .Include(game => game.Reviews) .Select(game => new GameDto { GameId = game.Id, Reviews = game.Reviews.Select(review => new GameReviewDto { GameId = review.GameId, Rating = review.Rating, ReviewerId = review.ReviewerId, Summary = review.Summary }), ReleasedOn = game.ReleasedOn, Summary = game.Summary, Title = game.Title }); } [GraphQLDescription("Find game by id")] public async Task<GameDto?> FindGameById([Service] AppDbContext context, Guid gameId) { var game = await context .Games .AsNoTracking() .TagWith($"{nameof(GamesQuery)}::{nameof(FindGameById)}") .Include(game => game.Reviews) .FirstOrDefaultAsync(game => game.Id == gameId); if (game is null) return null; return new GameDto { GameId = game.Id, Reviews = game.Reviews.Select(review => new GameReviewDto { GameId = review.GameId, Rating = review.Rating, ReviewerId = review.ReviewerId, Summary = review.Summary }), ReleasedOn = game.ReleasedOn, Summary = game.Summary, Title = game.Title }; } 
Enter fullscreen mode Exit fullscreen mode

 

Update GamesMutation

 

 [GraphQLDescription("Submit a game review")] public async Task<GameReviewDto> SubmitGameReview( [Service] AppDbContext context, GameReviewDto gameReview) { // use fluent validator await _validator.ValidateAndThrowAsync(gameReview); var game = await context .Games .FirstOrDefaultAsync(game => game.Id == gameReview.GameId) ?? throw GameNotFoundException.WithGameId(gameReview.GameId); var reviewer = await context .Reviewers .FirstOrDefaultAsync(reviewer => reviewer.Id == gameReview.ReviewerId) ?? throw ReviewerNotFoundException.WithReviewerId(gameReview.ReviewerId); var gameReviewFromDb = await context .Reviews .FirstOrDefaultAsync(review => review.GameId == gameReview.GameId && review.ReviewerId == gameReview.ReviewerId); if (gameReviewFromDb is null) { context.Reviews.Add(new GameReview { GameId = gameReview.GameId, Rating = gameReview.Rating, ReviewedOn = DateTime.UtcNow, ReviewerId = gameReview.ReviewerId, Summary = gameReview.Summary }); } else { gameReviewFromDb.Rating = gameReview.Rating; gameReviewFromDb.Summary = gameReview.Summary; } await context.SaveChangesAsync(); return gameReview; } [GraphQLDescription("Remove a game review")] public async Task<GameReviewDto> DeleteGameReview( [Service] AppDbContext context, Guid gameId, Guid reviewerId) { var gameReviewFromDb = await context .Reviews .FirstOrDefaultAsync(review => review.GameId == gameId && review.ReviewerId == reviewerId) ?? throw GameReviewNotFoundException.WithGameReviewId(gameId, reviewerId); context.Reviews.Remove(gameReviewFromDb); return new GameReviewDto { GameId = gameReviewFromDb.GameId, Rating = gameReviewFromDb.Rating, ReviewerId = gameReviewFromDb.ReviewerId, Summary = gameReviewFromDb.Summary }; } 
Enter fullscreen mode Exit fullscreen mode

 

Add Query Projection, Paging, Filtering and Sorting Support

 

There are 2 steps required to enable projections, paging, filtering and sorting.

 

Step 1 - Add Attributes To Queries

 

Add the following attributes to the method performing queries. The ordering of the attributes is important and should be defined in the following order

 

  • UsePaging
  • UseProjection
  • UseFiltering
  • UseSorting

 

 [GraphQLDescription("Get list of games")] [UsePaging] [UseProjection] [UseFiltering] [UseSorting] public IQueryable<GameDto> GetGames([Service] AppDbContext context) { ... } [GraphQLDescription("Find game by id")] [UseProjection] public async Task<GameDto?> FindGameById([Service] AppDbContext context, Guid gameId) { ... } 
Enter fullscreen mode Exit fullscreen mode

 

Step 2 - Configure GraphQL Service

 

 builder .Services .AddGraphQLServer() . . . // The .AddTypeExtension allows having queries defined in multiple files // whilst still having a single root query .AddQueryType() // Add GraphQL root query type .AddTypeExtension<GamesQuery>() .AddTypeExtension<ReviewerQuery>() // Add Projection, Filtering and Sorting support. The ordering matters.  .AddProjections() .AddFiltering() .AddSorting() ... ); 
Enter fullscreen mode Exit fullscreen mode

 

Add Database Seeding

 

Define Seeder class to generate fake data

 

 internal static class Seeder { public static async Task SeedDatabase(WebApplication app) { await using (var serviceScope = app.Services.CreateAsyncScope()) { var context = serviceScope .ServiceProvider .GetRequiredService<AppDbContext>(); await context.Database.EnsureCreatedAsync(); if (!await context.Reviewers.AnyAsync()) { var reviewers = JsonSerializer.Deserialize<IEnumerable<Reviewer>>( _reviewersText, new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!; context.AddRange(reviewers); } if (!await context.Games.AnyAsync()) { var games = JsonSerializer.Deserialize<IEnumerable<Game>>( _gamesText, new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!; context.Games.AddRange(games); } await context.SaveChangesAsync(); } } private static readonly string _gamesText = @" [ . . . ]"; private static readonly string _reviewersText = @" [ . . . ]"; } 
Enter fullscreen mode Exit fullscreen mode

 

Configure when to Seed database

 

 // Program.cs app.MapGraphQL("/"); // seed database after all middleware is setup await Seeder.SeedDatabase(app); 
Enter fullscreen mode Exit fullscreen mode

 


 

Example 4

 

For Example 4, we extend Example 3 to use a SQLite database.

 

Configure SQLite

 

Add Packages

 

 # add required EntityFramework packages dotnet add package Microsoft.EntityFrameworkCore.Design --version 6.0.6 dotnet add package Microsoft.EntityFrameworkCore.SQLite --version 6.0.6 # add HotChocolate package providing EntityFramework support dotnet add package HotChocolate.Data.EntityFramework --version 12.11.1 
Enter fullscreen mode Exit fullscreen mode

 

Configure AppDbContext

 

Update the OnModelCreating method to provide all the required entity-to-table mappings and relationships.

 

 public sealed class AppDbContext : DbContext { . . . protected override void OnModelCreating(ModelBuilder modelBuilder) { // configure Game modelBuilder.Entity<Game>(game => { // configure table game.ToTable(nameof(Game).ToLower()); // configure properties game.Property(e => e.Id).HasColumnName("id"); game.Property(e => e.ReleasedOn).HasColumnName("released_on").IsRequired(); game.Property(e => e.Summary).HasColumnName("summary").IsRequired(); game.Property(e => e.Title).HasColumnName("title").IsRequired(); // configure primary key game.HasKey(e => e.Id).HasName("pk_game_id"); // configure game relationship game.HasMany(e => e.Reviews).WithOne(e => e.Game); }); // configure Reviewer modelBuilder.Entity<Reviewer>(reviewer => { // configure table reviewer.ToTable(nameof(Reviewer).ToLower()); // configure properties reviewer.Property(e => e.Id).HasColumnName("id"); reviewer.Property(e => e.Email).HasColumnName("email").IsRequired(); reviewer.Property(e => e.Name).HasColumnName("name").IsRequired(); reviewer.Property(e => e.Picture).HasColumnName("picture").IsRequired(); reviewer.Property(e => e.Username).HasColumnName("username").IsRequired(); // configure primary key reviewer.HasKey(e => e.Id).HasName("pk_reviewer_id"); // configure reviewer relationship reviewer.HasMany(e => e.GameReviews).WithOne(e => e.Reviewer); }); // configure GameReview modelBuilder.Entity<GameReview>(gameReview => { // configure table gameReview.ToTable("game_review"); // configure properties gameReview.Property(e => e.GameId).HasColumnName("game_id").IsRequired(); gameReview.Property(e => e.ReviewerId).HasColumnName("reviewer_id").IsRequired(); gameReview.Property(e => e.Rating).HasColumnName("rating").IsRequired(); gameReview.Property(e => e.ReviewedOn).HasColumnName("reviewed_on").IsRequired(); gameReview.Property(e => e.Summary).HasColumnName("summary").HasDefaultValue(""); // configure primary key gameReview .HasKey(e => new { e.GameId, e.ReviewerId }) .HasName("pk_gamereview_gameidreviewerid"); // configure game relationship gameReview .HasOne(e => e.Game) .WithMany(e => e.Reviews) .HasConstraintName("fk_gamereview_gameid"); gameReview .HasIndex(e => e.GameId) .HasDatabaseName("ix_gamereview_gameid"); // configure reviewer relationship gameReview .HasOne(e => e.Reviewer) .WithMany(e => e.GameReviews) .HasConstraintName("fk_gamereview_reviewerid"); gameReview .HasIndex(e => e.ReviewerId) .HasDatabaseName("ix_gamereview_reviewerid"); }); base.OnModelCreating(modelBuilder); } } 
Enter fullscreen mode Exit fullscreen mode

 

Configure Data Services

 

 builder .Services .AddDbContextFactory<AppDbContext>(options => { options.UseSqlite("Data Source=myview.db"); options.EnableDetailedErrors(builder.Environment.IsDevelopment()); options.EnableSensitiveDataLogging(builder.Environment.IsDevelopment()); }); builder .Services .AddScoped<AppDbContext>(provider => provider .GetRequiredService<IDbContextFactory<AppDbContext>>() .CreateDbContext()); 
Enter fullscreen mode Exit fullscreen mode

 

Create Database Migrations

 

Before running the application, we need to create the database migrations in order to create/update the SQLite database. After the migrations and database update are complete, a SQLite database called myview.db will be created in the root of the project folder.

 

 # navigate to API project folder cd ./MyView.Api # create the database migrations dotnet ef migrations add "CreateInitialDatabaseSchema" 
Enter fullscreen mode Exit fullscreen mode

 

Create and Update SQLite Database

 

 # run the migrations to create database dotnet ef database update 
Enter fullscreen mode Exit fullscreen mode

 


 

Deploy GraphQL API

 

In this section, we will cover the following primary topics:

 

  • Provision Azure App Service
  • Deploy GraphQL API to Azure App Service

 

Provision Azure App Service

 

We will be deploying the 'MyView API' into [Azure App Service]. However, before we can deploy our API, we first need to provision our Azure App Service. This section demonstrates how to provision an Azure App Service using 3 different techniques and are listed as follows:

 

  • Azure CLI
  • Azure Powershell
  • Azure Bicep

 

Regardless of the chosen technique, there are 3 general steps that need to be completed in order to provision the Azure App Service.

 

  • Create a resource group
  • Create an App Service Plan
  • Create an App Service

 

Once all the required resources are created, we will be ready to deploy the 'MyView API' to Azure.

 

Create Azure App Service Using Azure CLI

 

All the commands that are required to create the Azure App Service using the Azure CLI can be found in the iac/azcli/deploy.azcli file that is part of the example github repository

 

 $location = "australiaeast" # STEP 1 - create resource group $rgName = "myview-rg" az group create ` --name $rgName ` --location $location # STEP 2 - create appservice plan $aspName = "myview-asp" $appSku = "F1" az appservice plan create ` --name $aspName ` --resource-group $rgName ` --sku $appSku ` --is-linux # STEP 3 - create webapp $appName = "myview-webapp-api" $webapp = az webapp create ` --name $appName ` --resource-group $rgName ` --plan $aspName ` --runtime '"DOTNETCORE|6.0"' # STEP 4 - cleanup az group delete --resource-group $rgName -y 
Enter fullscreen mode Exit fullscreen mode

 

Create Azure App Service Using Azure Powershell

 

All the commands that are required to create the Azure App Service using the Azure Powershell can be found in the /azpwsh/deploy.azcli file that is part of the example github repository.

 

 $location = "australiaeast" # STEP 1 - create resource group $rgName = "myview-rg" New-AzResourceGroup -Name $rgName -Location $location # STEP 2 - create appservice plan $aspName = "myview-asp" $appSku = "F1" New-AzAppServicePlan ` -Name $aspName ` -Location $location ` -ResourceGroupName $rgName ` -Tier $appSku ` -Linux # STEP 3 - create webapp $appName = "myview-webapp-api" New-AzWebApp ` -Name $appName ` -Location $location ` -AppServicePlan $aspName ` -ResourceGroupName $rgName # STEP 4 - configure webapp ## At this point, the webapp is not using .NET v6 as is required. ## This can be verified by the following commands az webapp config show ` --resource-group $rgName ` --name $appName ` --query netFrameworkVersion Get-AzWebApp ` -Name $appName ` -ResourceGroupName $rgName ` | Select-Object -ExpandProperty SiteConfig ` | Select-Object -Property NetFrameworkVersion Get-AzWebApp ` -Name $appName ` -ResourceGroupName $rgName ` | ConvertTo-Json ` | jq ".SiteConfig.NetFrameworkVersion" ## lets configure the webapp with the correct version of .NET Set-AzWebApp ` -Name $appName ` -ResourceGroupName $rgName ` -AppServicePlan $aspName ` -NetFrameworkVersion 'v6.0' $apiVersion = "2020-06-01" $config = Get-AzResource ` -ResourceGroupName $rgName ` -ResourceType Microsoft.Web/sites/config ` -ResourceName $appName/web ` -ApiVersion $apiVersion $config.Properties.linuxFxVersion = "DOTNETCORE|6.0" $config | Set-AzResource -ApiVersion $apiVersion -Force # cleanup Remove-AzResourceGroup -Name $rgName -Force 
Enter fullscreen mode Exit fullscreen mode

 

Create Azure App Service Using Azure Bicep

 

In this example, I demonstrate both a basic and more advanced option (uses modules) to deploy bicep templates.

 

Azure Bicep - Basic

 

All the commands and templates that are required to create the Azure App Service using Azure Bicep can be found in the iac/bicep/basic folder that is part of the example github repository

 

 # file: iac/bicep/basic/deploy.azcli $location = "australiaeast" # STEP 1 - create resource group $rgName = "myview-rg" az group create --name $rgName --location $location # STEP 2 - deploy template $aspName = "myview-asp" $appName = "myview-webapp-api" $deploymentName = "myview-deployment" az deployment group create ` --name $deploymentName ` --resource-group $rgName ` --template-file ./main.bicep ` --parameters appName=$appName ` --parameters aspName=$aspName # cleanup az group delete --resource-group $rgName -y 
Enter fullscreen mode Exit fullscreen mode

 

 // file: iac/bicep/basic/main.bicep // parameters @description('The name of app service') param appName string @description('The name of app service plan') param aspName string @description('The location of all resources') param location string = resourceGroup().location @description('The Runtime stack of current web app') param linuxFxVersion string = 'DOTNETCORE|6.0' @allowed([ 'F1' 'B1' ]) param skuName string = 'F1' // variables resource asp 'Microsoft.Web/serverfarms@2021-02-01' = { name: aspName location: location kind: 'linux' sku: { name: skuName } properties: { reserved: true } } resource app 'Microsoft.Web/sites@2021-02-01' = { name: appName location: location properties: { serverFarmId: asp.id httpsOnly: true siteConfig: { linuxFxVersion: linuxFxVersion } } } 
Enter fullscreen mode Exit fullscreen mode

 

Azure Bicep - Advanced (with modules)

 

All the commands and templates that are required to create the Azure App Service using Azure Bicep can be found in the iac/bicep/advanced folder that is part of the example github repository

 

 # file: iac/bicep/advanced/deploy.azcli $location = "australiaeast" # STEP 1 - deploy template $deploymentName = "myview-deployment" az deployment sub create ` --name $deploymentName ` --location $location ` --template-file ./main.bicep ` --parameters location=$location # STEP 2 - get outputs from deployment az deployment sub show --name $deploymentName --query "properties.outputs" $hostName = $(az deployment sub show --name $deploymentName --query "properties.outputs.defaultHostName.value") $rgName = $(az deployment sub show --name $deploymentName --query "properties.outputs.resourceGroupName.value") echo $hostName, $rgName # STEP 3 - cleanup az group delete --resource-group $rgName -y 
Enter fullscreen mode Exit fullscreen mode

 

 // file: iac/bicep/advanced/modules/app-service.bicep // parameters @description('The name of app service') param appName string @description('The name of app service plan') param aspName string @description('The location of all resources') param location string = resourceGroup().location @description('The Runtime stack of current web app') param linuxFxVersion string = 'DOTNETCORE|6.0' @allowed([ 'F1' 'B1' ]) param skuName string = 'F1' // define resources resource asp 'Microsoft.Web/serverfarms@2021-02-01' = { name: aspName location: location kind: 'linux' sku: { name: skuName } properties: { reserved: true } } resource app 'Microsoft.Web/sites@2021-02-01' = { name: appName location: location properties: { serverFarmId: asp.id httpsOnly: true siteConfig: { linuxFxVersion: linuxFxVersion } } } // define outputs output defaultHostName string = app.properties.defaultHostName 
Enter fullscreen mode Exit fullscreen mode

 

 // file: iac-src/bicep/advanced/main.bicep // define scope targetScope = 'subscription' // define parameters @description('The location of all resources') param location string = deployment().location // define variables @description('The name of resource group') var rgName = 'myview-rg' @description('The name of resource group') var aspName = 'myview-asp' @description('The name of app') var appName = 'myview-webapp-api' // define resources resource resourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = { name: rgName location: location } // define modules module appServiceModule 'modules/app-service.bicep' = { scope: resourceGroup name: 'myview-module' params: { appName: appName aspName: aspName location: location } } // define outputs output defaultHostName string = appServiceModule.outputs.defaultHostName output resourceGroupName string = rgName 
Enter fullscreen mode Exit fullscreen mode

 

Deploy GraphQL API to Azure App Service

 

Publish GraphQL API Locally

 

 dotnet publish ` --configuration Release ` --framework net6.0 ` --output ./release ` ./MyView.Api 
Enter fullscreen mode Exit fullscreen mode

 

Deploy GraphQL API

 

 cd ./release az webapp up ` --plan myview-asp ` --name myview-webapp-api ` --resource-group myview-rg ` --os-type linux ` --runtime "DOTNETCORE:6.0" 
Enter fullscreen mode Exit fullscreen mode

 


 

Where To Next?

 

I have provided a number of examples that show how to build a GraphQL Server using ChilliCream Hot Chocolate GraphQL Server. If you would like to learn more, please view the following learning resources:

 

 

HotChocolate Templates

 

There are also a number of Hot Chocolate templates that can be installed using the dotnet CLI tool.

 

  • Install HotChocolate Templates:  
 # install Hot Chocolate GraphQL server templates (includes Azure function template) dotnet new -i HotChocolate.Templates # install template that allows you to create a GraphQL Star Wars Demo dotnet new -i HotChocolate.Templates.StarWars 
Enter fullscreen mode Exit fullscreen mode

 

  • List HotChocolate Templates  
 # list HotChocolate templates dotnet new --list HotChocolate 
Enter fullscreen mode Exit fullscreen mode
 Template Name Short Name Language Tags ----------------------------------- ----------- -------- ------------------------------ HotChocolate GraphQL Function graphql-azf [C#] Web/GraphQL/Azure HotChocolate GraphQL Server graphql [C#] Web/GraphQL HotChocolate GraphQL Star Wars Demo starwars [C#] ChilliCream/HotChocolate/Demos 
Enter fullscreen mode Exit fullscreen mode

 

  • Create HotChocolate project using templates  
 # create ASP.NET GraphQL Server dotnet new graphql --name MyGraphQLDemo # create graphql server using Azure Function dotnet new graphql-azf --name MyGraphQLAzfDemo # create starwars GraphQL demo mkdir StarWars cd StarWars dotnet new starwars 
Enter fullscreen mode Exit fullscreen mode

 


Top comments (5)

Collapse
 
stphnwlsh profile image
Stephen Walsh

@drminnaar This is a great post! You might want to break it up into a couple of posts and use the series feature on here to make it a little more digestible.

I've been playing around with GraphQL and .NET myself. Have used the alternative option GraphQL.NET but am about to pick up Hot Chocolate to build it again using that library. This will be a big help.

It's a cool tech but definitely needs some high level control from the engineers building it. Don't want to smash the database on every frontend request. That's how you spend mega dollars in the cloud by accident

Collapse
 
drminnaar profile image
Douglas Minnaar

Thanks @stphnwlsh, using a series is a great suggestion.

The support for GraphQL has improved a lot from its humble beginnings. Much respect to the builders that are working on these projects. The Chillicream offerings (both client and server) are looking more solid with every release. The latest version of HotChocolate has some good performance improvements too. I also like that you can use different approaches like code-first vs annotation-based vs schema-first.

Collapse
 
stphnwlsh profile image
Stephen Walsh

Yeah it's only getting better and I think .NET is becoming more viable for GraphQL systems.

Code first approach seems nice to me. I prefer the models being returned to have some decent decoration/detail to them for a better user experience consuming the API. Plus not a difficult upgrade on my existing solution

I like your approaches on GitHub too. Keep making good stuff!!!!

Collapse
 
romefog profile image
romefog

Hello. I recommend visiting the Rich Palm casino review to anyone who wants to start playing at the best casino. By visiting this site, you can learn about the various gaming features of this casino, namely what types of games are available at this casino, bonuses, etc. It will also be useful to know how quick and easy it is to register at this casino.

Collapse
 
aksxhaukh profile image
ajkdsJIK

I've been messing with GraphQL and .NET myself. Have utilized the elective choice GraphQL.NET yet am going to get Hot cocoa to assemble it again utilizing that library. This will be a major assistance.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.