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 one series to take you from GraphQL basics to advanced patterns.
Part 1: Foundations & Library Backend
Part 2: Query (Arguments, Aliases & Fragments)
Part 3: Mutations (Create, Update & Delete)
Part 5: Advanced Patterns (Pagination, Errors, JWT & Testing)
Part 4: Real-Time Data with Web Sockets
Welcome to Part 4! We've learned to read data with queries and modify it with mutations. Now comes one of GraphQL's most powerful features: subscriptions for real-time updates.
Where We Left Off
Our Library app can now:
Browse and search books
View detailed information with nested data
Add books, authors, and reviews
But there's a problem: if someone adds a review, other users don't see it until they refresh. That's not a great experience! Let's fix it with subscriptions.
What Are Subscriptions?
Subscriptions are a GraphQL operation type that maintains a persistent connection between client and server. When specific events happen, the server pushes data to all connected clients.
Analogy: Newsletter vs Checking the Website
| Traditional (Polling) | Subscriptions |
|---|
| Visit website every hour to check for updates | Subscribe to newsletter |
| You do the work | Updates come to you |
| Wasteful if nothing changed | Efficient - only notified when there's news |
| Might miss time-sensitive updates | Instant notification |
Real-World Use Cases
Chat applications - New messages appear instantly
Live sports scores - Updates as games progress
Stock prices - Real-time market data
Notifications - Alerts, mentions, system events
Collaborative editing - See others' changes live
Our Library app - See new reviews without refreshing
How Subscriptions Work
The Technology: WebSockets
Regular HTTP requests follow a pattern:
Client sends request
Server sends response
Connection closes
Subscriptions need persistent connections - the connection stays open so the server can push data anytime. This is where WebSockets come in.
WebSockets:
Open a connection that stays alive
Two-way communication (client ↔ server)
Server can send data anytime without client asking
More efficient than constant polling
The Flow
1. Client opens WebSocket connection to /graphql
2. Client sends subscription query
3. Server acknowledges subscription
4. Connection stays open...
[Later, when something happens:]
5. Server pushes data through the connection
6. Client receives and displays update
7. Repeat steps 5-6 for each event
GraphQL Subscription Protocol
GraphQL subscriptions use the graphql-ws protocol (sometimes called graphql-transport-ws).
Backend Setup
WebSocket Configuration
First, enable WebSockets in your ASP.NET Core app:
// Program.cs
var app = builder.Build();
// IMPORTANT: This must come BEFORE MapGraphQL!
app.UseWebSockets();
app.MapGraphQL();
app.Run();
Configure HotChocolate for Subscriptions
// Program.cs
builder.Services
.AddGraphQLServer()
.AddQueryType<Query>()
.AddMutationType<Mutation>()
.AddSubscriptionType<Subscription>() // Add subscription type
.AddInMemorySubscriptions(); // Store subscription state
What is AddInMemorySubscriptions()?
It configures where subscription state is stored. Options:
AddInMemorySubscriptions() - Single server, stores in memory (good for development)
AddRedisSubscriptions(...) - Multiple servers, stores in Redis (production scale)
For our learning project, in-memory is perfect.
Creating a Subscription Class
// GraphQL/Subscriptions/Subscription.cs
using HotChocolate;
using HotChocolate.Execution;
using HotChocolate.Subscriptions;
using HotChocolate.Types;
using LibraryApi.Models;
namespace LibraryApi.GraphQL.Subscriptions;
public class Subscription
{
/// <summary>
/// Subscribe to receive notifications when any new book is added
/// </summary>
[Subscribe] // Marks this as a subscription resolver
[Topic(nameof(OnBookAdded))] // The topic/channel name
public Book OnBookAdded(
[EventMessage] Book book) // Data received from the event
{
return book;
}
}
Breaking it down:
| Attribute | Purpose |
|---|
| [Subscribe] | Tells HotChocolate this is a subscription |
| [Topic] | The "channel" name for this subscription |
| [EventMessage] | Injects the data that was published |
Publishing Events from Mutations
When a book is added, we publish to the subscription:
// In Mutation.cspublic async Task<AddBookPayload> AddBook(
LibraryDbContext context,
[Service] ITopicEventSender eventSender, // Injected service for publishing
AddBookInput input)
{
// ... create book logic ...
context.Books.Add(book);
await context.SaveChangesAsync();
// Publish event to all subscribers
// Topic name must match the [Topic] attribute in Subscription class
await eventSender.SendAsync(nameof(Subscription.OnBookAdded), book);
return new AddBookPayload(book, null);
}
How ITopicEventSender works:
SendAsync(topicName, payload) - Sends data to a topic
All clients subscribed to that topic receive the payload
Think of it like a pub/sub message queue
Dynamic Topics (Book-Specific Subscriptions)
What if you only want updates for a specific book's reviews?
Backend Implementation
[Subscribe]
[Topic("OnReviewAdded_{bookId}")] // Dynamic topic with parameter
public Review OnReviewAdded(
int bookId, // Subscription argument
[EventMessage] Review review)
{
return review;
}
The {bookId} placeholder is replaced with the actual value when subscribing.
Publishing to Dynamic Topic
public async Task<AddReviewPayload> AddReview(
LibraryDbContext context,
[Service] ITopicEventSender eventSender,
AddReviewInput input)
{
// ... create review logic ...
// Dynamic topic: includes the book ID
await eventSender.SendAsync($"OnReviewAdded_{input.BookId}", review);
return new AddReviewPayload(review, null);
}
Subscribing to Specific Book
subscription WatchBookReviews($bookId: Int!) {
onReviewAdded(bookId: $bookId) {
id
title
rating
reviewerName
}
}
Client A watching book #1 only gets reviews for book #1. Client B watching book #2 only gets reviews for book #2.
Frontend Setup
Understanding Apollo Link Architecture
Apollo Client uses a link chain to process GraphQL operations. Think of it like middleware:
Query/Mutation → HttpLink → Server
Subscription → WebSocketLink → Server
We need to tell Apollo: "Use WebSocket for subscriptions, HTTP for everything else."
Installing Dependencies
npm install graphql-ws
What is graphql-ws?
It's a library that implements the GraphQL over WebSocket protocol. It handles:
Connection management
Message serialization
Reconnection logic
Protocol compliance
Configuring Apollo Client
// src/graphql/client.ts
import {
ApolloClient,
InMemoryCache,
HttpLink,
split // For routing queries vs subscriptions
} from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
import { getMainDefinition } from '@apollo/client/utilities';
// HTTP Link for queries and mutations
const httpLink = new HttpLink({
uri: 'http://localhost:5000/graphql',
});
// WebSocket Link for subscriptions
const wsLink = new GraphQLWsLink(
createClient({
url: 'ws://localhost:5000/graphql', // Note: 'ws://' not 'http://'
// Optional: Configure reconnection
retryAttempts: 5,
// Optional: Handle connection events
on: {
connected: () => console.log('WebSocket connected'),
closed: () => console.log('WebSocket closed'),
error: (err) => console.error('WebSocket error:', err),
},
})
);
// Split: Route to correct link based on operation type
const splitLink = split(
({ query }) => {
// Get the operation type from the query
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription' // If subscription → use wsLink
);
},
wsLink, // If true (subscription) → WebSocket
httpLink // If false (query/mutation) → HTTP
);
// Create client with the split link
export const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache(),
});
Understanding split():
Takes a condition function
Returns first link if true, second link if false
Essential for using both HTTP and WebSocket
Using Subscriptions in React
The useSubscription Hook
Apollo provides useSubscription for subscribing to events:
import { useSubscription, gql } from '@apollo/client';
const ON_BOOK_ADDED = gql`
subscription OnBookAdded {
onBookAdded {
id
title
author {
name
}
}
}
`;
function BookNotifications() {
const { data, loading, error } = useSubscription(ON_BOOK_ADDED);
// loading is true only initially while connecting
if (loading) return <p>Connecting to notifications...</p>;
if (error) return <p>Subscription error: {error.message}</p>;
// data contains the LATEST event (overwrites previous)
if (data) {
return (
<div className="notification">
New book added: {data.onBookAdded.title}
</div>
);
}
return <p>Waiting for new books...</p>;
}
Important: data only contains the most recent event, not a history.
Accumulating Events
To keep track of all events, maintain local state:
function AllBookNotifications() {
const [newBooks, setNewBooks] = useState<Book[]>([]);
const { error } = useSubscription(ON_BOOK_ADDED, {
// onData is called for EACH event
onData: ({ data }) => {
const book = data.data?.onBookAdded;
if (book) {
setNewBooks(prev => [book, ...prev]); // Add to beginning
}
}
});
return (
<div>
<h3>New Books ({newBooks.length})</h3>
{newBooks.map(book => (
<div key={book.id}>📚 {book.title}</div>
))}
</div>
);
}
Subscription with Variables
For book-specific reviews:
const ON_REVIEW_ADDED = gql`
subscription OnReviewAdded($bookId: Int!) {
onReviewAdded(bookId: $bookId) {
id
title
rating
reviewerName
}
}
`;
function BookReviewsLive({ bookId }: { bookId: number }) {
const [reviews, setReviews] = useState<Review[]>([]);
useSubscription(ON_REVIEW_ADDED, {
variables: { bookId },
onData: ({ data }) => {
const review = data.data?.onReviewAdded;
if (review) {
setReviews(prev => [...prev, review]);
}
}
});
return (
<div>
<h4>Live Reviews</h4>
{reviews.map(review => (
<div key={review.id}>
⭐ {review.rating}/5 - {review.title} by {review.reviewerName}
</div>
))}
</div>
);
}
Combining Query + Subscription
A common pattern: load initial data with a query, then subscribe for updates:
function BookDetail({ bookId }: { bookId: number }) {
// Query: Get initial data
const { data: queryData, loading } = useQuery(GET_BOOK_BY_ID, {
variables: { id: bookId }
});
// State: Store all reviews
const [reviews, setReviews] = useState<Review[]>([]);
// Initialize reviews from query
useEffect(() => {
if (queryData?.bookById?.reviews) {
setReviews(queryData.bookById.reviews);
}
}, [queryData]);
// Subscription: Get new reviews in real-time
useSubscription(ON_REVIEW_ADDED, {
variables: { bookId },
onData: ({ data }) => {
const newReview = data.data?.onReviewAdded;
if (newReview) {
// Add to local state (avoids duplicates with ID check)
setReviews(prev => {
if (prev.some(r => r.id === newReview.id)) return prev;
return [...prev, newReview];
});
}
}
});
if (loading) return <p>Loading...</p>;
return (
<div>
<h1>{queryData.bookById.title}</h1>
<h3>Reviews ({reviews.length})</h3>
{reviews.map(review => (
<ReviewCard key={review.id} review={review} />
))}
</div>
);
}
Practical Example: Global New Book Banner
Let's add a banner that shows when any new book is added:
// components/NewBookBanner.tsx
import { useState, useEffect } from 'react';
import { useSubscription } from '@apollo/client';
import { ON_BOOK_ADDED } from '../graphql/subscriptions';
import type { Book } from '../types';
function NewBookBanner() {
const [newBook, setNewBook] = useState<Book | null>(null);
const [visible, setVisible] = useState(false);
useSubscription(ON_BOOK_ADDED, {
onData: ({ data }) => {
const book = data.data?.onBookAdded;
if (book) {
setNewBook(book);
setVisible(true);
}
}
});
// Auto-hide after 5 seconds
useEffect(() => {
if (visible) {
const timer = setTimeout(() => setVisible(false), 5000);
return () => clearTimeout(timer);
}
}, [visible, newBook]);
if (!visible || !newBook) return null;
return (
<div className="new-book-banner">
<span> New book added: </span>
<strong>{newBook.title}</strong>
{newBook.author && <span> by {newBook.author.name}</span>}
<button onClick={() => setVisible(false)}>×</button>
</div>
);
}
// CSS for the banner
const styles = `
.new-book-banner {
position: fixed;
top: 20px;
right: 20px;
background: #4caf50;
color: white;
padding: 16px 24px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
animation: slideIn 0.3s ease;
z-index: 1000;
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
`;
Connection Management
Handling Disconnections
WebSocket connections can drop. Handle reconnection gracefully:
// In client.ts
const wsLink = new GraphQLWsLink(
createClient({
url: 'ws://localhost:5000/graphql',
retryAttempts: 10, // Retry up to 10 times
retryWait: async (retries) => {
// Exponential backoff: 1s, 2s, 4s, 8s...
await new Promise(resolve =>
setTimeout(resolve, Math.min(1000 * Math.pow(2, retries), 30000))
);
},
})
);
Connection Status Component
Show users when they're disconnected:
function ConnectionStatus() {
const [connected, setConnected] = useState(true);
// Note: This requires custom setup with graphql-ws client
// The actual implementation depends on your client configuration
if (connected) return null;
return (
<div className="connection-warning">
⚠️ Connection lost. Trying to reconnect...
</div>
);
}
Subscription vs Polling
When to Use Subscriptions
Use subscriptions for:
Events that require immediate user action
Chat messages, notifications
Collaborative features (multiple users editing)
Frequently changing data that users are actively watching
When to Use Polling Instead
Sometimes polling (periodic queries) is simpler:
const { data } = useQuery(GET_BOOKS, {
pollInterval: 10000, // Refetch every 10 seconds
});
Use polling for:
Dashboards with periodic updates
Data that doesn't need instant updates
When WebSocket infrastructure is complex
As a fallback when subscriptions aren't available
Comparison
| Aspect | Subscriptions | Polling |
|---|
| Latency | Instant | Up to poll interval |
| Server load | Lower (event-based) | Higher (constant requests) |
| Complexity | Higher (WebSocket setup) | Lower |
| Offline handling | Requires reconnection logic | Automatic with HTTP |
Testing Subscriptions
Manual Testing
Open the app in two browser windows
In window A: Navigate to book list
In window B: Add a new book
Watch window A: Should show notification!
Using Banana Cake Pop
HotChocolate's built-in IDE supports subscriptions:
Go to http://localhost:5000/graphql
Run a subscription query:
subscription {
onBookAdded {
id
title
}}
Open another tab and run an addBook mutation
Watch the subscription tab receive the event!
Key Takeaways
Subscriptions - Real-time updates via persistent connections
WebSockets - Technology enabling two-way communication
Topics - Channels that group related events
ITopicEventSender - Backend service for publishing events
[Subscribe] / [Topic] - HotChocolate attributes for subscriptions
useSubscription - Apollo React hook for subscribing
split - Routes subscriptions to WebSocket, others to HTTP
Dynamic topics - OnReviewAdded_{bookId} for filtered subscriptions
Code: graphql-dotnet-react-from-zero-to-production
What's Next?
In Part 5: Advanced Topics, we'll cover:
Cursor-based pagination
Error handling strategies
Authentication and authorization
Performance optimization with DataLoaders
Testing GraphQL APIs
Exercises
Add a subscription for when a book is deleted
Implement a live "users online" counter
Create a subscription that notifies when book availability changes
Add sound/visual notification when new events arrive
Implement reconnection indicator UI
See you in Part 5!