ASP.NET  

GraphQL with .NET & React | Part 2: Query (Arguments, Aliases & Fragments)

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
    }}}
PartWhat it isRequired?
queryOperation typeOptional for queries (default)
GetBooksQueryOperation nameOptional but recommended
booksRoot fieldRequired
{ nodes { title } }Selection setRequired

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:

  • bookById - The field name

  • (id: 1) - Argument in parentheses

  • id - Argument name

  • 1 - Argument value

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

  1. Must specify a type (on Book) - Fragment fields must exist on that type

  2. Can be nested - Fragments can use other fragments

  3. 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:

  • "Include this field IF user wants details"

  • "Skip this field IF user is on mobile"

Built-in Directives

GraphQL has two built-in directives:

DirectiveEffect
@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

OperationMeaningExample
eqEquals{ price: { eq: 10 } }
neqNot equals{ genre: { neq: "Horror" } }
gtGreater than{ year: { gt: 2000 } }
gteGreater than or equal{ rating: { gte: 4 } }
ltLess than{ price: { lt: 20 } }
lteLess than or equal{ pages: { lte: 300 } }
inIn list{ id: { in: [1, 2, 3] } }
ninNot in list{ genre: { nin: ["Horror", "Romance"] } }
containsString contains{ title: { contains: "war" } }
startsWithString starts with{ title: { startsWith: "The" } }
endsWithString 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

  1. Arguments - Pass parameters to filter/customize queries

  2. Variables - Make queries reusable with dynamic values ($varName: Type)

  3. Aliases - Query same field multiple times (alias: field)

  4. Fragments - Reusable field sets (fragment Name on Type, ...Name)

  5. Directives - Conditional fields (@include, @skip)

  6. HotChocolate Filtering - Auto-generated with [UseFiltering]

  7. useLazyQuery - Execute queries on demand (great for search)

  8. 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

  1. Create a query with variables that fetches books by genre

  2. Use aliases to compare statistics between two categories

  3. Create a fragment for reviews and use it in the book detail query

  4. Implement a query with @include to optionally show author biography

  5. Use HotChocolate filtering to find books priced between $10 and $20

See you in Part 3!