πŸ“„

Realtime Api Cheatsheet

Intermediate 2 min read 300 words

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

  1. SignalR: Connection ID changes on reconnect - use User for stable identity
  2. gRPC: Requires HTTP/2 - doesn’t work through some proxies without configuration
  3. GraphQL: N+1 problem is default - always implement DataLoaders
  4. WebSocket: No automatic reconnection - must implement retry logic
  5. SignalR Scaling: Without backplane, messages only reach clients on same server

Last updated: December 2024 | .NET 8