Renamed facades

This commit is contained in:
Florian Rappl
2026-02-22 23:56:06 +01:00
parent 7e7bc21f2d
commit 16925f401b
15 changed files with 68 additions and 62 deletions

View File

@@ -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<ElectronHub>`
- Includes `ConvertToType<T>` 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<int>` handlers to fail silently.
**Solution**: `SignalRFacade.ConvertToType<T>` handles JsonElement deserialization and numeric conversions.
**Solution**: `SignalRConnection.ConvertToType<T>` 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)

View File

@@ -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

View File

@@ -6,7 +6,7 @@ namespace ElectronNET.API
internal static class BridgeConnector
{
public static IFacade Socket
public static ISocketConnection Socket
{
get
{

View File

@@ -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.
/// </summary>
internal interface IFacade
internal interface ISocketConnection : IDisposable
{
/// <summary>
/// Raised when the bridge connection is established.
@@ -53,10 +53,5 @@ namespace ElectronNET.API.Bridge
/// Sends a message to Electron.
/// </summary>
Task Emit(string eventName, params object[] args);
/// <summary>
/// Disposes the connection.
/// </summary>
void DisposeSocket();
}
}

View File

@@ -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));
}
}
}

View File

@@ -50,7 +50,7 @@
internal static Func<Task> OnAppReadyCallback { get; set; }
internal static IFacade GetSocket()
internal static ISocketConnection GetSocket()
{
return RuntimeControllerCore?.Socket;
}

View File

@@ -13,7 +13,7 @@
{
}
internal abstract IFacade Socket { get; }
internal abstract ISocketConnection Socket { get; }
internal abstract ElectronProcessBase ElectronProcess { get; }

View File

@@ -20,7 +20,7 @@
{
}
internal override IFacade Socket
internal override ISocketConnection Socket
{
get
{

View File

@@ -17,7 +17,7 @@
{
}
internal override SocketIoFacade Socket
internal override SocketIOConnection Socket
{
get
{

View File

@@ -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);

View File

@@ -1,6 +1,7 @@
namespace ElectronNET.API
{
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using ElectronNET.AspNet;

View File

@@ -9,7 +9,7 @@ namespace ElectronNET.API
using ElectronNET.AspNet.Hubs;
/// <summary>
/// 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
/// </summary>
internal class SignalRFacade : IFacade
internal class SignalRConnection : ISocketConnection
{
private readonly IHubContext<ElectronHub> _hubContext;
private string _connectionId;
private readonly ConcurrentDictionary<string, Action<object>> _eventHandlers;
private readonly object _lockObj = new object();
public SignalRFacade(IHubContext<ElectronHub> hubContext)
public SignalRConnection(IHubContext<ElectronHub> hubContext)
{
_hubContext = hubContext;
_eventHandlers = new ConcurrentDictionary<string, Action<object>>();
@@ -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()
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
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();
}
}
}
}

View File

@@ -65,12 +65,12 @@ namespace ElectronNET.AspNet.Hubs
/// <param name="args">The event arguments as an array</param>
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<object>());
socket.TriggerEvent(eventName, args ?? Array.Empty<object>());
}
await Task.CompletedTask;

View File

@@ -26,7 +26,7 @@
internal override SocketBridgeService SocketBridge => this.socketBridge;
internal override IFacade Socket
internal override ISocketConnection Socket
{
get
{

View File

@@ -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
/// </summary>
internal class RuntimeControllerAspNetDotnetFirstSignalR : RuntimeControllerAspNetBase
@@ -29,7 +29,7 @@ namespace ElectronNET.AspNet.Runtime
private readonly IServer server;
private readonly IHubContext<ElectronHub> 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();
}
}
}