Real-Time & API Communication - Principal Engineer Deep Dive
Table of Contents
- SignalR Real-Time Communication
- gRPC with ASP.NET Core
- GraphQL with Hot Chocolate
- WebSocket Direct Implementation
- 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:
- WebSocket (preferred) - Full duplex, lowest latency
- Server-Sent Events (SSE) - Server to client only
- 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:
- WebSocket (preferred) - Full duplex, HTTP/2
- Server-Sent Events - Server to client only, HTTP/1.1
- 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
reservedfor 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:
- Batch multiple requests for related data
- Cache results within a single request
- Defer loading until all resolvers have registered their needs
- 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:
- Detect disconnection (close event, error, timeout)
- Use exponential backoff with jitter for reconnection attempts
- Maintain message queue during disconnection
- Re-subscribe to relevant topics after reconnection
- 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 |