Files
WireMock.Net/copilot/WebSockets/v1/WEBSOCKET_PATTERNS_BEST_PRACTICES.md
Stef Heyenrath a3da39a9ec ws1
2026-02-08 10:30:59 +01:00

20 KiB

WebSocket Design Patterns - Visual Guide & Best Practices

Overview

This document provides visual examples and best practices for using WebSocket support in WireMock.Net following the established fluent interface patterns.


Part 1: Pattern Evolution in WireMock.Net

HTTP Request Matching Pattern

┌─────────────┐
│ Request.    │ Fluent methods return IRequestBuilder
│ Create()    │ for chaining
│             │
├─────────────┤
│ WithPath    │
├─────────────┤
│ WithHeader  │
├─────────────┤
│ UsingGet()  │  Each partial class file handles
├─────────────┤  one concern (path, headers, method)
│ WithParam   │
├─────────────┤
│ WithBody    │
└─────────────┘

HTTP Response Building Pattern

┌──────────────┐
│ Response.    │
│ Create()     │
│              │
├──────────────┤
│ WithStatus   │
├──────────────┤
│ WithHeader   │
├──────────────┤
│ WithBody     │  Returns IResponseBuilder
├──────────────┤  for chaining
│ WithDelay    │
├──────────────┤
│ WithTransf.  │  Transformer for template
├──────────────┤  substitution
│ WithCallback │  Dynamic responses
└──────────────┘

WebSocket Extension Pattern (New)

┌──────────────────────┐
│ Request.Create()     │
├──────────────────────┤
│ WithWebSocketPath    │
├──────────────────────┤
│ WithWebSocketSubprot │  Extends request builder
├──────────────────────┤  with WebSocket-specific
│ WithWebSocketOrigin  │  matching
└──────────────────────┘
           │
           ↓
┌──────────────────────┐
│ Response.Create()    │
├──────────────────────┤
│ WithWebSocket()      │
│ ├─ WithMessage()     │  Fluent builder for
│ ├─ WithJsonMessage() │  composing messages
│ └─ WithTransformer() │
├──────────────────────┤
│ WithWebSocketCallback│  Dynamic message gen
├──────────────────────┤
│ WithWebSocketClose() │  Graceful shutdown
└──────────────────────┘

Part 2: Usage Pattern Comparison

Pattern 1: Static Messages

Analogy: Pre-recorded HTTP responses

// HTTP (existing)
server.Given(Request.Create().WithPath("/api/users"))
    .RespondWith(Response.Create()
        .WithStatusCode(200)
        .WithBodyAsJson(new { id = 1, name = "John" })
    );

// WebSocket (new - sequential messages)
server.Given(Request.Create().WithWebSocketPath("/api/users/stream"))
    .RespondWith(Response.Create()
        .WithWebSocket(ws => ws
            .WithJsonMessage(new { type = "connected" }, delayMs: 0)
            .WithJsonMessage(new { id = 1, name = "John" }, delayMs: 500)
            .WithJsonMessage(new { id = 2, name = "Jane" }, delayMs: 1000)
            .WithClose(1000, "Stream complete")
        )
    );

Pattern 2: Dynamic Content (Request-Based)

Analogy: Response callbacks

// HTTP (existing)
server.Given(Request.Create().WithPath("/api/echo"))
    .RespondWith(Response.Create()
        .WithCallback(request =>
        {
            return new ResponseMessage
            {
                BodyData = new BodyData
                {
                    BodyAsString = $"Echo: {request.Body}",
                    DetectedBodyType = BodyType.String
                },
                StatusCode = 200
            };
        })
    );

// WebSocket (new - message stream from request)
server.Given(Request.Create().WithWebSocketPath("/echo"))
    .RespondWith(Response.Create()
        .WithWebSocketCallback(async request =>
        {
            // Generate messages based on request context
            return new[]
            {
                new WebSocketMessage
                {
                    BodyAsString = $"Echo: {request.Body}",
                    DelayMs = 100,
                    IsText = true
                }
            };
        })
    );

Pattern 3: Templating (Dynamic Values)

Analogy: Handlebars/Scriban transformers

// HTTP (existing)
server.Given(Request.Create().WithPath("/api/user"))
    .RespondWith(Response.Create()
        .WithBodyAsJson(new { 
            username = "{{request.headers.X-User-Name}}",
            timestamp = "{{now}}"
        })
        .WithTransformer()
    );

// WebSocket (new - template in messages)
server.Given(Request.Create().WithWebSocketPath("/notifications"))
    .RespondWith(Response.Create()
        .WithWebSocket(ws => ws
            .WithJsonMessage(new { 
                user = "{{request.headers.X-User-Name}}",
                connected = "{{now}}"
            })
            .WithJsonMessage(new { 
                message = "Hello {{request.headers.X-User-Name}}"
            }, delayMs: 1000)
            .WithTransformer()
        )
    );

Pattern 4: Metadata (Scenario State)

Analogy: Scenario state management

// HTTP (existing)
server.Given(Request.Create().WithPath("/login"))
    .InScenario("UserWorkflow")
    .WillSetStateTo("LoggedIn")
    .RespondWith(Response.Create()
        .WithStatusCode(200)
        .WithBodyAsJson(new { success = true })
    );

// WebSocket (new - state in WebSocket flow)
server.Given(Request.Create().WithWebSocketPath("/chat"))
    .InScenario("ChatSession")
    .WhenStateIs("LoggedIn")
    .WillSetStateTo("ChatActive")
    .RespondWith(Response.Create()
        .WithWebSocket(ws => ws
            .WithJsonMessage(new { type = "welcome" })
            .WithJsonMessage(new { type = "ready" })
        )
    );

Pattern 5: Extensions (Webhooks)

Analogy: Side-effects during request handling

// HTTP (existing) - Trigger external webhook
server.Given(Request.Create().WithPath("/process"))
    .WithWebhook(new Webhook
    {
        Request = new WebhookRequest
        {
            Url = "http://external-service/notify",
            Method = "post",
            BodyData = new BodyData { BodyAsString = "Processing..." }
        }
    })
    .RespondWith(Response.Create().WithStatusCode(200));

// WebSocket (new) - Webhook triggered by connection
server.Given(Request.Create().WithWebSocketPath("/events"))
    .WithWebhook(new Webhook
    {
        Request = new WebhookRequest
        {
            Url = "http://audit-log/event",
            Method = "post",
            BodyData = new BodyData { 
                BodyAsString = "WebSocket connected: {{request.url}}"
            }
        }
    })
    .RespondWith(Response.Create()
        .WithWebSocket(ws => ws.WithMessage("Connected"))
    );

Part 3: Real-World Scenarios

Scenario 1: Real-time Chat Server

// Simulate multiple users joining a chat room
server.Given(Request.Create()
    .WithWebSocketPath("/chat")
    .WithHeader("X-Room-Id", "room123"))
    .InScenario("ChatRoom")
    .RespondWith(Response.Create()
        .WithWebSocket(ws => ws
            .WithJsonMessage(
                new { type = "user-joined", username = "{{request.headers.X-Username}}" },
                delayMs: 0)
            .WithJsonMessage(
                new { type = "message", from = "System", text = "Welcome to room123" },
                delayMs: 500)
            .WithJsonMessage(
                new { type = "users-online", count = 3 },
                delayMs: 1000)
            .WithTransformer()
        )
        .WithWebhook(new Webhook  // Audit log
        {
            Request = new WebhookRequest
            {
                Url = "http://audit/log",
                Method = "post",
                BodyData = new BodyData
                {
                    BodyAsString = "User {{request.headers.X-Username}} joined {{request.headers.X-Room-Id}}"
                }
            }
        })
    );

// Handle user messages (dynamic, simulates echo)
server.Given(Request.Create().WithWebSocketPath("/chat"))
    .RespondWith(Response.Create()
        .WithWebSocketCallback(async request =>
        {
            var username = request.Headers["X-Username"]?.FirstOrDefault() ?? "Anonymous";
            var messageBody = request.Body ?? "";

            return new[]
            {
                new WebSocketMessage
                {
                    BodyAsString = JsonConvert.SerializeObject(new
                    {
                        type = "message-received",
                        from = username,
                        text = messageBody,
                        timestamp = DateTime.UtcNow
                    }),
                    DelayMs = 100
                },
                new WebSocketMessage
                {
                    BodyAsString = JsonConvert.SerializeObject(new
                    {
                        type = "acknowledgment",
                        status = "delivered"
                    }),
                    DelayMs = 200
                }
            };
        })
        .WithWebSocketTransformer()
    );

Scenario 2: Real-time Data Streaming

// Stream stock market data
server.Given(Request.Create()
    .WithWebSocketPath("/market-data")
    .WithWebSocketSubprotocol("market.v1"))
    .WithTitle("Market Data Stream")
    .WithDescription("Real-time stock market prices")
    .RespondWith(Response.Create()
        .WithWebSocketSubprotocol("market.v1")
        .WithWebSocketCallback(async request =>
        {
            var ticker = request.Headers.ContainsKey("X-Ticker") 
                ? request.Headers["X-Ticker"].First() 
                : "AAPL";

            var messages = new List<WebSocketMessage>();
            
            // Initial subscription confirmation
            messages.Add(new WebSocketMessage
            {
                BodyAsString = JsonConvert.SerializeObject(new
                {
                    type = "subscribed",
                    ticker = ticker,
                    timestamp = DateTime.UtcNow
                }),
                DelayMs = 0
            });

            // Simulate price updates
            var random = new Random();
            for (int i = 0; i < 5; i++)
            {
                var price = 150.00m + (decimal)random.NextDouble() * 10;
                messages.Add(new WebSocketMessage
                {
                    BodyAsString = JsonConvert.SerializeObject(new
                    {
                        type = "price-update",
                        ticker = ticker,
                        price = price,
                        timestamp = DateTime.UtcNow
                    }),
                    DelayMs = (i + 1) * 1000  // 1 second between updates
                });
            }

            // Final close message
            messages.Add(new WebSocketMessage
            {
                BodyAsString = JsonConvert.SerializeObject(new
                {
                    type = "stream-end",
                    reason = "Demo ended"
                }),
                DelayMs = 6000
            });

            return messages;
        })
    );

Scenario 3: Server Push Notifications

// Long-lived connection for push notifications
server.Given(Request.Create()
    .WithWebSocketPath("/push")
    .WithHeader("Authorization", new WildcardMatcher("Bearer *")))
    .AtPriority(1)  // Higher priority
    .RespondWith(Response.Create()
        .WithWebSocket(ws => ws
            .WithJsonMessage(new
            {
                type = "authenticated",
                user = "{{request.headers.Authorization}}",
                connectedAt = "{{now}}"
            }, delayMs: 0)
            .WithJsonMessage(new
            {
                type = "notification",
                title = "System Update",
                message = "A new update is available"
            }, delayMs: 3000)
            .WithJsonMessage(new
            {
                type = "notification",
                title = "New Message",
                message = "You have a new message from admin"
            }, delayMs: 6000)
            .WithTransformer()
            .WithClose(1000, "Connection closed by server")
        )
        .WithWebSocketAutoClose(30000)  // Auto-close after 30 seconds if idle
    );

Scenario 4: GraphQL Subscription Simulation

// Simulate GraphQL subscription (persistent query updates)
server.Given(Request.Create()
    .WithWebSocketPath("/graphql")
    .WithWebSocketSubprotocol("graphql-ws")
    .WithHeader("Content-Type", "application/json"))
    .RespondWith(Response.Create()
        .WithWebSocketSubprotocol("graphql-ws")
        .WithWebSocketCallback(async request =>
        {
            var messages = new List<WebSocketMessage>();

            // Parse subscription query from request
            var query = request.Body ?? "{}";

            // Connection ACK
            messages.Add(new WebSocketMessage
            {
                BodyAsString = JsonConvert.SerializeObject(new
                {
                    type = "connection_ack"
                }),
                DelayMs = 0,
                IsText = true
            });

            // Data messages
            for (int i = 0; i < 3; i++)
            {
                messages.Add(new WebSocketMessage
                {
                    BodyAsString = JsonConvert.SerializeObject(new
                    {
                        type = "data",
                        id = "1",
                        payload = new
                        {
                            data = new
                            {
                                userNotifications = new[] { new { id = i, message = $"Update {i}" } }
                            }
                        }
                    }),
                    DelayMs = (i + 1) * 2000,
                    IsText = true
                });
            }

            // Complete
            messages.Add(new WebSocketMessage
            {
                BodyAsString = JsonConvert.SerializeObject(new
                {
                    type = "complete",
                    id = "1"
                }),
                DelayMs = 6000,
                IsText = true
            });

            return messages;
        })
    );

Part 4: Best Practices

DO: Follow Request Matching Patterns

// Good: Follows established request builder pattern
server.Given(Request.Create()
    .WithWebSocketPath("/api/notifications")
    .WithWebSocketSubprotocol("notifications")
    .WithHeader("Authorization", "Bearer *")
)

DON'T: Overload builders with raw configuration

// Bad: Breaks fluent pattern
var req = new Request(...);
req.webSocketSettings = new { ... };

DO: Use callbacks for dynamic behavior

// Good: Dynamic based on request context
.WithWebSocketCallback(async request =>
{
    var userId = request.Headers["X-User-Id"].First();
    return GetMessagesForUser(userId);
})

DON'T: Mix static and dynamic in same mapping

// Bad: Confusing multiple patterns
.WithWebSocket(ws => ws.WithMessage("Static"))
.WithWebSocketCallback(async r => new[] { ... })  // Which wins?

DO: Use transformers for templating

// Good: Dynamic values via templates
.WithJsonMessage(new 
{ 
    userId = "{{request.headers.X-User-Id}}"
})
.WithTransformer()

DON'T: Hardcode request values

// Bad: Doesn't adapt to different requests
.WithJsonMessage(new { userId = "hardcoded-user-123" })

DO: Set appropriate delays for realistic simulation

// Good: Simulates realistic network latency
.WithJsonMessage(msg1, delayMs: 0)       // Immediate
.WithJsonMessage(msg2, delayMs: 500)     // 500ms later
.WithJsonMessage(msg3, delayMs: 2000)    // 2 seconds later

DON'T: Use excessively long delays

// Bad: Test hangs unnecessarily
.WithJsonMessage(msg, delayMs: 60000)  // 1 minute?

DO: Use subprotocol negotiation for versioning

// Good: Version the API
.WithWebSocketPath("/api")
.WithWebSocketSubprotocol("api.v2")

DON'T: Embed version in path alone

// Bad: Less testable for version negotiation
.WithWebSocketPath("/api/v2")

DO: Chain metadata methods logically

// Good: Clear order (matching → metadata → response)
server.Given(Request.Create().WithWebSocketPath("/api"))
    .AtPriority(1)
    .WithTitle("WebSocket API")
    .InScenario("ActiveConnections")
    .WithWebhook(...)
    .RespondWith(Response.Create()...);

DO: Test both happy path and error scenarios

// Connection accepted
server.Given(Request.Create().WithWebSocketPath("/api").WithHeader("Auth", "*"))
    .RespondWith(Response.Create().WithWebSocket(...));

// Connection rejected
server.Given(Request.Create().WithWebSocketPath("/api").WithHeader("Auth", "invalid"))
    .RespondWith(Response.Create().WithStatusCode(401));

Part 5: Fluent Chain Examples

Example 1: Minimal Setup

server.Given(Request.Create()
    .WithWebSocketPath("/ws"))
    .RespondWith(Response.Create()
        .WithWebSocketMessage("Connected")
    );
server.Given(Request.Create()
    .WithWebSocketPath("/api/events")
    .WithWebSocketSubprotocol("events.v1")
    .WithHeader("Authorization", "Bearer *")
    .WithHeader("X-Client-Id", "*")
)
    .AtPriority(10)
    .WithTitle("Event Stream API")
    .WithDescription("Real-time event streaming for client ID")
    .InScenario("EventStreaming")
    .WhenStateIs("Connected")
    .WillSetStateTo("StreamActive")
    .WithWebhook(new Webhook
    {
        Request = new WebhookRequest
        {
            Url = "http://audit/connections",
            Method = "post",
            BodyData = new BodyData
            {
                BodyAsString = "Client {{request.headers.X-Client-Id}} connected"
            }
        }
    })
    .RespondWith(Response.Create()
        .WithWebSocketSubprotocol("events.v1")
        .WithWebSocket(ws => ws
            .WithJsonMessage(new
            {
                type = "connected",
                clientId = "{{request.headers.X-Client-Id}}",
                timestamp = "{{now}}"
            }, delayMs: 0)
            .WithJsonMessage(new
            {
                type = "status",
                status = "ready"
            }, delayMs: 100)
            .WithTransformer()
            .WithClose(1000, "Graceful shutdown")
        )
        .WithWebSocketAutoClose(300000)  // 5 minute timeout
    );

Summary

The WebSocket fluent interface design:

  1. Extends, not replaces existing request/response builders
  2. Follows established patterns (partial classes, method chaining)
  3. Enables composition (messages, transformers, callbacks)
  4. Maintains readability (clear fluent chains)
  5. Supports testing (realistic delays, state, scenarios)
  6. Integrates seamlessly (webhooks, priority, metadata)

This ensures developers have a consistent, intuitive API for mocking WebSocket behavior.