# WebSocket Support in WireMock.Net - Fluent Interface Design Proposal ## Executive Summary This document analyzes the WireMock.Net architecture and proposes a fluent interface design for WebSocket support in the `WireMock.Net.Minimal` project, following the established patterns in the codebase. --- ## Part 1: Current Architecture Analysis ### 1.1 Project Structure **Core Projects:** - **WireMock.Net.Abstractions**: Defines interfaces and abstract models (no implementation) - **WireMock.Net.Minimal**: Core implementation with full fluent interface support - **WireMock.Net.StandAlone**: OWIN self-hosting wrapper - **WireMock.Net**: Full-featured version (extends Minimal) ### 1.2 Fluent Interface Pattern Overview The fluent interface is built on three primary components working together: #### **A. Request Builder Pattern** (`RequestBuilders/Request*.cs` files) ```csharp // Entry point var requestBuilder = Request.Create() .WithPath("/api/users") .UsingGet() .WithHeader("Authorization", "Bearer token"); ``` **Key Characteristics:** - Partial class `Request` with multiple `Request.WithXxx.cs` files - Each file focuses on a specific concern (Path, Headers, Params, etc.) - Implements `IRequestBuilder` interface - Returns `this` (IRequestBuilder) for chaining - Uses composition: `Request : RequestMessageCompositeMatcher, IRequestBuilder` #### **B. Response Builder Pattern** (`ResponseBuilders/Response*.cs` files) ```csharp // Fluent response building Response.Create() .WithStatusCode(200) .WithHeader("Content-Type", "application/json") .WithBodyAsJson(new { id = 1, name = "John" }) .WithDelay(TimeSpan.FromSeconds(1)) .WithTransformer() ``` **Key Characteristics:** - Partial class `Response` with separate files for features - Methods return `IResponseBuilder` (returns `this`) - Supports both sync and async callbacks via `WithCallback()` - Pluggable transformers (Handlebars, Scriban) - Examples: - `Response.WithCallback.cs`: Sync/async request handlers - `Response.WithTransformer.cs`: Template transformation - `Response.WithProxy.cs`: HTTP proxying - `Response.WithFault.cs`: Simulated faults #### **C. Mapping Builder Pattern** (`MappingBuilder.cs` + `RespondWithAProvider.cs`) ```csharp server.Given(Request.Create().WithPath("/endpoint")) .AtPriority(1) .WithTitle("My Endpoint") .InScenario("User Workflow") .WhenStateIs("LoggedIn") .WillSetStateTo("DataFetched") .WithWebhook(new Webhook { ... }) .RespondWith(Response.Create().WithBody("response")) ``` **Key Characteristics:** - `MappingBuilder.Given()` returns `IRespondWithAProvider` - `RespondWithAProvider` chains metadata (priority, scenario, webhooks) - Terminal method: `RespondWith(IResponseProvider)` or `ThenRespondWith()` - Fluent methods return `IRespondWithAProvider` for chaining - Example webhook support shows the pattern for extensions ### 1.3 Key Design Patterns Used | Pattern | Location | Purpose | |---------|----------|---------| | **Partial Classes** | `Response.cs`, `Request.cs` | Separation of concerns while maintaining fluent interface | | **Builder Pattern** | `RequestBuilders/`, `ResponseBuilders/` | Incremental construction | | **Composite Pattern** | `RequestMessageCompositeMatcher` | Composable matchers | | **Interface Segregation** | `IResponseBuilder`, `IRequestBuilder` | Contract definition | | **Fluent API** | All builders | Method chaining | | **Extension Methods** | Various `*.cs` partial files | Feature addition without breaking changes | --- ## Part 2: WebSocket Support Design ### 2.1 Architecture for WebSocket Support WebSocket support should follow a similar pattern to existing features. The key difference is that WebSockets are **bidirectional** and **stateful**, requiring: 1. **Request matching** (connection phase) 2. **Message routing** (message handling) 3. **State management** (connection state) 4. **Simulated server messages** (push messages) ### 2.2 Proposed Model Classes (WireMock.Net.Abstractions) Create new interfaces in `WireMock.Net.Abstractions`: ```csharp // File: Admin/Mappings/WebSocketModel.cs namespace WireMock.Admin.Mappings; public class WebSocketMessageModel { public int? DelayMs { get; set; } public string? BodyAsString { get; set; } public byte[]? BodyAsBytes { get; set; } public bool IsText { get; set; } = true; } public class WebSocketResponseModel { public List Messages { get; set; } = new(); public bool UseTransformer { get; set; } public TransformerType TransformerType { get; set; } = TransformerType.Handlebars; public string? CloseMessage { get; set; } public int? CloseCode { get; set; } } ``` ### 2.3 Domain Models (WireMock.Net.Minimal) Create new model in `Models/`: ```csharp // File: src/WireMock.Net.Minimal/Models/WebSocketMessage.cs namespace WireMock.Models; public class WebSocketMessage : IWebSocketMessage { public int DelayMs { get; set; } public string? BodyAsString { get; set; } public byte[]? BodyAsBytes { get; set; } public bool IsText { get; set; } = true; } // File: src/WireMock.Net.Minimal/Models/WebSocketResponse.cs namespace WireMock.Models; public class WebSocketResponse : IWebSocketResponse { public List Messages { get; set; } = new(); public bool UseTransformer { get; set; } public TransformerType TransformerType { get; set; } = TransformerType.Handlebars; public string? CloseMessage { get; set; } public int? CloseCode { get; set; } } ``` ### 2.4 Request Builder Extension (WireMock.Net.Minimal) Create partial class to extend request matching for WebSockets: ```csharp // File: src/WireMock.Net.Minimal/RequestBuilders/Request.WithWebSocket.cs namespace WireMock.RequestBuilders; public partial class Request { /// /// Match WebSocket connection upgrade requests. /// public IRequestBuilder WithWebSocketUpgrade() { Add(new RequestMessageHeaderMatcher("Upgrade", new ExactMatcher("websocket"))); Add(new RequestMessageHeaderMatcher("Connection", new WildcardMatcher("*Upgrade*"))); return this; } /// /// Match specific WebSocket subprotocol. /// public IRequestBuilder WithWebSocketSubprotocol(string subprotocol) { Guard.NotNullOrWhiteSpace(subprotocol); Add(new RequestMessageHeaderMatcher("Sec-WebSocket-Protocol", new ExactMatcher(subprotocol))); return this; } /// /// Match WebSocket connection by path (typical pattern). /// public IRequestBuilder WithWebSocketPath(string path) { Guard.NotNullOrWhiteSpace(path); return WithPath(path).WithWebSocketUpgrade(); } } ``` ### 2.5 Response Builder Extension (WireMock.Net.Minimal) Create partial class for WebSocket response handling: ```csharp // File: src/WireMock.Net.Minimal/ResponseBuilders/Response.WithWebSocket.cs namespace WireMock.ResponseBuilders; public partial class Response { public IWebSocketResponse? WebSocketResponse { get; private set; } public bool WithWebSocketUsed { get; private set; } /// /// Configure WebSocket response with messages to send after connection. /// public IResponseBuilder WithWebSocket(Action configure) { Guard.NotNull(configure); var builder = new WebSocketResponseBuilder(); configure(builder); WithWebSocketUsed = true; WebSocketResponse = builder.Build(); return this; } /// /// Configure WebSocket response with a single message. /// public IResponseBuilder WithWebSocketMessage(string message, int? delayMs = null) { Guard.NotNullOrWhiteSpace(message); return WithWebSocket(b => b .WithMessage(message, delayMs) ); } /// /// Configure WebSocket with async callback for dynamic message generation. /// public IResponseBuilder WithWebSocketCallback( Func>> handler) { Guard.NotNull(handler); WithWebSocketUsed = true; WebSocketCallbackAsync = handler; return this; } /// /// Sets transformer for WebSocket messages. /// public IResponseBuilder WithWebSocketTransformer( bool use = true, TransformerType transformerType = TransformerType.Handlebars) { if (WebSocketResponse != null) { WebSocketResponse.UseTransformer = use; WebSocketResponse.TransformerType = transformerType; } return this; } /// /// Configure WebSocket close frame (graceful disconnect). /// public IResponseBuilder WithWebSocketClose(int? closeCode = 1000, string? reason = null) { if (WebSocketResponse != null) { WebSocketResponse.CloseCode = closeCode; WebSocketResponse.CloseMessage = reason; } return this; } public Func>>? WebSocketCallbackAsync { get; private set; } public bool WithWebSocketCallbackUsed => WebSocketCallbackAsync != null; } ``` ### 2.6 WebSocket Response Builder (WireMock.Net.Minimal) Create fluent builder for WebSocket messages: ```csharp // File: src/WireMock.Net.Minimal/ResponseBuilders/WebSocketResponseBuilder.cs namespace WireMock.ResponseBuilders; public class WebSocketResponseBuilder : IWebSocketResponseBuilder { private readonly List _messages = new(); private bool _useTransformer; private TransformerType _transformerType = TransformerType.Handlebars; private int? _closeCode; private string? _closeMessage; public IWebSocketResponseBuilder WithMessage(string message, int? delayMs = null) { Guard.NotNullOrWhiteSpace(message); _messages.Add(new WebSocketMessage { BodyAsString = message, DelayMs = delayMs ?? 0, IsText = true }); return this; } public IWebSocketResponseBuilder WithBinaryMessage(byte[] data, int? delayMs = null) { Guard.NotNull(data); _messages.Add(new WebSocketMessage { BodyAsBytes = data, DelayMs = delayMs ?? 0, IsText = false }); return this; } public IWebSocketResponseBuilder WithJsonMessage(object data, int? delayMs = null) { Guard.NotNull(data); var json = JsonConvert.SerializeObject(data); _messages.Add(new WebSocketMessage { BodyAsString = json, DelayMs = delayMs ?? 0, IsText = true }); return this; } public IWebSocketResponseBuilder WithTransformer( bool use = true, TransformerType transformerType = TransformerType.Handlebars) { _useTransformer = use; _transformerType = transformerType; return this; } public IWebSocketResponseBuilder WithClose(int closeCode = 1000, string? reason = null) { _closeCode = closeCode; _closeMessage = reason; return this; } public IWebSocketResponse Build() { return new WebSocketResponse { Messages = _messages, UseTransformer = _useTransformer, TransformerType = _transformerType, CloseCode = _closeCode, CloseMessage = _closeMessage }; } } ``` ### 2.7 Usage Examples #### **Basic WebSocket Echo** ```csharp server.Given(Request.Create().WithWebSocketPath("/echo")) .RespondWith(Response.Create() .WithWebSocket(ws => ws .WithMessage("Connected to echo server") ) .WithWebSocketCallback(async request => { // Echo messages back from request body var messageText = request.Body; return new[] { new WebSocketMessage { BodyAsString = $"Echo: {messageText}", DelayMs = 100 } }; }) ); ``` #### **Simulated Server - Multiple Messages** ```csharp server.Given(Request.Create().WithWebSocketPath("/chat")) .RespondWith(Response.Create() .WithWebSocket(ws => ws .WithMessage("Welcome to chat room", delayMs: 0) .WithMessage("Other users: 2", delayMs: 500) .WithMessage("Ready for messages", delayMs: 1000) .WithTransformer() .WithClose(1000, "Room closing") ) ); ``` #### **JSON WebSocket API** ```csharp server.Given(Request.Create() .WithWebSocketPath("/api/notifications") .WithWebSocketSubprotocol("chat-v1")) .RespondWith(Response.Create() .WithWebSocket(ws => ws .WithJsonMessage(new { type = "connected", userId = "{{request.headers.Authorization}}" }) .WithJsonMessage(new { type = "notification", message = "You have a new message" }, delayMs: 2000) .WithTransformer() ) ); ``` #### **Dynamic Messages Based on Request** ```csharp server.Given(Request.Create().WithWebSocketPath("/data-stream")) .RespondWith(Response.Create() .WithWebSocketCallback(async request => { var userId = request.Headers["X-User-Id"]?.FirstOrDefault(); var messages = new List(); for (int i = 0; i < 3; i++) { messages.Add(new WebSocketMessage { BodyAsString = JsonConvert.SerializeObject(new { userId, sequence = i, timestamp = DateTime.UtcNow }), DelayMs = i * 1000, IsText = true }); } return messages; }) ); ``` #### **Binary WebSocket (e.g., Protobuf)** ```csharp server.Given(Request.Create().WithWebSocketPath("/binary")) .RespondWith(Response.Create() .WithWebSocket(ws => ws .WithBinaryMessage(protoBytes, delayMs: 100) .WithBinaryMessage(anotherProtoBytes, delayMs: 200) .WithClose(1000) ) ); ``` --- ## Part 3: Implementation Roadmap ### Phase 1: Abstractions & Core Models 1. Create `IWebSocketMessage` interface in `WireMock.Net.Abstractions` 2. Create `IWebSocketResponse` interface in `WireMock.Net.Abstractions` 3. Create `IWebSocketResponseBuilder` interface in `WireMock.Net.Abstractions` 4. Implement model classes in `WireMock.Net.Minimal` ### Phase 2: Request Builder Extensions 1. Create `Request.WithWebSocket.cs` partial class 2. Add WebSocket-specific matchers 3. Add integration tests for request matching ### Phase 3: Response Builder Extensions 1. Create `Response.WithWebSocket.cs` partial class 2. Create `WebSocketResponseBuilder.cs` 3. Implement transformer support 4. Add callback support for dynamic messages ### Phase 4: Server Integration 1. Extend `WireMockMiddleware.cs` to handle WebSocket upgrades 2. Implement WebSocket message routing 3. Connection lifecycle management (open/close) 4. Message queueing and delivery ### Phase 5: Admin Interface 1. Update `MappingModel` to include WebSocket configuration 2. Add WebSocket support to mapping serialization 3. REST API endpoints for WebSocket management --- ## Part 4: Key Design Decisions ### 4.1 Design Rationale | Decision | Rationale | |----------|-----------| | **Fluent API for WebSocket** | Consistent with existing Request/Response builders | | **Callback Support** | Enables dynamic message generation based on request context | | **Async Messages** | WebSocket communication is inherently async | | **Partial Classes** | Maintains separation of concerns (Path matching, Headers, WebSocket) | | **Builder Pattern** | Allows composing complex WebSocket scenarios incrementally | | **Message Queue** | Simulates realistic server behavior (message ordering, delays) | | **Transformer Support** | Reuses Handlebars/Scriban for dynamic message content | ### 4.2 Comparison with Existing Features ```csharp // Webhook (similar pattern - external notification) .WithWebhook(new Webhook { Request = new WebhookRequest { ... } }) // WebSocket (new - connection-based messaging) .WithWebSocket(ws => ws .WithMessage("...") .WithTransformer() ) // Callback (existing - dynamic response) .WithCallback(request => new ResponseMessage { ... }) // WebSocket Callback (new - dynamic WebSocket messages) .WithWebSocketCallback(async request => new[] { new WebSocketMessage { ... } } ) ``` --- ## Part 5: Implementation Considerations ### 5.1 Dependencies - **ASP.NET Core WebSocket support** (already available in Minimal) - **IRequestMessage/IResponseMessage** (reuse existing) - **Transformer infrastructure** (Handlebars/Scriban) - **Message serialization** (Newtonsoft.Json) ### 5.2 Edge Cases to Handle 1. **Connection timeouts** - Server should be able to simulate client disconnect 2. **Message ordering** - Ensure messages are sent in the order defined 3. **Backpressure** - Handle slow clients 4. **Concurrent connections** - Multiple WebSocket clients to same endpoint 5. **Subprotocol negotiation** - Support WebSocket subprotocols ### 5.3 Testing Strategy 1. Unit tests for `WebSocketResponseBuilder` 2. Integration tests for connection matching 3. Message ordering and delivery tests 4. Transformer execution tests 5. Callback execution tests ### 5.4 Breaking Changes - **None** - This is purely additive functionality following existing patterns --- ## Part 6: Integration Points ### 6.1 With Existing Features ```csharp // WebSocket + Scenario State server.Given(Request.Create().WithWebSocketPath("/status")) .InScenario("ServiceMonitoring") .WhenStateIs("Running") .WillSetStateTo("Stopped") .RespondWith(Response.Create() .WithWebSocket(ws => ws.WithMessage("Service is running")) ); // WebSocket + Priority server.Given(Request.Create().WithWebSocketPath("/ws")) .AtPriority(1) .RespondWith(Response.Create() .WithWebSocket(ws => ws.WithMessage("Priority response")) ); // WebSocket + Title & Description server.Given(Request.Create().WithWebSocketPath("/api")) .WithTitle("WebSocket API") .WithDescription("Real-time data stream") .RespondWith(Response.Create() .WithWebSocket(ws => ws.WithJsonMessage(data)) ); ``` ### 6.2 With Admin Interface ```csharp // Retrieve WebSocket mappings var mappings = server.MappingModels .Where(m => m.Response.WebSocket != null) .ToList(); // Export WebSocket configuration var json = server.MappingModels[0].ToString(); ``` --- ## Conclusion The proposed WebSocket support follows WireMock.Net's established fluent API patterns while adding the unique requirements of bidirectional, stateful WebSocket communication. The design: ✅ **Maintains consistency** with existing Request/Response builders ✅ **Enables reuse** of transformers and matchers ✅ **Provides flexibility** through callbacks and builders ✅ **Supports testing** scenarios (timing, multiple messages, state) ✅ **Integrates naturally** with existing features (scenarios, priority, webhooks) This approach allows developers to mock complex WebSocket scenarios with the same familiar fluent syntax they use for HTTP mocking.