Series: GraphQL with .NET & React, From Zero to Production
Welcome to this comprehensive GraphQL tutorial series! By the end of these 5 articles, you'll have a solid understanding of GraphQL and hands-on experience building a full-stack application with React and .NET.
Get your code here: https://github.com/RikamPalkar/graphql-dotnet-react-from-zero-to-production
This is the series that takes you from GraphQL basics to advanced patterns.
Part 1: Foundations & Library Backend
Part 3: Mutations (Create, Update & Delete)
Part 4: Real-Time Data with web sockets
Part 5: Advanced Patterns (Pagination, Errors, JWT & Testing)
Part 2: Query (Arguments, Aliases & Fragments)
Welcome back! In Part 1, we set up our Library Management System, learned about GraphQL, and wrote our first query. Now it's time to master the full power of GraphQL queries.
Where We Left Off
We have:
A working .NET backend with HotChocolate serving GraphQL
A React frontend with Apollo Client consuming it
Basic queries fetching books with nested author data
Now let's learn the advanced query features that make GraphQL truly powerful.
Understanding Query Structure
Before diving into advanced features, let's fully understand query anatomy:
query GetBooksQuery {
books {
nodes {
title
}}}
| Part | What it is | Required? |
|---|
| query | Operation type | Optional for queries (default) |
| GetBooksQuery | Operation name | Optional but recommended |
| books | Root field | Required |
| { nodes { title } } | Selection set | Required |
Query Arguments
Arguments allow you to pass parameters to your queries - like function parameters in programming.
What Are Arguments?
Without arguments, queries return ALL data. Arguments let you:
Filter: "Give me book #5"
Limit: "Give me only 10 books"
Search: "Give me books containing 'war'"
Basic Argument Syntax
query {
bookById(id: 1) {
title
description
}
}
Syntax breakdown:
How Arguments Work (Backend)
In HotChocolate, arguments become method parameters:
// The 'id' parameter becomes a GraphQL argument automatically
public async Task<Book?> GetBookById(LibraryDbContext context, int id)
{
return await context.Books.FirstOrDefaultAsync(b => b.Id == id);
}
Method name GetBookById → GraphQL field bookById
Parameter int id → GraphQL argument id: Int!
Return type Book? → GraphQL return type Book
Multiple Arguments
query {
filteredBooks(
filter: { genre: "Fantasy", minYear: 1950 }
sort: { field: PUBLISHED_YEAR, direction: DESC }
skip: 0
take: 10
) {
title
publishedYear
}
}
Variables - Making Queries Dynamic
Hardcoding values like id: 1 isn't practical. What if the ID comes from user input?
The Problem
# Hardcoded - not reusable
query {
bookById(id: 1) { title }
}
The Solution: Variables
Variables separate the query structure from the values:
# Dynamic - pass any ID
query GetBook($bookId: Int!) {
bookById(id: $bookId) {
title
description
}
}
Syntax breakdown:
$bookId - Variable name (starts with $)
Int! - Variable type (must match argument type)
: $bookId - Using the variable as argument value
Variables Are Passed Separately
When executing the query, variables are sent as JSON:
{
"bookId": 1
}
Why this separation?
Same query can be reused with different values
Better security (prevents injection attacks)
Query can be cached regardless of variables
React Implementation with Variables
const GET_BOOK = gql`
query GetBook($bookId: Int!) {
bookById(id: $bookId) {
title
description
}
}
`;
function BookDetail({ bookId }: { bookId: number }) {
// Pass variables as an option to useQuery
const { loading, data } = useQuery(GET_BOOK, {
variables: { bookId }, // This becomes the JSON above
});
if (loading) return <p>Loading...</p>;
return <h1>{data.bookById.title}</h1>;
}
Default Values for Variables
Make variables optional with defaults:
query GetBooks($first: Int = 10, $genre: String) {
filteredBooks(take: $first, filter: { genre: $genre }) {
title
}
}
Now you can call:
{ } - Uses defaults (first: 10, genre: null)
{ "first": 5 } - Override first, genre still null
{ "genre": "Fantasy" } - Use default first, filter by genre
Aliases - Query Same Field Multiple Times
The Problem
What if you need two different books?
# ERROR: Can't have duplicate field names
query {
bookById(id: 1) { title }
bookById(id: 2) { title }
}
GraphQL doesn't allow duplicate field names in the same selection set!
The Solution: Aliases
Aliases give fields custom names in the response:
# Works! Each field has a unique name
query {
firstBook: bookById(id: 1) { title }
secondBook: bookById(id: 2) { title }
}
Syntax: aliasName: fieldName(args)
Response
{
"data": {
"firstBook": { "title": "1984" },
"secondBook": { "title": "Animal Farm" }
}
}
Practical Use Case: Comparison
Compare two authors' statistics:
query CompareAuthors {
orwell: authorById(id: 1) {
name
bookCount
averageBookRating
}
tolkien: authorById(id: 5) {
name
bookCount
averageBookRating
}
}
You Can Alias Any Field
Even rename nested fields:
query {
bookById(id: 1) {
bookTitle: title # Rename 'title' to 'bookTitle'
writer: author { # Rename 'author' to 'writer'
authorName: name # Rename 'name' to 'authorName'
}
}
}
Fragments - Reusable Field Sets
The Problem: Repetition
When you need the same fields in multiple places:
query {
firstBook: bookById(id: 1) {
id
title
description
publishedYear
price
author { name }
}
secondBook: bookById(id: 2) {
id
title
description
publishedYear
price
author { name }
}
}
That's a lot of copy-paste! What if you need to add a field? You'd have to change it everywhere.
The Solution: Fragments
Fragments are reusable pieces of a query:
# Define the fragment once
fragment BookFields on Book {
id
title
description
publishedYear
price
author { name }
}
# Use it with the spread operator (...)
query {
firstBook: bookById(id: 1) {
...BookFields
}
secondBook: bookById(id: 2) {
...BookFields
}
}
Syntax breakdown:
fragment - Keyword to define a fragment
BookFields - Fragment name (you choose this)
on Book - The type this fragment applies to
...BookFields - Spread the fragment's fields here
Fragment Rules
Must specify a type (on Book) - Fragment fields must exist on that type
Can be nested - Fragments can use other fragments
Can add extra fields - Mix fragments with additional fields
query {
bookById(id: 1) {
...BookFields
reviews { # Extra field not in fragment
rating
}
}
}
Using Fragments in React
// Define fragments as constants for reuse
export const BOOK_FRAGMENT = gql`
fragment BookDetails on Book {
id
title
description
publishedYear
price
}
`;
export const AUTHOR_FRAGMENT = gql`
fragment AuthorDetails on Author {
id
name
biography
country
}
`;
// Use fragments in queries
export const GET_BOOK = gql`
${BOOK_FRAGMENT}
${AUTHOR_FRAGMENT}
query GetBook($id: Int!) {
bookById(id: $id) {
...BookDetails
author {
...AuthorDetails
}
}
}
`;
Why ${FRAGMENT} syntax?
JavaScript template literal interpolation
Includes the fragment definition in the query
Required so Apollo knows what ...BookDetails means
Inline Fragments
For when you don't need reusability:
query {
bookById(id: 1) {
... on Book {
title
price
}
}
}
Inline fragments are mainly used with union types and interfaces (we'll cover these in Part 5).
Directives - Conditional Fields
Directives modify how a query executes. They start with @.
What Are Directives?
Think of them as "if statements" for your query fields:
Built-in Directives
GraphQL has two built-in directives:
| Directive | Effect |
|---|
| @include(if: Boolean!) | Include field when condition is true |
| @skip(if: Boolean!) | Skip field when condition is true |
@include Directive
query GetBook($id: Int!, $includeReviews: Boolean!) {
bookById(id: $id) {
title
description
reviews @include(if: $includeReviews) {
title
rating
}}}
Variables:
{ "id": 1, "includeReviews": true } // Returns reviews
{ "id": 1, "includeReviews": false } // No reviews field at all
@skip Directive
The opposite of @include:
query GetBook($id: Int!, $hidePrice: Boolean!) {
bookById(id: $id) {
title
price @skip(if: $hidePrice)
}}
React Example
function BookDetail({ bookId, showReviews }: Props) {
const { data } = useQuery(GET_BOOK, {
variables: {
id: bookId,
includeReviews: showReviews
},
});
// When showReviews is false, data.bookById.reviews doesn't exist!// So we need to handle that:return (
<div>
<h1>{data.bookById.title}</h1>
{data.bookById.reviews?.map(review => (
<ReviewCard key={review.id} review={review} />
))}
</div>
);
}
HotChocolate's Automatic Filtering
HotChocolate can automatically generate filtering arguments!
Enabling Filtering (Backend)
public class Query
{
[UsePaging(IncludeTotalCount = true)]
[UseFiltering] // This attribute enables filtering
[UseSorting] // This enables sorting
public IQueryable<Book> GetBooks(LibraryDbContext context)
{
return context.Books.AsNoTracking();
}
}
Auto-Generated Filter Syntax
HotChocolate generates a where argument automatically:
query {
books(
where: {
publishedYear: { gte: 1940 }
isAvailable: { eq: true }
}) {
nodes {
title
publishedYear
}}}
Filter Operations
| Operation | Meaning | Example |
|---|
| eq | Equals | { price: { eq: 10 } } |
| neq | Not equals | { genre: { neq: "Horror" } } |
| gt | Greater than | { year: { gt: 2000 } } |
| gte | Greater than or equal | { rating: { gte: 4 } } |
| lt | Less than | { price: { lt: 20 } } |
| lte | Less than or equal | { pages: { lte: 300 } } |
| in | In list | { id: { in: [1, 2, 3] } } |
| nin | Not in list | { genre: { nin: ["Horror", "Romance"] } } |
| contains | String contains | { title: { contains: "war" } } |
| startsWith | String starts with | { title: { startsWith: "The" } } |
| endsWith | String ends with | { isbn: { endsWith: "X" } } |
Combining Filters
AND (all conditions must match):
where: {and: [
{ publishedYear: { gte: 1940 } }
{ publishedYear: { lte: 1960 } }
{ isAvailable: { eq: true } }]}
OR (any condition can match):
where: {or: [
{ genre: { eq: "Fantasy" } }
{ genre: { eq: "Science Fiction" } }]}
Auto-Generated Sorting
query {
books(
order: [
{ publishedYear: DESC }
{ title: ASC }
]) {
nodes {
title
publishedYear
}}}
Search Implementation
Let's implement a flexible search feature.
Backend
public async Task<IEnumerable<Book>> SearchBooks( LibraryDbContext context, string searchTerm){
var term = searchTerm.ToLower();
return await context.Books
.AsNoTracking()
.Where(b =>
b.Title.ToLower().Contains(term) ||
(b.Description != null &&
b.Description.ToLower().Contains(term)) ||
(b.Genre != null &&
b.Genre.ToLower().Contains(term)))
.ToListAsync();
}
GraphQL Query
query SearchBooks($term: String!) {
searchBooks(searchTerm: $term) {
id
title
description
author { name }
}
}
React with Lazy Query
For search, we don't want to run the query immediately. Use useLazyQuery:
import { useLazyQuery } from '@apollo/client';
function SearchBooks() {
const [searchTerm, setSearchTerm] = useState('');
// useLazyQuery returns a function to execute manually
const [executeSearch, { loading, data }] = useLazyQuery(SEARCH_BOOKS);
const handleSearch = () => {
if (searchTerm.trim()) {
executeSearch({ variables: { term: searchTerm } });
}
};
return (
<div>
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search books..."
/>
<button onClick={handleSearch} disabled={loading}>
{loading ? 'Searching...' : 'Search'}
</button>
{data?.searchBooks.map(book => (
<div key={book.id}>{book.title}</div>
))}
</div>
);
}
useQuery vs useLazyQuery:
useQuery - Runs immediately when component mounts
useLazyQuery - Returns a function, you decide when to run it
Query Optimization Tips
1. Only Request What You Need
# ❌ Bad - requesting everything
query {
books {
nodes {
id
title
description
isbn
publishedYear
genre
price
pageCount
isAvailable
createdAt
author { id name biography birthDate country }
reviews { id title content rating reviewerName createdAt }
}
}
}
# ✅ Good - request only what's displayed
query {
books {
nodes {
id
title
author { name }
price
}
}
}
2. Use Variables for Reusability
# ✅ Can be reused for any book
query GetBook($id: Int!) {
bookById(id: $id) { title }
}
3. Use Fragments for Consistency
# ✅ Same fields everywhere, change in one place
fragment BookCard on Book { id title price author { name } }
Key Takeaways
Arguments - Pass parameters to filter/customize queries
Variables - Make queries reusable with dynamic values ($varName: Type)
Aliases - Query same field multiple times (alias: field)
Fragments - Reusable field sets (fragment Name on Type, ...Name)
Directives - Conditional fields (@include, @skip)
HotChocolate Filtering - Auto-generated with [UseFiltering]
useLazyQuery - Execute queries on demand (great for search)
Code: graphql-dotnet-react-from-zero-to-production
What's Next?
In Part 3: Mutations, we'll learn how to:
Create new books and authors
Update existing records
Delete data safely
Handle mutation responses and errors
Implement optimistic updates in React
Exercises
Create a query with variables that fetches books by genre
Use aliases to compare statistics between two categories
Create a fragment for reviews and use it in the book detail query
Implement a query with @include to optionally show author biography
Use HotChocolate filtering to find books priced between $10 and $20
See you in Part 3!