Add ElectronNET.AspNet project (ASP.Net specific runtime code)

This commit is contained in:
softworkz
2025-10-13 13:26:26 +02:00
parent c6c73a7c8a
commit 3a42c479dd
9 changed files with 483 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
namespace ElectronNET.API
{
using Microsoft.Extensions.DependencyInjection;
/// <summary>
///
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds the <see cref="Electron"/> Members to the Service Collection
/// </summary>
public static IServiceCollection AddElectron(this IServiceCollection services)
=> services
// adding in this manner to ensure late binding.
.AddSingleton(_ => IpcMain.Instance)
.AddSingleton(_ => App.Instance)
.AddSingleton(_ => AutoUpdater.Instance)
.AddSingleton(_ => WindowManager.Instance)
.AddSingleton(_ => Menu.Instance)
.AddSingleton(_ => Dialog.Instance)
.AddSingleton(_ => Notification.Instance)
.AddSingleton(_ => Tray.Instance)
.AddSingleton(_ => GlobalShortcut.Instance)
.AddSingleton(_ => Shell.Instance)
.AddSingleton(_ => Screen.Instance)
.AddSingleton(_ => Clipboard.Instance)
.AddSingleton(_ => HostHook.Instance)
.AddSingleton(_ => PowerMonitor.Instance)
.AddSingleton(_ => NativeTheme.Instance)
.AddSingleton(_ => Dock.Instance);
}
}

View File

@@ -0,0 +1,103 @@
namespace ElectronNET.API
{
using System;
using System.IO;
using System.Threading.Tasks;
using ElectronNET.AspNet;
using ElectronNET.AspNet.Runtime;
using ElectronNET.Runtime;
using ElectronNET.Runtime.Data;
using ElectronNET.Runtime.Helpers;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
/// <summary>
/// Provides extension methods for <see cref="IWebHostBuilder"/> to enable Electron.NET
/// integration in ASP.NET Core applications (including Razor Pages) using the WebHost-based hosting model.
/// </summary>
/// <remarks>
/// Call this extension during web host configuration (for example, inside <c>ConfigureWebHostDefaults</c> in Program.cs)
/// to wire up Electron with any command-line arguments and an optional application-ready callback.
/// </remarks>
public static class WebHostBuilderExtensions
{
/// <summary>
/// Adds Electron.NET support to the current ASP.NET Core web host and registers an application-ready callback.
/// </summary>
/// <param name="builder">The <see cref="IWebHostBuilder"/> to extend.</param>
/// <param name="args">The command-line arguments passed to the process.</param>
/// <param name="onAppReadyCallback">
/// An asynchronous callback invoked when the Electron app is ready. Use this to create windows or perform initialization.
/// </param>
/// <returns>
/// The same <see cref="IWebHostBuilder"/> instance to enable fluent configuration.
/// </returns>
/// <example>
/// <code language="csharp">
/// using Microsoft.AspNetCore.Hosting;
/// using Microsoft.Extensions.Hosting;
/// using ElectronNET.API;
///
/// public class Program
/// {
/// public static void Main(string[] args)
/// {
/// Host.CreateDefaultBuilder(args)
/// .ConfigureWebHostDefaults(webBuilder =>
/// {
/// webBuilder.UseStartup&lt;Startup&gt;();
/// webBuilder.UseElectron(args, async () =>
/// {
/// // Create the main browser window or perform other startup tasks.
/// });
/// })
/// .Build()
/// .Run();
/// }
/// }
/// </code>
/// </example>
public static IWebHostBuilder UseElectron(this IWebHostBuilder builder, string[] args, Func<Task> onAppReadyCallback)
{
ElectronNetRuntime.OnAppReadyCallback = onAppReadyCallback;
var webPort = PortHelper.GetFreePort(ElectronNetRuntime.AspNetWebPort ?? ElectronNetRuntime.DefaultWebPort);
ElectronNetRuntime.AspNetWebPort = webPort;
// check for the content folder if its exists in base director otherwise no need to include
// It was used before because we are publishing the project which copies everything to bin folder and contentroot wwwroot was folder there.
// now we have implemented the live reload if app is run using /watch then we need to use the default project path.
if (Directory.Exists($"{AppDomain.CurrentDomain.BaseDirectory}\\wwwroot"))
{
builder = builder.UseContentRoot(AppDomain.CurrentDomain.BaseDirectory)
.UseUrls("http://localhost:" + webPort);
}
else
{
builder = builder.UseUrls("http://localhost:" + webPort);
}
builder = builder.ConfigureServices(services =>
{
services.AddTransient<IStartupFilter, ServerReadyStartupFilter>();
services.AddSingleton<AspNetLifetimeAdapter>();
switch (ElectronNetRuntime.StartupMethod)
{
case StartupMethod.PackagedElectronFirst:
case StartupMethod.UnpackedElectronFirst:
services.AddSingleton<IElectronNetRuntimeController, RuntimeControllerAspNetElectronFirst>();
break;
case StartupMethod.PackagedDotnetFirst:
case StartupMethod.UnpackedDotnetFirst:
services.AddSingleton<IElectronNetRuntimeController, RuntimeControllerAspNetDotnetFirst>();
break;
default:
throw new ArgumentOutOfRangeException();
}
});
return builder;
}
}
}

View File

@@ -0,0 +1,44 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\common.props" />
<PropertyGroup>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<PackageOutputPath>..\..\artifacts</PackageOutputPath>
<PackageId>$(PackageNamePrefix).AspNet</PackageId>
<Title>$(PackageId)</Title>
<Description>$(DescriptionFirstPart) This package contains the ASP.Net Core integration.</Description>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IncludeSymbols>True</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<Nullable>disable</Nullable>
<RootNamespace>ElectronNET</RootNamespace>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net6.0|AnyCPU'">
<NoWarn>1701;1702;4014;CS4014;CA1416;CS1591</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0|AnyCPU'">
<NoWarn>1701;1702;4014;CS4014;CA1416;CS1591</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net6.0|AnyCPU'">
<NoWarn>1701;1702;4014;CS4014;CA1416;CS1591</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0|AnyCPU'">
<NoWarn>1701;1702;4014;CS4014;CA1416;CS1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<None Include="PackageIcon.png" Pack="true" PackagePath="\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ElectronNET.API\ElectronNET.API.csproj" />
</ItemGroup>
</Project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -0,0 +1,132 @@
namespace ElectronNET.AspNet.Runtime
{
using System;
using System.Threading.Tasks;
using ElectronNET.API;
using ElectronNET.Common;
using ElectronNET.Runtime.Controllers;
using ElectronNET.Runtime.Data;
using ElectronNET.Runtime.Services.SocketBridge;
internal abstract class RuntimeControllerAspNetBase : RuntimeControllerBase
{
private readonly AspNetLifetimeAdapter aspNetLifetimeAdapter;
private SocketBridgeService socketBridge;
protected RuntimeControllerAspNetBase(AspNetLifetimeAdapter aspNetLifetimeAdapter)
{
this.aspNetLifetimeAdapter = aspNetLifetimeAdapter;
this.aspNetLifetimeAdapter.Ready += this.AspNetLifetimeAdapter_Ready;
this.aspNetLifetimeAdapter.Stopping += this.AspNetLifetimeAdapter_Stopping;
this.aspNetLifetimeAdapter.Stopped += this.AspNetLifetimeAdapter_Stopped;
ElectronNetRuntime.RuntimeControllerCore = this;
}
internal override SocketBridgeService SocketBridge => this.socketBridge;
internal override SocketIoFacade Socket
{
get
{
if (this.State == LifetimeState.Ready)
{
return this.socketBridge.Socket;
}
throw new Exception("Cannot access socket bridge. Runtime is not in 'Ready' state");
}
}
protected void CreateSocketBridge(int port)
{
this.socketBridge = new SocketBridgeService(port);
this.socketBridge.Ready += this.SocketBridge_Ready;
this.socketBridge.Stopped += this.SocketBridge_Stopped;
this.socketBridge.Start();
}
protected void HandleReady()
{
if (this.SocketBridge.IsReady() &&
this.ElectronProcess.IsReady() &&
this.aspNetLifetimeAdapter.IsReady())
{
this.TransitionState(LifetimeState.Ready);
Task.Run(this.RunReadyCallback);
}
}
protected void HandleStopped()
{
this.TransitionState(LifetimeState.Stopping);
if (this.SocketBridge.IsNotStopped())
{
this.SocketBridge.Stop();
}
if (this.ElectronProcess.IsNotStopped())
{
this.ElectronProcess.Stop();
}
if (this.aspNetLifetimeAdapter.IsNotStopped())
{
this.aspNetLifetimeAdapter.Stop();
}
if ((this.SocketBridge.IsNullOrStopped()) &&
(this.ElectronProcess.IsNullOrStopped()) &&
(this.aspNetLifetimeAdapter.IsNullOrStopped()))
{
this.TransitionState(LifetimeState.Stopped);
}
}
protected abstract override Task StopCore();
private void SocketBridge_Ready(object sender, EventArgs e)
{
this.HandleReady();
}
private void AspNetLifetimeAdapter_Ready(object sender, EventArgs e)
{
this.HandleReady();
}
private void SocketBridge_Stopped(object sender, EventArgs e)
{
this.HandleStopped();
}
private void AspNetLifetimeAdapter_Stopped(object sender, EventArgs e)
{
this.HandleStopped();
}
private void AspNetLifetimeAdapter_Stopping(object sender, EventArgs e)
{
}
private async Task RunReadyCallback()
{
if (ElectronNetRuntime.OnAppReadyCallback == null)
{
Console.WriteLine("Warning: Non OnReadyCallback provided in UseElectron() setup.");
return;
}
try
{
await ElectronNetRuntime.OnAppReadyCallback().ConfigureAwait(false);
}
catch (Exception ex)
{
Console.WriteLine("Exception while executing OnAppReadyCallback. Stopping...\n" + ex);
this.Stop();
}
}
}
}

View File

@@ -0,0 +1,58 @@
namespace ElectronNET.AspNet.Runtime
{
using System;
using System.Threading.Tasks;
using ElectronNET.Common;
using ElectronNET.Runtime.Data;
using ElectronNET.Runtime.Helpers;
using ElectronNET.Runtime.Services.ElectronProcess;
internal class RuntimeControllerAspNetDotnetFirst : RuntimeControllerAspNetBase
{
private ElectronProcessBase electronProcess;
private int? port;
public RuntimeControllerAspNetDotnetFirst(AspNetLifetimeAdapter aspNetLifetimeAdapter) : base(aspNetLifetimeAdapter)
{
}
internal override ElectronProcessBase ElectronProcess => this.electronProcess;
protected override Task StartCore()
{
var isUnPacked = ElectronNetRuntime.StartupMethod.IsUnpackaged();
var electronBinaryName = ElectronNetRuntime.ElectronExecutable;
var args = Environment.CommandLine;
this.port = ElectronNetRuntime.ElectronSocketPort;
if (!this.port.HasValue)
{
this.port = PortHelper.GetFreePort(ElectronNetRuntime.DefaultSocketPort);
ElectronNetRuntime.ElectronSocketPort = this.port;
}
this.electronProcess = new ElectronProcessActive(isUnPacked, electronBinaryName, args, this.port.Value);
this.electronProcess.Ready += this.ElectronProcess_Ready;
this.electronProcess.Stopped += this.ElectronProcess_Stopped;
return this.electronProcess.Start();
}
protected override Task StopCore()
{
this.electronProcess.Stop();
return Task.CompletedTask;
}
private void ElectronProcess_Ready(object sender, EventArgs e)
{
this.TransitionState(LifetimeState.Started);
this.CreateSocketBridge(this.port!.Value);
}
private void ElectronProcess_Stopped(object sender, EventArgs e)
{
this.HandleStopped();
}
}
}

View File

@@ -0,0 +1,56 @@
namespace ElectronNET.AspNet.Runtime
{
using System;
using System.Threading.Tasks;
using ElectronNET.Runtime.Data;
using ElectronNET.Runtime.Services.ElectronProcess;
internal class RuntimeControllerAspNetElectronFirst : RuntimeControllerAspNetBase
{
private ElectronProcessBase electronProcess;
private int? port;
public RuntimeControllerAspNetElectronFirst(AspNetLifetimeAdapter aspNetLifetimeAdapter) : base(aspNetLifetimeAdapter)
{
}
internal override ElectronProcessBase ElectronProcess => this.electronProcess;
protected override Task StartCore()
{
this.port = ElectronNetRuntime.ElectronSocketPort;
if (!this.port.HasValue)
{
throw new Exception("No port has been specified by Electron!");
}
if (!ElectronNetRuntime.ElectronProcessId.HasValue)
{
throw new Exception("No electronPID has been specified by Electron!");
}
this.CreateSocketBridge(this.port!.Value);
this.electronProcess = new ElectronProcessPassive(ElectronNetRuntime.ElectronProcessId.Value);
this.electronProcess.Stopped += this.ElectronProcess_Stopped;
this.electronProcess.Start();
Task.Run(() => this.TransitionState(LifetimeState.Started));
return Task.CompletedTask;
}
protected override Task StopCore()
{
this.electronProcess.Stop();
return Task.CompletedTask;
}
private void ElectronProcess_Stopped(object sender, EventArgs e)
{
this.HandleStopped();
}
}
}

View File

@@ -0,0 +1,30 @@
namespace ElectronNET.AspNet
{
using System;
using ElectronNET.AspNet.Runtime;
using ElectronNET.Runtime;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
internal sealed class ServerReadyStartupFilter : IStartupFilter
{
/// <summary>
/// Extends the provided <paramref name="next" /> and returns an <see cref="T:System.Action" /> of the same type.
/// </summary>
/// <param name="next">The Configure method to extend.</param>
/// <returns>A modified <see cref="T:System.Action" />.</returns>
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return app =>
{
_ = app.ApplicationServices.GetService<AspNetLifetimeAdapter>();
var runtimeController = app.ApplicationServices.GetService<IElectronNetRuntimeController>();
runtimeController.Start();
next(app);
};
}
}
}

View File

@@ -0,0 +1,27 @@
namespace ElectronNET.AspNet.Runtime
{
using System.Threading.Tasks;
using ElectronNET.Runtime.Data;
using ElectronNET.Runtime.Services;
using Microsoft.Extensions.Hosting;
internal class AspNetLifetimeAdapter : LifetimeServiceBase
{
private readonly IHostApplicationLifetime lifetimeSercice;
public AspNetLifetimeAdapter(IHostApplicationLifetime lifetimeSercice)
{
this.lifetimeSercice = lifetimeSercice;
this.lifetimeSercice.ApplicationStarted.Register(() => this.TransitionState(LifetimeState.Ready));
this.lifetimeSercice.ApplicationStopping.Register(() => this.TransitionState(LifetimeState.Stopping));
this.lifetimeSercice.ApplicationStopped.Register(() => this.TransitionState(LifetimeState.Stopped));
}
protected override Task StopCore()
{
this.lifetimeSercice.StopApplication();
return Task.CompletedTask;
}
}
}