skills/dotnet-graphql/SKILL.md
Guides implementation of GraphQL with HotChocolate in .NET 8 projects following Clean Architecture. Creates the GraphQL project with single or multi-schema design, queries returning IQueryable for EF Core optimization, type extensions for computed fields, field hiding, error logging, and DI registration. Use when adding GraphQL support, creating queries, type extensions, or configuring GraphQL schemas.
npx skillsauth add landim32/awesome-ai-skills dotnet-graphqlInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are an expert assistant that helps developers implement GraphQL using HotChocolate in .NET 8 projects following Clean Architecture. You guide the user through creating the GraphQL layer with IQueryable-based queries, type extensions, and proper DI registration. The schema design is flexible — single schema or multiple schemas depending on the project's needs.
The user will describe what to create or modify: $ARGUMENTS
Before generating code:
Lofn, MyApp)DbSet<T> entities available for GraphQL exposureApplication/Startup.cs){Project}.GraphQL/
├── GraphQLServiceExtensions.cs ← DI registration, configures schema(s)
├── GraphQLErrorLogger.cs ← Diagnostic event listener for logging
├── {Project}.GraphQL.csproj ← NuGet dependencies
├── Queries/
│ └── {Entity}Query.cs ← Query class(es) — single or split by access level
├── Types/
│ ├── {Entity}TypeExtension.cs ← Computed fields via ObjectTypeExtension
│ └── {Entity}Type.cs ← ObjectType with field hiding (if needed)
└── (optional) Public/ & Admin/ ← Only if using multi-schema design
{Project}.API (Startup) → {Project}.GraphQL → {Project}.Infra (DbContext, Entities)
→ {Project}.Domain (Interfaces)
Choose based on project requirements:
| Strategy | When to use | Structure |
|----------|-------------|-----------|
| Single schema | All queries require auth, or no auth distinction needed | One Query class, one AddGraphQLServer() |
| Dual schema (public + admin) | Some queries are anonymous, others require auth with different data visibility | PublicQuery + AdminQuery, two AddGraphQLServer() calls |
| Multiple named schemas | Complex projects with distinct API consumers | Multiple named servers with separate query types |
dotnet new classlib -n {Project}.GraphQL -f net8.0
dotnet sln add {Project}.GraphQL
<!-- {Project}.GraphQL.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="HotChocolate.AspNetCore" Version="14.3.0" />
<PackageReference Include="HotChocolate.AspNetCore.Authorization" Version="14.3.0" />
<PackageReference Include="HotChocolate.Data.EntityFramework" Version="14.3.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\{Project}.Domain\{Project}.Domain.csproj" />
<ProjectReference Include="..\{Project}.Infra\{Project}.Infra.csproj" />
</ItemGroup>
</Project>
Create GraphQLErrorLogger.cs — centralized error handling for all GraphQL operations.
using System;
using System.Collections.Generic;
using HotChocolate;
using HotChocolate.Execution;
using HotChocolate.Execution.Instrumentation;
using HotChocolate.Execution.Processing;
using HotChocolate.Resolvers;
using Microsoft.Extensions.Logging;
namespace {Project}.GraphQL;
public class GraphQLErrorLogger : ExecutionDiagnosticEventListener
{
private readonly ILogger<GraphQLErrorLogger> _logger;
public GraphQLErrorLogger(ILogger<GraphQLErrorLogger> logger)
{
_logger = logger;
}
public override void RequestError(IRequestContext context, Exception exception)
{
_logger.LogError(exception, "GraphQL request error");
}
public override void ValidationErrors(IRequestContext context, IReadOnlyList<IError> errors)
{
foreach (var error in errors)
_logger.LogWarning("GraphQL validation error: {Message}", error.Message);
}
public override void SyntaxError(IRequestContext context, IError error)
{
_logger.LogWarning("GraphQL syntax error: {Message}", error.Message);
}
public override void ResolverError(IMiddlewareContext context, IError error)
{
_logger.LogError(error.Exception, "GraphQL resolver error on {Path}: {Message}", context.Path, error.Message);
}
public override void ResolverError(IRequestContext context, ISelection selection, IError error)
{
_logger.LogError(error.Exception, "GraphQL resolver error on {Field}: {Message}", selection.Field.Name, error.Message);
}
}
Queries always return IQueryable<T> for EF Core optimization. The structure depends on the schema strategy.
using System.Linq;
using HotChocolate;
using HotChocolate.Authorization;
using HotChocolate.Data;
using HotChocolate.Types;
using {Project}.Infra.Context;
using Microsoft.AspNetCore.Http;
namespace {Project}.GraphQL.Queries;
public class Query
{
/// <summary>
/// List query with pagination, filtering, and sorting.
/// </summary>
[UseOffsetPaging]
[UseProjection]
[UseFiltering]
[UseSorting]
public IQueryable<{Entity}> Get{Entity}s({DbContext} context)
=> context.{Entity}s.Where(e => e.Status == 1);
/// <summary>
/// Single record lookup by unique field (no pagination needed).
/// </summary>
[UseProjection]
public IQueryable<{Entity}> Get{Entity}BySlug({DbContext} context, string slug)
=> context.{Entity}s.Where(e => e.Status == 1 && e.Slug == slug);
/// <summary>
/// Authenticated query — use [Authorize] at method level for mixed schemas.
/// </summary>
[Authorize]
[UseOffsetPaging]
[UseProjection]
[UseFiltering]
[UseSorting]
public IQueryable<{Entity}> GetMy{Entity}s(
{DbContext} context,
IHttpContextAccessor httpContextAccessor,
[Service] {IUserService} userService)
{
var user = userService.GetCurrentUser(httpContextAccessor.HttpContext!);
return context.{Entity}s.Where(e => e.OwnerId == user!.UserId);
}
}
Public (anonymous):
using System.Linq;
using HotChocolate;
using HotChocolate.Data;
using HotChocolate.Types;
using {Project}.Infra.Context;
namespace {Project}.GraphQL.Public;
public class PublicQuery
{
[UseOffsetPaging]
[UseProjection]
[UseFiltering]
[UseSorting]
public IQueryable<{Entity}> Get{Entity}s({DbContext} context)
=> context.{Entity}s.Where(e => e.Status == 1);
[UseProjection]
public IQueryable<{Entity}> Get{Entity}BySlug({DbContext} context, string slug)
=> context.{Entity}s.Where(e => e.Status == 1 && e.Slug == slug);
}
Admin (authenticated, user-scoped):
using System.Linq;
using HotChocolate;
using HotChocolate.Authorization;
using HotChocolate.Data;
using HotChocolate.Types;
using {Project}.Infra.Context;
using Microsoft.AspNetCore.Http;
namespace {Project}.GraphQL.Admin;
[Authorize]
public class AdminQuery
{
private IQueryable<long> GetUserEntityIds(
{DbContext} context,
IHttpContextAccessor httpContextAccessor,
{IUserService} userService)
{
var user = userService.GetCurrentUser(httpContextAccessor.HttpContext!);
var userId = user!.UserId;
return context.{UserEntityMapping}s
.Where(m => m.UserId == userId)
.Select(m => m.{EntityId});
}
[UseOffsetPaging]
[UseProjection]
[UseFiltering]
[UseSorting]
public IQueryable<{Entity}> GetMy{Entity}s(
{DbContext} context,
IHttpContextAccessor httpContextAccessor,
[Service] {IUserService} userService)
{
var entityIds = GetUserEntityIds(context, httpContextAccessor, userService);
return context.{Entity}s.Where(e => entityIds.Contains(e.{EntityId}));
}
}
Critical patterns:
IQueryable<T> — never List<T> or Task<List<T>>. HotChocolate builds optimized SQL via EF Core[UseOffsetPaging] → [UseProjection] → [UseFiltering] → [UseSorting][Authorize] — at class level (all queries require auth) or method level (mixed access)[Service] attribute — for custom interfaces not auto-resolved by HotChocolateGetUserEntityIds() returns IQueryable<long> for SQL subquery composition (no materialization)Create an ObjectType<T> to hide sensitive fields. Used when certain fields should not be exposed in a schema.
using HotChocolate.Types;
using {Project}.Infra.Context;
namespace {Project}.GraphQL.Types;
public class {Entity}Type : ObjectType<{Entity}>
{
protected override void Configure(IObjectTypeDescriptor<{Entity}> descriptor)
{
descriptor.Ignore(e => e.OwnerId);
descriptor.Ignore(e => e.{SensitiveNavigationProperty});
}
}
Guidelines:
Create files in Types/ — add derived/computed fields to entities without modifying the entity model.
using HotChocolate.Types;
using {Project}.Infra.Context;
namespace {Project}.GraphQL.Types;
public class {Entity}TypeExtension : ObjectTypeExtension<{Entity}>
{
protected override void Configure(IObjectTypeDescriptor<{Entity}> descriptor)
{
// Ensure the source field is included in SQL projection
descriptor.Field(t => t.{SourceField}).IsProjected(true);
descriptor
.Field("{computedFieldName}")
.Type<StringType>()
.Resolve(async ctx =>
{
var entity = ctx.Parent<{Entity}>();
if (string.IsNullOrEmpty(entity.{SourceField})) return null;
var service = ctx.Service<{IService}>();
return await service.{Method}(entity.{SourceField});
});
}
}
using System.Linq;
using HotChocolate.Types;
using {Project}.Infra.Context;
using Microsoft.EntityFrameworkCore;
namespace {Project}.GraphQL.Types;
public class {Entity}TypeExtension : ObjectTypeExtension<{Entity}>
{
protected override void Configure(IObjectTypeDescriptor<{Entity}> descriptor)
{
descriptor
.Field("{computedFieldName}")
.Type<StringType>()
.Resolve(async ctx =>
{
var entity = ctx.Parent<{Entity}>();
var dbContext = ctx.Service<{DbContext}>();
var result = await dbContext.{RelatedEntities}
.Where(r => r.{EntityId} == entity.{EntityId})
.OrderBy(r => r.{SortField})
.Select(r => r.{TargetField})
.FirstOrDefaultAsync();
if (string.IsNullOrEmpty(result)) return null;
var service = ctx.Service<{IService}>();
return await service.{Method}(result);
});
}
}
using System.Linq;
using HotChocolate;
using HotChocolate.Types;
using {Project}.Infra.Context;
namespace {Project}.GraphQL.Types;
[ExtendObjectType(typeof({Entity}))]
public class {Entity}TypeExtension
{
public int Get{ComputedField}(
[Parent] {Entity} entity,
[Service] {DbContext} context)
{
return context.{RelatedEntities}.Count(r => r.{EntityId} == entity.{EntityId} && r.Status == 1);
}
}
When to use each pattern:
IsProjected(true) to ensure the source field is in SQL[Parent] and [Service] attributesKey elements:
.IsProjected(true) — ensures the source field is included in SQL even if not in the GraphQL queryctx.Parent<T>() — accesses the parent entity being extendedctx.Service<T>() — resolves a service from DI containerCreate GraphQLServiceExtensions.cs — the configuration varies by schema strategy.
using HotChocolate.Execution.Configuration;
using HotChocolate.Execution.Options;
using HotChocolate.Types.Pagination;
using {Project}.GraphQL.Queries;
using {Project}.GraphQL.Types;
using Microsoft.Extensions.DependencyInjection;
namespace {Project}.GraphQL;
public static class GraphQLServiceExtensions
{
public static IServiceCollection Add{Project}GraphQL(this IServiceCollection services)
{
services
.AddGraphQLServer()
.AddAuthorization()
.AddDiagnosticEventListener<GraphQLErrorLogger>()
.AddQueryType<Query>()
.AddType<{Entity}Type>() // Field hiding (if needed)
.AddTypeExtension<{Entity}TypeExtension>() // Computed fields
.SetPagingOptions(new PagingOptions
{
MaxPageSize = 50,
DefaultPageSize = 10,
IncludeTotalCount = true
})
.AddProjections()
.AddFiltering()
.AddSorting()
.ModifyCostOptions(o => o.MaxFieldCost = 8000);
return services;
}
}
using HotChocolate.Execution.Configuration;
using HotChocolate.Execution.Options;
using HotChocolate.Types.Pagination;
using {Project}.GraphQL.Admin;
using {Project}.GraphQL.Public;
using {Project}.GraphQL.Types;
using Microsoft.Extensions.DependencyInjection;
namespace {Project}.GraphQL;
public static class GraphQLServiceExtensions
{
public static IServiceCollection Add{Project}GraphQL(this IServiceCollection services)
{
// ── Public Server (anonymous access) ──
services
.AddGraphQLServer()
.AddAuthorization()
.AddDiagnosticEventListener<GraphQLErrorLogger>()
.AddQueryType<PublicQuery>()
.AddType<{Entity}Type>() // Field hiding (only on public)
.AddTypeExtension<{Entity}TypeExtension>() // Computed fields
.SetPagingOptions(new PagingOptions
{
MaxPageSize = 50,
DefaultPageSize = 10,
IncludeTotalCount = true
})
.AddProjections()
.AddFiltering()
.AddSorting()
.ModifyCostOptions(o => o.MaxFieldCost = 8000);
// ── Admin Server (authenticated access) ──
services
.AddGraphQLServer("admin")
.AddAuthorization()
.AddDiagnosticEventListener<GraphQLErrorLogger>()
.AddQueryType<AdminQuery>()
// NO {Entity}Type here — admin sees all fields
.AddTypeExtension<{Entity}TypeExtension>() // Same computed fields
.SetPagingOptions(new PagingOptions
{
MaxPageSize = 50,
DefaultPageSize = 10,
IncludeTotalCount = true
})
.AddProjections()
.AddFiltering()
.AddSorting()
.ModifyCostOptions(o => o.MaxFieldCost = 8000);
return services;
}
}
Key configuration:
AddAuthorization() — enables [Authorize] attribute supportAddDiagnosticEventListener<GraphQLErrorLogger>() — connects error loggingAddType<{Entity}Type>() — field hiding (register only where needed)MaxPageSize=50, DefaultPageSize=10, IncludeTotalCount=trueAddProjections() + AddFiltering() + AddSorting() — HotChocolate EF Core integrationModifyCostOptions — MaxFieldCost = 8000 prevents expensive nested queriesAdd to the API's Startup.cs. Configuration varies by schema strategy.
// ConfigureServices:
services.Add{Project}GraphQL();
// Configure (middleware pipeline):
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapGraphQL("/graphql"); // Single endpoint
});
// ConfigureServices:
services.Add{Project}GraphQL();
// Configure (middleware pipeline):
// Authentication runs conditionally — skip for public endpoint
app.UseWhen(
context => !context.Request.Path.StartsWithSegments("/graphql")
|| context.Request.Path.StartsWithSegments("/graphql/admin"),
branch => branch.UseAuthentication()
);
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapGraphQL("/graphql").AllowAnonymous(); // Public schema
endpoints.MapGraphQL("/graphql/admin", "admin"); // Admin schema
});
All endpoints expose the Banana Cake Pop interactive playground for development.
Add the GraphQL project reference to {Project}.Application.csproj:
<ProjectReference Include="..\{Project}.GraphQL\{Project}.GraphQL.csproj" />
And call Add{Project}GraphQL() from the centralized Startup.cs in Application.
Add a method to the query class:
[UseOffsetPaging]
[UseProjection]
[UseFiltering]
[UseSorting]
public IQueryable<{Entity}> Get{Entity}s({DbContext} context)
=> context.{Entity}s.Where(e => e.Status == 1);
For authenticated queries, add [Authorize] (method level for single schema, class level for dedicated admin query class).
Types/{Entity}TypeExtension.cs using Pattern A, B, or CGraphQLServiceExtensions.cs:.AddTypeExtension<{Entity}TypeExtension>()
Types/{Entity}Type.cs with descriptor.Ignore()GraphQLServiceExtensions.cs:.AddType<{Entity}Type>()
services
.AddGraphQLServer("{schemaName}")
.AddQueryType<{SchemaName}Query>()
// ... same configuration chain
// Endpoint:
endpoints.MapGraphQL("/graphql/{schemaName}", "{schemaName}");
| # | File | Action | Description |
|---|------|--------|-------------|
| 1 | {Project}.GraphQL.csproj | Create | Project with HotChocolate packages |
| 2 | GraphQLErrorLogger.cs | Create | Diagnostic event listener for error logging |
| 3 | Query class(es) | Create | Query.cs (single) or PublicQuery.cs + AdminQuery.cs (dual) |
| 4 | Types/{Entity}Type.cs | Create | ObjectType with field hiding (if needed) |
| 5 | Types/{Entity}TypeExtension.cs | Create | Computed fields via type extensions (one per entity) |
| 6 | GraphQLServiceExtensions.cs | Create | DI registration for schema(s) |
| 7 | Application/Startup.cs | Modify | Call Add{Project}GraphQL() |
| 8 | API/Startup.cs | Modify | Add endpoint mapping and auth middleware |
| 9 | Application .csproj | Modify | Add project reference to GraphQL project |
DbSet<T> entities and their relationshipsIQueryable<T> — Never materialize queries in GraphQL resolvers[UseOffsetPaging] → [UseProjection] → [UseFiltering] → [UseSorting]IsProjected(true) — When a computed field depends on a source fieldtools
Guides how to integrate the zTools package for ChatGPT, DALL-E image generation, file upload (S3), slug generation, email sending, and document validation in a .NET 8 project. Use when the user wants to use AI features, upload files, generate slugs, send emails, or understand zTools integration.
documentation
Generates a comprehensive, standardized README.md for any project. Use when the user wants to create or regenerate a README file following the project's documentation standard.
development
Create modal dialogs in the frontend using a custom Modal component built on top of Radix UI Dialog. Use this skill whenever the user asks to create, add, or modify a modal, dialog, popup, or confirmation prompt in the React application.
development
Create the complete frontend architecture for a new entity in the React application. Generates TypeScript types, service class, context provider, custom hook, and registers the provider in main.tsx. Use this skill when the user asks to create a new entity, feature module, or domain area in the frontend.