Real-Time & API Communication - Quick Reference Cheatsheet
Principal Engineer Interview Quick Reference - SignalR, gRPC, GraphQL, WebSockets
1. SignalR
Core Concepts
Hub = Server-side endpoint for client communication
Connection = Single client WebSocket/SSE/Long-Polling connection
Group = Named collection of connections for targeted messaging
Basic Hub Setup
// Hub definition
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
public async Task JoinRoom(string roomName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, roomName);
}
public override async Task OnConnectedAsync()
{
await base.OnConnectedAsync();
// Connection lifecycle handling
}
}
// Registration
builder.Services.AddSignalR();
app.MapHub<ChatHub>("/chatHub");
Client Targeting Options
// All clients
await Clients.All.SendAsync("Method", data);
// Specific client
await Clients.Client(connectionId).SendAsync("Method", data);
// Group
await Clients.Group("roomName").SendAsync("Method", data);
// Caller only
await Clients.Caller.SendAsync("Method", data);
// All except caller
await Clients.Others.SendAsync("Method", data);
// Specific user (by ClaimTypes.NameIdentifier)
await Clients.User(userId).SendAsync("Method", data);
Strongly Typed Hub
public interface IChatClient
{
Task ReceiveMessage(string user, string message);
Task UserJoined(string user);
}
public class ChatHub : Hub<IChatClient>
{
public async Task SendMessage(string user, string message)
{
await Clients.All.ReceiveMessage(user, message); // Compile-time safety
}
}
Scaling with Redis Backplane
builder.Services.AddSignalR()
.AddStackExchangeRedis("localhost:6379", options =>
{
options.Configuration.ChannelPrefix = RedisChannel.Literal("MyApp");
});
2. gRPC
Service Definition (.proto)
syntax = "proto3";
option csharp_namespace = "MyApp.Grpc";
service ProductService {
rpc GetProduct (ProductRequest) returns (ProductResponse); // Unary
rpc GetProducts (ProductsRequest) returns (stream ProductResponse); // Server streaming
rpc CreateProducts (stream ProductRequest) returns (ProductsResponse); // Client streaming
rpc SyncProducts (stream ProductRequest) returns (stream ProductResponse); // Bidirectional
}
message ProductRequest { int32 id = 1; }
message ProductResponse { int32 id = 1; string name = 2; double price = 3; }
Server Implementation
public class ProductServiceImpl : ProductService.ProductServiceBase
{
// Unary
public override async Task<ProductResponse> GetProduct(
ProductRequest request, ServerCallContext context)
{
var product = await _repo.GetByIdAsync(request.Id);
return new ProductResponse { Id = product.Id, Name = product.Name };
}
// Server streaming
public override async Task GetProducts(ProductsRequest request,
IServerStreamWriter<ProductResponse> responseStream, ServerCallContext context)
{
await foreach (var product in _repo.GetAllAsync(context.CancellationToken))
{
await responseStream.WriteAsync(new ProductResponse { ... });
}
}
}
// Registration
builder.Services.AddGrpc();
app.MapGrpcService<ProductServiceImpl>();
Client Usage
// Setup
var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new ProductService.ProductServiceClient(channel);
// Unary call
var response = await client.GetProductAsync(new ProductRequest { Id = 1 });
// Server streaming
var call = client.GetProducts(new ProductsRequest());
await foreach (var product in call.ResponseStream.ReadAllAsync())
{
Console.WriteLine(product.Name);
}
gRPC vs REST Comparison
| Aspect | gRPC | REST |
|---|---|---|
| Protocol | HTTP/2 | HTTP/1.1 or HTTP/2 |
| Format | Protobuf (binary) | JSON (text) |
| Contract | .proto (strict) | OpenAPI (optional) |
| Streaming | Native support | Limited (SSE) |
| Browser | Requires gRPC-Web | Native |
| Performance | ~10x faster | Baseline |
3. GraphQL (Hot Chocolate)
Schema Definition
// Query type
public class Query
{
public IQueryable<Product> GetProducts([Service] AppDbContext db)
=> db.Products;
public async Task<Product?> GetProduct(int id, [Service] AppDbContext db)
=> await db.Products.FindAsync(id);
}
// Mutation type
public class Mutation
{
public async Task<Product> CreateProduct(CreateProductInput input,
[Service] AppDbContext db)
{
var product = new Product { Name = input.Name, Price = input.Price };
db.Products.Add(product);
await db.SaveChangesAsync();
return product;
}
}
// Registration
builder.Services
.AddGraphQLServer()
.AddQueryType<Query>()
.AddMutationType<Mutation>()
.AddFiltering()
.AddSorting()
.AddProjections();
app.MapGraphQL();
DataLoader (N+1 Prevention)
public class ProductByCategoryDataLoader : BatchDataLoader<int, List<Product>>
{
protected override async Task<IReadOnlyDictionary<int, List<Product>>> LoadBatchAsync(
IReadOnlyList<int> categoryIds, CancellationToken ct)
{
var products = await _db.Products
.Where(p => categoryIds.Contains(p.CategoryId))
.ToListAsync(ct);
return products.GroupBy(p => p.CategoryId)
.ToDictionary(g => g.Key, g => g.ToList());
}
}
Query Examples
# Simple query
query {
products {
id
name
price
}
}
# With filtering and sorting
query {
products(where: { price: { gt: 10 } }, order: { name: ASC }) {
id
name
}
}
# Mutation
mutation {
createProduct(input: { name: "Widget", price: 29.99 }) {
id
name
}
}
Security Configuration
builder.Services.AddGraphQLServer()
.AddMaxExecutionDepthRule(10) // Prevent deep queries
.AddQueryComplexityValidator(1000) // Limit complexity
.UsePersistedQueryPipeline() // Only allow pre-approved queries
.AddAuthorization();
4. WebSockets (Native)
Server Setup
app.UseWebSockets(new WebSocketOptions
{
KeepAliveInterval = TimeSpan.FromSeconds(30)
});
app.Map("/ws", async context =>
{
if (!context.WebSockets.IsWebSocketRequest)
{
context.Response.StatusCode = 400;
return;
}
using var ws = await context.WebSockets.AcceptWebSocketAsync();
await HandleWebSocket(ws, context.RequestAborted);
});
async Task HandleWebSocket(WebSocket ws, CancellationToken ct)
{
var buffer = new byte[4096];
while (ws.State == WebSocketState.Open)
{
var result = await ws.ReceiveAsync(buffer, ct);
if (result.MessageType == WebSocketMessageType.Close)
break;
// Echo back
await ws.SendAsync(buffer.AsMemory(0, result.Count),
result.MessageType, result.EndOfMessage, ct);
}
}
Client with Reconnection
public class RobustWebSocketClient : IAsyncDisposable
{
private ClientWebSocket? _ws;
private int _reconnectAttempt = 0;
public async Task ConnectAsync(Uri uri, CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
try
{
_ws = new ClientWebSocket();
await _ws.ConnectAsync(uri, ct);
_reconnectAttempt = 0;
return;
}
catch (WebSocketException)
{
var delay = Math.Min(30000, 1000 * Math.Pow(2, _reconnectAttempt++));
await Task.Delay((int)delay, ct);
}
}
}
}
Quick Decision Tree
Technology Selection
Need real-time bidirectional? β SignalR (simplest) or WebSocket (more control)
Internal microservices? β gRPC (performance)
Flexible client queries? β GraphQL
Simple API? β REST (still the default)
Browser + .NET both? β SignalR or REST
High throughput streaming? β gRPC
SignalR Transport Selection
Client supports WebSocket? β WebSocket (best performance)
WebSocket blocked? β Server-Sent Events
SSE not available? β Long Polling (fallback)
(SignalR negotiates automatically)
Interview Quick Answers
Q: When to use SignalR vs raw WebSocket?
SignalR for rapid development with automatic reconnection, groups, and transport fallback. Raw WebSocket when you need full control or custom protocol.
Q: gRPC vs REST for microservices?
gRPC for internal service-to-service (~10x faster, streaming, strong contracts). REST for public APIs (universal browser support, human-readable).
Q: How to prevent N+1 in GraphQL?
Use DataLoader to batch requests. Instead of N separate database calls, batch into single call with WHERE IN clause.
Q: SignalR scaling strategy?
Use Redis backplane for multiple server instances. Sticky sessions OR backplane required for load-balanced deployments.
Q: gRPC streaming types?
Unary (1:1), Server streaming (1:N), Client streaming (N:1), Bidirectional (N:N). Each has specific use cases.
Common Patterns
SignalR Groups Pattern
// Join on connect
public override async Task OnConnectedAsync()
{
var userId = Context.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
await Groups.AddToGroupAsync(Context.ConnectionId, $"user:{userId}");
}
// Broadcast to group
public async Task SendToUser(string userId, string message)
{
await Clients.Group($"user:{userId}").SendAsync("Receive", message);
}
gRPC Interceptor Pattern
public class LoggingInterceptor : Interceptor
{
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request, ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
var sw = Stopwatch.StartNew();
try
{
return await continuation(request, context);
}
finally
{
_logger.LogInformation("{Method} completed in {Ms}ms",
context.Method, sw.ElapsedMilliseconds);
}
}
}
Gotchas
- SignalR: Connection ID changes on reconnect - use User for stable identity
- gRPC: Requires HTTP/2 - doesnβt work through some proxies without configuration
- GraphQL: N+1 problem is default - always implement DataLoaders
- WebSocket: No automatic reconnection - must implement retry logic
- SignalR Scaling: Without backplane, messages only reach clients on same server
Last updated: December 2024 | .NET 8