19 KiB
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)
// Entry point
var requestBuilder = Request.Create()
.WithPath("/api/users")
.UsingGet()
.WithHeader("Authorization", "Bearer token");
Key Characteristics:
- Partial class
Requestwith multipleRequest.WithXxx.csfiles - Each file focuses on a specific concern (Path, Headers, Params, etc.)
- Implements
IRequestBuilderinterface - Returns
this(IRequestBuilder) for chaining - Uses composition:
Request : RequestMessageCompositeMatcher, IRequestBuilder
B. Response Builder Pattern (ResponseBuilders/Response*.cs files)
// 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
Responsewith separate files for features - Methods return
IResponseBuilder(returnsthis) - Supports both sync and async callbacks via
WithCallback() - Pluggable transformers (Handlebars, Scriban)
- Examples:
Response.WithCallback.cs: Sync/async request handlersResponse.WithTransformer.cs: Template transformationResponse.WithProxy.cs: HTTP proxyingResponse.WithFault.cs: Simulated faults
C. Mapping Builder Pattern (MappingBuilder.cs + RespondWithAProvider.cs)
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()returnsIRespondWithAProviderRespondWithAProviderchains metadata (priority, scenario, webhooks)- Terminal method:
RespondWith(IResponseProvider)orThenRespondWith() - Fluent methods return
IRespondWithAProviderfor 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:
- Request matching (connection phase)
- Message routing (message handling)
- State management (connection state)
- Simulated server messages (push messages)
2.2 Proposed Model Classes (WireMock.Net.Abstractions)
Create new interfaces in WireMock.Net.Abstractions:
// 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<WebSocketMessageModel> 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/:
// 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<IWebSocketMessage> 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:
// File: src/WireMock.Net.Minimal/RequestBuilders/Request.WithWebSocket.cs
namespace WireMock.RequestBuilders;
public partial class Request
{
/// <summary>
/// Match WebSocket connection upgrade requests.
/// </summary>
public IRequestBuilder WithWebSocketUpgrade()
{
Add(new RequestMessageHeaderMatcher("Upgrade", new ExactMatcher("websocket")));
Add(new RequestMessageHeaderMatcher("Connection", new WildcardMatcher("*Upgrade*")));
return this;
}
/// <summary>
/// Match specific WebSocket subprotocol.
/// </summary>
public IRequestBuilder WithWebSocketSubprotocol(string subprotocol)
{
Guard.NotNullOrWhiteSpace(subprotocol);
Add(new RequestMessageHeaderMatcher("Sec-WebSocket-Protocol", new ExactMatcher(subprotocol)));
return this;
}
/// <summary>
/// Match WebSocket connection by path (typical pattern).
/// </summary>
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:
// 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; }
/// <summary>
/// Configure WebSocket response with messages to send after connection.
/// </summary>
public IResponseBuilder WithWebSocket(Action<IWebSocketResponseBuilder> configure)
{
Guard.NotNull(configure);
var builder = new WebSocketResponseBuilder();
configure(builder);
WithWebSocketUsed = true;
WebSocketResponse = builder.Build();
return this;
}
/// <summary>
/// Configure WebSocket response with a single message.
/// </summary>
public IResponseBuilder WithWebSocketMessage(string message, int? delayMs = null)
{
Guard.NotNullOrWhiteSpace(message);
return WithWebSocket(b => b
.WithMessage(message, delayMs)
);
}
/// <summary>
/// Configure WebSocket with async callback for dynamic message generation.
/// </summary>
public IResponseBuilder WithWebSocketCallback(
Func<IRequestMessage, Task<IEnumerable<IWebSocketMessage>>> handler)
{
Guard.NotNull(handler);
WithWebSocketUsed = true;
WebSocketCallbackAsync = handler;
return this;
}
/// <summary>
/// Sets transformer for WebSocket messages.
/// </summary>
public IResponseBuilder WithWebSocketTransformer(
bool use = true,
TransformerType transformerType = TransformerType.Handlebars)
{
if (WebSocketResponse != null)
{
WebSocketResponse.UseTransformer = use;
WebSocketResponse.TransformerType = transformerType;
}
return this;
}
/// <summary>
/// Configure WebSocket close frame (graceful disconnect).
/// </summary>
public IResponseBuilder WithWebSocketClose(int? closeCode = 1000, string? reason = null)
{
if (WebSocketResponse != null)
{
WebSocketResponse.CloseCode = closeCode;
WebSocketResponse.CloseMessage = reason;
}
return this;
}
public Func<IRequestMessage, Task<IEnumerable<IWebSocketMessage>>>? WebSocketCallbackAsync { get; private set; }
public bool WithWebSocketCallbackUsed => WebSocketCallbackAsync != null;
}
2.6 WebSocket Response Builder (WireMock.Net.Minimal)
Create fluent builder for WebSocket messages:
// File: src/WireMock.Net.Minimal/ResponseBuilders/WebSocketResponseBuilder.cs
namespace WireMock.ResponseBuilders;
public class WebSocketResponseBuilder : IWebSocketResponseBuilder
{
private readonly List<IWebSocketMessage> _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
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
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
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
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<WebSocketMessage>();
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)
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
- Create
IWebSocketMessageinterface inWireMock.Net.Abstractions - Create
IWebSocketResponseinterface inWireMock.Net.Abstractions - Create
IWebSocketResponseBuilderinterface inWireMock.Net.Abstractions - Implement model classes in
WireMock.Net.Minimal
Phase 2: Request Builder Extensions
- Create
Request.WithWebSocket.cspartial class - Add WebSocket-specific matchers
- Add integration tests for request matching
Phase 3: Response Builder Extensions
- Create
Response.WithWebSocket.cspartial class - Create
WebSocketResponseBuilder.cs - Implement transformer support
- Add callback support for dynamic messages
Phase 4: Server Integration
- Extend
WireMockMiddleware.csto handle WebSocket upgrades - Implement WebSocket message routing
- Connection lifecycle management (open/close)
- Message queueing and delivery
Phase 5: Admin Interface
- Update
MappingModelto include WebSocket configuration - Add WebSocket support to mapping serialization
- 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
// 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
- Connection timeouts - Server should be able to simulate client disconnect
- Message ordering - Ensure messages are sent in the order defined
- Backpressure - Handle slow clients
- Concurrent connections - Multiple WebSocket clients to same endpoint
- Subprotocol negotiation - Support WebSocket subprotocols
5.3 Testing Strategy
- Unit tests for
WebSocketResponseBuilder - Integration tests for connection matching
- Message ordering and delivery tests
- Transformer execution tests
- 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
// 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
// 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.