diff --git a/WireMock.Net Solution.sln b/WireMock.Net Solution.sln
index 1654e8cd..56b67d24 100644
--- a/WireMock.Net Solution.sln
+++ b/WireMock.Net Solution.sln
@@ -154,6 +154,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.Console.NET8.W
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.WebApplication.IIS", "examples\WireMock.Net.WebApplication.IIS\WireMock.Net.WebApplication.IIS.csproj", "{5E6E9FA7-9135-7B82-2CCD-8CA87AC8043C}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.WebSocketExamples", "examples\WireMock.Net.WebSocketExamples\WireMock.Net.WebSocketExamples.csproj", "{2CE8E3A6-59CC-FE9C-9399-AD54E1FA862B}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -788,6 +790,18 @@ Global
{9957038D-F9C3-CA5D-E8AE-BE188E512635}.Release|x64.Build.0 = Release|Any CPU
{9957038D-F9C3-CA5D-E8AE-BE188E512635}.Release|x86.ActiveCfg = Release|Any CPU
{9957038D-F9C3-CA5D-E8AE-BE188E512635}.Release|x86.Build.0 = Release|Any CPU
+ {4005E20C-D42B-138A-79BE-B3F5420C563F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4005E20C-D42B-138A-79BE-B3F5420C563F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4005E20C-D42B-138A-79BE-B3F5420C563F}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {4005E20C-D42B-138A-79BE-B3F5420C563F}.Debug|x64.Build.0 = Debug|Any CPU
+ {4005E20C-D42B-138A-79BE-B3F5420C563F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {4005E20C-D42B-138A-79BE-B3F5420C563F}.Debug|x86.Build.0 = Debug|Any CPU
+ {4005E20C-D42B-138A-79BE-B3F5420C563F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4005E20C-D42B-138A-79BE-B3F5420C563F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4005E20C-D42B-138A-79BE-B3F5420C563F}.Release|x64.ActiveCfg = Release|Any CPU
+ {4005E20C-D42B-138A-79BE-B3F5420C563F}.Release|x64.Build.0 = Release|Any CPU
+ {4005E20C-D42B-138A-79BE-B3F5420C563F}.Release|x86.ActiveCfg = Release|Any CPU
+ {4005E20C-D42B-138A-79BE-B3F5420C563F}.Release|x86.Build.0 = Release|Any CPU
{2D86546D-8A24-0A55-C962-2071BD299E05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2D86546D-8A24-0A55-C962-2071BD299E05}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2D86546D-8A24-0A55-C962-2071BD299E05}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -812,18 +826,18 @@ Global
{5E6E9FA7-9135-7B82-2CCD-8CA87AC8043C}.Release|x64.Build.0 = Release|Any CPU
{5E6E9FA7-9135-7B82-2CCD-8CA87AC8043C}.Release|x86.ActiveCfg = Release|Any CPU
{5E6E9FA7-9135-7B82-2CCD-8CA87AC8043C}.Release|x86.Build.0 = Release|Any CPU
- {4005E20C-D42B-138A-79BE-B3F5420C563F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {4005E20C-D42B-138A-79BE-B3F5420C563F}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {4005E20C-D42B-138A-79BE-B3F5420C563F}.Debug|x64.ActiveCfg = Debug|Any CPU
- {4005E20C-D42B-138A-79BE-B3F5420C563F}.Debug|x64.Build.0 = Debug|Any CPU
- {4005E20C-D42B-138A-79BE-B3F5420C563F}.Debug|x86.ActiveCfg = Debug|Any CPU
- {4005E20C-D42B-138A-79BE-B3F5420C563F}.Debug|x86.Build.0 = Debug|Any CPU
- {4005E20C-D42B-138A-79BE-B3F5420C563F}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {4005E20C-D42B-138A-79BE-B3F5420C563F}.Release|Any CPU.Build.0 = Release|Any CPU
- {4005E20C-D42B-138A-79BE-B3F5420C563F}.Release|x64.ActiveCfg = Release|Any CPU
- {4005E20C-D42B-138A-79BE-B3F5420C563F}.Release|x64.Build.0 = Release|Any CPU
- {4005E20C-D42B-138A-79BE-B3F5420C563F}.Release|x86.ActiveCfg = Release|Any CPU
- {4005E20C-D42B-138A-79BE-B3F5420C563F}.Release|x86.Build.0 = Release|Any CPU
+ {2CE8E3A6-59CC-FE9C-9399-AD54E1FA862B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2CE8E3A6-59CC-FE9C-9399-AD54E1FA862B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2CE8E3A6-59CC-FE9C-9399-AD54E1FA862B}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {2CE8E3A6-59CC-FE9C-9399-AD54E1FA862B}.Debug|x64.Build.0 = Debug|Any CPU
+ {2CE8E3A6-59CC-FE9C-9399-AD54E1FA862B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {2CE8E3A6-59CC-FE9C-9399-AD54E1FA862B}.Debug|x86.Build.0 = Debug|Any CPU
+ {2CE8E3A6-59CC-FE9C-9399-AD54E1FA862B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2CE8E3A6-59CC-FE9C-9399-AD54E1FA862B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2CE8E3A6-59CC-FE9C-9399-AD54E1FA862B}.Release|x64.ActiveCfg = Release|Any CPU
+ {2CE8E3A6-59CC-FE9C-9399-AD54E1FA862B}.Release|x64.Build.0 = Release|Any CPU
+ {2CE8E3A6-59CC-FE9C-9399-AD54E1FA862B}.Release|x86.ActiveCfg = Release|Any CPU
+ {2CE8E3A6-59CC-FE9C-9399-AD54E1FA862B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -883,9 +897,10 @@ Global
{2DBBD70D-8051-441F-92BB-FF9B8B4B4982} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2}
{C8F4E6D2-9A3B-4F1C-8D5E-7A2B3C4D5E6F} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2}
{9957038D-F9C3-CA5D-E8AE-BE188E512635} = {985E0ADB-D4B4-473A-AA40-567E279B7946}
+ {4005E20C-D42B-138A-79BE-B3F5420C563F} = {985E0ADB-D4B4-473A-AA40-567E279B7946}
{2D86546D-8A24-0A55-C962-2071BD299E05} = {985E0ADB-D4B4-473A-AA40-567E279B7946}
{5E6E9FA7-9135-7B82-2CCD-8CA87AC8043C} = {985E0ADB-D4B4-473A-AA40-567E279B7946}
- {4005E20C-D42B-138A-79BE-B3F5420C563F} = {985E0ADB-D4B4-473A-AA40-567E279B7946}
+ {2CE8E3A6-59CC-FE9C-9399-AD54E1FA862B} = {985E0ADB-D4B4-473A-AA40-567E279B7946}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {DC539027-9852-430C-B19F-FD035D018458}
diff --git a/examples/WireMock.Net.WebSocketExamples/Program.cs b/examples/WireMock.Net.WebSocketExamples/Program.cs
new file mode 100644
index 00000000..4cf92135
--- /dev/null
+++ b/examples/WireMock.Net.WebSocketExamples/Program.cs
@@ -0,0 +1,725 @@
+// Copyright © WireMock.Net
+
+using System;
+using System.Linq;
+using System.Net.WebSockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using WireMock.Logging;
+using WireMock.RequestBuilders;
+using WireMock.ResponseBuilders;
+using WireMock.Server;
+using WireMock.Settings;
+
+namespace WireMock.Net.WebSocketExamples;
+
+public static class Program
+{
+ public static async Task Main(string[] args)
+ {
+ Console.WriteLine("WireMock.Net WebSocket Examples");
+ Console.WriteLine("================================\n");
+
+ Console.WriteLine("Choose an example to run:");
+ Console.WriteLine("1. Echo Server");
+ Console.WriteLine("2. Custom Message Handler");
+ Console.WriteLine("3. Broadcast Server");
+ Console.WriteLine("4. Scenario/State Machine");
+ Console.WriteLine("5. WebSocket Proxy");
+ Console.WriteLine("6. Multiple WebSocket Endpoints");
+ Console.WriteLine("7. All Examples (runs all endpoints)");
+ Console.WriteLine("0. Exit\n");
+
+ Console.Write("Enter choice: ");
+ var choice = Console.ReadLine();
+
+ switch (choice)
+ {
+ case "1":
+ await RunEchoServerExample();
+ break;
+ case "2":
+ await RunCustomMessageHandlerExample();
+ break;
+ case "3":
+ await RunBroadcastExample();
+ break;
+ case "4":
+ await RunScenarioExample();
+ break;
+ case "5":
+ await RunProxyExample();
+ break;
+ case "6":
+ await RunMultipleEndpointsExample();
+ break;
+ case "7":
+ await RunAllExamples();
+ break;
+ case "0":
+ return;
+ default:
+ Console.WriteLine("Invalid choice");
+ break;
+ }
+ }
+
+ ///
+ /// Example 1: Simple Echo Server
+ /// Echoes back all messages received from the client
+ ///
+ private static async Task RunEchoServerExample()
+ {
+ Console.WriteLine("\n=== Echo Server Example ===");
+ Console.WriteLine("Starting WebSocket echo server...\n");
+
+ var server = WireMockServer.Start(new WireMockServerSettings
+ {
+ Port = 9091,
+ Logger = new WireMockConsoleLogger()
+ });
+
+ server
+ .Given(Request.Create()
+ .WithPath("/ws/echo")
+ .WithWebSocketUpgrade()
+ )
+ .RespondWith(Response.Create()
+ .WithWebSocket(ws => ws
+ .WithEcho()
+ )
+ );
+
+ Console.WriteLine($"Echo server listening at: {server.Urls[0]}/ws/echo");
+ Console.WriteLine("\nTest with a WebSocket client:");
+ Console.WriteLine(" wscat -c ws://localhost:9091/ws/echo");
+ Console.WriteLine("\nPress any key to test or CTRL+C to exit...");
+ Console.ReadKey();
+
+ // Test the echo server
+ await TestWebSocketEcho(server.Urls[0]);
+
+ Console.WriteLine("\nPress any key to stop server...");
+ Console.ReadKey();
+ server.Stop();
+ }
+
+ ///
+ /// Example 2: Custom Message Handler
+ /// Processes messages and sends custom responses
+ ///
+ private static async Task RunCustomMessageHandlerExample()
+ {
+ Console.WriteLine("\n=== Custom Message Handler Example ===");
+ Console.WriteLine("Starting WebSocket server with custom message handler...\n");
+
+ var server = WireMockServer.Start(new WireMockServerSettings
+ {
+ Port = 9091,
+ Logger = new WireMockConsoleLogger()
+ });
+
+ server
+ .Given(Request.Create()
+ .WithPath("/ws/chat")
+ .WithWebSocketUpgrade()
+ )
+ .RespondWith(Response.Create()
+ .WithWebSocket(ws => ws
+ .WithMessageHandler(async (message, context) =>
+ {
+ if (message.MessageType == WebSocketMessageType.Text)
+ {
+ var text = message.Text ?? string.Empty;
+
+ // Handle different commands
+ if (text.StartsWith("/help"))
+ {
+ await context.SendTextAsync("Available commands: /help, /time, /echo , /upper , /reverse ");
+ }
+ else if (text.StartsWith("/time"))
+ {
+ await context.SendTextAsync($"Server time: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC");
+ }
+ else if (text.StartsWith("/echo "))
+ {
+ await context.SendTextAsync(text.Substring(6));
+ }
+ else if (text.StartsWith("/upper "))
+ {
+ await context.SendTextAsync(text.Substring(7).ToUpper());
+ }
+ else if (text.StartsWith("/reverse "))
+ {
+ var toReverse = text.Substring(9);
+ var reversed = new string(toReverse.Reverse().ToArray());
+ await context.SendTextAsync(reversed);
+ }
+ else if (text == "/quit")
+ {
+ await context.SendTextAsync("Goodbye!");
+ await context.CloseAsync(WebSocketCloseStatus.NormalClosure, "Client requested disconnect");
+ }
+ else
+ {
+ await context.SendTextAsync($"Unknown command: {text}. Type /help for available commands.");
+ }
+ }
+ })
+ )
+ );
+
+ Console.WriteLine($"Chat server listening at: {server.Urls[0]}/ws/chat");
+ Console.WriteLine("\nTest with:");
+ Console.WriteLine(" wscat -c ws://localhost:9091/ws/chat");
+ Console.WriteLine("\nThen try commands: /help, /time, /echo hello, /upper hello, /reverse hello");
+ Console.WriteLine("\nPress any key to test or CTRL+C to exit...");
+ Console.ReadKey();
+
+ await TestWebSocketChat(server.Urls[0]);
+
+ Console.WriteLine("\nPress any key to stop server...");
+ Console.ReadKey();
+ server.Stop();
+ }
+
+ ///
+ /// Example 3: Broadcast Server
+ /// Broadcasts messages to all connected clients
+ ///
+ private static async Task RunBroadcastExample()
+ {
+ Console.WriteLine("\n=== Broadcast Server Example ===");
+ Console.WriteLine("Starting WebSocket broadcast server...\n");
+
+ var server = WireMockServer.Start(new WireMockServerSettings
+ {
+ Port = 9091,
+ Logger = new WireMockConsoleLogger()
+ });
+
+ var broadcastMappingGuid = Guid.NewGuid();
+
+ server
+ .Given(Request.Create()
+ .WithPath("/ws/broadcast")
+ .WithWebSocketUpgrade()
+ )
+ .WithGuid(broadcastMappingGuid)
+ .RespondWith(Response.Create()
+ .WithWebSocket(ws => ws
+ .WithBroadcast()
+ .WithMessageHandler(async (message, context) =>
+ {
+ if (message.MessageType == WebSocketMessageType.Text)
+ {
+ var text = message.Text ?? string.Empty;
+ var timestamp = DateTime.UtcNow.ToString("HH:mm:ss");
+ var broadcastMessage = $"[{timestamp}] Broadcast: {text}";
+
+ // Broadcast to all connected clients
+ await context.BroadcastTextAsync(broadcastMessage);
+
+ Console.WriteLine($"Broadcasted to {server.GetWebSocketConnections(broadcastMappingGuid).Count} clients: {text}");
+ }
+ })
+ )
+ );
+
+ Console.WriteLine($"Broadcast server listening at: {server.Urls[0]}/ws/broadcast");
+ Console.WriteLine("\nConnect multiple clients:");
+ Console.WriteLine(" wscat -c ws://localhost:9091/ws/broadcast");
+ Console.WriteLine("\nMessages sent from any client will be broadcast to all clients");
+ Console.WriteLine("\nPress any key to stop server...");
+ Console.ReadKey();
+ server.Stop();
+ }
+
+ ///
+ /// Example 4: Scenario/State Machine
+ /// Demonstrates state transitions during WebSocket session
+ ///
+ private static async Task RunScenarioExample()
+ {
+ Console.WriteLine("\n=== Scenario/State Machine Example ===");
+ Console.WriteLine("Starting WebSocket server with scenario support...\n");
+
+ var server = WireMockServer.Start(new WireMockServerSettings
+ {
+ Port = 9091,
+ Logger = new WireMockConsoleLogger()
+ });
+
+ // Initial state: Waiting for players
+ server
+ .Given(Request.Create()
+ .WithPath("/ws/game")
+ .WithWebSocketUpgrade()
+ )
+ .InScenario("GameSession")
+ .WillSetStateTo("Lobby")
+ .RespondWith(Response.Create()
+ .WithWebSocket(ws => ws
+ .WithMessageHandler(async (msg, ctx) =>
+ {
+ await ctx.SendTextAsync("Welcome to the game lobby! Type 'ready' to start or 'quit' to leave.");
+ })
+ )
+ );
+
+ // Lobby state: Waiting for ready
+ server
+ .Given(Request.Create()
+ .WithPath("/ws/game")
+ .WithWebSocketUpgrade()
+ )
+ .InScenario("GameSession")
+ .WhenStateIs("Lobby")
+ .RespondWith(Response.Create()
+ .WithWebSocket(ws => ws
+ .WithMessageHandler(async (msg, ctx) =>
+ {
+ var text = msg.Text?.ToLower() ?? string.Empty;
+
+ if (text == "ready")
+ {
+ ctx.SetScenarioState("Playing");
+ await ctx.SendTextAsync("Game started! Type 'attack' to attack, 'defend' to defend, or 'quit' to exit.");
+ }
+ else if (text == "quit")
+ {
+ await ctx.SendTextAsync("You left the lobby. Goodbye!");
+ await ctx.CloseAsync(WebSocketCloseStatus.NormalClosure, "Player quit");
+ }
+ else
+ {
+ await ctx.SendTextAsync("In lobby. Type 'ready' to start or 'quit' to leave.");
+ }
+ })
+ )
+ );
+
+ // Playing state: Game is active
+ server
+ .Given(Request.Create()
+ .WithPath("/ws/game")
+ .WithWebSocketUpgrade()
+ )
+ .InScenario("GameSession")
+ .WhenStateIs("Playing")
+ .RespondWith(Response.Create()
+ .WithWebSocket(ws => ws
+ .WithMessageHandler(async (msg, ctx) =>
+ {
+ var text = msg.Text?.ToLower() ?? string.Empty;
+
+ if (text == "attack")
+ {
+ await ctx.SendTextAsync("You attacked! Critical hit! 💥");
+ }
+ else if (text == "defend")
+ {
+ await ctx.SendTextAsync("You defended! Shield up! 🛡️");
+ }
+ else if (text == "quit")
+ {
+ ctx.SetScenarioState("GameOver");
+ await ctx.SendTextAsync("Game over! Thanks for playing.");
+ await ctx.CloseAsync(WebSocketCloseStatus.NormalClosure, "Game ended");
+ }
+ else
+ {
+ await ctx.SendTextAsync("Unknown action. Type 'attack', 'defend', or 'quit'.");
+ }
+ })
+ )
+ );
+
+ Console.WriteLine($"Game server listening at: {server.Urls[0]}/ws/game");
+ Console.WriteLine("\nConnect and follow the game flow:");
+ Console.WriteLine(" wscat -c ws://localhost:9091/ws/game");
+ Console.WriteLine("\nGame flow: Lobby -> Type 'ready' -> Playing -> Type 'attack'/'defend' -> Type 'quit'");
+ Console.WriteLine("\nPress any key to stop server...");
+ Console.ReadKey();
+ server.Stop();
+ }
+
+ ///
+ /// Example 5: WebSocket Proxy
+ /// Proxies WebSocket connections to another server
+ ///
+ private static async Task RunProxyExample()
+ {
+ Console.WriteLine("\n=== WebSocket Proxy Example ===");
+ Console.WriteLine("Starting WebSocket proxy server...\n");
+
+ var server = WireMockServer.Start(new WireMockServerSettings
+ {
+ Port = 9091,
+ Logger = new WireMockConsoleLogger()
+ });
+
+ server
+ .Given(Request.Create()
+ .WithPath("/ws/proxy")
+ .WithWebSocketUpgrade()
+ )
+ .RespondWith(Response.Create()
+ .WithWebSocketProxy("ws://echo.websocket.org")
+ );
+
+ Console.WriteLine($"Proxy server listening at: {server.Urls[0]}/ws/proxy");
+ Console.WriteLine("Proxying to: ws://echo.websocket.org");
+ Console.WriteLine("\nTest with:");
+ Console.WriteLine(" wscat -c ws://localhost:9091/ws/proxy");
+ Console.WriteLine("\nPress any key to stop server...");
+ Console.ReadKey();
+ server.Stop();
+ }
+
+ ///
+ /// Example 6: Multiple WebSocket Endpoints
+ /// Demonstrates running multiple WebSocket endpoints simultaneously
+ ///
+ private static async Task RunMultipleEndpointsExample()
+ {
+ Console.WriteLine("\n=== Multiple WebSocket Endpoints Example ===");
+ Console.WriteLine("Starting server with multiple WebSocket endpoints...\n");
+
+ var server = WireMockServer.Start(new WireMockServerSettings
+ {
+ Port = 9091,
+ Logger = new WireMockConsoleLogger(),
+ WebSocketSettings = new WebSocketSettings
+ {
+ MaxConnections = 100,
+ KeepAliveIntervalSeconds = 30
+ }
+ });
+
+ // Endpoint 1: Echo
+ server
+ .Given(Request.Create()
+ .WithPath("/ws/echo")
+ .WithWebSocketUpgrade()
+ )
+ .RespondWith(Response.Create()
+ .WithWebSocket(ws => ws.WithEcho())
+ );
+
+ // Endpoint 2: Time service
+ server
+ .Given(Request.Create()
+ .WithPath("/ws/time")
+ .WithWebSocketUpgrade()
+ )
+ .RespondWith(Response.Create()
+ .WithWebSocket(ws => ws
+ .WithMessageHandler(async (msg, ctx) =>
+ {
+ await ctx.SendTextAsync($"Server time: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC");
+ })
+ )
+ );
+
+ // Endpoint 3: JSON service
+ server
+ .Given(Request.Create()
+ .WithPath("/ws/json")
+ .WithWebSocketUpgrade()
+ )
+ .RespondWith(Response.Create()
+ .WithWebSocket(ws => ws
+ .WithMessageHandler(async (msg, ctx) =>
+ {
+ var response = new
+ {
+ timestamp = DateTime.UtcNow,
+ message = msg.Text,
+ length = msg.Text?.Length ?? 0,
+ type = msg.MessageType.ToString()
+ };
+ await ctx.SendJsonAsync(response);
+ })
+ )
+ );
+
+ // Endpoint 4: Protocol-specific
+ server
+ .Given(Request.Create()
+ .WithPath("/ws/protocol")
+ .WithWebSocketUpgrade("chat", "superchat")
+ )
+ .RespondWith(Response.Create()
+ .WithWebSocket(ws => ws
+ .WithAcceptProtocol("chat")
+ .WithMessageHandler(async (msg, ctx) =>
+ {
+ await ctx.SendTextAsync($"Using protocol: chat. Message: {msg.Text}");
+ })
+ )
+ );
+
+ Console.WriteLine("Available WebSocket endpoints:");
+ Console.WriteLine($" 1. Echo: {server.Urls[0]}/ws/echo");
+ Console.WriteLine($" 2. Time: {server.Urls[0]}/ws/time");
+ Console.WriteLine($" 3. JSON: {server.Urls[0]}/ws/json");
+ Console.WriteLine($" 4. Protocol: {server.Urls[0]}/ws/protocol");
+ Console.WriteLine("\nTest with wscat:");
+ Console.WriteLine(" wscat -c ws://localhost:9091/ws/echo");
+ Console.WriteLine(" wscat -c ws://localhost:9091/ws/time");
+ Console.WriteLine(" wscat -c ws://localhost:9091/ws/json");
+ Console.WriteLine(" wscat -c ws://localhost:9091/ws/protocol -s chat");
+ Console.WriteLine("\nPress any key to stop server...");
+ Console.ReadKey();
+ server.Stop();
+ }
+
+ ///
+ /// Example 7: Run All Examples
+ /// Starts a server with all example endpoints
+ ///
+ private static async Task RunAllExamples()
+ {
+ Console.WriteLine("\n=== All Examples Running ===");
+ Console.WriteLine("Starting server with all WebSocket endpoints...\n");
+
+ var server = WireMockServer.Start(new WireMockServerSettings
+ {
+ Port = 9091,
+ Logger = new WireMockConsoleLogger(),
+ WebSocketSettings = new WebSocketSettings
+ {
+ MaxConnections = 200
+ }
+ });
+
+ SetupAllEndpoints(server);
+
+ Console.WriteLine("All WebSocket endpoints are running:");
+ Console.WriteLine($" Echo: {server.Urls[0]}/ws/echo");
+ Console.WriteLine($" Chat: {server.Urls[0]}/ws/chat");
+ Console.WriteLine($" Broadcast: {server.Urls[0]}/ws/broadcast");
+ Console.WriteLine($" Game: {server.Urls[0]}/ws/game");
+ Console.WriteLine($" Time: {server.Urls[0]}/ws/time");
+ Console.WriteLine($" JSON: {server.Urls[0]}/ws/json");
+ Console.WriteLine("\nServer statistics:");
+ Console.WriteLine($" Total mappings: {server.Mappings.Count}");
+
+ Console.WriteLine("\nPress any key to view connection stats or CTRL+C to exit...");
+
+ while (true)
+ {
+ Console.ReadKey(true);
+ var connections = server.GetWebSocketConnections();
+ Console.WriteLine($"\nActive WebSocket connections: {connections.Count}");
+ foreach (var conn in connections)
+ {
+ Console.WriteLine($" - {conn.ConnectionId}: {conn.RequestMessage.Path} (State: {conn.WebSocket.State})");
+ }
+ Console.WriteLine("\nPress any key to refresh or CTRL+C to exit...");
+ }
+ }
+
+ private static void SetupAllEndpoints(WireMockServer server)
+ {
+ // Echo endpoint
+ server
+ .Given(Request.Create()
+ .WithPath("/ws/echo")
+ .WithWebSocketUpgrade()
+ )
+ .RespondWith(Response.Create()
+ .WithWebSocket(ws => ws.WithEcho())
+ );
+
+ // Chat endpoint
+ server
+ .Given(Request.Create()
+ .WithPath("/ws/chat")
+ .WithWebSocketUpgrade()
+ )
+ .RespondWith(Response.Create()
+ .WithWebSocket(ws => ws
+ .WithMessageHandler(async (message, context) =>
+ {
+ if (message.MessageType == WebSocketMessageType.Text)
+ {
+ await context.SendTextAsync($"Echo: {message.Text}");
+ }
+ })
+ )
+ );
+
+ // Broadcast endpoint
+ var broadcastGuid = Guid.NewGuid();
+ server
+ .Given(Request.Create()
+ .WithPath("/ws/broadcast")
+ .WithWebSocketUpgrade()
+ )
+ .WithGuid(broadcastGuid)
+ .RespondWith(Response.Create()
+ .WithWebSocket(ws => ws
+ .WithBroadcast()
+ .WithMessageHandler(async (message, context) =>
+ {
+ if (message.MessageType == WebSocketMessageType.Text)
+ {
+ await context.BroadcastTextAsync($"[Broadcast] {message.Text}");
+ }
+ })
+ )
+ );
+
+ // Game scenario endpoint
+ SetupGameScenario(server);
+
+ // Time endpoint
+ server
+ .Given(Request.Create()
+ .WithPath("/ws/time")
+ .WithWebSocketUpgrade()
+ )
+ .RespondWith(Response.Create()
+ .WithWebSocket(ws => ws
+ .WithMessageHandler(async (msg, ctx) =>
+ {
+ await ctx.SendTextAsync($"Server time: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC");
+ })
+ )
+ );
+
+ // JSON endpoint
+ server
+ .Given(Request.Create()
+ .WithPath("/ws/json")
+ .WithWebSocketUpgrade()
+ )
+ .RespondWith(Response.Create()
+ .WithWebSocket(ws => ws
+ .WithMessageHandler(async (msg, ctx) =>
+ {
+ var response = new
+ {
+ timestamp = DateTime.UtcNow,
+ message = msg.Text,
+ connectionId = ctx.ConnectionId
+ };
+ await ctx.SendJsonAsync(response);
+ })
+ )
+ );
+ }
+
+ private static void SetupGameScenario(WireMockServer server)
+ {
+ server
+ .Given(Request.Create()
+ .WithPath("/ws/game")
+ .WithWebSocketUpgrade()
+ )
+ .InScenario("GameSession")
+ .WillSetStateTo("Lobby")
+ .RespondWith(Response.Create()
+ .WithWebSocket(ws => ws
+ .WithMessageHandler(async (msg, ctx) =>
+ {
+ await ctx.SendTextAsync("Welcome! Type 'ready' to start.");
+ })
+ )
+ );
+
+ server
+ .Given(Request.Create()
+ .WithPath("/ws/game")
+ .WithWebSocketUpgrade()
+ )
+ .InScenario("GameSession")
+ .WhenStateIs("Lobby")
+ .RespondWith(Response.Create()
+ .WithWebSocket(ws => ws
+ .WithMessageHandler(async (msg, ctx) =>
+ {
+ if (msg.Text?.ToLower() == "ready")
+ {
+ ctx.SetScenarioState("Playing");
+ await ctx.SendTextAsync("Game started!");
+ }
+ })
+ )
+ );
+ }
+
+ // Helper methods for testing
+ private static async Task TestWebSocketEcho(string baseUrl)
+ {
+ try
+ {
+ using var client = new ClientWebSocket();
+ var uri = new Uri($"{baseUrl.Replace("http://", "ws://")}/ws/echo");
+
+ Console.WriteLine($"\nConnecting to {uri}...");
+ await client.ConnectAsync(uri, CancellationToken.None);
+ Console.WriteLine("Connected!");
+
+ var testMessages = new[] { "Hello", "World", "WebSocket", "Test" };
+
+ foreach (var testMessage in testMessages)
+ {
+ Console.WriteLine($"\nSending: {testMessage}");
+ var bytes = Encoding.UTF8.GetBytes(testMessage);
+ await client.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, CancellationToken.None);
+
+ var buffer = new byte[1024];
+ var result = await client.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None);
+ var received = Encoding.UTF8.GetString(buffer, 0, result.Count);
+ Console.WriteLine($"Received: {received}");
+ }
+
+ await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", CancellationToken.None);
+ Console.WriteLine("\nTest completed successfully!");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"\nTest failed: {ex.Message}");
+ }
+ }
+
+ private static async Task TestWebSocketChat(string baseUrl)
+ {
+ try
+ {
+ using var client = new ClientWebSocket();
+ var uri = new Uri($"{baseUrl.Replace("http://", "ws://")}/ws/chat");
+
+ Console.WriteLine($"\nConnecting to {uri}...");
+ await client.ConnectAsync(uri, CancellationToken.None);
+ Console.WriteLine("Connected!");
+
+ var commands = new[] { "/help", "/time", "/echo Hello", "/upper test", "/reverse hello" };
+
+ foreach (var command in commands)
+ {
+ Console.WriteLine($"\nSending: {command}");
+ var bytes = Encoding.UTF8.GetBytes(command);
+ await client.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, CancellationToken.None);
+
+ var buffer = new byte[1024];
+ var result = await client.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None);
+ var received = Encoding.UTF8.GetString(buffer, 0, result.Count);
+ Console.WriteLine($"Received: {received}");
+
+ await Task.Delay(500);
+ }
+
+ await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", CancellationToken.None);
+ Console.WriteLine("\nTest completed successfully!");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"\nTest failed: {ex.Message}");
+ }
+ }
+}
diff --git a/examples/WireMock.Net.WebSocketExamples/README.md b/examples/WireMock.Net.WebSocketExamples/README.md
new file mode 100644
index 00000000..3411c68f
--- /dev/null
+++ b/examples/WireMock.Net.WebSocketExamples/README.md
@@ -0,0 +1,156 @@
+# WireMock.Net WebSocket Examples
+
+This project demonstrates all the WebSocket capabilities of WireMock.Net.
+
+## Prerequisites
+
+- .NET 8.0 SDK
+- Optional: `wscat` for manual testing (`npm install -g wscat`)
+
+## Running the Examples
+
+```bash
+cd examples/WireMock.Net.WebSocketExamples
+dotnet run
+```
+
+## Available Examples
+
+### 1. Echo Server
+Simple WebSocket echo server that returns all messages back to the client.
+
+**Test with:**
+```bash
+wscat -c ws://localhost:9091/ws/echo
+```
+
+### 2. Custom Message Handler
+Chat server with commands: `/help`, `/time`, `/echo`, `/upper`, `/reverse`, `/quit`
+
+**Test with:**
+```bash
+wscat -c ws://localhost:9091/ws/chat
+> /help
+> /time
+> /echo Hello World
+> /upper test
+> /reverse hello
+```
+
+### 3. Broadcast Server
+Messages sent by any client are broadcast to all connected clients.
+
+**Test with multiple terminals:**
+```bash
+# Terminal 1
+wscat -c ws://localhost:9091/ws/broadcast
+
+# Terminal 2
+wscat -c ws://localhost:9091/ws/broadcast
+
+# Terminal 3
+wscat -c ws://localhost:9091/ws/broadcast
+```
+
+Type messages in any terminal and see them appear in all terminals.
+
+### 4. Scenario/State Machine
+Game server with state transitions: Lobby -> Playing -> GameOver
+
+**Test with:**
+```bash
+wscat -c ws://localhost:9091/ws/game
+> ready
+> attack
+> defend
+> quit
+```
+
+### 5. WebSocket Proxy
+Proxies WebSocket connections to echo.websocket.org
+
+**Test with:**
+```bash
+wscat -c ws://localhost:9091/ws/proxy
+```
+
+### 6. Multiple Endpoints
+Runs multiple WebSocket endpoints simultaneously:
+- `/ws/echo` - Echo server
+- `/ws/time` - Returns server time
+- `/ws/json` - Returns JSON responses
+- `/ws/protocol` - Protocol-specific endpoint
+
+### 7. All Examples
+Runs all endpoints at once with connection statistics.
+
+## Features Demonstrated
+
+- ✅ **Echo Server** - Simple message echo
+- ✅ **Custom Handlers** - Complex message processing
+- ✅ **Broadcast** - Multi-client communication
+- ✅ **Scenarios** - State machine patterns
+- ✅ **Proxy** - Forwarding to real WebSocket servers
+- ✅ **Protocol Negotiation** - Sec-WebSocket-Protocol support
+- ✅ **JSON Messaging** - Structured data exchange
+- ✅ **Connection Management** - Track and manage connections
+- ✅ **Configuration** - Custom WebSocket settings
+
+## Testing with wscat
+
+Install wscat globally:
+```bash
+npm install -g wscat
+```
+
+Basic usage:
+```bash
+# Connect to endpoint
+wscat -c ws://localhost:9091/ws/echo
+
+# Connect with protocol
+wscat -c ws://localhost:9091/ws/protocol -s chat
+
+# Connect with headers
+wscat -c ws://localhost:9091/ws/echo -H "X-Custom-Header: value"
+```
+
+## Testing with C# Client
+
+The examples include built-in C# WebSocket clients for automated testing.
+Select options 1 or 2 and press any key to run the automated tests.
+
+## Configuration
+
+WebSocket settings can be configured:
+
+```csharp
+var server = WireMockServer.Start(new WireMockServerSettings
+{
+ Port = 9091,
+ WebSocketSettings = new WebSocketSettings
+ {
+ MaxConnections = 100,
+ ReceiveBufferSize = 8192,
+ MaxMessageSize = 1048576,
+ KeepAliveInterval = TimeSpan.FromSeconds(30),
+ CloseTimeout = TimeSpan.FromMinutes(10),
+ EnableCompression = true
+ }
+});
+```
+
+## Monitoring
+
+When running "All Examples" (option 7), press any key to view:
+- Active connection count
+- Connection IDs
+- Request paths
+- WebSocket states
+
+## Notes
+
+- All examples run on port 9091 by default
+- Press CTRL+C to stop the server
+- Multiple clients can connect simultaneously
+- Connection states are tracked and can be queried
diff --git a/examples/WireMock.Net.WebSocketExamples/WireMock.Net.WebSocketExamples.csproj b/examples/WireMock.Net.WebSocketExamples/WireMock.Net.WebSocketExamples.csproj
new file mode 100644
index 00000000..c62051d0
--- /dev/null
+++ b/examples/WireMock.Net.WebSocketExamples/WireMock.Net.WebSocketExamples.csproj
@@ -0,0 +1,18 @@
+
+
+
+ Exe
+ net8.0
+ WireMock.Net.WebSocketExamples
+ WireMock.Net.WebSocketExamples
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/WireMock.Net.Abstractions/Admin/Settings/SettingsModel.cs b/src/WireMock.Net.Abstractions/Admin/Settings/SettingsModel.cs
index 976dfcd9..90351c58 100644
--- a/src/WireMock.Net.Abstractions/Admin/Settings/SettingsModel.cs
+++ b/src/WireMock.Net.Abstractions/Admin/Settings/SettingsModel.cs
@@ -131,4 +131,9 @@ public class SettingsModel
/// Whether to accept any client certificate
///
public bool AcceptAnyClientCertificate { get; set; }
+
+ ///
+ /// Gets or sets the WebSocket settings.
+ ///
+ public WebSocketSettingsModel? WebSocketSettings { get; set; }
}
\ No newline at end of file
diff --git a/src/WireMock.Net.Abstractions/Admin/Settings/WebSocketSettingsModel.cs b/src/WireMock.Net.Abstractions/Admin/Settings/WebSocketSettingsModel.cs
new file mode 100644
index 00000000..624d7918
--- /dev/null
+++ b/src/WireMock.Net.Abstractions/Admin/Settings/WebSocketSettingsModel.cs
@@ -0,0 +1,40 @@
+// Copyright © WireMock.Net
+
+namespace WireMock.Admin.Settings;
+
+///
+/// WebSocket Settings Model
+///
+[FluentBuilder.AutoGenerateBuilder]
+public class WebSocketSettingsModel
+{
+ ///
+ /// Maximum number of concurrent WebSocket connections (default: 100)
+ ///
+ public int MaxConnections { get; set; } = 100;
+
+ ///
+ /// Default receive buffer size in bytes (default: 4096)
+ ///
+ public int ReceiveBufferSize { get; set; } = 4096;
+
+ ///
+ /// Default keep-alive interval in seconds (default: 30)
+ ///
+ public int KeepAliveIntervalSeconds { get; set; } = 30;
+
+ ///
+ /// Maximum message size in bytes (default: 1048576 - 1 MB)
+ ///
+ public int MaxMessageSize { get; set; } = 1048576;
+
+ ///
+ /// Enable WebSocket compression (default: true)
+ ///
+ public bool EnableCompression { get; set; } = true;
+
+ ///
+ /// Default close timeout in minutes (default: 10)
+ ///
+ public int CloseTimeoutMinutes { get; set; } = 10;
+}
diff --git a/src/WireMock.Net.Abstractions/Constants/WebSocketConstants.cs b/src/WireMock.Net.Abstractions/Constants/WebSocketConstants.cs
new file mode 100644
index 00000000..1cdefdd4
--- /dev/null
+++ b/src/WireMock.Net.Abstractions/Constants/WebSocketConstants.cs
@@ -0,0 +1,39 @@
+// Copyright © WireMock.Net
+
+namespace WireMock.Constants;
+
+///
+/// WebSocket constants
+///
+public static class WebSocketConstants
+{
+ ///
+ /// Default receive buffer size for WebSocket messages (4 KB)
+ ///
+ public const int DefaultReceiveBufferSize = 4096;
+
+ ///
+ /// Default keep-alive interval in seconds
+ ///
+ public const int DefaultKeepAliveIntervalSeconds = 30;
+
+ ///
+ /// Default close timeout in minutes
+ ///
+ public const int DefaultCloseTimeoutMinutes = 10;
+
+ ///
+ /// Minimum buffer size for WebSocket operations (1 KB)
+ ///
+ public const int MinimumBufferSize = 1024;
+
+ ///
+ /// Default maximum message size (1 MB)
+ ///
+ public const int DefaultMaxMessageSize = 1024 * 1024;
+
+ ///
+ /// Proxy forward buffer size (4 KB)
+ ///
+ public const int ProxyForwardBufferSize = 4096;
+}
diff --git a/src/WireMock.Net.Abstractions/WireMock.Net.Abstractions.csproj b/src/WireMock.Net.Abstractions/WireMock.Net.Abstractions.csproj
index 1e1f1682..019e8a6b 100644
--- a/src/WireMock.Net.Abstractions/WireMock.Net.Abstractions.csproj
+++ b/src/WireMock.Net.Abstractions/WireMock.Net.Abstractions.csproj
@@ -36,7 +36,7 @@
-
+
diff --git a/src/WireMock.Net.Minimal/Owin/AspNetCoreSelfHost.cs b/src/WireMock.Net.Minimal/Owin/AspNetCoreSelfHost.cs
index 424c6e9d..bb696c2d 100644
--- a/src/WireMock.Net.Minimal/Owin/AspNetCoreSelfHost.cs
+++ b/src/WireMock.Net.Minimal/Owin/AspNetCoreSelfHost.cs
@@ -80,6 +80,14 @@ internal partial class AspNetCoreSelfHost : IOwinSelfHost
#if NET8_0_OR_GREATER
UseCors(appBuilder);
+
+ var webSocketOptions = new WebSocketOptions();
+ if (_wireMockMiddlewareOptions.WebSocketSettings?.KeepAliveIntervalSeconds != null)
+ {
+ webSocketOptions.KeepAliveInterval = TimeSpan.FromSeconds(_wireMockMiddlewareOptions.WebSocketSettings.KeepAliveIntervalSeconds);
+ }
+
+ appBuilder.UseWebSockets(webSocketOptions);
#endif
_wireMockMiddlewareOptions.PreWireMockMiddlewareInit?.Invoke(appBuilder);
diff --git a/src/WireMock.Net.Minimal/Owin/IWireMockMiddlewareOptions.cs b/src/WireMock.Net.Minimal/Owin/IWireMockMiddlewareOptions.cs
index 14ce7e50..55dd5e65 100644
--- a/src/WireMock.Net.Minimal/Owin/IWireMockMiddlewareOptions.cs
+++ b/src/WireMock.Net.Minimal/Owin/IWireMockMiddlewareOptions.cs
@@ -11,6 +11,7 @@ using WireMock.Matchers;
using WireMock.Settings;
using WireMock.Types;
using WireMock.Util;
+using WireMock.WebSockets;
using ClientCertificateMode = Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode;
namespace WireMock.Owin;
@@ -82,11 +83,21 @@ internal interface IWireMockMiddlewareOptions
QueryParameterMultipleValueSupport? QueryParameterMultipleValueSupport { get; set; }
- public bool ProxyAll { get; set; }
+ bool ProxyAll { get; set; }
///
/// Gets or sets the activity tracing options.
/// When set, System.Diagnostics.Activity objects are created for request tracing.
///
ActivityTracingOptions? ActivityTracingOptions { get; set; }
+
+ ///
+ /// The WebSocket connection registries per mapping (used for broadcast).
+ ///
+ ConcurrentDictionary WebSocketRegistries { get; }
+
+ ///
+ /// WebSocket settings.
+ ///
+ WebSocketSettings? WebSocketSettings { get; set; }
}
\ No newline at end of file
diff --git a/src/WireMock.Net.Minimal/Owin/Mappers/OwinResponseMapper.cs b/src/WireMock.Net.Minimal/Owin/Mappers/OwinResponseMapper.cs
index a6c65f11..b0618eca 100644
--- a/src/WireMock.Net.Minimal/Owin/Mappers/OwinResponseMapper.cs
+++ b/src/WireMock.Net.Minimal/Owin/Mappers/OwinResponseMapper.cs
@@ -15,6 +15,7 @@ using RandomDataGenerator.Randomizers;
using Stef.Validation;
using WireMock.Http;
using WireMock.ResponseBuilders;
+using WireMock.ResponseProviders;
using WireMock.Types;
using WireMock.Util;
@@ -58,7 +59,7 @@ namespace WireMock.Owin.Mappers
///
public async Task MapAsync(IResponseMessage? responseMessage, HttpResponse response)
{
- if (responseMessage == null)
+ if (responseMessage == null || responseMessage is WebSocketHandledResponse)
{
return;
}
diff --git a/src/WireMock.Net.Minimal/Owin/WireMockMiddleware.cs b/src/WireMock.Net.Minimal/Owin/WireMockMiddleware.cs
index 1cb84ce6..20c2f876 100644
--- a/src/WireMock.Net.Minimal/Owin/WireMockMiddleware.cs
+++ b/src/WireMock.Net.Minimal/Owin/WireMockMiddleware.cs
@@ -25,7 +25,6 @@ namespace WireMock.Owin;
internal class WireMockMiddleware
{
private readonly object _lock = new();
- private static readonly Task CompletedTask = Task.FromResult(false);
private readonly IWireMockMiddlewareOptions _options;
private readonly IOwinRequestMapper _requestMapper;
@@ -66,6 +65,9 @@ internal class WireMockMiddleware
private async Task InvokeInternalAsync(HttpContext ctx)
{
+ // Store options in HttpContext for providers to access (e.g., WebSocketResponseProvider)
+ ctx.Items[nameof(WireMockMiddlewareOptions)] = _options;
+
var request = await _requestMapper.MapAsync(ctx.Request, _options).ConfigureAwait(false);
var logRequest = false;
@@ -144,9 +146,7 @@ internal class WireMockMiddleware
var (theResponse, theOptionalNewMapping) = await targetMapping.ProvideResponseAsync(ctx, request).ConfigureAwait(false);
response = theResponse;
- var responseBuilder = targetMapping.Provider as Response;
-
- if (!targetMapping.IsAdminInterface && theOptionalNewMapping != null)
+ if (targetMapping.Provider is Response responseBuilder && !targetMapping.IsAdminInterface && theOptionalNewMapping != null)
{
if (responseBuilder?.ProxyAndRecordSettings?.SaveMapping == true || targetMapping.Settings.ProxyAndRecordSettings?.SaveMapping == true)
{
@@ -227,8 +227,6 @@ internal class WireMockMiddleware
await _responseMapper.MapAsync(notFoundResponse, ctx.Response).ConfigureAwait(false);
}
}
-
- await CompletedTask.ConfigureAwait(false);
}
private async Task SendToWebhooksAsync(IMapping mapping, IRequestMessage request, IResponseMessage response)
diff --git a/src/WireMock.Net.Minimal/Owin/WireMockMiddlewareOptions.cs b/src/WireMock.Net.Minimal/Owin/WireMockMiddlewareOptions.cs
index 002b5a1b..e07c85f8 100644
--- a/src/WireMock.Net.Minimal/Owin/WireMockMiddlewareOptions.cs
+++ b/src/WireMock.Net.Minimal/Owin/WireMockMiddlewareOptions.cs
@@ -8,10 +8,10 @@ using Microsoft.Extensions.DependencyInjection;
using WireMock.Handlers;
using WireMock.Logging;
using WireMock.Matchers;
-using WireMock.Owin.ActivityTracing;
using WireMock.Settings;
using WireMock.Types;
using WireMock.Util;
+using WireMock.WebSockets;
using ClientCertificateMode = Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode;
namespace WireMock.Owin;
@@ -40,7 +40,6 @@ internal class WireMockMiddlewareOptions : IWireMockMiddlewareOptions
public Action? PostWireMockMiddlewareInit { get; set; }
-//#if USE_ASPNETCORE
public Action? AdditionalServiceRegistration { get; set; }
public CorsPolicyOptions? CorsPolicyOptions { get; set; }
@@ -49,7 +48,6 @@ internal class WireMockMiddlewareOptions : IWireMockMiddlewareOptions
///
public bool AcceptAnyClientCertificate { get; set; }
- //#endif
///
public IFileSystemHandler? FileSystemHandler { get; set; }
@@ -107,4 +105,9 @@ internal class WireMockMiddlewareOptions : IWireMockMiddlewareOptions
///
public ActivityTracingOptions? ActivityTracingOptions { get; set; }
+
+ ///
+ public ConcurrentDictionary WebSocketRegistries { get; } = new();
+
+ public WebSocketSettings? WebSocketSettings { get; set; }
}
\ No newline at end of file
diff --git a/src/WireMock.Net.Minimal/RequestBuilders/Request.WithWebSocket.cs b/src/WireMock.Net.Minimal/RequestBuilders/Request.WithWebSocket.cs
new file mode 100644
index 00000000..5f47b2b7
--- /dev/null
+++ b/src/WireMock.Net.Minimal/RequestBuilders/Request.WithWebSocket.cs
@@ -0,0 +1,43 @@
+// Copyright © WireMock.Net
+
+using System.Linq;
+using WireMock.Matchers;
+using WireMock.Matchers.Request;
+
+namespace WireMock.RequestBuilders;
+
+public partial class Request
+{
+ ///
+ public IRequestBuilder WithWebSocketUpgrade(params string[] protocols)
+ {
+ _requestMatchers.Add(new RequestMessageHeaderMatcher(
+ MatchBehaviour.AcceptOnMatch,
+ MatchOperator.Or,
+ "Upgrade",
+ true,
+ new ExactMatcher(true, "websocket")
+ ));
+
+ _requestMatchers.Add(new RequestMessageHeaderMatcher(
+ MatchBehaviour.AcceptOnMatch,
+ MatchOperator.Or,
+ "Connection",
+ true,
+ new WildcardMatcher("*Upgrade*", true)
+ ));
+
+ if (protocols.Length > 0)
+ {
+ _requestMatchers.Add(new RequestMessageHeaderMatcher(
+ MatchBehaviour.AcceptOnMatch,
+ MatchOperator.Or,
+ "Sec-WebSocket-Protocol",
+ true,
+ protocols.Select(p => new ExactMatcher(true, p)).ToArray()
+ ));
+ }
+
+ return this;
+ }
+}
\ No newline at end of file
diff --git a/src/WireMock.Net.Minimal/ResponseBuilders/Response.WithWebSocket.cs b/src/WireMock.Net.Minimal/ResponseBuilders/Response.WithWebSocket.cs
new file mode 100644
index 00000000..93c14714
--- /dev/null
+++ b/src/WireMock.Net.Minimal/ResponseBuilders/Response.WithWebSocket.cs
@@ -0,0 +1,49 @@
+// Copyright © WireMock.Net
+
+using System;
+using WireMock.Settings;
+using WireMock.WebSockets;
+
+namespace WireMock.ResponseBuilders;
+
+public partial class Response
+{
+ ///
+ /// Internal property to store WebSocket builder configuration
+ ///
+ internal WebSocketBuilder? WebSocketBuilder { get; set; }
+
+ ///
+ /// Configure WebSocket response behavior
+ ///
+ public IResponseBuilder WithWebSocket(Action configure)
+ {
+ var builder = new WebSocketBuilder();
+ configure(builder);
+
+ WebSocketBuilder = builder;
+
+ return this;
+ }
+
+ ///
+ /// Proxy WebSocket to another server
+ ///
+ public IResponseBuilder WithWebSocketProxy(string targetUrl)
+ {
+ return WithWebSocketProxy(new ProxyAndRecordSettings { Url = targetUrl });
+ }
+
+ ///
+ /// Proxy WebSocket to another server with settings
+ ///
+ public IResponseBuilder WithWebSocketProxy(ProxyAndRecordSettings settings)
+ {
+ var builder = new WebSocketBuilder();
+ builder.WithProxy(settings);
+
+ WebSocketBuilder = builder;
+
+ return this;
+ }
+}
\ No newline at end of file
diff --git a/src/WireMock.Net.Minimal/ResponseMessage.cs b/src/WireMock.Net.Minimal/ResponseMessage.cs
index e97cd929..3c269f64 100644
--- a/src/WireMock.Net.Minimal/ResponseMessage.cs
+++ b/src/WireMock.Net.Minimal/ResponseMessage.cs
@@ -8,6 +8,7 @@ using WireMock.ResponseBuilders;
using WireMock.Types;
using WireMock.Util;
using Stef.Validation;
+using WireMock.WebSockets;
namespace WireMock;
diff --git a/src/WireMock.Net.Minimal/ResponseProviders/WebSocketResponseProvider.cs b/src/WireMock.Net.Minimal/ResponseProviders/WebSocketResponseProvider.cs
new file mode 100644
index 00000000..ce80fa8d
--- /dev/null
+++ b/src/WireMock.Net.Minimal/ResponseProviders/WebSocketResponseProvider.cs
@@ -0,0 +1,345 @@
+// Copyright © WireMock.Net
+
+using System;
+using System.Net;
+using System.Net.WebSockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Stef.Validation;
+using WireMock.Constants;
+using WireMock.Owin;
+using WireMock.Settings;
+using WireMock.WebSockets;
+
+namespace WireMock.ResponseProviders;
+
+internal class WebSocketResponseProvider : IResponseProvider
+{
+ private readonly WebSocketBuilder _builder;
+
+ public WebSocketResponseProvider(WebSocketBuilder builder)
+ {
+ _builder = Guard.NotNull(builder);
+ }
+
+ public async Task<(IResponseMessage Message, IMapping? Mapping)> ProvideResponseAsync(
+ IMapping mapping,
+ HttpContext context,
+ IRequestMessage requestMessage,
+ WireMockServerSettings settings)
+ {
+ // Check if this is a WebSocket upgrade request
+ if (!context.WebSockets.IsWebSocketRequest)
+ {
+ return (ResponseMessageBuilder.Create(HttpStatusCode.BadRequest, "Bad Request: Not a WebSocket upgrade request"), null);
+ }
+
+ try
+ {
+ // Accept the WebSocket connection
+#if NET8_0_OR_GREATER
+ var acceptContext = new WebSocketAcceptContext
+ {
+ SubProtocol = _builder.AcceptProtocol,
+ KeepAliveInterval = _builder.KeepAliveIntervalSeconds ?? TimeSpan.FromSeconds(WebSocketConstants.DefaultKeepAliveIntervalSeconds)
+
+ };
+ var webSocket = await context.WebSockets.AcceptWebSocketAsync(acceptContext).ConfigureAwait(false);
+#else
+ var webSocket = await context.WebSockets.AcceptWebSocketAsync(_builder.AcceptProtocol).ConfigureAwait(false);
+#endif
+
+ // Get options from HttpContext.Items (set by WireMockMiddleware)
+ if (!context.Items.TryGetValue(nameof(WireMockMiddlewareOptions), out var optionsObj) ||
+ optionsObj is not IWireMockMiddlewareOptions options)
+ {
+ throw new InvalidOperationException("WireMockMiddlewareOptions not found in HttpContext.Items");
+ }
+
+ // Get or create registry from options (not from server)
+ var registry = _builder.IsBroadcast
+ ? options.WebSocketRegistries.GetOrAdd(mapping.Guid, _ => new WebSocketConnectionRegistry())
+ : null;
+
+ // Create WebSocket context
+ var wsContext = new WireMockWebSocketContext(
+ context,
+ webSocket,
+ requestMessage,
+ mapping,
+ registry,
+ _builder
+ );
+
+ // Update scenario state following the same pattern as WireMockMiddleware
+ if (mapping.Scenario != null)
+ {
+ wsContext.UpdateScenarioState();
+ }
+
+ // Add to registry if broadcast is enabled
+ if (registry != null)
+ {
+ registry.AddConnection(wsContext);
+ }
+
+ try
+ {
+ // Handle the WebSocket based on configuration
+ if (_builder.ProxySettings != null)
+ {
+ await HandleProxyAsync(wsContext, _builder.ProxySettings).ConfigureAwait(false);
+ }
+ else if (_builder.IsEcho)
+ {
+ await HandleEchoAsync(wsContext).ConfigureAwait(false);
+ }
+ else if (_builder.MessageHandler != null)
+ {
+ await HandleCustomAsync(wsContext, _builder.MessageHandler).ConfigureAwait(false);
+ }
+ else if (_builder.MessageSequence != null)
+ {
+ await HandleSequenceAsync(wsContext, _builder.MessageSequence).ConfigureAwait(false);
+ }
+ else
+ {
+ // Default: keep connection open until client closes
+ await WaitForCloseAsync(wsContext).ConfigureAwait(false);
+ }
+ }
+ finally
+ {
+ // Remove from registry
+ if (registry != null)
+ {
+ registry.RemoveConnection(wsContext.ConnectionId);
+ }
+ }
+
+ // Return special marker to indicate WebSocket was handled
+ return (new WebSocketHandledResponse(), null);
+ }
+ catch (Exception ex)
+ {
+ settings.Logger?.Error($"WebSocket error for mapping '{mapping.Guid}': {ex.Message}", ex);
+
+ // If we haven't upgraded yet, we can return HTTP error
+ if (!context.Response.HasStarted)
+ {
+ return (ResponseMessageBuilder.Create(HttpStatusCode.InternalServerError, $"WebSocket error: {ex.Message}"), null);
+ }
+
+ // Already upgraded - return marker
+ return (new WebSocketHandledResponse(), null);
+ }
+ }
+
+ private async Task HandleEchoAsync(WireMockWebSocketContext context)
+ {
+ var bufferSize = context.Builder.MaxMessageSize ?? WebSocketConstants.DefaultReceiveBufferSize;
+ var buffer = new byte[bufferSize];
+ var timeout = context.Builder.CloseTimeout ?? TimeSpan.FromMinutes(WebSocketConstants.DefaultCloseTimeoutMinutes);
+ var cts = new CancellationTokenSource(timeout);
+
+ try
+ {
+ while (context.WebSocket.State == WebSocketState.Open && !cts.Token.IsCancellationRequested)
+ {
+ var result = await context.WebSocket.ReceiveAsync(
+ new ArraySegment(buffer),
+ cts.Token
+ ).ConfigureAwait(false);
+
+ if (result.MessageType == WebSocketMessageType.Close)
+ {
+ await context.CloseAsync(
+ WebSocketCloseStatus.NormalClosure,
+ "Closed by client"
+ ).ConfigureAwait(false);
+ break;
+ }
+
+ // Echo back
+ await context.WebSocket.SendAsync(
+ new ArraySegment(buffer, 0, result.Count),
+ result.MessageType,
+ result.EndOfMessage,
+ cts.Token
+ ).ConfigureAwait(false);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ if (context.WebSocket.State == WebSocketState.Open)
+ {
+ await context.CloseAsync(WebSocketCloseStatus.NormalClosure, "Timeout");
+ }
+ }
+ }
+
+ private async Task HandleCustomAsync(
+ WireMockWebSocketContext context,
+ Func handler)
+ {
+ var bufferSize = context.Builder.MaxMessageSize ?? WebSocketConstants.DefaultReceiveBufferSize;
+ var buffer = new byte[bufferSize];
+ var timeout = context.Builder.CloseTimeout ?? TimeSpan.FromMinutes(WebSocketConstants.DefaultCloseTimeoutMinutes);
+ var cts = new CancellationTokenSource(timeout);
+
+ try
+ {
+ while (context.WebSocket.State == WebSocketState.Open && !cts.Token.IsCancellationRequested)
+ {
+ var result = await context.WebSocket.ReceiveAsync(
+ new ArraySegment(buffer),
+ cts.Token
+ ).ConfigureAwait(false);
+
+ if (result.MessageType == WebSocketMessageType.Close)
+ {
+ await context.CloseAsync(
+ WebSocketCloseStatus.NormalClosure,
+ "Closed by client"
+ ).ConfigureAwait(false);
+ break;
+ }
+
+ var message = CreateWebSocketMessage(result, buffer);
+
+ // Call custom handler
+ await handler(message, context).ConfigureAwait(false);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ if (context.WebSocket.State == WebSocketState.Open)
+ {
+ await context.CloseAsync(WebSocketCloseStatus.NormalClosure, "Timeout");
+ }
+ }
+ }
+
+ private async Task HandleSequenceAsync(WireMockWebSocketContext context, WebSocketMessageSequence sequence)
+ {
+ await sequence.ExecuteAsync(context).ConfigureAwait(false);
+ }
+
+ private async Task HandleProxyAsync(WireMockWebSocketContext context, ProxyAndRecordSettings settings)
+ {
+ using var clientWebSocket = new ClientWebSocket();
+
+ var targetUri = new Uri(settings.Url);
+ await clientWebSocket.ConnectAsync(targetUri, CancellationToken.None).ConfigureAwait(false);
+
+ // Bidirectional proxy
+ var clientToServer = ForwardMessagesAsync(context.WebSocket, clientWebSocket);
+ var serverToClient = ForwardMessagesAsync(clientWebSocket, context.WebSocket);
+
+ await Task.WhenAny(clientToServer, serverToClient).ConfigureAwait(false);
+
+ // Close both
+ if (context.WebSocket.State == WebSocketState.Open)
+ {
+ await context.CloseAsync(WebSocketCloseStatus.NormalClosure, "Proxy closed");
+ }
+ if (clientWebSocket.State == WebSocketState.Open)
+ {
+ await clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Proxy closed", CancellationToken.None);
+ }
+ }
+
+ private async Task ForwardMessagesAsync(WebSocket source, WebSocket destination)
+ {
+ var buffer = new byte[WebSocketConstants.ProxyForwardBufferSize];
+
+ while (source.State == WebSocketState.Open && destination.State == WebSocketState.Open)
+ {
+ var result = await source.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None);
+
+ if (result.MessageType == WebSocketMessageType.Close)
+ {
+ await destination.CloseAsync(
+ result.CloseStatus ?? WebSocketCloseStatus.NormalClosure,
+ result.CloseStatusDescription,
+ CancellationToken.None
+ );
+ break;
+ }
+
+ await destination.SendAsync(
+ new ArraySegment(buffer, 0, result.Count),
+ result.MessageType,
+ result.EndOfMessage,
+ CancellationToken.None
+ );
+ }
+ }
+
+ private async Task WaitForCloseAsync(WireMockWebSocketContext context)
+ {
+ var buffer = new byte[WebSocketConstants.MinimumBufferSize];
+ var timeout = context.Builder.CloseTimeout ?? TimeSpan.FromMinutes(WebSocketConstants.DefaultCloseTimeoutMinutes);
+ var cts = new CancellationTokenSource(timeout);
+
+ try
+ {
+ while (context.WebSocket.State == WebSocketState.Open && !cts.Token.IsCancellationRequested)
+ {
+ var result = await context.WebSocket.ReceiveAsync(
+ new ArraySegment(buffer),
+ cts.Token
+ );
+
+ if (result.MessageType == WebSocketMessageType.Close)
+ {
+ await context.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed by client");
+ break;
+ }
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ if (context.WebSocket.State == WebSocketState.Open)
+ {
+ await context.CloseAsync(WebSocketCloseStatus.NormalClosure, "Timeout");
+ }
+ }
+ }
+
+ private static WebSocketMessage CreateWebSocketMessage(WebSocketReceiveResult result, byte[] buffer)
+ {
+ var message = new WebSocketMessage
+ {
+ MessageType = result.MessageType,
+ EndOfMessage = result.EndOfMessage,
+ Timestamp = DateTime.UtcNow
+ };
+
+ if (result.MessageType == WebSocketMessageType.Text)
+ {
+ message.Text = Encoding.UTF8.GetString(buffer, 0, result.Count);
+ }
+ else
+ {
+ message.Bytes = new byte[result.Count];
+ Array.Copy(buffer, message.Bytes, result.Count);
+ }
+
+ return message;
+ }
+}
+
+///
+/// Special response marker to indicate WebSocket has been handled
+///
+internal class WebSocketHandledResponse : ResponseMessage
+{
+ public WebSocketHandledResponse()
+ {
+ // 101 Switching Protocols
+ StatusCode = (int)HttpStatusCode.SwitchingProtocols;
+ }
+}
\ No newline at end of file
diff --git a/src/WireMock.Net.Minimal/Serialization/MappingFileNameSanitizer.cs b/src/WireMock.Net.Minimal/Serialization/MappingFileNameSanitizer.cs
index e00651be..94cefa2b 100644
--- a/src/WireMock.Net.Minimal/Serialization/MappingFileNameSanitizer.cs
+++ b/src/WireMock.Net.Minimal/Serialization/MappingFileNameSanitizer.cs
@@ -30,7 +30,7 @@ public class MappingFileNameSanitizer
if (!string.IsNullOrEmpty(mapping.Title))
{
// remove 'Proxy Mapping for ' and an extra space character after the HTTP request method
- name = mapping.Title.Replace(ProxyAndRecordSettings.DefaultPrefixForSavedMappingFile, "").Replace(' '.ToString(), string.Empty);
+ name = mapping.Title!.Replace(ProxyAndRecordSettings.DefaultPrefixForSavedMappingFile, "").Replace(' '.ToString(), string.Empty);
if (_settings.ProxyAndRecordSettings?.AppendGuidToSavedMappingFile == true)
{
name += $"{ReplaceChar}{mapping.Guid}";
diff --git a/src/WireMock.Net.Minimal/Server/RespondWithAProvider.cs b/src/WireMock.Net.Minimal/Server/RespondWithAProvider.cs
index 510252b8..58e86abc 100644
--- a/src/WireMock.Net.Minimal/Server/RespondWithAProvider.cs
+++ b/src/WireMock.Net.Minimal/Server/RespondWithAProvider.cs
@@ -37,7 +37,7 @@ internal class RespondWithAProvider : IRespondWithAProvider
private int _timesInSameState = 1;
private bool? _useWebhookFireAndForget;
private double? _probability;
- private GraphQLSchemaDetails? _graphQLSchemaDetails;
+ private GraphQLSchemaDetails? _graphQLSchemaDetails; // Future Use.
public Guid Guid { get; private set; }
@@ -79,6 +79,12 @@ internal class RespondWithAProvider : IRespondWithAProvider
///
public void RespondWith(IResponseProvider provider)
{
+ if (provider is Response response && response.WebSocketBuilder != null)
+ {
+ // If the provider is a Response with a WebSocketBuilder, we need to use a WebSocketResponseProvider instead.
+ provider = new WebSocketResponseProvider(response.WebSocketBuilder);
+ }
+
var mapping = new Mapping
(
Guid,
diff --git a/src/WireMock.Net.Minimal/Server/WireMockServer.AdminFiles.cs b/src/WireMock.Net.Minimal/Server/WireMockServer.AdminFiles.cs
index 164970ea..a80e2ee2 100644
--- a/src/WireMock.Net.Minimal/Server/WireMockServer.AdminFiles.cs
+++ b/src/WireMock.Net.Minimal/Server/WireMockServer.AdminFiles.cs
@@ -106,7 +106,6 @@ public partial class WireMockServer
/// Checks if file exists.
/// Note: Response is returned with no body as a head request doesn't accept a body, only the status code.
///
- /// The request message.
private IResponseMessage FileHead(HttpContext _, IRequestMessage requestMessage)
{
var filename = GetFileNameFromRequestMessage(requestMessage);
diff --git a/src/WireMock.Net.Minimal/Server/WireMockServer.WebSocket.cs b/src/WireMock.Net.Minimal/Server/WireMockServer.WebSocket.cs
new file mode 100644
index 00000000..aef3f354
--- /dev/null
+++ b/src/WireMock.Net.Minimal/Server/WireMockServer.WebSocket.cs
@@ -0,0 +1,77 @@
+// Copyright © WireMock.Net
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.WebSockets;
+using System.Threading.Tasks;
+using JetBrains.Annotations;
+using WireMock.WebSockets;
+
+namespace WireMock.Server;
+
+public partial class WireMockServer
+{
+ ///
+ /// Get all active WebSocket connections
+ ///
+ [PublicAPI]
+ public IReadOnlyCollection GetWebSocketConnections()
+ {
+ return _options.WebSocketRegistries.Values
+ .SelectMany(r => r.GetConnections())
+ .ToList();
+ }
+
+ ///
+ /// Get WebSocket connections for a specific mapping
+ ///
+ [PublicAPI]
+ public IReadOnlyCollection GetWebSocketConnections(Guid mappingGuid)
+ {
+ return _options.WebSocketRegistries.TryGetValue(mappingGuid, out var registry) ? registry.GetConnections() : [];
+ }
+
+ ///
+ /// Close a specific WebSocket connection
+ ///
+ [PublicAPI]
+ public async Task CloseWebSocketConnectionAsync(
+ Guid connectionId,
+ WebSocketCloseStatus closeStatus = WebSocketCloseStatus.NormalClosure,
+ string statusDescription = "Closed by server")
+ {
+ foreach (var registry in _options.WebSocketRegistries.Values)
+ {
+ if (registry.TryGetConnection(connectionId, out var connection))
+ {
+ await connection.CloseAsync(closeStatus, statusDescription);
+ return;
+ }
+ }
+ }
+
+ ///
+ /// Broadcast a message to all WebSocket connections in a specific mapping
+ ///
+ [PublicAPI]
+ public async Task BroadcastToWebSocketsAsync(Guid mappingGuid, string text)
+ {
+ if (_options.WebSocketRegistries.TryGetValue(mappingGuid, out var registry))
+ {
+ await registry.BroadcastTextAsync(text);
+ }
+ }
+
+ ///
+ /// Broadcast a message to all WebSocket connections
+ ///
+ [PublicAPI]
+ public async Task BroadcastToAllWebSocketsAsync(string text)
+ {
+ foreach (var registry in _options.WebSocketRegistries.Values)
+ {
+ await registry.BroadcastTextAsync(text);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/WireMock.Net.Minimal/Settings/WireMockServerSettingsParser.cs b/src/WireMock.Net.Minimal/Settings/WireMockServerSettingsParser.cs
index 6cd639a5..f038afab 100644
--- a/src/WireMock.Net.Minimal/Settings/WireMockServerSettingsParser.cs
+++ b/src/WireMock.Net.Minimal/Settings/WireMockServerSettingsParser.cs
@@ -7,6 +7,7 @@ using System.Globalization;
using System.Linq;
using JetBrains.Annotations;
using Stef.Validation;
+using WireMock.Constants;
using WireMock.Logging;
using WireMock.Models;
using WireMock.Types;
@@ -86,6 +87,7 @@ public static class WireMockServerSettingsParser
ParseCertificateSettings(settings, parser);
ParseHandlebarsSettings(settings, parser);
ParseActivityTracingSettings(settings, parser);
+ ParseWebSocketSettings(settings, parser);
return true;
}
@@ -242,4 +244,20 @@ public static class WireMockServerSettingsParser
};
}
}
+
+ private static void ParseWebSocketSettings(WireMockServerSettings settings, SimpleSettingsParser parser)
+ {
+ // Check if any WebSocket setting is present
+ if (parser.ContainsAny(
+ nameof(WebSocketSettings) + '.' + nameof(WebSocketSettings.MaxConnections),
+ nameof(WebSocketSettings) + '.' + nameof(WebSocketSettings.KeepAliveIntervalSeconds))
+ )
+ {
+ settings.WebSocketSettings = new WebSocketSettings
+ {
+ MaxConnections = parser.GetIntValue(nameof(WebSocketSettings) + '.' + nameof(WebSocketSettings.MaxConnections), 100),
+ KeepAliveIntervalSeconds = parser.GetIntValue(nameof(WebSocketSettings) + '.' + nameof(WebSocketSettings.KeepAliveIntervalSeconds), WebSocketConstants.DefaultKeepAliveIntervalSeconds),
+ };
+ }
+ }
}
\ No newline at end of file
diff --git a/src/WireMock.Net.Minimal/Util/TinyMapperUtils.cs b/src/WireMock.Net.Minimal/Util/TinyMapperUtils.cs
index 5fcee4cd..70a19d36 100644
--- a/src/WireMock.Net.Minimal/Util/TinyMapperUtils.cs
+++ b/src/WireMock.Net.Minimal/Util/TinyMapperUtils.cs
@@ -1,5 +1,6 @@
// Copyright © WireMock.Net
+using System.Diagnostics.CodeAnalysis;
using Nelibur.ObjectMapper;
using WireMock.Admin.Mappings;
using WireMock.Admin.Settings;
@@ -7,6 +8,7 @@ using WireMock.Settings;
namespace WireMock.Util;
+[SuppressMessage("Performance", "CA1822:Mark members as static")]
internal sealed class TinyMapperUtils
{
public static TinyMapperUtils Instance { get; } = new();
@@ -22,6 +24,9 @@ internal sealed class TinyMapperUtils
TinyMapper.Bind();
TinyMapper.Bind();
TinyMapper.Bind();
+
+ TinyMapper.Bind();
+ TinyMapper.Bind();
}
public ProxyAndRecordSettingsModel? Map(ProxyAndRecordSettings? instance)
@@ -53,4 +58,14 @@ internal sealed class TinyMapperUtils
{
return model == null ? null : TinyMapper.Map(model);
}
+
+ public WebSocketSettingsModel? Map(WebSocketSettings? instance)
+ {
+ return instance == null ? null : TinyMapper.Map(instance);
+ }
+
+ public WebSocketSettings? Map(WebSocketSettingsModel? model)
+ {
+ return model == null ? null : TinyMapper.Map(model);
+ }
}
\ No newline at end of file
diff --git a/src/WireMock.Net.Minimal/WebSockets/WebSocketBuilder.cs b/src/WireMock.Net.Minimal/WebSockets/WebSocketBuilder.cs
new file mode 100644
index 00000000..6f24ca80
--- /dev/null
+++ b/src/WireMock.Net.Minimal/WebSockets/WebSocketBuilder.cs
@@ -0,0 +1,133 @@
+// Copyright © WireMock.Net
+
+using System;
+using System.Threading.Tasks;
+using Stef.Validation;
+using WireMock.Settings;
+using WireMock.Types;
+
+namespace WireMock.WebSockets;
+
+internal class WebSocketBuilder : IWebSocketBuilder
+{
+ ///
+ public string? AcceptProtocol { get; private set; }
+
+ ///
+ public bool IsEcho { get; private set; }
+
+ ///
+ public bool IsBroadcast { get; private set; }
+
+ ///
+ public Func? MessageHandler { get; private set; }
+
+ ///
+ public WebSocketMessageSequence? MessageSequence { get; private set; }
+
+ ///
+ public ProxyAndRecordSettings? ProxySettings { get; private set; }
+
+ ///
+ public TimeSpan? CloseTimeout { get; private set; }
+
+ ///
+ public int? MaxMessageSize { get; private set; }
+
+ ///
+ public int? ReceiveBufferSize { get; private set; }
+
+ ///
+ public TimeSpan? KeepAliveIntervalSeconds { get; private set; }
+
+ ///
+ public bool UseTransformer { get; private set; }
+
+ ///
+ public TransformerType TransformerType { get; private set; }
+
+ ///
+ public bool UseTransformerForBodyAsFile { get; private set; }
+
+ ///
+ public ReplaceNodeOptions TransformerReplaceNodeOptions { get; private set; }
+
+ public IWebSocketBuilder WithAcceptProtocol(string protocol)
+ {
+ AcceptProtocol = Guard.NotNull(protocol);
+ return this;
+ }
+
+ public IWebSocketBuilder WithEcho()
+ {
+ IsEcho = true;
+ return this;
+ }
+
+ public IWebSocketBuilder WithMessageHandler(
+ Func handler)
+ {
+ MessageHandler = Guard.NotNull(handler);
+ IsEcho = false; // Disable echo if custom handler is set
+ return this;
+ }
+
+ public IWebSocketBuilder WithMessageSequence(
+ Action configure)
+ {
+ var sequenceBuilder = new WebSocketMessageSequenceBuilder();
+ configure(sequenceBuilder);
+ MessageSequence = sequenceBuilder.Build();
+ IsEcho = false;
+ return this;
+ }
+
+ public IWebSocketBuilder WithBroadcast()
+ {
+ IsBroadcast = true;
+ return this;
+ }
+
+ public IWebSocketBuilder WithProxy(ProxyAndRecordSettings settings)
+ {
+ ProxySettings = Guard.NotNull(settings);
+ IsEcho = false;
+ return this;
+ }
+
+ public IWebSocketBuilder WithCloseTimeout(TimeSpan timeout)
+ {
+ CloseTimeout = timeout;
+ return this;
+ }
+
+ public IWebSocketBuilder WithMaxMessageSize(int sizeInBytes)
+ {
+ MaxMessageSize = Guard.Condition(sizeInBytes, s => s > 0);
+ return this;
+ }
+
+ public IWebSocketBuilder WithReceiveBufferSize(int sizeInBytes)
+ {
+ ReceiveBufferSize = Guard.Condition(sizeInBytes, s => s > 0);
+ return this;
+ }
+
+ public IWebSocketBuilder WithKeepAliveInterval(TimeSpan interval)
+ {
+ KeepAliveIntervalSeconds = interval;
+ return this;
+ }
+
+ public IWebSocketBuilder WithTransformer(
+ TransformerType transformerType = TransformerType.Handlebars,
+ bool useTransformerForBodyAsFile = false,
+ ReplaceNodeOptions transformerReplaceNodeOptions = ReplaceNodeOptions.EvaluateAndTryToConvert)
+ {
+ UseTransformer = true;
+ TransformerType = transformerType;
+ UseTransformerForBodyAsFile = useTransformerForBodyAsFile;
+ TransformerReplaceNodeOptions = transformerReplaceNodeOptions;
+ return this;
+ }
+}
\ No newline at end of file
diff --git a/src/WireMock.Net.Minimal/WebSockets/WebSocketConnectionRegistry.cs b/src/WireMock.Net.Minimal/WebSockets/WebSocketConnectionRegistry.cs
new file mode 100644
index 00000000..c41eb696
--- /dev/null
+++ b/src/WireMock.Net.Minimal/WebSockets/WebSocketConnectionRegistry.cs
@@ -0,0 +1,74 @@
+// Copyright © WireMock.Net
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Net.WebSockets;
+using System.Threading;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+
+namespace WireMock.WebSockets;
+
+///
+/// Registry for managing WebSocket connections per mapping
+///
+internal class WebSocketConnectionRegistry
+{
+ private readonly ConcurrentDictionary _connections = new();
+
+ ///
+ /// Add a connection to the registry
+ ///
+ public void AddConnection(WireMockWebSocketContext context)
+ {
+ _connections.TryAdd(context.ConnectionId, context);
+ }
+
+ ///
+ /// Remove a connection from the registry
+ ///
+ public void RemoveConnection(Guid connectionId)
+ {
+ _connections.TryRemove(connectionId, out _);
+ }
+
+ ///
+ /// Get all connections
+ ///
+ public IReadOnlyCollection GetConnections()
+ {
+ return _connections.Values.ToList();
+ }
+
+ ///
+ /// Try to get a specific connection
+ ///
+ public bool TryGetConnection(Guid connectionId, [NotNullWhen(true)] out WireMockWebSocketContext? connection)
+ {
+ return _connections.TryGetValue(connectionId, out connection);
+ }
+
+ ///
+ /// Broadcast text to all connections
+ ///
+ public async Task BroadcastTextAsync(string text, CancellationToken cancellationToken = default)
+ {
+ var tasks = _connections.Values
+ .Where(c => c.WebSocket.State == WebSocketState.Open)
+ .Select(c => c.SendTextAsync(text, cancellationToken));
+
+ await Task.WhenAll(tasks);
+ }
+
+ ///
+ /// Broadcast JSON to all connections
+ ///
+ public async Task BroadcastJsonAsync(object data, CancellationToken cancellationToken = default)
+ {
+ var json = JsonConvert.SerializeObject(data);
+ await BroadcastTextAsync(json, cancellationToken);
+ }
+}
\ No newline at end of file
diff --git a/src/WireMock.Net.Minimal/WebSockets/WebSocketMessageSequence.cs b/src/WireMock.Net.Minimal/WebSockets/WebSocketMessageSequence.cs
new file mode 100644
index 00000000..1e5850b9
--- /dev/null
+++ b/src/WireMock.Net.Minimal/WebSockets/WebSocketMessageSequence.cs
@@ -0,0 +1,14 @@
+// Copyright © WireMock.Net
+
+using System.Threading.Tasks;
+
+namespace WireMock.WebSockets;
+
+// Placeholder classes for future implementation
+internal class WebSocketMessageSequence
+{
+ public Task ExecuteAsync(WireMockWebSocketContext context)
+ {
+ return Task.CompletedTask;
+ }
+}
diff --git a/src/WireMock.Net.Minimal/WebSockets/WebSocketMessageSequenceBuilder.cs b/src/WireMock.Net.Minimal/WebSockets/WebSocketMessageSequenceBuilder.cs
new file mode 100644
index 00000000..e8acd0f8
--- /dev/null
+++ b/src/WireMock.Net.Minimal/WebSockets/WebSocketMessageSequenceBuilder.cs
@@ -0,0 +1,11 @@
+// Copyright © WireMock.Net
+
+namespace WireMock.WebSockets;
+
+internal class WebSocketMessageSequenceBuilder : IWebSocketMessageSequenceBuilder
+{
+ public WebSocketMessageSequence Build()
+ {
+ return new WebSocketMessageSequence();
+ }
+}
diff --git a/src/WireMock.Net.Minimal/WebSockets/WireMockWebSocketContext.cs b/src/WireMock.Net.Minimal/WebSockets/WireMockWebSocketContext.cs
new file mode 100644
index 00000000..291728c7
--- /dev/null
+++ b/src/WireMock.Net.Minimal/WebSockets/WireMockWebSocketContext.cs
@@ -0,0 +1,195 @@
+// Copyright © WireMock.Net
+
+using System;
+using System.Net.WebSockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Newtonsoft.Json;
+using Stef.Validation;
+using WireMock.Owin;
+
+namespace WireMock.WebSockets;
+
+///
+/// WebSocket context implementation
+///
+public class WireMockWebSocketContext : IWebSocketContext
+{
+ private readonly IWireMockMiddlewareOptions _options;
+
+ ///
+ public Guid ConnectionId { get; } = Guid.NewGuid();
+
+ ///
+ public HttpContext HttpContext { get; }
+
+ ///
+ public WebSocket WebSocket { get; }
+
+ ///
+ public IRequestMessage RequestMessage { get; }
+
+ ///
+ public IMapping Mapping { get; }
+
+ internal WebSocketConnectionRegistry? Registry { get; }
+ internal WebSocketBuilder Builder { get; }
+
+ ///
+ /// Creates a new WebSocketContext
+ ///
+ internal WireMockWebSocketContext(
+ HttpContext httpContext,
+ WebSocket webSocket,
+ IRequestMessage requestMessage,
+ IMapping mapping,
+ WebSocketConnectionRegistry? registry,
+ WebSocketBuilder builder)
+ {
+ HttpContext = Guard.NotNull(httpContext);
+ WebSocket = Guard.NotNull(webSocket);
+ RequestMessage = Guard.NotNull(requestMessage);
+ Mapping = Guard.NotNull(mapping);
+ Registry = registry;
+ Builder = Guard.NotNull(builder);
+
+ // Get options from HttpContext
+ if (httpContext.Items.TryGetValue("WireMockMiddlewareOptions", out var options))
+ {
+ _options = (IWireMockMiddlewareOptions)options!;
+ }
+ else
+ {
+ throw new InvalidOperationException("WireMockMiddlewareOptions not found in HttpContext.Items");
+ }
+ }
+
+ ///
+ public Task SendTextAsync(string text, CancellationToken cancellationToken = default)
+ {
+ var bytes = Encoding.UTF8.GetBytes(text);
+ return WebSocket.SendAsync(
+ new ArraySegment(bytes),
+ WebSocketMessageType.Text,
+ true,
+ cancellationToken
+ );
+ }
+
+ ///
+ public Task SendBytesAsync(byte[] bytes, CancellationToken cancellationToken = default)
+ {
+ return WebSocket.SendAsync(
+ new ArraySegment(bytes),
+ WebSocketMessageType.Binary,
+ true,
+ cancellationToken
+ );
+ }
+
+ ///
+ public Task SendJsonAsync(object data, CancellationToken cancellationToken = default)
+ {
+ var json = JsonConvert.SerializeObject(data);
+ return SendTextAsync(json, cancellationToken);
+ }
+
+ ///
+ public Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription)
+ {
+ return WebSocket.CloseAsync(closeStatus, statusDescription, CancellationToken.None);
+ }
+
+ ///
+ public void SetScenarioState(string nextState)
+ {
+ SetScenarioState(nextState, null);
+ }
+
+ ///
+ public void SetScenarioState(string nextState, string? description)
+ {
+ if (Mapping.Scenario == null)
+ {
+ return;
+ }
+
+ // Use the same logic as WireMockMiddleware
+ if (_options.Scenarios.TryGetValue(Mapping.Scenario, out var scenarioState))
+ {
+ // Directly set the next state (bypass counter logic for manual WebSocket state changes)
+ scenarioState.NextState = nextState;
+ scenarioState.Started = true;
+ scenarioState.Finished = nextState == null;
+
+ // Reset counter when manually setting state
+ scenarioState.Counter = 0;
+ }
+ else
+ {
+ // Create new scenario state if it doesn't exist
+ _options.Scenarios.TryAdd(Mapping.Scenario, new ScenarioState
+ {
+ Name = Mapping.Scenario,
+ NextState = nextState,
+ Started = true,
+ Finished = nextState == null,
+ Counter = 0
+ });
+ }
+ }
+
+ ///
+ /// Update scenario state following the same pattern as WireMockMiddleware.UpdateScenarioState
+ /// This is called automatically when the WebSocket connection is established.
+ ///
+ internal void UpdateScenarioState()
+ {
+ if (Mapping.Scenario == null)
+ {
+ return;
+ }
+
+ // Ensure scenario exists
+ if (!_options.Scenarios.TryGetValue(Mapping.Scenario, out var scenario))
+ {
+ return;
+ }
+
+ // Follow exact same logic as WireMockMiddleware.UpdateScenarioState
+ // Increase the number of times this state has been executed
+ scenario.Counter++;
+
+ // Only if the number of times this state is executed equals the required StateTimes,
+ // proceed to next state and reset the counter to 0
+ if (scenario.Counter == (Mapping.TimesInSameState ?? 1))
+ {
+ scenario.NextState = Mapping.NextState;
+ scenario.Counter = 0;
+ }
+
+ // Else just update Started and Finished
+ scenario.Started = true;
+ scenario.Finished = Mapping.NextState == null;
+ }
+
+ ///
+ public async Task BroadcastTextAsync(string text, CancellationToken cancellationToken = default)
+ {
+ if (Registry != null)
+ {
+ await Registry.BroadcastTextAsync(text, cancellationToken);
+ }
+ }
+
+ ///
+ public async Task BroadcastJsonAsync(object data, CancellationToken cancellationToken = default)
+ {
+ if (Registry != null)
+ {
+ await Registry.BroadcastJsonAsync(data, cancellationToken);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/WireMock.Net.Minimal/WireMock.Net.Minimal.csproj b/src/WireMock.Net.Minimal/WireMock.Net.Minimal.csproj
index ca2dbeb1..7b903a73 100644
--- a/src/WireMock.Net.Minimal/WireMock.Net.Minimal.csproj
+++ b/src/WireMock.Net.Minimal/WireMock.Net.Minimal.csproj
@@ -52,7 +52,7 @@
-
+
diff --git a/src/WireMock.Net.Shared/RequestBuilders/IHttpVersionBuilder.cs b/src/WireMock.Net.Shared/RequestBuilders/IHttpVersionBuilder.cs
index f10b1a39..ba9e9ac3 100644
--- a/src/WireMock.Net.Shared/RequestBuilders/IHttpVersionBuilder.cs
+++ b/src/WireMock.Net.Shared/RequestBuilders/IHttpVersionBuilder.cs
@@ -1,14 +1,13 @@
// Copyright © WireMock.Net
using WireMock.Matchers;
-using WireMock.Matchers.Request;
namespace WireMock.RequestBuilders;
///
/// The HttpVersionBuilder interface.
///
-public interface IHttpVersionBuilder : IRequestMatcher
+public interface IHttpVersionBuilder : IWebSocketRequestBuilder
{
///
/// WithHttpVersion
diff --git a/src/WireMock.Net.Shared/RequestBuilders/IWebSocketBuilder.cs b/src/WireMock.Net.Shared/RequestBuilders/IWebSocketBuilder.cs
new file mode 100644
index 00000000..0a5528b3
--- /dev/null
+++ b/src/WireMock.Net.Shared/RequestBuilders/IWebSocketBuilder.cs
@@ -0,0 +1,15 @@
+// Copyright © WireMock.Net
+using WireMock.Matchers.Request;
+
+namespace WireMock.RequestBuilders;
+
+///
+/// The BodyRequestBuilder interface.
+///
+public interface IWebSocketRequestBuilder : IRequestMatcher
+{
+ ///
+ /// Match WebSocket upgrade with optional protocols.
+ ///
+ IRequestBuilder WithWebSocketUpgrade(params string[] protocols);
+}
\ No newline at end of file
diff --git a/src/WireMock.Net.Shared/ResponseBuilders/ICallbackResponseBuilder.cs b/src/WireMock.Net.Shared/ResponseBuilders/ICallbackResponseBuilder.cs
index 5a927542..8205f6fa 100644
--- a/src/WireMock.Net.Shared/ResponseBuilders/ICallbackResponseBuilder.cs
+++ b/src/WireMock.Net.Shared/ResponseBuilders/ICallbackResponseBuilder.cs
@@ -3,14 +3,13 @@
using System;
using System.Threading.Tasks;
using JetBrains.Annotations;
-using WireMock.ResponseProviders;
namespace WireMock.ResponseBuilders;
///
/// The CallbackResponseBuilder interface.
///
-public interface ICallbackResponseBuilder : IResponseProvider
+public interface ICallbackResponseBuilder : IWebSocketResponseBuilder
{
///
/// The callback builder
diff --git a/src/WireMock.Net.Shared/ResponseBuilders/IWebSocketResponseBuilder.cs b/src/WireMock.Net.Shared/ResponseBuilders/IWebSocketResponseBuilder.cs
new file mode 100644
index 00000000..47e65290
--- /dev/null
+++ b/src/WireMock.Net.Shared/ResponseBuilders/IWebSocketResponseBuilder.cs
@@ -0,0 +1,29 @@
+// Copyright © WireMock.Net
+
+using System;
+using WireMock.ResponseProviders;
+using WireMock.Settings;
+using WireMock.WebSockets;
+
+namespace WireMock.ResponseBuilders;
+
+///
+/// The WebSocketResponseBuilder interface.
+///
+public interface IWebSocketResponseBuilder : IResponseProvider
+{
+ ///
+ /// Configure WebSocket response behavior
+ ///
+ IResponseBuilder WithWebSocket(Action configure);
+
+ ///
+ /// Proxy WebSocket to another server
+ ///
+ IResponseBuilder WithWebSocketProxy(string targetUrl);
+
+ ///
+ /// Proxy WebSocket to another server with settings
+ ///
+ IResponseBuilder WithWebSocketProxy(ProxyAndRecordSettings settings);
+}
\ No newline at end of file
diff --git a/src/WireMock.Net.Shared/Settings/WebSocketSettings.cs b/src/WireMock.Net.Shared/Settings/WebSocketSettings.cs
new file mode 100644
index 00000000..a3ce6466
--- /dev/null
+++ b/src/WireMock.Net.Shared/Settings/WebSocketSettings.cs
@@ -0,0 +1,21 @@
+// Copyright © WireMock.Net
+
+using WireMock.Constants;
+
+namespace WireMock.Settings;
+
+///
+/// WebSocket-specific settings
+///
+public class WebSocketSettings
+{
+ ///
+ /// Maximum number of concurrent WebSocket connections (default: 100)
+ ///
+ public int MaxConnections { get; set; } = 100;
+
+ ///
+ /// Default keep-alive interval (default: 30 seconds)
+ ///
+ public int KeepAliveIntervalSeconds { get; set; } = WebSocketConstants.DefaultKeepAliveIntervalSeconds;
+}
\ No newline at end of file
diff --git a/src/WireMock.Net.Shared/Settings/WireMockServerSettings.cs b/src/WireMock.Net.Shared/Settings/WireMockServerSettings.cs
index 9df15904..3b563c65 100644
--- a/src/WireMock.Net.Shared/Settings/WireMockServerSettings.cs
+++ b/src/WireMock.Net.Shared/Settings/WireMockServerSettings.cs
@@ -346,4 +346,10 @@ public class WireMockServerSettings
///
[PublicAPI]
public ActivityTracingOptions? ActivityTracingOptions { get; set; }
+
+ ///
+ /// WebSocket settings.
+ ///
+ [PublicAPI]
+ public WebSocketSettings? WebSocketSettings { get; set; }
}
\ No newline at end of file
diff --git a/src/WireMock.Net.Shared/WebSockets/IWebSocketBuilder.cs b/src/WireMock.Net.Shared/WebSockets/IWebSocketBuilder.cs
new file mode 100644
index 00000000..0f362d2d
--- /dev/null
+++ b/src/WireMock.Net.Shared/WebSockets/IWebSocketBuilder.cs
@@ -0,0 +1,84 @@
+// Copyright © WireMock.Net
+
+using System;
+using System.Threading.Tasks;
+using JetBrains.Annotations;
+using WireMock.Settings;
+using WireMock.Types;
+
+namespace WireMock.WebSockets;
+
+///
+/// WebSocket Response Builder interface
+///
+public interface IWebSocketBuilder
+{
+ ///
+ /// Accept the WebSocket with a specific protocol
+ ///
+ [PublicAPI]
+ IWebSocketBuilder WithAcceptProtocol(string protocol);
+
+ ///
+ /// Echo all received messages back to client
+ ///
+ [PublicAPI]
+ IWebSocketBuilder WithEcho();
+
+ ///
+ /// Handle incoming WebSocket messages
+ ///
+ [PublicAPI]
+ IWebSocketBuilder WithMessageHandler(Func handler);
+
+ ///
+ /// Define a sequence of messages to send
+ ///
+ [PublicAPI]
+ IWebSocketBuilder WithMessageSequence(Action configure);
+
+ ///
+ /// Enable broadcast mode for this mapping
+ ///
+ [PublicAPI]
+ IWebSocketBuilder WithBroadcast();
+
+ ///
+ /// Proxy to another WebSocket server
+ ///
+ [PublicAPI]
+ IWebSocketBuilder WithProxy(ProxyAndRecordSettings settings);
+
+ ///
+ /// Set close timeout (default: 10 minutes)
+ ///
+ [PublicAPI]
+ IWebSocketBuilder WithCloseTimeout(TimeSpan timeout);
+
+ ///
+ /// Set maximum message size in bytes (default: 1 MB)
+ ///
+ [PublicAPI]
+ IWebSocketBuilder WithMaxMessageSize(int sizeInBytes);
+
+ ///
+ /// Set receive buffer size (default: 4096 bytes)
+ ///
+ [PublicAPI]
+ IWebSocketBuilder WithReceiveBufferSize(int sizeInBytes);
+
+ ///
+ /// Set keep-alive interval (default: 30 seconds)
+ ///
+ [PublicAPI]
+ IWebSocketBuilder WithKeepAliveInterval(TimeSpan interval);
+
+ ///
+ /// Enable transformer support (Handlebars/Scriban)
+ ///
+ [PublicAPI]
+ IWebSocketBuilder WithTransformer(
+ TransformerType transformerType = TransformerType.Handlebars,
+ bool useTransformerForBodyAsFile = false,
+ ReplaceNodeOptions transformerReplaceNodeOptions = ReplaceNodeOptions.EvaluateAndTryToConvert);
+}
\ No newline at end of file
diff --git a/src/WireMock.Net.Shared/WebSockets/IWebSocketContext.cs b/src/WireMock.Net.Shared/WebSockets/IWebSocketContext.cs
new file mode 100644
index 00000000..b8bf4de4
--- /dev/null
+++ b/src/WireMock.Net.Shared/WebSockets/IWebSocketContext.cs
@@ -0,0 +1,85 @@
+// Copyright © WireMock.Net
+
+using System;
+using System.Net.WebSockets;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+
+namespace WireMock.WebSockets;
+
+///
+/// WebSocket context interface for handling WebSocket connections
+///
+public interface IWebSocketContext
+{
+ ///
+ /// Unique connection identifier
+ ///
+ Guid ConnectionId { get; }
+
+ ///
+ /// The ASP.NET Core HttpContext
+ ///
+ HttpContext HttpContext { get; }
+
+ ///
+ /// The WebSocket instance
+ ///
+ WebSocket WebSocket { get; }
+
+ ///
+ /// The original request that initiated the WebSocket connection
+ ///
+ IRequestMessage RequestMessage { get; }
+
+ ///
+ /// The mapping that matched this WebSocket request
+ ///
+ IMapping Mapping { get; }
+
+ ///
+ /// Send text message to the client
+ ///
+ Task SendTextAsync(string text, CancellationToken cancellationToken = default);
+
+ ///
+ /// Send binary message to the client
+ ///
+ Task SendBytesAsync(byte[] bytes, CancellationToken cancellationToken = default);
+
+ ///
+ /// Send JSON message to the client
+ ///
+ Task SendJsonAsync(object data, CancellationToken cancellationToken = default);
+
+ ///
+ /// Close the WebSocket connection
+ ///
+ Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription);
+
+ ///
+ /// Manually set the scenario state. This bypasses the counter logic and directly sets the next state.
+ /// Use this for programmatic state changes during WebSocket sessions.
+ ///
+ /// The next state to transition to
+ void SetScenarioState(string nextState);
+
+ ///
+ /// Manually set the scenario state with description. This bypasses the counter logic and directly sets the next state.
+ /// Use this for programmatic state changes during WebSocket sessions.
+ ///
+ /// The next state to transition to
+ /// Optional description for logging
+ void SetScenarioState(string nextState, string? description);
+
+ ///
+ /// Broadcast text message to all connections in this mapping
+ ///
+ Task BroadcastTextAsync(string text, CancellationToken cancellationToken = default);
+
+ ///
+ /// Broadcast JSON message to all connections in this mapping
+ ///
+ Task BroadcastJsonAsync(object data, CancellationToken cancellationToken = default);
+}
diff --git a/src/WireMock.Net.Shared/WebSockets/IWebSocketMessageSequenceBuilder.cs b/src/WireMock.Net.Shared/WebSockets/IWebSocketMessageSequenceBuilder.cs
new file mode 100644
index 00000000..bb5a229a
--- /dev/null
+++ b/src/WireMock.Net.Shared/WebSockets/IWebSocketMessageSequenceBuilder.cs
@@ -0,0 +1,11 @@
+// Copyright © WireMock.Net
+
+namespace WireMock.WebSockets;
+
+///
+/// WebSocket Message Sequence Builder interface (placeholder for future implementation)
+///
+public interface IWebSocketMessageSequenceBuilder
+{
+ // Future: Methods for building message sequences
+}
diff --git a/src/WireMock.Net.Shared/WebSockets/WebSocketMessage.cs b/src/WireMock.Net.Shared/WebSockets/WebSocketMessage.cs
new file mode 100644
index 00000000..9dd0da98
--- /dev/null
+++ b/src/WireMock.Net.Shared/WebSockets/WebSocketMessage.cs
@@ -0,0 +1,37 @@
+// Copyright © WireMock.Net
+
+using System;
+using System.Net.WebSockets;
+
+namespace WireMock.WebSockets;
+
+///
+/// Represents a WebSocket message
+///
+public class WebSocketMessage
+{
+ ///
+ /// The message type (Text or Binary)
+ ///
+ public WebSocketMessageType MessageType { get; set; }
+
+ ///
+ /// Text content (when MessageType is Text)
+ ///
+ public string? Text { get; set; }
+
+ ///
+ /// Binary content (when MessageType is Binary)
+ ///
+ public byte[]? Bytes { get; set; }
+
+ ///
+ /// Indicates whether this is the final message
+ ///
+ public bool EndOfMessage { get; set; }
+
+ ///
+ /// Timestamp when the message was received
+ ///
+ public DateTime Timestamp { get; set; }
+}