diff --git a/docs/SignalR-Implementation-Summary.md b/docs/SignalR-Implementation-Summary.md index 72c0c31..7efede1 100644 --- a/docs/SignalR-Implementation-Summary.md +++ b/docs/SignalR-Implementation-Summary.md @@ -41,7 +41,7 @@ Blazor Server applications where: - Implemented Socket.IO-compatible interface for API compatibility ### Phase 4: API Bridge Adaptation ✅ -- Created `SignalRFacade` implementing `IFacade` interface +- Created `SignalRConnection` implementing `ISocketConnection` interface - Ensured existing Electron API classes work with SignalR - Implemented type conversion helper for SignalR's JSON deserialization - Event routing from both directions (.NET ↔ Electron) @@ -60,8 +60,8 @@ Blazor Server applications where: ## Key Components -### 1. SignalRFacade (`src/ElectronNET.AspNet/Bridge/SignalRFacade.cs`) -- Implements `IFacade` interface to match Socket.IO facade API +### 1. SignalRConnection (`src/ElectronNET.AspNet/Bridge/SignalRConnection.cs`) +- Implements `ISocketConnection` interface to match Socket.IO facade API - Handles bidirectional event routing using `IHubContext` - Includes `ConvertToType` helper for handling SignalR's JSON deserialization quirks - Critical fix: Handles `JsonElement` and numeric type conversions (long → int) @@ -155,8 +155,8 @@ SignalR mode uses .NET-first startup (vs. Electron-first in Socket.IO mode) beca 4. **Better for Blazor Server** - Blazor is already running when Electron starts 5. **Single process debugging** - Developer debugs .NET process which owns Electron -### Why IFacade Interface? -Introducing `IFacade` allows `BridgeConnector.Socket` to return either `SocketIOFacade` or `SignalRFacade` based on startup mode, ensuring existing API code works with both transport mechanisms without modification. +### Why ISocketConnection Interface? +Introducing `ISocketConnection` allows `BridgeConnector.Socket` to return either `SocketIOConnection` or `SignalRConnection` based on startup mode, ensuring existing API code works with both transport mechanisms without modification. ### Why Keep-Alive Window? Electron quits immediately on macOS if no windows exist. The keep-alive window ensures Electron stays running during the connection and API initialization phase. It's automatically destroyed when the first real window is created. @@ -208,7 +208,7 @@ Blazor Server already uses SignalR for component communication (`/_blazor` hub). ### 3. Type Conversion Failures **Problem**: SignalR deserializes JSON numbers as `JsonElement` or `long`, causing `Once` handlers to fail silently. -**Solution**: `SignalRFacade.ConvertToType` handles JsonElement deserialization and numeric conversions. +**Solution**: `SignalRConnection.ConvertToType` handles JsonElement deserialization and numeric conversions. ### 4. Window Shutdown Not Triggering Exit **Problem**: Keep-alive window prevented `window-all-closed` event from firing. @@ -391,7 +391,7 @@ Existing applications do not need to change. SignalR mode is opt-in via command- ## File Changes Summary **New Files**: -- `src/ElectronNET.AspNet/Bridge/SignalRFacade.cs` (225 lines) +- `src/ElectronNET.AspNet/Bridge/SignalRConnection.cs` (225 lines) - `src/ElectronNET.AspNet/Hubs/ElectronHub.cs` (108 lines) - `src/ElectronNET.AspNet/Runtime/Controllers/RuntimeControllerAspNetDotnetFirstSignalR.cs` (163 lines) - `src/ElectronNET.AspNet/Services/IElectronAuthenticationService.cs` (20 lines) diff --git a/docs/SignalR-Startup-Mode.md b/docs/SignalR-Startup-Mode.md index 27c7805..f79e1a2 100644 --- a/docs/SignalR-Startup-Mode.md +++ b/docs/SignalR-Startup-Mode.md @@ -107,7 +107,7 @@ The application will: ### .NET Side - **`ElectronHub`** - SignalR hub at `/electron-hub` -- **`SignalRFacade`** - Mimics `SocketIoFacade` interface for compatibility +- **`SignalRConnection`** - Mimics `SocketIOConnection` interface for compatibility - **`RuntimeControllerAspNetDotnetFirstSignalR`** - Lifecycle management - **`StartupMethod.PackagedDotnetFirstSignalR`** - For packaged apps - **`StartupMethod.UnpackedDotnetFirstSignalR`** - For debugging @@ -128,7 +128,7 @@ The application will: ## Current Limitations (Phase 6 Work Needed) -⚠️ **Electron API Integration** - Existing Electron APIs (WindowManager, Dialog, etc.) still use SocketIoFacade. Full integration requires: +⚠️ **Electron API Integration** - Existing Electron APIs (WindowManager, Dialog, etc.) still use SocketIOConnection. Full integration requires: - Refactoring APIs to work with both facades, or - Creating an adapter pattern @@ -158,7 +158,7 @@ The application will: - URL parameter handling ### Phase 4: API Bridge ✅ (Basic Structure) -- `SignalRFacade` class +- `SignalRConnection` class - Event handler system - Hub connection integration @@ -182,7 +182,7 @@ To fully utilize this feature, the following work is recommended: ### .NET - `src/ElectronNET.API/Runtime/Data/StartupMethod.cs` - `src/ElectronNET.AspNet/Hubs/ElectronHub.cs` -- `src/ElectronNET.AspNet/Bridge/SignalRFacade.cs` +- `src/ElectronNET.AspNet/Bridge/SignalRConnection.cs` - `src/ElectronNET.AspNet/Runtime/Controllers/RuntimeControllerAspNetDotnetFirstSignalR.cs` - `src/ElectronNET.AspNet/API/ElectronEndpointRouteBuilderExtensions.cs` - `src/ElectronNET.AspNet/API/WebHostBuilderExtensions.cs` @@ -200,8 +200,8 @@ To fully utilize this feature, the following work is recommended: 8ee81f6 - Add ElectronHub and SignalR infrastructure for new startup modes 40aed60 - Add RuntimeControllerAspNetDotnetFirstSignalR for SignalR-based startup c1740b5 - Add SignalR client support to Electron Host for new startup modes -cb7d721 - Add SignalRFacade for SignalR-based API communication -268b9c9 - Update RuntimeControllerAspNetDotnetFirstSignalR to use SignalRFacade +cb7d721 - Add SignalRConnection for SignalR-based API communication +268b9c9 - Update RuntimeControllerAspNetDotnetFirstSignalR to use SignalRConnection 04ec522 - Fix compilation errors - Phase 4 complete (basic structure) 054f5b1 - Complete Phase 5: Add SignalR startup detection and port 0 configuration ``` @@ -219,7 +219,7 @@ cb7d721 - Add SignalRFacade for SignalR-based API communication To contribute to Phase 6 (full API integration): -1. Focus on adapting existing Electron API classes to work with SignalRFacade +1. Focus on adapting existing Electron API classes to work with SignalRConnection 2. Implement request-response pattern in ElectronHub 3. Add integration tests 4. Create sample applications diff --git a/src/ElectronNET.API/Bridge/BridgeConnector.cs b/src/ElectronNET.API/Bridge/BridgeConnector.cs index 0835992..44aa8b7 100644 --- a/src/ElectronNET.API/Bridge/BridgeConnector.cs +++ b/src/ElectronNET.API/Bridge/BridgeConnector.cs @@ -6,7 +6,7 @@ namespace ElectronNET.API internal static class BridgeConnector { - public static IFacade Socket + public static ISocketConnection Socket { get { diff --git a/src/ElectronNET.API/Bridge/IFacade.cs b/src/ElectronNET.API/Bridge/ISocketConnection.cs similarity index 91% rename from src/ElectronNET.API/Bridge/IFacade.cs rename to src/ElectronNET.API/Bridge/ISocketConnection.cs index c3ae754..214cb1f 100644 --- a/src/ElectronNET.API/Bridge/IFacade.cs +++ b/src/ElectronNET.API/Bridge/ISocketConnection.cs @@ -7,7 +7,7 @@ namespace ElectronNET.API.Bridge /// Common interface for communication facades (SocketIO and SignalR). /// Provides methods for bidirectional communication between .NET and Electron. /// - internal interface IFacade + internal interface ISocketConnection : IDisposable { /// /// Raised when the bridge connection is established. @@ -53,10 +53,5 @@ namespace ElectronNET.API.Bridge /// Sends a message to Electron. /// Task Emit(string eventName, params object[] args); - - /// - /// Disposes the connection. - /// - void DisposeSocket(); } } diff --git a/src/ElectronNET.API/Bridge/SocketIOFacade.cs b/src/ElectronNET.API/Bridge/SocketIOConnection.cs similarity index 95% rename from src/ElectronNET.API/Bridge/SocketIOFacade.cs rename to src/ElectronNET.API/Bridge/SocketIOConnection.cs index 96d5454..cab6636 100644 --- a/src/ElectronNET.API/Bridge/SocketIOFacade.cs +++ b/src/ElectronNET.API/Bridge/SocketIOConnection.cs @@ -9,13 +9,13 @@ using ElectronNET.API.Serialization; using SocketIO.Serializer.SystemTextJson; using SocketIO = SocketIOClient.SocketIO; -internal class SocketIoFacade : IFacade, IDisposable +internal class SocketIOConnection : ISocketConnection { private readonly SocketIO _socket; private readonly object _lockObj = new object(); private bool _isDisposed; - public SocketIoFacade(string uri) + public SocketIOConnection(string uri) { _socket = new SocketIO(uri); _socket.Serializer = new SystemTextJsonSerializer(ElectronJson.Options); @@ -141,7 +141,7 @@ internal class SocketIoFacade : IFacade, IDisposable { if (this._isDisposed) { - throw new ObjectDisposedException(nameof(SocketIoFacade)); + throw new ObjectDisposedException(nameof(SocketIOConnection)); } } } \ No newline at end of file diff --git a/src/ElectronNET.API/ElectronNetRuntime.cs b/src/ElectronNET.API/ElectronNetRuntime.cs index e04f5a0..290587f 100644 --- a/src/ElectronNET.API/ElectronNetRuntime.cs +++ b/src/ElectronNET.API/ElectronNetRuntime.cs @@ -50,7 +50,7 @@ internal static Func OnAppReadyCallback { get; set; } - internal static IFacade GetSocket() + internal static ISocketConnection GetSocket() { return RuntimeControllerCore?.Socket; } diff --git a/src/ElectronNET.API/Runtime/Controllers/RuntimeControllerBase.cs b/src/ElectronNET.API/Runtime/Controllers/RuntimeControllerBase.cs index 510e845..405301c 100644 --- a/src/ElectronNET.API/Runtime/Controllers/RuntimeControllerBase.cs +++ b/src/ElectronNET.API/Runtime/Controllers/RuntimeControllerBase.cs @@ -13,7 +13,7 @@ { } - internal abstract IFacade Socket { get; } + internal abstract ISocketConnection Socket { get; } internal abstract ElectronProcessBase ElectronProcess { get; } diff --git a/src/ElectronNET.API/Runtime/Controllers/RuntimeControllerDotNetFirst.cs b/src/ElectronNET.API/Runtime/Controllers/RuntimeControllerDotNetFirst.cs index d5e37a7..c271a11 100644 --- a/src/ElectronNET.API/Runtime/Controllers/RuntimeControllerDotNetFirst.cs +++ b/src/ElectronNET.API/Runtime/Controllers/RuntimeControllerDotNetFirst.cs @@ -20,7 +20,7 @@ { } - internal override IFacade Socket + internal override ISocketConnection Socket { get { diff --git a/src/ElectronNET.API/Runtime/Controllers/RuntimeControllerElectronFirst.cs b/src/ElectronNET.API/Runtime/Controllers/RuntimeControllerElectronFirst.cs index fdb458f..36d2c84 100644 --- a/src/ElectronNET.API/Runtime/Controllers/RuntimeControllerElectronFirst.cs +++ b/src/ElectronNET.API/Runtime/Controllers/RuntimeControllerElectronFirst.cs @@ -17,7 +17,7 @@ { } - internal override SocketIoFacade Socket + internal override SocketIOConnection Socket { get { diff --git a/src/ElectronNET.API/Runtime/Services/SocketBridge/SocketBridgeService.cs b/src/ElectronNET.API/Runtime/Services/SocketBridge/SocketBridgeService.cs index 7e8be64..7200f49 100644 --- a/src/ElectronNET.API/Runtime/Services/SocketBridge/SocketBridgeService.cs +++ b/src/ElectronNET.API/Runtime/Services/SocketBridge/SocketBridgeService.cs @@ -9,7 +9,7 @@ { private readonly int socketPort; private readonly string socketUrl; - private SocketIoFacade socket; + private SocketIOConnection socket; public SocketBridgeService(int socketPort) { @@ -19,11 +19,11 @@ public int SocketPort => this.socketPort; - internal SocketIoFacade Socket => this.socket; + internal SocketIOConnection Socket => this.socket; protected override Task StartCore() { - this.socket = new SocketIoFacade(this.socketUrl); + this.socket = new SocketIOConnection(this.socketUrl); this.socket.BridgeConnected += this.Socket_BridgeConnected; this.socket.BridgeDisconnected += this.Socket_BridgeDisconnected; Task.Run(this.Connect); diff --git a/src/ElectronNET.AspNet/API/WebHostBuilderExtensions.cs b/src/ElectronNET.AspNet/API/WebHostBuilderExtensions.cs index 25a9875..b49a9bb 100644 --- a/src/ElectronNET.AspNet/API/WebHostBuilderExtensions.cs +++ b/src/ElectronNET.AspNet/API/WebHostBuilderExtensions.cs @@ -1,6 +1,7 @@ namespace ElectronNET.API { using System; + using System.Diagnostics; using System.IO; using System.Threading.Tasks; using ElectronNET.AspNet; diff --git a/src/ElectronNET.AspNet/Bridge/SignalRFacade.cs b/src/ElectronNET.AspNet/Bridge/SignalRConnection.cs similarity index 83% rename from src/ElectronNET.AspNet/Bridge/SignalRFacade.cs rename to src/ElectronNET.AspNet/Bridge/SignalRConnection.cs index 185fc31..902d26c 100644 --- a/src/ElectronNET.AspNet/Bridge/SignalRFacade.cs +++ b/src/ElectronNET.AspNet/Bridge/SignalRConnection.cs @@ -9,7 +9,7 @@ namespace ElectronNET.API using ElectronNET.AspNet.Hubs; /// - /// SignalR-based facade that mimics the SocketIoFacade interface + /// SignalR-based facade that mimics the SocketIOConnection interface /// for compatibility with existing Electron API code. /// /// Key implementation details: @@ -19,14 +19,14 @@ namespace ElectronNET.API /// - Event args are passed as arrays to match SignalR serialization behavior /// - Connection ID is set by ElectronHub when Electron client connects /// - internal class SignalRFacade : IFacade + internal class SignalRConnection : ISocketConnection { private readonly IHubContext _hubContext; private string _connectionId; private readonly ConcurrentDictionary> _eventHandlers; private readonly object _lockObj = new object(); - public SignalRFacade(IHubContext hubContext) + public SignalRConnection(IHubContext hubContext) { _hubContext = hubContext; _eventHandlers = new ConcurrentDictionary>(); @@ -76,7 +76,7 @@ namespace ElectronNET.API } else { - Console.Error.WriteLine($"[SignalRFacade] Failed to convert event data to type {typeof(T).Name}"); + Console.Error.WriteLine($"[SignalR] Failed to convert event data to type {typeof(T).Name}"); } }; } @@ -108,7 +108,7 @@ namespace ElectronNET.API } else { - Console.Error.WriteLine($"[SignalRFacade] Failed to convert event data to type {typeof(T).Name} for event '{eventName}'"); + Console.Error.WriteLine($"[SignalR] Failed to convert event data to type {typeof(T).Name} for event '{eventName}'"); } }; } @@ -126,7 +126,7 @@ namespace ElectronNET.API { if (string.IsNullOrEmpty(_connectionId)) { - Console.Error.WriteLine($"[SignalRFacade] Cannot emit '{eventName}' - no connection ID"); + Console.Error.WriteLine($"[SignalR] Cannot emit '{eventName}' - no connection ID"); return; } @@ -138,7 +138,7 @@ namespace ElectronNET.API } catch (Exception ex) { - Console.Error.WriteLine($"[SignalRFacade] Error emitting '{eventName}': {ex.Message}"); + Console.Error.WriteLine($"[SignalR] Error emitting '{eventName}': {ex.Message}"); throw; } } @@ -176,7 +176,7 @@ namespace ElectronNET.API } catch (Exception ex) { - Console.Error.WriteLine($"[SignalRFacade] JsonElement deserialization failed: {ex.Message}"); + Console.Error.WriteLine($"[SignalR] JsonElement deserialization failed: {ex.Message}"); return default; } } @@ -218,15 +218,25 @@ namespace ElectronNET.API } catch (Exception ex) { - Console.Error.WriteLine($"[SignalRFacade] Type conversion failed from {obj.GetType().Name} to {targetType.Name}: {ex.Message}"); + Console.Error.WriteLine($"[SignalR] Type conversion failed from {obj.GetType().Name} to {targetType.Name}: {ex.Message}"); return default; } } - - public void DisposeSocket() + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() { - // SignalR connections are managed by ASP.NET Core - _eventHandlers.Clear(); + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + // SignalR connections are managed by ASP.NET Core + _eventHandlers.Clear(); + } } } } diff --git a/src/ElectronNET.AspNet/Hubs/ElectronHub.cs b/src/ElectronNET.AspNet/Hubs/ElectronHub.cs index fac8a24..bda9b6f 100644 --- a/src/ElectronNET.AspNet/Hubs/ElectronHub.cs +++ b/src/ElectronNET.AspNet/Hubs/ElectronHub.cs @@ -65,12 +65,12 @@ namespace ElectronNET.AspNet.Hubs /// The event arguments as an array public async Task ElectronEvent(string eventName, object[] args) { - // Get the SignalRFacade and trigger the event handlers + // Get the SignalRConnection and trigger the event handlers var runtimeController = ElectronNetRuntime.RuntimeController as RuntimeControllerAspNetDotnetFirstSignalR; - if (runtimeController?.SignalRSocket is SignalRFacade signalRFacade) + if (runtimeController?.SignalRSocket is SignalRConnection socket) { // Invoke the event handlers registered via On/Once - signalRFacade.TriggerEvent(eventName, args ?? Array.Empty()); + socket.TriggerEvent(eventName, args ?? Array.Empty()); } await Task.CompletedTask; diff --git a/src/ElectronNET.AspNet/Runtime/Controllers/RuntimeControllerAspNetBase.cs b/src/ElectronNET.AspNet/Runtime/Controllers/RuntimeControllerAspNetBase.cs index b5478f9..35ebc1a 100644 --- a/src/ElectronNET.AspNet/Runtime/Controllers/RuntimeControllerAspNetBase.cs +++ b/src/ElectronNET.AspNet/Runtime/Controllers/RuntimeControllerAspNetBase.cs @@ -26,7 +26,7 @@ internal override SocketBridgeService SocketBridge => this.socketBridge; - internal override IFacade Socket + internal override ISocketConnection Socket { get { diff --git a/src/ElectronNET.AspNet/Runtime/Controllers/RuntimeControllerAspNetDotnetFirstSignalR.cs b/src/ElectronNET.AspNet/Runtime/Controllers/RuntimeControllerAspNetDotnetFirstSignalR.cs index 5a06160..593ff4c 100644 --- a/src/ElectronNET.AspNet/Runtime/Controllers/RuntimeControllerAspNetDotnetFirstSignalR.cs +++ b/src/ElectronNET.AspNet/Runtime/Controllers/RuntimeControllerAspNetDotnetFirstSignalR.cs @@ -20,7 +20,7 @@ namespace ElectronNET.AspNet.Runtime /// Key differences from Socket.IO mode: /// - Waits for ASP.NET server to start, then captures the dynamic port /// - Launches Electron with the actual URL (no port scanning needed) - /// - Uses SignalRFacade instead of SocketIOFacade for bidirectional communication + /// - Uses SignalRConnection instead of SocketIOConnection for bidirectional communication /// - Waits for 'electron-host-ready' signal to ensure API modules are loaded before calling app callback /// internal class RuntimeControllerAspNetDotnetFirstSignalR : RuntimeControllerAspNetBase @@ -29,7 +29,7 @@ namespace ElectronNET.AspNet.Runtime private readonly IServer server; private readonly IHubContext hubContext; private readonly IElectronAuthenticationService authenticationService; - private SignalRFacade signalRFacade; + private SignalRConnection socket; private int? port; private string actualUrl; private bool electronLaunched; @@ -45,11 +45,11 @@ namespace ElectronNET.AspNet.Runtime this.server = server; this.hubContext = hubContext; this.authenticationService = authenticationService; - this.signalRFacade = new SignalRFacade(hubContext); + this.socket = new SignalRConnection(hubContext); this.electronLaunched = false; - this.signalRFacade.BridgeConnected += this.SignalRFacade_Connected; - this.signalRFacade.BridgeDisconnected += this.SignalRFacade_Disconnected; + this.socket.BridgeConnected += this.SignalRConnection_Connected; + this.socket.BridgeDisconnected += this.SignalRConnection_Disconnected; // Subscribe to ASP.NET ready event to launch Electron aspNetLifetimeAdapter.Ready += this.OnAspNetReady; @@ -58,20 +58,20 @@ namespace ElectronNET.AspNet.Runtime internal override ElectronProcessBase ElectronProcess => this.electronProcess; internal override SocketBridgeService SocketBridge => null; - internal override IFacade Socket + internal override ISocketConnection Socket { get { if (this.State == LifetimeState.Ready) { - return this.signalRFacade; + return this.socket; } throw new Exception("Cannot access SignalR facade. Runtime is not in 'Ready' state"); } } - internal SignalRFacade SignalRSocket => this.signalRFacade; + internal SignalRConnection SignalRSocket => this.socket; protected override Task StartCore() { @@ -81,7 +81,7 @@ namespace ElectronNET.AspNet.Runtime protected override Task StopCore() { this.electronProcess?.Stop(); - this.signalRFacade?.DisposeSocket(); + this.socket?.Dispose(); return Task.CompletedTask; } @@ -132,11 +132,11 @@ namespace ElectronNET.AspNet.Runtime _ = this.electronProcess.Start(); } - private async void SignalRFacade_Connected(object sender, EventArgs e) + private async void SignalRConnection_Connected(object sender, EventArgs e) { // Register handler for 'electron-host-ready' signal from Electron. // This ensures API modules are fully loaded before calling the app ready callback. - this.signalRFacade.Once("electron-host-ready", () => + this.socket.Once("electron-host-ready", () => { this.OnElectronHostReady(); }); @@ -161,7 +161,7 @@ namespace ElectronNET.AspNet.Runtime } } - private void SignalRFacade_Disconnected(object sender, EventArgs e) + private void SignalRConnection_Disconnected(object sender, EventArgs e) { // IMPORTANT: Do NOT call HandleStopped synchronously here! // This event fires from within SignalR's OnDisconnectedAsync, and calling @@ -178,12 +178,12 @@ namespace ElectronNET.AspNet.Runtime public void OnSignalRConnected(string connectionId) { - this.signalRFacade.SetConnectionId(connectionId); + this.socket.SetConnectionId(connectionId); } public void OnSignalRDisconnected() { - this.signalRFacade.OnDisconnected(); + this.socket.OnDisconnected(); } } }