diff --git a/Directory.Packages.props b/Directory.Packages.props index 65f53ad..8d0b503 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -27,18 +27,27 @@ + + + - - - + + + + + + + + + diff --git a/README.md b/README.md index b653150..19e1dd7 100644 --- a/README.md +++ b/README.md @@ -1,353 +1,990 @@ -NOTE: The namespacing has been changed to ReactiveMarbles.Extensions.Hosting. Please update your references to the new namespace. +# Extensions.Hosting + +`Extensions.Hosting` adds Generic Host integration for desktop UI frameworks, ReactiveUI/Splat, plug-ins, Windows service hosting, single-instance applications, and Identity/Entity Framework Core helpers. + +The public namespaces use `ReactiveMarbles.Extensions.Hosting.*`. + +## Package Matrix + +| Package | Main namespace | Purpose | +| --- | --- | --- | +| `CP.Extensions.Hosting.MainUIThread` | `ReactiveMarbles.Extensions.Hosting.UiThread` | Shared UI context and UI-thread base types used by the platform host packages. | +| `CP.Extensions.Hosting.Wpf` | `ReactiveMarbles.Extensions.Hosting.Wpf` | WPF host integration, application/window registration, and WPF lifetime linking. | +| `CP.Extensions.Hosting.WinForms` | `ReactiveMarbles.Extensions.Hosting.WinForms` | WinForms host integration, form/shell registration, and WinForms lifetime linking. | +| `CP.Extensions.Hosting.WinUI` | `ReactiveMarbles.Extensions.Hosting.WinUI` | WinUI host integration for `Application` and main `Window` types. | +| `CP.Extensions.Hosting.Avalonia` | `ReactiveMarbles.Extensions.Hosting.Avalonia` | Avalonia host integration, `AppBuilder` configuration, application/window registration, and Avalonia lifetime linking. | +| `CP.Extensions.Hosting.Maui` | `ReactiveMarbles.Extensions.Hosting.Maui` | .NET MAUI host integration, `MauiAppBuilder` configuration, application/page registration, and MAUI lifetime linking. | +| `CP.Extensions.Hosting.SingleInstance` | `ReactiveMarbles.Extensions.Hosting.AppServices` | Mutex-backed single-instance enforcement and direct named resource mutex helpers. | +| `CP.Extensions.Hosting.Plugins` | `ReactiveMarbles.Extensions.Hosting.Plugins` | Plug-in discovery, assembly loading, ordering, and hosted-service plug-in base types. | +| `CP.Extensions.Hosting.Plugins.Reactive` | `ReactiveMarbles.Extensions.Hosting.Reactive.Plugins` | Reactive package variant of the plug-in API with the same source and API shape. | +| `CP.Extensions.Hosting.PluginService` | `ReactiveMarbles.Extensions.Hosting.PluginService` | Service-host bootstrapper, plug-in service discovery defaults, and `ServiceBase` lifetime support. | +| `CP.Extensions.Hosting.PluginService.Reactive` | `ReactiveMarbles.Extensions.Hosting.Reactive.PluginService` | Reactive package variant of the service-host API, referencing `CP.Extensions.Hosting.Plugins.Reactive`. | +| `CP.Extensions.Hosting.ReactiveUI.Wpf` | `ReactiveMarbles.Extensions.Hosting.ReactiveUI` | ReactiveUI/Splat integration for WPF using the standard ReactiveUI package set. | +| `CP.Extensions.Hosting.ReactiveUI.Wpf.Reactive` | `ReactiveMarbles.Extensions.Hosting.Reactive.ReactiveUI` | Reactive package variant of the WPF ReactiveUI/Splat integration. | +| `CP.Extensions.Hosting.ReactiveUI.WinForms` | `ReactiveMarbles.Extensions.Hosting.ReactiveUI` | ReactiveUI/Splat integration for WinForms. | +| `CP.Extensions.Hosting.ReactiveUI.WinForms.Reactive` | `ReactiveMarbles.Extensions.Hosting.Reactive.ReactiveUI` | Reactive package variant of the WinForms ReactiveUI/Splat integration. | +| `CP.Extensions.Hosting.ReactiveUI.WinUI` | `ReactiveMarbles.Extensions.Hosting.ReactiveUI` | ReactiveUI/Splat integration for WinUI. | +| `CP.Extensions.Hosting.ReactiveUI.WinUI.Reactive` | `ReactiveMarbles.Extensions.Hosting.Reactive.ReactiveUI` | Reactive package variant of the WinUI ReactiveUI/Splat integration. | +| `CP.Extensions.Hosting.ReactiveUI.Avalonia` | `ReactiveMarbles.Extensions.Hosting.ReactiveUI` | ReactiveUI/Splat integration for Avalonia. | +| `CP.Extensions.Hosting.ReactiveUI.Avalonia.Reactive` | `ReactiveMarbles.Extensions.Hosting.Reactive.ReactiveUI` | Reactive package variant of the Avalonia ReactiveUI/Splat integration. | +| `CP.Extensions.Hosting.ReactiveUI.Maui` | `ReactiveMarbles.Extensions.Hosting.ReactiveUI` | ReactiveUI/Splat integration for .NET MAUI. | +| `CP.Extensions.Hosting.ReactiveUI.Maui.Reactive` | `ReactiveMarbles.Extensions.Hosting.Reactive.ReactiveUI` | Reactive package variant of the MAUI ReactiveUI/Splat integration. | +| `CP.Extensions.Hosting.Identity.EntityFrameworkCore.SqlServer` | `ReactiveMarbles.Extensions.Hosting.Identity.EntityFrameworkCore` | SQL Server DbContext and ASP.NET Core Identity registration helpers. | +| `CP.Extensions.Hosting.Identity.EntityFrameworkCore.Sqlite` | `ReactiveMarbles.Extensions.Hosting.Identity.EntityFrameworkCore.Sqlite` | SQLite DbContext and ASP.NET Core Identity registration helpers. | + +## Normal And Reactive Packages + +The normal packages use the standard `ReactiveMarbles.Extensions.Hosting.*` namespaces. The `.Reactive` packages are source-linked sibling projects compiled with `REACTIVE_SHIM` and use `ReactiveMarbles.Extensions.Hosting.Reactive.*` namespaces where a namespace split is needed. + +Use the normal packages when the application uses the standard ReactiveUI/ReactiveUI.Primitives package set. Use the `.Reactive` packages when the application uses `ReactiveUI.Primitives.*.Reactive` or `ReactiveUI.*.Reactive` packages. The hosting APIs are intentionally the same; migration usually means changing the package reference and namespace. + +Do not add direct Rx.NET package references to application projects for this repository's hosting APIs. Use the ReactiveUI.Primitives package family, and use the `.Reactive` package variants only when a reactive bridge package is required. + +```xml + + + + +``` + +```xml + + + + +``` + +```csharp +// Normal package +using ReactiveMarbles.Extensions.Hosting.Plugins; +using ReactiveMarbles.Extensions.Hosting.ReactiveUI; + +// Reactive package +using ReactiveMarbles.Extensions.Hosting.Reactive.Plugins; +using ReactiveMarbles.Extensions.Hosting.Reactive.ReactiveUI; +``` -# ReactiveMarbles.Extensions.Hosting -Extensions for Microsoft.Extensions.Hosting that bring WPF, WinForms, WinUI, ReactiveUI, plug-ins, single-instance control, and common host utilities to desktop apps. +For ReactiveUI.Primitives application code, project-file using aliases keep view models small and avoid repeated namespace noise: -This repository supports both classic IHostBuilder and the newer IHostApplicationBuilder hosting model introduced in .NET 8+. Existing IHostBuilder APIs remain unchanged; equivalent IHostApplicationBuilder overloads are available where appropriate. +```xml + + + + + +``` + +## Target Frameworks + +The libraries target .NET Framework where the platform supports it, current .NET TFMs, and `net11.0` preview TFMs. Windows desktop packages use Windows-specific TFMs. -Supported targets include .NET Framework 4.6.2/4.8, .NET Standard 2.0, and .NET 8/9/10 (Windows where applicable). +| Area | Target frameworks | +| --- | --- | +| Shared, plug-ins, single instance | `net462`, `net472`, `net48`, `net481`, `net8.0`, `net9.0`, `net10.0`, `net11.0` | +| WPF, WinForms, PluginService | .NET Framework TFMs plus `net8.0-windows`, `net9.0-windows`, `net10.0-windows`, `net11.0-windows` | +| ReactiveUI WPF/WinForms | .NET Framework TFMs plus `net8.0-windows10.0.19041`, `net9.0-windows10.0.19041`, `net10.0-windows10.0.19041`, `net11.0-windows10.0.19041` | +| WinUI and ReactiveUI WinUI | `net8.0-windows10.0.19041`, `net9.0-windows10.0.19041`, `net10.0-windows10.0.19041`, `net11.0-windows10.0.19041` | +| Avalonia and ReactiveUI Avalonia | `net8.0`, `net9.0`, `net10.0`, `net11.0` | +| MAUI and ReactiveUI MAUI | `net10.0`, `net11.0`, Android, iOS, Mac Catalyst, macOS, tvOS, and Windows where supported | +| Identity EF Core helpers | `net8.0`, `net9.0`, `net10.0`, `net11.0` | -## Quick start +The repository uses Central Package Management in `Directory.Packages.props`. Notable centrally managed versions include `StyleSharp.Analyzers` `3.16.0`, `ReactiveUI.Primitives` `6.0.0`, `ReactiveUI` `24.0.0-beta.3`, and `ReactiveUI.Avalonia` `12.1.0-beta.1`. -Choose a hosting model: -- IHostBuilder (generic host): Host.CreateDefaultBuilder(args) -- IHostApplicationBuilder (new app builder): Host.CreateApplicationBuilder(args) +## Host Builder Styles + +Most packages support both Generic Host builder styles: -### Example: WPF app with IHostBuilder ```csharp using Microsoft.Extensions.Hosting; -using ReactiveMarbles.Extensions.Hosting.Wpf; -var host = Host.CreateDefaultBuilder(args) - .ConfigureWpf(wpf => +var host = new HostBuilder() + .ConfigureServices(services => { - // Optional: register Application type and windows via the WpfBuilder - wpf.ApplicationType = typeof(App); - wpf.WindowTypes.Add(typeof(MainWindow)); - wpf.ConfigureContextAction = ctx => ctx.ShutdownMode = ShutdownMode.OnMainWindowClose; + // Classic IHostBuilder configuration. }) - .UseWpfLifetime(ShutdownMode.OnMainWindowClose) .Build(); +``` + +```csharp +using Microsoft.Extensions.Hosting; -await host.RunAsync(); +var builder = Host.CreateApplicationBuilder(args); + +// Modern IHostApplicationBuilder configuration. + +using var host = builder.Build(); +await host.RunAsync().ConfigureAwait(false); ``` -### Example: WPF app with IHostApplicationBuilder +The extension methods are fluent. Platform packages usually register a context, a UI thread adapter, and an `IHostedService` that starts the UI loop with the host. + +## Shared UI Thread Infrastructure + +Package: `CP.Extensions.Hosting.MainUIThread` + +Namespace: `ReactiveMarbles.Extensions.Hosting.UiThread` + +### API + +| API | Description | +| --- | --- | +| `IUiContext.IsLifetimeLinked` | When `true`, the host is stopped when the UI application exits. | +| `IUiContext.IsRunning` | Tracks whether the UI application has started and is still running. | +| `BaseUiContext` | Simple base implementation of `IUiContext`. | +| `BaseUiThread` | Base class for UI-thread adapters where `TContext : class, IUiContext`. | +| `BaseUiThread.Start()` | Signals or starts the UI loop. | +| `BaseUiThread.Dispose()` | Releases startup synchronization resources. | +| `BaseUiThread.PreUiThreadStart()` | Override point that runs before the UI loop starts. | +| `BaseUiThread.UiThreadStart()` | Override point that runs the platform message loop. | +| `BaseUiThread.HandleApplicationExit()` | Marks the UI context stopped and requests host shutdown when lifetime is linked. | + +### Custom UI Adapter Example + +Most applications consume a platform package instead of deriving from `BaseUiThread` directly. Use the base class when adding a new UI framework adapter. + ```csharp +using Microsoft.Extensions.DependencyInjection; +using ReactiveMarbles.Extensions.Hosting.UiThread; + +public sealed class CustomUiContext : BaseUiContext +{ + public object? Dispatcher { get; set; } +} + +public sealed class CustomUiThread(IServiceProvider services) + : BaseUiThread(services) +{ + protected override void PreUiThreadStart() + { + UiContext.Dispatcher = new object(); + } + + protected override void UiThreadStart() + { + try + { + // Run the UI framework message loop here. + } + finally + { + HandleApplicationExit(); + } + } +} +``` + +## WPF Hosting + +Package: `CP.Extensions.Hosting.Wpf` + +Namespace: `ReactiveMarbles.Extensions.Hosting.Wpf` + +### API + +| API | Description | +| --- | --- | +| `ConfigureWpf(Action? configureDelegate = null)` | Registers WPF hosting services and optional application/window types. Available on `IHostBuilder` and `IHostApplicationBuilder`. | +| `UseWpfLifetime(ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose)` | Links host shutdown to the WPF application lifetime. Call after `ConfigureWpf`. | +| `IWpfBuilder.UseApplication()` | Registers a WPF `Application` type. | +| `IWpfBuilder.UseCurrentApplication(TApplication currentApplication)` | Registers an existing WPF `Application` instance. | +| `IWpfBuilder.UseWindow()` | Registers a WPF `Window` type. If the window implements `IWpfShell`, it is also registered as the shell. | +| `IWpfBuilder.ConfigureContext(Action configureAction)` | Customizes the WPF context before it is used. | +| `IWpfContext.ShutdownMode` | WPF shutdown behavior. | +| `IWpfContext.WpfApplication` | Current WPF application instance. | +| `IWpfContext.Dispatcher` | WPF dispatcher for UI-thread work. | +| `IWpfShell` | Marker interface for a shell window. | + +### Example + +```csharp +using System.Windows; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using ReactiveMarbles.Extensions.Hosting.Wpf; -var builder = Host.CreateApplicationBuilder(args) - .ConfigureWpf(wpf => - { - wpf.ApplicationType = typeof(App); - wpf.WindowTypes.Add(typeof(MainWindow)); - wpf.ConfigureContextAction = ctx => ctx.ShutdownMode = ShutdownMode.OnMainWindowClose; - }) - .UseWpfLifetime(ShutdownMode.OnMainWindowClose); +public sealed partial class App : Application +{ + private IHost? _host; -await builder.Build().RunAsync(); + protected override void OnStartup(StartupEventArgs e) + { + _host = new HostBuilder() + .ConfigureServices(services => + { + services.AddSingleton(); + }) + .ConfigureWpf(wpf => + { + wpf.UseCurrentApplication(this); + wpf.UseWindow(); + wpf.ConfigureContext(context => + context.ShutdownMode = ShutdownMode.OnLastWindowClose); + }) + .UseWpfLifetime() + .Build(); + + _ = _host.RunAsync(); + base.OnStartup(e); + } +} + +public sealed partial class MainWindow : Window, IWpfShell +{ + public MainWindow(MainViewModel viewModel) + { + InitializeComponent(); + DataContext = viewModel; + } +} ``` ---- - -## Packages and APIs -The following sections outline the main features, their APIs for both hosting models, and example usage. - -### WPF (CP.Extensions.Hosting.Wpf) -Namespace: ReactiveMarbles.Extensions.Hosting.Wpf -- ConfigureWpf - - IHostBuilder ConfigureWpf(this IHostBuilder, Action?) - - IHostApplicationBuilder ConfigureWpf(this IHostApplicationBuilder, Action?) - - Use IWpfBuilder to set: - - Type? ApplicationType - - Application? Application (optional existing instance) - - IList WindowTypes - - Action? ConfigureContextAction -- UseWpfLifetime - - IHostBuilder UseWpfLifetime(this IHostBuilder, ShutdownMode = OnLastWindowClose) - - IHostApplicationBuilder UseWpfLifetime(this IHostApplicationBuilder, ShutdownMode = OnLastWindowClose) -- IWpfContext - - ShutdownMode ShutdownMode { get; set; } - - Application? WpfApplication { get; set; } - - Dispatcher Dispatcher { get; } - - bool IsLifetimeLinked { get; set; } (set internally when using UseWpfLifetime) - -Example (builder model): see Quick start above. - -### WinForms (CP.Extensions.Hosting.WinForms) -Namespace: ReactiveMarbles.Extensions.Hosting.WinForms -- ConfigureWinForms - - IHostBuilder ConfigureWinForms(this IHostBuilder, Action?) - - IHostApplicationBuilder ConfigureWinForms(this IHostApplicationBuilder, Action?) -- ConfigureWinForms() where TView : Form - - IHostBuilder ConfigureWinForms(...) - - IHostApplicationBuilder ConfigureWinForms(...) - - Registers the main form and, if it implements IWinFormsShell, also registers it as IWinFormsShell -- ConfigureWinFormsShell() where TShell : Form, IWinFormsShell - - IHostBuilder ConfigureWinFormsShell() - - IHostApplicationBuilder ConfigureWinFormsShell() -- UseWinFormsLifetime - - IHostBuilder UseWinFormsLifetime(this IHostBuilder) - - IHostApplicationBuilder UseWinFormsLifetime(this IHostApplicationBuilder) -- IWinFormsContext - - bool EnableVisualStyles { get; set; } - - Dispatcher? Dispatcher { get; set; } (WinForms dispatcher abstraction) - -Example (application builder): +## WinForms Hosting + +Package: `CP.Extensions.Hosting.WinForms` + +Namespace: `ReactiveMarbles.Extensions.Hosting.WinForms` + +### API + +| API | Description | +| --- | --- | +| `ConfigureWinForms(Action? configureAction = null)` | Registers WinForms hosting services. Available on `IHostBuilder` and `IHostApplicationBuilder`. | +| `ConfigureWinForms(Action? configureAction = null)` | Registers a `Form` as a singleton. If it implements `IWinFormsShell`, it is also registered as the shell. | +| `ConfigureWinFormsShell()` | Registers a shell form where `TShell : Form, IWinFormsShell`. | +| `UseWinFormsLifetime()` | Links host shutdown to the WinForms message loop. | +| `IWinFormsContext.EnableVisualStyles` | Enables or disables WinForms visual styles before the UI starts. | +| `IWinFormsContext.Dispatcher` | Optional dispatcher used for marshaling work. | +| `IWinFormsShell` | Marker interface for the main form. | + +### Example + ```csharp +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using ReactiveMarbles.Extensions.Hosting.WinForms; +using System.Windows.Forms; -var builder = Host.CreateApplicationBuilder(args) - .ConfigureWinForms(ctx => +[STAThread] +public static class Program +{ + public static async Task Main(string[] args) { - ctx.EnableVisualStyles = true; - }) - .ConfigureWinFormsShell() - .UseWinFormsLifetime(); - -await builder.Build().RunAsync(); + using var host = new HostBuilder() + .ConfigureServices(services => + { + services.AddSingleton(); + }) + .ConfigureWinForms(context => context.EnableVisualStyles = true) + .UseWinFormsLifetime() + .Build(); + + await host.RunAsync().ConfigureAwait(false); + } +} + +public sealed class MainForm : Form, IWinFormsShell +{ + public MainForm(MainPresenter presenter) + { + Text = presenter.Title; + } +} ``` -### WinUI (CP.Extensions.Hosting.WinUI) -Namespace: ReactiveMarbles.Extensions.Hosting.WinUI -- ConfigureWinUI() where TApp : Microsoft.UI.Xaml.Application where TAppWindow : Microsoft.UI.Xaml.Window - - IHostBuilder ConfigureWinUI() - - IHostApplicationBuilder ConfigureWinUI() -- IWinUIContext - - Window? AppWindow { get; set; } - - Type? AppWindowType { get; set; } - - DispatcherQueue? Dispatcher { get; set; } - - Application? WinUIApplication { get; set; } - -Example (application builder): +## WinUI Hosting + +Package: `CP.Extensions.Hosting.WinUI` + +Namespace: `ReactiveMarbles.Extensions.Hosting.WinUI` + +### API + +| API | Description | +| --- | --- | +| `ConfigureWinUI()` | Registers the WinUI `Application` and main `Window` types, the WinUI context, and the hosted service. Available on `IHostBuilder` and `IHostApplicationBuilder`. | +| `IWinUIContext.AppWindow` | Current WinUI window instance. | +| `IWinUIContext.AppWindowType` | Window type to create. Set by `ConfigureWinUI()`. | +| `IWinUIContext.Dispatcher` | `DispatcherQueue` for UI-thread work. | +| `IWinUIContext.WinUIApplication` | Current WinUI application instance. | +| `IWinUIService` | Marker interface for WinUI services. | + +### Example + ```csharp using Microsoft.Extensions.Hosting; +using Microsoft.UI.Xaml; using ReactiveMarbles.Extensions.Hosting.WinUI; -var builder = Host.CreateApplicationBuilder(args) - .ConfigureWinUI(); +public partial class App : Application +{ + private readonly IHost _host; + + public App() + { + _host = new HostBuilder() + .ConfigureWinUI() + .Build(); + } +} + +public sealed partial class MainWindow : Window +{ + public MainWindow() + { + InitializeComponent(); + } +} +``` + +## Avalonia Hosting + +Package: `CP.Extensions.Hosting.Avalonia` + +Namespace: `ReactiveMarbles.Extensions.Hosting.Avalonia` + +### API + +| API | Description | +| --- | --- | +| `ConfigureAvalonia(Action? configureDelegate = null)` | Registers Avalonia hosting services and optional application/window types. Available on `IHostBuilder` and `IHostApplicationBuilder`. | +| `UseAvaloniaLifetime(ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose)` | Links host shutdown to the Avalonia desktop lifetime. | +| `IAvaloniaBuilder.UseApplication()` | Registers an Avalonia `Application` type. | +| `IAvaloniaBuilder.UseCurrentApplication(TApplication currentApplication)` | Registers an existing Avalonia application instance. | +| `IAvaloniaBuilder.UseWindow()` | Registers an Avalonia `Window` type. If it implements `IAvaloniaShell`, it is also registered as the shell. | +| `IAvaloniaBuilder.ConfigureContext(Action configureAction)` | Customizes the Avalonia context. | +| `IAvaloniaBuilder.ConfigureAppBuilder(Action configureAction)` | Customizes Avalonia `AppBuilder` before startup. | +| `IAvaloniaContext.ShutdownMode` | Avalonia shutdown behavior. | +| `IAvaloniaContext.AvaloniaApplication` | Current Avalonia application. | +| `IAvaloniaContext.ApplicationLifetime` | Avalonia desktop lifetime. | +| `IAvaloniaContext.Dispatcher` | Avalonia dispatcher. | +| `IAvaloniaShell` | Marker interface for the main window. | + +### Example + +```csharp +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Microsoft.Extensions.Hosting; +using ReactiveMarbles.Extensions.Hosting.Avalonia; + +public static class Program +{ + [STAThread] + public static int Main(string[] args) + { + using var host = new HostBuilder() + .ConfigureAvalonia(avalonia => + { + avalonia.UseApplication(); + avalonia.UseWindow(); + avalonia.ConfigureAppBuilder(appBuilder => + appBuilder.UsePlatformDetect().LogToTrace()); + }) + .UseAvaloniaLifetime(ShutdownMode.OnLastWindowClose) + .Build(); + + host.Run(); + return 0; + } +} + +public sealed class MainWindow : Window, IAvaloniaShell +{ +} +``` + +## MAUI Hosting + +Package: `CP.Extensions.Hosting.Maui` + +Namespace: `ReactiveMarbles.Extensions.Hosting.Maui` + +### API -await builder.Build().RunAsync(); +| API | Description | +| --- | --- | +| `ConfigureMaui(Action? configureDelegate = null)` | Registers MAUI hosting services, builds a `MauiApp`, and registers application/page types. Available on `IHostBuilder` and `IHostApplicationBuilder`. | +| `UseMauiLifetime()` | Links host shutdown to the MAUI lifetime. | +| `ConfigureMauiShell()` | Registers a singleton shell page where `TShell : Page, IMauiShell`. | +| `IMauiBuilder.AddSingletonPage()` | Registers a MAUI `Page` as a singleton. If it implements `IMauiShell`, it is also registered as the shell. | +| `IMauiBuilder.UseMauiApp(Action? configureMauiApp = null)` | Registers a MAUI `Application` type and configures the underlying `MauiAppBuilder`. | +| `IMauiBuilder.UseMauiApp(TApplication currentApplication, Action? configureMauiApp = null)` | Registers an existing MAUI application instance. | +| `IMauiBuilder.ConfigureContext(Action configureAction)` | Customizes the MAUI context. | +| `IMauiBuilder.MauiAppBuilder` | Underlying MAUI builder for fonts, handlers, services, and app configuration. | +| `IMauiContext.MauiApplication` | Current MAUI application. | +| `IMauiContext.Dispatcher` | MAUI dispatcher. | +| `IMauiShell` | Marker interface for the main shell page. | + +### Example + +```csharp +using Microsoft.Extensions.Hosting; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Hosting; +using ReactiveMarbles.Extensions.Hosting.Maui; + +public static class MauiProgram +{ + public static IHost CreateHost() + { + return Host.CreateDefaultBuilder() + .ConfigureMaui(maui => + { + maui.UseMauiApp(builder => + { + builder.ConfigureFonts(fonts => + { + fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); + }); + }); + + maui.AddSingletonPage(); + }) + .UseMauiLifetime() + .Build(); + } +} + +public sealed class AppShell : Shell, IMauiShell +{ +} ``` -### ReactiveUI integration -Namespaces: ReactiveMarbles.Extensions.Hosting.ReactiveUI (per UI stack) -- ConfigureSplatForMicrosoftDependencyResolver - - IHostBuilder ConfigureSplatForMicrosoftDependencyResolver(this IHostBuilder) - - IHostApplicationBuilder ConfigureSplatForMicrosoftDependencyResolver(this IHostApplicationBuilder) -- Variants: - - WPF: Adds WithWpf() builder stage - - WinForms: Adds WithWinForms() builder stage - - WinUI: Adds WithWinUI() builder stage -- MapSplatLocator - - IHost MapSplatLocator(this IHost host, Action containerFactory) - -Example (WPF + ReactiveUI with application builder): +## ReactiveUI And Splat Integration + +Packages: + +- `CP.Extensions.Hosting.ReactiveUI.Wpf` +- `CP.Extensions.Hosting.ReactiveUI.WinForms` +- `CP.Extensions.Hosting.ReactiveUI.WinUI` +- `CP.Extensions.Hosting.ReactiveUI.Avalonia` +- `CP.Extensions.Hosting.ReactiveUI.Maui` +- `.Reactive` siblings for each package + +Normal namespace: `ReactiveMarbles.Extensions.Hosting.ReactiveUI` + +Reactive namespace: `ReactiveMarbles.Extensions.Hosting.Reactive.ReactiveUI` + +### API + +| API | Description | +| --- | --- | +| `ConfigureSplatForMicrosoftDependencyResolver()` on `IHostBuilder` | Registers Splat to use Microsoft.Extensions.DependencyInjection and initializes ReactiveUI for the target platform. | +| `ConfigureSplatForMicrosoftDependencyResolver()` on `IHostApplicationBuilder` | Same behavior for the modern builder API. | +| `MapSplatLocator(Action containerFactory)` on `IHost` | Maps Splat to the built host service provider and invokes a custom service-provider callback. | + +Each platform package initializes the matching ReactiveUI builder extension: + +| Package | ReactiveUI builder call | +| --- | --- | +| WPF | `WithWpf()` | +| WinForms | `WithWinForms()` | +| WinUI | `WithWinUI()` | +| Avalonia | `WithAvalonia()` | +| MAUI | `WithMaui()` | + +### WPF Example + ```csharp +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using ReactiveMarbles.Extensions.Hosting.ReactiveUI; using ReactiveMarbles.Extensions.Hosting.Wpf; +using ReactiveUI; -var builder = Host.CreateApplicationBuilder(args) +var host = new HostBuilder() .ConfigureSplatForMicrosoftDependencyResolver() - .ConfigureWpf(wpf => + .ConfigureServices(services => { - wpf.ApplicationType = typeof(App); - wpf.WindowTypes.Add(typeof(MainWindow)); + services.AddSingleton(); + services.AddTransient, MainWindow>(); }) - .UseWpfLifetime(); + .ConfigureWpf(wpf => wpf.UseApplication().UseWindow()) + .UseWpfLifetime() + .Build(); -await builder.Build().RunAsync(); +host.MapSplatLocator(serviceProvider => +{ + // Register additional Splat services that need the built provider. +}); + +await host.RunAsync().ConfigureAwait(false); ``` -### Plug-in system (CP.Extensions.Hosting.Plugins) -Namespace: ReactiveMarbles.Extensions.Hosting.Plugins -- ConfigurePlugins - - IHostBuilder ConfigurePlugins(this IHostBuilder, Action configure) - - IHostApplicationBuilder ConfigurePlugins(this IHostApplicationBuilder, Action configure) -- IPluginBuilder key options (typical): - - UseContentRoot (bool): also scan content root - - IncludeFrameworks(params string[] globs) - - IncludePlugins(params string[] globs) - - AddScanDirectories(params string[] directories) - - PluginMatcher / FrameworkMatcher (advanced globbing) - - AssemblyScanFunc: Func> for discovery - -Example (application builder): +### Reactive Package Example + +The API is the same. Use the `.Reactive` package and namespace. + ```csharp using Microsoft.Extensions.Hosting; -using ReactiveMarbles.Extensions.Hosting.Plugins; +using ReactiveMarbles.Extensions.Hosting.Reactive.ReactiveUI; +using ReactiveMarbles.Extensions.Hosting.Wpf; -var builder = Host.CreateApplicationBuilder(args) - .ConfigurePlugins(plugins => +var host = new HostBuilder() + .ConfigureSplatForMicrosoftDependencyResolver() + .ConfigureWpf(wpf => wpf.UseApplication().UseWindow()) + .UseWpfLifetime() + .Build(); +``` + +### ReactiveUI.Primitives View Model Example + +```csharp +using ReactiveUI; + +public sealed class SearchViewModel : ReactiveObject +{ + private string? _searchText; + private IReadOnlyList _results = []; + + public string? SearchText { - plugins.UseContentRoot = true; - // Add framework assemblies and plugin patterns - plugins.IncludeFrameworks(@"\\netstandard2.0\\*.FrameworkLib.dll"); - plugins.IncludePlugins(@"\\Plugins\\{runtime}\\ReactiveMarbles.Plugin.*.dll"); - }); + get => _searchText; + set => this.RaiseAndSetIfChanged(ref _searchText, value); + } -await builder.Build().RunAsync(); + public IReadOnlyList Results + { + get => _results; + private set => this.RaiseAndSetIfChanged(ref _results, value); + } + + public void Search(IEnumerable source) + { + var filter = SearchText ?? string.Empty; + Results = source + .Where(item => item.Contains(filter, StringComparison.OrdinalIgnoreCase)) + .ToArray(); + } +} ``` -### Single instance (CP.Extensions.Hosting.SingleInstance) -Namespace: ReactiveMarbles.Extensions.Hosting.AppServices -- ConfigureSingleInstance - - IHostBuilder ConfigureSingleInstance(this IHostBuilder, Action configure) - - IHostApplicationBuilder ConfigureSingleInstance(this IHostApplicationBuilder, Action configure) - - IHostBuilder ConfigureSingleInstance(this IHostBuilder, string mutexId) - - IHostApplicationBuilder ConfigureSingleInstance(this IHostApplicationBuilder, string mutexId) -- IMutexBuilder - - string MutexId { get; set; } - - Action? WhenNotFirstInstance { get; set; } - -Example (application builder): +## Single-Instance Applications + +Package: `CP.Extensions.Hosting.SingleInstance` + +Namespace: `ReactiveMarbles.Extensions.Hosting.AppServices` + +### API + +| API | Description | +| --- | --- | +| `ConfigureSingleInstance(Action configureAction)` | Registers mutex-backed single-instance enforcement. Available on `IHostBuilder` and `IHostApplicationBuilder`. | +| `ConfigureSingleInstance(string mutexId)` | Convenience overload that sets only the mutex id. | +| `IMutexBuilder.MutexId` | Unique named mutex identifier. | +| `IMutexBuilder.IsGlobal` | Uses the `Global\` mutex namespace when `true`; otherwise uses `Local\`. | +| `IMutexBuilder.WhenNotFirstInstance` | Callback invoked with `IHostEnvironment` and `ILogger` when another instance already owns the mutex. | +| `ResourceMutex.Create(ILogger? logger, string? mutexId, string? resourceName = null, bool global = false)` | Creates and locks a named mutex directly. | +| `ResourceMutex.IsLocked` | Indicates whether the mutex was acquired. | +| `ResourceMutex.Lock()` | Attempts to acquire the mutex. | +| `ResourceMutex.Dispose()` | Releases the mutex on the owning thread. | + +### Host Example + ```csharp using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using ReactiveMarbles.Extensions.Hosting.AppServices; -var builder = Host.CreateApplicationBuilder(args) - .ConfigureSingleInstance(cfg => +var host = new HostBuilder() + .ConfigureSingleInstance(singleInstance => + { + singleInstance.MutexId = "{D7F93D6D-6B4D-4F58-8E76-E68C51D9E92C}"; + singleInstance.IsGlobal = false; + singleInstance.WhenNotFirstInstance = (environment, logger) => + { + logger.LogWarning( + "Application {ApplicationName} is already running.", + environment.ApplicationName); + }; + }) + .Build(); + +await host.RunAsync().ConfigureAwait(false); +``` + +### Direct ResourceMutex Example + +```csharp +using Microsoft.Extensions.Logging; +using ReactiveMarbles.Extensions.Hosting.AppServices; + +using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); +var logger = loggerFactory.CreateLogger("Mutex"); + +using var mutex = ResourceMutex.Create( + logger, + "import-job", + resourceName: "Nightly import", + global: false); + +if (!mutex.IsLocked) +{ + return; +} + +// Run the protected work. +``` + +## Plug-ins + +Packages: + +- `CP.Extensions.Hosting.Plugins` +- `CP.Extensions.Hosting.Plugins.Reactive` + +Normal namespace: `ReactiveMarbles.Extensions.Hosting.Plugins` + +Reactive namespace: `ReactiveMarbles.Extensions.Hosting.Reactive.Plugins` + +### API + +| API | Description | +| --- | --- | +| `ConfigurePlugins(Action configurePlugin)` | Configures plug-in scanning and loading. Available on `IHostBuilder` and `IHostApplicationBuilder`. | +| `IPlugin.ConfigureHost(object hostBuilderContext, IServiceCollection serviceCollection)` | Called for each discovered plug-in so it can register services. | +| `IPluginBuilder.PluginDirectories` | Root directories scanned for plug-in assemblies. | +| `IPluginBuilder.FrameworkDirectories` | Root directories scanned for framework assemblies. | +| `IPluginBuilder.UseContentRoot` | Adds the host content root to the scan directories when enabled. | +| `IPluginBuilder.FailIfNoPlugins` | Throws during startup when no plug-ins are discovered. Defaults to `false`. | +| `IPluginBuilder.FrameworkMatcher` | Glob matcher for framework assemblies. | +| `IPluginBuilder.PluginMatcher` | Glob matcher for plug-in assemblies. | +| `IPluginBuilder.ValidatePlugin` | Predicate used before loading a plug-in assembly path. | +| `IPluginBuilder.AssemblyScanFunc` | Assembly scanning delegate. Defaults to scanning exported public plug-in types. | +| `AddScanDirectories(params string[] directories)` | Adds directories to both framework and plug-in scan roots. | +| `IncludeFrameworks(params string[] frameworkGlobs)` | Adds framework include globs. | +| `ExcludeFrameworks(params string[] frameworkGlobs)` | Adds framework exclude globs. | +| `IncludePlugins(params string[] pluginGlobs)` | Adds plug-in include globs. | +| `ExcludePlugins(params string[] pluginGlobs)` | Adds plug-in exclude globs. | +| `RequirePlugins(bool failIfNone = true)` | Sets `FailIfNoPlugins`. | +| `PluginScanner.ByNamingConvention(Assembly pluginAssembly)` | Finds a conventional `{AssemblyName}.Plugin` type. | +| `PluginScanner.ScanForPluginInstances(Assembly pluginAssembly)` | Finds public concrete classes implementing `IPlugin`. | +| `PluginOrderAttribute` | Orders discovered plug-ins. Lower values run earlier. Constructors accept no value, an `int`, or an enum/object value. | +| `PluginBase`, `PluginBase`, `PluginBase` | Base plug-ins that register one, two, or three hosted services. | +| `HostedServiceBase` | Base hosted service with logging, cleanup, and lifetime callback hooks. | + +`HostedServiceBase` exposes: + +| Member | Description | +| --- | --- | +| `CleanUp` | Disposable collection cleaned when the service stops or is disposed. | +| `Logger` | Logger passed to the base constructor. | +| `IsDisposed` | Indicates whether the cleanup collection has been disposed. | +| `OnStarted()` | Override to start work after the application starts. | +| `OnStopping()` | Override to stop work and dispose resources. Call the base implementation unless you intentionally replace cleanup behavior. | +| `OnStopped()` | Override to run final stopped logic. | + +### Host Scanning Example + +```csharp +using Microsoft.Extensions.Hosting; +using ReactiveMarbles.Extensions.Hosting.Plugins; + +var host = new HostBuilder() + .ConfigurePlugins(plugins => { - cfg.MutexId = "{ea031523-3a63-45e5-85f2-6fa75fbf37ed}"; - cfg.WhenNotFirstInstance = (env, logger) => - logger.LogWarning("Application {0} already running.", env.ApplicationName); - }); + plugins?.AddScanDirectories(AppContext.BaseDirectory); + plugins?.IncludeFrameworks("frameworks/**/*.dll"); + plugins?.IncludePlugins("plugins/**/*.Plugin.dll"); + plugins?.ExcludePlugins("plugins/**/*.Disabled.dll"); + plugins?.RequirePlugins(); + if (plugins is not null) + { + plugins.AssemblyScanFunc = PluginScanner.ScanForPluginInstances; + } + }) + .Build(); -await builder.Build().RunAsync(); +await host.RunAsync().ConfigureAwait(false); ``` -### Service host utilities (CP.Extensions.Hosting.PluginService) -Namespace: ReactiveMarbles.Extensions.Hosting.PluginService -- UseServiceBaseLifetime - - IHostBuilder UseServiceBaseLifetime(this IHostBuilder) - - IHostApplicationBuilder UseServiceBaseLifetime(this IHostApplicationBuilder) -- UseConsoleLifetime (IHostApplicationBuilder only) - - IHostApplicationBuilder UseConsoleLifetime(this IHostApplicationBuilder) -- RunAsServiceAsync - - Task RunAsServiceAsync(this IHostBuilder, CancellationToken = default) - - Task RunAsServiceAsync(this HostApplicationBuilder, CancellationToken = default) -- ServiceHost - - Task Create(Type type, string[] args, Func? configureHostBuilder = null, Action? configureHost = null, string nameSpace = "ReactiveMarbles.Plugin", string? targetRuntime = null) - - Task CreateApplication(Type type, string[] args, Func? configureHostBuilder = null, Action? configureHost = null, string nameSpace = "ReactiveMarbles.Plugin", string? targetRuntime = null) - -Example (service/console dual mode using IHostApplicationBuilder): +### Plug-in Assembly Example + ```csharp +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using ReactiveMarbles.Extensions.Hosting.PluginService; +using Microsoft.Extensions.Logging; +using ReactiveMarbles.Extensions.Hosting.Plugins; + +[PluginOrder(10)] +public sealed class Plugin : IPlugin +{ + public void ConfigureHost(object hostBuilderContext, IServiceCollection serviceCollection) + { + serviceCollection.AddHostedService(); + } +} + +public sealed class ImportHostedService( + ILogger logger, + IHostApplicationLifetime lifetime) + : HostedServiceBase(logger, lifetime) +{ + public override Task OnStarted() + { + Logger.LogInformation("Import service started."); + return Task.CompletedTask; + } +} +``` + +### Plug-in Base Example + +```csharp +using ReactiveMarbles.Extensions.Hosting.Plugins; + +public sealed class Plugin + : PluginBase +{ +} +``` + +### Reactive Plug-in Example + +Only the package and namespace change. + +```csharp +using Microsoft.Extensions.DependencyInjection; +using ReactiveMarbles.Extensions.Hosting.Reactive.Plugins; -var builder = Host.CreateApplicationBuilder(args) - .UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureLogging() - .ConfigureConfiguration(args) - .UseConsoleLifetime(); // or .UseServiceBaseLifetime() for Windows Service +public sealed class Plugin : IPlugin +{ + public void ConfigureHost(object hostBuilderContext, IServiceCollection serviceCollection) + { + serviceCollection.AddSingleton(); + } +} +``` + +## Plug-in Service Hosting + +Packages: + +- `CP.Extensions.Hosting.PluginService` +- `CP.Extensions.Hosting.PluginService.Reactive` + +Normal namespace: `ReactiveMarbles.Extensions.Hosting.PluginService` + +Reactive namespace: `ReactiveMarbles.Extensions.Hosting.Reactive.PluginService` + +### API + +| API | Description | +| --- | --- | +| `ServiceHost.Logger` | Gets the `ILogger` created by the service host after startup. | +| `ServiceHost.Create(Type type, string[] args, Func? hostBuilder = default, Action? configureHost = default, string nameSpace = "ReactiveMarbles.Plugin", string? targetRuntime = null)` | Creates, configures, and runs a classic `IHostBuilder` service host. | +| `ServiceHost.CreateApplication(Type type, string[] args, Func? hostBuilder = default, Action? configureHost = default, string nameSpace = "ReactiveMarbles.Plugin", string? targetRuntime = null)` | Creates, configures, and runs a modern `IHostApplicationBuilder` service host. | +| `UseServiceBaseLifetime()` on `IHostBuilder` | Registers `ServiceBaseLifetime` as `IHostLifetime`. | +| `UseServiceBaseLifetime()` on `IHostApplicationBuilder` | Registers `ServiceBaseLifetime` as `IHostLifetime`. | +| `UseConsoleLifetime()` on `IHostApplicationBuilder` | Registers `ConsoleLifetime` as `IHostLifetime`. | +| `RunAsServiceAsync(CancellationToken cancellationToken = default)` on `IHostBuilder` | Builds and runs with `ServiceBaseLifetime`. | +| `RunAsServiceAsync(CancellationToken cancellationToken = default)` on `HostApplicationBuilder` | Builds and runs with `ServiceBaseLifetime`. | +| `ServiceBaseLifetime` | `IHostLifetime` implementation backed by `System.ServiceProcess.ServiceBase`. | + +`ServiceHost` configures: + +- Content root as the current directory. +- Logging from configuration plus console, event log, log4net, and debug providers. +- Host configuration from `hostsettings.json`, environment variables with the `PREFIX_` prefix, and command line. +- Application configuration from `appsettings.json`, `appsettings.{Environment}.json`, environment variables with the `PREFIX_` prefix, and command line. +- Plug-in scanning from the process directory, framework assemblies matching `\netstandard2.0\*.FrameworkLib.dll`, and plug-ins matching `\Plugins\{runtime}\{nameSpace}*.dll`. +- Windows service lifetime when a debugger is not attached and `--console` is not supplied; console lifetime otherwise. + +### Console Or Service Entry Point + +```csharp +using ReactiveMarbles.Extensions.Hosting.PluginService; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; -await builder.Build().RunAsync(); +await ServiceHost.Create( + typeof(Program), + args, + hostBuilder: builder => builder?.ConfigureServices(services => + { + services.AddSingleton(); + }), + configureHost: host => + { + ServiceHost.Logger?.LogInformation("Host configured."); + }, + nameSpace: "Contoso.Plugin", + targetRuntime: "win-x64") + .ConfigureAwait(false); ``` -Example (helper): +### Modern Builder Entry Point + ```csharp using ReactiveMarbles.Extensions.Hosting.PluginService; await ServiceHost.CreateApplication( typeof(Program), args, - hb => hb // external builder customization - .ConfigurePlugins(pb => - { - /* plugin globs */ - pb.RequirePlugins(true); // fail if none found - optional default false - }), - host => { /* use host.Services */ }, - nameSpace: "ReactiveMarbles.Plugin"); + hostBuilder: builder => + { + builder?.Services.AddSingleton(); + return builder; + }) + .ConfigureAwait(false); +``` + +### Direct Service Lifetime Example + +```csharp +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.DependencyInjection; +using ReactiveMarbles.Extensions.Hosting.PluginService; + +var builder = Host.CreateApplicationBuilder(args); +builder.Services.AddHostedService(); +builder.UseServiceBaseLifetime(); + +await builder.Build().RunAsync().ConfigureAwait(false); ``` -### Identity + Entity Framework Core -Namespaces: -- ReactiveMarbles.Extensions.Hosting.Identity.EntityFrameworkCore (SqlServer) -- ReactiveMarbles.Extensions.Hosting.Identity.EntityFrameworkCore.Sqlite (Sqlite) +### Reactive Service Host Example -APIs (service collection extensions used inside UseWebHostServices): -- IServiceCollection UseEntityFrameworkCoreSqlServer(WebHostBuilderContext, string connectionStringName, ServiceLifetime = Scoped) -- IServiceCollection UseEntityFrameworkCoreSqlServer(WebHostBuilderContext, string connectionStringName, ServiceLifetime = Scoped) -- IServiceCollection UseEntityFrameworkCoreSqlite(WebHostBuilderContext, string connectionStringName, ServiceLifetime = Scoped) -- IServiceCollection UseEntityFrameworkCoreSqlite(WebHostBuilderContext, string connectionStringName, ServiceLifetime = Scoped) +```csharp +using ReactiveMarbles.Extensions.Hosting.Reactive.PluginService; + +await ServiceHost.CreateApplication(typeof(Program), args) + .ConfigureAwait(false); +``` + +## Identity And Entity Framework Core + +SQL Server package: `CP.Extensions.Hosting.Identity.EntityFrameworkCore.SqlServer` + +SQL Server namespace: `ReactiveMarbles.Extensions.Hosting.Identity.EntityFrameworkCore` + +SQLite package: `CP.Extensions.Hosting.Identity.EntityFrameworkCore.Sqlite` + +SQLite namespace: `ReactiveMarbles.Extensions.Hosting.Identity.EntityFrameworkCore.Sqlite` + +### SQL Server API -Host/web host wiring: -- IHostBuilder UseWebHostServices(this IHostBuilder, Action configureServices, bool validateScopes = false) -- IHostBuilder UseWebHostServices(this IHostBuilder, Action configureServices, Func configureWebHost, bool validateScopes = false) -- IHostBuilder UseWebHostServices(this IHostBuilder, Action configureServices, Func configureWebHost, Func configureApp, bool validateScopes = false) +| API | Description | +| --- | --- | +| `IConfiguration.HasConnectionString(string connectionStringName)` | Returns `true` when the named connection string exists and is not empty. | +| `IConfiguration.GetRequiredConnectionString(string connectionStringName)` | Gets the named connection string or throws a descriptive `InvalidOperationException`. | +| `IHostApplicationBuilder.AddSqlServerDbContext(string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped)` | Registers a SQL Server `DbContext`. | +| `IHostApplicationBuilder.AddSqlServerWithIdentity(string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped)` | Registers SQL Server EF Core plus ASP.NET Core Identity with roles. | +| `IHostApplicationBuilder.AddSqlServerWithIdentity(string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped)` | Registers SQL Server EF Core plus ASP.NET Core Identity without a custom role type. | +| `IHostBuilder.UseWebHostServices(...)` | Adds minimal ASP.NET Core web-host services. Three overloads support service-only, web-host customization, and web-host plus app-pipeline customization. | +| `IServiceCollection.UseEntityFrameworkCoreSqlServer(WebHostBuilderContext context, string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped)` | Registers SQL Server EF Core plus Identity with roles in a web-host service callback. | +| `IServiceCollection.UseEntityFrameworkCoreSqlServer(WebHostBuilderContext context, string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped)` | Registers SQL Server EF Core plus Identity without a custom role type. | +| `IServiceCollection.AddSqlServerDbContext(IConfiguration configuration, string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped)` | Registers a SQL Server `DbContext` from configuration. | +| `IServiceCollection.AddSqlServerDbContextWithConnectionString(string connectionString, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped)` | Registers a SQL Server `DbContext` from a direct connection string. | + +### SQLite API + +| API | Description | +| --- | --- | +| `IHostApplicationBuilder.AddSqliteDbContext(string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped)` | Registers a SQLite `DbContext`. | +| `IHostApplicationBuilder.AddSqliteWithIdentity(string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped)` | Registers SQLite EF Core plus ASP.NET Core Identity with roles. | +| `IHostApplicationBuilder.AddSqliteWithIdentity(string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped)` | Registers SQLite EF Core plus ASP.NET Core Identity without a custom role type. | +| `IHostBuilder.UseWebHostServices(...)` | Adds minimal ASP.NET Core web-host services. Three overloads match the SQL Server package. | +| `IServiceCollection.UseEntityFrameworkCoreSqlite(WebHostBuilderContext context, string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped)` | Registers SQLite EF Core plus Identity with roles in a web-host service callback. | +| `IServiceCollection.UseEntityFrameworkCoreSqlite(WebHostBuilderContext context, string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped)` | Registers SQLite EF Core plus Identity without a custom role type. | +| `IServiceCollection.AddSqliteDbContext(IConfiguration configuration, string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped)` | Registers a SQLite `DbContext` from configuration. | +| `IServiceCollection.AddSqliteDbContextWithConnectionString(string connectionString, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped)` | Registers a SQLite `DbContext` from a direct connection string. | +| `CreateInMemoryConnectionString(string? databaseName = null)` | Creates a SQLite in-memory connection string. | +| `CreateFileConnectionString(string filePath)` | Creates a SQLite file connection string. | + +### Modern Builder Example -Example: ```csharp using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Hosting; using ReactiveMarbles.Extensions.Hosting.Identity.EntityFrameworkCore; -var host = Host.CreateDefaultBuilder(args) - .UseWebHostServices((whb, services) => - { - services.UseEntityFrameworkCoreSqlServer(whb, "DefaultConnection"); - }) - .Build(); +var builder = Host.CreateApplicationBuilder(args); + +builder.AddSqlServerWithIdentity( + "DefaultConnection"); -await host.RunAsync(); +using var host = builder.Build(); +await host.RunAsync().ConfigureAwait(false); + +public sealed class AppDbContext(DbContextOptions options) + : IdentityDbContext(options) +{ +} ``` -### Reactive examples -- WPF + ReactiveUI + Single instance: +### Web Host Services Example + ```csharp -var builder = Host.CreateApplicationBuilder(args) - .ConfigureSplatForMicrosoftDependencyResolver() - .ConfigureWpf(wpf => +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Hosting; +using ReactiveMarbles.Extensions.Hosting.Identity.EntityFrameworkCore.Sqlite; + +var host = new HostBuilder() + .UseWebHostServices((context, services) => { - wpf.ApplicationType = typeof(App); - wpf.WindowTypes.Add(typeof(MainWindow)); + services.UseEntityFrameworkCoreSqlite( + context, + "DefaultConnection"); }) - .UseWpfLifetime() - .ConfigureSingleInstance("{ea031523-3a63-45e5-85f2-6fa75fbf37ed}"); + .Build(); -await builder.Build().RunAsync(); +await host.RunAsync().ConfigureAwait(false); ``` -### Notes -- WPF/WinForms/WinUI components target Windows only. -- When using IHostApplicationBuilder, prefer chaining extension methods that return IHostApplicationBuilder so you can call Build() on the final HostApplicationBuilder instance. -- Plugin scanning uses glob patterns. Ensure your plugin folders are copied to output and the patterns match your runtime folder (e.g., Plugins\net9.0-windows\Your.Plugin.*.dll). +### Direct SQLite Connection Example ---- +```csharp +using Microsoft.Extensions.DependencyInjection; +using ReactiveMarbles.Extensions.Hosting.Identity.EntityFrameworkCore.Sqlite; + +var services = new ServiceCollection(); +var connectionString = + HostBuilderEntityFrameworkCoreExtensions.CreateInMemoryConnectionString("tests"); + +services.AddSqliteDbContextWithConnectionString(connectionString); +``` + +## Recommended Composition -## Legacy snippets (IHostBuilder only) +The packages are designed to compose as small host extensions. A desktop app can combine single-instance enforcement, ReactiveUI/Splat, platform lifetime, and plug-in loading in one host. -#### ReactiveMarbles.Extensions.Hosting.Plugins ```csharp -.ConfigurePlugins(pluginBuilder => -{ - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("Running using dotNet {0}", Environment.Version); +using Microsoft.Extensions.Hosting; +using ReactiveMarbles.Extensions.Hosting.AppServices; +using ReactiveMarbles.Extensions.Hosting.Plugins; +using ReactiveMarbles.Extensions.Hosting.ReactiveUI; +using ReactiveMarbles.Extensions.Hosting.Wpf; - var process = Process.GetCurrentProcess(); - var fullPath = process.MainModule?.FileName?.Replace(process.MainModule.ModuleName!, string.Empty); - Console.WriteLine("Add Scan Directories: {0}", fullPath); - pluginBuilder?.AddScanDirectories(fullPath!); +var host = new HostBuilder() + .ConfigureSplatForMicrosoftDependencyResolver() + .ConfigureSingleInstance("{1E938C68-6B3E-4105-BE52-2432FE80F19C}") + .ConfigurePlugins(plugins => + { + plugins?.AddScanDirectories(AppContext.BaseDirectory); + plugins?.IncludePlugins("plugins/**/*.dll"); + }) + .ConfigureWpf(wpf => wpf.UseApplication().UseWindow()) + .UseWpfLifetime() + .Build(); + +await host.RunAsync().ConfigureAwait(false); +``` + +## Build And Test - pluginBuilder?.IncludeFrameworks(@"\netstandard2.0\*.FrameworkLib.dll"); +The repository uses the XML `.slnx` solution format and Microsoft Testing Platform with TUnit. - var runtime = Path.GetFileName(AppContext.BaseDirectory); - Console.WriteLine(@"Include Plugins from: \Plugins\{0}\{1}*.dll", runtime, "ReactiveMarbles.Plugin"); - pluginBuilder?.IncludePlugins(@$"\Plugins\{runtime}\{{YourPluginNamespace}}*.dll"); - Console.ResetColor(); -}) +```powershell +cd src +dotnet workload restore +dotnet restore Extensions.Hosting.slnx +dotnet build Extensions.Hosting.slnx -c Release -warnaserror +dotnet test --solution Extensions.Hosting.slnx -c Release +dotnet test --solution Extensions.Hosting.slnx --coverage --coverage-output-format cobertura ``` -#### ReactiveMarbles.Extensions.Hosting.PluginService -```csharp -await ServiceHost.Create( - typeof(Program), - args, - hb => hb, // Configure the HostBuilder - host => {}, // Configure the Host - nameSpace: "ReactiveMarbles.Plugin").ConfigureAwait(false); +Run full builds on Windows because WPF, WinForms, WinUI, Windows service, and .NET Framework target frameworks are part of the solution. diff --git a/src/Extensions.Hosting.PluginService.Reactive/Extensions.Hosting.PluginService.Reactive.csproj b/src/Extensions.Hosting.PluginService.Reactive/Extensions.Hosting.PluginService.Reactive.csproj new file mode 100644 index 0000000..95159fc --- /dev/null +++ b/src/Extensions.Hosting.PluginService.Reactive/Extensions.Hosting.PluginService.Reactive.csproj @@ -0,0 +1,27 @@ + + + + net462;net472;net48;net481;net8.0-windows;net9.0-windows;net10.0-windows;net11.0-windows + enable + enable + CP.Extensions.Hosting.PluginService.Reactive + $(DefineConstants);REACTIVE_SHIM + + + + + + + + + + + + + + + + + + + diff --git a/src/Extensions.Hosting.PluginService/DefaultLogger.cs b/src/Extensions.Hosting.PluginService/DefaultLogger.cs index af13208..0e93a9c 100644 --- a/src/Extensions.Hosting.PluginService/DefaultLogger.cs +++ b/src/Extensions.Hosting.PluginService/DefaultLogger.cs @@ -4,7 +4,11 @@ using Microsoft.Extensions.Logging; +#if REACTIVE_SHIM +namespace ReactiveMarbles.Extensions.Hosting.Reactive.PluginService; +#else namespace ReactiveMarbles.Extensions.Hosting.PluginService; +#endif /// Provides a default implementation of a logger that wraps an existing instance. /// The underlying instance to use for logging operations. Cannot be null. diff --git a/src/Extensions.Hosting.PluginService/ServiceBaseLifetime.cs b/src/Extensions.Hosting.PluginService/ServiceBaseLifetime.cs index 547fda8..c3bdc57 100644 --- a/src/Extensions.Hosting.PluginService/ServiceBaseLifetime.cs +++ b/src/Extensions.Hosting.PluginService/ServiceBaseLifetime.cs @@ -5,7 +5,11 @@ using System.ServiceProcess; using Microsoft.Extensions.Hosting; +#if REACTIVE_SHIM +namespace ReactiveMarbles.Extensions.Hosting.Reactive.PluginService; +#else namespace ReactiveMarbles.Extensions.Hosting.PluginService; +#endif /// Provides an implementation backed by . /// diff --git a/src/Extensions.Hosting.PluginService/ServiceBaseLifetimeHostExtensions.cs b/src/Extensions.Hosting.PluginService/ServiceBaseLifetimeHostExtensions.cs index 0156c66..41bdf78 100644 --- a/src/Extensions.Hosting.PluginService/ServiceBaseLifetimeHostExtensions.cs +++ b/src/Extensions.Hosting.PluginService/ServiceBaseLifetimeHostExtensions.cs @@ -6,7 +6,11 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting.Internal; +#if REACTIVE_SHIM +namespace ReactiveMarbles.Extensions.Hosting.Reactive.PluginService; +#else namespace ReactiveMarbles.Extensions.Hosting.PluginService; +#endif /// Provides extension methods for configuring host lifetimes to enable running .NET applications as Windows services or with console lifetime support. /// These extensions allow integration of service-based or console-based lifetimes into host builders, diff --git a/src/Extensions.Hosting.PluginService/ServiceHost.cs b/src/Extensions.Hosting.PluginService/ServiceHost.cs index fe4ed61..bb236da 100644 --- a/src/Extensions.Hosting.PluginService/ServiceHost.cs +++ b/src/Extensions.Hosting.PluginService/ServiceHost.cs @@ -7,10 +7,18 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +#if REACTIVE_SHIM +using ReactiveMarbles.Extensions.Hosting.Reactive.Plugins; +#else using ReactiveMarbles.Extensions.Hosting.Plugins; +#endif using ReactiveMarbles.Extensions.Logging; +#if REACTIVE_SHIM +namespace ReactiveMarbles.Extensions.Hosting.Reactive.PluginService; +#else namespace ReactiveMarbles.Extensions.Hosting.PluginService; +#endif /// Provides static methods for configuring and running a host or application host with plugin and logging support. /// The ServiceHost class simplifies the setup of .NET hosts, including configuration, logging, and diff --git a/src/Extensions.Hosting.ReactiveUI.Avalonia.Reactive/Extensions.Hosting.ReactiveUI.Avalonia.Reactive.csproj b/src/Extensions.Hosting.ReactiveUI.Avalonia.Reactive/Extensions.Hosting.ReactiveUI.Avalonia.Reactive.csproj new file mode 100644 index 0000000..137a849 --- /dev/null +++ b/src/Extensions.Hosting.ReactiveUI.Avalonia.Reactive/Extensions.Hosting.ReactiveUI.Avalonia.Reactive.csproj @@ -0,0 +1,26 @@ + + + + net8.0;net9.0;net10.0;net11.0 + enable + enable + CP.Extensions.Hosting.ReactiveUI.Avalonia.Reactive + $(DefineConstants);REACTIVE_SHIM + + + + + + + + + + + + + + + + + + diff --git a/src/Extensions.Hosting.ReactiveUI.Avalonia/HostBuilderReactiveUiExtensions.cs b/src/Extensions.Hosting.ReactiveUI.Avalonia/HostBuilderReactiveUiExtensions.cs index 4ca054e..7435edb 100644 --- a/src/Extensions.Hosting.ReactiveUI.Avalonia/HostBuilderReactiveUiExtensions.cs +++ b/src/Extensions.Hosting.ReactiveUI.Avalonia/HostBuilderReactiveUiExtensions.cs @@ -3,12 +3,21 @@ // See the LICENSE file in the project root for full license information. using Microsoft.Extensions.Hosting; +#if REACTIVE_SHIM +using ReactiveUI.Avalonia.Reactive; +using ReactiveUI.Reactive.Builder; +#else using ReactiveUI.Avalonia; using ReactiveUI.Builder; +#endif using Splat; using Splat.Microsoft.Extensions.DependencyInjection; +#if REACTIVE_SHIM +namespace ReactiveMarbles.Extensions.Hosting.Reactive.ReactiveUI; +#else namespace ReactiveMarbles.Extensions.Hosting.ReactiveUI; +#endif /// Provides extension methods for configuring ReactiveUI with Microsoft dependency injection in .NET host builder scenarios. /// These extension methods enable seamless integration of ReactiveUI and Splat with the Microsoft diff --git a/src/Extensions.Hosting.ReactiveUI.Maui.Reactive/Extensions.Hosting.ReactiveUI.Maui.Reactive.csproj b/src/Extensions.Hosting.ReactiveUI.Maui.Reactive/Extensions.Hosting.ReactiveUI.Maui.Reactive.csproj new file mode 100644 index 0000000..42eead1 --- /dev/null +++ b/src/Extensions.Hosting.ReactiveUI.Maui.Reactive/Extensions.Hosting.ReactiveUI.Maui.Reactive.csproj @@ -0,0 +1,36 @@ + + + + net10.0;net11.0;net10.0-android;net11.0-android;net10.0-ios;net10.0-tvos;net10.0-macos;net10.0-maccatalyst;net11.0-ios;net11.0-tvos;net11.0-macos;net11.0-maccatalyst; + $(TargetFrameworks);net10.0-windows10.0.19041.0;net11.0-windows10.0.19041.0 + enable + enable + true + CP.Extensions.Hosting.ReactiveUI.Maui.Reactive + $(DefineConstants);REACTIVE_SHIM + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Extensions.Hosting.ReactiveUI.Maui/HostBuilderReactiveUiExtensions.cs b/src/Extensions.Hosting.ReactiveUI.Maui/HostBuilderReactiveUiExtensions.cs index 4edef8d..57c6154 100644 --- a/src/Extensions.Hosting.ReactiveUI.Maui/HostBuilderReactiveUiExtensions.cs +++ b/src/Extensions.Hosting.ReactiveUI.Maui/HostBuilderReactiveUiExtensions.cs @@ -3,11 +3,19 @@ // See the LICENSE file in the project root for full license information. using Microsoft.Extensions.Hosting; +#if REACTIVE_SHIM +using ReactiveUI.Reactive.Builder; +#else using ReactiveUI.Builder; +#endif using Splat; using Splat.Microsoft.Extensions.DependencyInjection; +#if REACTIVE_SHIM +namespace ReactiveMarbles.Extensions.Hosting.Reactive.ReactiveUI; +#else namespace ReactiveMarbles.Extensions.Hosting.ReactiveUI; +#endif /// Provides extension methods for configuring ReactiveUI integration with Microsoft dependency injection in .NET host builders. /// These extensions enable seamless setup of ReactiveUI and Splat with Microsoft.Extensions.Hosting and diff --git a/src/Extensions.Hosting.ReactiveUI.WinForms.Reactive/Extensions.Hosting.ReactiveUI.WinForms.Reactive.csproj b/src/Extensions.Hosting.ReactiveUI.WinForms.Reactive/Extensions.Hosting.ReactiveUI.WinForms.Reactive.csproj new file mode 100644 index 0000000..ee4ce25 --- /dev/null +++ b/src/Extensions.Hosting.ReactiveUI.WinForms.Reactive/Extensions.Hosting.ReactiveUI.WinForms.Reactive.csproj @@ -0,0 +1,28 @@ + + + + + net462;net472;net48;net481;net8.0-windows10.0.19041;net9.0-windows10.0.19041;net10.0-windows10.0.19041;net11.0-windows10.0.19041 + enable + enable + This extension adds reactive ReactiveUI support to generic host based WinForms applications. + CP.Extensions.Hosting.ReactiveUI.WinForms.Reactive + $(DefineConstants);REACTIVE_SHIM + + + + + + + + + + + + + + + + + + diff --git a/src/Extensions.Hosting.ReactiveUI.WinForms/HostBuilderReactiveUiExtensions.cs b/src/Extensions.Hosting.ReactiveUI.WinForms/HostBuilderReactiveUiExtensions.cs index 0aa305c..44c0336 100644 --- a/src/Extensions.Hosting.ReactiveUI.WinForms/HostBuilderReactiveUiExtensions.cs +++ b/src/Extensions.Hosting.ReactiveUI.WinForms/HostBuilderReactiveUiExtensions.cs @@ -3,11 +3,19 @@ // See the LICENSE file in the project root for full license information. using Microsoft.Extensions.Hosting; +#if REACTIVE_SHIM +using ReactiveUI.Reactive.Builder; +#else using ReactiveUI.Builder; +#endif using Splat; using Splat.Microsoft.Extensions.DependencyInjection; +#if REACTIVE_SHIM +namespace ReactiveMarbles.Extensions.Hosting.Reactive.ReactiveUI; +#else namespace ReactiveMarbles.Extensions.Hosting.ReactiveUI; +#endif /// Provides extension methods for configuring ReactiveUI with Microsoft dependency injection in .NET host builders. /// These extensions enable integration of ReactiveUI's dependency resolution with the diff --git a/src/Extensions.Hosting.ReactiveUI.WinUI.Reactive/Extensions.Hosting.ReactiveUI.WinUI.Reactive.csproj b/src/Extensions.Hosting.ReactiveUI.WinUI.Reactive/Extensions.Hosting.ReactiveUI.WinUI.Reactive.csproj new file mode 100644 index 0000000..95931d0 --- /dev/null +++ b/src/Extensions.Hosting.ReactiveUI.WinUI.Reactive/Extensions.Hosting.ReactiveUI.WinUI.Reactive.csproj @@ -0,0 +1,29 @@ + + + + + net8.0-windows10.0.19041;net9.0-windows10.0.19041;net10.0-windows10.0.19041;net11.0-windows10.0.19041 + enable + enable + $(NoWarn);NETSDK1206 + win-x64;win-x86;win-arm64 + This extension adds reactive ReactiveUI support to generic host based WinUI applications. + CP.Extensions.Hosting.ReactiveUI.WinUI.Reactive + $(DefineConstants);REACTIVE_SHIM + + + + + + + + + + + + + + + + + diff --git a/src/Extensions.Hosting.ReactiveUI.WinUI/HostBuilderReactiveUiExtensions.cs b/src/Extensions.Hosting.ReactiveUI.WinUI/HostBuilderReactiveUiExtensions.cs index 1fe75de..8ff3e61 100644 --- a/src/Extensions.Hosting.ReactiveUI.WinUI/HostBuilderReactiveUiExtensions.cs +++ b/src/Extensions.Hosting.ReactiveUI.WinUI/HostBuilderReactiveUiExtensions.cs @@ -4,10 +4,18 @@ using Microsoft.Extensions.Hosting; using ReactiveUI.Builder; + +#if REACTIVE_SHIM +using ReactiveUI.Reactive.Builder; +#endif using Splat; using Splat.Microsoft.Extensions.DependencyInjection; +#if REACTIVE_SHIM +namespace ReactiveMarbles.Extensions.Hosting.Reactive.ReactiveUI; +#else namespace ReactiveMarbles.Extensions.Hosting.ReactiveUI; +#endif /// Provides extension methods for configuring ReactiveUI integration with Microsoft dependency injection in .NET host builders. /// These extensions enable seamless setup of ReactiveUI and Splat with Microsoft.Extensions.Hosting and diff --git a/src/Extensions.Hosting.ReactiveUI.Wpf.Reactive/Extensions.Hosting.ReactiveUI.Wpf.Reactive.csproj b/src/Extensions.Hosting.ReactiveUI.Wpf.Reactive/Extensions.Hosting.ReactiveUI.Wpf.Reactive.csproj new file mode 100644 index 0000000..bcb05ab --- /dev/null +++ b/src/Extensions.Hosting.ReactiveUI.Wpf.Reactive/Extensions.Hosting.ReactiveUI.Wpf.Reactive.csproj @@ -0,0 +1,28 @@ + + + + + net462;net472;net48;net481;net8.0-windows10.0.19041;net9.0-windows10.0.19041;net10.0-windows10.0.19041;net11.0-windows10.0.19041 + enable + enable + This extension adds reactive ReactiveUI support to generic host based WPF applications. + CP.Extensions.Hosting.ReactiveUI.Wpf.Reactive + $(DefineConstants);REACTIVE_SHIM + + + + + + + + + + + + + + + + + + diff --git a/src/Extensions.Hosting.ReactiveUI.Wpf/HostBuilderReactiveUiExtensions.cs b/src/Extensions.Hosting.ReactiveUI.Wpf/HostBuilderReactiveUiExtensions.cs index feb65c3..1c709f1 100644 --- a/src/Extensions.Hosting.ReactiveUI.Wpf/HostBuilderReactiveUiExtensions.cs +++ b/src/Extensions.Hosting.ReactiveUI.Wpf/HostBuilderReactiveUiExtensions.cs @@ -3,11 +3,19 @@ // See the LICENSE file in the project root for full license information. using Microsoft.Extensions.Hosting; +#if REACTIVE_SHIM +using ReactiveUI.Reactive.Builder; +#else using ReactiveUI.Builder; +#endif using Splat; using Splat.Microsoft.Extensions.DependencyInjection; +#if REACTIVE_SHIM +namespace ReactiveMarbles.Extensions.Hosting.Reactive.ReactiveUI; +#else namespace ReactiveMarbles.Extensions.Hosting.ReactiveUI; +#endif /// Provides extension methods for configuring ReactiveUI with Microsoft dependency injection in .NET host builder scenarios. /// These extension methods enable seamless integration of ReactiveUI and Splat with the Microsoft diff --git a/src/Extensions.Hosting.slnx b/src/Extensions.Hosting.slnx index aecfb10..4a989a3 100644 --- a/src/Extensions.Hosting.slnx +++ b/src/Extensions.Hosting.slnx @@ -33,11 +33,17 @@ + + + + + +