πŸ“„

Realtime Communication Guide

Intermediate 5 min read 900 words

Real-Time & API Communication - Principal Engineer Deep Dive

Table of Contents

  1. SignalR Real-Time Communication
  2. gRPC with ASP.NET Core
  3. GraphQL with Hot Chocolate
  4. WebSocket Direct Implementation
  5. Interview Questions & Answers

1. SignalR Real-Time Communication

1.1 SignalR Architecture

SignalR is a library that simplifies adding real-time web functionality to applications, enabling server-side code to push content to connected clients instantly.

[INTERNALS] How SignalR Works:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        SignalR Architecture                    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                               β”‚
β”‚   Client                          Server                      β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”            β”‚
β”‚   β”‚ SignalR  β”‚ ←──WebSocket───→  β”‚     Hub      β”‚            β”‚
β”‚   β”‚  Client  β”‚ ←──SSE────────→  β”‚   (Server)   β”‚            β”‚
β”‚   β”‚          β”‚ ←──LongPolling─→ β”‚              β”‚            β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜            β”‚
β”‚                                         β”‚                     β”‚
β”‚                                         ↓                     β”‚
β”‚                                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”            β”‚
β”‚                                  β”‚  Backplane   β”‚            β”‚
β”‚                                  β”‚(Redis/Azure) β”‚            β”‚
β”‚                                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜            β”‚
β”‚                                         β”‚                     β”‚
β”‚                               β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”‚
β”‚                               ↓                   ↓           β”‚
β”‚                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚
β”‚                        β”‚ Server 2 β”‚        β”‚ Server N β”‚      β”‚
β”‚                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Transport Negotiation Order:

  1. WebSocket (preferred) - Full duplex, lowest latency
  2. Server-Sent Events (SSE) - Server to client only
  3. Long Polling - Fallback for older browsers

[CODE] Basic SignalR Setup:

// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSignalR(options =>
{
    options.EnableDetailedErrors = builder.Environment.IsDevelopment();
    options.MaximumReceiveMessageSize = 64 * 1024; // 64KB
    options.StreamBufferCapacity = 10;
    options.KeepAliveInterval = TimeSpan.FromSeconds(15);
    options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
    options.HandshakeTimeout = TimeSpan.FromSeconds(15);
});

var app = builder.Build();

app.MapHub<ChatHub>("/chatHub");

app.Run();

// ChatHub.cs
public class ChatHub : Hub
{
    private readonly ILogger<ChatHub> _logger;

    public ChatHub(ILogger<ChatHub> logger)
    {
        _logger = logger;
    }

    // Called when a client connects
    public override async Task OnConnectedAsync()
    {
        _logger.LogInformation("Client connected: {ConnectionId}", Context.ConnectionId);
        await Clients.All.SendAsync("UserConnected", Context.ConnectionId);
        await base.OnConnectedAsync();
    }

    // Called when a client disconnects
    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        _logger.LogInformation("Client disconnected: {ConnectionId}", Context.ConnectionId);
        await Clients.All.SendAsync("UserDisconnected", Context.ConnectionId);
        await base.OnDisconnectedAsync(exception);
    }

    // Hub method - callable from clients
    public async Task SendMessage(string user, string message)
    {
        _logger.LogInformation("Message from {User}: {Message}", user, message);

        // Send to all connected clients
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }

    // Send to specific user
    public async Task SendPrivateMessage(string userId, string message)
    {
        await Clients.User(userId).SendAsync("ReceivePrivateMessage", message);
    }

    // Send to caller only
    public async Task Echo(string message)
    {
        await Clients.Caller.SendAsync("Echo", message);
    }

    // Send to all except caller
    public async Task Broadcast(string message)
    {
        await Clients.Others.SendAsync("Broadcast", message);
    }
}

1.2 Groups and User Management

[CODE] Group Management:

public class GroupChatHub : Hub
{
    public async Task JoinGroup(string groupName)
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
        await Clients.Group(groupName).SendAsync("UserJoined", Context.ConnectionId, groupName);
    }

    public async Task LeaveGroup(string groupName)
    {
        await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
        await Clients.Group(groupName).SendAsync("UserLeft", Context.ConnectionId, groupName);
    }

    public async Task SendToGroup(string groupName, string message)
    {
        await Clients.Group(groupName).SendAsync("ReceiveGroupMessage", message);
    }

    // Send to multiple groups
    public async Task SendToGroups(IReadOnlyList<string> groupNames, string message)
    {
        await Clients.Groups(groupNames).SendAsync("ReceiveMessage", message);
    }

    // Exclude specific connections
    public async Task SendToGroupExcept(string groupName, string message, IReadOnlyList<string> excludedConnections)
    {
        await Clients.GroupExcept(groupName, excludedConnections).SendAsync("ReceiveMessage", message);
    }
}

// User-based messaging (requires authentication)
public class AuthenticatedHub : Hub
{
    // Maps connection to user identity
    public override async Task OnConnectedAsync()
    {
        var userId = Context.User?.FindFirstValue(ClaimTypes.NameIdentifier);
        if (userId != null)
        {
            // User ID is automatically mapped to connection
            await Clients.User(userId).SendAsync("Connected");
        }
        await base.OnConnectedAsync();
    }

    // Send to user by their identity (works across multiple connections)
    public async Task SendToUser(string userId, string message)
    {
        await Clients.User(userId).SendAsync("ReceiveMessage", message);
    }

    // Send to multiple users
    public async Task SendToUsers(IReadOnlyList<string> userIds, string message)
    {
        await Clients.Users(userIds).SendAsync("ReceiveMessage", message);
    }
}

// Custom user ID provider
public class CustomUserIdProvider : IUserIdProvider
{
    public string? GetUserId(HubConnectionContext connection)
    {
        // Custom logic to determine user ID
        return connection.User?.FindFirstValue("custom_user_id")
            ?? connection.User?.FindFirstValue(ClaimTypes.NameIdentifier);
    }
}

// Register in DI
builder.Services.AddSingleton<IUserIdProvider, CustomUserIdProvider>();

1.3 Streaming

[CODE] Server-to-Client Streaming:

public class StreamingHub : Hub
{
    // Server streaming - returns IAsyncEnumerable
    public async IAsyncEnumerable<int> Counter(
        int count,
        int delay,
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        for (int i = 0; i < count; i++)
        {
            cancellationToken.ThrowIfCancellationRequested();
            yield return i;
            await Task.Delay(delay, cancellationToken);
        }
    }

    // Alternative: ChannelReader
    public ChannelReader<int> CounterChannel(
        int count,
        int delay,
        CancellationToken cancellationToken)
    {
        var channel = Channel.CreateUnbounded<int>();

        _ = WriteToChannel(channel.Writer, count, delay, cancellationToken);

        return channel.Reader;
    }

    private async Task WriteToChannel(
        ChannelWriter<int> writer,
        int count,
        int delay,
        CancellationToken cancellationToken)
    {
        try
        {
            for (int i = 0; i < count; i++)
            {
                cancellationToken.ThrowIfCancellationRequested();
                await writer.WriteAsync(i, cancellationToken);
                await Task.Delay(delay, cancellationToken);
            }
        }
        catch (Exception ex)
        {
            writer.Complete(ex);
            return;
        }

        writer.Complete();
    }
}

// Client-to-Server Streaming
public class UploadHub : Hub
{
    // Receives stream from client
    public async Task UploadStream(ChannelReader<string> stream)
    {
        while (await stream.WaitToReadAsync())
        {
            while (stream.TryRead(out var item))
            {
                Console.WriteLine($"Received: {item}");
            }
        }
    }

    // IAsyncEnumerable version
    public async Task UploadStreamAsync(IAsyncEnumerable<string> stream)
    {
        await foreach (var item in stream)
        {
            Console.WriteLine($"Received: {item}");
        }
    }
}

1.4 SignalR at Scale

[PRODUCTION] Redis Backplane:

// Program.cs
builder.Services.AddSignalR()
    .AddStackExchangeRedis(builder.Configuration.GetConnectionString("Redis")!, options =>
    {
        options.Configuration.ChannelPrefix = RedisChannel.Literal("MyApp:");
    });

// Azure SignalR Service (managed)
builder.Services.AddSignalR()
    .AddAzureSignalR(options =>
    {
        options.ConnectionString = builder.Configuration["Azure:SignalR:ConnectionString"];
        options.ServerStickyMode = ServerStickyMode.Required;
    });

[CODE] Strongly Typed Hubs:

// Define client interface
public interface IChatClient
{
    Task ReceiveMessage(string user, string message);
    Task UserConnected(string connectionId);
    Task UserDisconnected(string connectionId);
}

// Strongly typed hub
public class ChatHub : Hub<IChatClient>
{
    public override async Task OnConnectedAsync()
    {
        // Compile-time safety
        await Clients.All.UserConnected(Context.ConnectionId);
        await base.OnConnectedAsync();
    }

    public async Task SendMessage(string user, string message)
    {
        // No magic strings
        await Clients.All.ReceiveMessage(user, message);
    }
}

1.5 SignalR Security

[SECURITY] Authentication:

// JWT Authentication for SignalR
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
        };

        // SignalR sends token in query string
        options.Events = new JwtBearerEvents
        {
            OnMessageReceived = context =>
            {
                var accessToken = context.Request.Query["access_token"];

                var path = context.HttpContext.Request.Path;
                if (!string.IsNullOrEmpty(accessToken) &&
                    path.StartsWithSegments("/chatHub"))
                {
                    context.Token = accessToken;
                }

                return Task.CompletedTask;
            }
        };
    });

// Protected hub
[Authorize]
public class SecureChatHub : Hub
{
    public string GetUserId()
    {
        return Context.User?.FindFirstValue(ClaimTypes.NameIdentifier)
            ?? throw new HubException("User not authenticated");
    }

    [Authorize(Roles = "Admin")]
    public async Task AdminBroadcast(string message)
    {
        await Clients.All.SendAsync("AdminMessage", message);
    }

    [Authorize(Policy = "PremiumUser")]
    public async Task PremiumFeature(string data)
    {
        await Clients.Caller.SendAsync("PremiumResult", data);
    }
}

2. gRPC with ASP.NET Core

2.1 gRPC Fundamentals

gRPC is a high-performance, cross-platform RPC framework using Protocol Buffers for serialization.

[CODE] Proto File Definition:

// Protos/greet.proto
syntax = "proto3";

option csharp_namespace = "GrpcService";

package greet;

// Service definition
service Greeter {
    // Unary RPC
    rpc SayHello (HelloRequest) returns (HelloReply);

    // Server streaming
    rpc SayHelloStream (HelloRequest) returns (stream HelloReply);

    // Client streaming
    rpc SayHelloClientStream (stream HelloRequest) returns (HelloReply);

    // Bidirectional streaming
    rpc SayHelloBidirectional (stream HelloRequest) returns (stream HelloReply);
}

// Messages
message HelloRequest {
    string name = 1;
    int32 age = 2;
    repeated string tags = 3;
}

message HelloReply {
    string message = 1;
    google.protobuf.Timestamp timestamp = 2;
}

// Import for Timestamp
import "google/protobuf/timestamp.proto";

[CODE] gRPC Service Implementation:

// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddGrpc(options =>
{
    options.EnableDetailedErrors = builder.Environment.IsDevelopment();
    options.MaxReceiveMessageSize = 4 * 1024 * 1024; // 4MB
    options.MaxSendMessageSize = 4 * 1024 * 1024;
});

var app = builder.Build();

app.MapGrpcService<GreeterService>();

app.Run();

// GreeterService.cs
public class GreeterService : Greeter.GreeterBase
{
    private readonly ILogger<GreeterService> _logger;

    public GreeterService(ILogger<GreeterService> logger)
    {
        _logger = logger;
    }

    // Unary RPC
    public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
    {
        _logger.LogInformation("Received greeting request for: {Name}", request.Name);

        return Task.FromResult(new HelloReply
        {
            Message = $"Hello, {request.Name}!",
            Timestamp = Timestamp.FromDateTime(DateTime.UtcNow)
        });
    }

    // Server streaming
    public override async Task SayHelloStream(
        HelloRequest request,
        IServerStreamWriter<HelloReply> responseStream,
        ServerCallContext context)
    {
        for (int i = 0; i < 10; i++)
        {
            if (context.CancellationToken.IsCancellationRequested)
                break;

            await responseStream.WriteAsync(new HelloReply
            {
                Message = $"Hello {request.Name} #{i}",
                Timestamp = Timestamp.FromDateTime(DateTime.UtcNow)
            });

            await Task.Delay(500, context.CancellationToken);
        }
    }

    // Client streaming
    public override async Task<HelloReply> SayHelloClientStream(
        IAsyncStreamReader<HelloRequest> requestStream,
        ServerCallContext context)
    {
        var names = new List<string>();

        await foreach (var request in requestStream.ReadAllAsync(context.CancellationToken))
        {
            names.Add(request.Name);
        }

        return new HelloReply
        {
            Message = $"Hello to all: {string.Join(", ", names)}",
            Timestamp = Timestamp.FromDateTime(DateTime.UtcNow)
        };
    }

    // Bidirectional streaming
    public override async Task SayHelloBidirectional(
        IAsyncStreamReader<HelloRequest> requestStream,
        IServerStreamWriter<HelloReply> responseStream,
        ServerCallContext context)
    {
        await foreach (var request in requestStream.ReadAllAsync(context.CancellationToken))
        {
            await responseStream.WriteAsync(new HelloReply
            {
                Message = $"Hello, {request.Name}!",
                Timestamp = Timestamp.FromDateTime(DateTime.UtcNow)
            });
        }
    }
}

2.2 gRPC Client

[CODE] gRPC Client Implementation:

// Program.cs (Client)
builder.Services.AddGrpcClient<Greeter.GreeterClient>(options =>
{
    options.Address = new Uri("https://localhost:5001");
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
    var handler = new HttpClientHandler();
    // Allow self-signed certificates in development
    if (builder.Environment.IsDevelopment())
    {
        handler.ServerCertificateCustomValidationCallback =
            HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
    }
    return handler;
});

// Usage in service
public class GreetingService
{
    private readonly Greeter.GreeterClient _client;

    public GreetingService(Greeter.GreeterClient client)
    {
        _client = client;
    }

    // Unary call
    public async Task<string> GreetAsync(string name)
    {
        var reply = await _client.SayHelloAsync(new HelloRequest { Name = name });
        return reply.Message;
    }

    // Server streaming
    public async IAsyncEnumerable<string> GreetStreamAsync(
        string name,
        [EnumeratorCancellation] CancellationToken ct = default)
    {
        using var call = _client.SayHelloStream(new HelloRequest { Name = name });

        await foreach (var reply in call.ResponseStream.ReadAllAsync(ct))
        {
            yield return reply.Message;
        }
    }

    // Client streaming
    public async Task<string> GreetManyAsync(IEnumerable<string> names)
    {
        using var call = _client.SayHelloClientStream();

        foreach (var name in names)
        {
            await call.RequestStream.WriteAsync(new HelloRequest { Name = name });
        }

        await call.RequestStream.CompleteAsync();

        var reply = await call;
        return reply.Message;
    }

    // Bidirectional streaming
    public async Task GreetBidirectionalAsync(
        IAsyncEnumerable<string> names,
        Func<string, Task> onReply,
        CancellationToken ct = default)
    {
        using var call = _client.SayHelloBidirectional();

        // Write task
        var writeTask = Task.Run(async () =>
        {
            await foreach (var name in names.WithCancellation(ct))
            {
                await call.RequestStream.WriteAsync(new HelloRequest { Name = name });
            }
            await call.RequestStream.CompleteAsync();
        }, ct);

        // Read task
        await foreach (var reply in call.ResponseStream.ReadAllAsync(ct))
        {
            await onReply(reply.Message);
        }

        await writeTask;
    }
}

2.3 gRPC Interceptors

[CODE] Server and Client Interceptors:

// Server interceptor
public class ServerLoggingInterceptor : Interceptor
{
    private readonly ILogger<ServerLoggingInterceptor> _logger;

    public ServerLoggingInterceptor(ILogger<ServerLoggingInterceptor> logger)
    {
        _logger = logger;
    }

    public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
        TRequest request,
        ServerCallContext context,
        UnaryServerMethod<TRequest, TResponse> continuation)
    {
        var sw = Stopwatch.StartNew();

        _logger.LogInformation(
            "gRPC call started: {Method}",
            context.Method);

        try
        {
            var response = await continuation(request, context);

            _logger.LogInformation(
                "gRPC call completed: {Method} in {ElapsedMs}ms",
                context.Method,
                sw.ElapsedMilliseconds);

            return response;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex,
                "gRPC call failed: {Method} after {ElapsedMs}ms",
                context.Method,
                sw.ElapsedMilliseconds);
            throw;
        }
    }
}

// Client interceptor
public class ClientLoggingInterceptor : Interceptor
{
    private readonly ILogger<ClientLoggingInterceptor> _logger;

    public ClientLoggingInterceptor(ILogger<ClientLoggingInterceptor> logger)
    {
        _logger = logger;
    }

    public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
        TRequest request,
        ClientInterceptorContext<TRequest, TResponse> context,
        AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
    {
        _logger.LogInformation("gRPC client calling: {Method}", context.Method);

        var call = continuation(request, context);

        return new AsyncUnaryCall<TResponse>(
            HandleResponse(call.ResponseAsync, context.Method.FullName),
            call.ResponseHeadersAsync,
            call.GetStatus,
            call.GetTrailers,
            call.Dispose);
    }

    private async Task<TResponse> HandleResponse<TResponse>(
        Task<TResponse> responseTask,
        string method)
    {
        try
        {
            var response = await responseTask;
            _logger.LogInformation("gRPC call succeeded: {Method}", method);
            return response;
        }
        catch (RpcException ex)
        {
            _logger.LogError(ex, "gRPC call failed: {Method}, Status: {Status}", method, ex.Status);
            throw;
        }
    }
}

// Registration
builder.Services.AddGrpc(options =>
{
    options.Interceptors.Add<ServerLoggingInterceptor>();
});

builder.Services.AddGrpcClient<Greeter.GreeterClient>(options =>
{
    options.Address = new Uri("https://localhost:5001");
})
.AddInterceptor<ClientLoggingInterceptor>();

2.4 gRPC vs REST Comparison

[BENCHMARK] Performance Comparison:

Aspect gRPC REST (JSON)
Serialization Protocol Buffers (binary) JSON (text)
Payload Size ~30% smaller Baseline
Serialization Speed 3-10x faster Baseline
Streaming Native (4 types) Limited (SSE, chunked)
Contract Strongly typed (.proto) OpenAPI (optional)
Browser Support gRPC-Web required Native
HTTP Version HTTP/2 required HTTP/1.1+
Tooling Code generation Manual/generators

[PRODUCTION] When to Use Each:

Use gRPC When Use REST When
Microservices communication Public APIs
Low latency requirements Browser clients
Streaming needed Simplicity preferred
Polyglot environments HTTP/1.1 only environments
Internal APIs Human-readable debugging

3. GraphQL with Hot Chocolate

3.1 GraphQL Implementation

[CODE] Hot Chocolate Setup:

// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddGraphQLServer()
    .AddQueryType<Query>()
    .AddMutationType<Mutation>()
    .AddSubscriptionType<Subscription>()
    .AddType<ProductType>()
    .AddFiltering()
    .AddSorting()
    .AddProjections();

var app = builder.Build();

app.MapGraphQL();

app.Run();

// Types
public class Query
{
    [UseProjection]
    [UseFiltering]
    [UseSorting]
    public IQueryable<Product> GetProducts([Service] AppDbContext context)
    {
        return context.Products;
    }

    public async Task<Product?> GetProductById(
        int id,
        [Service] AppDbContext context,
        CancellationToken ct)
    {
        return await context.Products
            .Include(p => p.Category)
            .FirstOrDefaultAsync(p => p.Id == id, ct);
    }
}

public class Mutation
{
    public async Task<Product> CreateProduct(
        CreateProductInput input,
        [Service] AppDbContext context,
        CancellationToken ct)
    {
        var product = new Product
        {
            Name = input.Name,
            Price = input.Price,
            CategoryId = input.CategoryId
        };

        context.Products.Add(product);
        await context.SaveChangesAsync(ct);

        return product;
    }

    public async Task<Product?> UpdateProduct(
        int id,
        UpdateProductInput input,
        [Service] AppDbContext context,
        CancellationToken ct)
    {
        var product = await context.Products.FindAsync(new object[] { id }, ct);

        if (product == null)
            return null;

        if (input.Name != null) product.Name = input.Name;
        if (input.Price.HasValue) product.Price = input.Price.Value;

        await context.SaveChangesAsync(ct);

        return product;
    }
}

public class Subscription
{
    [Subscribe]
    [Topic("ProductCreated")]
    public Product ProductCreated([EventMessage] Product product) => product;
}

// Input types
public record CreateProductInput(string Name, decimal Price, int CategoryId);
public record UpdateProductInput(string? Name, decimal? Price);

// Custom type with resolvers
public class ProductType : ObjectType<Product>
{
    protected override void Configure(IObjectTypeDescriptor<Product> descriptor)
    {
        descriptor.Field(p => p.Id).Type<NonNullType<IdType>>();
        descriptor.Field(p => p.Name).Type<NonNullType<StringType>>();

        // Computed field
        descriptor.Field("isExpensive")
            .Type<BooleanType>()
            .Resolve(ctx => ctx.Parent<Product>().Price > 100);

        // Related data with DataLoader
        descriptor.Field(p => p.Category)
            .ResolveWith<ProductResolvers>(r => r.GetCategory(default!, default!, default!));
    }
}

public class ProductResolvers
{
    public async Task<Category?> GetCategory(
        [Parent] Product product,
        CategoryBatchDataLoader dataLoader,
        CancellationToken ct)
    {
        return await dataLoader.LoadAsync(product.CategoryId, ct);
    }
}

3.2 DataLoader Pattern (N+1 Prevention)

[CODE] DataLoader Implementation:

// DataLoaders prevent N+1 queries
public class CategoryBatchDataLoader : BatchDataLoader<int, Category>
{
    private readonly IDbContextFactory<AppDbContext> _contextFactory;

    public CategoryBatchDataLoader(
        IDbContextFactory<AppDbContext> contextFactory,
        IBatchScheduler batchScheduler,
        DataLoaderOptions? options = null)
        : base(batchScheduler, options)
    {
        _contextFactory = contextFactory;
    }

    protected override async Task<IReadOnlyDictionary<int, Category>> LoadBatchAsync(
        IReadOnlyList<int> keys,
        CancellationToken ct)
    {
        await using var context = await _contextFactory.CreateDbContextAsync(ct);

        var categories = await context.Categories
            .Where(c => keys.Contains(c.Id))
            .ToDictionaryAsync(c => c.Id, ct);

        return categories;
    }
}

// Group DataLoader for one-to-many relationships
public class ProductsByCategoryDataLoader : GroupedDataLoader<int, Product>
{
    private readonly IDbContextFactory<AppDbContext> _contextFactory;

    public ProductsByCategoryDataLoader(
        IDbContextFactory<AppDbContext> contextFactory,
        IBatchScheduler batchScheduler,
        DataLoaderOptions? options = null)
        : base(batchScheduler, options)
    {
        _contextFactory = contextFactory;
    }

    protected override async Task<ILookup<int, Product>> LoadGroupedBatchAsync(
        IReadOnlyList<int> keys,
        CancellationToken ct)
    {
        await using var context = await _contextFactory.CreateDbContextAsync(ct);

        var products = await context.Products
            .Where(p => keys.Contains(p.CategoryId))
            .ToListAsync(ct);

        return products.ToLookup(p => p.CategoryId);
    }
}

// Registration
builder.Services
    .AddGraphQLServer()
    .AddDataLoader<CategoryBatchDataLoader>()
    .AddDataLoader<ProductsByCategoryDataLoader>();

3.3 GraphQL Security

[SECURITY] Query Complexity and Depth Limiting:

builder.Services
    .AddGraphQLServer()
    .AddQueryType<Query>()
    // Limit query depth
    .AddMaxExecutionDepthRule(10)
    // Custom complexity calculation
    .AddOperationComplexityAnalyzer((options, type, field, cost) =>
    {
        // Lists are more expensive
        if (type.IsListType())
        {
            return cost * 10;
        }
        return cost;
    }, maximumAllowedComplexity: 1000)
    // Timeout
    .ModifyRequestOptions(options =>
    {
        options.ExecutionTimeout = TimeSpan.FromSeconds(30);
    })
    // Introspection control
    .AddIntrospectionAllowedRule()
    .ModifyOptions(options =>
    {
        options.EnableIntrospection = builder.Environment.IsDevelopment();
    });

// Authentication
builder.Services
    .AddGraphQLServer()
    .AddAuthorization()
    .AddQueryType<Query>();

// In type definitions
public class Query
{
    [Authorize]
    public IQueryable<Product> GetProducts([Service] AppDbContext context)
        => context.Products;

    [Authorize(Roles = new[] { "Admin" })]
    public IQueryable<User> GetUsers([Service] AppDbContext context)
        => context.Users;
}

4. WebSocket Direct Implementation

4.1 Native WebSocket API

[CODE] WebSocket Middleware:

// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<WebSocketHandler>();

var app = builder.Build();

app.UseWebSockets(new WebSocketOptions
{
    KeepAliveInterval = TimeSpan.FromSeconds(30)
});

app.Map("/ws", async context =>
{
    if (context.WebSockets.IsWebSocketRequest)
    {
        var handler = context.RequestServices.GetRequiredService<WebSocketHandler>();
        using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
        await handler.HandleAsync(webSocket, context.RequestAborted);
    }
    else
    {
        context.Response.StatusCode = StatusCodes.Status400BadRequest;
    }
});

app.Run();

// WebSocketHandler.cs
public class WebSocketHandler
{
    private readonly ConcurrentDictionary<string, WebSocket> _connections = new();
    private readonly ILogger<WebSocketHandler> _logger;

    public WebSocketHandler(ILogger<WebSocketHandler> logger)
    {
        _logger = logger;
    }

    public async Task HandleAsync(WebSocket webSocket, CancellationToken ct)
    {
        var connectionId = Guid.NewGuid().ToString();
        _connections.TryAdd(connectionId, webSocket);

        _logger.LogInformation("WebSocket connected: {ConnectionId}", connectionId);

        try
        {
            await ProcessMessagesAsync(connectionId, webSocket, ct);
        }
        finally
        {
            _connections.TryRemove(connectionId, out _);
            _logger.LogInformation("WebSocket disconnected: {ConnectionId}", connectionId);
        }
    }

    private async Task ProcessMessagesAsync(
        string connectionId,
        WebSocket webSocket,
        CancellationToken ct)
    {
        var buffer = new byte[4096];
        var messageBuffer = new List<byte>();

        while (webSocket.State == WebSocketState.Open && !ct.IsCancellationRequested)
        {
            try
            {
                var result = await webSocket.ReceiveAsync(buffer, ct);

                if (result.MessageType == WebSocketMessageType.Close)
                {
                    await webSocket.CloseAsync(
                        WebSocketCloseStatus.NormalClosure,
                        "Closed by client",
                        ct);
                    break;
                }

                messageBuffer.AddRange(buffer.Take(result.Count));

                if (result.EndOfMessage)
                {
                    var message = Encoding.UTF8.GetString(messageBuffer.ToArray());
                    messageBuffer.Clear();

                    await HandleMessageAsync(connectionId, webSocket, message, ct);
                }
            }
            catch (WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely)
            {
                _logger.LogWarning("WebSocket closed prematurely: {ConnectionId}", connectionId);
                break;
            }
        }
    }

    private async Task HandleMessageAsync(
        string connectionId,
        WebSocket webSocket,
        string message,
        CancellationToken ct)
    {
        _logger.LogDebug("Message from {ConnectionId}: {Message}", connectionId, message);

        // Echo back
        var response = Encoding.UTF8.GetBytes($"Echo: {message}");
        await webSocket.SendAsync(
            response,
            WebSocketMessageType.Text,
            true,
            ct);
    }

    public async Task BroadcastAsync(string message, CancellationToken ct = default)
    {
        var data = Encoding.UTF8.GetBytes(message);

        var tasks = _connections.Values
            .Where(ws => ws.State == WebSocketState.Open)
            .Select(ws => ws.SendAsync(data, WebSocketMessageType.Text, true, ct));

        await Task.WhenAll(tasks);
    }

    public async Task SendToAsync(string connectionId, string message, CancellationToken ct = default)
    {
        if (_connections.TryGetValue(connectionId, out var webSocket) &&
            webSocket.State == WebSocketState.Open)
        {
            var data = Encoding.UTF8.GetBytes(message);
            await webSocket.SendAsync(data, WebSocketMessageType.Text, true, ct);
        }
    }
}

4.2 Production WebSocket Patterns

[CODE] Robust WebSocket Client:

public class ResilientWebSocketClient : IAsyncDisposable
{
    private ClientWebSocket? _webSocket;
    private readonly Uri _uri;
    private readonly ILogger<ResilientWebSocketClient> _logger;
    private readonly CancellationTokenSource _cts = new();
    private readonly Channel<string> _sendChannel = Channel.CreateUnbounded<string>();
    private int _reconnectAttempts;
    private const int MaxReconnectAttempts = 10;

    public event Func<string, Task>? OnMessage;
    public event Func<Task>? OnConnected;
    public event Func<Task>? OnDisconnected;

    public ResilientWebSocketClient(Uri uri, ILogger<ResilientWebSocketClient> logger)
    {
        _uri = uri;
        _logger = logger;
    }

    public async Task ConnectAsync(CancellationToken ct = default)
    {
        using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct, _cts.Token);

        while (!linkedCts.Token.IsCancellationRequested)
        {
            try
            {
                _webSocket = new ClientWebSocket();
                await _webSocket.ConnectAsync(_uri, linkedCts.Token);

                _reconnectAttempts = 0;
                _logger.LogInformation("WebSocket connected to {Uri}", _uri);

                if (OnConnected != null)
                    await OnConnected.Invoke();

                // Start send/receive loops
                var receiveTask = ReceiveLoopAsync(linkedCts.Token);
                var sendTask = SendLoopAsync(linkedCts.Token);

                await Task.WhenAny(receiveTask, sendTask);

                // One completed, cancel the other
                break;
            }
            catch (Exception ex) when (!linkedCts.Token.IsCancellationRequested)
            {
                _logger.LogError(ex, "WebSocket connection failed");

                if (_reconnectAttempts >= MaxReconnectAttempts)
                {
                    _logger.LogError("Max reconnection attempts reached");
                    throw;
                }

                // Exponential backoff with jitter
                var delay = TimeSpan.FromSeconds(Math.Pow(2, _reconnectAttempts))
                    + TimeSpan.FromMilliseconds(Random.Shared.Next(0, 1000));

                _reconnectAttempts++;
                _logger.LogInformation(
                    "Reconnecting in {Delay}s (attempt {Attempt}/{Max})",
                    delay.TotalSeconds,
                    _reconnectAttempts,
                    MaxReconnectAttempts);

                await Task.Delay(delay, linkedCts.Token);
            }
        }

        if (OnDisconnected != null)
            await OnDisconnected.Invoke();
    }

    private async Task ReceiveLoopAsync(CancellationToken ct)
    {
        var buffer = new byte[4096];
        var messageBuffer = new List<byte>();

        while (_webSocket?.State == WebSocketState.Open && !ct.IsCancellationRequested)
        {
            var result = await _webSocket.ReceiveAsync(buffer, ct);

            if (result.MessageType == WebSocketMessageType.Close)
            {
                await _webSocket.CloseAsync(
                    WebSocketCloseStatus.NormalClosure,
                    "Closed by server",
                    ct);
                break;
            }

            messageBuffer.AddRange(buffer.Take(result.Count));

            if (result.EndOfMessage)
            {
                var message = Encoding.UTF8.GetString(messageBuffer.ToArray());
                messageBuffer.Clear();

                if (OnMessage != null)
                    await OnMessage.Invoke(message);
            }
        }
    }

    private async Task SendLoopAsync(CancellationToken ct)
    {
        await foreach (var message in _sendChannel.Reader.ReadAllAsync(ct))
        {
            if (_webSocket?.State == WebSocketState.Open)
            {
                var data = Encoding.UTF8.GetBytes(message);
                await _webSocket.SendAsync(data, WebSocketMessageType.Text, true, ct);
            }
        }
    }

    public async Task SendAsync(string message)
    {
        await _sendChannel.Writer.WriteAsync(message);
    }

    public async ValueTask DisposeAsync()
    {
        _cts.Cancel();

        if (_webSocket?.State == WebSocketState.Open)
        {
            await _webSocket.CloseAsync(
                WebSocketCloseStatus.NormalClosure,
                "Client closing",
                CancellationToken.None);
        }

        _webSocket?.Dispose();
        _cts.Dispose();
    }
}

// Usage
var client = new ResilientWebSocketClient(
    new Uri("wss://example.com/ws"),
    logger);

client.OnMessage += async message =>
{
    Console.WriteLine($"Received: {message}");
    await Task.CompletedTask;
};

client.OnConnected += async () =>
{
    Console.WriteLine("Connected!");
    await client.SendAsync("Hello!");
};

await client.ConnectAsync();

5. Interview Questions & Answers

SignalR

Q: What happens when WebSocket is not available? A: SignalR automatically falls back to other transports:

  1. WebSocket (preferred) - Full duplex, HTTP/2
  2. Server-Sent Events - Server to client only, HTTP/1.1
  3. Long Polling - Simulates real-time with repeated HTTP requests

The negotiation happens automatically, client and server agree on the best available transport.

Q: How do you scale SignalR across multiple servers? A: Use a backplane:

  • Redis: Most common, pub/sub for message distribution
  • Azure SignalR Service: Managed solution, handles scaling automatically
  • SQL Server: For smaller deployments

The backplane ensures messages reach clients regardless of which server they’re connected to.

Q: What’s the difference between Hub groups and user targeting? A:

  • Groups: Logical groupings you manage (join/leave). One connection can be in multiple groups.
  • Users: Based on authenticated identity. A user can have multiple connections (multiple browsers/devices) that all receive the message.

gRPC

Q: When would you choose gRPC over REST? A: Choose gRPC for:

  • Internal microservices communication
  • Streaming requirements (real-time data)
  • Performance-critical paths (3-10x faster serialization)
  • Strongly typed contracts are important
  • Polyglot environments (same .proto across languages)

Choose REST for:

  • Public APIs (better tooling, familiarity)
  • Browser clients without proxy
  • Simple CRUD operations
  • When human readability matters

Q: How do you handle versioning in gRPC? A: Protocol Buffers support evolution:

  • Add new fields (use new field numbers)
  • Don’t change field numbers or types
  • Use reserved for removed fields
  • Create new service versions for breaking changes
  • Can maintain multiple versions simultaneously

GraphQL

Q: How do you prevent the N+1 query problem in GraphQL? A: Use DataLoaders:

  1. Batch multiple requests for related data
  2. Cache results within a single request
  3. Defer loading until all resolvers have registered their needs
  4. Then execute a single optimized query

Example: Instead of 100 separate queries for 100 product categories, DataLoader batches into one query.

Q: How do you protect against malicious queries? A: Multiple strategies:

  • Depth limiting: Prevent deeply nested queries
  • Complexity analysis: Assign costs to fields, reject expensive queries
  • Timeouts: Kill long-running queries
  • Persisted queries: Only allow pre-approved queries in production
  • Rate limiting: Per-client query limits

WebSockets

Q: How do you handle WebSocket reconnection? A: Implement robust reconnection logic:

  1. Detect disconnection (close event, error, timeout)
  2. Use exponential backoff with jitter for reconnection attempts
  3. Maintain message queue during disconnection
  4. Re-subscribe to relevant topics after reconnection
  5. Consider message acknowledgment for reliability

Q: What’s the difference between WebSocket and SignalR? A: SignalR is an abstraction over WebSockets that provides:

  • Automatic transport fallback (WebSocket β†’ SSE β†’ Long Polling)
  • Connection management and reconnection
  • Hub-based messaging patterns
  • Groups and user targeting
  • Strongly typed hubs

WebSockets are raw bidirectional communication - you implement everything yourself.


Summary

Technology Best For Latency Browser Support
SignalR Real-time web apps, notifications Low Excellent
gRPC Microservices, streaming Very Low gRPC-Web required
GraphQL Flexible querying, mobile apps Medium Excellent
WebSocket Custom real-time protocols Low Good

Decision Matrix:

Requirement Recommended
Real-time notifications to web SignalR
Microservice-to-microservice gRPC
Mobile app with varying bandwidth GraphQL
Full control over protocol WebSocket
Streaming large data gRPC or WebSocket
Browser-first, simple setup SignalR
Multiple client types (mobile, web, desktop) GraphQL or gRPC