From c86fd84b0375ad20694da96c96093d67959466c0 Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Sun, 5 Jul 2026 16:46:03 +0100 Subject: [PATCH 1/4] feat: migrate hosting packages to ReactiveUI.Primitives Package management and targets: - Add Central Package Management with StyleSharp.Analyzers 3.16.0, ReactiveUI.Primitives 6.0.0, ReactiveUI 24.0.0-beta.3, ReactiveUI.Avalonia 12.1.0-beta.1, and net11 preview package versions. - Expand projects to net11.0 and relevant net11.0 platform TFMs. ReactiveUI.Primitives migration: - Remove direct System.Reactive source and project usage in favor of ReactiveUI.Primitives aliases and APIs. - Update hosted service, platform, plugin, ReactiveUI bridge, and example code for the new primitives surface. Analyzer and quality updates: - Resolve StyleSharp/Roslyn analyzer findings without suppressions and keep project documentation/XML comments aligned. - Convert repository guidance to AGENTS.md and include it in the solution-oriented update. Tests and validation: - Add focused TUnit coverage for plugin loading, hosted service lifecycle, plugin base registration, assembly loading, and single-instance behavior. - Fix the ReactiveAvalonia example result template so searched NuGet packages render in the list. --- .editorconfig | 2249 +++++++++++++---- .github/copilot-instructions.md | 6 +- agent.md => AGENTS.md | 7 +- CLAUDE.md | 4 +- Directory.Packages.props | 138 + build/_build.csproj | 10 +- src/Directory.Build.props | 26 +- .../AvaloniaHostedService.cs | 20 +- .../Extensions.Hosting.Avalonia.csproj | 10 +- .../Extensions/AvaloniaBuilderExtensions.cs | 174 +- ...ostApplicationBuilderAvaloniaExtensions.cs | 67 +- .../HostBuilderAvaloniaExtensions.cs | 72 +- .../InternalBuilderAvaloniaUtility.cs | 179 +- .../IAvaloniaBuilder.cs | 29 +- .../IAvaloniaContext.cs | 25 +- .../IAvaloniaService.cs | 12 +- .../IAvaloniaShell.cs | 12 +- .../Internals/AvaloniaBuilder.cs | 6 +- .../Internals/AvaloniaContext.cs | 6 +- .../Internals/AvaloniaThread.cs | 22 +- ...Identity.EntityFrameworkCore.Sqlite.csproj | 51 +- ...ostBuilderEntityFrameworkCoreExtensions.cs | 562 ++-- ...ntity.EntityFrameworkCore.SqlServer.csproj | 51 +- ...ostBuilderEntityFrameworkCoreExtensions.cs | 590 ++--- .../BaseUiContext.cs | 4 +- .../BaseUiThread.cs | 74 +- .../Extensions.Hosting.MainUIThread.csproj | 6 +- .../IUiContext.cs | 17 +- .../Extensions.Hosting.Maui.csproj | 19 +- .../HostBuilderMauiExtensions.cs | 401 ++- src/Extensions.Hosting.Maui/IMauiBuilder.cs | 28 +- src/Extensions.Hosting.Maui/IMauiContext.cs | 17 +- src/Extensions.Hosting.Maui/IMauiService.cs | 13 +- src/Extensions.Hosting.Maui/IMauiShell.cs | 13 +- .../Internals/MauiBuilder.cs | 10 +- .../Internals/MauiContext.cs | 6 +- .../Internals/MauiThread.cs | 15 +- .../MauiBuilderExtensions.cs | 133 +- .../MauiHostedService.cs | 38 +- .../DefaultLogger.cs | 12 +- .../Extensions.Hosting.PluginService.csproj | 12 +- .../ServiceBaseLifetime.cs | 30 +- .../ServiceBaseLifetimeHostExtensions.cs | 145 +- .../ServiceHost.cs | 435 ++-- .../Extensions.Hosting.Plugins.csproj | 18 +- .../HostBuilderPluginExtensions.cs | 380 ++- .../HostedServiceBase.cs | 116 +- src/Extensions.Hosting.Plugins/IPlugin.cs | 12 +- .../IPluginBuilder.cs | 37 +- .../Internals/AssemblyDependencyResolver.cs | 62 +- .../Internals/AssemblyLoadContext.cs | 43 +- .../AssemblyLoadContextExtensions.cs | 69 +- .../Internals/PluginBuilder.cs | 10 +- .../Internals/PluginLoadContext.cs | 49 +- .../PluginBase{T1,T2,T3}.cs | 8 +- .../PluginBase{T1,T2}.cs | 8 +- .../PluginBase{T}.cs | 9 +- .../PluginBuilderExtensions.cs | 202 +- .../PluginOrderAttribute.cs | 28 +- .../PluginScanner.cs | 35 +- ...ensions.Hosting.ReactiveUI.Avalonia.csproj | 10 +- .../HostBuilderReactiveUiExtensions.cs | 118 +- .../Extensions.Hosting.ReactiveUI.Maui.csproj | 22 +- .../HostBuilderReactiveUiExtensions.cs | 109 +- ...ensions.Hosting.ReactiveUI.WinForms.csproj | 12 +- .../HostBuilderReactiveUiExtensions.cs | 119 +- ...Extensions.Hosting.ReactiveUI.WinUI.csproj | 10 +- .../HostBuilderReactiveUiExtensions.cs | 114 +- .../Extensions.Hosting.ReactiveUI.Wpf.csproj | 12 +- .../HostBuilderReactiveUiExtensions.cs | 118 +- .../Extensions.Hosting.SingleInstance.csproj | 6 +- .../HostBuilderApplicationExtensions.cs | 185 +- .../IMutexBuilder.cs | 21 +- .../Internal/MutexBuilder.cs | 10 +- .../Internal/MutexLifetimeService.cs | 47 +- .../ResourceMutex.cs | 145 +- .../Extensions.Hosting.WinForms.csproj | 4 +- .../HostBuilderWinFormsExtensions.cs | 339 ++- .../IWinFormsContext.cs | 17 +- .../IWinFormsService.cs | 12 +- .../IWinFormsShell.cs | 12 +- .../Internals/MultiShellContext.cs | 27 +- .../Internals/WinFormsContext.cs | 8 +- .../Internals/WinFormsThread.cs | 23 +- .../WinFormsHostedService.cs | 53 +- .../Extensions.Hosting.WinUI.csproj | 6 +- .../HostBuilderWinUIExtensions.cs | 210 +- src/Extensions.Hosting.WinUI/IWinUIContext.cs | 25 +- src/Extensions.Hosting.WinUI/IWinUIService.cs | 12 +- .../Internals/WinUIContext.cs | 9 +- .../Internals/WinUIThread.cs | 11 +- .../WinUIHostedService.cs | 38 +- .../Extensions.Hosting.Wpf.csproj | 4 +- .../HostBuilderWpfExtensions.cs | 366 ++- src/Extensions.Hosting.Wpf/IWpfBuilder.cs | 25 +- src/Extensions.Hosting.Wpf/IWpfContext.cs | 21 +- src/Extensions.Hosting.Wpf/IWpfService.cs | 12 +- src/Extensions.Hosting.Wpf/IWpfShell.cs | 13 +- .../Internals/WpfBuilder.cs | 11 +- .../Internals/WpfContext.cs | 11 +- .../Internals/WpfThread.cs | 34 +- .../WpfBuilderExtensions.cs | 123 +- .../WpfHostedService.cs | 26 +- src/Extensions.Hosting.slnx | 1 + .../Entities/MessageCandidate.cs | 35 +- .../Entities/NodeInfo.cs | 20 +- .../Extensions.Logging.Log4Net.csproj | 48 +- .../Extensions/DocumentExtensions.cs | 68 +- .../Extensions/Log4NetProviderExtensions.cs | 52 +- .../Extensions/LogExtensions.cs | 40 +- .../ILog4NetLogLevelTranslator.cs | 12 +- .../ILog4NetLoggingEventFactory.cs | 18 +- .../Log4NetExtensions.cs | 166 +- .../Log4NetLogLevelTranslator.cs | 57 +- .../Log4NetLogger.cs | 69 +- .../Log4NetLoggingEventFactory.cs | 246 +- .../Log4NetProvider.cs | 303 +-- .../Log4NetProviderOptions.cs | 66 +- .../Scope/NullScope.cs | 17 +- .../Scope/NullScopeProvider.cs | 17 +- .../App.axaml.cs | 13 +- ...Extensions.Hosting.Avalonia.Example.csproj | 10 +- .../HostBuilderExtensions.cs | 63 + .../MainWindow.axaml.cs | 31 +- .../Program.cs | 67 +- .../ViewLocator.cs | 27 +- .../Extensions.Hosting.Console.Example.csproj | 2 +- .../Program.cs | 4 +- .../App.xaml.cs | 18 +- .../AppShell.xaml.cs | 12 +- .../ExampleMauiService.cs | 26 +- .../Extensions.Hosting.Maui.Example.csproj | 14 +- .../MainPage.xaml.cs | 25 +- .../MauiProgram.cs | 44 +- .../Platforms/Android/MainActivity.cs | 9 +- .../Platforms/Android/MainApplication.cs | 25 +- .../Platforms/MacCatalyst/AppDelegate.cs | 10 +- .../Platforms/MacCatalyst/Program.cs | 10 +- .../Platforms/Windows/App.xaml.cs | 20 +- .../Platforms/iOS/AppDelegate.cs | 10 +- .../Platforms/iOS/Program.cs | 10 +- .../SecondPage.xaml.cs | 17 +- .../LifetimeEventsHostedService.cs | 51 +- .../Plugin.cs | 8 +- .../ReactiveMarbles.Plugin.Example.csproj | 4 +- .../TimedHostedService.cs | 49 +- .../App.axaml.cs | 10 +- .../AppViewModel.cs | 45 +- ...ns.Hosting.ReactiveAvalonia.Example.csproj | 21 +- .../HostBuilderExtensions.cs | 58 + .../MainWindow.axaml | 4 +- .../MainWindow.axaml.cs | 14 +- .../NugetDetailsView.axaml.cs | 12 +- .../NugetDetailsViewModel.cs | 64 +- .../Program.cs | 77 +- .../App.xaml.cs | 47 +- .../AppMixins.cs | 65 +- .../AppViewModel.cs | 41 +- ...ensions.Hosting.ReactiveWpf.Example.csproj | 15 +- .../MainWindow.xaml.cs | 19 +- .../NugetDetailsView.xaml.cs | 21 +- .../NugetDetailsViewModel.cs | 48 +- .../App.xaml.cs | 20 +- .../AppMixins.cs | 65 +- .../AssemblyInfo.cs | 4 +- .../Extensions.Hosting.Wpf.Example.csproj | 4 +- .../MainWindow.xaml.cs | 30 +- .../SecondWindow.xaml.cs | 12 +- .../AbstractTestPlugin.cs | 8 +- .../AssemblyLoadingTests.cs | 360 +++ .../ConcreteTestPlugin.cs | 8 +- .../Extensions.Hosting.Tests.csproj | 15 +- .../HostBuilderApplicationExtensionsTests.cs | 146 +- .../HostBuilderPluginExtensionsTests.cs | 320 +++ .../HostedServiceBaseTests.cs | 222 ++ .../MutexBuilderTests.cs | 52 +- .../OrderedTestPlugin.cs | 8 +- .../PluginBaseTests.cs | 101 + .../PluginBuilderExtensionsTests.cs | 71 +- .../PluginOrderAttributeTests.cs | 28 +- .../PluginScannerTests.cs | 42 +- .../ResourceMutexTests.cs | 69 +- .../TestMutexBuilder.cs | 8 +- .../Extensions.Hosting.Tests/TestOrder.cs | 8 +- .../Extensions.Hosting.Tests/TestPlugin.cs | 12 +- .../TestPluginBuilder.cs | 8 +- stylecop.json | 41 - 187 files changed, 7580 insertions(+), 5809 deletions(-) rename agent.md => AGENTS.md (98%) create mode 100644 Directory.Packages.props create mode 100644 src/examples/Extensions.Hosting.Avalonia.Example/HostBuilderExtensions.cs create mode 100644 src/examples/Extensions.Hosting.ReactiveAvalonia.Example/HostBuilderExtensions.cs create mode 100644 src/tests/Extensions.Hosting.Tests/AssemblyLoadingTests.cs create mode 100644 src/tests/Extensions.Hosting.Tests/HostBuilderPluginExtensionsTests.cs create mode 100644 src/tests/Extensions.Hosting.Tests/HostedServiceBaseTests.cs create mode 100644 src/tests/Extensions.Hosting.Tests/PluginBaseTests.cs delete mode 100644 stylecop.json diff --git a/.editorconfig b/.editorconfig index 77a6c40..1303212 100644 --- a/.editorconfig +++ b/.editorconfig @@ -78,8 +78,15 @@ dotnet_naming_symbols.constant_fields.applicable_kinds = field dotnet_naming_symbols.constant_fields.required_modifiers = const dotnet_naming_style.pascal_case_style.capitalization = pascal_case +# Local constants should be PascalCase +dotnet_naming_rule.local_constants_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.local_constants_should_be_pascal_case.symbols = local_constants +dotnet_naming_rule.local_constants_should_be_pascal_case.style = pascal_case_style +dotnet_naming_symbols.local_constants.applicable_kinds = local +dotnet_naming_symbols.local_constants.required_modifiers = const + # Static fields should have s_ prefix -dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion +dotnet_naming_rule.static_fields_should_have_prefix.severity = none dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style dotnet_naming_symbols.static_fields.applicable_kinds = field @@ -105,31 +112,20 @@ dotnet_sort_system_directives_first = true csharp_prefer_braces = true:silent csharp_preserve_single_line_blocks = true:none csharp_preserve_single_line_statements = false:none -csharp_prefer_static_local_function = true:suggestion -csharp_prefer_simple_using_statement = false:none -csharp_style_prefer_switch_expression = true:suggestion ################### # Code quality ################### -dotnet_style_readonly_field = true:suggestion dotnet_code_quality_unused_parameters = non_public:suggestion ################### # Expression-level preferences ################### -dotnet_style_object_initializer = true:suggestion -dotnet_style_collection_initializer = true:suggestion -dotnet_style_explicit_tuple_names = true:suggestion dotnet_style_coalesce_expression = true:suggestion dotnet_style_null_propagation = true:suggestion -dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion -dotnet_style_prefer_inferred_tuple_names = true:suggestion -dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion dotnet_style_prefer_auto_properties = true:suggestion dotnet_style_prefer_conditional_expression_over_assignment = true:silent dotnet_style_prefer_conditional_expression_over_return = true:silent -csharp_prefer_simple_default_expression = true:suggestion ################### # Expression-bodied members @@ -143,19 +139,6 @@ csharp_style_expression_bodied_accessors = true:suggestion csharp_style_expression_bodied_lambdas = true:suggestion csharp_style_expression_bodied_local_functions = true:suggestion -################### -# Pattern matching -################### -csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion -csharp_style_pattern_matching_over_as_with_null_check = true:suggestion -csharp_style_inlined_variable_declaration = true:suggestion - -################### -# Null checking preferences -################### -csharp_style_throw_expression = true:suggestion -csharp_style_conditional_delegate_call = true:suggestion - ################### # Other features ################### @@ -201,18 +184,49 @@ dotnet_diagnostic.AvoidAsyncVoid.severity = suggestion ################### # Microsoft .NET Analyzers (CA) - Design Rules ################### -dotnet_diagnostic.CA1000.severity = none # Do not declare static members on generic types +dotnet_diagnostic.CA1000.severity = none # Do not declare static members on generic types — common factory pattern dotnet_diagnostic.CA1001.severity = error # Types that own disposable fields should be disposable +dotnet_diagnostic.CA1002.severity = none # Do not expose generic lists — we deliberately expose List; interface-based collections are an older convention we don't follow +dotnet_diagnostic.CA1003.severity = error # Use generic event handler instances +dotnet_diagnostic.CA1005.severity = none # Avoid excessive parameters on generic types — we deliberately expose 3+ type-parameter types (tuple-style handles, raw engine signals); the ergonomic guidance conflicts with that design +dotnet_diagnostic.CA1008.severity = error # Enums should have zero value +dotnet_diagnostic.CA1010.severity = none # Collections should implement generic interface — we deliberately expose concrete collection types; interface-based collections are an older convention we don't follow +dotnet_diagnostic.CA1012.severity = error # Abstract types should not have public constructors +dotnet_diagnostic.CA1014.severity = none # Mark assemblies with CLSCompliantAttribute — we don't ship CLS-compliant assemblies dotnet_diagnostic.CA1016.severity = error # Mark assemblies with AssemblyVersionAttribute +dotnet_diagnostic.CA1017.severity = none # Mark assemblies with ComVisibleAttribute — we don't ship COM-visible assemblies +dotnet_diagnostic.CA1018.severity = error # Mark attributes with AttributeUsageAttribute +dotnet_diagnostic.CA1019.severity = error # Define accessors for attribute arguments +dotnet_diagnostic.CA1021.severity = none # Avoid out parameters - disabled - needed for the zero-allocation idiom in Try/Find APIs and other performance-critical paths +dotnet_diagnostic.CA1024.severity = error # Use properties where appropriate dotnet_diagnostic.CA1027.severity = error # Mark enums with FlagsAttribute -dotnet_diagnostic.CA1030.severity = none # Use events where appropriate -dotnet_diagnostic.CA1031.severity = none # Do not catch general exception types -dotnet_diagnostic.CA1033.severity = none # Interface methods should be callable by child types -dotnet_diagnostic.CA1036.severity = none # Override methods on comparable types +dotnet_diagnostic.CA1028.severity = error # Enum storage should be Int32 +dotnet_diagnostic.CA1030.severity = none # Use events where appropriate — we use Rx observables instead of CLR events +dotnet_diagnostic.CA1031.severity = none # Do not catch general exception types — required at logging/dispose/IO boundaries +dotnet_diagnostic.CA1032.severity = error # Implement standard exception constructors +dotnet_diagnostic.CA1033.severity = none # Interface methods should be callable by child types — explicit interface implementations are a deliberate design choice +dotnet_diagnostic.CA1034.severity = none # Nested types should not be visible — public nested types are sometimes the cleanest API (e.g. interface-scoped exception helpers) +dotnet_diagnostic.CA1036.severity = none # Override methods on comparable types — relational operators rarely meaningful for our types +dotnet_diagnostic.CA1040.severity = none # Avoid empty interfaces — duplicate of SST1437 (canonical); marker interfaces (IActivatableView etc.) are deliberate public API +dotnet_diagnostic.CA1041.severity = error # Provide ObsoleteAttribute message +dotnet_diagnostic.CA1043.severity = error # Use integral or string argument for indexers +dotnet_diagnostic.CA1044.severity = error # Properties should not be write only +dotnet_diagnostic.CA1045.severity = none # Do not pass types by reference — we deliberately use ref-passing static helpers so they carry only the data they touch; data-oriented layout is the default here +dotnet_diagnostic.CA1046.severity = error # Do not overload operator equals on reference types +dotnet_diagnostic.CA1047.severity = error # Do not declare protected member in sealed type +dotnet_diagnostic.CA1048.severity = error # Do not declare virtual members in sealed types +dotnet_diagnostic.CA1050.severity = error # Declare types in namespaces +dotnet_diagnostic.CA1051.severity = none # Duplicate of SST1401 (canonical) — do not declare visible instance fields +dotnet_diagnostic.CA1052.severity = error # Static holder types should be sealed +dotnet_diagnostic.CA1053.severity = error # Static holder types should not have constructors +dotnet_diagnostic.CA1054.severity = suggestion # URI parameters should not be strings +dotnet_diagnostic.CA1055.severity = suggestion # URI return values should not be strings dotnet_diagnostic.CA1056.severity = suggestion # URI properties should not be strings +dotnet_diagnostic.CA1058.severity = error # Types should not extend certain base types +dotnet_diagnostic.CA1059.severity = error # Members should not expose certain concrete types dotnet_diagnostic.CA1060.severity = error # Move P/Invokes to NativeMethods class dotnet_diagnostic.CA1061.severity = error # Do not hide base class methods -dotnet_diagnostic.CA1062.severity = error # Validate arguments of public methods +dotnet_diagnostic.CA1062.severity = none # Validate arguments of public methods - Nullable=enable + we own every consumer, so the compiler already guarantees non-null params dotnet_diagnostic.CA1063.severity = error # Implement IDisposable correctly dotnet_diagnostic.CA1064.severity = error # Exceptions should be public dotnet_diagnostic.CA1065.severity = error # Do not raise exceptions in unexpected locations @@ -220,30 +234,13 @@ dotnet_diagnostic.CA1066.severity = error # Implement IEquatable when overriding dotnet_diagnostic.CA1067.severity = error # Override Equals when implementing IEquatable dotnet_diagnostic.CA1068.severity = error # CancellationToken parameters must come last dotnet_diagnostic.CA1069.severity = error # Enums should not have duplicate values -dotnet_diagnostic.CA2000.severity = suggestion # Dispose objects before losing scope -dotnet_diagnostic.CA2002.severity = error # Do not lock on objects with weak identity -dotnet_diagnostic.CA2011.severity = error # Do not assign property within its setter -dotnet_diagnostic.CA2012.severity = error # Use ValueTasks correctly -dotnet_diagnostic.CA2013.severity = error # Do not use ReferenceEquals with value types -dotnet_diagnostic.CA2014.severity = error # Do not use stackalloc in loops -dotnet_diagnostic.CA2015.severity = error # Do not define finalizers for types derived from MemoryManager -dotnet_diagnostic.CA2016.severity = error # Forward the CancellationToken parameter to methods that take one -dotnet_diagnostic.CA2200.severity = error # Rethrow to preserve stack details -dotnet_diagnostic.CA2213.severity = error # Disposable fields should be disposed -dotnet_diagnostic.CA2214.severity = error # Do not call overridable methods in constructors -dotnet_diagnostic.CA2216.severity = error # Disposable types should declare finalizer -dotnet_diagnostic.CA2229.severity = error # Implement serialization constructors -dotnet_diagnostic.CA2231.severity = error # Overload operator equals on overriding ValueType.Equals -dotnet_diagnostic.CA2235.severity = error # Mark all non-serializable fields -dotnet_diagnostic.CA2237.severity = error # Mark ISerializable types with SerializableAttribute -dotnet_diagnostic.CA2241.severity = error # Provide correct arguments to formatting methods -dotnet_diagnostic.CA2242.severity = error # Test for NaN correctly +dotnet_diagnostic.CA1070.severity = error # Do not declare event fields as virtual ################### # Microsoft .NET Analyzers (CA) - Globalization Rules ################### -dotnet_diagnostic.CA1303.severity = none # Do not pass literals as localized parameters -dotnet_diagnostic.CA1308.severity = none # Normalize strings to uppercase +dotnet_diagnostic.CA1303.severity = none # Do not pass literals as localized parameters — we don't ship localized resources +dotnet_diagnostic.CA1308.severity = none # Normalize strings to uppercase — ToLowerInvariant is correct for filesystem path / cache key normalization ################### # Microsoft .NET Analyzers (CA) - Interoperability Rules @@ -253,44 +250,178 @@ dotnet_diagnostic.CA1401.severity = error # P/Invokes should not be visible ################### # Microsoft .NET Analyzers (CA) - Maintainability Rules ################### +dotnet_diagnostic.CA1500.severity = error # Variable names should not match field names +dotnet_diagnostic.CA1501.severity = none # Avoid excessive inheritance — disabled because the analyzer noticeably slows down the build +dotnet_diagnostic.CA1502.severity = none # Avoid excessive complexity — disabled because the analyzer noticeably slows down the build +dotnet_diagnostic.CA1505.severity = error # Avoid unmaintainable code +dotnet_diagnostic.CA1506.severity = none # Avoid excessive class coupling — adds little signal here, mostly trips on legitimate orchestration code dotnet_diagnostic.CA1507.severity = error # Use nameof in place of string +dotnet_diagnostic.CA1508.severity = error # Avoid dead conditional code +dotnet_diagnostic.CA1509.severity = error # Invalid entry in code metrics configuration file +dotnet_diagnostic.CA1510.severity = none # Use ArgumentNullException throw helper — disabled because we target older TFMs and use ArgumentExceptionHelper for cross-platform parity +dotnet_diagnostic.CA1511.severity = none # Use ArgumentException throw helper — disabled because we target older TFMs and use ArgumentExceptionHelper for cross-platform parity +dotnet_diagnostic.CA1512.severity = none # Use ArgumentOutOfRangeException throw helper — disabled because we target older TFMs and use ArgumentExceptionHelper for cross-platform parity +dotnet_diagnostic.CA1513.severity = none # Use ObjectDisposedException throw helper — disabled because we target older TFMs and use ArgumentExceptionHelper for cross-platform parity +dotnet_diagnostic.CA1514.severity = error # Avoid redundant length argument +dotnet_diagnostic.CA1515.severity = none # Consider making public types internal — interferes with tests and reflection-discovered types (BenchmarkDotNet, TUnit, etc.) +dotnet_diagnostic.CA1516.severity = error # Use cross-platform intrinsics ################### # Microsoft .NET Analyzers (CA) - Naming Rules ################### dotnet_diagnostic.CA1710.severity = suggestion # Identifiers should have correct suffix -dotnet_diagnostic.CA1724.severity = none # Type Names Should Not Match Namespaces +dotnet_diagnostic.CA1724.severity = none # Type Names Should Not Match Namespaces — namespace/type name overlap is intentional API surface ################### # Microsoft .NET Analyzers (CA) - Performance Rules ################### -dotnet_diagnostic.CA1802.severity = error # Use Literals Where Appropriate +dotnet_diagnostic.CA1802.severity = error # Use literals where appropriate dotnet_diagnostic.CA1805.severity = error # Do not initialize unnecessarily -dotnet_diagnostic.CA1810.severity = none # Initialize reference type static fields inline +dotnet_diagnostic.CA1806.severity = error # Do not ignore method results +dotnet_diagnostic.CA1810.severity = none # Initialize reference type static fields inline — explicit static constructors are deliberate in some types dotnet_diagnostic.CA1812.severity = error # Avoid uninstantiated internal classes dotnet_diagnostic.CA1813.severity = error # Avoid unsealed attributes dotnet_diagnostic.CA1814.severity = error # Prefer jagged arrays over multidimensional dotnet_diagnostic.CA1815.severity = error # Override equals and operator equals on value types +dotnet_diagnostic.CA1819.severity = none # Properties should not return arrays — incompatible with the RxUI/sqlite-net mapping style we use throughout the codebase +dotnet_diagnostic.CA1820.severity = error # Test for empty strings using string length dotnet_diagnostic.CA1821.severity = error # Remove empty finalizers dotnet_diagnostic.CA1822.severity = error # Mark members as static +dotnet_diagnostic.CA1823.severity = error # Avoid unused private fields +dotnet_diagnostic.CA1824.severity = error # Mark assemblies with NeutralResourcesLanguageAttribute dotnet_diagnostic.CA1825.severity = error # Avoid zero-length array allocations dotnet_diagnostic.CA1826.severity = error # Use property instead of Linq Enumerable method dotnet_diagnostic.CA1827.severity = error # Do not use Count/LongCount when Any can be used dotnet_diagnostic.CA1828.severity = error # Do not use CountAsync/LongCountAsync when AnyAsync can be used dotnet_diagnostic.CA1829.severity = error # Use Length/Count property instead of Enumerable.Count method dotnet_diagnostic.CA1830.severity = error # Prefer strongly-typed Append and Insert method overloads on StringBuilder -dotnet_diagnostic.CA1831.severity = error # Use AsSpan instead of Range-based indexers for string -dotnet_diagnostic.CA1832.severity = error # Use AsSpan or AsMemory instead of Range-based indexers for ReadOnlySpan/Memory -dotnet_diagnostic.CA1833.severity = error # Use AsSpan or AsMemory instead of Range-based indexers for Span/Memory +dotnet_diagnostic.CA1831.severity = error # Use AsSpan instead of Range-based indexers for string when appropriate +dotnet_diagnostic.CA1832.severity = error # Use AsSpan or AsMemory instead of Range-based indexers for getting ReadOnlySpan or ReadOnlyMemory portion of an array +dotnet_diagnostic.CA1833.severity = error # Use AsSpan or AsMemory instead of Range-based indexers for getting Span or Memory portion of an array dotnet_diagnostic.CA1834.severity = error # Use StringBuilder.Append(char) for single character strings -dotnet_diagnostic.CA1835.severity = error # Prefer Memory-based overloads for ReadAsync and WriteAsync +dotnet_diagnostic.CA1835.severity = error # Prefer the memory-based overloads of ReadAsync/WriteAsync methods in stream-based classes dotnet_diagnostic.CA1836.severity = error # Prefer IsEmpty over Count when available dotnet_diagnostic.CA1837.severity = error # Use Environment.ProcessId instead of Process.GetCurrentProcess().Id dotnet_diagnostic.CA1838.severity = error # Avoid StringBuilder parameters for P/Invokes -dotnet_diagnostic.CA2007.severity = none # Do not directly await a Task +dotnet_diagnostic.CA1839.severity = error # Use Environment.ProcessPath instead of Process.GetCurrentProcess().MainModule.FileName +dotnet_diagnostic.CA1840.severity = error # Use Environment.CurrentManagedThreadId instead of Thread.CurrentThread.ManagedThreadId +dotnet_diagnostic.CA1841.severity = error # Prefer Dictionary.Contains methods +dotnet_diagnostic.CA1842.severity = error # Do not use 'WhenAll' with a single task +dotnet_diagnostic.CA1843.severity = error # Do not use 'WaitAll' with a single task +dotnet_diagnostic.CA1844.severity = error # Provide memory-based overrides of async methods when subclassing 'Stream' +dotnet_diagnostic.CA1845.severity = error # Use span-based 'string.Concat' +dotnet_diagnostic.CA1846.severity = error # Prefer AsSpan over Substring +dotnet_diagnostic.CA1847.severity = none # Use char literal for a single character lookup — disabled because the string.Contains(char) overload doesn't exist on .NET Framework / netstandard2.0 and we target both +dotnet_diagnostic.CA1848.severity = error # Use the LoggerMessage delegates +dotnet_diagnostic.CA1849.severity = error # Call async methods when in an async method +dotnet_diagnostic.CA1850.severity = error # Prefer static HashData method over ComputeHash +dotnet_diagnostic.CA1851.severity = error # Possible multiple enumerations of IEnumerable collection +dotnet_diagnostic.CA1852.severity = error # Seal internal types +dotnet_code_quality.CA1852.api_surface = private, internal # only flag non-public classes; public classes stay open for inheritance +dotnet_diagnostic.CA1853.severity = error # Unnecessary call to 'Dictionary.ContainsKey(key)' +dotnet_diagnostic.CA1854.severity = error # Prefer the IDictionary.TryGetValue(TKey, out TValue) method +dotnet_diagnostic.CA1855.severity = error # Prefer 'Clear' over 'Fill' +dotnet_diagnostic.CA1856.severity = error # Incorrect usage of ConstantExpected attribute +dotnet_diagnostic.CA1857.severity = error # A constant is expected for the parameter +dotnet_diagnostic.CA1858.severity = error # Use 'StartsWith' instead of 'IndexOf' +dotnet_diagnostic.CA1859.severity = error # Use concrete types when possible for improved performance +dotnet_diagnostic.CA1860.severity = error # Avoid using 'Enumerable.Any()' extension method +dotnet_diagnostic.CA1861.severity = error # Avoid constant arrays as arguments +dotnet_diagnostic.CA1862.severity = error # Use the 'StringComparison' method overloads to perform case-insensitive string comparisons +dotnet_diagnostic.CA1863.severity = error # Use 'CompositeFormat' +dotnet_diagnostic.CA1864.severity = error # Prefer the 'IDictionary.TryAdd(TKey, TValue)' method +dotnet_diagnostic.CA1865.severity = none # Use char overload (string.StartsWith) — disabled because the string.StartsWith(char) overload doesn't exist on .NET Framework / netstandard2.0 and we target both +dotnet_diagnostic.CA1866.severity = none # Use char overload (string.EndsWith) — disabled because the string.EndsWith(char) overload doesn't exist on .NET Framework / netstandard2.0 and we target both +dotnet_diagnostic.CA1867.severity = none # Use char overload (string.IndexOf / string.LastIndexOf) — disabled because the char overloads don't exist on .NET Framework / netstandard2.0 and we target both +dotnet_diagnostic.CA1868.severity = error # Unnecessary call to 'Contains' for sets +dotnet_diagnostic.CA1869.severity = error # Cache and reuse 'JsonSerializerOptions' instances +dotnet_diagnostic.CA1870.severity = error # Use a cached 'SearchValues' instance +dotnet_diagnostic.CA1871.severity = error # Do not pass a nullable struct to 'ArgumentNullException.ThrowIfNull' +dotnet_diagnostic.CA1872.severity = error # Prefer 'Convert.ToHexString' and 'Convert.ToHexStringLower' over call chains based on 'BitConverter.ToString' +dotnet_diagnostic.CA1873.severity = error # Avoid potentially expensive evaluation of arguments to 'Debug.Assert' +dotnet_diagnostic.CA1874.severity = error # Use 'Regex.IsMatch' +dotnet_diagnostic.CA1875.severity = error # Use 'Regex.Count' +dotnet_diagnostic.CA1877.severity = error # Use 'Encoding.GetString' instead of 'Encoding.GetChars' + +################### +# Microsoft .NET Analyzers (CA) - Reliability Rules +################### +dotnet_diagnostic.CA2000.severity = suggestion # Dispose objects before losing scope +dotnet_diagnostic.CA2002.severity = error # Do not lock on objects with weak identity +dotnet_diagnostic.CA2007.severity = none # Do not directly await a Task — Rx and library callers drive synchronization context themselves dotnet_diagnostic.CA2008.severity = error # Do not create tasks without passing a TaskScheduler dotnet_diagnostic.CA2009.severity = error # Do not call ToImmutableCollection on an ImmutableCollection value +dotnet_diagnostic.CA2011.severity = error # Do not assign property within its setter +dotnet_diagnostic.CA2012.severity = error # Use ValueTasks correctly +dotnet_diagnostic.CA2013.severity = error # Do not use ReferenceEquals with value types +dotnet_diagnostic.CA2014.severity = error # Do not use stackalloc in loops +dotnet_diagnostic.CA2015.severity = error # Do not define finalizers for types derived from MemoryManager +dotnet_diagnostic.CA2016.severity = error # Forward the CancellationToken parameter to methods that take one +dotnet_diagnostic.CA2017.severity = error # Parameter count mismatch +dotnet_diagnostic.CA2018.severity = error # The 'count' argument to 'Buffer.BlockCopy' should specify the number of bytes to copy +dotnet_diagnostic.CA2019.severity = error # Improper 'ThreadStatic' field initialization +dotnet_diagnostic.CA2020.severity = error # Prevent behavioral change caused by built-in operators of IntPtr/UIntPtr +dotnet_diagnostic.CA2021.severity = error # Don't call Enumerable.Cast or Enumerable.OfType with incompatible types +dotnet_diagnostic.CA2022.severity = error # Avoid inexact read with 'Stream.Read' +dotnet_diagnostic.CA2023.severity = error # Invalid braces in message template +dotnet_diagnostic.CA2024.severity = error # Do not use 'StreamReader.EndOfStream' in async methods +dotnet_diagnostic.CA2025.severity = error # Do not pass 'IDisposable' instances into unawaited tasks +dotnet_diagnostic.CA2026.severity = error # Do not use methods or types annotated with [RequiresDynamicCode] in code that uses [RequiresDynamicCode] + +################### +# Microsoft .NET Analyzers (CA) - Usage Rules +################### +dotnet_diagnostic.CA1801.severity = error # Review unused parameters +dotnet_code_quality.CA1801.api_surface = private, internal # only flag non-public APIs so we don't break public signatures +dotnet_diagnostic.CA1816.severity = error # Call GC.SuppressFinalize correctly +dotnet_diagnostic.CA2200.severity = error # Rethrow to preserve stack details +dotnet_diagnostic.CA2201.severity = error # Do not raise reserved exception types dotnet_diagnostic.CA2207.severity = error # Initialize value type static fields inline +dotnet_diagnostic.CA2208.severity = none # Instantiate argument exceptions correctly — too sensitive: flags valid context-forwarding nameof(arg.Property) patterns +dotnet_diagnostic.CA2211.severity = error # Non-constant fields should not be visible +dotnet_diagnostic.CA2213.severity = error # Disposable fields should be disposed +dotnet_diagnostic.CA2214.severity = error # Do not call overridable methods in constructors +dotnet_diagnostic.CA2215.severity = error # Dispose methods should call base class dispose +dotnet_diagnostic.CA2216.severity = error # Disposable types should declare finalizer +dotnet_diagnostic.CA2217.severity = error # Do not mark enums with FlagsAttribute +dotnet_diagnostic.CA2218.severity = error # Override GetHashCode on overriding Equals +dotnet_diagnostic.CA2219.severity = error # Do not raise exceptions in finally clauses +dotnet_diagnostic.CA2224.severity = error # Override Equals on overloading operator equals +dotnet_diagnostic.CA2225.severity = error # Operator overloads have named alternates +dotnet_diagnostic.CA2226.severity = error # Operators should have symmetrical overloads +dotnet_diagnostic.CA2227.severity = none # Collection properties should be read only — settable collection properties are common in our DTOs and config types +dotnet_diagnostic.CA2231.severity = error # Overload operator equals on overriding ValueType.Equals +dotnet_diagnostic.CA2234.severity = error # Pass System.Uri objects instead of strings +dotnet_diagnostic.CA2241.severity = error # Provide correct arguments to formatting methods +dotnet_diagnostic.CA2242.severity = error # Test for NaN correctly +dotnet_diagnostic.CA2243.severity = error # Attribute string literals should parse correctly +dotnet_diagnostic.CA2244.severity = error # Do not duplicate indexed element initializations +dotnet_diagnostic.CA2245.severity = error # Do not assign a property to itself +dotnet_diagnostic.CA2246.severity = error # Do not assign a symbol and its member in the same statement +dotnet_diagnostic.CA2247.severity = error # Argument passed to TaskCompletionSource constructor should be TaskCreationOptions enum +dotnet_diagnostic.CA2248.severity = error # Provide correct enum argument to Enum.HasFlag +dotnet_diagnostic.CA2249.severity = error # Use String.Contains instead of String.IndexOf for substring checks +dotnet_diagnostic.CA2250.severity = error # Use ThrowIfCancellationRequested +dotnet_diagnostic.CA2251.severity = error # Use String.Equals over String.Compare +dotnet_diagnostic.CA2252.severity = error # Opt in to preview features before using them +dotnet_diagnostic.CA2253.severity = error # Named placeholders should not be numeric values +dotnet_diagnostic.CA2254.severity = error # Template should be a static expression +dotnet_diagnostic.CA2255.severity = error # The 'ModuleInitializer' attribute should not be used in libraries +dotnet_diagnostic.CA2256.severity = error # All members declared in parent interfaces must have an implementation in a 'DynamicInterfaceCastableImplementation' interface +dotnet_diagnostic.CA2257.severity = error # Members defined in a type with the 'DynamicInterfaceCastableImplementationAttribute' should be 'static' +dotnet_diagnostic.CA2258.severity = error # Providing a 'DynamicInterfaceCastableImplementation' interface in Visual Basic is unsupported +dotnet_diagnostic.CA2259.severity = error # 'ThreadStatic' only affects static fields +dotnet_diagnostic.CA2260.severity = error # Implement generic math interfaces correctly +dotnet_diagnostic.CA2261.severity = error # Do not use 'ConfigureAwaitOptions.SuppressThrowing' with 'Task' +dotnet_diagnostic.CA2262.severity = error # Set 'MaxResponseHeadersLength' properly +dotnet_diagnostic.CA2263.severity = error # Prefer generic overload when type is known +dotnet_diagnostic.CA2264.severity = error # Do not pass a non-nullable value to 'ArgumentNullException.ThrowIfNull' +dotnet_diagnostic.CA2265.severity = error # Do not compare Span to 'null' or 'default' +dotnet_diagnostic.CA2266.severity = error # Do not consume the result of 'Span.Enumerator.MoveNext()' more than once +dotnet_diagnostic.CA2267.severity = error # Member should not be marked with 'StaticAttribute' +dotnet_diagnostic.CA2268.severity = error # Use 'string.Equals(string, string, StringComparison)' instead of the implicit ordinal overload +# Skipped (deprecated ISerializable formatter): CA2229 Implement serialization constructors, +# CA2235 Mark all non-serializable fields, CA2237 Mark ISerializable types with SerializableAttribute. ################### # Microsoft .NET Analyzers (CA) - Security Rules @@ -345,25 +476,35 @@ dotnet_diagnostic.CA5362.severity = error # Potential reference cycle in deseria dotnet_diagnostic.CA5350.severity = error # Do not use weak cryptographic algorithms (SHA1, RIPEMD160, TripleDES) dotnet_diagnostic.CA5351.severity = error # Do not use broken cryptographic algorithms (MD5, DES, RC2) dotnet_diagnostic.CA5358.severity = error # Do not use unsafe cipher modes (ECB, OFB, CFB) +dotnet_diagnostic.CA5373.severity = error # Do not use obsolete key derivation function +dotnet_diagnostic.CA5379.severity = error # Ensure key derivation function algorithm is sufficiently strong dotnet_diagnostic.CA5384.severity = error # Do not use Digital Signature Algorithm (DSA) dotnet_diagnostic.CA5385.severity = error # Use RSA algorithm with sufficient key size (>= 2048 bits) +dotnet_diagnostic.CA5387.severity = error # Do not use weak key derivation function with insufficient iteration count +dotnet_diagnostic.CA5388.severity = error # Ensure sufficient iteration count when using weak key derivation function dotnet_diagnostic.CA5390.severity = error # Do not hard-code encryption key dotnet_diagnostic.CA5394.severity = error # Do not use insecure randomness (use RNGCryptoServiceProvider) dotnet_diagnostic.CA5401.severity = error # Do not use CreateEncryptor with non-default IV +dotnet_diagnostic.CA5402.severity = error # Use CreateEncryptor with the default IV dotnet_diagnostic.CA5403.severity = error # Do not hard-code certificate -dotnet_diagnostic.CA5373.severity = error # Do not use obsolete key derivation function # TLS/SSL Protocol Security dotnet_diagnostic.CA5359.severity = error # Do not disable certificate validation dotnet_diagnostic.CA5361.severity = error # Do not disable SChannel use of strong crypto dotnet_diagnostic.CA5364.severity = error # Do not use deprecated security protocols (TLS 1.0, TLS 1.1, SSL3) dotnet_diagnostic.CA5378.severity = error # Do not disable ServicePointManagerSecurityProtocols +dotnet_diagnostic.CA5380.severity = error # Do not add certificates to root store +dotnet_diagnostic.CA5381.severity = error # Ensure certificates are not added to root store dotnet_diagnostic.CA5386.severity = error # Avoid hardcoding SecurityProtocolType value dotnet_diagnostic.CA5397.severity = error # Do not use deprecated SslProtocols values dotnet_diagnostic.CA5398.severity = error # Avoid hardcoded SslProtocols values dotnet_diagnostic.CA5399.severity = error # Definitely disable HttpClient certificate revocation list check -dotnet_diagnostic.CA5380.severity = error # Do not add certificates to root store -dotnet_diagnostic.CA5381.severity = error # Ensure certificates are not added to root store +dotnet_diagnostic.CA5400.severity = error # Ensure HttpClient certificate revocation list check is not disabled + +# Azure Storage / Shared Access Signature +dotnet_diagnostic.CA5375.severity = error # Do not use account shared access signature +dotnet_diagnostic.CA5376.severity = error # Use SharedAccessProtocol HttpsOnly +dotnet_diagnostic.CA5377.severity = error # Use container level access policy # XML Security dotnet_diagnostic.CA3061.severity = error # Do not add schema by URL @@ -382,9 +523,15 @@ dotnet_diagnostic.CA3147.severity = error # Mark verb handlers with ValidateAnti dotnet_diagnostic.CA5363.severity = error # Do not disable request validation dotnet_diagnostic.CA5365.severity = error # Do not disable HTTP header checking dotnet_diagnostic.CA5368.severity = error # Set ViewStateUserKey for classes derived from Page +dotnet_diagnostic.CA5382.severity = error # Use secure cookies in ASP.NET Core +dotnet_diagnostic.CA5383.severity = error # Ensure use of secure cookies in ASP.NET Core +dotnet_diagnostic.CA5391.severity = error # Use antiforgery tokens in ASP.NET Core MVC controllers +dotnet_diagnostic.CA5395.severity = error # Miss HttpVerb attribute for action methods +dotnet_diagnostic.CA5396.severity = error # Set HttpOnly to true for HttpCookie # P/Invoke & DLL Security dotnet_diagnostic.CA2101.severity = error # Specify marshalling for P/Invoke string arguments +dotnet_diagnostic.CA5392.severity = error # Use DefaultDllImportSearchPaths attribute for P/Invokes dotnet_diagnostic.CA5393.severity = error # Do not use unsafe DllImportSearchPath value # Archive & File Security @@ -401,404 +548,956 @@ dotnet_diagnostic.CA2153.severity = error # Do not catch corrupted state excepti dotnet_diagnostic.CA5367.severity = error # Do not serialize types with pointer fields ################### -# Microsoft .NET Analyzers (CA) - Reliability Rules (Additional) -################### -dotnet_diagnostic.CA2017.severity = error # Parameter count mismatch in logging -dotnet_diagnostic.CA2018.severity = error # Buffer.BlockCopy count argument -dotnet_diagnostic.CA2019.severity = error # ThreadStatic fields should not use inline initialization -dotnet_diagnostic.CA2020.severity = error # Prevent behavioral change with IntPtr/UIntPtr -dotnet_diagnostic.CA2021.severity = error # Don't call Cast/OfType with incompatible types -dotnet_diagnostic.CA2022.severity = error # Avoid inexact read with Stream.Read -dotnet_diagnostic.CA2023.severity = error # Invalid braces in message template -dotnet_diagnostic.CA2025.severity = error # Do not pass IDisposable into unawaited tasks - -################### -# Roslynator Analyzers (RCS) - Code Simplification +# SonarAnalyzer (Sxxxx) — global suppressions +################### +dotnet_diagnostic.S1075.severity = none # Hardcoded URI — canonical SourceLink hosts are the point +dotnet_diagnostic.S2436.severity = none # Too many generic parameters — needed for the projector overload +dotnet_diagnostic.S4036.severity = none # PATH-relative process spawn — benchmark only, trusted env + +################### +# Microsoft .NET Runtime Obsoletions (SYSLIB0xxx) +################### +dotnet_diagnostic.SYSLIB0001.severity = error # The UTF-7 encoding is insecure and should not be used +dotnet_diagnostic.SYSLIB0002.severity = error # PrincipalPermissionAttribute is not honored by the runtime and must not be used +dotnet_diagnostic.SYSLIB0003.severity = error # Code Access Security (CAS) is not supported or honored by the runtime +dotnet_diagnostic.SYSLIB0004.severity = error # Constrained Execution Region (CER) feature is not supported +dotnet_diagnostic.SYSLIB0005.severity = error # Global Assembly Cache is not supported +dotnet_diagnostic.SYSLIB0006.severity = error # Thread.Abort is not supported and throws PlatformNotSupportedException +dotnet_diagnostic.SYSLIB0007.severity = error # The default implementation of this cryptography algorithm is not supported +dotnet_diagnostic.SYSLIB0008.severity = error # The CreatePdbGenerator API is not supported and throws PlatformNotSupportedException +dotnet_diagnostic.SYSLIB0009.severity = error # The AuthenticationManager Authenticate and PreAuthenticate methods are not supported +dotnet_diagnostic.SYSLIB0010.severity = error # This Remoting API is not supported and throws PlatformNotSupportedException +dotnet_diagnostic.SYSLIB0011.severity = error # BinaryFormatter serialization is obsolete +dotnet_diagnostic.SYSLIB0012.severity = error # Assembly.CodeBase and Assembly.EscapedCodeBase are obsolete +dotnet_diagnostic.SYSLIB0013.severity = error # Uri.EscapeUriString can corrupt the Uri string +dotnet_diagnostic.SYSLIB0014.severity = error # WebRequest, HttpWebRequest, ServicePoint, and WebClient are obsolete — use HttpClient +dotnet_diagnostic.SYSLIB0015.severity = error # DisablePrivateReflectionAttribute is not honored +dotnet_diagnostic.SYSLIB0016.severity = error # Use Marshal.GetExceptionPointers instead +dotnet_diagnostic.SYSLIB0017.severity = error # Strong name signing is not supported on .NET Core +dotnet_diagnostic.SYSLIB0018.severity = error # ReflectionOnly loading is not supported +dotnet_diagnostic.SYSLIB0019.severity = error # RuntimeEnvironment members SystemConfigurationFile, GetSystemVersion, and FromGlobalAccessCache are not supported +dotnet_diagnostic.SYSLIB0020.severity = error # JsonSerializerOptions.IgnoreNullValues is obsolete — use DefaultIgnoreCondition +dotnet_diagnostic.SYSLIB0021.severity = error # Derived cryptographic types are obsolete — use the Create factory methods +dotnet_diagnostic.SYSLIB0022.severity = error # Rijndael types are obsolete — use Aes +dotnet_diagnostic.SYSLIB0023.severity = error # RNGCryptoServiceProvider is obsolete — use RandomNumberGenerator +dotnet_diagnostic.SYSLIB0024.severity = error # Creating and unloading AppDomains is not supported and throws an exception +dotnet_diagnostic.SYSLIB0025.severity = error # SuppressIldasmAttribute has no effect in .NET 6.0+ +dotnet_diagnostic.SYSLIB0026.severity = error # X509Certificate and X509Certificate2 parameterless constructors are obsolete +dotnet_diagnostic.SYSLIB0027.severity = error # PublicKey.Key is obsolete — use the appropriate method to get the public key +dotnet_diagnostic.SYSLIB0028.severity = error # X509Certificate2.PrivateKey is obsolete — use the appropriate method to get the private key +dotnet_diagnostic.SYSLIB0029.severity = error # ProduceLegacyHmacValues is obsolete +dotnet_diagnostic.SYSLIB0030.severity = error # HMACSHA1 always uses the algorithm implementation provided by the platform +dotnet_diagnostic.SYSLIB0031.severity = error # EncodingProvider.GetEncoding(int, EncoderFallback, DecoderFallback) is obsolete +dotnet_diagnostic.SYSLIB0032.severity = error # Recovery from corrupted process state exceptions is not supported +dotnet_diagnostic.SYSLIB0033.severity = error # Rfc2898DeriveBytes.CryptDeriveKey is obsolete +dotnet_diagnostic.SYSLIB0034.severity = error # CmsSigner has been deprecated — use a constructor that accepts a SubjectIdentifierType +dotnet_diagnostic.SYSLIB0035.severity = error # ComputeCounterSignature without specifying a CmsSigner is obsolete +dotnet_diagnostic.SYSLIB0036.severity = error # Regex.CompileToAssembly is obsolete and not supported — use the RegexGenerator source generator +dotnet_diagnostic.SYSLIB0037.severity = error # AssemblyName members HashAlgorithm, ProcessorArchitecture, and VersionCompatibility are obsolete +dotnet_diagnostic.SYSLIB0038.severity = error # SerializationFormat.Binary is obsolete +dotnet_diagnostic.SYSLIB0039.severity = error # TLS versions 1.0 and 1.1 have known vulnerabilities and are not recommended +dotnet_diagnostic.SYSLIB0040.severity = error # EncryptionPolicy.NoEncryption and AllowEncryption significantly reduce security +dotnet_diagnostic.SYSLIB0041.severity = error # The default iteration count for Rfc2898DeriveBytes parameters is outdated and insecure +dotnet_diagnostic.SYSLIB0042.severity = error # ToXmlString and FromXmlString have no implementation for ECC types +dotnet_diagnostic.SYSLIB0043.severity = error # ECDiffieHellmanPublicKey.ToByteArray() and the associated constructor do not have a consistent and interoperable implementation +dotnet_diagnostic.SYSLIB0044.severity = error # AssemblyName.CodeBase and AssemblyName.EscapedCodeBase are obsolete +dotnet_diagnostic.SYSLIB0045.severity = error # Cryptographic factory methods accepting an algorithm name are obsolete +dotnet_diagnostic.SYSLIB0046.severity = error # ControlledExecution.Run may corrupt the process and should not be used in production code +dotnet_diagnostic.SYSLIB0047.severity = error # XmlSecureResolver is obsolete +dotnet_diagnostic.SYSLIB0048.severity = error # RSA.EncryptValue and RSA.DecryptValue are not supported and throw NotSupportedException +dotnet_diagnostic.SYSLIB0049.severity = error # JsonSerializerOptions.AddContext is obsolete +dotnet_diagnostic.SYSLIB0050.severity = error # Formatter-based serialization (ISerializable, FormatterServices, etc.) is obsolete +dotnet_diagnostic.SYSLIB0051.severity = error # The Exception API ISerializable / GetObjectData formatter-based serialization is obsolete +dotnet_diagnostic.SYSLIB0052.severity = error # Members for assigning to AssemblyName.CodeBase or AssemblyName.EscapedCodeBase are obsolete +dotnet_diagnostic.SYSLIB0053.severity = error # AesGcm should indicate the required tag size for encryption and decryption +dotnet_diagnostic.SYSLIB0054.severity = error # Strong name signing is not supported and throws PlatformNotSupportedException +dotnet_diagnostic.SYSLIB0055.severity = error # 'Memory.Pin' may be incorrectly used with structures +dotnet_diagnostic.SYSLIB0056.severity = error # LoadFromHash and related members are obsolete +dotnet_diagnostic.SYSLIB0057.severity = error # Use X509CertificateLoader instead to load certificates +dotnet_diagnostic.SYSLIB0058.severity = error # KeyExchangeAlgorithm and KeyExchangeStrength are obsolete on SslStream +dotnet_diagnostic.SYSLIB0059.severity = error # SystemEvents.EventsThreadShutdown event is obsolete +dotnet_diagnostic.SYSLIB0060.severity = error # Constructors of DirectoryServices.ActiveDirectory.ConfigurationContext are obsolete +dotnet_diagnostic.SYSLIB0061.severity = error # CryptographyConfig.AddOID and AddAlgorithm methods are obsolete + +################### +# Microsoft .NET Source Generator Diagnostics (SYSLIB1xxx) +################### +# LoggerMessage source generator +dotnet_diagnostic.SYSLIB1001.severity = error # Logging method names cannot start with _ +dotnet_diagnostic.SYSLIB1002.severity = error # Don't include log level parameters as templates in the logging message +dotnet_diagnostic.SYSLIB1003.severity = error # LoggerMessage attribute log level value must be defined +dotnet_diagnostic.SYSLIB1004.severity = error # Logging class cannot be in nested types +dotnet_diagnostic.SYSLIB1005.severity = error # Could not find a required type definition +dotnet_diagnostic.SYSLIB1006.severity = error # Multiple logging methods cannot use the same event ID within a class +dotnet_diagnostic.SYSLIB1007.severity = error # Logging methods must return void +dotnet_diagnostic.SYSLIB1008.severity = error # One of the arguments to a logging method must implement ILogger +dotnet_diagnostic.SYSLIB1009.severity = error # Logging methods must be static +dotnet_diagnostic.SYSLIB1010.severity = error # Logging methods must be partial +dotnet_diagnostic.SYSLIB1011.severity = error # Logging methods cannot be generic +dotnet_diagnostic.SYSLIB1012.severity = error # Logging methods must not have a body +dotnet_diagnostic.SYSLIB1013.severity = error # Missing logger argument to logging method +dotnet_diagnostic.SYSLIB1015.severity = error # Argument is not referenced from the logging message +dotnet_diagnostic.SYSLIB1016.severity = error # Logging methods cannot have a parameter named the same as one of the template parameters +dotnet_diagnostic.SYSLIB1017.severity = error # Logging methods cannot have more than one parameter of type LogLevel +dotnet_diagnostic.SYSLIB1018.severity = error # Logging method parameter doesn't appear in template +dotnet_diagnostic.SYSLIB1019.severity = error # Couldn't find a field of type ILogger +dotnet_diagnostic.SYSLIB1020.severity = error # Found multiple fields of type ILogger +dotnet_diagnostic.SYSLIB1021.severity = error # Cannot have multiple logging methods with the same name +dotnet_diagnostic.SYSLIB1022.severity = error # Logging methods cannot have multiple template parameters with the same name +dotnet_diagnostic.SYSLIB1023.severity = error # Generating more than 6 arguments is not supported +dotnet_diagnostic.SYSLIB1024.severity = error # Logging method template name conflicts with a method parameter name +dotnet_diagnostic.SYSLIB1025.severity = error # Multiple template parameters with the same name +# Skipped SYSLIB1014 (Reserved) + +# System.Text.Json source generator +dotnet_diagnostic.SYSLIB1030.severity = error # JsonSourceGenerator did not generate serialization metadata for type +dotnet_diagnostic.SYSLIB1031.severity = error # Duplicate type registration in JsonSerializerContext +dotnet_diagnostic.SYSLIB1032.severity = error # JsonSerializerContext classes must be partial +dotnet_diagnostic.SYSLIB1033.severity = error # Serializable types must have at least one constructor +dotnet_diagnostic.SYSLIB1034.severity = error # Type uses an unsupported [JsonExtensionData] property +dotnet_diagnostic.SYSLIB1036.severity = error # Init-only properties are not supported in source generation +dotnet_diagnostic.SYSLIB1037.severity = error # Source generator does not support [JsonInclude] on inaccessible members +dotnet_diagnostic.SYSLIB1038.severity = error # JsonInclude on a non-public property is not supported +dotnet_diagnostic.SYSLIB1039.severity = error # Type with extension data of unsupported shape +dotnet_diagnostic.SYSLIB1040.severity = error # Type with cycles is not supported +dotnet_diagnostic.SYSLIB1041.severity = error # Duplicate type names registered with JsonSerializerContext +# Skipped SYSLIB1035 (Reserved) + +# GeneratedRegex source generator +dotnet_diagnostic.SYSLIB1042.severity = error # Use 'GeneratedRegexAttribute' for static regex creation +dotnet_diagnostic.SYSLIB1043.severity = error # The regex pattern is not valid +dotnet_diagnostic.SYSLIB1044.severity = error # The regex source generator failed +dotnet_diagnostic.SYSLIB1045.severity = error # Convert to 'GeneratedRegexAttribute' for compile-time regex generation +dotnet_diagnostic.SYSLIB1046.severity = error # Could not find an applicable Regex constructor +dotnet_diagnostic.SYSLIB1047.severity = error # The regex options are not valid for source generation +dotnet_diagnostic.SYSLIB1048.severity = error # GeneratedRegex source generator encountered an unhandled error +dotnet_diagnostic.SYSLIB1049.severity = error # Multiple regex source generators conflict + +# LibraryImport (P/Invoke) source generator +dotnet_diagnostic.SYSLIB1050.severity = error # Method should be marked 'LibraryImport' instead of 'DllImport' +dotnet_diagnostic.SYSLIB1051.severity = error # Specified type is not supported by source-generated P/Invokes +dotnet_diagnostic.SYSLIB1052.severity = error # Specified configuration is not supported by source-generated P/Invokes +dotnet_diagnostic.SYSLIB1053.severity = error # 'LibraryImportAttribute' requires unsafe code +dotnet_diagnostic.SYSLIB1054.severity = error # Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time +dotnet_diagnostic.SYSLIB1055.severity = error # Invalid 'CustomMarshallerAttribute' usage +dotnet_diagnostic.SYSLIB1056.severity = error # Specified marshaller type is invalid +dotnet_diagnostic.SYSLIB1057.severity = error # Marshaller type does not have the required shape +dotnet_diagnostic.SYSLIB1058.severity = error # Invalid 'NativeMarshallingAttribute' usage +dotnet_diagnostic.SYSLIB1059.severity = error # Marshaller type must be a closed generic type +dotnet_diagnostic.SYSLIB1060.severity = error # Specified native type is invalid +dotnet_diagnostic.SYSLIB1061.severity = error # Marshaller type has incompatible method signatures +dotnet_diagnostic.SYSLIB1062.severity = error # Project must be updated with 'true' +dotnet_diagnostic.SYSLIB1063.severity = error # 'CustomMarshaller' types must have an 'in' or 'ref' first parameter +dotnet_diagnostic.SYSLIB1064.severity = error # Forwarder marshaller types should not have 'GenericPlaceholder' + +# GeneratedComInterface / COM source generator +dotnet_diagnostic.SYSLIB1090.severity = error # Convert to 'GeneratedComInterface' +dotnet_diagnostic.SYSLIB1091.severity = error # Type is an invalid 'GeneratedComInterface' base interface +dotnet_diagnostic.SYSLIB1092.severity = error # 'GeneratedComInterface' must specify 'Guid' +dotnet_diagnostic.SYSLIB1093.severity = error # Method on 'GeneratedComInterface' has unsupported return type +dotnet_diagnostic.SYSLIB1094.severity = error # Method on 'GeneratedComInterface' has unsupported parameter +dotnet_diagnostic.SYSLIB1095.severity = error # Type implements 'GeneratedComInterface' but is not partial +dotnet_diagnostic.SYSLIB1096.severity = error # 'GeneratedComClassAttribute' must be applied to a partial type +dotnet_diagnostic.SYSLIB1097.severity = error # 'GeneratedComClassAttribute' type must implement at least one 'GeneratedComInterface' +dotnet_diagnostic.SYSLIB1098.severity = error # 'GeneratedComInterface' interface inheritance not supported + +# Configuration binding source generator +dotnet_diagnostic.SYSLIB1100.severity = error # Configuration binding source generator: type is not supported +dotnet_diagnostic.SYSLIB1101.severity = error # Configuration binding source generator: property on type is not supported +dotnet_diagnostic.SYSLIB1102.severity = error # Configuration binding source generator: not enabled +dotnet_diagnostic.SYSLIB1103.severity = error # Configuration binding source generator: type lacks a parameterless constructor +dotnet_diagnostic.SYSLIB1104.severity = error # Configuration binding source generator: language version is too low + +################### +# Microsoft .NET Style Rules (IDExxxx) - Language Rules +################### +# EnforceCodeStyleInBuild=true (set in Directory.Build.props) promotes these +# IDE rules into `dotnet build` so they fire at compile time, not just in the +# IDE. We enable the ones that are unambiguous wins and skip the rules that +# conflict with an existing convention (SA1101, SA1200, SA1206, SA1400, SA1633, +# etc.) so we don't double-report. + +# Bug catchers — always error. +dotnet_diagnostic.IDE0035.severity = error # Remove unreachable code +dotnet_diagnostic.IDE0043.severity = error # Format string contains invalid placeholder +dotnet_diagnostic.IDE0051.severity = none # Remove unused private member — covered by SST1440 +dotnet_diagnostic.IDE0052.severity = none # Remove unread private member — covered by SST1441 +dotnet_diagnostic.IDE0076.severity = error # Remove invalid global SuppressMessage attribute +dotnet_diagnostic.IDE0077.severity = error # Avoid legacy format target in global SuppressMessage attribute +dotnet_diagnostic.IDE0080.severity = none # Remove unnecessary suppression operator — covered by SST2209 + +# Simplification / cleanup. +dotnet_diagnostic.IDE0001.severity = none # Simplify name — covered by SST1116 +dotnet_diagnostic.IDE0002.severity = none # Simplify member access — covered by SST1117 +dotnet_diagnostic.IDE0004.severity = none # Remove unnecessary cast — covered by SST1175 +dotnet_diagnostic.IDE0005.severity = none # Remove unnecessary import - DUPLICATE S1128 (slower: 2.364s vs 551ms) +dotnet_diagnostic.IDE0016.severity = none # Use throw expression — covered by SST2207 +dotnet_diagnostic.IDE0017.severity = none # Use object initializers — covered by SST1193 +dotnet_diagnostic.IDE0018.severity = none # Inline variable declaration — covered by SST2208 +dotnet_diagnostic.IDE0019.severity = none # Use pattern matching to avoid 'as' followed by a 'null' check — covered by SST2005 +dotnet_diagnostic.IDE0020.severity = none # Use pattern matching to avoid 'is' check followed by a cast — covered by SST2007 +dotnet_diagnostic.IDE0028.severity = none # Use collection initializers — covered by SST1194 +dotnet_diagnostic.IDE0029.severity = none # Use coalesce expression — covered by SST1195 +dotnet_diagnostic.IDE0030.severity = none # Use coalesce expression (nullable types) — covered by SST1195/SST2227 +dotnet_diagnostic.IDE0031.severity = none # Use null propagation — covered by SST1196 +dotnet_diagnostic.IDE0032.severity = none # Use auto property — covered by SST1420 +dotnet_diagnostic.IDE0033.severity = none # Use explicitly provided tuple name — covered by SST1142 +dotnet_diagnostic.IDE0034.severity = none # Simplify default expression — covered by SST1188 +dotnet_diagnostic.IDE0037.severity = none # Use inferred member name — covered by SST1173/SST2216 +dotnet_diagnostic.IDE0038.severity = none # Use pattern matching ('is' check without a cast) — covered by SST2007 +dotnet_diagnostic.IDE0041.severity = none # Use 'is null' check — covered by SST1149/SST2231 +dotnet_diagnostic.IDE0042.severity = none # Deconstruct variable declaration — covered by SST2214 +dotnet_diagnostic.IDE0044.severity = none # Add readonly modifier — covered by SST1424 +dotnet_diagnostic.IDE0046.severity = none # Use conditional expression for return — covered by SST1197 +dotnet_diagnostic.IDE0047.severity = error # Remove unnecessary parentheses +dotnet_diagnostic.IDE0049.severity = none # Use language keywords instead of framework type names for type references — covered by SST1121 +dotnet_diagnostic.IDE0054.severity = none # Use compound assignment — covered by SST1185 +dotnet_diagnostic.IDE0056.severity = none # Use index operator — covered by SST2203 +dotnet_diagnostic.IDE0057.severity = none # Use range operator — covered by SST2204 +dotnet_diagnostic.IDE0059.severity = none # Remove unnecessary value assignment — covered by SST2222 +dotnet_diagnostic.IDE0062.severity = error # Make local function static +dotnet_diagnostic.IDE0063.severity = error # Use simple 'using' statement +dotnet_diagnostic.IDE0064.severity = error # Make struct fields writable — flag readonly-but-mutable struct fields where intent diverges from declaration +dotnet_diagnostic.IDE0066.severity = none # Use switch expression — covered by SST2201 +dotnet_diagnostic.IDE0070.severity = none # Use 'System.HashCode.Combine' — covered by SST2217 +dotnet_diagnostic.IDE0071.severity = none # Simplify interpolation — covered by SST2220 +dotnet_diagnostic.IDE0072.severity = none # Add missing cases to switch expression — covered by SST2206 +dotnet_diagnostic.IDE0074.severity = none # Use compound assignment — covered by SST1185 +dotnet_diagnostic.IDE0075.severity = none # Simplify conditional expression — covered by SST1182 +dotnet_diagnostic.IDE0078.severity = none # Use pattern matching — covered by SST2006/SST2231 +dotnet_diagnostic.IDE0082.severity = none # Convert typeof to nameof — covered by SST1199 +dotnet_diagnostic.IDE0083.severity = none # Use pattern matching ('not' operator) — covered by SST2006 +dotnet_diagnostic.IDE0084.severity = error # Use pattern matching ('IsNot' operator) +dotnet_diagnostic.IDE0090.severity = none # Simplify 'new' expression — covered by SST2202 +dotnet_diagnostic.IDE0100.severity = none # Remove unnecessary equality operator — covered by SST1143 +dotnet_diagnostic.IDE0110.severity = none # Remove unnecessary discard — covered by SST2213 +dotnet_diagnostic.IDE0120.severity = none # Simplify LINQ expression — covered by SST2229/SST2233 +dotnet_diagnostic.IDE0150.severity = none # Prefer 'null' check over type check — covered by SST2231 +dotnet_diagnostic.IDE0161.severity = error # Use file-scoped namespace declaration +dotnet_diagnostic.IDE0170.severity = error # Simplify property pattern +dotnet_diagnostic.IDE0180.severity = none # Use tuple to swap values — covered by SST2215 +dotnet_diagnostic.IDE0200.severity = error # Remove unnecessary lambda expression +dotnet_diagnostic.IDE0220.severity = none # Add explicit cast in foreach loop — covered by SST2225 +dotnet_diagnostic.IDE0221.severity = none # Add explicit cast — covered by SST2226 +dotnet_diagnostic.IDE0240.severity = none # Remove redundant nullable directive — covered by SST2210 +dotnet_diagnostic.IDE0241.severity = none # Remove unnecessary nullable directive — covered by SST2211 +dotnet_diagnostic.IDE0250.severity = error # Make struct 'readonly' +dotnet_diagnostic.IDE0251.severity = error # Make member 'readonly' +dotnet_diagnostic.IDE0260.severity = none # Use pattern matching — covered by SST2006/SST2231 +dotnet_diagnostic.IDE0270.severity = none # Use coalesce expression — covered by SST2227 + +# Disabled — conflict with an existing SA/CA rule or project convention. +dotnet_diagnostic.IDE0003.severity = none # Remove 'this' qualifier — we don't follow the this. prefix style (SA1101 = none) +dotnet_diagnostic.IDE0007.severity = none # Use var — we don't force var in either direction +dotnet_diagnostic.IDE0008.severity = none # Use explicit type — we don't force var in either direction +dotnet_diagnostic.IDE0009.severity = none # Member access should be qualified — SA1101 = none +dotnet_diagnostic.IDE0010.severity = none # Add missing cases to switch statement — too noisy for default/fallthrough patterns +dotnet_diagnostic.IDE0011.severity = none # Add braces — RCS1001 / RCS1007 already handle this +dotnet_diagnostic.IDE0021.severity = none # Use expression body for constructors — preference not enforced +dotnet_diagnostic.IDE0022.severity = none # Use expression body for methods — preference not enforced +dotnet_diagnostic.IDE0023.severity = none # Use expression body for conversion operators — preference not enforced +dotnet_diagnostic.IDE0024.severity = none # Use expression body for operators — preference not enforced +dotnet_diagnostic.IDE0025.severity = none # Use expression body for properties — preference not enforced +dotnet_diagnostic.IDE0026.severity = none # Use expression body for indexers — preference not enforced +dotnet_diagnostic.IDE0027.severity = none # Use expression body for accessors — preference not enforced +dotnet_diagnostic.IDE0036.severity = none # Order modifiers — SA1206 handles +dotnet_diagnostic.IDE0039.severity = none # Use local function — covered by SST2228 +dotnet_diagnostic.IDE0040.severity = none # Add accessibility modifiers — SA1400 handles +dotnet_diagnostic.IDE0045.severity = none # Use conditional expression for assignment — covered by SST1198 +dotnet_diagnostic.IDE0048.severity = none # Add parentheses for clarity — preference not enforced +dotnet_diagnostic.IDE0053.severity = none # Use expression body for lambdas — preference not enforced +dotnet_diagnostic.IDE0055.severity = none # Formatting — StyleCop SA rules own formatting +dotnet_diagnostic.IDE0058.severity = none # Remove unnecessary expression value — covered by SST2221 +dotnet_diagnostic.IDE0060.severity = none # Remove unused parameter — CA1801 handles (with api_surface config) +dotnet_diagnostic.IDE0061.severity = none # Use expression body for local function — preference not enforced +dotnet_diagnostic.IDE0065.severity = none # 'using' directive placement — SA1200 = none (we put usings outside file-scoped namespaces) +dotnet_diagnostic.IDE0073.severity = none # Require file header — SA1633 handles +dotnet_diagnostic.IDE0079.severity = none # Remove unnecessary suppression — can misfire on multi-TFM suppressions +dotnet_diagnostic.IDE0130.severity = none # Namespace does not match folder structure — we don't enforce strict mirror +dotnet_diagnostic.IDE0160.severity = none # Use block-scoped namespace — we use file-scoped (IDE0161) +dotnet_diagnostic.IDE0210.severity = none # Convert to top-level statements — we use Main style +dotnet_diagnostic.IDE0211.severity = none # Convert to 'Program.Main' style — we use Main style +dotnet_diagnostic.IDE0280.severity = none # Use nameof — CA1507 / RCS1015 handle this +dotnet_diagnostic.IDE0290.severity = none # Use primary constructor — subjective, not enforced +dotnet_diagnostic.IDE0300.severity = none # Use collection expression for array — covered by SST2101 +dotnet_diagnostic.IDE0301.severity = none # Use collection expression for empty — covered by SST2100 +dotnet_diagnostic.IDE0302.severity = none # Use collection expression for stackalloc — covered by SST2102 +dotnet_diagnostic.IDE0303.severity = none # Use collection expression for Create — covered by SST2103 +dotnet_diagnostic.IDE0304.severity = none # Use collection expression for builder — covered by SST2104 +dotnet_diagnostic.IDE0305.severity = none # Use collection expression for fluent — covered by SST2105 +dotnet_diagnostic.IDE0306.severity = none # Use collection expression for new — covered by SST2101 +dotnet_diagnostic.IDE0320.severity = error # Make anonymous function static — eliminates the closure-capture display class allocation +dotnet_diagnostic.IDE0050.severity = none # Convert anonymous type to tuple — covered by SST2224 +dotnet_diagnostic.IDE0230.severity = none # Use UTF-8 string literal — covered by SST2212 +dotnet_diagnostic.IDE0310.severity = none # Convert lambda expression to method group — handled by RCS1207 +dotnet_diagnostic.IDE0340.severity = none # Use unbound generic type — covered by SST2232 +dotnet_diagnostic.IDE0350.severity = none # Use implicitly typed lambda — covered by SST2218 +dotnet_diagnostic.IDE0360.severity = none # Simplify property accessor — covered by SST2219 +dotnet_diagnostic.IDE0370.severity = none # Remove unnecessary suppression (null-forgiving operator) — disabled for the same reason as RCS1249: multi-TFM nullability annotations can differ per platform +dotnet_diagnostic.IDE0380.severity = error # Remove unnecessary unsafe modifier +dotnet_diagnostic.IDE1005.severity = error # Use conditional delegate call + +################### +# Microsoft .NET Style Rules (IDE1xxx / IDE3xxx) - Naming & Miscellaneous +################### +# Naming conventions — SA1300 family already enforces PascalCase / interface +# prefix / field casing (with our _underscore convention on SA1306/1309/1311 +# deliberately disabled). Leaving IDE1006 off because configuring +# `dotnet_naming_rule.*` would duplicate what SA already enforces and could +# conflict with the _underscore convention. +dotnet_diagnostic.IDE1006.severity = none # Naming rule violation — SA1300 family handles naming +dotnet_diagnostic.IDE3000.severity = none # Disabled per project convention — not enforced + +################### +# Roslynator Analyzers (RCS1xxx) - Code Simplification ################### dotnet_diagnostic.RCS1001.severity = error # Add braces (when expression spans over multiple lines) +dotnet_diagnostic.RCS1003.severity = error # Add braces to if-else (when expression spans over multiple lines) dotnet_diagnostic.RCS1005.severity = error # Simplify nested using statement dotnet_diagnostic.RCS1006.severity = error # Merge 'else' with nested 'if' -dotnet_diagnostic.RCS1020.severity = error # Simplify Nullable to T? +dotnet_diagnostic.RCS1007.severity = error # Add braces +dotnet_diagnostic.RCS1031.severity = none # Remove unnecessary braces in switch section -- we don't mind braces in switch statements +dotnet_diagnostic.RCS1032.severity = error # Remove redundant parentheses +dotnet_diagnostic.RCS1033.severity = error # Remove redundant boolean literal +dotnet_diagnostic.RCS1039.severity = error # Remove argument list from attribute +dotnet_diagnostic.RCS1042.severity = none # Remove enum default underlying type — covered by SST1177 +dotnet_diagnostic.RCS1043.severity = error # Remove 'partial' modifier from type with a single part dotnet_diagnostic.RCS1049.severity = error # Simplify boolean comparison -dotnet_diagnostic.RCS1068.severity = error # Simplify logical negation +dotnet_diagnostic.RCS1058.severity = none # Use compound assignment — covered by SST1185 +dotnet_diagnostic.RCS1061.severity = error # Merge 'if' with nested 'if' +dotnet_diagnostic.RCS1068.severity = none # Simplify logical negation — covered by SST1172/SST2006 dotnet_diagnostic.RCS1069.severity = error # Remove unnecessary case label -dotnet_diagnostic.RCS1071.severity = error # Remove redundant base constructor call +dotnet_diagnostic.RCS1070.severity = none # Remove redundant default switch section — covered by SST1179 +dotnet_diagnostic.RCS1071.severity = none # Remove redundant base constructor call — covered by SST1178 dotnet_diagnostic.RCS1073.severity = error # Convert 'if' to 'return' statement -dotnet_diagnostic.RCS1074.severity = error # Remove redundant constructor +dotnet_diagnostic.RCS1074.severity = none # Remove redundant constructor — covered by SST1433 +dotnet_diagnostic.RCS1078.severity = error # Use "" or 'string.Empty' dotnet_diagnostic.RCS1084.severity = error # Use coalesce expression instead of conditional expression +dotnet_diagnostic.RCS1085.severity = error # Use auto-implemented property +dotnet_diagnostic.RCS1089.severity = error # Use --/++ operator instead of assignment +dotnet_diagnostic.RCS1097.severity = error # Remove redundant 'ToString' call +dotnet_diagnostic.RCS1103.severity = error # Convert 'if' to assignment +dotnet_diagnostic.RCS1104.severity = none # Simplify conditional expression — covered by SST1182 +dotnet_diagnostic.RCS1105.severity = error # Unnecessary interpolation +dotnet_diagnostic.RCS1107.severity = error # Remove redundant 'ToCharArray' call +dotnet_diagnostic.RCS1114.severity = error # Remove redundant delegate creation +dotnet_diagnostic.RCS1124.severity = error # Inline local variable +dotnet_diagnostic.RCS1126.severity = error # Add braces to if-else dotnet_diagnostic.RCS1128.severity = error # Use coalesce expression +dotnet_diagnostic.RCS1129.severity = none # Remove redundant field initialization — covered by SST1176 +dotnet_diagnostic.RCS1132.severity = none # Remove redundant overriding member — covered by SST1181 +dotnet_diagnostic.RCS1133.severity = error # Remove redundant Dispose/Close call +dotnet_diagnostic.RCS1134.severity = error # Remove redundant statement dotnet_diagnostic.RCS1143.severity = error # Simplify coalesce expression +dotnet_diagnostic.RCS1145.severity = error # Remove redundant 'as' operator +dotnet_diagnostic.RCS1146.severity = error # Use conditional access +dotnet_diagnostic.RCS1151.severity = none # Remove redundant cast — covered by SST1175 dotnet_diagnostic.RCS1171.severity = error # Simplify lazy initialization dotnet_diagnostic.RCS1173.severity = error # Use coalesce expression instead of 'if' +dotnet_diagnostic.RCS1174.severity = error # Remove redundant async/await +dotnet_diagnostic.RCS1179.severity = error # Unnecessary assignment +dotnet_diagnostic.RCS1180.severity = error # Inline lazy initialization +dotnet_diagnostic.RCS1188.severity = none # Remove redundant auto-property initialization — covered by SST1176 +dotnet_diagnostic.RCS1192.severity = none # Unnecessary usage of verbatim string literal — covered by SST1184 +dotnet_diagnostic.RCS1199.severity = error # Unnecessary null check +dotnet_diagnostic.RCS1206.severity = error # Use conditional access instead of conditional expression +dotnet_diagnostic.RCS1207.severity = error # Use anonymous function or method group +dotnet_diagnostic.RCS1211.severity = error # Remove unnecessary 'else' +dotnet_diagnostic.RCS1212.severity = error # Remove redundant assignment +dotnet_diagnostic.RCS1214.severity = none # Unnecessary interpolated string — covered by SST1183 +dotnet_diagnostic.RCS1216.severity = error # Unnecessary unsafe context +dotnet_diagnostic.RCS1217.severity = error # Convert interpolated string to concatenation +dotnet_diagnostic.RCS1218.severity = error # Simplify code branching +dotnet_diagnostic.RCS1220.severity = error # Use pattern matching instead of combination of 'is' and cast +dotnet_diagnostic.RCS1221.severity = error # Use pattern matching instead of combination of 'as' and null check +dotnet_diagnostic.RCS1238.severity = error # Avoid nested ?: operators +dotnet_diagnostic.RCS1244.severity = error # Simplify 'default' expression +dotnet_diagnostic.RCS1249.severity = none # Unnecessary null-forgiving operator — disabled because multi-TFM nullability annotations can differ per platform, leading to false positives +dotnet_diagnostic.RCS1251.severity = error # Remove unnecessary braces from record declaration dotnet_diagnostic.RCS1259.severity = error # Remove empty syntax (replaces RCS1066) -dotnet_diagnostic.RCS1264.severity = error # Use 'var' or explicit type (replaces RCS1010, RCS1176, RCS1177) +dotnet_diagnostic.RCS1262.severity = error # Unnecessary raw string literal +dotnet_diagnostic.RCS1265.severity = error # Remove redundant catch block +dotnet_diagnostic.RCS1268.severity = error # Simplify numeric comparison ################### -# Roslynator Analyzers (RCS) - Code Quality & Best Practices +# Roslynator Analyzers (RCS1xxx) - Code Quality ################### -dotnet_diagnostic.RCS1018.severity = error # Add/remove accessibility modifiers -dotnet_diagnostic.RCS1037.severity = error # Remove trailing white-space -dotnet_diagnostic.RCS1055.severity = error # Unnecessary semicolon at the end of declaration -dotnet_diagnostic.RCS1078.severity = error # Use "" or 'string.Empty' -dotnet_diagnostic.RCS1085.severity = error # Use auto-implemented property -dotnet_diagnostic.RCS1090.severity = error # Add/remove 'ConfigureAwait(false)' call -dotnet_diagnostic.RCS1102.severity = error # Make class static -dotnet_diagnostic.RCS1105.severity = error # Unnecessary interpolation -dotnet_diagnostic.RCS1138.severity = error # Add summary to documentation comment -dotnet_diagnostic.RCS1139.severity = error # Add summary element to documentation comment -dotnet_diagnostic.RCS1158.severity = none # Static member in generic type should use a type parameter -dotnet_diagnostic.RCS1163.severity = none # Unused parameter +dotnet_diagnostic.RCS1013.severity = error # Use predefined type +dotnet_diagnostic.RCS1014.severity = error # Use explicitly/implicitly typed array +dotnet_diagnostic.RCS1015.severity = error # Use nameof operator +dotnet_diagnostic.RCS1016.severity = error # Use block body or expression body +dotnet_diagnostic.RCS1020.severity = error # Simplify Nullable to T? +dotnet_diagnostic.RCS1021.severity = error # Convert lambda expression body to expression body +dotnet_diagnostic.RCS1044.severity = none # Remove original exception from throw statement — covered by SST1430 +dotnet_diagnostic.RCS1046.severity = none # Asynchronous method name should end with 'Async' — TUnit test method naming convention doesn't follow the Async suffix — covered by SST1317 +dotnet_diagnostic.RCS1047.severity = error # Non-asynchronous method name should not end with 'Async' +dotnet_diagnostic.RCS1048.severity = error # Use lambda expression instead of anonymous method +dotnet_diagnostic.RCS1050.severity = error # Include/omit parentheses when creating new object +dotnet_diagnostic.RCS1051.severity = error # Add/remove parentheses from condition in conditional operator +dotnet_diagnostic.RCS1056.severity = none # Avoid usage of using alias directive - used to avoid conflicts +dotnet_diagnostic.RCS1059.severity = error # Avoid locking on publicly accessible instance +dotnet_diagnostic.RCS1075.severity = none # Avoid empty catch clause that catches System.Exception — covered by SST1429 +dotnet_diagnostic.RCS1079.severity = error # Throwing of new NotImplementedException +dotnet_diagnostic.RCS1081.severity = error # Split variable declaration +dotnet_diagnostic.RCS1093.severity = error # File contains no code +dotnet_diagnostic.RCS1094.severity = error # Declare using directive on top level +dotnet_diagnostic.RCS1096.severity = error # Use 'HasFlag' method or bitwise operator +dotnet_diagnostic.RCS1098.severity = none # Constant values should be placed on right side of comparisons — covered by SST1186 +dotnet_diagnostic.RCS1099.severity = error # Default label should be the last label in a switch section +dotnet_diagnostic.RCS1102.severity = none # Make class static — covered by SST1432 +dotnet_diagnostic.RCS1108.severity = error # Add 'static' modifier to all partial class declarations +dotnet_diagnostic.RCS1111.severity = error # Add braces to switch section with multiple statements +dotnet_diagnostic.RCS1113.severity = error # Use 'string.IsNullOrEmpty' method +dotnet_diagnostic.RCS1118.severity = error # Mark local variable as const +dotnet_diagnostic.RCS1123.severity = error # Add parentheses when necessary +dotnet_diagnostic.RCS1130.severity = error # Bitwise operation on enum without Flags attribute +dotnet_diagnostic.RCS1135.severity = error # Declare enum member with zero value (when enum has FlagsAttribute) +dotnet_diagnostic.RCS1136.severity = error # Merge switch sections with equivalent content +dotnet_diagnostic.RCS1154.severity = error # Sort enum members +dotnet_diagnostic.RCS1155.severity = error # Use StringComparison when comparing strings +dotnet_diagnostic.RCS1156.severity = error # Use string.Length instead of comparison with empty string +dotnet_diagnostic.RCS1157.severity = error # Composite enum value contains undefined flag +dotnet_diagnostic.RCS1159.severity = error # Use EventHandler +dotnet_diagnostic.RCS1160.severity = none # Abstract type should not have public constructors — covered by SST1428 +dotnet_diagnostic.RCS1161.severity = none # Enum should declare explicit values - do not need explicit values +dotnet_diagnostic.RCS1162.severity = none # Avoid chain of assignments — covered by SST1187 dotnet_diagnostic.RCS1166.severity = error # Value type object is never equal to null -dotnet_diagnostic.RCS1168.severity = suggestion # Parameter name differs from base name -dotnet_diagnostic.RCS1179.severity = error # Unnecessary assignment -dotnet_diagnostic.RCS1180.severity = error # Inline lazy initialization -dotnet_diagnostic.RCS1188.severity = error # Remove redundant auto-property initialization +dotnet_diagnostic.RCS1168.severity = none # Parameter name differs from base name — covered by SST1318 +dotnet_diagnostic.RCS1169.severity = error # Make field read-only +dotnet_diagnostic.RCS1170.severity = error # Use read-only auto-implemented property +dotnet_diagnostic.RCS1172.severity = none # Use 'is' operator instead of 'as' operator — covered by SST2005 +dotnet_diagnostic.RCS1187.severity = error # Use constant instead of field +dotnet_diagnostic.RCS1191.severity = error # Declare enum value as combination of names +dotnet_diagnostic.RCS1193.severity = error # Overriding member should not change 'params' modifier +dotnet_diagnostic.RCS1196.severity = error # Call extension method as instance method +dotnet_diagnostic.RCS1200.severity = error # Call 'Enumerable.ThenBy' instead of 'Enumerable.OrderBy' dotnet_diagnostic.RCS1201.severity = error # Use method chaining -dotnet_diagnostic.RCS1207.severity = error # Use anonymous function or method group -dotnet_diagnostic.RCS1211.severity = error # Remove unnecessary 'else' +dotnet_diagnostic.RCS1202.severity = error # Avoid NullReferenceException +dotnet_diagnostic.RCS1204.severity = error # Use EventArgs.Empty +dotnet_diagnostic.RCS1205.severity = error # Order named arguments according to the order of parameters +dotnet_diagnostic.RCS1208.severity = error # Reduce 'if' nesting +dotnet_diagnostic.RCS1209.severity = error # Order type parameter constraints +dotnet_diagnostic.RCS1210.severity = error # Return completed task instead of returning null +dotnet_diagnostic.RCS1215.severity = error # Expression is always equal to true/false +dotnet_diagnostic.RCS1222.severity = error # Merge preprocessor directives +dotnet_diagnostic.RCS1223.severity = suggestion # Mark publicly visible type with DebuggerDisplay attribute — only data types benefit; the rule is too broad to be an error +dotnet_diagnostic.RCS1224.severity = error # Make method an extension method +dotnet_diagnostic.RCS1225.severity = error # Make class sealed +dotnet_diagnostic.RCS1227.severity = error # Validate arguments correctly +dotnet_diagnostic.RCS1229.severity = error # Use async/await when necessary dotnet_diagnostic.RCS1231.severity = suggestion # Make parameter ref read-only +dotnet_diagnostic.RCS1233.severity = error # Use short-circuiting operator +dotnet_diagnostic.RCS1234.severity = error # Duplicate enum value +dotnet_diagnostic.RCS1239.severity = error # Use 'for' statement instead of 'while' statement +dotnet_diagnostic.RCS1240.severity = error # Operator is unnecessary dotnet_diagnostic.RCS1242.severity = error # Do not pass non-read-only struct by read-only reference +dotnet_diagnostic.RCS1243.severity = error # Duplicate word in a comment +dotnet_diagnostic.RCS1247.severity = error # Fix documentation comment tag dotnet_diagnostic.RCS1248.severity = error # Normalize null check -dotnet_diagnostic.RCS1256.severity = none # Invalid argument null check +dotnet_diagnostic.RCS1250.severity = error # Use implicit/explicit object creation +dotnet_diagnostic.RCS1252.severity = error # Normalize usage of infinite loop +dotnet_diagnostic.RCS1254.severity = error # Normalize format of enum flag value +dotnet_diagnostic.RCS1255.severity = none # Simplify argument null check — conflicts with our ArgumentExceptionHelper helper pattern +dotnet_diagnostic.RCS1257.severity = error # Use enum field explicitly +dotnet_diagnostic.RCS1258.severity = error # Unnecessary enum flag +dotnet_diagnostic.RCS1260.severity = error # Add/remove trailing comma +dotnet_diagnostic.RCS1261.severity = error # Resource can be disposed asynchronously +dotnet_diagnostic.RCS1264.severity = error # Use 'var' or explicit type (replaces RCS1010, RCS1176, RCS1177) +dotnet_diagnostic.RCS1266.severity = error # Use raw string literal +dotnet_diagnostic.RCS1267.severity = error # Use string interpolation instead of 'string.Concat' ################### -# Roslynator Analyzers (RCS) - Performance & Optimization +# Roslynator Analyzers (RCS1xxx) - Performance ################### -dotnet_diagnostic.RCS1058.severity = error # Use compound assignment dotnet_diagnostic.RCS1077.severity = error # Optimize LINQ method call dotnet_diagnostic.RCS1080.severity = error # Use 'Count/Length' property instead of 'Any' method dotnet_diagnostic.RCS1112.severity = error # Combine 'Enumerable.Where' method chain +dotnet_diagnostic.RCS1186.severity = error # Use Regex instance instead of static method dotnet_diagnostic.RCS1190.severity = error # Join string expressions dotnet_diagnostic.RCS1195.severity = error # Use ^ operator dotnet_diagnostic.RCS1197.severity = error # Optimize StringBuilder.Append/AppendLine call -dotnet_diagnostic.RCS1198.severity = none # Avoid unnecessary boxing of value type -dotnet_diagnostic.RCS1214.severity = error # Unnecessary interpolated string +dotnet_diagnostic.RCS1198.severity = none # Avoid unnecessary boxing of value type — boxing is unavoidable bridging Rx and IEnumerable +dotnet_diagnostic.RCS1230.severity = error # Unnecessary explicit use of enumerator dotnet_diagnostic.RCS1235.severity = error # Optimize method call - -################### -# StyleCop Analyzers (SA) - Spacing Rules -################### -dotnet_diagnostic.SA1000.severity = error # Keywords must be spaced correctly -dotnet_diagnostic.SA1001.severity = error # Commas must be spaced correctly -dotnet_diagnostic.SA1002.severity = error # Semicolons must be spaced correctly -dotnet_diagnostic.SA1003.severity = error # Symbols must be spaced correctly -dotnet_diagnostic.SA1004.severity = error # Documentation lines must begin with single space -dotnet_diagnostic.SA1005.severity = error # Single line comments must begin with single space -dotnet_diagnostic.SA1006.severity = error # Preprocessor keywords must not be preceded by space -dotnet_diagnostic.SA1007.severity = error # Operator keyword must be followed by space -dotnet_diagnostic.SA1008.severity = error # Opening parenthesis must be spaced correctly -dotnet_diagnostic.SA1009.severity = error # Closing parenthesis must be spaced correctly -dotnet_diagnostic.SA1010.severity = none # Opening square brackets must be spaced correctly -dotnet_diagnostic.SA1011.severity = error # Closing square brackets must be spaced correctly -dotnet_diagnostic.SA1012.severity = error # Opening braces must be spaced correctly -dotnet_diagnostic.SA1013.severity = error # Closing braces must be spaced correctly -dotnet_diagnostic.SA1014.severity = error # Opening generic brackets must be spaced correctly -dotnet_diagnostic.SA1015.severity = error # Closing generic brackets must be spaced correctly -dotnet_diagnostic.SA1016.severity = error # Opening attribute brackets must be spaced correctly -dotnet_diagnostic.SA1017.severity = error # Closing attribute brackets must be spaced correctly -dotnet_diagnostic.SA1018.severity = error # Nullable type symbols must not be preceded by space -dotnet_diagnostic.SA1019.severity = error # Member access symbols must be spaced correctly -dotnet_diagnostic.SA1020.severity = error # Increment decrement symbols must be spaced correctly -dotnet_diagnostic.SA1021.severity = error # Negative signs must be spaced correctly -dotnet_diagnostic.SA1022.severity = error # Positive signs must be spaced correctly -dotnet_diagnostic.SA1023.severity = error # Dereference and access of symbols must be spaced correctly -dotnet_diagnostic.SA1024.severity = error # Colons must be spaced correctly -dotnet_diagnostic.SA1025.severity = error # Code must not contain multiple whitespace in a row -dotnet_diagnostic.SA1026.severity = error # Code must not contain space after new keyword in implicitly typed array allocation -dotnet_diagnostic.SA1027.severity = error # Use tabs correctly -dotnet_diagnostic.SA1028.severity = error # Code must not contain trailing whitespace - -################### -# StyleCop Analyzers (SA) - Readability Rules -################### -dotnet_diagnostic.SA1100.severity = error # Do not prefix calls with base unless local implementation exists -dotnet_diagnostic.SA1101.severity = none # Prefix local calls with this -dotnet_diagnostic.SA1102.severity = error # Query clause must follow previous clause -dotnet_diagnostic.SA1103.severity = error # Query clauses must be on same line or separate lines -dotnet_diagnostic.SA1104.severity = error # Query clause must begin on new line when previous clause spans multiple lines -dotnet_diagnostic.SA1105.severity = error # Query clauses spanning multiple lines must begin on own line -dotnet_diagnostic.SA1106.severity = error # Code must not contain empty statements -dotnet_diagnostic.SA1107.severity = error # Code must not contain multiple statements on one line -dotnet_diagnostic.SA1108.severity = error # Block statements must not contain embedded comments -dotnet_diagnostic.SA1110.severity = error # Opening parenthesis or bracket must be on declaration line -dotnet_diagnostic.SA1111.severity = error # Closing parenthesis must be on line of last parameter -dotnet_diagnostic.SA1112.severity = error # Closing parenthesis must be on line of opening parenthesis -dotnet_diagnostic.SA1113.severity = error # Comma must be on same line as previous parameter -dotnet_diagnostic.SA1114.severity = error # Parameter list must follow declaration -dotnet_diagnostic.SA1115.severity = error # Parameter must follow comma -dotnet_diagnostic.SA1116.severity = error # Split parameters must start on line after declaration -dotnet_diagnostic.SA1117.severity = error # Parameters must be on same line or separate lines -dotnet_diagnostic.SA1118.severity = error # Parameter must not span multiple lines -dotnet_diagnostic.SA1120.severity = error # Comments must contain text -dotnet_diagnostic.SA1121.severity = error # Use built-in type alias -dotnet_diagnostic.SA1122.severity = error # Use string.Empty for empty strings -dotnet_diagnostic.SA1123.severity = error # Do not place regions within elements -dotnet_diagnostic.SA1124.severity = error # Do not use regions -dotnet_diagnostic.SA1125.severity = error # Use shorthand for nullable types -dotnet_diagnostic.SA1127.severity = error # Generic type constraints must be on own line -dotnet_diagnostic.SA1128.severity = error # Constructor initializer must be on own line -dotnet_diagnostic.SA1129.severity = error # Do not use default value type constructor -dotnet_diagnostic.SA1130.severity = error # Use lambda syntax -dotnet_diagnostic.SA1131.severity = error # Use readable conditions -dotnet_diagnostic.SA1132.severity = error # Do not combine fields -dotnet_diagnostic.SA1133.severity = error # Do not combine attributes -dotnet_diagnostic.SA1134.severity = error # Attributes must not share line -dotnet_diagnostic.SA1135.severity = error # Using directives must be qualified -dotnet_diagnostic.SA1136.severity = error # Enum values should be on separate lines -dotnet_diagnostic.SA1137.severity = error # Elements should have the same indentation -dotnet_diagnostic.SA1139.severity = error # Use literal suffix notation instead of casting - -################### -# StyleCop Analyzers (SA) - Ordering Rules -################### -dotnet_diagnostic.SA1200.severity = none # Using directives must be placed correctly -dotnet_diagnostic.SA1201.severity = error # Elements must appear in the correct order -dotnet_diagnostic.SA1202.severity = error # Elements must be ordered by access -dotnet_diagnostic.SA1203.severity = error # Constants must appear before fields -dotnet_diagnostic.SA1204.severity = error # Static elements must appear before instance elements -dotnet_diagnostic.SA1205.severity = error # Partial elements must declare access -dotnet_diagnostic.SA1206.severity = error # Declaration keywords must follow order -dotnet_diagnostic.SA1207.severity = error # Protected must come before internal -dotnet_diagnostic.SA1208.severity = error # System using directives must be placed before other using directives -dotnet_diagnostic.SA1209.severity = error # Using alias directives must be placed after other using directives -dotnet_diagnostic.SA1210.severity = error # Using directives must be ordered alphabetically by namespace -dotnet_diagnostic.SA1211.severity = error # Using alias directives must be ordered alphabetically by alias name -dotnet_diagnostic.SA1212.severity = error # Property accessors must follow order -dotnet_diagnostic.SA1213.severity = error # Event accessors must follow order -dotnet_diagnostic.SA1214.severity = error # Readonly elements must appear before non-readonly elements -dotnet_diagnostic.SA1216.severity = error # Using static directives must be placed at the correct location -dotnet_diagnostic.SA1217.severity = error # Using static directives must be ordered alphabetically - -################### -# StyleCop Analyzers (SA) - Naming Rules -################### -dotnet_diagnostic.SA1300.severity = error # Element must begin with upper-case letter -dotnet_diagnostic.SA1302.severity = error # Interface names must begin with I -dotnet_diagnostic.SA1303.severity = error # Const field names must begin with upper-case letter -dotnet_diagnostic.SA1304.severity = error # Non-private readonly fields must begin with upper-case letter -dotnet_diagnostic.SA1306.severity = none # Field names must begin with lower-case letter -dotnet_diagnostic.SA1307.severity = error # Accessible fields must begin with upper-case letter -dotnet_diagnostic.SA1308.severity = error # Variable names must not be prefixed -dotnet_diagnostic.SA1309.severity = none # Field names must not begin with underscore -dotnet_diagnostic.SA1310.severity = error # Field names must not contain underscore -dotnet_diagnostic.SA1311.severity = none # Static readonly fields must begin with upper-case letter -dotnet_diagnostic.SA1312.severity = error # Variable names must begin with lower-case letter -dotnet_diagnostic.SA1313.severity = error # Parameter names must begin with lower-case letter -dotnet_diagnostic.SA1314.severity = error # Type parameter names must begin with T -dotnet_diagnostic.SA1316.severity = none # Tuple element names should use correct casing - -################### -# StyleCop Analyzers (SA) - Maintainability Rules -################### -dotnet_diagnostic.SA1119.severity = error # Statement must not use unnecessary parenthesis -dotnet_diagnostic.SA1400.severity = error # Access modifier must be declared -dotnet_diagnostic.SA1401.severity = error # Fields must be private -dotnet_diagnostic.SA1402.severity = error # File may only contain a single type -dotnet_diagnostic.SA1403.severity = error # File may only contain a single namespace -dotnet_diagnostic.SA1404.severity = error # Code analysis suppression must have justification -dotnet_diagnostic.SA1405.severity = error # Debug.Assert must provide message text -dotnet_diagnostic.SA1406.severity = error # Debug.Fail must provide message text -dotnet_diagnostic.SA1407.severity = error # Arithmetic expressions must declare precedence -dotnet_diagnostic.SA1408.severity = error # Conditional expressions must declare precedence -dotnet_diagnostic.SA1410.severity = error # Remove delegate parenthesis when possible -dotnet_diagnostic.SA1411.severity = error # Attribute constructor must not use unnecessary parenthesis -dotnet_diagnostic.SA1413.severity = none # Use trailing commas in multi-line initializers - -################### -# StyleCop Analyzers (SA) - Layout Rules -################### -dotnet_diagnostic.SA1500.severity = error # Braces for multi-line statements must not share line -dotnet_diagnostic.SA1501.severity = error # Statement must not be on single line -dotnet_diagnostic.SA1502.severity = error # Element must not be on single line -dotnet_diagnostic.SA1503.severity = error # Braces must not be omitted -dotnet_diagnostic.SA1504.severity = error # All accessors must be single-line or multi-line -dotnet_diagnostic.SA1505.severity = none # Opening braces must not be followed by blank line -dotnet_diagnostic.SA1506.severity = error # Element documentation headers must not be followed by blank line -dotnet_diagnostic.SA1507.severity = error # Code must not contain multiple blank lines in a row -dotnet_diagnostic.SA1508.severity = error # Closing braces must not be preceded by blank line -dotnet_diagnostic.SA1509.severity = error # Opening braces must not be preceded by blank line -dotnet_diagnostic.SA1510.severity = error # Chained statement blocks must not be preceded by blank line -dotnet_diagnostic.SA1511.severity = error # While-do footer must not be preceded by blank line -dotnet_diagnostic.SA1512.severity = error # Single-line comments must not be followed by blank line -dotnet_diagnostic.SA1513.severity = error # Closing brace must be followed by blank line -dotnet_diagnostic.SA1514.severity = none # Element documentation header must be preceded by blank line -dotnet_diagnostic.SA1515.severity = error # Single-line comment must be preceded by blank line -dotnet_diagnostic.SA1516.severity = error # Elements must be separated by blank line -dotnet_diagnostic.SA1517.severity = error # Code must not contain blank lines at start of file -dotnet_diagnostic.SA1518.severity = error # Use line endings correctly at end of file -dotnet_diagnostic.SA1519.severity = error # Braces must not be omitted from multi-line child statement -dotnet_diagnostic.SA1520.severity = error # Use braces consistently - -################### -# StyleCop Analyzers (SA) - Documentation Rules -################### -dotnet_diagnostic.SA1600.severity = error # Elements must be documented -dotnet_diagnostic.SA1601.severity = error # Partial elements must be documented -dotnet_diagnostic.SA1602.severity = error # Enumeration items must be documented -dotnet_diagnostic.SA1604.severity = error # Element documentation must have summary -dotnet_diagnostic.SA1605.severity = error # Partial element documentation must have summary -dotnet_diagnostic.SA1606.severity = error # Element documentation must have summary text -dotnet_diagnostic.SA1607.severity = error # Partial element documentation must have summary text -dotnet_diagnostic.SA1608.severity = error # Element documentation must not have default summary -dotnet_diagnostic.SA1610.severity = error # Property documentation must have value text -dotnet_diagnostic.SA1611.severity = error # Element parameters must be documented -dotnet_diagnostic.SA1612.severity = error # Element parameter documentation must match element parameters -dotnet_diagnostic.SA1613.severity = error # Element parameter documentation must declare parameter name -dotnet_diagnostic.SA1614.severity = error # Element parameter documentation must have text -dotnet_diagnostic.SA1615.severity = error # Element return value must be documented -dotnet_diagnostic.SA1616.severity = error # Element return value documentation must have text -dotnet_diagnostic.SA1617.severity = error # Void return value must not be documented -dotnet_diagnostic.SA1618.severity = error # Generic type parameters must be documented -dotnet_diagnostic.SA1619.severity = error # Generic type parameters must be documented partial class -dotnet_diagnostic.SA1620.severity = error # Generic type parameter documentation must match type parameters -dotnet_diagnostic.SA1621.severity = error # Generic type parameter documentation must declare parameter name -dotnet_diagnostic.SA1622.severity = error # Generic type parameter documentation must have text -dotnet_diagnostic.SA1623.severity = error # Property summary documentation must match accessors -dotnet_diagnostic.SA1624.severity = error # Property summary documentation must omit set accessor with restricted access -dotnet_diagnostic.SA1625.severity = error # Element documentation must not be copied and pasted -dotnet_diagnostic.SA1626.severity = error # Single-line comments must not use documentation style slashes -dotnet_diagnostic.SA1627.severity = error # Documentation text must not be empty -dotnet_diagnostic.SA1629.severity = error # Documentation text must end with a period -dotnet_diagnostic.SA1633.severity = error # File must have header -dotnet_diagnostic.SA1634.severity = error # File header must show copyright -dotnet_diagnostic.SA1635.severity = error # File header must have copyright text -dotnet_diagnostic.SA1636.severity = error # File header copyright text must match -dotnet_diagnostic.SA1637.severity = none # File header must contain file name -dotnet_diagnostic.SA1638.severity = none # File header file name documentation must match file name -dotnet_diagnostic.SA1640.severity = error # File header must have valid company text -dotnet_diagnostic.SA1641.severity = error # File header company name text must match -dotnet_diagnostic.SA1642.severity = error # Constructor summary documentation must begin with standard text -dotnet_diagnostic.SA1643.severity = error # Destructor summary documentation must begin with standard text -dotnet_diagnostic.SA1649.severity = error # File name must match type name -dotnet_diagnostic.SA1651.severity = error # Do not use placeholder elements - -################### -# StyleCop Alternative Analyzers (SX) - Alternative Rules -################### -dotnet_diagnostic.SX1101.severity = error # Do not prefix local members with this -dotnet_diagnostic.SX1309.severity = error # Field names must begin with underscore -dotnet_diagnostic.SX1623.severity = none # Property summary documentation must match accessors (alternative) - -############################################# -# NUnit Analyzers -############################################# -[*.{cs,vb}] - -################### -# NUnit Analyzers - Structure Rules (NUnit1001 - NUnit1999) -################### -dotnet_diagnostic.NUnit1001.severity = error # TestCase args must match parameter types -dotnet_diagnostic.NUnit1002.severity = error # TestCaseSource should use nameof -dotnet_diagnostic.NUnit1003.severity = error # TestCase provided too few arguments -dotnet_diagnostic.NUnit1004.severity = error # TestCase provided too many arguments -dotnet_diagnostic.NUnit1005.severity = error # ExpectedResult type must match return type -dotnet_diagnostic.NUnit1006.severity = error # ExpectedResult must not be used on void methods -dotnet_diagnostic.NUnit1007.severity = error # Non-void method but no ExpectedResult provided -dotnet_diagnostic.NUnit1008.severity = error # ParallelScope.Self at assembly level has no effect -dotnet_diagnostic.NUnit1009.severity = error # ParallelScope.Children on non-parameterized test -dotnet_diagnostic.NUnit1010.severity = error # ParallelScope.Fixtures on a test method -dotnet_diagnostic.NUnit1011.severity = error # TestCaseSource member does not exist -dotnet_diagnostic.NUnit1012.severity = error # async test method must have non-void return type -dotnet_diagnostic.NUnit1013.severity = error # async method must use non-generic Task when no result -dotnet_diagnostic.NUnit1014.severity = error # async method must use Task when result expected -dotnet_diagnostic.NUnit1015.severity = error # Source type does not implement I(Async)Enumerable -dotnet_diagnostic.NUnit1016.severity = error # Source type lacks default constructor -dotnet_diagnostic.NUnit1017.severity = error # Specified source is not static -dotnet_diagnostic.NUnit1018.severity = error # TestCaseSource param count mismatch (target method) -dotnet_diagnostic.NUnit1019.severity = error # Source does not return I(Async)Enumerable -dotnet_diagnostic.NUnit1020.severity = error # Parameters provided to field/property source -dotnet_diagnostic.NUnit1021.severity = error # ValueSource should use nameof -dotnet_diagnostic.NUnit1022.severity = error # Specified ValueSource is not static -dotnet_diagnostic.NUnit1023.severity = error # ValueSource cannot supply required parameters -dotnet_diagnostic.NUnit1024.severity = error # ValueSource does not return I(Async)Enumerable -dotnet_diagnostic.NUnit1025.severity = error # ValueSource member does not exist -dotnet_diagnostic.NUnit1026.severity = error # Test or setup/teardown method is not public -dotnet_diagnostic.NUnit1027.severity = error # Test method has parameters but no arguments supplied -dotnet_diagnostic.NUnit1028.severity = error # Non-test method is public -dotnet_diagnostic.NUnit1029.severity = error # TestCaseSource param count mismatch (Test method) -dotnet_diagnostic.NUnit1030.severity = error # TestCaseSource parameter type mismatch (Test method) -dotnet_diagnostic.NUnit1031.severity = error # ValuesAttribute args must match parameter types -dotnet_diagnostic.NUnit1032.severity = error # IDisposable field/property should be disposed in TearDown -dotnet_diagnostic.NUnit1033.severity = error # TestContext.Write methods will be obsolete -dotnet_diagnostic.NUnit1034.severity = error # Base TestFixtures should be abstract -dotnet_diagnostic.NUnit1035.severity = error # Range 'step' parameter cannot be zero -dotnet_diagnostic.NUnit1036.severity = error # Range: from < to when step is positive -dotnet_diagnostic.NUnit1037.severity = error # Range: from > to when step is negative -dotnet_diagnostic.NUnit1038.severity = error # Attribute values' types must match parameter type - -################### -# NUnit Analyzers - Assertion Rules (NUnit2001 - NUnit2999) -################### -dotnet_diagnostic.NUnit2001.severity = error # Prefer Assert.That(..., Is.False) over ClassicAssert.False -dotnet_diagnostic.NUnit2002.severity = error # Prefer Assert.That(..., Is.False) over ClassicAssert.IsFalse -dotnet_diagnostic.NUnit2003.severity = error # Prefer Assert.That(..., Is.True) over ClassicAssert.IsTrue -dotnet_diagnostic.NUnit2004.severity = error # Prefer Assert.That(..., Is.True) over ClassicAssert.True -dotnet_diagnostic.NUnit2005.severity = error # Prefer Is.EqualTo over AreEqual -dotnet_diagnostic.NUnit2006.severity = error # Prefer Is.Not.EqualTo over AreNotEqual -dotnet_diagnostic.NUnit2007.severity = error # Actual value should not be a constant -dotnet_diagnostic.NUnit2008.severity = error # Incorrect IgnoreCase usage -dotnet_diagnostic.NUnit2009.severity = error # Same value used for actual and expected -dotnet_diagnostic.NUnit2010.severity = error # Use EqualConstraint for better messages -dotnet_diagnostic.NUnit2011.severity = error # Use ContainsConstraint for better messages -dotnet_diagnostic.NUnit2012.severity = error # Use StartsWithConstraint for better messages -dotnet_diagnostic.NUnit2013.severity = error # Use EndsWithConstraint for better messages -dotnet_diagnostic.NUnit2014.severity = error # Use SomeItemsConstraint for better messages -dotnet_diagnostic.NUnit2015.severity = error # Prefer Is.SameAs over AreSame -dotnet_diagnostic.NUnit2016.severity = error # Prefer Is.Null over ClassicAssert.Null -dotnet_diagnostic.NUnit2017.severity = error # Prefer Is.Null over ClassicAssert.IsNull -dotnet_diagnostic.NUnit2018.severity = error # Prefer Is.Not.Null over ClassicAssert.NotNull -dotnet_diagnostic.NUnit2019.severity = error # Prefer Is.Not.Null over ClassicAssert.IsNotNull -dotnet_diagnostic.NUnit2020.severity = error # Incompatible types for SameAs constraint -dotnet_diagnostic.NUnit2021.severity = error # Incompatible types for EqualTo constraint -dotnet_diagnostic.NUnit2022.severity = error # Missing property required for constraint -dotnet_diagnostic.NUnit2023.severity = error # Invalid NullConstraint usage -dotnet_diagnostic.NUnit2024.severity = error # Wrong actual type with String constraint -dotnet_diagnostic.NUnit2025.severity = error # Wrong actual type with ContainsConstraint -dotnet_diagnostic.NUnit2026.severity = error # Wrong actual type with SomeItems+EqualConstraint -dotnet_diagnostic.NUnit2027.severity = error # Prefer Is.GreaterThan over ClassicAssert.Greater -dotnet_diagnostic.NUnit2028.severity = error # Prefer Is.GreaterThanOrEqualTo over GreaterOrEqual -dotnet_diagnostic.NUnit2029.severity = error # Prefer Is.LessThan over ClassicAssert.Less -dotnet_diagnostic.NUnit2030.severity = error # Prefer Is.LessThanOrEqualTo over LessOrEqual -dotnet_diagnostic.NUnit2031.severity = error # Prefer Is.Not.SameAs over AreNotSame -dotnet_diagnostic.NUnit2032.severity = error # Prefer Is.Zero over ClassicAssert.Zero -dotnet_diagnostic.NUnit2033.severity = error # Prefer Is.Not.Zero over ClassicAssert.NotZero -dotnet_diagnostic.NUnit2034.severity = error # Prefer Is.NaN over ClassicAssert.IsNaN -dotnet_diagnostic.NUnit2035.severity = error # Prefer Is.Empty over ClassicAssert.IsEmpty -dotnet_diagnostic.NUnit2036.severity = error # Prefer Is.Not.Empty over ClassicAssert.IsNotEmpty -dotnet_diagnostic.NUnit2037.severity = error # Prefer Does.Contain over ClassicAssert.Contains -dotnet_diagnostic.NUnit2038.severity = error # Prefer Is.InstanceOf over ClassicAssert.IsInstanceOf -dotnet_diagnostic.NUnit2039.severity = error # Prefer Is.Not.InstanceOf over ClassicAssert.IsNotInstanceOf -dotnet_diagnostic.NUnit2040.severity = error # Non-reference types for SameAs constraint -dotnet_diagnostic.NUnit2041.severity = error # Incompatible types for comparison constraint -dotnet_diagnostic.NUnit2042.severity = error # Comparison constraint on object -dotnet_diagnostic.NUnit2043.severity = error # Use ComparisonConstraint for better messages -dotnet_diagnostic.NUnit2044.severity = error # Non-delegate actual parameter -dotnet_diagnostic.NUnit2045.severity = error # Use Assert.EnterMultipleScope or Assert.Multiple -dotnet_diagnostic.NUnit2046.severity = error # Use CollectionConstraint for better messages -dotnet_diagnostic.NUnit2047.severity = error # Incompatible types for Within constraint -dotnet_diagnostic.NUnit2048.severity = error # Prefer Assert.That over StringAssert -dotnet_diagnostic.NUnit2049.severity = error # Prefer Assert.That over CollectionAssert -dotnet_diagnostic.NUnit2050.severity = error # NUnit 4 no longer supports string.Format spec -dotnet_diagnostic.NUnit2051.severity = error # Prefer Is.Positive over ClassicAssert.Positive -dotnet_diagnostic.NUnit2052.severity = error # Prefer Is.Negative over ClassicAssert.Negative -dotnet_diagnostic.NUnit2053.severity = error # Prefer Is.AssignableFrom over ClassicAssert.IsAssignableFrom -dotnet_diagnostic.NUnit2054.severity = error # Prefer Is.Not.AssignableFrom over ClassicAssert.IsNotAssignableFrom -dotnet_diagnostic.NUnit2055.severity = error # Prefer Is.InstanceOf over 'is T' expression -dotnet_diagnostic.NUnit2056.severity = error # Prefer Assert.EnterMultipleScope statement over Multiple - -################### -# NUnit Analyzers - Suppressor Rules (NUnit3001 - NUnit3999) -################### -dotnet_diagnostic.NUnit3001.severity = error # Expression checked in NotNull/IsNotNull/Assert.That -dotnet_diagnostic.NUnit3002.severity = error # Field/Property initialized in SetUp/OneTimeSetUp -dotnet_diagnostic.NUnit3003.severity = error # TestFixture instantiated via reflection -dotnet_diagnostic.NUnit3004.severity = error # Field should be disposed in TearDown/OneTimeTearDown - -################### -# NUnit Analyzers - Style Rules (NUnit4001 - NUnit4999) -################### -dotnet_diagnostic.NUnit4001.severity = error # Simplify the Values attribute -dotnet_diagnostic.NUnit4002.severity = error # Use Specific constraint +dotnet_diagnostic.RCS1236.severity = error # Use exception filter +dotnet_diagnostic.RCS1246.severity = error # Use element access + +################### +# Roslynator Analyzers (RCS1xxx) - Maintainability +################### +dotnet_diagnostic.RCS1158.severity = none # Static member in generic type should use a type parameter — common factory pattern — covered by SST1431 +dotnet_diagnostic.RCS1163.severity = none # Unused parameter — interface implementations and Rx selectors often have unused parameters +dotnet_diagnostic.RCS1164.severity = none # Unused type parameter - DUPLICATE IDE0060 (UnusedParameter analyzer 210ms; IDE0060 bundled at lower cost) +dotnet_diagnostic.RCS1165.severity = none # Unconstrained type parameter checked for null - we validate all parameters for non-nullable enabled platforms +dotnet_diagnostic.RCS1182.severity = none # Remove redundant base interface — covered by SST1177 +dotnet_diagnostic.RCS1213.severity = none # Remove unused member declaration - DUPLICATE IDE0051 (slower: 230ms vs 75ms) +dotnet_diagnostic.RCS1241.severity = error # Implement non-generic counterpart +dotnet_diagnostic.RCS1256.severity = none # Invalid argument null check — conflicts with our ArgumentExceptionHelper helper pattern + +################### +# Roslynator Analyzers (RCS1xxx) - Documentation +################### +dotnet_diagnostic.RCS1181.severity = error # Convert comment to documentation comment +dotnet_diagnostic.RCS1189.severity = error # Add or remove region name +dotnet_diagnostic.RCS1226.severity = none # Add paragraph to documentation comment — <para> wrapping is subjective and adds noise +dotnet_diagnostic.RCS1228.severity = error # Unused element in a documentation comment +dotnet_diagnostic.RCS1232.severity = error # Order elements in documentation comment +dotnet_diagnostic.RCS1253.severity = error # Format documentation comment summary +dotnet_diagnostic.RCS1263.severity = none # Invalid reference in a documentation comment + +################### +# Roslynator Analyzers (RCS1xxx) - Disabled (covered by CA/SA equivalent) +################### +dotnet_diagnostic.RCS1018.severity = none # Add/remove accessibility modifiers — covered by SA1400 +dotnet_diagnostic.RCS1019.severity = none # Order modifiers — covered by SA1206 / SA1208 +dotnet_diagnostic.RCS1037.severity = none # Remove trailing white-space — covered by SA1028 +dotnet_diagnostic.RCS1052.severity = none # Declare each attribute separately — covered by SA1133 +dotnet_diagnostic.RCS1055.severity = none # Unnecessary semicolon at the end of declaration — covered by SA1106 +dotnet_diagnostic.RCS1060.severity = none # Declare each type in separate file — covered by SA1402 +dotnet_diagnostic.RCS1090.severity = none # Add/remove 'ConfigureAwait(false)' call — covered by CA2007 (also disabled) +dotnet_diagnostic.RCS1110.severity = none # Declare type inside namespace — covered by CA1050 +dotnet_diagnostic.RCS1138.severity = none # Add summary to documentation comment — covered by SA1600 +dotnet_diagnostic.RCS1139.severity = none # Add summary element to documentation comment — covered by SA1604 +dotnet_diagnostic.RCS1140.severity = none # Add exception to documentation comment — covered by SA1614 +dotnet_diagnostic.RCS1141.severity = none # Add 'param' element to documentation comment — covered by SA1611 +dotnet_diagnostic.RCS1142.severity = none # Add 'typeparam' element to documentation comment — covered by SA1618 +dotnet_diagnostic.RCS1175.severity = none # Unused 'this' parameter — covered by CA1822 +dotnet_diagnostic.RCS1194.severity = none # Implement exception constructors — covered by CA1032 +dotnet_diagnostic.RCS1203.severity = none # Use AttributeUsageAttribute — covered by CA1018 + +################### +# Roslynator Formatting Analyzers (RCS0xxx) - covered by StyleCop SA equivalents +################### +dotnet_diagnostic.RCS0001.severity = none # Add blank line after embedded statement — covered by StyleCop layout rules +dotnet_diagnostic.RCS0002.severity = none # Add blank line after #region — covered by StyleCop SA1517 family +dotnet_diagnostic.RCS0003.severity = none # Add blank line after using directive list — covered by SA1516 +dotnet_diagnostic.RCS0005.severity = none # Add blank line before #endregion — covered by StyleCop layout rules +dotnet_diagnostic.RCS0006.severity = none # Add blank line before using directive list — covered by SA1517 +dotnet_diagnostic.RCS0007.severity = none # Add blank line between accessors — covered by SA1513 +dotnet_diagnostic.RCS0008.severity = none # Add blank line between closing brace and next statement — covered by SA1513 +dotnet_diagnostic.RCS0009.severity = none # Add blank line between declaration and documentation comment — covered by SA1514 +dotnet_diagnostic.RCS0010.severity = none # Add blank line between declarations — covered by SA1516 +dotnet_diagnostic.RCS0011.severity = none # Add/remove blank line between single-line accessors — StyleCop already governs this +dotnet_diagnostic.RCS0012.severity = none # Add blank line between single-line declarations — covered by SA1516 +dotnet_diagnostic.RCS0013.severity = none # Add blank line between single-line declarations of different kind — covered by SA1516 +dotnet_diagnostic.RCS0015.severity = none # Add/remove blank line between using directives — covered by SA1209 / SA1210 +dotnet_diagnostic.RCS0016.severity = none # Put attribute list on its own line — StyleCop SA1133 governs attribute lists +dotnet_diagnostic.RCS0020.severity = none # Format accessor's braces — covered by SA1500 +dotnet_diagnostic.RCS0021.severity = none # Format block's braces — covered by SA1500 +dotnet_diagnostic.RCS0023.severity = none # Format type declaration's braces — covered by SA1500 +dotnet_diagnostic.RCS0024.severity = none # Add new line after switch label — covered by SA1003 +dotnet_diagnostic.RCS0025.severity = none # Put full accessor on its own line — covered by SA1502 +dotnet_diagnostic.RCS0027.severity = none # Place new line after/before binary operator — StyleCop wrapping rules cover this +dotnet_diagnostic.RCS0028.severity = none # Place new line after/before '?:' operator — StyleCop wrapping rules cover this +dotnet_diagnostic.RCS0029.severity = none # Put constructor initializer on its own line — StyleCop wrapping rules cover this +dotnet_diagnostic.RCS0030.severity = none # Put embedded statement on its own line — covered by SA1503 +dotnet_diagnostic.RCS0031.severity = none # Put enum member on its own line — covered by SA1136 +dotnet_diagnostic.RCS0032.severity = none # Place new line after/before arrow token — StyleCop wrapping rules cover this +dotnet_diagnostic.RCS0033.severity = none # Put statement on its own line — covered by SA1505 family +dotnet_diagnostic.RCS0034.severity = none # Put type parameter constraint on its own line — StyleCop wrapping rules cover this +dotnet_diagnostic.RCS0036.severity = none # Remove blank line between single-line declarations of same kind — covered by SA1516 +dotnet_diagnostic.RCS0039.severity = none # Remove new line before base list — StyleCop wrapping rules cover this +dotnet_diagnostic.RCS0041.severity = none # Remove new line between 'if' keyword and 'else' keyword — StyleCop layout rules cover this +dotnet_diagnostic.RCS0042.severity = none # Put auto-accessors on a single line — formatting preference, not enforced +dotnet_diagnostic.RCS0044.severity = none # Use carriage return + linefeed as new line — handled by .gitattributes / editorconfig end_of_line +dotnet_diagnostic.RCS0045.severity = none # Use linefeed as new line — handled by .gitattributes / editorconfig end_of_line +dotnet_diagnostic.RCS0046.severity = none # Use spaces instead of tab — handled by editorconfig indent_style +dotnet_diagnostic.RCS0048.severity = none # Put initializer on a single line — formatting preference, not enforced +dotnet_diagnostic.RCS0049.severity = none # Add blank line after top comment — covered by SA1517 +dotnet_diagnostic.RCS0050.severity = none # Add blank line before top declaration — covered by SA1517 +dotnet_diagnostic.RCS0051.severity = none # Add/remove new line before 'while' in 'do' statement — formatting preference +dotnet_diagnostic.RCS0052.severity = none # Place new line after/before equals token — StyleCop wrapping rules cover this +dotnet_diagnostic.RCS0053.severity = none # Fix formatting of a list — formatting preference +dotnet_diagnostic.RCS0054.severity = none # Fix formatting of a call chain — formatting preference +dotnet_diagnostic.RCS0055.severity = none # Fix formatting of a binary expression chain — formatting preference +dotnet_diagnostic.RCS0056.severity = none # A line is too long — line-length not enforced +dotnet_diagnostic.RCS0057.severity = none # Normalize whitespace at the beginning of a file — handled by editorconfig +dotnet_diagnostic.RCS0058.severity = none # Normalize whitespace at the end of a file — covered by SST1518 +dotnet_diagnostic.RCS0059.severity = none # Place new line after/before null-conditional operator — StyleSharp wrapping rules cover this +dotnet_diagnostic.RCS0060.severity = none # Add/remove line after file scoped namespace declaration — formatting preference +dotnet_diagnostic.RCS0061.severity = none # Add/remove blank line between switch sections — formatting preference +dotnet_diagnostic.RCS0062.severity = none # Put expression body on its own line — formatting preference +dotnet_diagnostic.RCS0063.severity = none # Remove unnecessary blank line — covered by SST1505 / SST1507 + +################### +# StyleSharp Analyzers (SST) +################### +file_header_template = Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved.\nReactiveUI and Contributors licenses this file to you under the MIT license.\nSee the LICENSE file in the project root for full license information. +stylesharp.summary_single_line_max_length = 120 + +# Documentation coverage scope (SST1600/SST1601/SST1602/SST1654). +# Require documentation on every element, including private members. +stylesharp.document_exposed_elements = true +stylesharp.document_internal_elements = true +stylesharp.document_private_elements = true +stylesharp.document_private_fields = true +stylesharp.document_interfaces = all +stylesharp.SST1305.allowed_hungarian_prefixes = rx +stylesharp.instance_member_qualification = omit_this +stylesharp.avoid_linq_on_hot_path = true +stylesharp.max_cyclomatic_complexity = 10 +stylesharp.max_cognitive_complexity = 15 +stylesharp.max_property_cognitive_complexity = 3 + +# Spacing +dotnet_diagnostic.SST1000.severity = error # A control-flow keyword is not followed by a space +dotnet_diagnostic.SST1001.severity = error # A comma is spaced incorrectly +dotnet_diagnostic.SST1002.severity = error # A semicolon is spaced incorrectly +dotnet_diagnostic.SST1003.severity = error # A binary operator is not surrounded by spaces +dotnet_diagnostic.SST1004.severity = error # A documentation comment line does not begin with a single space after the '///' +dotnet_diagnostic.SST1005.severity = error # A single-line comment does not begin with a single space +dotnet_diagnostic.SST1006.severity = error # A preprocessor keyword is preceded by a space after the '#' +dotnet_diagnostic.SST1007.severity = error # An operator keyword is not followed by a space +dotnet_diagnostic.SST1008.severity = error # An opening parenthesis is followed by a space +dotnet_diagnostic.SST1009.severity = error # A closing parenthesis is preceded by a space +dotnet_diagnostic.SST1010.severity = none # An opening square bracket has adjacent whitespace — conflicts with modern collection expressions +dotnet_diagnostic.SST1011.severity = error # A closing square bracket is preceded by a space +dotnet_diagnostic.SST1012.severity = error # An opening brace is not followed by a space on a single line +dotnet_diagnostic.SST1013.severity = error # A closing brace is not preceded by a space on a single line +dotnet_diagnostic.SST1014.severity = error # An opening generic bracket is preceded or followed by a space +dotnet_diagnostic.SST1015.severity = error # A closing generic bracket is preceded by a space +dotnet_diagnostic.SST1016.severity = error # An opening attribute bracket is followed by a space +dotnet_diagnostic.SST1017.severity = error # A closing attribute bracket is preceded by a space +dotnet_diagnostic.SST1018.severity = error # A nullable type symbol is preceded by a space +dotnet_diagnostic.SST1019.severity = error # A member access symbol is surrounded by spaces +dotnet_diagnostic.SST1020.severity = error # An increment or decrement symbol is separated from its operand by a space +dotnet_diagnostic.SST1021.severity = error # A unary negative sign is followed by a space +dotnet_diagnostic.SST1022.severity = error # A unary positive sign is followed by a space +dotnet_diagnostic.SST1023.severity = error # A dereference or address-of symbol is followed by a space +dotnet_diagnostic.SST1024.severity = error # A colon is spaced incorrectly for its context +dotnet_diagnostic.SST1025.severity = error # Two or more whitespace characters appear in a row within a line +dotnet_diagnostic.SST1026.severity = error # A space follows 'new' or 'stackalloc' in an implicit array creation +dotnet_diagnostic.SST1027.severity = error # A tab character is used where the project standardises on spaces +dotnet_diagnostic.SST1028.severity = error # A line ends with trailing whitespace + +# Readability and maintainability +dotnet_diagnostic.SST1100.severity = error # A base. prefix is used where the type does not override the member +dotnet_diagnostic.SST1101.severity = none # An instance member is accessed without a this. prefix — we don't require this. prefixing +dotnet_diagnostic.SST1102.severity = error # A query clause is separated from the previous clause by a blank line +dotnet_diagnostic.SST1103.severity = error # Query clauses mix single-line and multi-line layout +dotnet_diagnostic.SST1104.severity = error # A query clause shares the last line of a multi-line previous clause +dotnet_diagnostic.SST1105.severity = error # A multi-line query clause does not begin on its own line +dotnet_diagnostic.SST1106.severity = error # A statement is empty (a stray semicolon) +dotnet_diagnostic.SST1107.severity = error # More than one statement shares a line +dotnet_diagnostic.SST1110.severity = error # An opening parenthesis or bracket does not sit on the line of the preceding code +dotnet_diagnostic.SST1111.severity = error # A closing parenthesis or bracket does not sit on the last parameter's line +dotnet_diagnostic.SST1112.severity = error # An empty parameter list's closing parenthesis is on a different line +dotnet_diagnostic.SST1113.severity = error # A comma does not sit on the previous parameter's line +dotnet_diagnostic.SST1114.severity = error # A blank line separates the declaration from its parameter list +dotnet_diagnostic.SST1115.severity = error # A blank line separates a parameter from the preceding comma +dotnet_diagnostic.SST1116.severity = error # A qualified name can be shortened without changing the symbol it binds to +dotnet_diagnostic.SST1117.severity = error # Instance member access follows the configured this. qualification style +dotnet_diagnostic.SST1118.severity = none # A parameter should not span multiple lines +dotnet_diagnostic.SST1120.severity = error # A comment contains no text +dotnet_diagnostic.SST1121.severity = none # A framework type name is used instead of its built-in alias — duplicate of RCS1013 +dotnet_diagnostic.SST1122.severity = error # An empty string literal is used instead of string.Empty +dotnet_diagnostic.SST1123.severity = error # A #region is placed inside a code element body +dotnet_diagnostic.SST1124.severity = error # A #region directive is used +dotnet_diagnostic.SST1125.severity = error # A Nullable type is written in long form instead of the T? shorthand +dotnet_diagnostic.SST1127.severity = error # A generic type constraint shares a line with the declaration or another constraint +dotnet_diagnostic.SST1128.severity = error # A constructor initializer shares a line with the constructor signature +dotnet_diagnostic.SST1129.severity = error # A value type is created with a parameterless constructor call instead of default +dotnet_diagnostic.SST1130.severity = error # An anonymous delegate is used where a lambda expression is clearer +dotnet_diagnostic.SST1131.severity = error # A comparison places the constant on the left ("yoda" condition) +dotnet_diagnostic.SST1132.severity = error # Several fields are declared in a single statement +dotnet_diagnostic.SST1133.severity = error # Several attributes share one bracket list +dotnet_diagnostic.SST1134.severity = error # An attribute shares a line with another attribute or the element +dotnet_diagnostic.SST1135.severity = error # A using directive names a namespace or type that is not fully qualified +dotnet_diagnostic.SST1136.severity = error # Several enum members share a line +dotnet_diagnostic.SST1137.severity = error # Sibling elements are indented differently from one another +dotnet_diagnostic.SST1139.severity = error # A numeric literal is cast where a literal suffix would express the type +dotnet_diagnostic.SST1140.severity = error # Wrapped conditional operators should start indented continuation lines +dotnet_diagnostic.SST1141.severity = error # An explicit ValueTuple<...> is used where tuple syntax would do +dotnet_diagnostic.SST1142.severity = error # A tuple element is accessed by ItemN where it has a name +dotnet_diagnostic.SST1143.severity = error # A boolean expression is compared to a true/false literal +dotnet_diagnostic.SST1144.severity = error # Combine case labels with an or-pattern +dotnet_diagnostic.SST1145.severity = error # A wrapped conditional expression places ?/: inconsistently +dotnet_diagnostic.SST1146.severity = error # An if statement follows a closing brace on the same line +dotnet_diagnostic.SST1147.severity = error # Do not nest conditional operators +dotnet_diagnostic.SST1148.severity = error # Commented-out code should be removed +dotnet_diagnostic.SST1149.severity = error # Prefer the 'is null' pattern for null checks +dotnet_diagnostic.SST1150.severity = error # Require each constructor parameter on a unique line +dotnet_diagnostic.SST1151.severity = error # Require each method parameter on a unique line +dotnet_diagnostic.SST1152.severity = error # Require each delegate parameter on a unique line +dotnet_diagnostic.SST1153.severity = error # Require each indexer parameter on a unique line +dotnet_diagnostic.SST1154.severity = error # Require each invocation argument on a unique line +dotnet_diagnostic.SST1155.severity = error # Require each object creation argument on a unique line +dotnet_diagnostic.SST1156.severity = error # Require each element access argument on a unique line +dotnet_diagnostic.SST1157.severity = error # Require each attribute argument on a unique line +dotnet_diagnostic.SST1158.severity = error # Require each anonymous method parameter on a unique line +dotnet_diagnostic.SST1159.severity = error # Require each parenthesized lambda parameter on a unique line +dotnet_diagnostic.SST1160.severity = error # Require each record primary-constructor parameter on a unique line +dotnet_diagnostic.SST1161.severity = error # Require each class primary-constructor parameter on a unique line +dotnet_diagnostic.SST1162.severity = error # Require each struct primary-constructor parameter on a unique line +dotnet_diagnostic.SST1163.severity = error # Require each target-typed object creation argument on a unique line +dotnet_diagnostic.SST1164.severity = error # Require each constructor initializer argument on a unique line +dotnet_diagnostic.SST1165.severity = error # Require each primary-constructor base argument on a unique line +dotnet_diagnostic.SST1166.severity = error # Require each local-function parameter on a unique line +dotnet_diagnostic.SST1167.severity = error # Require each operator parameter on a unique line +dotnet_diagnostic.SST1168.severity = error # Require each conversion-operator parameter on a unique line +dotnet_diagnostic.SST1169.severity = error # Require each type parameter on a unique line +dotnet_diagnostic.SST1170.severity = error # Require each type argument on a unique line +dotnet_diagnostic.SST1171.severity = error # Require each function-pointer parameter on a unique line +dotnet_diagnostic.SST1172.severity = error # Negated comparisons should use the opposite operator +dotnet_diagnostic.SST1173.severity = error # Redundant anonymous-type member names should be omitted +dotnet_diagnostic.SST1174.severity = error # Redundant jump statements should be removed +dotnet_diagnostic.SST1175.severity = error # Unnecessary casts should be removed +dotnet_diagnostic.SST1176.severity = error # Members should not be initialized to their default value +dotnet_diagnostic.SST1177.severity = error # Redundant base types should be removed +dotnet_diagnostic.SST1178.severity = error # Redundant base constructor calls should be removed +dotnet_diagnostic.SST1179.severity = error # Redundant default switch sections should be removed +dotnet_diagnostic.SST1180.severity = error # Empty else clauses should be removed +dotnet_diagnostic.SST1181.severity = error # Redundant overriding members should be removed +dotnet_diagnostic.SST1182.severity = error # Conditional expressions should not just return boolean literals +dotnet_diagnostic.SST1183.severity = error # Interpolated strings without interpolations should be plain strings +dotnet_diagnostic.SST1184.severity = error # Verbatim string literals without escapes should be regular strings +dotnet_diagnostic.SST1185.severity = error # Use a compound assignment operator +dotnet_diagnostic.SST1186.severity = error # A literal should be on the right side of a comparison +dotnet_diagnostic.SST1187.severity = error # Assignments should not be chained +dotnet_diagnostic.SST1188.severity = error # Use the 'default' literal instead of 'default(T)' +dotnet_diagnostic.SST1189.severity = error # Variables should not be self-assigned +dotnet_diagnostic.SST1190.severity = error # Doubled negation operators should be removed +dotnet_diagnostic.SST1191.severity = error # Long numeric literals should use digit separators +dotnet_diagnostic.SST1192.severity = none # Control characters in string literals should be escaped +dotnet_diagnostic.SST1193.severity = error # Keep initial member values with construction +dotnet_diagnostic.SST1194.severity = error # Keep initial collection values with construction +dotnet_diagnostic.SST1195.severity = error # Write null fallback with ?? +dotnet_diagnostic.SST1196.severity = error # Write null-guarded access with ?. +dotnet_diagnostic.SST1197.severity = error # Collapse return-only branches into one return +dotnet_diagnostic.SST1198.severity = error # Collapse assignment-only branches into one assignment +dotnet_diagnostic.SST1199.severity = error # Prefer compile-time type names + +# Ordering +dotnet_diagnostic.SST1200.severity = none # Using directives should be placed outside the namespace — usings live outside file-scoped namespaces +dotnet_diagnostic.SST1201.severity = error # Members should be ordered by kind +dotnet_diagnostic.SST1202.severity = error # Members should be ordered by accessibility +dotnet_diagnostic.SST1203.severity = error # Constants should appear before fields +dotnet_diagnostic.SST1204.severity = error # Static members should appear before instance members +dotnet_diagnostic.SST1205.severity = error # Partial elements should declare an access modifier +dotnet_diagnostic.SST1206.severity = error # Declaration keywords should follow the standard order +dotnet_diagnostic.SST1207.severity = error # Place protected before internal +dotnet_diagnostic.SST1208.severity = error # System using directives should appear before other usings +dotnet_diagnostic.SST1209.severity = error # Using alias directives should appear after other usings +dotnet_diagnostic.SST1210.severity = error # Regular using directives should be ordered alphabetically +dotnet_diagnostic.SST1211.severity = error # Using alias directives should be ordered alphabetically by alias +dotnet_diagnostic.SST1212.severity = error # Property accessors should be ordered with get first +dotnet_diagnostic.SST1213.severity = error # Event accessors should be ordered with add first +dotnet_diagnostic.SST1214.severity = error # Static readonly fields should appear before static non-readonly fields +dotnet_diagnostic.SST1215.severity = error # Instance readonly fields should appear before instance non-readonly fields +dotnet_diagnostic.SST1216.severity = error # Using static directives should be placed after regular usings and before aliases +dotnet_diagnostic.SST1217.severity = error # Using static directives should be ordered alphabetically + +# Naming +dotnet_diagnostic.SST1300.severity = none # Types and members should be PascalCase — naming duplicates existing analyzers +dotnet_diagnostic.SST1302.severity = error # Interface names should begin with I +dotnet_diagnostic.SST1303.severity = error # Const names should be PascalCase +dotnet_diagnostic.SST1304.severity = error # Non-private readonly fields should be PascalCase +dotnet_diagnostic.SST1305.severity = error # Field names should not use Hungarian notation +dotnet_diagnostic.SST1306.severity = none # Field names should begin with a lower-case letter — private fields use _camelCase here +dotnet_diagnostic.SST1307.severity = error # Accessible fields should be PascalCase +dotnet_diagnostic.SST1308.severity = none # Field names should not be prefixed with m_ or s_ — too broad for existing conventions +dotnet_diagnostic.SST1309.severity = error # Private fields should be _camelCase +dotnet_diagnostic.SST1310.severity = none # Field names should not contain underscores — conflicts with _camelCase +dotnet_diagnostic.SST1311.severity = none # Static readonly fields should be PascalCase — we use _camelCase for private static readonly too +dotnet_diagnostic.SST1312.severity = error # Local variables should be camelCase +dotnet_diagnostic.SST1313.severity = error # Parameters should be camelCase +dotnet_diagnostic.SST1314.severity = error # Type parameters should begin with T +dotnet_diagnostic.SST1315.severity = error # Union member names should match the configured casing +dotnet_diagnostic.SST1316.severity = none # Tuple element names should use the configured casing — tuple naming is not enforced here +dotnet_diagnostic.SST1317.severity = none # Asynchronous method names should end with 'Async' — conflicts with this project's Rx-compatibility and naming mechanism +dotnet_diagnostic.SST1318.severity = error # Overriding parameter names should match the base declaration + +# Maintainability +dotnet_diagnostic.SST1400.severity = error # An element does not declare an access modifier +dotnet_diagnostic.SST1401.severity = error # A non-private, non-constant field is exposed +dotnet_diagnostic.SST1402.severity = error # A file declares more than one top-level type +dotnet_diagnostic.SST1403.severity = error # A file declares more than one namespace +dotnet_diagnostic.SST1404.severity = error # A code-analysis suppression has no justification +dotnet_diagnostic.SST1405.severity = error # A Debug.Assert call provides no message +dotnet_diagnostic.SST1406.severity = error # A Debug.Fail call provides no message +dotnet_diagnostic.SST1407.severity = error # Mixed-precedence arithmetic is not parenthesized +dotnet_diagnostic.SST1408.severity = error # Mixed conditional operators are not parenthesized +dotnet_diagnostic.SST1410.severity = error # An anonymous method has an empty parameter list +dotnet_diagnostic.SST1411.severity = error # An attribute uses an empty argument list +dotnet_diagnostic.SST1412.severity = none # Store files as UTF-8 with a byte order mark — conflicts with SST1450 (UTF-8 without BOM) +dotnet_diagnostic.SST1413.severity = none # A multi-line initializer omits the trailing comma — trailing commas are not required here +dotnet_diagnostic.SST1414.severity = error # A tuple type in a member signature has an unnamed element +dotnet_diagnostic.SST1415.severity = error # An argument-exception constructor uses a string literal where nameof would track renames +dotnet_diagnostic.SST1416.severity = none # Do not declare public members in a non-public type +dotnet_diagnostic.SST1417.severity = suggestion # Namespace should match the folder structure +dotnet_diagnostic.SST1418.severity = error # Declare precedence when mixing the null-coalescing operator +dotnet_diagnostic.SST1419.severity = error # Remove redundant modifiers +dotnet_diagnostic.SST1420.severity = error # Trivial properties should be auto-implemented +dotnet_diagnostic.SST1421.severity = error # Write-only properties should not be used +dotnet_diagnostic.SST1422.severity = error # Private fields used only as locals should be local variables +dotnet_diagnostic.SST1423.severity = error # Switch statements should not have too many sections +dotnet_diagnostic.SST1424.severity = error # Fields that are never reassigned should be readonly +dotnet_diagnostic.SST1425.severity = error # Do not reassign captured primary-constructor parameters +dotnet_diagnostic.SST1426.severity = error # Use [SuppressMessage] instead of #pragma warning disable +dotnet_diagnostic.SST1427.severity = error # Protected members of sealed types should not be used +dotnet_diagnostic.SST1428.severity = error # Abstract types should not declare public constructors +dotnet_diagnostic.SST1429.severity = error # Empty catch clauses should not swallow the base exception +dotnet_diagnostic.SST1430.severity = error # Rethrow with 'throw;' to preserve the stack trace +dotnet_diagnostic.SST1431.severity = error # Static members of a generic type should use a type parameter +dotnet_diagnostic.SST1432.severity = error # Classes with only static members should be static +dotnet_diagnostic.SST1433.severity = error # Redundant constructors should be removed +dotnet_diagnostic.SST1434.severity = error # Empty finalizers should be removed +dotnet_diagnostic.SST1435.severity = error # Empty namespace declarations should be removed +dotnet_diagnostic.SST1436.severity = error # Empty types should not be declared +dotnet_diagnostic.SST1437.severity = error # Empty interfaces should not be declared +dotnet_diagnostic.SST1438.severity = error # Methods should not be empty +dotnet_diagnostic.SST1439.severity = error # Nested code blocks should not be left empty +dotnet_diagnostic.SST1440.severity = error # Private members with no local use should be removed +dotnet_diagnostic.SST1441.severity = error # Private fields assigned but never read should be removed +dotnet_diagnostic.SST1442.severity = error # A function has too many direct branch points +dotnet_diagnostic.SST1443.severity = error # A function has too much nested control flow +dotnet_diagnostic.SST1444.severity = error # A loop cannot naturally reach a second iteration +dotnet_diagnostic.SST1450.severity = error # Store files as UTF-8 without a byte order mark + +# Layout +dotnet_diagnostic.SST1500.severity = error # A brace in a multi-line construct shares its line with other code +dotnet_diagnostic.SST1501.severity = error # A statement block is collapsed onto a single line +dotnet_diagnostic.SST1502.severity = error # An element body is collapsed onto a single line +dotnet_diagnostic.SST1503.severity = none # A control-flow statement omits the braces around its child statement — duplicate with existing brace preferences +dotnet_diagnostic.SST1504.severity = error # The accessors of a property/event mix single-line and multi-line forms +dotnet_diagnostic.SST1505.severity = error # An opening brace is followed by a blank line +dotnet_diagnostic.SST1506.severity = error # An element documentation header is followed by a blank line +dotnet_diagnostic.SST1507.severity = error # Two or more blank lines appear in a row +dotnet_diagnostic.SST1508.severity = error # A closing brace is preceded by a blank line +dotnet_diagnostic.SST1509.severity = error # An opening brace is preceded by a blank line +dotnet_diagnostic.SST1510.severity = error # A chained block such as else/catch/finally is preceded by a blank line +dotnet_diagnostic.SST1511.severity = error # The while footer of a do/while loop is preceded by a blank line +dotnet_diagnostic.SST1512.severity = error # A single-line comment is followed by a blank line +dotnet_diagnostic.SST1513.severity = error # A closing brace is not followed by a blank line +dotnet_diagnostic.SST1514.severity = error # An element documentation header is not preceded by a blank line +dotnet_diagnostic.SST1515.severity = error # A single-line comment is not preceded by a blank line +dotnet_diagnostic.SST1516.severity = error # Adjacent members or namespace elements are not separated by a blank line +dotnet_diagnostic.SST1517.severity = error # The file begins with one or more blank lines +dotnet_diagnostic.SST1518.severity = error # The file does not end with exactly one newline +dotnet_diagnostic.SST1519.severity = error # A multi-line child statement of a control-flow keyword omits its braces +dotnet_diagnostic.SST1520.severity = error # The clauses of an if/else chain use braces inconsistently + +# Documentation +dotnet_diagnostic.SST1600.severity = error # Externally visible members should be documented +dotnet_diagnostic.SST1601.severity = error # Partial elements should be documented +dotnet_diagnostic.SST1602.severity = error # Enumeration members should be documented +dotnet_diagnostic.SST1604.severity = error # Element documentation should contain a summary +dotnet_diagnostic.SST1605.severity = error # Partial element documentation should have a summary +dotnet_diagnostic.SST1606.severity = error # The summary should have text +dotnet_diagnostic.SST1607.severity = error # Partial element summary should have text +dotnet_diagnostic.SST1608.severity = error # Documentation should not use the default placeholder summary +dotnet_diagnostic.SST1609.severity = none # Property documentation should have a value +dotnet_diagnostic.SST1610.severity = error # Property value documentation should have text +dotnet_diagnostic.SST1611.severity = error # Parameters should be documented +dotnet_diagnostic.SST1612.severity = error # Parameter documentation should match the parameters +dotnet_diagnostic.SST1613.severity = error # Parameter documentation should declare a name +dotnet_diagnostic.SST1614.severity = error # Parameter documentation should have text +dotnet_diagnostic.SST1615.severity = error # The return value should be documented + +dotnet_diagnostic.SST1616.severity = error # Return value documentation should have text +dotnet_diagnostic.SST1617.severity = error # A void return value should not be documented +dotnet_diagnostic.SST1618.severity = error # Generic type parameters should be documented +dotnet_diagnostic.SST1619.severity = error # Partial generic type parameters should be documented +dotnet_diagnostic.SST1620.severity = error # Type parameter documentation should match the type parameters +dotnet_diagnostic.SST1621.severity = error # Type parameter documentation should declare a name +dotnet_diagnostic.SST1622.severity = error # Type parameter documentation should have text +dotnet_diagnostic.SST1623.severity = error # Property summaries should describe their accessors +dotnet_diagnostic.SST1624.severity = error # Property summary should omit a restricted set accessor +dotnet_diagnostic.SST1625.severity = error # Element documentation should not be copy-pasted +dotnet_diagnostic.SST1626.severity = error # A documentation-style comment is used where it does not document an element +dotnet_diagnostic.SST1627.severity = error # Documentation text should not be empty +dotnet_diagnostic.SST1628.severity = error # Documentation text should begin with a capital letter +dotnet_diagnostic.SST1629.severity = error # Documentation text should end with a period +dotnet_diagnostic.SST1630.severity = error # Documentation text should contain whitespace +dotnet_diagnostic.SST1631.severity = error # Documentation text should be mostly letters +dotnet_diagnostic.SST1632.severity = error # Documentation text should meet a minimum length +dotnet_diagnostic.SST1633.severity = error # Files should begin with the configured header +dotnet_diagnostic.SST1642.severity = error # Constructor summaries should begin with the standard text +dotnet_diagnostic.SST1643.severity = error # Destructor summaries should begin with the standard text +dotnet_diagnostic.SST1644.severity = error # Documentation headers should not contain blank lines +dotnet_diagnostic.SST1648.severity = error # inheritdoc should be used with inheriting elements +dotnet_diagnostic.SST1649.severity = error # The file name should match the first type name +dotnet_diagnostic.SST1651.severity = error # Placeholder documentation elements should be removed +dotnet_diagnostic.SST1653.severity = error # Keep short documentation summaries on a single line +dotnet_diagnostic.SST1654.severity = error # Extension blocks should be documented with a summary +dotnet_diagnostic.SST1655.severity = error # Extension block parameters should be documented +dotnet_diagnostic.SST1656.severity = error # Extension block type parameters should be documented +dotnet_diagnostic.SST1657.severity = error # Extension block documentation should reference a real parameter or type parameter + +# Concurrency and modernization +dotnet_diagnostic.SST1900.severity = error # A dedicated object lock field should be a System.Threading.Lock +dotnet_diagnostic.SST1901.severity = error # A lock targets a field or property reachable from outside the declaring type +dotnet_diagnostic.SST1902.severity = error # Do not lock on 'this', a Type, or a string +dotnet_diagnostic.SST1903.severity = error # Do not lock on a newly-created object +dotnet_diagnostic.SST2000.severity = suggestion # A null check plus throw should use ArgumentNullException.ThrowIfNull +dotnet_diagnostic.SST2001.severity = error # Use ArgumentException.ThrowIfNullOrEmpty +dotnet_diagnostic.SST2002.severity = error # Use ArgumentException.ThrowIfNullOrWhiteSpace +dotnet_diagnostic.SST2003.severity = suggestion # A disposed check should use ObjectDisposedException.ThrowIf +dotnet_diagnostic.SST2004.severity = suggestion # A range check should use an ArgumentOutOfRangeException.ThrowIf... helper +dotnet_diagnostic.SST2005.severity = error # Use the 'is' type pattern instead of comparing an 'as' cast to null +dotnet_diagnostic.SST2006.severity = error # Use the 'is not' pattern instead of negating an 'is' check +dotnet_diagnostic.SST2007.severity = error # Use declaration patterns instead of an is check followed by a cast local + +# Modern language and library usage +dotnet_diagnostic.SST1700.severity = error # An extension block declares no members +dotnet_diagnostic.SST1701.severity = error # Two extension blocks in a type share the same receiver type +dotnet_diagnostic.SST1702.severity = error # Extension blocks in a type are separated by other members +dotnet_diagnostic.SST1703.severity = error # A classic this-parameter extension method is used where an extension block could be +dotnet_diagnostic.SST1704.severity = error # A class declaring extension blocks is not named with an Extensions suffix +dotnet_diagnostic.SST1705.severity = error # A class mixes classic extension methods with extension blocks +dotnet_diagnostic.SST1706.severity = error # An extension block targets a broad receiver type such as object or dynamic +dotnet_diagnostic.SST1707.severity = error # Extension blocks should be ordered by receiver type +dotnet_diagnostic.SST1800.severity = error # Record classes should be sealed +dotnet_diagnostic.SST1801.severity = error # A positional record parameter does not match the configured casing +dotnet_diagnostic.SST1802.severity = error # A record declares a settable rather than init-only instance property +dotnet_diagnostic.SST1803.severity = error # A record struct is not declared readonly +dotnet_diagnostic.SST2100.severity = error # An empty collection creation can use [] +dotnet_diagnostic.SST2101.severity = error # An explicit collection creation can use [...] +dotnet_diagnostic.SST2102.severity = error # A span-targeted stackalloc initializer can use a collection expression +dotnet_diagnostic.SST2103.severity = error # A collection-builder factory call can use a collection expression +dotnet_diagnostic.SST2104.severity = error # A short builder-local sequence can return a collection expression +dotnet_diagnostic.SST2105.severity = error # A literal array materialized with LINQ can use the target collection expression directly +dotnet_diagnostic.SST2200.severity = error # A single-use backing field can use the C# 14 field keyword +dotnet_diagnostic.SST2201.severity = error # A return-only switch statement can use a switch expression +dotnet_diagnostic.SST2202.severity = error # An object creation repeats an explicit target type +dotnet_diagnostic.SST2203.severity = error # An array or string index can use from-end indexing +dotnet_diagnostic.SST2204.severity = error # A string slice can use range syntax +dotnet_diagnostic.SST2205.severity = none # An enum switch statement omits named enum values — duplicate of IDE0010 (disabled: too noisy for default/fallthrough) and S131; statement switches deliberately no-op omitted values, and a default to satisfy it is rejected by SST1179/S3532. SST2206 keeps the valuable switch-expression exhaustiveness check. +dotnet_diagnostic.SST2206.severity = error # An enum switch expression omits named enum values +dotnet_diagnostic.SST2207.severity = error # A null guard and return can keep the throw in the returned expression +dotnet_diagnostic.SST2208.severity = error # An out variable can be declared at the call site +dotnet_diagnostic.SST2209.severity = none # A null-forgiving operator has no local effect +dotnet_diagnostic.SST2210.severity = error # A nullable directive repeats the current file-local state +dotnet_diagnostic.SST2211.severity = error # A nullable restore directive has no file-local state to restore +dotnet_diagnostic.SST2212.severity = error # Literal UTF-8 byte data can use a u8 string literal +dotnet_diagnostic.SST2213.severity = error # A typed pattern has an unnecessary discard designation +dotnet_diagnostic.SST2214.severity = error # A tuple temporary only feeds copied element locals +dotnet_diagnostic.SST2215.severity = error # A temporary local swaps two locals +dotnet_diagnostic.SST2216.severity = error # A tuple element name repeats the inferred name +dotnet_diagnostic.SST2217.severity = error # A manual hash-code expression can use System.HashCode.Combine +dotnet_diagnostic.SST2218.severity = error # Lambda parameter types can be omitted when the target already supplies them +dotnet_diagnostic.SST2219.severity = error # A single-expression property accessor can use an expression body +dotnet_diagnostic.SST2220.severity = error # An interpolation hole can carry the value and literal format directly +dotnet_diagnostic.SST2221.severity = error # An ignored expression value is assigned to the discard +dotnet_diagnostic.SST2222.severity = error # A local value is overwritten before it is read +dotnet_diagnostic.SST2223.severity = error # A null fallback assignment can use ??= +dotnet_diagnostic.SST2224.severity = error # An anonymous object can become a tuple literal for local value bundles +dotnet_diagnostic.SST2225.severity = error # A foreach loop hides an explicit element cast +dotnet_diagnostic.SST2226.severity = error # A cast hides an inner explicit conversion +dotnet_diagnostic.SST2227.severity = error # A post-assignment null fallback can be folded into the assigned expression +dotnet_diagnostic.SST2228.severity = error # A delegate local used only as a call target can be a local function +dotnet_diagnostic.SST2229.severity = none # LINQ terminal predicate simplification is reserved for test code; production code should avoid LINQ on hot paths +dotnet_diagnostic.SST2230.severity = none # LINQ type-filter simplification is reserved for test code; production code should avoid LINQ on hot paths +dotnet_diagnostic.SST2231.severity = error # A broad object pattern can use a direct null pattern +dotnet_diagnostic.SST2232.severity = error # nameof does not need concrete generic type arguments +dotnet_diagnostic.SST2233.severity = error # Hot-path code should avoid System.Linq.Enumerable calls +dotnet_diagnostic.RCS1040.severity = none # covered by SST1180 + +################### +# PublicApiAnalyzers (RSxxxx) - public API surface tracking +################### +dotnet_diagnostic.RS0016.severity = error # public symbol missing from the PublicAPI baseline +dotnet_diagnostic.RS0017.severity = error # PublicAPI baseline entry no longer in source ################### # Trimming Analyzer Warnings (IL2001 - IL2123) @@ -932,6 +1631,683 @@ dotnet_diagnostic.IL3055.severity = error # MakeGenericType on non-supported typ dotnet_diagnostic.IL3056.severity = error # MakeGenericMethod on non-supported method requires dynamic code dotnet_diagnostic.IL3057.severity = error # Reflection access to generic parameter requires dynamic code + +################### +# SonarAnalyzer (Sxxxx) - Blocker Bug +################### +dotnet_diagnostic.S1048.severity = error # Finalizers should not throw exceptions +dotnet_diagnostic.S2190.severity = none # Loops and recursions should not be infinite +dotnet_diagnostic.S2275.severity = none # Composite format strings should not lead to unexpected behavior at runtime - DUPLICATE CA2241 +dotnet_diagnostic.S2857.severity = error # SQL keywords should be delimited by whitespace +dotnet_diagnostic.S2930.severity = error # "IDisposables" should be disposed +dotnet_diagnostic.S2931.severity = error # Classes with "IDisposable" members should implement "IDisposable" +dotnet_diagnostic.S3464.severity = error # Type inheritance should not be recursive +dotnet_diagnostic.S3869.severity = error # "SafeHandle.DangerousGetHandle" should not be called +dotnet_diagnostic.S3889.severity = error # "Thread.Resume" and "Thread.Suspend" should not be used +dotnet_diagnostic.S4159.severity = error # Classes should implement their "ExportAttribute" interfaces + +################### +# SonarAnalyzer (Sxxxx) - Critical Bug +################### +dotnet_diagnostic.S2551.severity = error # Shared resources should not be used for locking +dotnet_diagnostic.S2952.severity = error # Classes should "Dispose" of members from the classes' own "Dispose" methods +dotnet_diagnostic.S3449.severity = error # Right operands of shift operators should be integers +dotnet_diagnostic.S4275.severity = error # Getters and setters should access the expected fields +dotnet_diagnostic.S4277.severity = error # "Shared" parts should not be created with "new" +dotnet_diagnostic.S4583.severity = error # Calls to delegate's method "BeginInvoke" should be paired with calls to "EndInvoke" +dotnet_diagnostic.S4586.severity = error # Non-async "Task/Task" methods should not return null +dotnet_diagnostic.S5856.severity = error # Regular expressions should be syntactically valid +dotnet_diagnostic.S6674.severity = error # Log message template should be syntactically correct + +################### +# SonarAnalyzer (Sxxxx) - Major Bug +################### +dotnet_diagnostic.S1244.severity = error # Floating point numbers should not be tested for equality +dotnet_diagnostic.S1656.severity = none # Variables should not be self-assigned — covered by SST1189 +dotnet_diagnostic.S1751.severity = none # Loops with at most one iteration should be refactored - covered by SST1444 +dotnet_diagnostic.S1764.severity = error # Identical expressions should not be used on both sides of operators +dotnet_diagnostic.S1848.severity = error # Objects should not be created to be dropped immediately without being used +dotnet_diagnostic.S1862.severity = error # Related "if/else if" statements should not have the same condition +dotnet_diagnostic.S2114.severity = error # Collections should not be passed as arguments to their own methods +dotnet_diagnostic.S2123.severity = error # Values should not be uselessly incremented +dotnet_diagnostic.S2201.severity = error # Methods without side effects should not have their return values ignored +dotnet_diagnostic.S2225.severity = error # "ToString()" method should not return null +dotnet_diagnostic.S2251.severity = error # A "for" loop update clause should move the counter in the right direction +dotnet_diagnostic.S2252.severity = error # For-loop conditions should be true at least once +dotnet_diagnostic.S2445.severity = error # Blocks should be synchronized on read-only fields +dotnet_diagnostic.S2688.severity = error # "NaN" should not be used in comparisons +dotnet_diagnostic.S2757.severity = error # Non-existent operators like "=+" should not be used +dotnet_diagnostic.S2761.severity = none # Doubled prefix operators "!!" and "~~" should not be used — covered by SST1190 +dotnet_diagnostic.S2995.severity = error # "Object.ReferenceEquals" should not be used for value types +dotnet_diagnostic.S2996.severity = error # "ThreadStatic" fields should not be initialized +dotnet_diagnostic.S2997.severity = error # "IDisposables" created in a "using" statement should not be returned +dotnet_diagnostic.S3005.severity = error # "ThreadStatic" should not be used on non-static fields +dotnet_diagnostic.S3168.severity = error # "async" methods should not return "void" +dotnet_diagnostic.S3172.severity = error # Delegates should not be subtracted +dotnet_diagnostic.S3244.severity = error # Anonymous delegates should not be used to unsubscribe from Events +dotnet_diagnostic.S3249.severity = error # Classes directly extending "object" should not call "base" in "GetHashCode" or "Equals" +dotnet_diagnostic.S3263.severity = error # Static fields should appear in the order they must be initialized +dotnet_diagnostic.S3343.severity = error # Caller information parameters should come at the end of the parameter list +dotnet_diagnostic.S3346.severity = error # Expressions used in "Debug.Assert" should not produce side effects +dotnet_diagnostic.S3453.severity = error # Classes should not have only "private" constructors +dotnet_diagnostic.S3466.severity = error # Optional parameters should be passed to "base" calls +dotnet_diagnostic.S3598.severity = error # One-way "OperationContract" methods should have "void" return type +dotnet_diagnostic.S3603.severity = error # Methods with "Pure" attribute should return a value +dotnet_diagnostic.S3610.severity = error # Nullable type comparison should not be redundant +dotnet_diagnostic.S3903.severity = error # Types should be defined in named namespaces +dotnet_diagnostic.S3923.severity = error # All branches in a conditional structure should not have exactly the same implementation +dotnet_diagnostic.S3926.severity = error # Deserialization methods should be provided for "OptionalField" members +dotnet_diagnostic.S3927.severity = error # Serialization event handlers should be implemented correctly +dotnet_diagnostic.S3981.severity = error # Collection sizes and array length comparisons should make sense +dotnet_diagnostic.S3984.severity = error # Exceptions should not be created without being thrown +dotnet_diagnostic.S4143.severity = error # Collection elements should not be replaced unconditionally +dotnet_diagnostic.S4210.severity = error # Windows Forms entry points should be marked with STAThread +dotnet_diagnostic.S4260.severity = error # "ConstructorArgument" parameters should exist in constructors +dotnet_diagnostic.S4428.severity = error # "PartCreationPolicyAttribute" should be used with "ExportAttribute" +dotnet_diagnostic.S6507.severity = error # Blocks should not be synchronized on local variables +dotnet_diagnostic.S6677.severity = error # Message template placeholders should be unique +dotnet_diagnostic.S6797.severity = error # Blazor query parameter type should be supported +dotnet_diagnostic.S6798.severity = error # [JSInvokable] attribute should only be used on public methods +dotnet_diagnostic.S6800.severity = error # Component parameter type should match the route parameter type constraint +dotnet_diagnostic.S6930.severity = error # Backslash should be avoided in route templates + +################### +# SonarAnalyzer (Sxxxx) - Minor Bug +################### +dotnet_diagnostic.S1206.severity = none # "Equals(Object)" and "GetHashCode()" should be overridden in pairs - DUPLICATE CA2218 +dotnet_diagnostic.S1226.severity = none # Method parameters, caught exceptions and foreach variables' initial values should not be ignored +dotnet_diagnostic.S2183.severity = error # Integral numbers should not be shifted by zero or more than their number of bits-1 +dotnet_diagnostic.S2184.severity = error # Results of integer division should not be assigned to floating point variables +dotnet_diagnostic.S2328.severity = error # "GetHashCode" should not reference mutable fields +dotnet_diagnostic.S2345.severity = error # Flags enumerations should explicitly initialize all their members +dotnet_diagnostic.S2674.severity = error # The length returned from a stream read should be checked +dotnet_diagnostic.S2934.severity = error # Property assignments should not be made for "readonly" fields not constrained to reference types +dotnet_diagnostic.S2955.severity = error # Generic parameters not constrained to reference types should not be compared to "null" +dotnet_diagnostic.S3363.severity = error # Date and time should not be used as a type for primary keys +dotnet_diagnostic.S3397.severity = error # "base.Equals" should not be used to check for reference equality in "Equals" if "base" is not "object" +dotnet_diagnostic.S3456.severity = error # "string.ToCharArray()" and "ReadOnlySpan.ToArray()" should not be called redundantly +dotnet_diagnostic.S3887.severity = error # Mutable, non-private fields should not be "readonly" + +################### +# SonarAnalyzer (Sxxxx) - Blocker Vulnerability +################### +dotnet_diagnostic.S2115.severity = error # A secure password should be used when connecting to a database +dotnet_diagnostic.S2755.severity = error # XML parsers should not be vulnerable to XXE attacks +dotnet_diagnostic.S3884.severity = error # "CoSetProxyBlanket" and "CoInitializeSecurity" should not be used +dotnet_diagnostic.S6418.severity = error # Secrets should not be hard-coded + +################### +# SonarAnalyzer (Sxxxx) - Critical Vulnerability +################### +dotnet_diagnostic.S4423.severity = error # Weak SSL/TLS protocols should not be used +dotnet_diagnostic.S4426.severity = error # Cryptographic keys should be robust +dotnet_diagnostic.S4433.severity = error # LDAP connections should be authenticated +dotnet_diagnostic.S4830.severity = error # Server certificates should be verified during SSL/TLS connections +dotnet_diagnostic.S5344.severity = error # Passwords should not be stored in plaintext or with a fast hashing algorithm +dotnet_diagnostic.S5445.severity = error # Insecure temporary file creation methods should not be used +dotnet_diagnostic.S5542.severity = error # Encryption algorithms should be used with secure mode and padding scheme +dotnet_diagnostic.S5547.severity = error # Cipher algorithms should be robust +dotnet_diagnostic.S5659.severity = error # JWT should be signed and verified with strong cipher algorithms + +################### +# SonarAnalyzer (Sxxxx) - Major Vulnerability +################### +dotnet_diagnostic.S2068.severity = error # Credentials should not be hard-coded +dotnet_diagnostic.S2612.severity = error # File permissions should not be set to world-accessible values +dotnet_diagnostic.S4211.severity = error # Members should not have conflicting transparency annotations +dotnet_diagnostic.S4212.severity = error # Serialization constructors should be secured +dotnet_diagnostic.S6377.severity = error # XML signatures should be validated securely +dotnet_diagnostic.S7039.severity = error # Content Security Policies should be restrictive + +################### +# SonarAnalyzer (Sxxxx) - Blocker Code Smell +################### +dotnet_diagnostic.S1147.severity = error # Exit methods should not be called +dotnet_diagnostic.S1451.severity = none # Track lack of copyright and license headers +dotnet_diagnostic.S2178.severity = error # Short-circuit logic should be used in boolean contexts +dotnet_diagnostic.S2187.severity = error # Test classes should contain at least one test case +dotnet_diagnostic.S2306.severity = error # "async" and "await" should not be used as identifiers +dotnet_diagnostic.S2368.severity = none # Public methods should not have multidimensional array parameters — jagged arrays are the chosen layout for hot-path lookup tables +dotnet_diagnostic.S2387.severity = error # Child class fields should not shadow parent class fields +dotnet_diagnostic.S2437.severity = error # Unnecessary bit operations should not be performed +dotnet_diagnostic.S2699.severity = error # Tests should include assertions +dotnet_diagnostic.S2953.severity = error # Methods named "Dispose" should implement "IDisposable.Dispose" +dotnet_diagnostic.S2970.severity = error # Assertions should be complete +dotnet_diagnostic.S3060.severity = error # "is" should not be used with "this" +dotnet_diagnostic.S3237.severity = error # "value" contextual keyword should be used +dotnet_diagnostic.S3427.severity = error # Method overloads with default parameter values should not overlap +dotnet_diagnostic.S3433.severity = error # Test method signatures should be correct +dotnet_diagnostic.S3443.severity = error # Type should not be examined on "System.Type" instances +dotnet_diagnostic.S3875.severity = error # "operator==" should not be overloaded on reference types +dotnet_diagnostic.S3877.severity = error # Exceptions should not be thrown from unexpected methods +dotnet_diagnostic.S4462.severity = error # Calls to "async" methods should not be blocking +dotnet_diagnostic.S6422.severity = error # Calls to "async" methods should not be blocking in Azure Functions +dotnet_diagnostic.S6424.severity = error # Interfaces for durable entities should satisfy the restrictions + +################### +# SonarAnalyzer (Sxxxx) - Critical Code Smell +################### +dotnet_diagnostic.S1006.severity = error # Method overrides should not change parameter defaults +dotnet_diagnostic.S1067.severity = none # Expressions should not be too complex +dotnet_diagnostic.S1163.severity = error # Exceptions should not be thrown in finally blocks +dotnet_diagnostic.S1186.severity = none # Methods should not be empty — covered by SST1438 +dotnet_diagnostic.S121.severity = error # Control structures should use curly braces (kept over SA1503 — Sonar 20ms vs SA1503 50ms) +dotnet_diagnostic.S1215.severity = none # "GC.Collect" should not be called +dotnet_diagnostic.S126.severity = none # "if ... else if" constructs should end with "else" clauses +dotnet_diagnostic.S131.severity = none # "switch/Select" statements should contain a "default/Case Else" clauses +dotnet_diagnostic.S134.severity = none # Control flow statements "if", "switch", "for", "foreach", "while", "do" and "try" should not be nested too deeply +dotnet_diagnostic.S1541.severity = none # Methods and properties should not be too complex - covered by SST1442 +dotnet_diagnostic.S1699.severity = error # Constructors should only call non-overridable methods +dotnet_diagnostic.S1821.severity = error # "switch" statements should not be nested +dotnet_diagnostic.S1944.severity = error # Invalid casts should be avoided +dotnet_diagnostic.S1994.severity = error # "for" loop increment clauses should modify the loops' counters +dotnet_diagnostic.S2197.severity = error # Modulus results should not be checked for direct equality +dotnet_diagnostic.S2198.severity = error # Unnecessary mathematical comparisons should not be made +dotnet_diagnostic.S2223.severity = none # Non-constant static fields should not be visible - DUPLICATE CA2211 +dotnet_diagnostic.S2290.severity = error # Field-like events should not be virtual +dotnet_diagnostic.S2291.severity = error # Overflow checking should not be disabled for "Enumerable.Sum" +dotnet_diagnostic.S2302.severity = error # "nameof" should be used +dotnet_diagnostic.S2330.severity = error # Array covariance should not be used +dotnet_diagnostic.S2339.severity = error # Public constant members should not be used +dotnet_diagnostic.S2346.severity = none # Flags enumerations zero-value members should be named "None" - DUPLICATE CA1008 +dotnet_diagnostic.S2360.severity = error # Optional parameters should not be used +dotnet_diagnostic.S2365.severity = error # Properties should not make collection or array copies +dotnet_diagnostic.S2479.severity = none # Whitespace and control characters in string literals should be explicit — covered by SST1192 +dotnet_diagnostic.S2692.severity = error # "IndexOf" checks should not be for positive numbers +dotnet_diagnostic.S2696.severity = error # Instance members should not write to "static" fields +dotnet_diagnostic.S2701.severity = error # Literal boolean values should not be used in assertions +dotnet_diagnostic.S3215.severity = error # "interface" instances should not be cast to concrete types +dotnet_diagnostic.S3216.severity = error # "ConfigureAwait(false)" should be used +dotnet_diagnostic.S3217.severity = error # "Explicit" conversions of "foreach" loops should not be used +dotnet_diagnostic.S3218.severity = error # Inner class members should not shadow outer class "static" or type members +dotnet_diagnostic.S3265.severity = error # Non-flags enums should not be used in bitwise operations +dotnet_diagnostic.S3353.severity = error # Unchanged variables should be marked as "const" +dotnet_diagnostic.S3447.severity = error # "[Optional]" should not be used on "ref" or "out" parameters +dotnet_diagnostic.S3451.severity = error # "[DefaultValue]" should not be used when "[DefaultParameterValue]" is meant +dotnet_diagnostic.S3600.severity = error # "params" should not be introduced on overrides +dotnet_diagnostic.S3776.severity = none # Cognitive Complexity of methods should not be too high - covered by SST1443 +dotnet_diagnostic.S3871.severity = error # Exception types should be "public" +dotnet_diagnostic.S3874.severity = none # "out" and "ref" parameters — repo idiom is TryX(..., out T value) +dotnet_diagnostic.S3904.severity = error # Assemblies should have version information +dotnet_diagnostic.S3937.severity = error # Number patterns should be regular +dotnet_diagnostic.S3972.severity = error # Conditionals should start on new lines +dotnet_diagnostic.S3973.severity = error # A conditionally executed single line should be denoted by indentation +dotnet_diagnostic.S3998.severity = error # Threads should not lock on objects with weak identity +dotnet_diagnostic.S4000.severity = error # Pointers to unmanaged memory should not be visible +dotnet_diagnostic.S4015.severity = error # Inherited member visibility should not be decreased +dotnet_diagnostic.S4019.severity = error # Base class methods should not be hidden +dotnet_diagnostic.S4025.severity = error # Child class fields should not differ from parent class fields only by capitalization +dotnet_diagnostic.S4039.severity = none # Interface methods should be callable by derived types - DUPLICATE CA1033 +dotnet_diagnostic.S4487.severity = none # Unread "private" fields should be removed +dotnet_diagnostic.S4524.severity = error # "default" clauses should be first or last +dotnet_diagnostic.S4635.severity = error # Start index should be used instead of calling Substring +dotnet_diagnostic.S5034.severity = error # "ValueTask" should be consumed correctly +dotnet_diagnostic.S6967.severity = error # ModelState.IsValid should be called in controller actions +dotnet_diagnostic.S8367.severity = error # Identifiers should not conflict with the C# 14 "field" contextual keyword +dotnet_diagnostic.S8368.severity = error # Identifiers should not conflict with the C# 14 "extension" contextual keyword +dotnet_diagnostic.S8380.severity = error # Return types named "partial" should be escaped with "@" +dotnet_diagnostic.S8381.severity = error # "scoped" should be escaped when used as an identifier or type name in parenthesized lambda parameter lists +dotnet_diagnostic.S927.severity = none # Parameter names should match base declaration and other partial definitions - DUPLICATE CA1725 + +################### +# SonarAnalyzer (Sxxxx) - Major Code Smell +################### +dotnet_diagnostic.S103.severity = error # Lines should not be too long +dotnet_diagnostic.S104.severity = error # Files should not have too many lines of code +dotnet_diagnostic.S106.severity = error # Standard outputs should not be used directly to log anything +dotnet_diagnostic.S1066.severity = error # Mergeable "if" statements should be combined +dotnet_diagnostic.S107.severity = error # Methods should not have too many parameters +dotnet_diagnostic.S108.severity = none # Nested blocks of code should not be left empty — covered by SST1439 +dotnet_diagnostic.S109.severity = error # Magic numbers should not be used +dotnet_diagnostic.S110.severity = error # Inheritance tree of classes should not be too deep +dotnet_diagnostic.S1110.severity = error # Redundant pairs of parentheses should be removed +dotnet_diagnostic.S1117.severity = error # Local variables should not shadow class fields or properties +dotnet_diagnostic.S1118.severity = none # Utility classes should not have public constructors - DUPLICATE CA1052 +dotnet_diagnostic.S112.severity = error # General or reserved exceptions should never be thrown +dotnet_diagnostic.S1121.severity = none # Assignments should not be made from within sub-expressions — covered by SST1187 +dotnet_diagnostic.S1123.severity = error # "Obsolete" attributes should include explanations +dotnet_diagnostic.S1134.severity = error # Track uses of "FIXME" tags +dotnet_diagnostic.S1144.severity = none # Unused private types or members should be removed - DUPLICATE IDE0051 +dotnet_diagnostic.S1151.severity = error # "switch case" clauses should not have too many lines of code +dotnet_diagnostic.S1168.severity = error # Empty arrays and collections should be returned instead of null +dotnet_diagnostic.S1172.severity = error # Unused method parameters should be removed +dotnet_diagnostic.S1200.severity = none # Classes should not be coupled to too many other classes +dotnet_diagnostic.S122.severity = error # Statements should be on separate lines +dotnet_diagnostic.S125.severity = none # Sections of code should not be commented out — covered by SST1148 +dotnet_diagnostic.S127.severity = error # "for" loop stop conditions should be invariant +dotnet_diagnostic.S138.severity = error # Functions should not have too many lines of code +dotnet_diagnostic.S1479.severity = error # "switch" statements with many "case" clauses should have only one statement +dotnet_diagnostic.S1607.severity = error # Tests should not be ignored +dotnet_diagnostic.S1696.severity = error # NullReferenceException should not be caught +dotnet_diagnostic.S1854.severity = none # Unused assignments should be removed - DUPLICATE IDE0059 +dotnet_diagnostic.S1871.severity = error # Two branches in a conditional structure should not have exactly the same implementation +dotnet_diagnostic.S2139.severity = error # Exceptions should be either logged or rethrown but not both +dotnet_diagnostic.S2166.severity = none # Classes named like "Exception" should extend "Exception" or a subclass - DUPLICATE CA1710 +dotnet_diagnostic.S2234.severity = error # Arguments should be passed in the same order as the method parameters +dotnet_diagnostic.S2326.severity = error # Unused type parameters should be removed +dotnet_diagnostic.S2327.severity = error # "try" statements with identical "catch" and/or "finally" blocks should be merged +dotnet_diagnostic.S2357.severity = none # Fields should be private — duplicate of SST1401 (canonical); fields intentionally exposed (e.g. public test fields for reflection) already carry per-site SST1401 suppressions +dotnet_diagnostic.S2372.severity = error # Exceptions should not be thrown from property getters +dotnet_diagnostic.S2376.severity = error # Write-only properties should not be used +dotnet_diagnostic.S2629.severity = error # Logging templates should be constant +dotnet_diagnostic.S2681.severity = error # Multiline blocks should be enclosed in curly braces +dotnet_diagnostic.S2743.severity = none # Static fields should not be used in generic types - DUPLICATE CA1000 — covered by SST1431 +dotnet_diagnostic.S2925.severity = error # "Thread.Sleep" should not be used in tests +dotnet_diagnostic.S2933.severity = none # Fields that are only assigned in the constructor should be "readonly" - DUPLICATE IDE0044 +dotnet_diagnostic.S2971.severity = error # LINQ expressions should be simplified +dotnet_diagnostic.S3010.severity = error # Static fields should not be updated in constructors +dotnet_diagnostic.S3011.severity = error # Reflection should not be used to increase accessibility of classes, methods, or fields +dotnet_diagnostic.S3059.severity = none # Types should not have members with visibility set higher than the type's visibility +dotnet_diagnostic.S3063.severity = error # "StringBuilder" data should be used +dotnet_diagnostic.S3169.severity = error # Multiple "OrderBy" calls should not be used +dotnet_diagnostic.S3246.severity = error # Generic type parameters should be co/contravariant when possible +dotnet_diagnostic.S3262.severity = error # "params" should be used on overrides +dotnet_diagnostic.S3264.severity = error # Events should be invoked +dotnet_diagnostic.S3358.severity = none # Ternary operators should not be nested — covered by SST1147 +dotnet_diagnostic.S3366.severity = error # "this" should not be exposed from constructors +dotnet_diagnostic.S3415.severity = error # Assertion arguments should be passed in the correct order +dotnet_diagnostic.S3431.severity = error # "[ExpectedException]" should not be used +dotnet_diagnostic.S3442.severity = none # "abstract" classes should not have "public" constructors — covered by SST1428 +dotnet_diagnostic.S3445.severity = none # Exceptions should not be explicitly rethrown — covered by SST1430 +dotnet_diagnostic.S3457.severity = error # Composite format strings should be used correctly +dotnet_diagnostic.S3597.severity = error # "ServiceContract" and "OperationContract" attributes should be used together +dotnet_diagnostic.S3880.severity = none # Finalizers should not be empty — covered by SST1434 +dotnet_diagnostic.S3881.severity = error # "IDisposable" should be implemented correctly +dotnet_diagnostic.S3885.severity = error # "Assembly.Load" should be used +dotnet_diagnostic.S3898.severity = error # Value types should implement "IEquatable" +dotnet_diagnostic.S3902.severity = error # "Assembly.GetExecutingAssembly" should not be called +dotnet_diagnostic.S3906.severity = error # Event Handlers should have the correct signature +dotnet_diagnostic.S3908.severity = error # Generic event handlers should be used +dotnet_diagnostic.S3909.severity = error # Collections should implement the generic interface +dotnet_diagnostic.S3925.severity = none # "ISerializable" should be implemented correctly - BinaryFormatter / ISerializable serialization is obsoleted (SYSLIB0050/0051) in modern .NET; we do not opt into legacy serialization for any exception type +dotnet_diagnostic.S3928.severity = none # Parameter names used into ArgumentException constructors should match an existing one - DUPLICATE CA2208 +dotnet_diagnostic.S3956.severity = none # "Generic.List" instances should not be part of public APIs +dotnet_diagnostic.S3971.severity = error # "GC.SuppressFinalize" should not be called +dotnet_diagnostic.S3990.severity = error # Assemblies should be marked as CLS compliant +dotnet_diagnostic.S3992.severity = error # Assemblies should explicitly specify COM visibility +dotnet_diagnostic.S3993.severity = error # Custom attributes should be marked with "System.AttributeUsageAttribute" +dotnet_diagnostic.S3994.severity = none # URI Parameters should not be strings +dotnet_diagnostic.S3995.severity = none # URI return values should not be strings +dotnet_diagnostic.S3996.severity = none # URI properties should not be strings +dotnet_diagnostic.S3997.severity = error # String URI overloads should call "System.Uri" overloads +dotnet_diagnostic.S4002.severity = error # Disposable types should declare finalizers +dotnet_diagnostic.S4004.severity = error # Collection properties should be readonly +dotnet_diagnostic.S4005.severity = none # "System.Uri" arguments should be used instead of strings +dotnet_diagnostic.S4016.severity = error # Enumeration members should not be named "Reserved" +dotnet_diagnostic.S4017.severity = none # Method signatures should not contain nested generic types +dotnet_diagnostic.S4035.severity = error # Classes implementing "IEquatable" should be sealed +dotnet_diagnostic.S4050.severity = error # Operators should be overloaded consistently +dotnet_diagnostic.S4055.severity = none # Literals should not be passed as localized parameters +dotnet_diagnostic.S4057.severity = error # Locales should be set for data types +dotnet_diagnostic.S4059.severity = none # Property names should not match get methods - DUPLICATE CA1721 +dotnet_diagnostic.S4070.severity = error # Non-flags enums should not be marked with "FlagsAttribute" +dotnet_diagnostic.S4144.severity = error # Methods should not have identical implementations +dotnet_diagnostic.S4200.severity = error # Native methods should be wrapped +dotnet_diagnostic.S4214.severity = none # "P/Invoke" methods should not be visible - DUPLICATE CA1401 +dotnet_diagnostic.S4220.severity = error # Events should have proper arguments +dotnet_diagnostic.S4456.severity = error # Parameter validation in yielding methods should be wrapped +dotnet_diagnostic.S4457.severity = none # Parameter validation in "async"/"await" methods should be wrapped +dotnet_diagnostic.S4545.severity = error # "DebuggerDisplayAttribute" strings should reference existing members +dotnet_diagnostic.S4581.severity = error # "new Guid()" should not be used +dotnet_diagnostic.S6354.severity = error # Use a testable date/time provider +dotnet_diagnostic.S6419.severity = error # Azure Functions should be stateless +dotnet_diagnostic.S6420.severity = error # Client instances should not be recreated on each Azure Function invocation +dotnet_diagnostic.S6421.severity = error # Azure Functions should use Structured Error Handling +dotnet_diagnostic.S6423.severity = error # Azure Functions should log all failures +dotnet_diagnostic.S6561.severity = error # Avoid using "DateTime.Now" for benchmarking or timing operations +dotnet_diagnostic.S6562.severity = error # Always set the "DateTimeKind" when creating new "DateTime" instances +dotnet_diagnostic.S6563.severity = error # Use UTC when recording DateTime instants +dotnet_diagnostic.S6566.severity = error # Use "DateTimeOffset" instead of "DateTime" +dotnet_diagnostic.S6575.severity = error # Use "TimeZoneInfo.FindSystemTimeZoneById" without converting the timezones with "TimezoneConverter" +dotnet_diagnostic.S6580.severity = none # Use a format provider when parsing date and time - DUPLICATE CA1305 +dotnet_diagnostic.S6673.severity = error # Log message template placeholders should be in the right order +dotnet_diagnostic.S6802.severity = error # Using lambda expressions in loops should be avoided in Blazor markup section +dotnet_diagnostic.S6803.severity = error # Parameters with SupplyParameterFromQuery attribute should be used only in routable components +dotnet_diagnostic.S6931.severity = error # ASP.NET controller actions should not have a route template starting with "/" +dotnet_diagnostic.S6932.severity = error # Use model binding instead of reading raw request data +dotnet_diagnostic.S6934.severity = error # A Route attribute should be added to the controller when a route template is specified at the action level +dotnet_diagnostic.S6960.severity = error # Controllers should not have mixed responsibilities +dotnet_diagnostic.S6961.severity = error # API Controllers should derive from ControllerBase instead of Controller +dotnet_diagnostic.S6962.severity = error # You should pool HTTP connections with HttpClientFactory +dotnet_diagnostic.S6964.severity = error # Value type property used as input in a controller action should be nullable, required or annotated with the JsonRequiredAttribute to avoid under-posting. +dotnet_diagnostic.S6965.severity = error # REST API actions should be annotated with an HTTP verb attribute +dotnet_diagnostic.S6966.severity = error # Awaitable method should be used +dotnet_diagnostic.S6968.severity = error # Actions that return a value should be annotated with ProducesResponseTypeAttribute containing the return type +dotnet_diagnostic.S881.severity = error # Increment (++) and decrement (--) operators should not be used in a method call or mixed with other operators in an expression +dotnet_diagnostic.S907.severity = error # "goto" statement should not be used + +################### +# SonarAnalyzer (Sxxxx) - Minor Code Smell +################### +dotnet_diagnostic.S100.severity = error # Methods and properties should be named in PascalCase (kept over SA1300 — S100+S101 47ms vs SA1300 101ms) +dotnet_diagnostic.S101.severity = error # Types should be named in PascalCase (kept over SA1300 — see S100) +dotnet_diagnostic.S105.severity = error # Tabulation characters should not be used +dotnet_diagnostic.S1104.severity = none # Fields should not have public accessibility - DUPLICATE CA1051 +dotnet_diagnostic.S1109.severity = error # A close curly brace should be located at the beginning of a line +dotnet_diagnostic.S1116.severity = none # Empty statements should be removed - DUPLICATE SA1106 +dotnet_diagnostic.S1125.severity = none # Boolean literals should not be redundant — covered by SST1182 +dotnet_diagnostic.S1128.severity = error # Unnecessary "using" should be removed (kept over IDE0005 — Sonar 551ms vs IDE0005 2.364s) +dotnet_diagnostic.S113.severity = none # Files should end with a newline +dotnet_diagnostic.S1155.severity = error # "Any()" should be used to test for emptiness +dotnet_diagnostic.S1185.severity = none # Overriding members should do more than simply call the same member in the base class - DUPLICATE RCS1132 +dotnet_diagnostic.S1192.severity = error # String literals should not be duplicated +dotnet_diagnostic.S1199.severity = error # Nested code blocks should not be used +dotnet_diagnostic.S1210.severity = none # "Equals" and the comparison operators should be overridden when implementing "IComparable" - DUPLICATE CA1036 +dotnet_diagnostic.S1227.severity = none # break statements should not be used except for switch cases +dotnet_diagnostic.S1264.severity = error # A "while" loop should be used instead of a "for" loop +dotnet_diagnostic.S1301.severity = none # "switch" statements should have at least 3 "case" clauses +dotnet_diagnostic.S1312.severity = none # Logger fields should be "private static readonly" +dotnet_diagnostic.S1449.severity = error # Culture should be specified for "string" operations +dotnet_diagnostic.S1450.severity = error # Private fields only used as local variables in methods should become local variables +dotnet_diagnostic.S1481.severity = error # Unused local variables should be removed +dotnet_diagnostic.S1643.severity = error # Strings should not be concatenated using '+' in a loop +dotnet_diagnostic.S1659.severity = error # Multiple variables should not be declared on the same line +dotnet_diagnostic.S1694.severity = error # An abstract class should have both abstract and concrete methods +dotnet_diagnostic.S1698.severity = error # "==" should not be used when "Equals" is overridden +dotnet_diagnostic.S1858.severity = error # "ToString()" calls should not be redundant +dotnet_diagnostic.S1905.severity = none # Redundant casts should not be used - DUPLICATE IDE0004 — covered by SST1175 +dotnet_diagnostic.S1939.severity = error # Inheritance list should not be redundant +dotnet_diagnostic.S1940.severity = error # Boolean checks should not be inverted +dotnet_diagnostic.S2094.severity = none # Classes should not be empty — covered by SST1436 +dotnet_diagnostic.S2148.severity = none # Underscores should be used to make large numbers readable — covered by SST1191 +dotnet_diagnostic.S2156.severity = error # "sealed" classes should not have "protected" members +dotnet_diagnostic.S2219.severity = error # Runtime type checking should be simplified +dotnet_diagnostic.S2221.severity = none # "Exception" should not be caught +dotnet_diagnostic.S2292.severity = error # Trivial properties should be auto-implemented +dotnet_diagnostic.S2325.severity = none # Methods and properties that don't access instance data should be static - DUPLICATE CA1822 +dotnet_diagnostic.S2333.severity = error # Redundant modifiers should not be used +dotnet_diagnostic.S2342.severity = error # Enumeration types should comply with a naming convention +dotnet_diagnostic.S2344.severity = none # Enumeration type names should not have "Flags" or "Enum" suffixes - DUPLICATE CA1711 +dotnet_diagnostic.S2386.severity = error # Mutable fields should not be "public static" +dotnet_diagnostic.S2486.severity = none # Generic exceptions should not be ignored — covered by SST1429 +dotnet_diagnostic.S2737.severity = error # "catch" clauses should do more than rethrow +dotnet_diagnostic.S2760.severity = error # Sequential tests should not check the same condition +dotnet_diagnostic.S3052.severity = error # Members should not be initialized to default values +dotnet_diagnostic.S3220.severity = error # Method calls should not resolve ambiguously to overloads with "params" +dotnet_diagnostic.S3234.severity = error # "GC.SuppressFinalize" should not be invoked for types without destructors +dotnet_diagnostic.S3235.severity = error # Redundant parentheses should not be used +dotnet_diagnostic.S3236.severity = error # Caller information arguments should not be provided explicitly +dotnet_diagnostic.S3240.severity = none # The simplest possible condition syntax should be used - duplicate of SST1198 +dotnet_diagnostic.S3241.severity = error # Methods should not return values that are never used +dotnet_diagnostic.S3242.severity = none # Method parameters should be declared with base types +dotnet_diagnostic.S3247.severity = error # Duplicate casts should not be made +dotnet_diagnostic.S3251.severity = error # Implementations should be provided for "partial" methods +dotnet_diagnostic.S3253.severity = none # Constructor and destructor declarations should not be redundant — covered by SST1433 +dotnet_diagnostic.S3254.severity = error # Default parameter values should not be passed as arguments +dotnet_diagnostic.S3256.severity = error # "string.IsNullOrEmpty" should be used +dotnet_diagnostic.S3257.severity = error # Declarations and initializations should be as concise as possible +dotnet_diagnostic.S3260.severity = error # Non-derived "private" classes and records should be "sealed" +dotnet_diagnostic.S3261.severity = none # Namespaces should not be empty — covered by SST1435 +dotnet_diagnostic.S3267.severity = none # Loops should be simplified with "LINQ" expressions +dotnet_diagnostic.S3376.severity = none # Attribute, EventArgs, and Exception type names should end with the type being extended - DUPLICATE CA1710 +dotnet_diagnostic.S3398.severity = error # "private" methods called only by inner classes should be moved to those classes +dotnet_diagnostic.S3400.severity = error # Methods should not return constants +dotnet_diagnostic.S3416.severity = error # Loggers should be named for their enclosing types +dotnet_diagnostic.S3440.severity = error # Variables should not be checked against the values they're about to be assigned +dotnet_diagnostic.S3441.severity = error # Redundant property names should be omitted in anonymous classes +dotnet_diagnostic.S3444.severity = error # Interfaces should not simply inherit from base interfaces with colliding members +dotnet_diagnostic.S3450.severity = error # Parameters with "[DefaultParameterValue]" attributes should also be marked "[Optional]" +dotnet_diagnostic.S3458.severity = error # Empty "case" clauses that fall through to the "default" should be omitted +dotnet_diagnostic.S3459.severity = error # Unassigned members should be removed +dotnet_diagnostic.S3532.severity = error # Empty "default" clauses should be removed +dotnet_diagnostic.S3604.severity = error # Member initializer values should not be redundant +dotnet_diagnostic.S3626.severity = none # Jump statements should not be redundant — covered by SST1174 +dotnet_diagnostic.S3717.severity = error # Track use of "NotImplementedException" +dotnet_diagnostic.S3872.severity = error # Parameter names should not duplicate the names of their methods +dotnet_diagnostic.S3876.severity = error # Strings or integral types should be used for indexers +dotnet_diagnostic.S3878.severity = error # Arrays should not be created for params parameters +dotnet_diagnostic.S3897.severity = error # Classes that provide "Equals()" should implement "IEquatable" +dotnet_diagnostic.S3962.severity = none # "static readonly" constants should be "const" instead - DUPLICATE CA1802 +dotnet_diagnostic.S3963.severity = none # "static" fields should be initialized inline - DUPLICATE CA1810 +dotnet_diagnostic.S3967.severity = none # Multidimensional arrays should not be used - DUPLICATE CA1814 +dotnet_diagnostic.S4018.severity = error # All type parameters should be used in the parameter list to enable type inference +dotnet_diagnostic.S4022.severity = error # Enumerations should have "Int32" storage +dotnet_diagnostic.S4023.severity = none # Interfaces should not be empty — covered by SST1437 +dotnet_diagnostic.S4026.severity = error # Assemblies should be marked with "NeutralResourcesLanguageAttribute" +dotnet_diagnostic.S4027.severity = error # Exceptions should provide standard constructors +dotnet_diagnostic.S4040.severity = none # Strings should be normalized to uppercase - DUPLICATE CA1308 +dotnet_diagnostic.S4041.severity = none # Type names should not match namespaces - DUPLICATE CA1724 +dotnet_diagnostic.S4047.severity = error # Generics should be used when appropriate +dotnet_diagnostic.S4049.severity = none # Properties should be preferred - DUPLICATE CA1024 +dotnet_diagnostic.S4052.severity = error # Types should not extend outdated base types +dotnet_diagnostic.S4056.severity = none # Overloads with a "CultureInfo" or an "IFormatProvider" parameter should be used - DUPLICATE CA1305 +dotnet_diagnostic.S4058.severity = error # Overloads with a "StringComparison" parameter should be used +dotnet_diagnostic.S4060.severity = none # Non-abstract attributes should be sealed - DUPLICATE CA1813 +dotnet_diagnostic.S4061.severity = error # "params" should be used instead of "varargs" +dotnet_diagnostic.S4069.severity = none # Operator overloads should have named alternatives - DUPLICATE CA2225 +dotnet_diagnostic.S4136.severity = error # Method overloads should be grouped together +dotnet_diagnostic.S4201.severity = error # Null checks should not be combined with "is" operator checks +dotnet_diagnostic.S4225.severity = error # Extension methods should not extend "object" +dotnet_diagnostic.S4226.severity = none # Extensions should be in separate namespaces +dotnet_diagnostic.S4261.severity = none # Methods should be named according to their synchronicities - Async suffix not used +dotnet_diagnostic.S4663.severity = error # Comments should not be empty +dotnet_diagnostic.S6513.severity = none # "ExcludeFromCodeCoverage" attributes should include a justification - not available on net462 and older TFMs +dotnet_diagnostic.S6585.severity = error # Don't hardcode the format when turning dates and times to strings +dotnet_diagnostic.S6588.severity = error # Use the "UnixEpoch" field instead of creating "DateTime" instances that point to the beginning of the Unix epoch +dotnet_diagnostic.S6602.severity = none # "Find" method should be used instead of the "FirstOrDefault" extension - DUPLICATE CA1826 +dotnet_diagnostic.S6603.severity = error # The collection-specific "TrueForAll" method should be used instead of the "All" extension +dotnet_diagnostic.S6605.severity = error # Collection-specific "Exists" method should be used instead of the "Any" extension +dotnet_diagnostic.S6607.severity = error # The collection should be filtered before sorting by using "Where" before "OrderBy" +dotnet_diagnostic.S6608.severity = none # Prefer indexing instead of "Enumerable" methods on types implementing "IList" - DUPLICATE CA1826 +dotnet_diagnostic.S6609.severity = error # "Min/Max" properties of "Set" types should be used instead of the "Enumerable" extension methods +dotnet_diagnostic.S6610.severity = error # "StartsWith" and "EndsWith" overloads that take a "char" should be used instead of the ones that take a "string" +dotnet_diagnostic.S6612.severity = error # The lambda parameter should be used instead of capturing arguments in "ConcurrentDictionary" methods +dotnet_diagnostic.S6613.severity = error # "First" and "Last" properties of "LinkedList" should be used instead of the "First()" and "Last()" extension methods +dotnet_diagnostic.S6617.severity = error # "Contains" should be used instead of "Any" for simple equality checks +dotnet_diagnostic.S6618.severity = error # "string.Create" should be used instead of "FormattableString" +dotnet_diagnostic.S6664.severity = error # The code block contains too many logging calls +dotnet_diagnostic.S6667.severity = error # Logging in a catch clause should pass the caught exception as a parameter. +dotnet_diagnostic.S6668.severity = error # Logging arguments should be passed to the correct parameter +dotnet_diagnostic.S6669.severity = error # Logger field or property name should comply with a naming convention +dotnet_diagnostic.S6670.severity = error # "Trace.Write" and "Trace.WriteLine" should not be used +dotnet_diagnostic.S6672.severity = error # Generic logger injection should match enclosing type +dotnet_diagnostic.S6675.severity = error # "Trace.WriteLineIf" should not be used with "TraceSwitch" levels +dotnet_diagnostic.S6678.severity = error # Use PascalCase for named placeholders +dotnet_diagnostic.S818.severity = error # Literal suffixes should be upper case + +################### +# SonarAnalyzer (Sxxxx) - Info Code Smell +################### +dotnet_diagnostic.S1133.severity = error # Deprecated code should be removed +dotnet_diagnostic.S1135.severity = error # Track uses of "TODO" tags +dotnet_diagnostic.S1309.severity = none # Track uses of in-source issue suppressions + +################### +# SonarAnalyzer (Sxxxx) - Uncategorized +################### +dotnet_diagnostic.S9999-cpd.severity = error # Copy-paste token calculator +dotnet_diagnostic.S9999-log.severity = error # Log generator +dotnet_diagnostic.S9999-metadata.severity = error # File metadata generator +dotnet_diagnostic.S9999-metrics.severity = error # Metrics calculator +dotnet_diagnostic.S9999-symbolRef.severity = error # Symbol reference calculator +dotnet_diagnostic.S9999-telemetry.severity = error # Telemetry generator +dotnet_diagnostic.S9999-testMethodDeclaration.severity = error # Test method declarations generator +dotnet_diagnostic.S9999-token-type.severity = error # Token type calculator +dotnet_diagnostic.S9999-warning.severity = error # Analysis Warning generator + +################### +# SonarAnalyzer (Sxxxx) - Critical Security Hotspot +################### +dotnet_diagnostic.S2245.severity = none # Using pseudorandom number generators (PRNGs) is security-sensitive - DUPLICATE CA5394 +dotnet_diagnostic.S2257.severity = error # Using non-standard cryptographic algorithms is security-sensitive +dotnet_diagnostic.S4502.severity = error # Disabling CSRF protections is security-sensitive +dotnet_diagnostic.S4790.severity = error # Using weak hashing algorithms is security-sensitive +dotnet_diagnostic.S4792.severity = error # Configuring loggers is security-sensitive +dotnet_diagnostic.S5042.severity = error # Expanding archive files without controlling resource consumption is security-sensitive +dotnet_diagnostic.S5332.severity = error # Using clear-text protocols is security-sensitive +dotnet_diagnostic.S5443.severity = error # Using publicly writable directories is security-sensitive + +################### +# SonarAnalyzer (Sxxxx) - Major Security Hotspot +################### +dotnet_diagnostic.S1313.severity = error # Using hardcoded IP addresses is security-sensitive +dotnet_diagnostic.S2077.severity = error # Formatting SQL queries is security-sensitive +dotnet_diagnostic.S5693.severity = error # Allowing requests with excessive content length is security-sensitive +dotnet_diagnostic.S5753.severity = error # Disabling ASP.NET "Request Validation" feature is security-sensitive +dotnet_diagnostic.S5766.severity = error # Creating Serializable objects without data validation checks is security-sensitive +dotnet_diagnostic.S6444.severity = error # Not specifying a timeout for regular expressions is security-sensitive +dotnet_diagnostic.S6640.severity = error # Using unsafe code blocks is security-sensitive + +################### +# SonarAnalyzer (Sxxxx) - Minor Security Hotspot +################### +dotnet_diagnostic.S2092.severity = error # Creating cookies without the "secure" flag is security-sensitive +dotnet_diagnostic.S3330.severity = error # Creating cookies without the "HttpOnly" flag is security-sensitive +dotnet_diagnostic.S4507.severity = error # Delivering code in production with debug features activated is security-sensitive +dotnet_diagnostic.S5122.severity = error # Having a permissive Cross-Origin Resource Sharing policy is security-sensitive + +############################################# +# JetBrains ReSharper / Rider Inspections +############################################# +# Mirrors the CA / SA / RCS choices above for Rider and ReSharper users. Only +# inspections with a clear mapping to our Roslyn analyzer rules are listed; +# every other ReSharper inspection keeps its default severity. The goal is to +# avoid Rider double-reporting things the Roslyn analyzers already catch and +# to keep IDE severities in sync with CI-enforced severities. + +################### +# ReSharper - Layout / Formatting (covered by StyleCop SA) +################### +resharper_redundant_linebreak_highlighting = none # covered by SA layout rules +resharper_missing_linebreak_highlighting = none # covered by SA layout rules +resharper_bad_empty_braces_line_breaks_highlighting = none # covered by SA1500 +resharper_missing_indent_highlighting = none # covered by editorconfig indent_size +resharper_missing_blank_lines_highlighting = none # covered by SA1516 / SA1517 +resharper_wrong_indent_size_highlighting = none # covered by editorconfig indent_size +resharper_bad_indent_highlighting = none # covered by editorconfig indent_size +resharper_bad_expression_braces_line_breaks_highlighting = none # covered by SA layout rules +resharper_multiple_spaces_highlighting = none # covered by SA1025 +resharper_bad_expression_braces_indent_highlighting = none # covered by SA layout rules +resharper_bad_control_braces_indent_highlighting = none # covered by SA layout rules +resharper_bad_preprocessor_indent_highlighting = none # covered by SA layout rules +resharper_redundant_blank_lines_highlighting = none # covered by SA1516 / SA1507 +resharper_multiple_statements_on_one_line_highlighting = none # covered by SA1119 / SA1503 +resharper_bad_braces_spaces_highlighting = none # covered by SA1012 / SA1013 +resharper_outdent_is_off_prev_level_highlighting = none # editorconfig indentation +resharper_bad_symbol_spaces_highlighting = none # covered by SA spacing rules +resharper_bad_colon_spaces_highlighting = none # covered by SA1024 +resharper_bad_semicolon_spaces_highlighting = none # covered by SA1002 +resharper_bad_square_brackets_spaces_highlighting = none # covered by SA1010 / SA1011 +resharper_bad_parens_spaces_highlighting = none # covered by SA1008 / SA1009 + +################### +# ReSharper - 'this' qualifier (matches SA1101 = none) +################### +resharper_redundant_this_qualifier_highlighting = none # we don't follow the this. prefix style +resharper_arrange_this_qualifier_highlighting = none # we don't follow the this. prefix style +resharper_arrange_default_access_modifier_highlighting = none # SA1400 requires explicit access modifiers — don't suggest removing them +resharper_redundant_default_access_modifier_highlighting = none # SA1400 requires explicit access modifiers — don't suggest removing them +resharper_redundant_access_modifier_highlighting = none # access modifiers are deliberate and SA1400 requires them +resharper_arrange_type_member_modifiers_highlighting = none # SA1206 handles modifier ordering; don't duplicate +resharper_redundant_empty_switch_section_highlighting = none # handled by RCS1070 + +################### +# ReSharper - Redundancies / cleanup +################### +resharper_redundant_using_directive_highlighting = error # IDE0005 may not fire at compile time reliably — keep Rider nagging +resharper_redundant_qualifier_highlighting = error # IDE0001 may not fire at compile time reliably — keep Rider nagging +resharper_redundant_name_qualifier_highlighting = error # IDE0001 may not fire at compile time reliably — keep Rider nagging +resharper_redundant_base_qualifier_highlighting = none # IDE0009 is disabled (SA1101 = none) — don't nag in Rider either +resharper_redundant_cast_highlighting = none # handled by RCS1151 +resharper_redundant_explicit_array_creation_highlighting = none # handled by RCS1014 +resharper_redundant_empty_object_creation_argument_list_highlighting = none # handled by RCS1039 +resharper_redundant_lambda_signature_parentheses_highlighting = error # IDE0063 may not fire at compile time reliably — keep Rider nagging +resharper_redundant_default_member_initializer_highlighting = none # handled by RCS1188 +resharper_redundant_field_initializer_highlighting = none # handled by RCS1129 +resharper_redundant_overriding_member_highlighting = none # handled by RCS1132 +resharper_redundant_verbatim_string_prefix_highlighting = none # handled by RCS1192 +resharper_redundant_string_interpolation_highlighting = none # handled by RCS1105 / RCS1214 +resharper_redundant_to_string_call_highlighting = none # handled by RCS1097 + +################### +# ReSharper - Naming (matches SA/CA naming rules) +################### +resharper_inconsistent_naming_highlighting = none # too many false positives on test/fixture naming; SA1300 family covers the real cases + +################### +# ReSharper - Unused code (matches CA1801 / CA1823 / IDE0051-0052) +################### +resharper_unused_parameter_local_highlighting = none # handled by CA1801 (private/internal surface) +resharper_unused_parameter_global_highlighting = none # public-API parameters can be unused for compat +resharper_unused_member_local_highlighting = error # IDE0051 may not fire at compile time reliably — keep Rider nagging +resharper_unused_member_global_highlighting = suggestion # public-API members may be intentionally exposed +resharper_unused_field_local_highlighting = none # handled by CA1823 +resharper_unused_field_global_highlighting = none # public-API fields may be intentionally exposed +resharper_unused_type_local_highlighting = error # IDE0052 may not fire at compile time reliably — keep Rider nagging +resharper_unused_type_global_highlighting = suggestion # public-API types may be intentionally exposed +resharper_unused_variable_highlighting = none # handled by CS0219 +resharper_unused_auto_property_accessor_local_highlighting = none # data-class setters are intentional even when unused locally +resharper_unused_auto_property_accessor_global_highlighting = none # public auto-properties may be intentionally exposed + +################### +# ReSharper - Code quality (matches CA/RCS semantic analyzers) +################### +resharper_class_never_instantiated_local_highlighting = none # handled by CA1812 +resharper_class_never_instantiated_global_highlighting = suggestion # public types may be instantiated externally +resharper_class_cannot_be_instantiated_highlighting = none # handled by CA1052 +resharper_class_can_be_sealed_local_highlighting = none # handled by CA1852 (private/internal surface) +resharper_class_can_be_sealed_global_highlighting = none # public types stay open for inheritance +resharper_use_nameof_expression_highlighting = none # handled by CA1507 / RCS1015 +resharper_possible_null_reference_exception_highlighting = none # handled bynullability analyzer +resharper_possible_invalid_operation_exception_highlighting = none # handled bynullability analyzer +resharper_possible_multiple_enumeration_highlighting = none # handled by CA1851 +resharper_possible_mistaken_argument_null_check_highlighting = none # handled by CA1062 +resharper_convert_to_constant_local_highlighting = none # handled by RCS1118 +resharper_convert_to_static_class_highlighting = none # handled by CA1052 +resharper_convert_to_auto_property_highlighting = none # handled by RCS1085 +resharper_convert_to_auto_property_when_possible_highlighting = none # handled by RCS1085 +resharper_convert_to_auto_property_with_private_setter_highlighting = none # handled by RCS1085 +resharper_auto_property_can_be_made_get_only_local_highlighting = none # handled by RCS1170 +resharper_auto_property_can_be_made_get_only_global_highlighting = suggestion # public setters kept for API consumers +resharper_member_can_be_made_static_local_highlighting = none # handled by CA1822 (private/internal surface) +resharper_member_can_be_made_static_global_highlighting = none # public members kept virtual for inheritance +resharper_member_can_be_private_local_highlighting = none # access modifiers are deliberate — don't suggest tightening +resharper_method_has_async_overload_highlighting = none # sync call sites are intentional (e.g. test setup, disposal, sample code) +resharper_member_can_be_private_global_highlighting = none # access modifiers are deliberate — don't suggest tightening +resharper_field_can_be_made_read_only_local_highlighting = none # handled by RCS1169 +resharper_field_can_be_made_read_only_global_highlighting = none # handled by RCS1169 +resharper_merge_into_pattern_highlighting = none # handled by RCS1220 +resharper_merge_conditional_expression_highlighting = none # handled by RCS1173 +resharper_merge_sequential_checks_highlighting = none # handled by RCS1206 +resharper_simplify_conditional_ternary_expression_highlighting = none # handled by RCS1049 / RCS1104 +resharper_simplify_linq_expression_highlighting = error # handled by RCS1077 +resharper_string_compare_to_is_culture_specific_highlighting = none # handled by CA1310 +resharper_string_ends_with_is_culture_specific_highlighting = none # handled by CA1310 +resharper_string_index_of_is_culture_specific_1_highlighting = none # handled by CA1310 +resharper_string_index_of_is_culture_specific_2_highlighting = none # handled by CA1310 +resharper_string_index_of_is_culture_specific_3_highlighting = none # handled by CA1310 +resharper_string_last_index_of_is_culture_specific_1_highlighting = none # handled by CA1310 +resharper_string_last_index_of_is_culture_specific_2_highlighting = none # handled by CA1310 +resharper_string_last_index_of_is_culture_specific_3_highlighting = none # handled by CA1310 +resharper_string_starts_with_is_culture_specific_highlighting = none # handled by CA1310 +resharper_specify_a_culture_in_string_conversion_explicitly_highlighting = none # handled by CA1304 + +################### +# ReSharper - Control flow / nesting +################### +resharper_invert_if_highlighting = suggestion # related to RCS1208 — Rider is more aggressive +resharper_tail_recursive_call_highlighting = suggestion + +################### +# ReSharper - Documentation (covered by StyleCop SA1600 family) +################### +resharper_missing_xml_doc_highlighting = none # covered by SA1600 family +resharper_missing_xml_doc_global_highlighting = none # covered by SA1600 family +resharper_invalid_xml_doc_comment_highlighting = none # handled by SA1612 / SA1613 + +################### +# ReSharper - Typo / spelling (not enforced) +################### +resharper_comment_typo_highlighting = none # typo checks are out of scope +resharper_string_literal_typo_highlighting = none # typo checks are out of scope +resharper_identifier_typo_highlighting = none # typo checks are out of scope +resharper_markup_text_typo_highlighting = none # typo checks are out of scope + +############################################# +# Other Settings +############################################# +vsspell_dictionary_languages = en-US + ############################################# # C++ Files ############################################# @@ -963,7 +2339,18 @@ end_of_line = lf [*.{cmd, bat}] end_of_line = crlf + ############################################# -# Other Settings +# Test projects (TUnit) ############################################# -vsspell_dictionary_languages = en-US +# TUnit instantiates test classes per test, so they must remain instance classes and +# cannot be marked static — even when a partial declaration happens to hold only static +# members (the instance [Test] methods live in sibling partial files). +[**/tests/**/*.cs] +stylesharp.avoid_linq_on_hot_path = false +dotnet_diagnostic.SST2229.severity = error # In tests, simplify LINQ Where plus terminal calls instead of banning LINQ +dotnet_diagnostic.SST2230.severity = error # In tests, simplify LINQ type filters instead of banning LINQ +dotnet_diagnostic.SST2233.severity = none # Tests can use LINQ for clarity +dotnet_diagnostic.SST1432.severity = none +dotnet_diagnostic.RCS1072.severity = none # covered by SST1435 +dotnet_diagnostic.RCS1106.severity = none # covered by SST1434 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2a6472e..aaa2dc1 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,7 +1,7 @@ This repository’s canonical agent guidance has been consolidated into: -- `agent.md` in the root folder or relative path is `../agent.md` to this file. +- `agents.md` in the root folder or relative path is `../agents.md` to this file. -GitHub Copilot (and other coding agents) should read and follow `agent.md` for build/test commands, repository architecture context, coding standards, and AOT requirements. +GitHub Copilot (and other coding agents) should read and follow `agents.md` for build/test commands, repository architecture context, coding standards, and AOT requirements. -If there is any conflict between this file and `agent.md`, follow **agent.md**. +If there is any conflict between this file and `agents.md`, follow **agents.md**. diff --git a/agent.md b/AGENTS.md similarity index 98% rename from agent.md rename to AGENTS.md index 19c0054..b28de08 100644 --- a/agent.md +++ b/AGENTS.md @@ -1,4 +1,4 @@ -# agent.md +# AGENTS.md This file is the single source of truth for AI/agent assistance in this repository (Claude Code, GitHub Copilot, and other coding agents). It consolidates build/test commands, architecture context, coding standards, and AOT guidance. @@ -373,3 +373,8 @@ _total = this.WhenAnyValue( * Expression trees in hot paths without caching * Platform-specific code in `src/Extensions.Hosting.SingleInstance/` core library * Breaking public APIs without proper versioning and documentation + + +Follow the canonical repository agent guidance in @agent.md. + +If anything in this file conflicts with @agent.md, follow @agent.md. diff --git a/CLAUDE.md b/CLAUDE.md index c37cbd2..30d1bba 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,5 +1,5 @@ # CLAUDE.md -Follow the canonical repository agent guidance in @agent.md. +Follow the canonical repository agent guidance in @agents.md. -If anything in this file conflicts with @agent.md, follow @agent.md. +If anything in this file conflicts with @agents.md, follow @agents.md. diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..fb648d3 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,138 @@ + + + true + true + 8.0.28 + 9.0.17 + 10.0.9 + 11.0.0-preview.5.26302.115 + 9.0.120 + 10.0.71 + 11.0.0-preview.5.26304.4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/_build.csproj b/build/_build.csproj index 5e2abc1..0f27490 100644 --- a/build/_build.csproj +++ b/build/_build.csproj @@ -1,4 +1,4 @@ - + Exe @@ -12,13 +12,13 @@ - - - + + + - + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index f0f4f2e..6f90fd7 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -13,7 +13,7 @@ enable logo.png README.md - Compatability with Net 8 / 9 / 10 and net462 / net472 / net481 + Compatability with Net 8 / 9 / 10 / 11 and net462 / net472 / net481 Microsoft;Extensions;Hosting;Rx;ReactiveUI;net;netframework True latest @@ -30,8 +30,10 @@ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb true - 10.0.7 - 12.0.2 + + + + $(Features);runtime-async=on @@ -47,11 +49,11 @@ - - - - - + + + + + $(AssemblyName).testconfig.json @@ -60,7 +62,7 @@ - + @@ -70,9 +72,7 @@ - - - - + + diff --git a/src/Extensions.Hosting.Avalonia/AvaloniaHostedService.cs b/src/Extensions.Hosting.Avalonia/AvaloniaHostedService.cs index fd997a5..91eb488 100644 --- a/src/Extensions.Hosting.Avalonia/AvaloniaHostedService.cs +++ b/src/Extensions.Hosting.Avalonia/AvaloniaHostedService.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -11,9 +11,7 @@ namespace ReactiveMarbles.Extensions.Hosting.Avalonia; -/// -/// Provides a hosted service for managing the lifecycle of an Avalonia application within a hosted environment. -/// +/// Provides a hosted service for managing the lifecycle of an Avalonia application within a hosted environment. /// This service is responsible for starting and stopping the Avalonia application, ensuring that the UI /// thread is properly managed. It requires a valid logger, threading model, and Avalonia context to function /// correctly. @@ -28,6 +26,10 @@ namespace ReactiveMarbles.Extensions.Hosting.Avalonia; /// The IAvaloniaContext instance that provides access to Avalonia-specific services and context information. public class AvaloniaHostedService(ILogger logger, AvaloniaThread avaloniaThread, IAvaloniaContext avaloniaContext) : IHostedService { + /// Logs when the Avalonia application is stopping. + private static readonly Action LogStoppingAvalonia = + LoggerMessage.Define(LogLevel.Debug, new EventId(1, nameof(LogStoppingAvalonia)), "Stopping Avalonia due to application exit."); + /// public Task StartAsync(CancellationToken cancellationToken) { @@ -48,10 +50,12 @@ public Task StartAsync(CancellationToken cancellationToken) /// public async Task StopAsync(CancellationToken cancellationToken) { - if (avaloniaContext.IsRunning) + if (!avaloniaContext.IsRunning) { - logger.LogDebug("Stopping Avalonia due to application exit."); - await avaloniaContext.Dispatcher.InvokeAsync(() => avaloniaContext.ApplicationLifetime?.Shutdown()); + return; } + + LogStoppingAvalonia(logger, null); + await avaloniaContext.Dispatcher.InvokeAsync(() => avaloniaContext.ApplicationLifetime?.Shutdown()); } } diff --git a/src/Extensions.Hosting.Avalonia/Extensions.Hosting.Avalonia.csproj b/src/Extensions.Hosting.Avalonia/Extensions.Hosting.Avalonia.csproj index 159e742..2193846 100644 --- a/src/Extensions.Hosting.Avalonia/Extensions.Hosting.Avalonia.csproj +++ b/src/Extensions.Hosting.Avalonia/Extensions.Hosting.Avalonia.csproj @@ -1,7 +1,7 @@ - + - net8.0;net9.0;net10.0 + net8.0;net9.0;net10.0;net11.0 AnyCPU;x64 This extension adds Avalonia support to generic host applications. With this you can enhance your application with a UI, and use all the services provided by the generic host like DI, logging etc. CP.Extensions.Hosting.Avalonia @@ -12,9 +12,9 @@ - - - + + + diff --git a/src/Extensions.Hosting.Avalonia/Extensions/AvaloniaBuilderExtensions.cs b/src/Extensions.Hosting.Avalonia/Extensions/AvaloniaBuilderExtensions.cs index 8d532ce..d47a167 100644 --- a/src/Extensions.Hosting.Avalonia/Extensions/AvaloniaBuilderExtensions.cs +++ b/src/Extensions.Hosting.Avalonia/Extensions/AvaloniaBuilderExtensions.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -8,117 +8,103 @@ namespace ReactiveMarbles.Extensions.Hosting.Avalonia; -/// -/// Provides extension methods for configuring the Avalonia application builder. -/// +/// Provides extension methods for configuring the Avalonia application builder. /// This static class contains methods that allow developers to customize the Avalonia application setup, /// including registering window types, configuring application instances, and applying custom configuration /// actions. public static class AvaloniaBuilderExtensions { - /// - /// Registers a window type with the Avalonia application builder for use when creating application windows. - /// - /// Use this method to configure the application builder to create instances of a custom window - /// type. This is typically called during application startup to specify the main window or additional window - /// types. - /// Specifies the window type to register. The type must derive from the Window class. - /// The Avalonia builder instance to which the window type will be added. Cannot be null. - /// The updated Avalonia builder instance, enabling further configuration through method chaining. - /// Thrown if the avaloniaBuilder parameter is null. - public static IAvaloniaBuilder UseWindow(this IAvaloniaBuilder avaloniaBuilder) - where TWindow : Window + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IAvaloniaBuilder avaloniaBuilder) { - if (avaloniaBuilder == null) + /// Registers a window type with the Avalonia application builder for use when creating application windows. + /// Use this method to configure the application builder to create instances of a custom window + /// type. This is typically called during application startup to specify the main window or additional window + /// types. + /// Specifies the window type to register. The type must derive from the Window class. + /// The updated Avalonia builder instance, enabling further configuration through method chaining. + /// Thrown if the avaloniaBuilder parameter is null. + public IAvaloniaBuilder UseWindow() + where TWindow : Window { - throw new ArgumentNullException(nameof(avaloniaBuilder)); - } + if (avaloniaBuilder is null) + { + throw new ArgumentNullException(nameof(avaloniaBuilder)); + } - avaloniaBuilder.WindowTypes.Add(typeof(TWindow)); - return avaloniaBuilder; - } + avaloniaBuilder.WindowTypes.Add(typeof(TWindow)); + return avaloniaBuilder; + } - /// - /// Configures the Avalonia application to use the specified application type. - /// - /// This method sets the application type for the Avalonia application, allowing the framework to - /// instantiate the specified application class during startup. - /// The type of the application to be used, which must derive from the Application class. - /// The Avalonia builder instance used to configure the application settings. - /// The updated IAvaloniaBuilder instance for further configuration. - public static IAvaloniaBuilder UseApplication(this IAvaloniaBuilder avaloniaBuilder) - where TApplication : Application - { - if (avaloniaBuilder == null) + /// Configures the Avalonia application to use the specified application type. + /// This method sets the application type for the Avalonia application, allowing the framework to + /// instantiate the specified application class during startup. + /// The type of the application to be used, which must derive from the Application class. + /// The updated IAvaloniaBuilder instance for further configuration. + public IAvaloniaBuilder UseApplication() + where TApplication : Application { - throw new ArgumentNullException(nameof(avaloniaBuilder)); - } + if (avaloniaBuilder is null) + { + throw new ArgumentNullException(nameof(avaloniaBuilder)); + } - avaloniaBuilder.ApplicationType = typeof(TApplication); - return avaloniaBuilder; - } + avaloniaBuilder.ApplicationType = typeof(TApplication); + return avaloniaBuilder; + } - /// - /// Configures the specified Avalonia builder to use the provided application instance for managing the - /// application's lifecycle. - /// - /// This method sets both the application type and the current application instance on the - /// builder, allowing the builder to manage the application's lifecycle. Use this method when you want to provide an - /// existing application instance rather than letting the builder create one. - /// Specifies the type of the application to be used. The type must derive from the Application class. - /// The Avalonia builder instance to configure. This parameter cannot be null. - /// The application instance to associate with the Avalonia builder. - /// The configured IAvaloniaBuilder instance, enabling further method chaining. - /// Thrown if the avaloniaBuilder parameter is null. - public static IAvaloniaBuilder UseCurrentApplication(this IAvaloniaBuilder avaloniaBuilder, TApplication currentApplication) - where TApplication : Application - { - if (avaloniaBuilder == null) + /// Configures the specified Avalonia builder to use the provided application instance for managing the application's lifecycle. + /// This method sets both the application type and the current application instance on the + /// builder, allowing the builder to manage the application's lifecycle. Use this method when you want to provide an + /// existing application instance rather than letting the builder create one. + /// Specifies the type of the application to be used. The type must derive from the Application class. + /// The application instance to associate with the Avalonia builder. + /// The configured IAvaloniaBuilder instance, enabling further method chaining. + /// Thrown if the avaloniaBuilder parameter is null. + public IAvaloniaBuilder UseCurrentApplication(TApplication currentApplication) + where TApplication : Application { - throw new ArgumentNullException(nameof(avaloniaBuilder)); - } + if (avaloniaBuilder is null) + { + throw new ArgumentNullException(nameof(avaloniaBuilder)); + } - avaloniaBuilder.ApplicationType = typeof(TApplication); - avaloniaBuilder.Application = currentApplication; - return avaloniaBuilder; - } + avaloniaBuilder.ApplicationType = typeof(TApplication); + avaloniaBuilder.Application = currentApplication; + return avaloniaBuilder; + } - /// - /// Configures the Avalonia application context by applying the specified configuration action to the builder. - /// - /// Use this method to customize the Avalonia context during application setup by providing a - /// configuration action. - /// The Avalonia builder instance to configure. This parameter cannot be null. - /// An action that configures the Avalonia context. The action is invoked with the context as its parameter. - /// The updated Avalonia builder instance, enabling further configuration through method chaining. - /// Thrown if is null. - public static IAvaloniaBuilder ConfigureContext(this IAvaloniaBuilder avaloniaBuilder, Action configureAction) - { - if (avaloniaBuilder == null) + /// Configures the Avalonia application context by applying the specified configuration action to the builder. + /// Use this method to customize the Avalonia context during application setup by providing a + /// configuration action. + /// An action that configures the Avalonia context. The action is invoked with the context as its parameter. + /// The updated Avalonia builder instance, enabling further configuration through method chaining. + /// Thrown if is null. + public IAvaloniaBuilder ConfigureContext(Action configureAction) { - throw new ArgumentNullException(nameof(avaloniaBuilder)); - } + if (avaloniaBuilder is null) + { + throw new ArgumentNullException(nameof(avaloniaBuilder)); + } - avaloniaBuilder.ConfigureContextAction = configureAction; - return avaloniaBuilder; - } + avaloniaBuilder.ConfigureContextAction = configureAction; + return avaloniaBuilder; + } - /// - /// Configures the application builder with a specified action, allowing for custom setup of the Avalonia - /// application. - /// - /// The Avalonia builder instance to configure. This parameter cannot be null. - /// An action that configures the AppBuilder instance. The action is invoked to apply custom configuration logic. - /// The configured IAvaloniaBuilder instance, enabling method chaining for further configuration. - /// Thrown if is null. - public static IAvaloniaBuilder ConfigureAppBuilder(this IAvaloniaBuilder avaloniaBuilder, Action configureAction) - { - if (avaloniaBuilder == null) + /// Configures the application builder with a specified action, allowing for custom setup of the Avalonia application. + /// An action that configures the AppBuilder instance. The action is invoked to apply custom configuration logic. + /// The configured IAvaloniaBuilder instance, enabling method chaining for further configuration. + /// Thrown if is null. + public IAvaloniaBuilder ConfigureAppBuilder(Action configureAction) { - throw new ArgumentNullException(nameof(avaloniaBuilder)); - } + if (avaloniaBuilder is null) + { + throw new ArgumentNullException(nameof(avaloniaBuilder)); + } - avaloniaBuilder.ConfigureAppBuilderAction = configureAction; - return avaloniaBuilder; + avaloniaBuilder.ConfigureAppBuilderAction = configureAction; + return avaloniaBuilder; + } } } diff --git a/src/Extensions.Hosting.Avalonia/Extensions/HostApplicationBuilderAvaloniaExtensions.cs b/src/Extensions.Hosting.Avalonia/Extensions/HostApplicationBuilderAvaloniaExtensions.cs index 544f659..db283ef 100644 --- a/src/Extensions.Hosting.Avalonia/Extensions/HostApplicationBuilderAvaloniaExtensions.cs +++ b/src/Extensions.Hosting.Avalonia/Extensions/HostApplicationBuilderAvaloniaExtensions.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -8,53 +8,42 @@ namespace ReactiveMarbles.Extensions.Hosting.Avalonia; -/// -/// Provides extension methods for configuring Avalonia support in a host application builder. -/// +/// Provides extension methods for configuring Avalonia support in a host application builder. /// These methods enable integration of Avalonia's lifetime management and application builder /// customization within a generic host application. Use them to ensure proper shutdown behavior and to apply /// Avalonia-specific settings. public static class HostApplicationBuilderAvaloniaExtensions { - /// - /// Configures Avalonia lifetime management for the application, enabling controlled shutdown behavior based on the - /// specified shutdown mode. - /// - /// This method integrates Avalonia's lifetime management into the host application, ensuring - /// that the application shuts down according to the selected mode. Use this extension when building Avalonia - /// applications with generic host support. - /// The application builder used to configure services and lifetime management for the host application. Cannot be - /// null. - /// Specifies the shutdown behavior for the application. The default is . - /// The updated instance, allowing for further configuration. - public static IHostApplicationBuilder UseAvaloniaLifetime(this IHostApplicationBuilder hostApplicationBuilder, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose) + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHostApplicationBuilder hostApplicationBuilder) { - if (hostApplicationBuilder == null) + /// Configures Avalonia lifetime management for the application, enabling controlled shutdown behavior based on the specified shutdown mode. + /// This method integrates Avalonia's lifetime management into the host application, ensuring + /// that the application shuts down according to the selected mode. Use this extension when building Avalonia + /// applications with generic host support. + /// Specifies the shutdown behavior for the application. The default is . + /// The updated instance, allowing for further configuration. + public IHostApplicationBuilder UseAvaloniaLifetime(ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose) { - throw new ArgumentNullException(nameof(hostApplicationBuilder)); - } + if (hostApplicationBuilder is null) + { + throw new ArgumentNullException(nameof(hostApplicationBuilder)); + } - return InternalBuilderAvaloniaUtility.UseAvaloniaLifetime(hostApplicationBuilder, shutdownMode); - } + return InternalBuilderAvaloniaUtility.UseAvaloniaLifetime(hostApplicationBuilder, shutdownMode); + } - /// - /// Configures the Avalonia application builder for integration with the host application builder, optionally - /// allowing further customization. - /// - /// Use this method to enable Avalonia support in your application's host builder. The optional - /// delegate allows you to modify Avalonia-specific settings before the application is built. - /// The application builder to configure for Avalonia support. Cannot be null. - /// An optional delegate that provides additional customization for the Avalonia builder. If null, default - /// configuration is applied. - /// An instance of IHostApplicationBuilder that is configured to support Avalonia. - public static IHostApplicationBuilder ConfigureAvalonia(this IHostApplicationBuilder hostApplicationBuilder, Action configureDelegate = null) - { - if (hostApplicationBuilder == null) + /// Configures the Avalonia application builder for integration with the host application builder, optionally allowing further customization. + /// Use this method to enable Avalonia support in your application's host builder. The optional + /// delegate allows you to modify Avalonia-specific settings before the application is built. + /// An optional delegate that provides additional customization for the Avalonia builder. If null, default + /// configuration is applied. + /// An instance of IHostApplicationBuilder that is configured to support Avalonia. + public IHostApplicationBuilder ConfigureAvalonia(Action? configureDelegate = null) { - throw new ArgumentNullException(nameof(hostApplicationBuilder)); + return InternalBuilderAvaloniaUtility.ConfigureAvalonia(hostApplicationBuilder, configureDelegate); } - - return InternalBuilderAvaloniaUtility.ConfigureAvalonia(hostApplicationBuilder, configureDelegate); } } diff --git a/src/Extensions.Hosting.Avalonia/Extensions/HostBuilderAvaloniaExtensions.cs b/src/Extensions.Hosting.Avalonia/Extensions/HostBuilderAvaloniaExtensions.cs index 8272ad7..938f3c8 100644 --- a/src/Extensions.Hosting.Avalonia/Extensions/HostBuilderAvaloniaExtensions.cs +++ b/src/Extensions.Hosting.Avalonia/Extensions/HostBuilderAvaloniaExtensions.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -8,52 +8,46 @@ namespace ReactiveMarbles.Extensions.Hosting.Avalonia; -/// -/// Provides extension methods for configuring Avalonia integration with a host builder, enabling application lifetime -/// management and customization of Avalonia settings. -/// +/// Provides extension methods for configuring Avalonia integration with a host builder, enabling application lifetime management and customization of Avalonia settings. /// These methods allow developers to integrate Avalonia's application lifecycle and settings into a /// generic host, ensuring proper shutdown behavior and enabling customization before application startup. public static class HostBuilderAvaloniaExtensions { - /// - /// Configures the host builder to use Avalonia application lifetime management, enabling proper shutdown behavior - /// based on the specified shutdown mode. - /// - /// This method integrates Avalonia's lifetime management with the host builder, ensuring that - /// the application shuts down according to the selected shutdown mode. Use this extension when hosting Avalonia - /// applications to control shutdown behavior. - /// The host builder instance used to configure application services and lifetime management. - /// Specifies the shutdown mode for the Avalonia application. The default is , which shuts down the application when the last window is closed. - /// The updated instance, allowing for further configuration. - public static IHostBuilder UseAvaloniaLifetime(this IHostBuilder hostBuilder, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose) + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHostBuilder hostBuilder) { - if (hostBuilder == null) + /// Configures the host builder to use Avalonia application lifetime management, enabling proper shutdown behavior based on the specified shutdown mode. + /// This method integrates Avalonia's lifetime management with the host builder, ensuring that + /// the application shuts down according to the selected shutdown mode. Use this extension when hosting Avalonia + /// applications to control shutdown behavior. + /// Specifies the shutdown mode for the Avalonia application. The default is , which shuts down the application when the last window is closed. + /// The updated instance, allowing for further configuration. + public IHostBuilder UseAvaloniaLifetime(ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose) { - throw new ArgumentNullException(nameof(hostBuilder)); - } + if (hostBuilder is null) + { + throw new ArgumentNullException(nameof(hostBuilder)); + } - return InternalBuilderAvaloniaUtility.UseAvaloniaLifetime(hostBuilder, shutdownMode); - } + return InternalBuilderAvaloniaUtility.UseAvaloniaLifetime(hostBuilder, shutdownMode); + } - /// - /// Configures the Avalonia framework for the specified host builder, enabling integration and customization of - /// Avalonia application settings before startup. - /// - /// Use this method to set up Avalonia-specific configurations and options prior to application - /// startup. This enables customization of the Avalonia environment within a generic host. - /// The host builder to configure for Avalonia integration. Cannot be null. - /// An optional delegate that allows further customization of the Avalonia builder. If provided, it is invoked to - /// configure Avalonia-specific options. - /// An instance of IHostBuilder that has been configured for Avalonia support. - public static IHostBuilder ConfigureAvalonia(this IHostBuilder hostBuilder, Action? configureDelegate = null) - { - if (hostBuilder == null) + /// Configures the Avalonia framework for the specified host builder, enabling integration and customization of Avalonia application settings before startup. + /// Use this method to set up Avalonia-specific configurations and options prior to application + /// startup. This enables customization of the Avalonia environment within a generic host. + /// An optional delegate that allows further customization of the Avalonia builder. If provided, it is invoked to + /// configure Avalonia-specific options. + /// An instance of IHostBuilder that has been configured for Avalonia support. + public IHostBuilder ConfigureAvalonia(Action? configureDelegate = null) { - throw new ArgumentNullException(nameof(hostBuilder)); - } + if (hostBuilder is null) + { + throw new ArgumentNullException(nameof(hostBuilder)); + } - return InternalBuilderAvaloniaUtility.ConfigureAvalonia(hostBuilder, configureDelegate); + return InternalBuilderAvaloniaUtility.ConfigureAvalonia(hostBuilder, configureDelegate); + } } } diff --git a/src/Extensions.Hosting.Avalonia/Extensions/InternalBuilderAvaloniaUtility.cs b/src/Extensions.Hosting.Avalonia/Extensions/InternalBuilderAvaloniaUtility.cs index 1d3adf5..e43135d 100644 --- a/src/Extensions.Hosting.Avalonia/Extensions/InternalBuilderAvaloniaUtility.cs +++ b/src/Extensions.Hosting.Avalonia/Extensions/InternalBuilderAvaloniaUtility.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -12,20 +12,15 @@ namespace ReactiveMarbles.Extensions.Hosting.Avalonia; -/// -/// Provides utility methods for configuring Avalonia integration within a host application, enabling proper lifetime -/// management and service registration. -/// +/// Provides utility methods for configuring Avalonia integration within a host application, enabling proper lifetime management and service registration. /// This class contains static methods that facilitate the setup of Avalonia in a generic host /// environment, ensuring that the application can manage its lifecycle and dependencies effectively. internal static class InternalBuilderAvaloniaUtility { + /// Stores the avalonia context key value. private const string AvaloniaContextKey = "AvaloniaContext"; - /// - /// Configures Avalonia lifetime management for the application host, enabling proper shutdown behavior based on the - /// specified shutdown mode. - /// + /// Configures Avalonia lifetime management for the application host, enabling proper shutdown behavior based on the specified shutdown mode. /// This method integrates Avalonia's window management with the application's lifetime, ensuring /// that the application shuts down according to user interactions with the UI. Use this method to enable /// Avalonia-specific lifetime handling in a generic host environment. @@ -36,10 +31,7 @@ internal static class InternalBuilderAvaloniaUtility internal static IHostBuilder UseAvaloniaLifetime(IHostBuilder hostBuilder, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose) => hostBuilder.ConfigureServices((hostBuilderContext, serviceCollection) => InnerUseAvaloniaLifetime(hostBuilder.Properties, shutdownMode)); - /// - /// Configures the Avalonia application by adding Avalonia services to the host builder and applying an optional - /// configuration delegate. - /// + /// Configures the Avalonia application by adding Avalonia services to the host builder and applying an optional configuration delegate. /// This method integrates Avalonia into the application's dependency injection system, enabling /// Avalonia UI functionality within a generic host environment. Use the configuration delegate to customize /// Avalonia-specific options as needed. @@ -50,10 +42,7 @@ internal static IHostBuilder UseAvaloniaLifetime(IHostBuilder hostBuilder, Shutd internal static IHostBuilder ConfigureAvalonia(IHostBuilder hostBuilder, Action? configureDelegate = null) => hostBuilder.ConfigureServices((hostBuilderContext, serviceCollection) => InnerConfigureAvalonia(hostBuilder.Properties, serviceCollection, configureDelegate)); - /// - /// Configures Avalonia lifetime management for the application host, enabling proper shutdown behavior according to - /// the specified shutdown mode. - /// + /// Configures Avalonia lifetime management for the application host, enabling proper shutdown behavior according to the specified shutdown mode. /// This method integrates Avalonia's lifetime management into the application's host, ensuring /// that the application shuts down correctly based on the defined shutdown mode. /// The application builder used to configure the host for the Avalonia application. @@ -67,10 +56,7 @@ internal static IHostApplicationBuilder UseAvaloniaLifetime(IHostApplicationBuil return hostApplicationBuilder; } - /// - /// Configures the Avalonia application within the specified host application builder, optionally allowing further - /// customization through a delegate. - /// + /// Configures the Avalonia application within the specified host application builder, optionally allowing further customization through a delegate. /// This method sets up Avalonia integration for the host application builder and applies any /// custom configuration provided by the delegate. Use this method to prepare the application builder for /// Avalonia-specific features before building the host. @@ -86,9 +72,7 @@ internal static IHostApplicationBuilder ConfigureAvalonia(IHostApplicationBuilde return hostApplicationBuilder; } - /// - /// Helper method to retrieve the IAvaloniaContext. - /// + /// Helper method to retrieve the IAvaloniaContext. /// IDictionary. /// IAvaloniaContext out value. /// bool if there was already an IAvaloniaContext. @@ -105,6 +89,10 @@ private static bool TryRetrieveAvaloniaContext(IDictionary prope return false; } + /// Applies Avalonia lifetime settings to an existing configured Avalonia context. + /// The host builder property bag that stores the Avalonia context. + /// The Avalonia shutdown mode to apply. + /// Thrown when Avalonia has not been configured yet. private static void InnerUseAvaloniaLifetime(IDictionary properties, ShutdownMode shutdownMode) { if (!TryRetrieveAvaloniaContext(properties, out var avaloniaContext)) @@ -116,10 +104,83 @@ private static void InnerUseAvaloniaLifetime(IDictionary propert avaloniaContext.IsLifetimeLinked = true; } - /// - /// Configures the Avalonia application and registers its services with the provided service collection using the - /// specified properties and optional configuration delegate. - /// + /// Registers the core Avalonia hosting services. + /// The service collection to register services into. + /// The Avalonia context instance to register. + /// The builder that can customize the application builder. + private static void RegisterAvaloniaHostingServices(IServiceCollection serviceCollection, IAvaloniaContext avaloniaContext, AvaloniaBuilder avaloniaBuilder) + { + _ = serviceCollection.AddSingleton(avaloniaContext); + + _ = serviceCollection.AddSingleton(serviceProvider => + { + var appBuilder = AppBuilder.Configure(() => serviceProvider.GetService() ?? new Application()) + .UsePlatformDetect() + .LogToTrace(); + + avaloniaBuilder.ConfigureAppBuilderAction?.Invoke(appBuilder); + + return new AvaloniaThread(serviceProvider, appBuilder); + }); + + _ = serviceCollection.AddHostedService(); + } + + /// Registers a configured Avalonia application type or instance. + /// The service collection to register services into. + /// The builder that contains the application configuration. + /// The public parameter name used for exception reporting. + /// Thrown if the configured application type does not derive from . + private static void RegisterAvaloniaApplication(IServiceCollection serviceCollection, AvaloniaBuilder avaloniaBuilder, string parameterName) + { + if (avaloniaBuilder.ApplicationType is null) + { + return; + } + + var baseApplicationType = typeof(Application); + if (!baseApplicationType.IsAssignableFrom(avaloniaBuilder.ApplicationType)) + { + throw new ArgumentException("The registered Application type must inherit Avalonia.Application", parameterName); + } + + if (avaloniaBuilder.Application is not null) + { + _ = serviceCollection.AddSingleton(avaloniaBuilder.ApplicationType, avaloniaBuilder.Application); + } + else + { + _ = serviceCollection.AddSingleton(avaloniaBuilder.ApplicationType); + } + + if (avaloniaBuilder.ApplicationType == baseApplicationType) + { + return; + } + + _ = serviceCollection.AddSingleton(serviceProvider => (Application)serviceProvider.GetRequiredService(avaloniaBuilder.ApplicationType)); + } + + /// Registers configured Avalonia shell windows. + /// The service collection to register services into. + /// The builder that contains the window configuration. + private static void RegisterAvaloniaWindows(IServiceCollection serviceCollection, AvaloniaBuilder avaloniaBuilder) + { + foreach (var avaloniaWindowType in avaloniaBuilder.WindowTypes) + { + _ = serviceCollection.AddSingleton(avaloniaWindowType); + + var shellInterfaceType = typeof(IAvaloniaShell); + if (!shellInterfaceType.IsAssignableFrom(avaloniaWindowType)) + { + continue; + } + + _ = serviceCollection.AddSingleton(shellInterfaceType, serviceProvider => serviceProvider.GetRequiredService(avaloniaWindowType)); + } + } + + /// Configures the Avalonia application and registers its services with the provided service collection using the specified properties and optional configuration delegate. /// This method sets up the Avalonia application context and registers necessary services for /// dependency injection, including hosted services and window types. It ensures that the Application type is valid /// and allows for custom configuration through the provided delegate. Window types implementing IAvaloniaShell are @@ -137,65 +198,11 @@ private static void InnerConfigureAvalonia(IDictionary propertie if (!TryRetrieveAvaloniaContext(properties, out var avaloniaContext)) { - serviceCollection.AddSingleton(avaloniaContext); - - // The AppBuilder will be created by resolving the Application from DI - // and applying any custom configuration - serviceCollection.AddSingleton(serviceProvider => - { - var appBuilder = AppBuilder.Configure(() => serviceProvider.GetService() ?? new Application()) - .UsePlatformDetect() - .LogToTrace(); - - // Allow custom configuration of the AppBuilder - avaloniaBuilder.ConfigureAppBuilderAction?.Invoke(appBuilder); - - return new AvaloniaThread(serviceProvider, appBuilder); - }); - - serviceCollection.AddHostedService(); + RegisterAvaloniaHostingServices(serviceCollection, avaloniaContext, avaloniaBuilder); } avaloniaBuilder.ConfigureContextAction?.Invoke(avaloniaContext); - - if (avaloniaBuilder.ApplicationType != null) - { - // Check if the registered application does inherit Avalonia.Application - var baseApplicationType = typeof(Application); - if (!baseApplicationType.IsAssignableFrom(avaloniaBuilder.ApplicationType)) - { - throw new ArgumentException("The registered Application type must inherit Avalonia.Application", nameof(configureDelegate)); - } - - if (avaloniaBuilder.Application != null) - { - // Add existing Application - serviceCollection.AddSingleton(avaloniaBuilder.ApplicationType, avaloniaBuilder.Application); - } - else - { - serviceCollection.AddSingleton(avaloniaBuilder.ApplicationType); - } - - if (avaloniaBuilder.ApplicationType != baseApplicationType) - { - serviceCollection.AddSingleton(serviceProvider => (Application)serviceProvider.GetRequiredService(avaloniaBuilder.ApplicationType)); - } - } - - if (avaloniaBuilder.WindowTypes.Count > 0) - { - foreach (var avaloniaWindowType in avaloniaBuilder.WindowTypes) - { - serviceCollection.AddSingleton(avaloniaWindowType); - - // Check if it also implements IAvaloniaShell so we can register it as this - var shellInterfaceType = typeof(IAvaloniaShell); - if (shellInterfaceType.IsAssignableFrom(avaloniaWindowType)) - { - serviceCollection.AddSingleton(shellInterfaceType, serviceProvider => serviceProvider.GetRequiredService(avaloniaWindowType)); - } - } - } + RegisterAvaloniaApplication(serviceCollection, avaloniaBuilder, nameof(configureDelegate)); + RegisterAvaloniaWindows(serviceCollection, avaloniaBuilder); } } diff --git a/src/Extensions.Hosting.Avalonia/IAvaloniaBuilder.cs b/src/Extensions.Hosting.Avalonia/IAvaloniaBuilder.cs index 471adea..2bd117f 100644 --- a/src/Extensions.Hosting.Avalonia/IAvaloniaBuilder.cs +++ b/src/Extensions.Hosting.Avalonia/IAvaloniaBuilder.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -8,38 +8,25 @@ namespace ReactiveMarbles.Extensions.Hosting.Avalonia; -/// -/// Provides a contract for configuring an Avalonia application, including application type, existing application -/// instance, window types, and context and builder configuration actions. -/// +/// Provides a contract for configuring an Avalonia application, including application type, existing application instance, window types, and context and builder configuration actions. /// Implementations of this interface allow developers to set up the Avalonia application environment by /// specifying the application type, supplying an existing application instance, defining window types, and providing /// actions to configure both the Avalonia context and the AppBuilder. This enables flexible customization of the /// application startup process and environment. public interface IAvaloniaBuilder { - /// - /// Gets or sets type of the application that will be used. - /// + /// Gets or sets type of the application that will be used. Type? ApplicationType { get; set; } - /// - /// Gets or sets an existing application. - /// + /// Gets or sets an existing application. Application? Application { get; set; } - /// - /// Gets type of the windows that will be used. - /// + /// Gets type of the windows that will be used. IList WindowTypes { get; } - /// - /// Gets or sets action to configure the Avalonia context. - /// + /// Gets or sets action to configure the Avalonia context. Action? ConfigureContextAction { get; set; } - /// - /// Gets or sets action to configure the AppBuilder. - /// + /// Gets or sets action to configure the AppBuilder. Action? ConfigureAppBuilderAction { get; set; } } diff --git a/src/Extensions.Hosting.Avalonia/IAvaloniaContext.cs b/src/Extensions.Hosting.Avalonia/IAvaloniaContext.cs index d7f1402..53e9b33 100644 --- a/src/Extensions.Hosting.Avalonia/IAvaloniaContext.cs +++ b/src/Extensions.Hosting.Avalonia/IAvaloniaContext.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Avalonia; @@ -10,33 +10,22 @@ namespace ReactiveMarbles.Extensions.Hosting.Avalonia; -/// -/// Provides a context for Avalonia applications, enabling management of application lifetime, shutdown behavior, and -/// access to the Avalonia dispatcher. -/// +/// Provides a context for Avalonia applications, enabling management of application lifetime, shutdown behavior, and access to the Avalonia dispatcher. /// This interface is essential for configuring and controlling Avalonia application behavior. It allows /// developers to specify shutdown modes, access the application instance, manage the application lifetime, and interact /// with the Avalonia dispatcher for UI operations. Use this context to integrate Avalonia-specific features into /// hosting environments or application frameworks. public interface IAvaloniaContext : IUiContext { - /// - /// Gets or sets this is the Avalonia ShutdownMode used for the Avalonia application lifetime, default is OnLastWindowClose. - /// + /// Gets or sets this is the Avalonia ShutdownMode used for the Avalonia application lifetime, default is OnLastWindowClose. ShutdownMode ShutdownMode { get; set; } - /// - /// Gets or sets the Application. - /// + /// Gets or sets the Application. Application? AvaloniaApplication { get; set; } - /// - /// Gets or sets the Application Lifetime. - /// + /// Gets or sets the Application Lifetime. IClassicDesktopStyleApplicationLifetime? ApplicationLifetime { get; set; } - /// - /// Gets the Avalonia dispatcher. - /// + /// Gets the Avalonia dispatcher. Dispatcher Dispatcher { get; } } diff --git a/src/Extensions.Hosting.Avalonia/IAvaloniaService.cs b/src/Extensions.Hosting.Avalonia/IAvaloniaService.cs index 4c36f89..0acea9b 100644 --- a/src/Extensions.Hosting.Avalonia/IAvaloniaService.cs +++ b/src/Extensions.Hosting.Avalonia/IAvaloniaService.cs @@ -1,23 +1,19 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Avalonia; namespace ReactiveMarbles.Extensions.Hosting.Avalonia; -/// -/// Defines methods for initializing and managing Avalonia application services within a host environment. -/// +/// Defines methods for initializing and managing Avalonia application services within a host environment. /// Implementations of this interface should ensure that initialization occurs on the UI thread and /// adheres to Avalonia's application lifecycle requirements. Thread safety and proper sequencing are important when /// invoking initialization routines. This interface is intended for use in scenarios where Avalonia is required for UI /// rendering and integration with hosting frameworks. public interface IAvaloniaService { - /// - /// Initializes the application with the specified configuration settings. - /// + /// Initializes the application with the specified configuration settings. /// This method must be called before any other operations on the application to ensure it is /// properly configured. /// The application instance that contains the configuration settings to be applied during initialization. diff --git a/src/Extensions.Hosting.Avalonia/IAvaloniaShell.cs b/src/Extensions.Hosting.Avalonia/IAvaloniaShell.cs index db331d2..f53ba57 100644 --- a/src/Extensions.Hosting.Avalonia/IAvaloniaShell.cs +++ b/src/Extensions.Hosting.Avalonia/IAvaloniaShell.cs @@ -1,14 +1,14 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using Avalonia.Input; + namespace ReactiveMarbles.Extensions.Hosting.Avalonia; -/// -/// Defines a contract for managing the Avalonia application's lifecycle and user interface components. -/// +/// Defines a contract for managing the Avalonia application's lifecycle and user interface components. /// Implementations of this interface should provide functionality to initialize, configure, and control /// the Avalonia application environment. This includes handling window creation, application startup, and exit /// procedures. Use this interface to abstract the shell responsibilities and enable flexible integration with /// Avalonia-based applications. -public interface IAvaloniaShell; +public interface IAvaloniaShell : IInputElement; diff --git a/src/Extensions.Hosting.Avalonia/Internals/AvaloniaBuilder.cs b/src/Extensions.Hosting.Avalonia/Internals/AvaloniaBuilder.cs index c5ea088..f9142b0 100644 --- a/src/Extensions.Hosting.Avalonia/Internals/AvaloniaBuilder.cs +++ b/src/Extensions.Hosting.Avalonia/Internals/AvaloniaBuilder.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -9,7 +9,7 @@ namespace ReactiveMarbles.Extensions.Hosting.Avalonia.Internals; /// -internal class AvaloniaBuilder : IAvaloniaBuilder +internal sealed class AvaloniaBuilder : IAvaloniaBuilder { /// public Type? ApplicationType { get; set; } diff --git a/src/Extensions.Hosting.Avalonia/Internals/AvaloniaContext.cs b/src/Extensions.Hosting.Avalonia/Internals/AvaloniaContext.cs index de1e8b8..cf86bf2 100644 --- a/src/Extensions.Hosting.Avalonia/Internals/AvaloniaContext.cs +++ b/src/Extensions.Hosting.Avalonia/Internals/AvaloniaContext.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Avalonia; @@ -10,7 +10,7 @@ namespace ReactiveMarbles.Extensions.Hosting.Avalonia.Internals; /// -internal class AvaloniaContext : IAvaloniaContext +internal sealed class AvaloniaContext : IAvaloniaContext { /// public ShutdownMode ShutdownMode { get; set; } = ShutdownMode.OnLastWindowClose; diff --git a/src/Extensions.Hosting.Avalonia/Internals/AvaloniaThread.cs b/src/Extensions.Hosting.Avalonia/Internals/AvaloniaThread.cs index b9ccbc3..df5bb51 100644 --- a/src/Extensions.Hosting.Avalonia/Internals/AvaloniaThread.cs +++ b/src/Extensions.Hosting.Avalonia/Internals/AvaloniaThread.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -12,10 +12,7 @@ namespace ReactiveMarbles.Extensions.Hosting.Avalonia.Internals; -/// -/// Manages the Avalonia UI thread and application lifetime for desktop applications, ensuring proper initialization and -/// integration of Avalonia services and shell windows. -/// +/// Manages the Avalonia UI thread and application lifetime for desktop applications, ensuring proper initialization and integration of Avalonia services and shell windows. /// This class is responsible for starting the Avalonia UI thread, initializing the application lifetime, /// and registering Avalonia-specific services. It ensures that the application is properly set up and that any shell /// windows are displayed according to the application's startup configuration. @@ -31,7 +28,7 @@ protected override void PreUiThreadStart() /// protected override void UiThreadStart() { - appBuilder.StartWithClassicDesktopLifetime([], lifetime => + _ = appBuilder.StartWithClassicDesktopLifetime([], lifetime => { lifetime.ShutdownMode = UiContext.ShutdownMode; UiContext.ApplicationLifetime = lifetime; @@ -42,8 +39,7 @@ protected override void UiThreadStart() UiContext.AvaloniaApplication = Application.Current ?? ServiceProvider.GetService() ?? throw new InvalidOperationException("Unable to initialize the Avalonia application."); - var avaloniaServices = ServiceProvider.GetServices(); - foreach (var avaloniaService in avaloniaServices) + foreach (var avaloniaService in ServiceProvider.GetServices()) { avaloniaService.Initialize(UiContext.AvaloniaApplication); } @@ -53,12 +49,19 @@ protected override void UiThreadStart() switch (shellWindows.Count) { case 1: + { lifetime.MainWindow = shellWindows[0]; shellWindows[0].Show(); break; + } + case 0: + { break; + } + default: + { lifetime.MainWindow = shellWindows[0]; for (var i = 0; i < shellWindows.Count; i++) @@ -67,6 +70,7 @@ protected override void UiThreadStart() } break; + } } UiContext.IsRunning = true; diff --git a/src/Extensions.Hosting.Identity.EntityFrameworkCore.Sqlite/Extensions.Hosting.Identity.EntityFrameworkCore.Sqlite.csproj b/src/Extensions.Hosting.Identity.EntityFrameworkCore.Sqlite/Extensions.Hosting.Identity.EntityFrameworkCore.Sqlite.csproj index 9777a41..57215f8 100644 --- a/src/Extensions.Hosting.Identity.EntityFrameworkCore.Sqlite/Extensions.Hosting.Identity.EntityFrameworkCore.Sqlite.csproj +++ b/src/Extensions.Hosting.Identity.EntityFrameworkCore.Sqlite/Extensions.Hosting.Identity.EntityFrameworkCore.Sqlite.csproj @@ -1,46 +1,61 @@ - + - net8.0;net9.0;net10.0 + net8.0;net9.0;net10.0;net11.0 enable enable CP.Extensions.Hosting.Identity.EntityFrameworkCore.Sqlite - + - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Extensions.Hosting.Identity.EntityFrameworkCore.Sqlite/HostBuilderEntityFrameworkCoreExtensions.cs b/src/Extensions.Hosting.Identity.EntityFrameworkCore.Sqlite/HostBuilderEntityFrameworkCoreExtensions.cs index e7988ab..c7e54c8 100644 --- a/src/Extensions.Hosting.Identity.EntityFrameworkCore.Sqlite/HostBuilderEntityFrameworkCoreExtensions.cs +++ b/src/Extensions.Hosting.Identity.EntityFrameworkCore.Sqlite/HostBuilderEntityFrameworkCoreExtensions.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.AspNetCore.Builder; @@ -11,262 +11,293 @@ namespace ReactiveMarbles.Extensions.Hosting.Identity.EntityFrameworkCore; -/// -/// Provides extension methods for configuring Entity Framework Core with SQLite and web host services on host and -/// service builders. -/// +/// Provides extension methods for configuring Entity Framework Core with SQLite and web host services on host and service builders. /// These extensions simplify the setup of Entity Framework Core with SQLite and ASP.NET Core Identity in /// .NET host-based applications. They also provide utilities for configuring web host services and customizing the web /// host and application pipeline during host building. All methods are intended to be used during application startup /// configuration. public static class HostBuilderEntityFrameworkCoreExtensions { - /// - /// Configures Entity Framework Core with a SQLite provider and sets up ASP.NET Core Identity using the specified - /// context, user, and role types. - /// - /// This method registers the DbContext with the SQLite provider and configures ASP.NET Core - /// Identity to use the specified user and role types with Entity Framework Core stores. It is typically called - /// during application startup to enable authentication and authorization using SQLite as the backing - /// store. - /// The type of the Entity Framework Core DbContext to use for data access. - /// The type representing application users for ASP.NET Core Identity. - /// The type representing application roles for ASP.NET Core Identity. - /// The service collection to which the Entity Framework Core and Identity services will be added. Cannot be null. - /// The web host builder context containing configuration and environment information. Cannot be null. - /// The name of the connection string in the configuration to use for the SQLite database. Cannot be null or - /// whitespace. - /// The lifetime with which to register the DbContext service. The default is Scoped. - /// The same IServiceCollection instance so that additional calls can be chained. - /// Thrown if connectionStringName is null or consists only of white-space characters. - /// Thrown if the specified connection string is not found in the configuration. - public static IServiceCollection UseEntityFrameworkCoreSqlite(this IServiceCollection services, WebHostBuilderContext context, string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) - where TContext : DbContext - where TUser : class - where TRole : class + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHostApplicationBuilder builder) { - ArgumentNullException.ThrowIfNull(services); - ArgumentNullException.ThrowIfNull(context); - - if (string.IsNullOrWhiteSpace(connectionStringName)) + /// Configures Entity Framework Core with SQLite using the IHostApplicationBuilder pattern. + /// This method provides integration with the modern IHostApplicationBuilder pattern + /// introduced in .NET 7+. It registers the DbContext with SQLite using the connection string + /// from configuration. + /// The type of the Entity Framework Core DbContext to use for data access. + /// The name of the connection string in the configuration. Cannot be null or whitespace. + /// The lifetime with which to register the DbContext service. Defaults to ServiceLifetime.Scoped. + /// The same IHostApplicationBuilder instance so that additional calls can be chained. + /// Thrown if connectionStringName is null or consists only of white-space characters. + /// Thrown if the specified connection string is not found in the configuration. + public IHostApplicationBuilder AddSqliteDbContext(string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) + where TContext : DbContext { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(connectionStringName)); - } + ArgumentNullException.ThrowIfNull(builder); - var conString = context.Configuration.GetConnectionString(connectionStringName); - services - .AddDbContext( - options => options.UseSqlite(conString ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' not found.")), - serviceLifetime) - .AddDefaultIdentity() - .AddRoles() - .AddEntityFrameworkStores(); - return services; - } + ArgumentException.ThrowIfNullOrWhiteSpace(connectionStringName); - /// - /// Configures Entity Framework Core with a SQLite provider and ASP.NET Core Identity using the specified DbContext - /// and user type. - /// - /// This method registers the specified DbContext with a SQLite provider and configures ASP.NET - /// Core Identity to use Entity Framework Core stores. It is typically called during application startup to enable - /// authentication and data access using SQLite. - /// The type of the DbContext to use for Entity Framework Core operations. - /// The type representing the user entity for ASP.NET Core Identity. - /// The IServiceCollection to which the Entity Framework Core and Identity services will be added. - /// The WebHostBuilderContext containing application configuration and environment information. - /// The name of the connection string in the configuration to use for the SQLite database. Cannot be null or - /// whitespace. - /// The lifetime with which to register the DbContext service. The default is Scoped. - /// The IServiceCollection instance with Entity Framework Core and Identity services configured for SQLite. - /// Thrown if connectionStringName is null or consists only of white-space characters. - /// Thrown if the specified connection string is not found in the configuration. - public static IServiceCollection UseEntityFrameworkCoreSqlite(this IServiceCollection services, WebHostBuilderContext context, string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) - where TContext : DbContext - where TUser : class - { - ArgumentNullException.ThrowIfNull(services); - ArgumentNullException.ThrowIfNull(context); + var conString = builder.Configuration.GetConnectionString(connectionStringName); + _ = builder.Services.AddDbContext( + options => options.UseSqlite(conString ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' not found.")), + serviceLifetime); + return builder; + } - if (string.IsNullOrWhiteSpace(connectionStringName)) + /// Configures Entity Framework Core with SQLite and ASP.NET Core Identity using IHostApplicationBuilder. + /// This method provides integration with the modern IHostApplicationBuilder pattern and + /// sets up both Entity Framework Core and ASP.NET Core Identity with the specified user and role types. + /// The type of the Entity Framework Core DbContext to use for data access. + /// The type representing application users for ASP.NET Core Identity. + /// The type representing application roles for ASP.NET Core Identity. + /// The name of the connection string in the configuration. Cannot be null or whitespace. + /// The lifetime with which to register the DbContext service. Defaults to ServiceLifetime.Scoped. + /// The same IHostApplicationBuilder instance so that additional calls can be chained. + /// Thrown if connectionStringName is null or consists only of white-space characters. + /// Thrown if the specified connection string is not found in the configuration. + public IHostApplicationBuilder AddSqliteWithIdentity(string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) + where TContext : DbContext + where TUser : class + where TRole : class { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(connectionStringName)); + ArgumentNullException.ThrowIfNull(builder); + + ArgumentException.ThrowIfNullOrWhiteSpace(connectionStringName); + + var conString = builder.Configuration.GetConnectionString(connectionStringName); + _ = builder.Services + .AddDbContext( + options => options.UseSqlite(conString ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' not found.")), + serviceLifetime) + .AddDefaultIdentity() + .AddRoles() + .AddEntityFrameworkStores(); + return builder; } - var conString = context.Configuration.GetConnectionString(connectionStringName); - services - .AddDbContext( - options => options.UseSqlite(conString ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' not found.")), - serviceLifetime) - .AddDefaultIdentity() - .AddEntityFrameworkStores(); - return services; + /// Configures Entity Framework Core with SQLite and ASP.NET Core Identity (user only) using IHostApplicationBuilder. + /// This method provides integration with the modern IHostApplicationBuilder pattern and + /// sets up both Entity Framework Core and ASP.NET Core Identity with the specified user type only. + /// The type of the Entity Framework Core DbContext to use for data access. + /// The type representing application users for ASP.NET Core Identity. + /// The name of the connection string in the configuration. Cannot be null or whitespace. + /// The lifetime with which to register the DbContext service. Defaults to ServiceLifetime.Scoped. + /// The same IHostApplicationBuilder instance so that additional calls can be chained. + /// Thrown if connectionStringName is null or consists only of white-space characters. + /// Thrown if the specified connection string is not found in the configuration. + public IHostApplicationBuilder AddSqliteWithIdentity(string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) + where TContext : DbContext + where TUser : class + { + ArgumentNullException.ThrowIfNull(builder); + + ArgumentException.ThrowIfNullOrWhiteSpace(connectionStringName); + + var conString = builder.Configuration.GetConnectionString(connectionStringName); + _ = builder.Services + .AddDbContext( + options => options.UseSqlite(conString ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' not found.")), + serviceLifetime) + .AddDefaultIdentity() + .AddEntityFrameworkStores(); + return builder; + } } - /// - /// Configures Entity Framework Core with SQLite for the specified DbContext without ASP.NET Core Identity. - /// - /// Use this method when you need Entity Framework Core with SQLite but do not require - /// ASP.NET Core Identity services. This is useful for applications that handle authentication externally or - /// do not need user management. - /// The type of the Entity Framework Core DbContext to use for data access. - /// The service collection to which the Entity Framework Core services will be added. Cannot be null. - /// The configuration instance containing the connection string. Cannot be null. - /// The name of the connection string in the configuration. Cannot be null or whitespace. - /// The lifetime with which to register the DbContext service. Defaults to ServiceLifetime.Scoped. - /// The same IServiceCollection instance so that additional calls can be chained. - /// Thrown if connectionStringName is null or consists only of white-space characters. - /// Thrown if the specified connection string is not found in the configuration. - public static IServiceCollection AddSqliteDbContext(this IServiceCollection services, IConfiguration configuration, string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) - where TContext : DbContext + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHostBuilder hostBuilder) { - ArgumentNullException.ThrowIfNull(services); - ArgumentNullException.ThrowIfNull(configuration); - - if (string.IsNullOrWhiteSpace(connectionStringName)) + /// Configures the host builder to use web host services with custom service configuration and optional scope validation. + /// This method sets up minimal web host defaults and allows custom service registration for + /// scenarios where a full web application pipeline is not required. It is useful for integrating web host services + /// into generic host scenarios or for testing purposes. + /// A delegate that configures the application's service collection. Invoked with the web host builder context and + /// the service collection. + /// true to enable scope validation for the service provider; otherwise, false. The default is false. + /// The same instance of the host builder for chaining further configuration. + public IHostBuilder UseWebHostServices(Action configureServices, bool validateScopes = false) { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(connectionStringName)); + ArgumentNullException.ThrowIfNull(hostBuilder); + + return hostBuilder.ConfigureWebHostDefaults(webBuilder => + webBuilder.UseDefaultServiceProvider(options => options.ValidateScopes = validateScopes) + .Configure(app => app.Run(async (_) => await Task.CompletedTask)) // Dummy app.Run to prevent 'No application service provider was found' error. + .ConfigureServices((context, services) => configureServices(context, services))); } - var conString = configuration.GetConnectionString(connectionStringName); - services.AddDbContext( - options => options.UseSqlite(conString ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' not found.")), - serviceLifetime); - return services; - } + /// Configures the host builder to use ASP.NET Core web host services with custom service and web host configuration. + /// This method is intended for advanced scenarios where you need to customize both the web host + /// and its services during host building. It sets up a minimal web application to ensure the web host services are + /// available, even if no application logic is provided. + /// A delegate that configures services for the web host. Receives the web host builder context and the service + /// collection. + /// A delegate that further configures the web host builder. Receives and returns an instance of . + /// A value indicating whether to validate service scopes when building the service provider. Set to to enable scope validation; otherwise, . The default is . + /// The same instance for chaining further configuration. + public IHostBuilder UseWebHostServices(Action configureServices, Func configureWebHost, bool validateScopes = false) + { + ArgumentNullException.ThrowIfNull(hostBuilder); - /// - /// Configures Entity Framework Core with SQLite for the specified DbContext using a direct connection string. - /// - /// Use this overload when you have a connection string available directly rather than from - /// configuration. This is useful for testing scenarios or when connection strings are obtained from - /// other sources such as environment variables or secret managers. - /// The type of the Entity Framework Core DbContext to use for data access. - /// The service collection to which the Entity Framework Core services will be added. Cannot be null. - /// The SQLite connection string. Cannot be null or whitespace. - /// The lifetime with which to register the DbContext service. Defaults to ServiceLifetime.Scoped. - /// The same IServiceCollection instance so that additional calls can be chained. - /// Thrown if connectionString is null or consists only of white-space characters. - public static IServiceCollection AddSqliteDbContextWithConnectionString(this IServiceCollection services, string connectionString, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) - where TContext : DbContext - { - ArgumentNullException.ThrowIfNull(services); + return hostBuilder.ConfigureWebHostDefaults(webBuilder => + configureWebHost(webBuilder) + .UseDefaultServiceProvider(options => options.ValidateScopes = validateScopes) + .Configure(app => app.Run(async (_) => await Task.CompletedTask)) // Dummy app.Run to prevent 'No application service provider was found' error. + .ConfigureServices((context, services) => configureServices(context, services))); + } - if (string.IsNullOrWhiteSpace(connectionString)) + /// Configures the specified host builder to use web host services with custom service, web host, and application configuration. + /// This method enables advanced scenarios where you need to customize the web host's services, + /// configuration, and application pipeline within a generic host. It is intended for use when integrating ASP.NET + /// Core web hosting into a generic host setup. The method applies the provided delegates in the order: web host + /// configuration, service configuration, and application pipeline configuration. + /// A delegate that configures services for the web host. Receives the web host builder context and the service + /// collection to configure. + /// A delegate that configures the web host builder. Receives the current web host builder and returns the + /// configured builder. + /// A delegate that configures the application's request pipeline. Receives the application builder and returns the + /// configured builder. + /// true to validate service scopes when building the service provider; otherwise, false. The default is false. + /// The same instance of the host builder for chaining further configuration. + public IHostBuilder UseWebHostServices(Action configureServices, Func configureWebHost, Func configureApp, bool validateScopes = false) { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(connectionString)); - } + ArgumentNullException.ThrowIfNull(hostBuilder); - services.AddDbContext( - options => options.UseSqlite(connectionString), - serviceLifetime); - return services; + return hostBuilder.ConfigureWebHostDefaults(webBuilder => + configureWebHost(webBuilder) + .UseDefaultServiceProvider(options => options.ValidateScopes = validateScopes) + .Configure(app => configureApp(app).Run(async (_) => await Task.CompletedTask)) // Dummy app.Run to prevent 'No application service provider was found' error. + .ConfigureServices((context, services) => configureServices(context, services))); + } } - /// - /// Configures Entity Framework Core with SQLite using the IHostApplicationBuilder pattern. - /// - /// This method provides integration with the modern IHostApplicationBuilder pattern - /// introduced in .NET 7+. It registers the DbContext with SQLite using the connection string - /// from configuration. - /// The type of the Entity Framework Core DbContext to use for data access. - /// The host application builder. Cannot be null. - /// The name of the connection string in the configuration. Cannot be null or whitespace. - /// The lifetime with which to register the DbContext service. Defaults to ServiceLifetime.Scoped. - /// The same IHostApplicationBuilder instance so that additional calls can be chained. - /// Thrown if connectionStringName is null or consists only of white-space characters. - /// Thrown if the specified connection string is not found in the configuration. - public static IHostApplicationBuilder AddSqliteDbContext(this IHostApplicationBuilder builder, string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) - where TContext : DbContext + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IServiceCollection services) { - ArgumentNullException.ThrowIfNull(builder); + /// Configures Entity Framework Core with a SQLite provider and sets up ASP.NET Core Identity using the specified context, user, and role types. + /// This method registers the DbContext with the SQLite provider and configures ASP.NET Core + /// Identity to use the specified user and role types with Entity Framework Core stores. It is typically called + /// during application startup to enable authentication and authorization using SQLite as the backing + /// store. + /// The type of the Entity Framework Core DbContext to use for data access. + /// The type representing application users for ASP.NET Core Identity. + /// The type representing application roles for ASP.NET Core Identity. + /// The web host builder context containing configuration and environment information. Cannot be null. + /// The name of the connection string in the configuration to use for the SQLite database. Cannot be null or + /// whitespace. + /// The lifetime with which to register the DbContext service. The default is Scoped. + /// The same IServiceCollection instance so that additional calls can be chained. + /// Thrown if connectionStringName is null or consists only of white-space characters. + /// Thrown if the specified connection string is not found in the configuration. + public IServiceCollection UseEntityFrameworkCoreSqlite(WebHostBuilderContext context, string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) + where TContext : DbContext + where TUser : class + where TRole : class + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(context); + + ArgumentException.ThrowIfNullOrWhiteSpace(connectionStringName); + + var conString = context.Configuration.GetConnectionString(connectionStringName); + _ = services + .AddDbContext( + options => options.UseSqlite(conString ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' not found.")), + serviceLifetime) + .AddDefaultIdentity() + .AddRoles() + .AddEntityFrameworkStores(); + return services; + } - if (string.IsNullOrWhiteSpace(connectionStringName)) + /// Configures Entity Framework Core with a SQLite provider and ASP.NET Core Identity using the specified DbContext and user type. + /// This method registers the specified DbContext with a SQLite provider and configures ASP.NET + /// Core Identity to use Entity Framework Core stores. It is typically called during application startup to enable + /// authentication and data access using SQLite. + /// The type of the DbContext to use for Entity Framework Core operations. + /// The type representing the user entity for ASP.NET Core Identity. + /// The WebHostBuilderContext containing application configuration and environment information. + /// The name of the connection string in the configuration to use for the SQLite database. Cannot be null or + /// whitespace. + /// The lifetime with which to register the DbContext service. The default is Scoped. + /// The IServiceCollection instance with Entity Framework Core and Identity services configured for SQLite. + /// Thrown if connectionStringName is null or consists only of white-space characters. + /// Thrown if the specified connection string is not found in the configuration. + public IServiceCollection UseEntityFrameworkCoreSqlite(WebHostBuilderContext context, string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) + where TContext : DbContext + where TUser : class { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(connectionStringName)); + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(context); + + ArgumentException.ThrowIfNullOrWhiteSpace(connectionStringName); + + var conString = context.Configuration.GetConnectionString(connectionStringName); + _ = services + .AddDbContext( + options => options.UseSqlite(conString ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' not found.")), + serviceLifetime) + .AddDefaultIdentity() + .AddEntityFrameworkStores(); + return services; } - var conString = builder.Configuration.GetConnectionString(connectionStringName); - builder.Services.AddDbContext( - options => options.UseSqlite(conString ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' not found.")), - serviceLifetime); - return builder; - } + /// Configures Entity Framework Core with SQLite for the specified DbContext without ASP.NET Core Identity. + /// Use this method when you need Entity Framework Core with SQLite but do not require + /// ASP.NET Core Identity services. This is useful for applications that handle authentication externally or + /// do not need user management. + /// The type of the Entity Framework Core DbContext to use for data access. + /// The configuration instance containing the connection string. Cannot be null. + /// The name of the connection string in the configuration. Cannot be null or whitespace. + /// The lifetime with which to register the DbContext service. Defaults to ServiceLifetime.Scoped. + /// The same IServiceCollection instance so that additional calls can be chained. + /// Thrown if connectionStringName is null or consists only of white-space characters. + /// Thrown if the specified connection string is not found in the configuration. + public IServiceCollection AddSqliteDbContext(IConfiguration configuration, string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) + where TContext : DbContext + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configuration); - /// - /// Configures Entity Framework Core with SQLite and ASP.NET Core Identity using IHostApplicationBuilder. - /// - /// This method provides integration with the modern IHostApplicationBuilder pattern and - /// sets up both Entity Framework Core and ASP.NET Core Identity with the specified user and role types. - /// The type of the Entity Framework Core DbContext to use for data access. - /// The type representing application users for ASP.NET Core Identity. - /// The type representing application roles for ASP.NET Core Identity. - /// The host application builder. Cannot be null. - /// The name of the connection string in the configuration. Cannot be null or whitespace. - /// The lifetime with which to register the DbContext service. Defaults to ServiceLifetime.Scoped. - /// The same IHostApplicationBuilder instance so that additional calls can be chained. - /// Thrown if connectionStringName is null or consists only of white-space characters. - /// Thrown if the specified connection string is not found in the configuration. - public static IHostApplicationBuilder AddSqliteWithIdentity(this IHostApplicationBuilder builder, string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) - where TContext : DbContext - where TUser : class - where TRole : class - { - ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrWhiteSpace(connectionStringName); - if (string.IsNullOrWhiteSpace(connectionStringName)) - { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(connectionStringName)); + var conString = configuration.GetConnectionString(connectionStringName); + _ = services.AddDbContext( + options => options.UseSqlite(conString ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' not found.")), + serviceLifetime); + return services; } - var conString = builder.Configuration.GetConnectionString(connectionStringName); - builder.Services - .AddDbContext( - options => options.UseSqlite(conString ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' not found.")), - serviceLifetime) - .AddDefaultIdentity() - .AddRoles() - .AddEntityFrameworkStores(); - return builder; - } + /// Configures Entity Framework Core with SQLite for the specified DbContext using a direct connection string. + /// Use this overload when you have a connection string available directly rather than from + /// configuration. This is useful for testing scenarios or when connection strings are obtained from + /// other sources such as environment variables or secret managers. + /// The type of the Entity Framework Core DbContext to use for data access. + /// The SQLite connection string. Cannot be null or whitespace. + /// The lifetime with which to register the DbContext service. Defaults to ServiceLifetime.Scoped. + /// The same IServiceCollection instance so that additional calls can be chained. + /// Thrown if connectionString is null or consists only of white-space characters. + public IServiceCollection AddSqliteDbContextWithConnectionString(string connectionString, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) + where TContext : DbContext + { + ArgumentNullException.ThrowIfNull(services); - /// - /// Configures Entity Framework Core with SQLite and ASP.NET Core Identity (user only) using IHostApplicationBuilder. - /// - /// This method provides integration with the modern IHostApplicationBuilder pattern and - /// sets up both Entity Framework Core and ASP.NET Core Identity with the specified user type only. - /// The type of the Entity Framework Core DbContext to use for data access. - /// The type representing application users for ASP.NET Core Identity. - /// The host application builder. Cannot be null. - /// The name of the connection string in the configuration. Cannot be null or whitespace. - /// The lifetime with which to register the DbContext service. Defaults to ServiceLifetime.Scoped. - /// The same IHostApplicationBuilder instance so that additional calls can be chained. - /// Thrown if connectionStringName is null or consists only of white-space characters. - /// Thrown if the specified connection string is not found in the configuration. - public static IHostApplicationBuilder AddSqliteWithIdentity(this IHostApplicationBuilder builder, string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) - where TContext : DbContext - where TUser : class - { - ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrWhiteSpace(connectionString); - if (string.IsNullOrWhiteSpace(connectionStringName)) - { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(connectionStringName)); + _ = services.AddDbContext( + options => options.UseSqlite(connectionString), + serviceLifetime); + return services; } - - var conString = builder.Configuration.GetConnectionString(connectionStringName); - builder.Services - .AddDbContext( - options => options.UseSqlite(conString ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' not found.")), - serviceLifetime) - .AddDefaultIdentity() - .AddEntityFrameworkStores(); - return builder; } - /// - /// Creates a SQLite connection string for an in-memory database. - /// + /// Creates a SQLite connection string for an in-memory database. /// In-memory SQLite databases are useful for testing scenarios where you need a fast, /// isolated database that doesn't persist to disk. Note that in-memory databases only persist /// as long as the connection remains open. @@ -274,17 +305,12 @@ public static IHostApplicationBuilder AddSqliteWithIdentity(thi /// A SQLite connection string for an in-memory database. public static string CreateInMemoryConnectionString(string? databaseName = null) { - if (string.IsNullOrWhiteSpace(databaseName)) - { - return "DataSource=:memory:"; - } - - return $"DataSource={databaseName};Mode=Memory;Cache=Shared"; + return string.IsNullOrWhiteSpace(databaseName) + ? "DataSource=:memory:" + : $"DataSource={databaseName};Mode=Memory;Cache=Shared"; } - /// - /// Creates a SQLite connection string for a file-based database. - /// + /// Creates a SQLite connection string for a file-based database. /// Use this helper to create properly formatted SQLite connection strings for /// file-based databases. The path can be absolute or relative. /// The path to the SQLite database file. Cannot be null or whitespace. @@ -292,88 +318,8 @@ public static string CreateInMemoryConnectionString(string? databaseName = null) /// Thrown if filePath is null or consists only of white-space characters. public static string CreateFileConnectionString(string filePath) { - if (string.IsNullOrWhiteSpace(filePath)) - { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(filePath)); - } + ArgumentException.ThrowIfNullOrWhiteSpace(filePath); return $"DataSource={filePath}"; } - - /// - /// Configures the host builder to use web host services with custom service configuration and optional scope - /// validation. - /// - /// This method sets up minimal web host defaults and allows custom service registration for - /// scenarios where a full web application pipeline is not required. It is useful for integrating web host services - /// into generic host scenarios or for testing purposes. - /// The host builder to configure. Cannot be null. - /// A delegate that configures the application's service collection. Invoked with the web host builder context and - /// the service collection. - /// true to enable scope validation for the service provider; otherwise, false. The default is false. - /// The same instance of the host builder for chaining further configuration. - public static IHostBuilder UseWebHostServices(this IHostBuilder hostBuilder, Action configureServices, bool validateScopes = false) - { - ArgumentNullException.ThrowIfNull(hostBuilder); - - return hostBuilder.ConfigureWebHostDefaults(webBuilder => - webBuilder.UseDefaultServiceProvider(options => options.ValidateScopes = validateScopes) - .Configure(app => app.Run(async (_) => await Task.CompletedTask)) // Dummy app.Run to prevent 'No application service provider was found' error. - .ConfigureServices((context, services) => configureServices(context, services))); - } - - /// - /// Configures the host builder to use ASP.NET Core web host services with custom service and web host - /// configuration. - /// - /// This method is intended for advanced scenarios where you need to customize both the web host - /// and its services during host building. It sets up a minimal web application to ensure the web host services are - /// available, even if no application logic is provided. - /// The host builder to configure. Cannot be null. - /// A delegate that configures services for the web host. Receives the web host builder context and the service - /// collection. - /// A delegate that further configures the web host builder. Receives and returns an instance of . - /// A value indicating whether to validate service scopes when building the service provider. Set to to enable scope validation; otherwise, . The default is . - /// The same instance for chaining further configuration. - public static IHostBuilder UseWebHostServices(this IHostBuilder hostBuilder, Action configureServices, Func configureWebHost, bool validateScopes = false) - { - ArgumentNullException.ThrowIfNull(hostBuilder); - - return hostBuilder.ConfigureWebHostDefaults(webBuilder => - configureWebHost(webBuilder) - .UseDefaultServiceProvider(options => options.ValidateScopes = validateScopes) - .Configure(app => app.Run(async (_) => await Task.CompletedTask)) // Dummy app.Run to prevent 'No application service provider was found' error. - .ConfigureServices((context, services) => configureServices(context, services))); - } - - /// - /// Configures the specified host builder to use web host services with custom service, web host, and application - /// configuration. - /// - /// This method enables advanced scenarios where you need to customize the web host's services, - /// configuration, and application pipeline within a generic host. It is intended for use when integrating ASP.NET - /// Core web hosting into a generic host setup. The method applies the provided delegates in the order: web host - /// configuration, service configuration, and application pipeline configuration. - /// The host builder to configure. Cannot be null. - /// A delegate that configures services for the web host. Receives the web host builder context and the service - /// collection to configure. - /// A delegate that configures the web host builder. Receives the current web host builder and returns the - /// configured builder. - /// A delegate that configures the application's request pipeline. Receives the application builder and returns the - /// configured builder. - /// true to validate service scopes when building the service provider; otherwise, false. The default is false. - /// The same instance of the host builder for chaining further configuration. - public static IHostBuilder UseWebHostServices(this IHostBuilder hostBuilder, Action configureServices, Func configureWebHost, Func configureApp, bool validateScopes = false) - { - ArgumentNullException.ThrowIfNull(hostBuilder); - - return hostBuilder.ConfigureWebHostDefaults(webBuilder => - configureWebHost(webBuilder) - .UseDefaultServiceProvider(options => options.ValidateScopes = validateScopes) - .Configure(app => configureApp(app).Run(async (_) => await Task.CompletedTask)) // Dummy app.Run to prevent 'No application service provider was found' error. - .ConfigureServices((context, services) => configureServices(context, services))); - } } diff --git a/src/Extensions.Hosting.Identity.EntityFrameworkCore/Extensions.Hosting.Identity.EntityFrameworkCore.SqlServer.csproj b/src/Extensions.Hosting.Identity.EntityFrameworkCore/Extensions.Hosting.Identity.EntityFrameworkCore.SqlServer.csproj index 09d2ad9..baf9e9e 100644 --- a/src/Extensions.Hosting.Identity.EntityFrameworkCore/Extensions.Hosting.Identity.EntityFrameworkCore.SqlServer.csproj +++ b/src/Extensions.Hosting.Identity.EntityFrameworkCore/Extensions.Hosting.Identity.EntityFrameworkCore.SqlServer.csproj @@ -1,46 +1,61 @@ - + - net8.0;net9.0;net10.0 + net8.0;net9.0;net10.0;net11.0 enable enable CP.Extensions.Hosting.Identity.EntityFrameworkCore.SqlServer - + - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Extensions.Hosting.Identity.EntityFrameworkCore/HostBuilderEntityFrameworkCoreExtensions.cs b/src/Extensions.Hosting.Identity.EntityFrameworkCore/HostBuilderEntityFrameworkCoreExtensions.cs index 7ec1072..5b8c0a5 100644 --- a/src/Extensions.Hosting.Identity.EntityFrameworkCore/HostBuilderEntityFrameworkCoreExtensions.cs +++ b/src/Extensions.Hosting.Identity.EntityFrameworkCore/HostBuilderEntityFrameworkCoreExtensions.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.AspNetCore.Builder; @@ -11,374 +11,326 @@ namespace ReactiveMarbles.Extensions.Hosting.Identity.EntityFrameworkCore; -/// -/// Provides extension methods for configuring Entity Framework Core with SQL Server and web host services on ASP.NET -/// Core host builders and service collections. -/// +/// Provides extension methods for configuring Entity Framework Core with SQL Server and web host services on ASP.NET Core host builders and service collections. /// These extensions simplify the setup of Entity Framework Core and related identity services in ASP.NET /// Core applications, as well as the configuration of web host services for integration and testing scenarios. Methods /// in this class are intended to be used during application startup to register required services and configure the /// host environment. public static class HostBuilderEntityFrameworkCoreExtensions { - /// - /// Configures Entity Framework Core with SQL Server and ASP.NET Core Identity using the specified context, user, - /// and role types, and registers the required services in the dependency injection container. - /// - /// This method sets up Entity Framework Core to use SQL Server as the database provider and - /// configures ASP.NET Core Identity with the specified user and role types. It is typically called during - /// application startup to register data access and identity services. The method also adds the Entity Framework - /// stores for Identity, enabling user and role management backed by the specified DbContext. - /// The type of the Entity Framework Core DbContext to use for data access. - /// The type representing application users for ASP.NET Core Identity. Must be a reference type. - /// The type representing application roles for ASP.NET Core Identity. Must be a reference type. - /// The service collection to which the Entity Framework Core and Identity services will be added. Cannot be null. - /// The web host builder context containing configuration and environment information. Cannot be null. - /// The name of the connection string in the application's configuration to use for the SQL Server database. Cannot - /// be null or whitespace. - /// The lifetime with which to register the DbContext service. Defaults to ServiceLifetime.Scoped. - /// The same IServiceCollection instance so that additional calls can be chained. - /// Thrown if connectionStringName is null or consists only of white-space characters. - /// Thrown if the specified connection string is not found in the configuration. - public static IServiceCollection UseEntityFrameworkCoreSqlServer(this IServiceCollection services, WebHostBuilderContext context, string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) - where TContext : DbContext - where TUser : class - where TRole : class + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IConfiguration configuration) { - ArgumentNullException.ThrowIfNull(services); - ArgumentNullException.ThrowIfNull(context); - - if (string.IsNullOrWhiteSpace(connectionStringName)) + /// Validates that a connection string exists in the configuration. + /// The name of the connection string to validate. + /// True if the connection string exists and is not empty; otherwise, false. + public bool HasConnectionString(string connectionStringName) { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(connectionStringName)); - } + ArgumentNullException.ThrowIfNull(configuration); - var conString = context.Configuration.GetConnectionString(connectionStringName); - services - .AddDbContext( - options => options.UseSqlServer(conString ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' not found.")), - serviceLifetime) - .AddDefaultIdentity() - .AddRoles() - .AddEntityFrameworkStores(); - return services; - } + if (string.IsNullOrWhiteSpace(connectionStringName)) + { + return false; + } - /// - /// Configures Entity Framework Core to use SQL Server with the specified DbContext and identity user types, and - /// registers the necessary services for identity and data access. - /// - /// This method adds the DbContext, ASP.NET Core Identity, and Entity Framework stores to the - /// service collection, enabling authentication and data access using SQL Server. The connection string must be - /// defined in the application's configuration under the provided name. - /// The type of the DbContext to register for use with SQL Server. - /// The type of the user entity to use with ASP.NET Core Identity. - /// The service collection to which the Entity Framework Core and identity services will be added. Cannot be null. - /// The web host builder context containing configuration information. Cannot be null. - /// The name of the connection string in the configuration to use for the SQL Server database. Cannot be null or - /// whitespace. - /// The lifetime with which to register the DbContext service. The default is Scoped. - /// The IServiceCollection instance with Entity Framework Core and identity services configured for SQL Server. - /// Thrown if connectionStringName is null or consists only of white-space characters. - /// Thrown if the specified connection string is not found in the configuration. - public static IServiceCollection UseEntityFrameworkCoreSqlServer(this IServiceCollection services, WebHostBuilderContext context, string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) - where TContext : DbContext - where TUser : class - { - ArgumentNullException.ThrowIfNull(services); - ArgumentNullException.ThrowIfNull(context); + var connectionString = configuration.GetConnectionString(connectionStringName); + return !string.IsNullOrWhiteSpace(connectionString); + } - if (string.IsNullOrWhiteSpace(connectionStringName)) + /// Gets a connection string from configuration, throwing a descriptive exception if not found. + /// The name of the connection string to retrieve. + /// The connection string value. + /// Thrown if the connection string is not found or is empty. + public string GetRequiredConnectionString(string connectionStringName) { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(connectionStringName)); - } + ArgumentNullException.ThrowIfNull(configuration); - var conString = context.Configuration.GetConnectionString(connectionStringName); - services - .AddDbContext( - options => options.UseSqlServer(conString ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' not found.")), - serviceLifetime) - .AddDefaultIdentity() - .AddEntityFrameworkStores(); - return services; - } + ArgumentException.ThrowIfNullOrWhiteSpace(connectionStringName); - /// - /// Configures Entity Framework Core with SQL Server for the specified DbContext without ASP.NET Core Identity. - /// - /// Use this method when you need Entity Framework Core with SQL Server but do not require - /// ASP.NET Core Identity services. This is useful for applications that handle authentication externally or - /// do not need user management. - /// The type of the Entity Framework Core DbContext to use for data access. - /// The service collection to which the Entity Framework Core services will be added. Cannot be null. - /// The configuration instance containing the connection string. Cannot be null. - /// The name of the connection string in the configuration. Cannot be null or whitespace. - /// The lifetime with which to register the DbContext service. Defaults to ServiceLifetime.Scoped. - /// The same IServiceCollection instance so that additional calls can be chained. - /// Thrown if connectionStringName is null or consists only of white-space characters. - /// Thrown if the specified connection string is not found in the configuration. - public static IServiceCollection AddSqlServerDbContext(this IServiceCollection services, IConfiguration configuration, string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) - where TContext : DbContext - { - ArgumentNullException.ThrowIfNull(services); - ArgumentNullException.ThrowIfNull(configuration); + var connectionString = configuration.GetConnectionString(connectionStringName); + if (string.IsNullOrWhiteSpace(connectionString)) + { + throw new InvalidOperationException($"Connection string '{connectionStringName}' not found in configuration. Ensure it is defined in appsettings.json or environment variables under 'ConnectionStrings:{connectionStringName}'."); + } - if (string.IsNullOrWhiteSpace(connectionStringName)) - { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(connectionStringName)); + return connectionString; } - - var conString = configuration.GetConnectionString(connectionStringName); - services.AddDbContext( - options => options.UseSqlServer(conString ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' not found.")), - serviceLifetime); - return services; } - /// - /// Configures Entity Framework Core with SQL Server for the specified DbContext using a direct connection string. - /// - /// Use this overload when you have a connection string available directly rather than from - /// configuration. This is useful for testing scenarios or when connection strings are obtained from - /// other sources such as environment variables or secret managers. - /// The type of the Entity Framework Core DbContext to use for data access. - /// The service collection to which the Entity Framework Core services will be added. Cannot be null. - /// The SQL Server connection string. Cannot be null or whitespace. - /// The lifetime with which to register the DbContext service. Defaults to ServiceLifetime.Scoped. - /// The same IServiceCollection instance so that additional calls can be chained. - /// Thrown if connectionString is null or consists only of white-space characters. - public static IServiceCollection AddSqlServerDbContextWithConnectionString(this IServiceCollection services, string connectionString, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) - where TContext : DbContext + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHostApplicationBuilder builder) { - ArgumentNullException.ThrowIfNull(services); - - if (string.IsNullOrWhiteSpace(connectionString)) + /// Configures Entity Framework Core with SQL Server using the IHostApplicationBuilder pattern. + /// This method provides integration with the modern IHostApplicationBuilder pattern + /// introduced in .NET 7+. It registers the DbContext with SQL Server using the connection string + /// from configuration. + /// The type of the Entity Framework Core DbContext to use for data access. + /// The name of the connection string in the configuration. Cannot be null or whitespace. + /// The lifetime with which to register the DbContext service. Defaults to ServiceLifetime.Scoped. + /// The same IHostApplicationBuilder instance so that additional calls can be chained. + /// Thrown if connectionStringName is null or consists only of white-space characters. + /// Thrown if the specified connection string is not found in the configuration. + public IHostApplicationBuilder AddSqlServerDbContext(string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) + where TContext : DbContext { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(connectionString)); - } + ArgumentNullException.ThrowIfNull(builder); - services.AddDbContext( - options => options.UseSqlServer(connectionString), - serviceLifetime); - return services; - } + ArgumentException.ThrowIfNullOrWhiteSpace(connectionStringName); - /// - /// Configures Entity Framework Core with SQL Server using the IHostApplicationBuilder pattern. - /// - /// This method provides integration with the modern IHostApplicationBuilder pattern - /// introduced in .NET 7+. It registers the DbContext with SQL Server using the connection string - /// from configuration. - /// The type of the Entity Framework Core DbContext to use for data access. - /// The host application builder. Cannot be null. - /// The name of the connection string in the configuration. Cannot be null or whitespace. - /// The lifetime with which to register the DbContext service. Defaults to ServiceLifetime.Scoped. - /// The same IHostApplicationBuilder instance so that additional calls can be chained. - /// Thrown if connectionStringName is null or consists only of white-space characters. - /// Thrown if the specified connection string is not found in the configuration. - public static IHostApplicationBuilder AddSqlServerDbContext(this IHostApplicationBuilder builder, string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) - where TContext : DbContext - { - ArgumentNullException.ThrowIfNull(builder); + var conString = builder.Configuration.GetConnectionString(connectionStringName); + _ = builder.Services.AddDbContext( + options => options.UseSqlServer(conString ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' not found.")), + serviceLifetime); + return builder; + } - if (string.IsNullOrWhiteSpace(connectionStringName)) + /// Configures Entity Framework Core with SQL Server and ASP.NET Core Identity using IHostApplicationBuilder. + /// This method provides integration with the modern IHostApplicationBuilder pattern and + /// sets up both Entity Framework Core and ASP.NET Core Identity with the specified user and role types. + /// The type of the Entity Framework Core DbContext to use for data access. + /// The type representing application users for ASP.NET Core Identity. + /// The type representing application roles for ASP.NET Core Identity. + /// The name of the connection string in the configuration. Cannot be null or whitespace. + /// The lifetime with which to register the DbContext service. Defaults to ServiceLifetime.Scoped. + /// The same IHostApplicationBuilder instance so that additional calls can be chained. + /// Thrown if connectionStringName is null or consists only of white-space characters. + /// Thrown if the specified connection string is not found in the configuration. + public IHostApplicationBuilder AddSqlServerWithIdentity(string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) + where TContext : DbContext + where TUser : class + where TRole : class { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(connectionStringName)); + ArgumentNullException.ThrowIfNull(builder); + + ArgumentException.ThrowIfNullOrWhiteSpace(connectionStringName); + + var conString = builder.Configuration.GetConnectionString(connectionStringName); + _ = builder.Services + .AddDbContext( + options => options.UseSqlServer(conString ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' not found.")), + serviceLifetime) + .AddDefaultIdentity() + .AddRoles() + .AddEntityFrameworkStores(); + return builder; } - var conString = builder.Configuration.GetConnectionString(connectionStringName); - builder.Services.AddDbContext( - options => options.UseSqlServer(conString ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' not found.")), - serviceLifetime); - return builder; - } - - /// - /// Configures Entity Framework Core with SQL Server and ASP.NET Core Identity using IHostApplicationBuilder. - /// - /// This method provides integration with the modern IHostApplicationBuilder pattern and - /// sets up both Entity Framework Core and ASP.NET Core Identity with the specified user and role types. - /// The type of the Entity Framework Core DbContext to use for data access. - /// The type representing application users for ASP.NET Core Identity. - /// The type representing application roles for ASP.NET Core Identity. - /// The host application builder. Cannot be null. - /// The name of the connection string in the configuration. Cannot be null or whitespace. - /// The lifetime with which to register the DbContext service. Defaults to ServiceLifetime.Scoped. - /// The same IHostApplicationBuilder instance so that additional calls can be chained. - /// Thrown if connectionStringName is null or consists only of white-space characters. - /// Thrown if the specified connection string is not found in the configuration. - public static IHostApplicationBuilder AddSqlServerWithIdentity(this IHostApplicationBuilder builder, string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) - where TContext : DbContext - where TUser : class - where TRole : class - { - ArgumentNullException.ThrowIfNull(builder); - - if (string.IsNullOrWhiteSpace(connectionStringName)) + /// Configures Entity Framework Core with SQL Server and ASP.NET Core Identity (user only) using IHostApplicationBuilder. + /// This method provides integration with the modern IHostApplicationBuilder pattern and + /// sets up both Entity Framework Core and ASP.NET Core Identity with the specified user type only. + /// The type of the Entity Framework Core DbContext to use for data access. + /// The type representing application users for ASP.NET Core Identity. + /// The name of the connection string in the configuration. Cannot be null or whitespace. + /// The lifetime with which to register the DbContext service. Defaults to ServiceLifetime.Scoped. + /// The same IHostApplicationBuilder instance so that additional calls can be chained. + /// Thrown if connectionStringName is null or consists only of white-space characters. + /// Thrown if the specified connection string is not found in the configuration. + public IHostApplicationBuilder AddSqlServerWithIdentity(string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) + where TContext : DbContext + where TUser : class { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(connectionStringName)); + ArgumentNullException.ThrowIfNull(builder); + + ArgumentException.ThrowIfNullOrWhiteSpace(connectionStringName); + + var conString = builder.Configuration.GetConnectionString(connectionStringName); + _ = builder.Services + .AddDbContext( + options => options.UseSqlServer(conString ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' not found.")), + serviceLifetime) + .AddDefaultIdentity() + .AddEntityFrameworkStores(); + return builder; } - - var conString = builder.Configuration.GetConnectionString(connectionStringName); - builder.Services - .AddDbContext( - options => options.UseSqlServer(conString ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' not found.")), - serviceLifetime) - .AddDefaultIdentity() - .AddRoles() - .AddEntityFrameworkStores(); - return builder; } - /// - /// Configures Entity Framework Core with SQL Server and ASP.NET Core Identity (user only) using IHostApplicationBuilder. - /// - /// This method provides integration with the modern IHostApplicationBuilder pattern and - /// sets up both Entity Framework Core and ASP.NET Core Identity with the specified user type only. - /// The type of the Entity Framework Core DbContext to use for data access. - /// The type representing application users for ASP.NET Core Identity. - /// The host application builder. Cannot be null. - /// The name of the connection string in the configuration. Cannot be null or whitespace. - /// The lifetime with which to register the DbContext service. Defaults to ServiceLifetime.Scoped. - /// The same IHostApplicationBuilder instance so that additional calls can be chained. - /// Thrown if connectionStringName is null or consists only of white-space characters. - /// Thrown if the specified connection string is not found in the configuration. - public static IHostApplicationBuilder AddSqlServerWithIdentity(this IHostApplicationBuilder builder, string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) - where TContext : DbContext - where TUser : class + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHostBuilder hostBuilder) { - ArgumentNullException.ThrowIfNull(builder); - - if (string.IsNullOrWhiteSpace(connectionStringName)) + /// Configures the host to use web host services and allows additional service configuration for the web host builder. + /// This method sets up the web host with default configurations and applies the specified + /// service configuration. It is useful for scenarios where you want to customize the web host's service + /// registrations within a generic host setup. + /// A delegate that configures services for the web host. Receives the web host builder context and the service + /// collection to configure. + /// true to validate service scopes when building the service provider; otherwise, false. The default is false. + /// The same instance of the host builder for chaining further configuration. + public IHostBuilder UseWebHostServices(Action configureServices, bool validateScopes = false) { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(connectionStringName)); + ArgumentNullException.ThrowIfNull(hostBuilder); + + return hostBuilder.ConfigureWebHostDefaults(webBuilder => + webBuilder.UseDefaultServiceProvider(options => options.ValidateScopes = validateScopes) + .Configure(app => app.Run(async (_) => await Task.CompletedTask)) // Dummy app.Run to prevent 'No application service provider was found' error. + .ConfigureServices((context, services) => configureServices(context, services))); } - var conString = builder.Configuration.GetConnectionString(connectionStringName); - builder.Services - .AddDbContext( - options => options.UseSqlServer(conString ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' not found.")), - serviceLifetime) - .AddDefaultIdentity() - .AddEntityFrameworkStores(); - return builder; - } + /// Configures the specified host builder to use ASP.NET Core web host services with custom service and web host configuration. + /// This method is intended for advanced scenarios where custom service registration and web host + /// configuration are required. It provides a way to integrate ASP.NET Core web hosting features into a generic host + /// builder pipeline. + /// A delegate that configures services for the web host. Receives the web host builder context and the service + /// collection. + /// A delegate that configures the web host builder. Receives the current web host builder and returns the + /// configured instance. + /// true to enable scope validation for the default service provider; otherwise, false. The default is false. + /// The same IHostBuilder instance for chaining further configuration. + public IHostBuilder UseWebHostServices(Action configureServices, Func configureWebHost, bool validateScopes = false) + { + ArgumentNullException.ThrowIfNull(hostBuilder); - /// - /// Validates that a connection string exists in the configuration. - /// - /// The configuration instance to check. - /// The name of the connection string to validate. - /// True if the connection string exists and is not empty; otherwise, false. - public static bool HasConnectionString(this IConfiguration configuration, string connectionStringName) - { - ArgumentNullException.ThrowIfNull(configuration); + return hostBuilder.ConfigureWebHostDefaults(webBuilder => + configureWebHost(webBuilder) + .UseDefaultServiceProvider(options => options.ValidateScopes = validateScopes) + .Configure(app => app.Run(async (_) => await Task.CompletedTask)) // Dummy app.Run to prevent 'No application service provider was found' error. + .ConfigureServices((context, services) => configureServices(context, services))); + } - if (string.IsNullOrWhiteSpace(connectionStringName)) + /// Configures the host builder to use web host services with custom service, web host, and application configurations. + /// This method allows advanced customization of the web host's service collection, web host + /// builder, and application pipeline during host configuration. It is intended for scenarios where the default web + /// host setup needs to be extended or replaced with custom logic. + /// A delegate that configures services for the web host. Receives the web host builder context and the service + /// collection to configure. + /// A delegate that configures the web host builder. Receives the current web host builder and returns the + /// configured builder. + /// A delegate that configures the application builder. Receives the application builder and returns the configured + /// builder. + /// true to validate service scopes when building the service provider; otherwise, false. The default is false. + /// The same IHostBuilder instance for chaining further configuration. + public IHostBuilder UseWebHostServices(Action configureServices, Func configureWebHost, Func configureApp, bool validateScopes = false) { - return false; - } + ArgumentNullException.ThrowIfNull(hostBuilder); - var connectionString = configuration.GetConnectionString(connectionStringName); - return !string.IsNullOrWhiteSpace(connectionString); + return hostBuilder.ConfigureWebHostDefaults(webBuilder => + configureWebHost(webBuilder) + .UseDefaultServiceProvider(options => options.ValidateScopes = validateScopes) + .Configure(app => configureApp(app).Run(async (_) => await Task.CompletedTask)) // Dummy app.Run to prevent 'No application service provider was found' error. + .ConfigureServices((context, services) => configureServices(context, services))); + } } - /// - /// Gets a connection string from configuration, throwing a descriptive exception if not found. - /// - /// The configuration instance. - /// The name of the connection string to retrieve. - /// The connection string value. - /// Thrown if the connection string is not found or is empty. - public static string GetRequiredConnectionString(this IConfiguration configuration, string connectionStringName) + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IServiceCollection services) { - ArgumentNullException.ThrowIfNull(configuration); - - if (string.IsNullOrWhiteSpace(connectionStringName)) + /// Configures Entity Framework Core with SQL Server and ASP.NET Core Identity using the specified context, user, and role types, and registers the required services in the dependency injection container. + /// This method sets up Entity Framework Core to use SQL Server as the database provider and + /// configures ASP.NET Core Identity with the specified user and role types. It is typically called during + /// application startup to register data access and identity services. The method also adds the Entity Framework + /// stores for Identity, enabling user and role management backed by the specified DbContext. + /// The type of the Entity Framework Core DbContext to use for data access. + /// The type representing application users for ASP.NET Core Identity. Must be a reference type. + /// The type representing application roles for ASP.NET Core Identity. Must be a reference type. + /// The web host builder context containing configuration and environment information. Cannot be null. + /// The name of the connection string in the application's configuration to use for the SQL Server database. Cannot + /// be null or whitespace. + /// The lifetime with which to register the DbContext service. Defaults to ServiceLifetime.Scoped. + /// The same IServiceCollection instance so that additional calls can be chained. + /// Thrown if connectionStringName is null or consists only of white-space characters. + /// Thrown if the specified connection string is not found in the configuration. + public IServiceCollection UseEntityFrameworkCoreSqlServer(WebHostBuilderContext context, string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) + where TContext : DbContext + where TUser : class + where TRole : class { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(connectionStringName)); + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(context); + + ArgumentException.ThrowIfNullOrWhiteSpace(connectionStringName); + + var conString = context.Configuration.GetConnectionString(connectionStringName); + _ = services + .AddDbContext( + options => options.UseSqlServer(conString ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' not found.")), + serviceLifetime) + .AddDefaultIdentity() + .AddRoles() + .AddEntityFrameworkStores(); + return services; } - var connectionString = configuration.GetConnectionString(connectionStringName); - if (string.IsNullOrWhiteSpace(connectionString)) + /// Configures Entity Framework Core to use SQL Server with the specified DbContext and identity user types, and registers the necessary services for identity and data access. + /// This method adds the DbContext, ASP.NET Core Identity, and Entity Framework stores to the + /// service collection, enabling authentication and data access using SQL Server. The connection string must be + /// defined in the application's configuration under the provided name. + /// The type of the DbContext to register for use with SQL Server. + /// The type of the user entity to use with ASP.NET Core Identity. + /// The web host builder context containing configuration information. Cannot be null. + /// The name of the connection string in the configuration to use for the SQL Server database. Cannot be null or + /// whitespace. + /// The lifetime with which to register the DbContext service. The default is Scoped. + /// The IServiceCollection instance with Entity Framework Core and identity services configured for SQL Server. + /// Thrown if connectionStringName is null or consists only of white-space characters. + /// Thrown if the specified connection string is not found in the configuration. + public IServiceCollection UseEntityFrameworkCoreSqlServer(WebHostBuilderContext context, string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) + where TContext : DbContext + where TUser : class { - throw new InvalidOperationException($"Connection string '{connectionStringName}' not found in configuration. Ensure it is defined in appsettings.json or environment variables under 'ConnectionStrings:{connectionStringName}'."); + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(context); + + ArgumentException.ThrowIfNullOrWhiteSpace(connectionStringName); + + var conString = context.Configuration.GetConnectionString(connectionStringName); + _ = services + .AddDbContext( + options => options.UseSqlServer(conString ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' not found.")), + serviceLifetime) + .AddDefaultIdentity() + .AddEntityFrameworkStores(); + return services; } - return connectionString; - } - - /// - /// Configures the host to use web host services and allows additional service configuration for the web host - /// builder. - /// - /// This method sets up the web host with default configurations and applies the specified - /// service configuration. It is useful for scenarios where you want to customize the web host's service - /// registrations within a generic host setup. - /// The host builder to configure. Cannot be null. - /// A delegate that configures services for the web host. Receives the web host builder context and the service - /// collection to configure. - /// true to validate service scopes when building the service provider; otherwise, false. The default is false. - /// The same instance of the host builder for chaining further configuration. - public static IHostBuilder UseWebHostServices(this IHostBuilder hostBuilder, Action configureServices, bool validateScopes = false) - { - ArgumentNullException.ThrowIfNull(hostBuilder); + /// Configures Entity Framework Core with SQL Server for the specified DbContext without ASP.NET Core Identity. + /// Use this method when you need Entity Framework Core with SQL Server but do not require + /// ASP.NET Core Identity services. This is useful for applications that handle authentication externally or + /// do not need user management. + /// The type of the Entity Framework Core DbContext to use for data access. + /// The configuration instance containing the connection string. Cannot be null. + /// The name of the connection string in the configuration. Cannot be null or whitespace. + /// The lifetime with which to register the DbContext service. Defaults to ServiceLifetime.Scoped. + /// The same IServiceCollection instance so that additional calls can be chained. + /// Thrown if connectionStringName is null or consists only of white-space characters. + /// Thrown if the specified connection string is not found in the configuration. + public IServiceCollection AddSqlServerDbContext(IConfiguration configuration, string connectionStringName, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) + where TContext : DbContext + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configuration); - return hostBuilder.ConfigureWebHostDefaults(webBuilder => - webBuilder.UseDefaultServiceProvider(options => options.ValidateScopes = validateScopes) - .Configure(app => app.Run(async (_) => await Task.CompletedTask)) // Dummy app.Run to prevent 'No application service provider was found' error. - .ConfigureServices((context, services) => configureServices(context, services))); - } + ArgumentException.ThrowIfNullOrWhiteSpace(connectionStringName); - /// - /// Configures the specified host builder to use ASP.NET Core web host services with custom service and web host - /// configuration. - /// - /// This method is intended for advanced scenarios where custom service registration and web host - /// configuration are required. It provides a way to integrate ASP.NET Core web hosting features into a generic host - /// builder pipeline. - /// The host builder to configure. Cannot be null. - /// A delegate that configures services for the web host. Receives the web host builder context and the service - /// collection. - /// A delegate that configures the web host builder. Receives the current web host builder and returns the - /// configured instance. - /// true to enable scope validation for the default service provider; otherwise, false. The default is false. - /// The same IHostBuilder instance for chaining further configuration. - public static IHostBuilder UseWebHostServices(this IHostBuilder hostBuilder, Action configureServices, Func configureWebHost, bool validateScopes = false) - { - ArgumentNullException.ThrowIfNull(hostBuilder); + var conString = configuration.GetConnectionString(connectionStringName); + _ = services.AddDbContext( + options => options.UseSqlServer(conString ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' not found.")), + serviceLifetime); + return services; + } - return hostBuilder.ConfigureWebHostDefaults(webBuilder => - configureWebHost(webBuilder) - .UseDefaultServiceProvider(options => options.ValidateScopes = validateScopes) - .Configure(app => app.Run(async (_) => await Task.CompletedTask)) // Dummy app.Run to prevent 'No application service provider was found' error. - .ConfigureServices((context, services) => configureServices(context, services))); - } + /// Configures Entity Framework Core with SQL Server for the specified DbContext using a direct connection string. + /// Use this overload when you have a connection string available directly rather than from + /// configuration. This is useful for testing scenarios or when connection strings are obtained from + /// other sources such as environment variables or secret managers. + /// The type of the Entity Framework Core DbContext to use for data access. + /// The SQL Server connection string. Cannot be null or whitespace. + /// The lifetime with which to register the DbContext service. Defaults to ServiceLifetime.Scoped. + /// The same IServiceCollection instance so that additional calls can be chained. + /// Thrown if connectionString is null or consists only of white-space characters. + public IServiceCollection AddSqlServerDbContextWithConnectionString(string connectionString, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) + where TContext : DbContext + { + ArgumentNullException.ThrowIfNull(services); - /// - /// Configures the host builder to use web host services with custom service, web host, and application - /// configurations. - /// - /// This method allows advanced customization of the web host's service collection, web host - /// builder, and application pipeline during host configuration. It is intended for scenarios where the default web - /// host setup needs to be extended or replaced with custom logic. - /// The host builder to configure. Cannot be null. - /// A delegate that configures services for the web host. Receives the web host builder context and the service - /// collection to configure. - /// A delegate that configures the web host builder. Receives the current web host builder and returns the - /// configured builder. - /// A delegate that configures the application builder. Receives the application builder and returns the configured - /// builder. - /// true to validate service scopes when building the service provider; otherwise, false. The default is false. - /// The same IHostBuilder instance for chaining further configuration. - public static IHostBuilder UseWebHostServices(this IHostBuilder hostBuilder, Action configureServices, Func configureWebHost, Func configureApp, bool validateScopes = false) - { - ArgumentNullException.ThrowIfNull(hostBuilder); + ArgumentException.ThrowIfNullOrWhiteSpace(connectionString); - return hostBuilder.ConfigureWebHostDefaults(webBuilder => - configureWebHost(webBuilder) - .UseDefaultServiceProvider(options => options.ValidateScopes = validateScopes) - .Configure(app => configureApp(app).Run(async (_) => await Task.CompletedTask)) // Dummy app.Run to prevent 'No application service provider was found' error. - .ConfigureServices((context, services) => configureServices(context, services))); + _ = services.AddDbContext( + options => options.UseSqlServer(connectionString), + serviceLifetime); + return services; + } } } diff --git a/src/Extensions.Hosting.MainUIThread/BaseUiContext.cs b/src/Extensions.Hosting.MainUIThread/BaseUiContext.cs index a51eb2b..543bb2e 100644 --- a/src/Extensions.Hosting.MainUIThread/BaseUiContext.cs +++ b/src/Extensions.Hosting.MainUIThread/BaseUiContext.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. namespace ReactiveMarbles.Extensions.Hosting.UiThread; diff --git a/src/Extensions.Hosting.MainUIThread/BaseUiThread.cs b/src/Extensions.Hosting.MainUIThread/BaseUiThread.cs index 281fdd0..7c424a2 100644 --- a/src/Extensions.Hosting.MainUIThread/BaseUiThread.cs +++ b/src/Extensions.Hosting.MainUIThread/BaseUiThread.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -9,10 +9,7 @@ namespace ReactiveMarbles.Extensions.Hosting.UiThread; -/// -/// Provides a base class for managing a UI thread and its associated context using dependency injection. Intended for -/// use with UI frameworks that require operations to run on a dedicated thread. -/// +/// Provides a base class for managing a UI thread and its associated context using dependency injection. Intended for use with UI frameworks that require operations to run on a dedicated thread. /// This class is designed to be inherited by platform-specific UI thread implementations. It manages the /// lifecycle of a UI thread, including initialization, startup synchronization, and graceful shutdown. The class uses /// dependency injection to provide services and context to the UI thread. Derived classes must implement the : IDisposable where T : class, IUiContext { - private readonly System.Threading.ManualResetEventSlim _serviceManualResetEvent = new(false); + /// Stores the service manual reset event value. + private readonly ManualResetEventSlim _serviceManualResetEvent = new(false); + + /// Stores the host application lifetime value. private readonly IHostApplicationLifetime? _hostApplicationLifetime; + + /// Stores the use dedicated ui thread value. private readonly bool _useDedicatedUiThread; + + /// Stores the disposed value. private bool _disposedValue; - /// - /// Initializes a new instance of the class and starts a dedicated UI thread using the specified. - /// service provider. - /// + /// Initializes a new instance of the class and starts a dedicated UI thread using the specified. service provider. /// The constructor creates and starts a new background thread to run the UI. On Windows /// platforms, the thread is set to single-threaded apartment (STA) state to support UI frameworks that require it. /// The provided service provider is used to resolve dependencies needed by the UI thread and is stored for later @@ -70,19 +71,13 @@ protected BaseUiThread(IServiceProvider serviceProvider, bool useDedicatedUiThre newUiThread.Start(); } - /// - /// Gets the UI context associated with the current instance. - /// + /// Gets the UI context associated with the current instance. protected T UiContext { get; } - /// - /// Gets the service provider used to resolve application services. - /// + /// Gets the service provider used to resolve application services. protected IServiceProvider ServiceProvider { get; } - /// - /// Signals the service to begin processing or resume operation. - /// + /// Signals the service to begin processing or resume operation. /// Call this method to allow the service to proceed if it is waiting for a start signal. This /// method is typically used to control the execution flow of a service that waits for an external trigger before /// starting. @@ -97,9 +92,7 @@ public void Start() InternalUiThreadStart(); } - /// - /// Releases all resources used by the current instance of the class. - /// + /// Releases all resources used by the current instance of the class. /// Call this method when you are finished using the object to release unmanaged resources and /// perform other cleanup operations. After calling Dispose, the object should not be used further. public void Dispose() @@ -109,25 +102,19 @@ public void Dispose() GC.SuppressFinalize(this); } - /// - /// Performs custom initialization logic before the UI thread starts. - /// + /// Performs custom initialization logic before the UI thread starts. /// Override this method to execute any setup required prior to launching the UI thread. This /// method is called during the application startup sequence, before any UI components are created or /// shown. protected abstract void PreUiThreadStart(); - /// - /// Executes the main logic for the UI thread. Called when the UI thread is started. - /// + /// Executes the main logic for the UI thread. Called when the UI thread is started. /// Override this method to implement the operations that should run on the UI thread. This /// method is invoked on the thread designated for UI processing and typically contains the application's message /// loop or event handling logic. protected abstract void UiThreadStart(); - /// - /// Handles application exit by updating the UI context and initiating application shutdown if appropriate. - /// + /// Handles application exit by updating the UI context and initiating application shutdown if appropriate. /// This method sets the UI context to indicate that the application is no longer running. If the /// UI context is configured to link its lifetime to the host application, this method will request application /// shutdown unless the application is already stopping or has stopped. Intended to be called during application @@ -148,27 +135,24 @@ protected void HandleApplicationExit() _hostApplicationLifetime?.StopApplication(); } - /// - /// Releases unmanaged and - optionally - managed resources. - /// + /// Releases unmanaged and - optionally - managed resources. /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool disposing) { - if (!_disposedValue) + if (_disposedValue) { - if (disposing) - { - _serviceManualResetEvent.Dispose(); - } + return; + } - _disposedValue = true; + if (disposing) + { + _serviceManualResetEvent.Dispose(); } + + _disposedValue = true; } - /// - /// Initializes and starts the UI thread, performing any required pre-initialization and waiting for the service to - /// signal readiness before running the main UI logic. - /// + /// Initializes and starts the UI thread, performing any required pre-initialization and waiting for the service to signal readiness before running the main UI logic. /// This method is intended for internal use to coordinate the startup sequence of the UI thread. /// It ensures that any necessary pre-initialization is completed and that the service is ready before the UI thread /// begins execution. This method should not be called directly by user code. diff --git a/src/Extensions.Hosting.MainUIThread/Extensions.Hosting.MainUIThread.csproj b/src/Extensions.Hosting.MainUIThread/Extensions.Hosting.MainUIThread.csproj index 1ff5605..4528c24 100644 --- a/src/Extensions.Hosting.MainUIThread/Extensions.Hosting.MainUIThread.csproj +++ b/src/Extensions.Hosting.MainUIThread/Extensions.Hosting.MainUIThread.csproj @@ -1,12 +1,12 @@ - + - net462;net472;net481;net8.0;net9.0;net10.0;net8.0-windows;net9.0-windows;net10.0-windows + net462;net472;net48;net481;net8.0;net9.0;net10.0;net11.0;net8.0-windows;net9.0-windows;net10.0-windows;net11.0-windows CP.Extensions.Hosting.MainUIThread - + diff --git a/src/Extensions.Hosting.MainUIThread/IUiContext.cs b/src/Extensions.Hosting.MainUIThread/IUiContext.cs index b886493..b3aa266 100644 --- a/src/Extensions.Hosting.MainUIThread/IUiContext.cs +++ b/src/Extensions.Hosting.MainUIThread/IUiContext.cs @@ -1,25 +1,18 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. namespace ReactiveMarbles.Extensions.Hosting.UiThread; -/// -/// Defines the contract for managing the lifecycle and running state of a user interface (UI) application within a host -/// environment. -/// +/// Defines the contract for managing the lifecycle and running state of a user interface (UI) application within a host environment. /// Implementations of this interface allow coordination between the UI application's lifecycle and the /// host application's lifecycle. This is useful for scenarios where the UI and host application need to be started, /// stopped, or monitored in tandem. public interface IUiContext { - /// - /// Gets or sets a value indicating whether defines if the host application is stopped when the UI applications stops. - /// + /// Gets or sets a value indicating whether defines if the host application is stopped when the UI applications stops. bool IsLifetimeLinked { get; set; } - /// - /// Gets or sets a value indicating whether is the application started and still running?. - /// + /// Gets or sets a value indicating whether is the application started and still running?. bool IsRunning { get; set; } } diff --git a/src/Extensions.Hosting.Maui/Extensions.Hosting.Maui.csproj b/src/Extensions.Hosting.Maui/Extensions.Hosting.Maui.csproj index b6e34b6..c216667 100644 --- a/src/Extensions.Hosting.Maui/Extensions.Hosting.Maui.csproj +++ b/src/Extensions.Hosting.Maui/Extensions.Hosting.Maui.csproj @@ -1,8 +1,8 @@ - + - net9.0;net10.0;net9.0-android;net10.0-android;net9.0-ios;net9.0-tvos;net9.0-macos;net9.0-maccatalyst;net10.0-ios;net10.0-tvos;net10.0-macos;net10.0-maccatalyst; - $(TargetFrameworks);net9.0-windows10.0.19041.0;net10.0-windows10.0.19041.0 + net9.0;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 true This extension adds MAUI support to generic host based applications. With this you can enhance your application with a UI, and use all the services provided by the generic host like DI, logging etc. CP.Extensions.Hosting.Maui @@ -13,13 +13,18 @@ - - + + + + + + + - - + + diff --git a/src/Extensions.Hosting.Maui/HostBuilderMauiExtensions.cs b/src/Extensions.Hosting.Maui/HostBuilderMauiExtensions.cs index 6623e24..164331a 100644 --- a/src/Extensions.Hosting.Maui/HostBuilderMauiExtensions.cs +++ b/src/Extensions.Hosting.Maui/HostBuilderMauiExtensions.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -11,9 +11,7 @@ namespace ReactiveMarbles.Extensions.Hosting.Maui; -/// -/// Provides extension methods for configuring and integrating .NET MAUI applications with generic host builders. -/// +/// Provides extension methods for configuring and integrating .NET MAUI applications with generic host builders. /// These extensions enable seamless setup of MAUI applications using dependency injection and host-based /// lifecycles. They support both traditional IHostBuilder and the newer IHostApplicationBuilder APIs, allowing /// developers to configure MAUI services, set the application shell, and link the application lifetime to the host. Use @@ -21,259 +19,236 @@ namespace ReactiveMarbles.Extensions.Hosting.Maui; /// with .NET MAUI and the generic host. public static class HostBuilderMauiExtensions { + /// Stores the maui context key value. private const string MauiContextKey = nameof(MauiContext); - /// - /// Configures the host builder to use the .NET MAUI application lifetime, enabling integration with the MAUI app - /// lifecycle. - /// - /// Call this method during host configuration to ensure that the application's lifetime is - /// managed according to .NET MAUI conventions. This is typically required for MAUI apps to handle startup and - /// shutdown events correctly. - /// The host builder to configure for MAUI lifetime support. Cannot be null. - /// The same instance of with MAUI lifetime services configured, or if is null. - public static IHostBuilder? UseMauiLifetime(this IHostBuilder hostBuilder) => - hostBuilder?.ConfigureServices((_, __) => - { - TryRetrieveMauiContext(hostBuilder.Properties, out var mauiContext); - mauiContext.IsLifetimeLinked = true; - }); - - /// - /// Enables the MAUI-specific application lifetime integration for the specified host builder. - /// - /// This method configures the host builder to use the MAUI application lifetime, which manages - /// the application's startup and shutdown events in a manner compatible with .NET MAUI. Call this method when - /// building a MAUI app to ensure correct lifetime management. - /// The host builder to configure with MAUI lifetime support. Cannot be null. - /// The same instance of for chaining further configuration. - public static IHostApplicationBuilder UseMauiLifetime(this IHostApplicationBuilder hostBuilder) + /// Attempts to retrieve an existing IMauiContext instance from the specified property dictionary. + /// If the IMauiContext is not present in the dictionary, this method creates a new instance, + /// assigns it to the out parameter, and adds it to the dictionary for future retrieval. + /// The property dictionary used to store the context. + /// When this method returns, contains the IMauiContext instance retrieved from the dictionary if found; otherwise, + /// a new IMauiContext instance. + /// true if an existing IMauiContext was found in the dictionary; otherwise, false. + private static bool TryRetrieveMauiContext(IDictionary properties, out IMauiContext mauiContext) { - ArgumentNullException.ThrowIfNull(hostBuilder); + if (properties.TryGetValue(MauiContextKey, out var mauiContextAsObject)) + { + mauiContext = (IMauiContext)mauiContextAsObject; + return true; + } - TryRetrieveMauiContext(hostBuilder.Properties, out var mauiContext); - mauiContext.IsLifetimeLinked = true; - return hostBuilder; + mauiContext = new MauiContext(); + properties[MauiContextKey] = mauiContext; + return false; } - /// - /// Configures the .NET MAUI application and related services for the specified host builder. - /// - /// Call this method to add .NET MAUI support to a generic host builder, enabling dependency - /// injection and service registration for MAUI applications. This method should be called before building the host. - /// If an application type is registered, it must inherit from . - /// The host builder to configure for .NET MAUI support. Cannot be null. - /// An optional delegate to further configure the MAUI application, pages, and services before they are registered - /// with the host builder. May be null. - /// The same instance of with .NET MAUI services and configuration applied. This enables - /// further chaining of host builder configuration methods. - /// Thrown if the application type registered via does not inherit from . - public static IHostBuilder ConfigureMaui(this IHostBuilder hostBuilder, Action? configureDelegate = null) + /// Uses the currently running MAUI application instance when it matches the configured application type. + /// The builder that contains application configuration. + private static void CaptureCurrentMauiApplication(MauiBuilder mauiBuilder) { - ArgumentNullException.ThrowIfNull(hostBuilder); + if (mauiBuilder.ApplicationType is null || mauiBuilder.Application is not null || Application.Current?.GetType() != mauiBuilder.ApplicationType) + { + return; + } - var mauiBuilder = new MauiBuilder(); - configureDelegate?.Invoke(mauiBuilder); + mauiBuilder.Application = Application.Current; + } - if (mauiBuilder.ApplicationType != null && mauiBuilder.Application == null && Application.Current?.GetType() == mauiBuilder.ApplicationType) + /// Registers the core MAUI hosting services. + /// The service collection to register services into. + /// The MAUI context instance to register. + private static void RegisterMauiHostingServices(IServiceCollection services, IMauiContext mauiContext) => + _ = services + .AddSingleton(mauiContext) + .AddSingleton(serviceProvider => new MauiThread(serviceProvider)) + .AddHostedService(); + + /// Registers a configured MAUI application type or instance. + /// The service collection to register services into. + /// The builder that contains the application configuration. + /// The public parameter name used for exception reporting. + /// Thrown if the configured application type does not derive from . + private static void RegisterMauiApplication(IServiceCollection services, MauiBuilder mauiBuilder, string parameterName) + { + if (mauiBuilder.ApplicationType is null) { - mauiBuilder.Application = Application.Current; + return; } - hostBuilder.ConfigureServices((_, serviceCollection) => + var baseApplicationType = typeof(Application); + if (!baseApplicationType.IsAssignableFrom(mauiBuilder.ApplicationType)) { - if (!TryRetrieveMauiContext(hostBuilder.Properties, out var mauiContext)) - { - serviceCollection - .AddSingleton(mauiContext) - .AddSingleton(serviceProvider => new MauiThread(serviceProvider)) - .AddHostedService(); - } - - mauiBuilder.ConfigureContextAction?.Invoke(mauiContext); - }); + throw new ArgumentException("The registered Application type must inherit Microsoft.Maui.Controls.Application", parameterName); + } - if (mauiBuilder.ApplicationType != null) + if (mauiBuilder.Application is not null) { - // Check if the registered application does inherit Microsoft.Maui.Controls.Application - var baseApplicationType = typeof(Application); - if (!baseApplicationType.IsAssignableFrom(mauiBuilder.ApplicationType)) - { - throw new ArgumentException("The registered Application type must inherit Microsoft.Maui.Controls.Application", nameof(configureDelegate)); - } - - hostBuilder.ConfigureServices((_, serviceCollection) => - { - if (mauiBuilder.Application != null) - { - // Add existing Application - serviceCollection.AddSingleton(mauiBuilder.ApplicationType, mauiBuilder.Application); - } - else - { - serviceCollection.AddSingleton(mauiBuilder.ApplicationType); - } - - if (mauiBuilder.ApplicationType != baseApplicationType) - { - serviceCollection.AddSingleton(serviceProvider => (Application)serviceProvider.GetRequiredService(mauiBuilder.ApplicationType)); - } - }); + _ = services.AddSingleton(mauiBuilder.ApplicationType, mauiBuilder.Application); + } + else + { + _ = services.AddSingleton(mauiBuilder.ApplicationType); } - if (mauiBuilder.PageTypes.Count > 0) + if (mauiBuilder.ApplicationType == baseApplicationType) { - hostBuilder.ConfigureServices(serviceCollection => - { - foreach (var mauiPageType in mauiBuilder.PageTypes) - { - serviceCollection.AddSingleton(mauiPageType); - - // Check if it also implements IMauiShell so we can register it as this - var shellInterfaceType = typeof(IMauiShell); - if (shellInterfaceType.IsAssignableFrom(mauiPageType)) - { - serviceCollection.AddSingleton(shellInterfaceType, serviceProvider => serviceProvider.GetRequiredService(mauiPageType)); - } - } - }); + return; } - return hostBuilder; + _ = services.AddSingleton(serviceProvider => (Application)serviceProvider.GetRequiredService(mauiBuilder.ApplicationType)); } - /// - /// Configures the .NET MAUI services and application types for the specified host builder. - /// - /// This method should be called during application startup to ensure that all required MAUI - /// services and application types are registered with the dependency injection container. If an existing - /// Application instance is available, it will be registered; otherwise, the Application type will be registered for - /// instantiation by the container. - /// The host builder to configure with .NET MAUI services. Cannot be null. - /// An optional delegate to further configure the MAUI builder before services are registered. If null, default - /// configuration is applied. - /// The same host builder instance, configured with .NET MAUI services and application types. - /// Thrown if the registered Application type does not inherit from Microsoft.Maui.Controls.Application. - public static IHostApplicationBuilder ConfigureMaui(this IHostApplicationBuilder hostBuilder, Action? configureDelegate = null) + /// Registers configured MAUI pages and shell mappings. + /// The service collection to register services into. + /// The builder that contains the page configuration. + private static void RegisterMauiPages(IServiceCollection services, MauiBuilder mauiBuilder) { - ArgumentNullException.ThrowIfNull(hostBuilder); + foreach (var mauiPageType in mauiBuilder.PageTypes) + { + _ = services.AddSingleton(mauiPageType); - var mauiBuilder = new MauiBuilder(); - configureDelegate?.Invoke(mauiBuilder); + var shellInterfaceType = typeof(IMauiShell); + if (!shellInterfaceType.IsAssignableFrom(mauiPageType)) + { + continue; + } - if (mauiBuilder.ApplicationType != null && mauiBuilder.Application == null && Application.Current?.GetType() == mauiBuilder.ApplicationType) - { - mauiBuilder.Application = Application.Current; + _ = services.AddSingleton(shellInterfaceType, serviceProvider => serviceProvider.GetRequiredService(mauiPageType)); } + } - if (!TryRetrieveMauiContext(hostBuilder.Properties, out var mauiContext)) + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHostApplicationBuilder hostBuilder) + { + /// Enables the MAUI-specific application lifetime integration for the specified host builder. + /// This method configures the host builder to use the MAUI application lifetime, which manages + /// the application's startup and shutdown events in a manner compatible with .NET MAUI. Call this method when + /// building a MAUI app to ensure correct lifetime management. + /// The same instance of for chaining further configuration. + public IHostApplicationBuilder UseMauiLifetime() { - hostBuilder.Services - .AddSingleton(mauiContext) - .AddSingleton(serviceProvider => new MauiThread(serviceProvider)) - .AddHostedService(); - } + ArgumentNullException.ThrowIfNull(hostBuilder); - mauiBuilder.ConfigureContextAction?.Invoke(mauiContext); + _ = TryRetrieveMauiContext(hostBuilder.Properties, out var mauiContext); + mauiContext.IsLifetimeLinked = true; + return hostBuilder; + } - if (mauiBuilder.ApplicationType != null) + /// Configures the .NET MAUI services and application types for the specified host builder. + /// This method should be called during application startup to ensure that all required MAUI + /// services and application types are registered with the dependency injection container. If an existing + /// Application instance is available, it will be registered; otherwise, the Application type will be registered for + /// instantiation by the container. + /// An optional delegate to further configure the MAUI builder before services are registered. If null, default + /// configuration is applied. + /// The same host builder instance, configured with .NET MAUI services and application types. + /// Thrown if the registered Application type does not inherit from Microsoft.Maui.Controls.Application. + public IHostApplicationBuilder ConfigureMaui(Action? configureDelegate = null) { - // Check if the registered application does inherit Microsoft.Maui.Controls.Application - var baseApplicationType = typeof(Application); - if (!baseApplicationType.IsAssignableFrom(mauiBuilder.ApplicationType)) - { - throw new ArgumentException("The registered Application type must inherit Microsoft.Maui.Controls.Application", nameof(configureDelegate)); - } + ArgumentNullException.ThrowIfNull(hostBuilder); - if (mauiBuilder.Application != null) - { - // Add existing Application - hostBuilder.Services.AddSingleton(mauiBuilder.ApplicationType, mauiBuilder.Application); - } - else - { - hostBuilder.Services.AddSingleton(mauiBuilder.ApplicationType); - } + var mauiBuilder = new MauiBuilder(); + configureDelegate?.Invoke(mauiBuilder); + + CaptureCurrentMauiApplication(mauiBuilder); - if (mauiBuilder.ApplicationType != baseApplicationType) + if (!TryRetrieveMauiContext(hostBuilder.Properties, out var mauiContext)) { - hostBuilder.Services.AddSingleton(serviceProvider => (Application)serviceProvider.GetRequiredService(mauiBuilder.ApplicationType)); + RegisterMauiHostingServices(hostBuilder.Services, mauiContext); } + + mauiBuilder.ConfigureContextAction?.Invoke(mauiContext); + RegisterMauiApplication(hostBuilder.Services, mauiBuilder, nameof(configureDelegate)); + RegisterMauiPages(hostBuilder.Services, mauiBuilder); + + var app = mauiBuilder.MauiAppBuilder.Build(); + _ = hostBuilder.Services.AddSingleton(app); + + return hostBuilder; } - if (mauiBuilder.PageTypes.Count > 0) - { - foreach (var mauiPageType in mauiBuilder.PageTypes) + /// Configures the specified host builder to use a custom shell page as the application's main navigation shell. + /// This method registers the specified shell page type as a singleton and sets it as the root + /// navigation shell for the application. Use this method to customize the application's navigation structure by + /// providing your own implementation of IMauiShell. + /// The type of the shell page to use as the application's main navigation shell. Must implement the IMauiShell + /// interface and derive from Page. + /// The same IHostApplicationBuilder instance, enabling further configuration. + public IHostApplicationBuilder ConfigureMauiShell() + where TShell : Page, IMauiShell + => hostBuilder.ConfigureMaui(maui => maui.AddSingletonPage()); + } + + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHostBuilder hostBuilder) + { + /// Configures the host builder to use the .NET MAUI application lifetime, enabling integration with the MAUI app lifecycle. + /// Call this method during host configuration to ensure that the application's lifetime is + /// managed according to .NET MAUI conventions. This is typically required for MAUI apps to handle startup and + /// shutdown events correctly. + /// The same instance of with MAUI lifetime services configured, or if is null. + public IHostBuilder? UseMauiLifetime() => + hostBuilder?.ConfigureServices((context, services) => { - hostBuilder.Services.AddSingleton(mauiPageType); + _ = TryRetrieveMauiContext(hostBuilder.Properties, out var mauiContext); + mauiContext.IsLifetimeLinked = true; + }); + + /// Configures the .NET MAUI application and related services for the specified host builder. + /// Call this method to add .NET MAUI support to a generic host builder, enabling dependency + /// injection and service registration for MAUI applications. This method should be called before building the host. + /// If an application type is registered, it must inherit from . + /// An optional delegate to further configure the MAUI application, pages, and services before they are registered + /// with the host builder. May be null. + /// The same instance of with .NET MAUI services and configuration applied. This enables + /// further chaining of host builder configuration methods. + /// Thrown if the application type registered via does not inherit from . + public IHostBuilder ConfigureMaui(Action? configureDelegate = null) + { + ArgumentNullException.ThrowIfNull(hostBuilder); + + var mauiBuilder = new MauiBuilder(); + configureDelegate?.Invoke(mauiBuilder); - // Check if it also implements IMauiShell so we can register it as this - var shellInterfaceType = typeof(IMauiShell); - if (shellInterfaceType.IsAssignableFrom(mauiPageType)) + CaptureCurrentMauiApplication(mauiBuilder); + + _ = hostBuilder.ConfigureServices((context, serviceCollection) => + { + if (!TryRetrieveMauiContext(hostBuilder.Properties, out var mauiContext)) { - hostBuilder.Services.AddSingleton(shellInterfaceType, serviceProvider => serviceProvider.GetRequiredService(mauiPageType)); + RegisterMauiHostingServices(serviceCollection, mauiContext); } - } - } - var app = mauiBuilder.MauiAppBuilder.Build(); - hostBuilder.Services.AddSingleton(app); + mauiBuilder.ConfigureContextAction?.Invoke(mauiContext); + }); - return hostBuilder; - } + if (mauiBuilder.ApplicationType is not null) + { + _ = hostBuilder.ConfigureServices((context, serviceCollection) => RegisterMauiApplication(serviceCollection, mauiBuilder, nameof(configureDelegate))); + } - /// - /// Configures the specified host builder to use a singleton instance of the specified shell page type as the - /// application's main shell. - /// - /// This method registers the specified shell page type as a singleton in the dependency - /// injection container, enabling it to serve as the application's main navigation shell. Use this method during - /// application startup to set up the shell for a .NET MAUI app. - /// The type of the shell page to use as the application's main shell. Must implement the IMauiShell interface and - /// derive from Page. - /// The host builder to configure. Cannot be null. - /// The configured host builder instance, or null if the input host builder is null. - public static IHostBuilder? ConfigureMauiShell(this IHostBuilder hostBuilder) - where TShell : Page, IMauiShell - => hostBuilder?.ConfigureMaui(maui => maui.AddSingletonPage()); - - /// - /// Configures the specified host builder to use a custom shell page as the application's main navigation shell. - /// - /// This method registers the specified shell page type as a singleton and sets it as the root - /// navigation shell for the application. Use this method to customize the application's navigation structure by - /// providing your own implementation of IMauiShell. - /// The type of the shell page to use as the application's main navigation shell. Must implement the IMauiShell - /// interface and derive from Page. - /// The host application builder to configure. - /// The same IHostApplicationBuilder instance, enabling further configuration. - public static IHostApplicationBuilder ConfigureMauiShell(this IHostApplicationBuilder hostBuilder) - where TShell : Page, IMauiShell - => hostBuilder.ConfigureMaui(maui => maui.AddSingletonPage()); - - /// - /// Attempts to retrieve an existing IMauiContext instance from the specified property dictionary. - /// - /// If the IMauiContext is not present in the dictionary, this method creates a new instance, - /// assigns it to the out parameter, and adds it to the dictionary for future retrieval. - /// The dictionary containing property values, typically used to store and retrieve context-specific data. Cannot be - /// null. - /// When this method returns, contains the IMauiContext instance retrieved from the dictionary if found; otherwise, - /// a new IMauiContext instance. - /// true if an existing IMauiContext was found in the dictionary; otherwise, false. - private static bool TryRetrieveMauiContext(this IDictionary properties, out IMauiContext mauiContext) - { - if (properties.TryGetValue(MauiContextKey, out var mauiContextAsObject)) - { - mauiContext = (IMauiContext)mauiContextAsObject; - return true; + if (mauiBuilder.PageTypes.Count > 0) + { + _ = hostBuilder.ConfigureServices(serviceCollection => RegisterMauiPages(serviceCollection, mauiBuilder)); + } + + return hostBuilder; } - mauiContext = new MauiContext(); - properties[MauiContextKey] = mauiContext; - return false; + /// Configures the specified host builder to use a singleton instance of the specified shell page type as the application's main shell. + /// This method registers the specified shell page type as a singleton in the dependency + /// injection container, enabling it to serve as the application's main navigation shell. Use this method during + /// application startup to set up the shell for a .NET MAUI app. + /// The type of the shell page to use as the application's main shell. Must implement the IMauiShell interface and + /// derive from Page. + /// The configured host builder instance, or null if the input host builder is null. + public IHostBuilder? ConfigureMauiShell() + where TShell : Page, IMauiShell + => hostBuilder?.ConfigureMaui(maui => maui.AddSingletonPage()); } } diff --git a/src/Extensions.Hosting.Maui/IMauiBuilder.cs b/src/Extensions.Hosting.Maui/IMauiBuilder.cs index 3492d75..56c5398 100644 --- a/src/Extensions.Hosting.Maui/IMauiBuilder.cs +++ b/src/Extensions.Hosting.Maui/IMauiBuilder.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -9,41 +9,29 @@ namespace ReactiveMarbles.Extensions.Hosting.Maui; -/// -/// Defines a builder interface for configuring and constructing a .NET MAUI application. -/// +/// Defines a builder interface for configuring and constructing a .NET MAUI application. /// The IMauiBuilder interface provides properties and configuration hooks for setting up a MAUI /// application, including specifying the application type, registering pages, and customizing the application context. /// Implementations of this interface are typically used during application startup to prepare and build the MAUI app /// instance. public interface IMauiBuilder { - /// - /// Gets or sets the type of the application associated with this instance. - /// + /// Gets or sets the type of the application associated with this instance. Type? ApplicationType { get; set; } - /// - /// Gets or sets the application associated with the current context. - /// + /// Gets or sets the application associated with the current context. Application? Application { get; set; } - /// - /// Gets the collection of page types supported by the current context. - /// + /// Gets the collection of page types supported by the current context. IList PageTypes { get; } - /// - /// Gets or sets an action that configures the provided before it is used. - /// + /// Gets or sets an action that configures the provided before it is used. /// Use this property to customize the instance, such as registering /// services or modifying context-specific settings, prior to its consumption by dependent components. If not set, /// the context will be used without additional configuration. Action? ConfigureContextAction { get; set; } - /// - /// Gets the builder used to configure and create the .NET MAUI application instance. - /// + /// Gets the builder used to configure and create the .NET MAUI application instance. /// Use this property to access the underlying builder for registering services, configuring app /// settings, or customizing the application startup process before building the final app instance. MauiAppBuilder MauiAppBuilder { get; } diff --git a/src/Extensions.Hosting.Maui/IMauiContext.cs b/src/Extensions.Hosting.Maui/IMauiContext.cs index 6926146..e57f5d9 100644 --- a/src/Extensions.Hosting.Maui/IMauiContext.cs +++ b/src/Extensions.Hosting.Maui/IMauiContext.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.Maui.Controls; @@ -8,24 +8,17 @@ namespace ReactiveMarbles.Extensions.Hosting.Maui; -/// -/// Provides contextual services and resources for .NET MAUI applications, including access to the current application -/// and dispatcher. -/// +/// Provides contextual services and resources for .NET MAUI applications, including access to the current application and dispatcher. /// IMauiContext is used to supply platform-specific context and services required by .NET MAUI /// components. It enables access to the current Application instance and dispatcher, which are essential for /// interacting with the application lifecycle and performing UI operations on the correct thread. public interface IMauiContext : IUiContext { - /// - /// Gets or sets the current .NET MAUI application instance associated with the host environment. - /// + /// Gets or sets the current .NET MAUI application instance associated with the host environment. /// This property is typically used to access or assign the main application object in a .NET /// MAUI app. Setting this property allows integration with the MAUI application lifecycle and services. Application? MauiApplication { get; set; } - /// - /// Gets the dispatcher associated with the current context, if any. - /// + /// Gets the dispatcher associated with the current context, if any. IDispatcher? Dispatcher { get; } } diff --git a/src/Extensions.Hosting.Maui/IMauiService.cs b/src/Extensions.Hosting.Maui/IMauiService.cs index f831b79..3c30e99 100644 --- a/src/Extensions.Hosting.Maui/IMauiService.cs +++ b/src/Extensions.Hosting.Maui/IMauiService.cs @@ -1,23 +1,18 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.Maui.Controls; namespace ReactiveMarbles.Extensions.Hosting.Maui; -/// -/// Defines a service that provides initialization logic for a .NET MAUI application. Implementations perform necessary -/// setup when the application starts. -/// +/// Defines a service that provides initialization logic for a .NET MAUI application. Implementations perform necessary setup when the application starts. /// The Initialize method is called from the UI thread and should contain any platform-specific or /// application-wide initialization required before the app runs. Implementers should ensure that any long-running or /// blocking operations are avoided to prevent UI delays. public interface IMauiService { - /// - /// Initializes the specified application instance, preparing it for use. - /// + /// Initializes the specified application instance, preparing it for use. /// The application instance to initialize. Cannot be null. void Initialize(Application application); } diff --git a/src/Extensions.Hosting.Maui/IMauiShell.cs b/src/Extensions.Hosting.Maui/IMauiShell.cs index a580570..f0796e2 100644 --- a/src/Extensions.Hosting.Maui/IMauiShell.cs +++ b/src/Extensions.Hosting.Maui/IMauiShell.cs @@ -1,14 +1,13 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using Microsoft.Maui; + namespace ReactiveMarbles.Extensions.Hosting.Maui; -/// -/// Represents a contract for a Shell implementation in a .NET MAUI application, providing the structure and navigation -/// model for multi-page apps. -/// +/// Represents a contract for a Shell implementation in a .NET MAUI application, providing the structure and navigation model for multi-page apps. /// Implementations of this interface define the application's navigation hierarchy, visual structure, /// and routing logic using the Shell paradigm. This interface is typically used internally by the .NET MAUI framework /// and is not intended to be implemented directly by application code. -public interface IMauiShell; +public interface IMauiShell : IView; diff --git a/src/Extensions.Hosting.Maui/Internals/MauiBuilder.cs b/src/Extensions.Hosting.Maui/Internals/MauiBuilder.cs index e879c7c..d2ed7b7 100644 --- a/src/Extensions.Hosting.Maui/Internals/MauiBuilder.cs +++ b/src/Extensions.Hosting.Maui/Internals/MauiBuilder.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -10,7 +10,7 @@ namespace ReactiveMarbles.Extensions.Hosting.Maui.Internals; /// -internal class MauiBuilder : IMauiBuilder +internal sealed class MauiBuilder : IMauiBuilder { /// public Type? ApplicationType { get; set; } @@ -24,9 +24,7 @@ internal class MauiBuilder : IMauiBuilder /// public Action? ConfigureContextAction { get; set; } - /// - /// Gets the maui application builder. - /// + /// Gets the maui application builder. /// /// The maui application builder. /// diff --git a/src/Extensions.Hosting.Maui/Internals/MauiContext.cs b/src/Extensions.Hosting.Maui/Internals/MauiContext.cs index 7e9beb8..26eebd2 100644 --- a/src/Extensions.Hosting.Maui/Internals/MauiContext.cs +++ b/src/Extensions.Hosting.Maui/Internals/MauiContext.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.Maui.Controls; @@ -8,7 +8,7 @@ namespace ReactiveMarbles.Extensions.Hosting.Maui.Internals; /// -internal class MauiContext : IMauiContext +internal sealed class MauiContext : IMauiContext { /// public bool IsLifetimeLinked { get; set; } diff --git a/src/Extensions.Hosting.Maui/Internals/MauiThread.cs b/src/Extensions.Hosting.Maui/Internals/MauiThread.cs index d4672fb..d7c4581 100644 --- a/src/Extensions.Hosting.Maui/Internals/MauiThread.cs +++ b/src/Extensions.Hosting.Maui/Internals/MauiThread.cs @@ -1,24 +1,20 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; -using System.Linq; using Microsoft.Extensions.DependencyInjection; using Microsoft.Maui.Controls; using ReactiveMarbles.Extensions.Hosting.UiThread; namespace ReactiveMarbles.Extensions.Hosting.Maui.Internals; -/// -/// Provides a UI thread implementation for .NET MAUI applications, enabling initialization and management of the MAUI -/// application lifecycle within a host environment. -/// +/// Provides a UI thread implementation for .NET MAUI applications, enabling initialization and management of the MAUI application lifecycle within a host environment. /// This class is typically used to host and manage a MAUI application's main UI thread in scenarios /// where integration with a custom host or dependency injection container is required. It ensures that the MAUI /// application and its services are properly initialized and managed on the correct thread. /// The service provider used to resolve application and service dependencies required by the MAUI UI thread. -public partial class MauiThread(IServiceProvider serviceProvider) : BaseUiThread(serviceProvider) +public class MauiThread(IServiceProvider serviceProvider) : BaseUiThread(serviceProvider) { /// protected override void PreUiThreadStart() @@ -43,8 +39,7 @@ protected override void UiThreadStart() => UiContext.IsRunning = true; // Use the provided IMauiService - var mauiServices = ServiceProvider.GetServices(); - foreach (var mauiService in mauiServices) + foreach (var mauiService in ServiceProvider.GetServices()) { mauiService.Initialize(UiContext.MauiApplication); } diff --git a/src/Extensions.Hosting.Maui/MauiBuilderExtensions.cs b/src/Extensions.Hosting.Maui/MauiBuilderExtensions.cs index 477ed46..b394ac0 100644 --- a/src/Extensions.Hosting.Maui/MauiBuilderExtensions.cs +++ b/src/Extensions.Hosting.Maui/MauiBuilderExtensions.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -9,88 +9,75 @@ namespace ReactiveMarbles.Extensions.Hosting.Maui; -/// -/// Provides extension methods for configuring pages, applications, and context in a .NET MAUI application using the -/// IMauiBuilder interface. -/// +/// Provides extension methods for configuring pages, applications, and context in a .NET MAUI application using the IMauiBuilder interface. /// These extension methods simplify the registration and configuration of pages and applications within /// the MAUI dependency injection and application startup pipeline. They are intended to be used during application /// initialization to customize the app's composition and behavior. public static class MauiBuilderExtensions { - /// - /// Registers a page type as a singleton in the MAUI application's dependency injection container. - /// - /// Use this method to ensure that only a single instance of the specified page type is created - /// and used throughout the application's lifetime. This is useful for pages that should maintain state or resources - /// across the application. - /// The type of the page to register. Must inherit from Page. - /// The IMauiBuilder instance to configure. Cannot be null. - /// The same IMauiBuilder instance for method chaining, or null if the input was null. - public static IMauiBuilder? AddSingletonPage(this IMauiBuilder mauiBuilder) - where TPage : Page + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IMauiBuilder mauiBuilder) { - mauiBuilder?.PageTypes.Add(typeof(TPage)); - return mauiBuilder; - } + /// Registers a page type as a singleton in the MAUI application's dependency injection container. + /// Use this method to ensure that only a single instance of the specified page type is created + /// and used throughout the application's lifetime. This is useful for pages that should maintain state or resources + /// across the application. + /// The type of the page to register. Must inherit from Page. + /// The same IMauiBuilder instance for method chaining, or null if the input was null. + public IMauiBuilder? AddSingletonPage() + where TPage : Page + { + mauiBuilder?.PageTypes.Add(typeof(TPage)); + return mauiBuilder; + } - /// - /// Configures the Maui application to use the specified application type and applies optional additional - /// configuration to the Maui app builder. - /// - /// The type of the application to use. Must derive from . - /// The instance to configure. Cannot be null. - /// An optional delegate to further configure the before building the application. If - /// null, no additional configuration is applied. - /// The same instance, configured to use the specified application type. - public static IMauiBuilder UseMauiApp(this IMauiBuilder mauiBuilder, Action? configureMauiApp = null) - where TApplication : Application - { - ArgumentNullException.ThrowIfNull(mauiBuilder); + /// Configures the Maui application to use the specified application type and applies optional additional configuration to the Maui app builder. + /// The type of the application to use. Must derive from . + /// An optional delegate to further configure the before building the application. If + /// null, no additional configuration is applied. + /// The same instance, configured to use the specified application type. + public IMauiBuilder UseMauiApp(Action? configureMauiApp = null) + where TApplication : Application + { + ArgumentNullException.ThrowIfNull(mauiBuilder); - mauiBuilder.ApplicationType = typeof(TApplication); - mauiBuilder.MauiAppBuilder.UseMauiApp(); - configureMauiApp?.Invoke(mauiBuilder.MauiAppBuilder); - return mauiBuilder; - } + mauiBuilder.ApplicationType = typeof(TApplication); + _ = mauiBuilder.MauiAppBuilder.UseMauiApp(); + configureMauiApp?.Invoke(mauiBuilder.MauiAppBuilder); + return mauiBuilder; + } - /// - /// Configures the Maui application to use the specified application instance and applies optional additional - /// configuration to the Maui app builder. - /// - /// The type of the application to use. Must derive from Application. - /// The builder used to configure the Maui application. Cannot be null. - /// The application instance to use as the root of the Maui app. Cannot be null. - /// An optional delegate to further configure the Maui app builder before building the application. May be null. - /// The same IMauiBuilder instance for chaining further configuration. - public static IMauiBuilder UseMauiApp(this IMauiBuilder mauiBuilder, TApplication currentApplication, Action? configureMauiApp = null) - where TApplication : Application - { - ArgumentNullException.ThrowIfNull(mauiBuilder); + /// Configures the Maui application to use the specified application instance and applies optional additional configuration to the Maui app builder. + /// The type of the application to use. Must derive from Application. + /// The application instance to use as the root of the Maui app. Cannot be null. + /// An optional delegate to further configure the Maui app builder before building the application. May be null. + /// The same IMauiBuilder instance for chaining further configuration. + public IMauiBuilder UseMauiApp(TApplication currentApplication, Action? configureMauiApp = null) + where TApplication : Application + { + ArgumentNullException.ThrowIfNull(mauiBuilder); - mauiBuilder.ApplicationType = typeof(TApplication); - mauiBuilder.Application = currentApplication; - mauiBuilder.MauiAppBuilder.UseMauiApp(); - configureMauiApp?.Invoke(mauiBuilder.MauiAppBuilder); - return mauiBuilder; - } + mauiBuilder.ApplicationType = typeof(TApplication); + mauiBuilder.Application = currentApplication; + _ = mauiBuilder.MauiAppBuilder.UseMauiApp(); + configureMauiApp?.Invoke(mauiBuilder.MauiAppBuilder); + return mauiBuilder; + } - /// - /// Configures the application context by specifying an action to be invoked with the application's during app startup. - /// - /// Use this method to customize the application's context, such as registering services or - /// modifying context-specific settings before the app is fully built. This method supports method - /// chaining. - /// The instance to configure. - /// An action to perform additional configuration on the application's . Can be to clear any existing configuration. - /// The instance for chaining further configuration. - public static IMauiBuilder ConfigureContext(this IMauiBuilder mauiBuilder, Action configureAction) - { - ArgumentNullException.ThrowIfNull(mauiBuilder); + /// Configures the application context by specifying an action to be invoked with the application's during app startup. + /// Use this method to customize the application's context, such as registering services or + /// modifying context-specific settings before the app is fully built. This method supports method + /// chaining. + /// An action to perform additional configuration on the application's . Can be to clear any existing configuration. + /// The instance for chaining further configuration. + public IMauiBuilder ConfigureContext(Action configureAction) + { + ArgumentNullException.ThrowIfNull(mauiBuilder); - mauiBuilder.ConfigureContextAction = configureAction; - return mauiBuilder; + mauiBuilder.ConfigureContextAction = configureAction; + return mauiBuilder; + } } } diff --git a/src/Extensions.Hosting.Maui/MauiHostedService.cs b/src/Extensions.Hosting.Maui/MauiHostedService.cs index 0ced3e6..704e720 100644 --- a/src/Extensions.Hosting.Maui/MauiHostedService.cs +++ b/src/Extensions.Hosting.Maui/MauiHostedService.cs @@ -1,7 +1,8 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; @@ -10,10 +11,7 @@ namespace ReactiveMarbles.Extensions.Hosting.Maui; -/// -/// Provides an implementation of the IHostedService interface to manage the lifecycle of a .NET MAUI application within -/// a generic host environment. -/// +/// Provides an implementation of the IHostedService interface to manage the lifecycle of a .NET MAUI application within a generic host environment. /// This service enables integration of a MAUI application's startup and shutdown with the ASP.NET Core /// hosting model. It is typically used to coordinate application lifetime events between the MAUI UI thread and the /// host. @@ -22,6 +20,10 @@ namespace ReactiveMarbles.Extensions.Hosting.Maui; /// The context that provides access to the MAUI application's services and dispatcher. public class MauiHostedService(ILogger logger, MauiThread mauiThread, IMauiContext mauiContext) : IHostedService { + /// Logs when the MAUI application is stopping. + private static readonly Action LogStoppingMaui = + LoggerMessage.Define(LogLevel.Debug, new EventId(1, nameof(LogStoppingMaui)), "Stopping MAUI due to application exit."); + /// public Task StartAsync(CancellationToken cancellationToken) { @@ -38,18 +40,20 @@ public Task StartAsync(CancellationToken cancellationToken) /// public async Task StopAsync(CancellationToken cancellationToken) { - if (mauiContext.IsRunning) + if (!mauiContext.IsRunning) { - logger.LogDebug("Stopping MAUI due to application exit."); - - // Stop application - var completion = new TaskCompletionSource(); - mauiContext.Dispatcher?.Dispatch(() => - { - mauiContext.MauiApplication?.Quit(); - completion.SetResult(); - }); - await completion.Task; + return; } + + LogStoppingMaui(logger, null); + + // Stop application + var completion = new TaskCompletionSource(); + _ = mauiContext.Dispatcher?.Dispatch(() => + { + mauiContext.MauiApplication?.Quit(); + completion.SetResult(); + }); + await completion.Task; } } diff --git a/src/Extensions.Hosting.PluginService/DefaultLogger.cs b/src/Extensions.Hosting.PluginService/DefaultLogger.cs index 72a95a0..af13208 100644 --- a/src/Extensions.Hosting.PluginService/DefaultLogger.cs +++ b/src/Extensions.Hosting.PluginService/DefaultLogger.cs @@ -1,19 +1,15 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.Extensions.Logging; namespace ReactiveMarbles.Extensions.Hosting.PluginService; -/// -/// Provides a default implementation of a logger that wraps an existing instance. -/// +/// Provides a default implementation of a logger that wraps an existing instance. /// The underlying instance to use for logging operations. Cannot be null. public class DefaultLogger(ILogger logger) { - /// - /// Gets the logger instance used for recording diagnostic and operational messages. - /// + /// Gets the logger instance used for recording diagnostic and operational messages. public ILogger Logger { get; } = logger; } diff --git a/src/Extensions.Hosting.PluginService/Extensions.Hosting.PluginService.csproj b/src/Extensions.Hosting.PluginService/Extensions.Hosting.PluginService.csproj index f641dc8..108e566 100644 --- a/src/Extensions.Hosting.PluginService/Extensions.Hosting.PluginService.csproj +++ b/src/Extensions.Hosting.PluginService/Extensions.Hosting.PluginService.csproj @@ -1,17 +1,17 @@ - + - net462;net472;net481;net8.0-windows;net9.0-windows;net10.0-windows + net462;net472;net48;net481;net8.0-windows;net9.0-windows;net10.0-windows;net11.0-windows enable enable CP.Extensions.Hosting.PluginService - - - - + + + + diff --git a/src/Extensions.Hosting.PluginService/ServiceBaseLifetime.cs b/src/Extensions.Hosting.PluginService/ServiceBaseLifetime.cs index 16122e1..547fda8 100644 --- a/src/Extensions.Hosting.PluginService/ServiceBaseLifetime.cs +++ b/src/Extensions.Hosting.PluginService/ServiceBaseLifetime.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.ServiceProcess; @@ -7,9 +7,7 @@ namespace ReactiveMarbles.Extensions.Hosting.PluginService; -/// -/// ServiceBaseLifetime. -/// +/// Provides an implementation backed by . /// /// /// @@ -19,30 +17,27 @@ namespace ReactiveMarbles.Extensions.Hosting.PluginService; /// applicationLifetime. public class ServiceBaseLifetime(IHostApplicationLifetime applicationLifetime) : ServiceBase, IHostLifetime { + /// Stores the delay start value. private readonly TaskCompletionSource _delayStart = new(); + /// Gets the host application lifetime used to stop the application from service callbacks. private IHostApplicationLifetime ApplicationLifetime { get; } = applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime)); - /// - /// Called at the start of which will wait until it's complete before - /// continuing. This can be used to delay startup until signaled by an external event. - /// + /// Called at the start of which will wait until it's complete before continuing. This can be used to delay startup until signaled by an external event. /// Used to indicate when stop should no longer be graceful. /// /// A . /// public Task WaitForStartAsync(CancellationToken cancellationToken) { - cancellationToken.Register(() => _delayStart.TrySetCanceled()); - ApplicationLifetime.ApplicationStopping.Register(Stop); + _ = cancellationToken.Register(() => _delayStart.TrySetCanceled()); + _ = ApplicationLifetime.ApplicationStopping.Register(Stop); new Thread(Run).Start(); // Otherwise this would block and prevent IHost.StartAsync from finishing. return _delayStart.Task; } - /// - /// Called from to indicate that the host is stopping and it's time to shut down. - /// + /// Called from to indicate that the host is stopping and it's time to shut down. /// Used to indicate when stop should no longer be graceful. /// /// A . @@ -59,7 +54,7 @@ public Task StopAsync(CancellationToken cancellationToken) /// Data passed by the start command. protected override void OnStart(string[] args) { - _delayStart.TrySetResult(null!); + _ = _delayStart.TrySetResult(null!); base.OnStart(args); } @@ -70,16 +65,17 @@ protected override void OnStop() base.OnStop(); } + /// Runs the service control loop on a background thread. private void Run() { try { Run(this); // This blocks until the service is stopped. - _delayStart.TrySetException(new InvalidOperationException("Stopped without starting")); + _ = _delayStart.TrySetException(new InvalidOperationException("Stopped without starting")); } catch (Exception ex) { - _delayStart.TrySetException(ex); + _ = _delayStart.TrySetException(ex); } } } diff --git a/src/Extensions.Hosting.PluginService/ServiceBaseLifetimeHostExtensions.cs b/src/Extensions.Hosting.PluginService/ServiceBaseLifetimeHostExtensions.cs index c6a8f5b..0156c66 100644 --- a/src/Extensions.Hosting.PluginService/ServiceBaseLifetimeHostExtensions.cs +++ b/src/Extensions.Hosting.PluginService/ServiceBaseLifetimeHostExtensions.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.Extensions.DependencyInjection; @@ -8,96 +8,89 @@ namespace ReactiveMarbles.Extensions.Hosting.PluginService; -/// -/// Provides extension methods for configuring host lifetimes to enable running .NET applications as Windows services or -/// with console lifetime support. -/// +/// 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, /// enabling applications to run as Windows services or respond to console signals for graceful shutdown. Methods in /// this class are typically used during application startup to configure the desired lifetime behavior. public static class ServiceBaseLifetimeHostExtensions { - /// - /// Enables Windows Service lifetime management for the host, allowing the application to be run as a Windows - /// Service. - /// - /// This method configures the host to use , which enables - /// integration with Windows Service control events. Use this method when deploying applications as Windows Services - /// to ensure proper start and stop behavior. - /// The host builder to configure with Windows Service lifetime support. Cannot be null. - /// The same instance of for chaining, or if is null. - public static IHostBuilder? UseServiceBaseLifetime(this IHostBuilder hostBuilder) => - hostBuilder?.ConfigureServices(services => services.AddSingleton()); - - /// - /// Enables Windows Service lifetime management for the application, allowing it to be run as a Windows Service - /// using ServiceBase. - /// - /// This method configures the application to use , which - /// enables integration with Windows Service control events. Use this method when deploying the application as a - /// Windows Service. This should not be used in environments where Windows Services are not supported. - /// The host builder to configure with Windows Service lifetime support. Cannot be null. - /// The same instance of for chaining further configuration. - /// Thrown if is null. - public static IHostApplicationBuilder UseServiceBaseLifetime(this IHostApplicationBuilder hostBuilder) + /// Provides extension members for this receiver. + /// The receiver instance. + extension(HostApplicationBuilder hostBuilder) { - if (hostBuilder == null) + /// Runs the application as a Windows service using the specified host builder. + /// This method configures the application to use Windows service lifetime management. It should + /// be called when running the application as a Windows service. If called in a non-Windows environment, the + /// behavior may differ. + /// A cancellation token that can be used to request cancellation of the service run operation. The default value is + /// . + /// A task that represents the lifetime of the running service. The task completes when the service stops. + public Task RunAsServiceAsync(CancellationToken cancellationToken = default) { - throw new ArgumentNullException(nameof(hostBuilder)); + _ = UseServiceBaseLifetime((IHostApplicationBuilder)hostBuilder); + return hostBuilder.Build().RunAsync(cancellationToken); } - - hostBuilder.Services.AddSingleton(); - return hostBuilder; } - /// - /// Configures the host to use a console lifetime, enabling the application to be controlled by console events such - /// as Ctrl+C or SIGTERM. - /// - /// This method registers as the - /// implementation, allowing the application to respond to console signals for graceful shutdown. Use this method - /// when building console applications that require proper handling of shutdown events. - /// The host builder to configure with console lifetime support. Cannot be null. - /// The same instance of for chaining further configuration. - /// Thrown if is null. - public static IHostApplicationBuilder UseConsoleLifetime(this IHostApplicationBuilder hostBuilder) + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHostApplicationBuilder hostBuilder) { - if (hostBuilder is null) + /// Enables Windows Service lifetime management for the application, allowing it to be run as a Windows Service using ServiceBase. + /// This method configures the application to use , which + /// enables integration with Windows Service control events. Use this method when deploying the application as a + /// Windows Service. This should not be used in environments where Windows Services are not supported. + /// The same instance of for chaining further configuration. + /// Thrown if is null. + public IHostApplicationBuilder UseServiceBaseLifetime() { - throw new ArgumentNullException(nameof(hostBuilder)); + if (hostBuilder is null) + { + throw new ArgumentNullException(nameof(hostBuilder)); + } + + _ = hostBuilder.Services.AddSingleton(); + return hostBuilder; } - hostBuilder.Services.AddSingleton(); - return hostBuilder; - } + /// Configures the host to use a console lifetime, enabling the application to be controlled by console events such as Ctrl+C or SIGTERM. + /// This method registers as the + /// implementation, allowing the application to respond to console signals for graceful shutdown. Use this method + /// when building console applications that require proper handling of shutdown events. + /// The same instance of for chaining further configuration. + /// Thrown if is null. + public IHostApplicationBuilder UseConsoleLifetime() + { + if (hostBuilder is null) + { + throw new ArgumentNullException(nameof(hostBuilder)); + } - /// - /// Runs the host as a Windows service or systemd service, enabling integration with the operating system's service - /// management. - /// - /// This method configures the host to run as a background service using the appropriate service - /// infrastructure for the current platform (Windows service or systemd on Linux). It should be called instead of - /// RunAsync when deploying as a service. - /// The host builder used to configure and build the application host. - /// A cancellation token that can be used to request cancellation of the service run operation. - /// A task that represents the lifetime of the service. The task completes when the service stops. - public static Task RunAsServiceAsync(this IHostBuilder hostBuilder, CancellationToken cancellationToken = default) => - hostBuilder.UseServiceBaseLifetime()!.Build().RunAsync(cancellationToken); + _ = hostBuilder.Services.AddSingleton(); + return hostBuilder; + } + } - /// - /// Runs the application as a Windows service using the specified host builder. - /// - /// This method configures the application to use Windows service lifetime management. It should - /// be called when running the application as a Windows service. If called in a non-Windows environment, the - /// behavior may differ. - /// The host application builder used to configure and build the application. - /// A cancellation token that can be used to request cancellation of the service run operation. The default value is - /// . - /// A task that represents the lifetime of the running service. The task completes when the service stops. - public static Task RunAsServiceAsync(this HostApplicationBuilder hostBuilder, CancellationToken cancellationToken = default) + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHostBuilder hostBuilder) { - UseServiceBaseLifetime((IHostApplicationBuilder)hostBuilder); - return hostBuilder.Build().RunAsync(cancellationToken); + /// Enables Windows Service lifetime management for the host, allowing the application to be run as a Windows Service. + /// This method configures the host to use , which enables + /// integration with Windows Service control events. Use this method when deploying applications as Windows Services + /// to ensure proper start and stop behavior. + /// The same instance of for chaining, or if is null. + public IHostBuilder? UseServiceBaseLifetime() => + hostBuilder?.ConfigureServices(services => _ = services.AddSingleton()); + + /// Runs the host as a Windows service or systemd service, enabling integration with the operating system's service management. + /// This method configures the host to run as a background service using the appropriate service + /// infrastructure for the current platform (Windows service or systemd on Linux). It should be called instead of + /// RunAsync when deploying as a service. + /// A cancellation token that can be used to request cancellation of the service run operation. + /// A task that represents the lifetime of the service. The task completes when the service stops. + public Task RunAsServiceAsync(CancellationToken cancellationToken = default) => + hostBuilder.UseServiceBaseLifetime()!.Build().RunAsync(cancellationToken); } } diff --git a/src/Extensions.Hosting.PluginService/ServiceHost.cs b/src/Extensions.Hosting.PluginService/ServiceHost.cs index cf0fa3c..fe4ed61 100644 --- a/src/Extensions.Hosting.PluginService/ServiceHost.cs +++ b/src/Extensions.Hosting.PluginService/ServiceHost.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Diagnostics; @@ -12,28 +12,28 @@ namespace ReactiveMarbles.Extensions.Hosting.PluginService; -/// -/// Provides static methods for configuring and running a host or application host with plugin and logging support. -/// +/// 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 /// plugin loading. It is intended for use in applications that require dynamic plugin discovery and flexible host /// configuration. All members are thread-safe for typical usage scenarios. public static class ServiceHost { + /// Stores the app settings file prefix value. private const string AppSettingsFilePrefix = "appsettings"; + + /// Stores the host settings file value. private const string HostSettingsFile = "hostsettings.json"; + + /// Stores the prefix value. private const string Prefix = "PREFIX_"; + + /// Stores the logger value. private static DefaultLogger? _logger; - /// - /// Gets the current logger instance used for application logging. - /// + /// Gets the current logger instance used for application logging. public static ILogger? Logger => _logger?.Logger; - /// - /// Creates and runs a host for the specified plugin type, configuring logging, services, and plugin discovery - /// according to the provided parameters. - /// + /// Creates and runs a host for the specified plugin type, configuring logging, services, and plugin discovery according to the provided parameters. /// This method sets up a .NET host with default configuration, logging, and plugin scanning /// based on the specified parameters. It supports both service and console lifetimes, automatically selecting the /// appropriate mode based on the execution context and command-line arguments. Plugins are discovered using the @@ -58,69 +58,38 @@ public static Task Create( string nameSpace = "ReactiveMarbles.Plugin", string? targetRuntime = null) { - if (type == null) + if (type is null) { throw new ArgumentNullException(nameof(type)); } var executableLocation = Path.GetDirectoryName(type.Assembly.Location); - var isService = (!Debugger.IsAttached) && (args?.Length == 0 || args![0].IndexOf("--console", StringComparison.InvariantCultureIgnoreCase) < 0); + var isService = IsServiceMode(args); if (isService) { - var pathToExe = Process.GetCurrentProcess().MainModule?.FileName; - var pathToContentRoot = Path.GetDirectoryName(pathToExe); - Directory.SetCurrentDirectory(pathToContentRoot!); + SetCurrentDirectoryToProcessRoot(); } var builder = Host.CreateDefaultBuilder() - .UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureLogging() - .ConfigureExternal(hostBuilder) - .ConfigureConfiguration(args) - .ConfigurePlugins(pluginBuilder => - { - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("Running using dotNet {0}", Environment.Version); - - //// Specify the location from where the Dll's are "globbed" - 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!); - - //// Add the framework libraries which can be found with the specified globs - pluginBuilder?.IncludeFrameworks(@"\netstandard2.0\*.FrameworkLib.dll"); - - //// Add the plugins which can be found with the specified globs - var runtime = targetRuntime ?? Path.GetFileName(executableLocation); - Console.WriteLine(@"Include Plugins from: \Plugins\{0}\{1}*.dll", runtime, nameSpace); - pluginBuilder?.IncludePlugins(@$"\Plugins\{runtime}\{nameSpace}*.dll"); - Console.ResetColor(); - })! - .ConfigureServices(serviceCollection => - //// Make DefaultLogger available for logging - serviceCollection.AddTransient()); + .UseContentRoot(Directory.GetCurrentDirectory()); - if (isService) - { - builder.UseServiceBaseLifetime(); - } - else - { - builder.UseConsoleLifetime(); - } + builder = ConfigureLogging(builder)!; + builder = ConfigureExternal(builder, hostBuilder)!; + builder = ConfigureConfiguration(builder, args)!; + builder = builder + .ConfigurePlugins(pluginBuilder => ConfigurePluginScanning(pluginBuilder, executableLocation, targetRuntime, nameSpace))! + .ConfigureServices(serviceCollection => _ = serviceCollection.AddTransient()); + + ConfigureLifetime(builder, isService); var host = builder.Build(); _logger = host.Services.GetRequiredService(); configureHost?.Invoke(host); - return host.RunAsync(new CancellationToken(false)); + return host.RunAsync(CancellationToken.None); } - /// - /// Initializes and runs a plugin-based .NET application using the specified entry type, command-line arguments, and - /// optional host configuration. - /// + /// Initializes and runs a plugin-based .NET application using the specified entry type, command-line arguments, and optional host configuration. /// This method configures the application host, logging, plugin discovery, and lifetime /// management based on the provided parameters. It supports both console and Windows service modes, automatically /// selecting the appropriate lifetime based on the execution context and command-line arguments. Plugins are loaded @@ -148,261 +117,207 @@ public static Task CreateApplication( string nameSpace = "ReactiveMarbles.Plugin", string? targetRuntime = null) { - if (type == null) + if (type is null) { throw new ArgumentNullException(nameof(type)); } var executableLocation = Path.GetDirectoryName(type.Assembly.Location); - var isService = (!Debugger.IsAttached) && (args?.Length == 0 || args![0].IndexOf("--console", StringComparison.InvariantCultureIgnoreCase) < 0); + var isService = IsServiceMode(args); if (isService) { - var pathToExe = Process.GetCurrentProcess().MainModule?.FileName; - var pathToContentRoot = Path.GetDirectoryName(pathToExe); - Directory.SetCurrentDirectory(pathToContentRoot!); + SetCurrentDirectoryToProcessRoot(); } var builder = Host.CreateApplicationBuilder(); - builder - .UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureLogging() - .ConfigureExternal(hostBuilder) - .ConfigureConfiguration(args) - .ConfigurePlugins(pluginBuilder => - { - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("Running using dotNet {0}", Environment.Version); - - //// Specify the location from where the Dll's are "globbed" - 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!); - - //// Add the framework libraries which can be found with the specified globs - pluginBuilder?.IncludeFrameworks(@"\netstandard2.0\*.FrameworkLib.dll"); - - //// Add the plugins which can be found with the specified globs - var runtime = targetRuntime ?? Path.GetFileName(executableLocation); - Console.WriteLine(@"Include Plugins from: \Plugins\{0}\{1}*.dll", runtime, nameSpace); - pluginBuilder?.IncludePlugins(@$"\Plugins\{runtime}\{nameSpace}*.dll"); - Console.ResetColor(); - }); - //// Make DefaultLogger available for logging - builder.Services.AddTransient(); + _ = UseContentRoot(builder, Directory.GetCurrentDirectory()); + _ = ConfigureLogging(builder); + _ = ConfigureExternal(builder, hostBuilder); + _ = ConfigureConfiguration(builder, args); + _ = builder.ConfigurePlugins(pluginBuilder => ConfigurePluginScanning(pluginBuilder, executableLocation, targetRuntime, nameSpace)); + _ = builder.Services.AddTransient(); - if (isService) - { - builder.UseServiceBaseLifetime(); - } - else - { - builder.UseConsoleLifetime(); - } + ConfigureLifetime(builder, isService); var host = builder.Build(); _logger = host.Services.GetRequiredService(); configureHost?.Invoke(host); - return host.RunAsync(new CancellationToken(false)); + return host.RunAsync(CancellationToken.None); } - /// - /// Invokes the specified delegate to configure the provided host builder, if the delegate is not null. - /// - /// This method writes informational messages to the console before and after invoking the - /// configuration delegate. - /// The host builder instance to configure. Can be null. - /// A delegate that receives the current host builder and returns a configured host builder. If null, no - /// configuration is performed. - /// The configured host builder returned by the delegate, or the original host builder if the delegate is null. - private static IHostBuilder? ConfigureExternal(this IHostBuilder? hostBuilder, Func? hostBuilderFunc) + /// Determines whether the current process should run as a service. + /// The command-line arguments. + /// true when the host should run as a service; otherwise, false. + private static bool IsServiceMode(string[] args) => + !Debugger.IsAttached && (args.Length == 0 || args[0].IndexOf("--console", StringComparison.InvariantCultureIgnoreCase) < 0); + + /// Sets the current directory to the executable directory. + private static void SetCurrentDirectoryToProcessRoot() { - if (hostBuilderFunc == null) + var pathToExe = Process.GetCurrentProcess().MainModule?.FileName; + var pathToContentRoot = Path.GetDirectoryName(pathToExe); + Directory.SetCurrentDirectory(pathToContentRoot!); + } + + /// Configures the host lifetime for service or console execution. + /// The host builder to configure. + /// true to use the service lifetime; otherwise, false. + private static void ConfigureLifetime(IHostBuilder hostBuilder, bool isService) + { + if (isService) { - return hostBuilder; + _ = hostBuilder.UseServiceBaseLifetime(); + return; } - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("Configure External Start"); - Console.ResetColor(); - var builder = hostBuilderFunc(hostBuilder); - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("Configure External Complete"); - Console.ResetColor(); - return builder; + _ = hostBuilder.UseConsoleLifetime(); } - /// - /// Sets the content root directory for the application host builder. - /// - /// The content root determines the base path for content files, such as configuration files and - /// static assets, used by the application. - /// The application host builder to configure. - /// The absolute path to the directory that should be used as the content root. Cannot be null or empty. - /// The same instance of for chaining further configuration. - /// Thrown if is null or empty. - private static IHostApplicationBuilder UseContentRoot(this IHostApplicationBuilder hostBuilder, string contentRoot) + /// Configures the host application builder lifetime for service or console execution. + /// The host application builder to configure. + /// true to use the service lifetime; otherwise, false. + private static void ConfigureLifetime(IHostApplicationBuilder hostBuilder, bool isService) { - if (string.IsNullOrEmpty(contentRoot)) + if (isService) { - throw new ArgumentNullException(nameof(contentRoot), "Content root cannot be null or empty."); + _ = hostBuilder.UseServiceBaseLifetime(); + return; } - hostBuilder.Configuration.AddInMemoryCollection(new[] - { - new KeyValuePair(HostDefaults.ContentRootKey, contentRoot) - }); + _ = hostBuilder.UseConsoleLifetime(); + } + + /// Configures plugin scanning using process and runtime information. + /// The plugin builder to configure. + /// The executable location used to infer the runtime. + /// The target runtime override. + /// The namespace prefix used to discover plugins. + private static void ConfigurePluginScanning(IPluginBuilder? pluginBuilder, string? executableLocation, string? targetRuntime, string nameSpace) + { + var process = Process.GetCurrentProcess(); + var fullPath = process.MainModule?.FileName?.Replace(process.MainModule.ModuleName!, string.Empty); + pluginBuilder?.AddScanDirectories(fullPath!); + + pluginBuilder?.IncludeFrameworks(@"\netstandard2.0\*.FrameworkLib.dll"); + + var runtime = targetRuntime ?? Path.GetFileName(executableLocation); + pluginBuilder?.IncludePlugins(@$"\Plugins\{runtime}\{nameSpace}*.dll"); + } + + /// Sets the content root directory for the application host builder. + /// The host application builder to configure. + /// The absolute path to the directory that should be used as the content root. Cannot be null or empty. + /// The same instance of for chaining further configuration. + /// Thrown if contentRoot is null or empty. + private static IHostApplicationBuilder UseContentRoot(IHostApplicationBuilder hostBuilder, string contentRoot) + { + var checkedContentRoot = !string.IsNullOrEmpty(contentRoot) + ? contentRoot + : throw new ArgumentNullException(nameof(contentRoot), "Content root cannot be null or empty."); + + _ = hostBuilder.Configuration.AddInMemoryCollection([new(HostDefaults.ContentRootKey, checkedContentRoot)]); return hostBuilder; } - /// - /// Invokes the specified delegate to configure the provided host application builder, if the delegate is not null. - /// - /// This method writes informational messages to the console before and after invoking the - /// configuration delegate. Use this method to apply external configuration logic to the host builder in a - /// composable manner. - /// The current instance to be configured. Can be null. - /// A delegate that receives the current and returns a configured instance. If - /// null, no configuration is performed. - /// The configured instance returned by , or - /// the original if is null. - private static IHostApplicationBuilder? ConfigureExternal(this IHostApplicationBuilder? hostBuilder, Func? hostBuilderFunc) + /// Invokes an external host application builder configuration delegate when one was provided. + /// The current host application builder. + /// The optional external configuration delegate. + /// The configured host application builder. + private static IHostApplicationBuilder? ConfigureExternal(IHostApplicationBuilder? hostBuilder, Func? hostBuilderFunc) => + hostBuilderFunc is null ? hostBuilder : hostBuilderFunc(hostBuilder); + + /// Configures logging providers and settings for the specified host application builder. + /// The host application builder to configure. + /// The configured host application builder. + private static IHostApplicationBuilder? ConfigureLogging(IHostApplicationBuilder? hostBuilder) { - if (hostBuilderFunc == null) + if (hostBuilder is null) { - return hostBuilder; + return null; } - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("Configure External Start"); - Console.ResetColor(); - var builder = hostBuilderFunc(hostBuilder); - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("Configure External Complete"); - Console.ResetColor(); - return builder; + _ = hostBuilder.Logging + .AddConfiguration(hostBuilder.Configuration.GetSection("Logging")) + .AddConsole() + .AddEventLog() + .AddLog4Net("log4net.config") + .AddDebug(); + + return hostBuilder; } - /// - /// Configures logging providers for the specified host builder, including console, event log, Log4Net, and debug - /// logging, using settings from the "Logging" configuration section. - /// - /// This method adds multiple logging providers to the host, including console, event log, - /// Log4Net (using the "log4net.config" file), and debug output. Logging settings are read from the "Logging" - /// section of the application's configuration. Call this method during host setup to enable these logging - /// providers. - /// The host builder to configure logging for. Can be null. - /// The same instance with logging configured, or null if - /// is null. - private static IHostBuilder? ConfigureLogging(this IHostBuilder? hostBuilder) => - hostBuilder?.ConfigureLogging((hostContext, configLogging) => - configLogging - .AddConfiguration(hostContext.Configuration.GetSection("Logging")) - .AddConsole() - .AddEventLog() - .AddLog4Net("log4net.config") - .AddDebug()); - - /// - /// Configures logging providers and settings for the specified host application builder. - /// - /// This method adds configuration from the "Logging" section, and registers console, event log, - /// Log4Net (using "log4net.config"), and debug logging providers. Call this method during application startup to - /// enable these logging mechanisms. - /// The host application builder to configure. Can be null. - /// The same instance with logging configured, or null if is null. - private static IHostApplicationBuilder? ConfigureLogging(this IHostApplicationBuilder? hostBuilder) + /// Configures application configuration sources for the host application builder. + /// The host application builder to configure. + /// The command-line arguments to include as a configuration source. + /// The configured host application builder. + private static IHostApplicationBuilder? ConfigureConfiguration(IHostApplicationBuilder? hostBuilder, string[] args) { - hostBuilder?.Logging - .AddConfiguration(hostBuilder.Configuration.GetSection("Logging")) - .AddConsole() - .AddEventLog() - .AddLog4Net("log4net.config") - .AddDebug(); + if (hostBuilder is null) + { + return null; + } + + _ = hostBuilder.Configuration.SetBasePath(Directory.GetCurrentDirectory()); + _ = hostBuilder.Configuration.AddJsonFile(HostSettingsFile, optional: true); + _ = hostBuilder.Configuration.AddEnvironmentVariables(prefix: Prefix); + _ = hostBuilder.Configuration.AddCommandLine(args); + + _ = hostBuilder.Configuration.AddJsonFile(AppSettingsFilePrefix + ".json", optional: true); + if (!string.IsNullOrEmpty(hostBuilder.Environment.EnvironmentName)) + { + _ = hostBuilder.Configuration.AddJsonFile(AppSettingsFilePrefix + $".{hostBuilder.Environment.EnvironmentName}.json", optional: true); + } + _ = hostBuilder.Configuration.AddEnvironmentVariables(prefix: Prefix); + _ = hostBuilder.Configuration.AddCommandLine(args); return hostBuilder; } - /// - /// Configures the host and application configuration for the specified host builder using JSON files, environment - /// variables, and command-line arguments. - /// - /// This method adds configuration sources for both the host and application, including JSON - /// files, environment variables with a specific prefix, and command-line arguments. It is intended to be used as an - /// extension method during host setup to ensure consistent configuration across environments. - /// The host builder to configure. Can be null. - /// The command-line arguments to include in the configuration. Cannot be null. - /// The configured host builder instance, or null if the input host builder is null. - private static IHostBuilder? ConfigureConfiguration(this IHostBuilder? hostBuilder, string[] args) => + /// Invokes an external host builder configuration delegate when one was provided. + /// The current host builder. + /// The optional external configuration delegate. + /// The configured host builder. + private static IHostBuilder? ConfigureExternal(IHostBuilder? hostBuilder, Func? hostBuilderFunc) => + hostBuilderFunc is null ? hostBuilder : hostBuilderFunc(hostBuilder); + + /// Configures logging providers for the specified host builder. + /// The host builder to configure. + /// The configured host builder. + private static IHostBuilder? ConfigureLogging(IHostBuilder? hostBuilder) => + hostBuilder?.ConfigureLogging((hostContext, configLogging) => + { + _ = configLogging + .AddConfiguration(hostContext.Configuration.GetSection("Logging")) + .AddConsole() + .AddEventLog() + .AddLog4Net("log4net.config") + .AddDebug(); + }); + + /// Configures host and application configuration sources for the host builder. + /// The host builder to configure. + /// The command-line arguments to include in the configuration. + /// The configured host builder. + private static IHostBuilder? ConfigureConfiguration(IHostBuilder? hostBuilder, string[] args) => hostBuilder? .ConfigureHostConfiguration(configHost => { - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("Configure Host Start"); - configHost.SetBasePath(Directory.GetCurrentDirectory()); - configHost.AddJsonFile(HostSettingsFile, optional: true); - configHost.AddEnvironmentVariables(prefix: Prefix); - configHost.AddCommandLine(args); - Console.WriteLine("Configure Host Complete"); - Console.ResetColor(); + _ = configHost.SetBasePath(Directory.GetCurrentDirectory()); + _ = configHost.AddJsonFile(HostSettingsFile, optional: true); + _ = configHost.AddEnvironmentVariables(prefix: Prefix); + _ = configHost.AddCommandLine(args); }) .ConfigureAppConfiguration((hostContext, configApp) => { - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("Configure App Start"); - configApp.AddJsonFile(AppSettingsFilePrefix + ".json", optional: true); + _ = configApp.AddJsonFile(AppSettingsFilePrefix + ".json", optional: true); if (!string.IsNullOrEmpty(hostContext.HostingEnvironment.EnvironmentName)) { - configApp.AddJsonFile(AppSettingsFilePrefix + $".{hostContext.HostingEnvironment.EnvironmentName}.json", optional: true); + _ = configApp.AddJsonFile(AppSettingsFilePrefix + $".{hostContext.HostingEnvironment.EnvironmentName}.json", optional: true); } - configApp.AddEnvironmentVariables(prefix: Prefix); - configApp.AddCommandLine(args); - Console.WriteLine("Configure App Complete"); - Console.ResetColor(); + _ = configApp.AddEnvironmentVariables(prefix: Prefix); + _ = configApp.AddCommandLine(args); }); - - /// - /// Configures the application's configuration sources for the specified host builder using JSON files, environment - /// variables, and command-line arguments. - /// - /// This method adds configuration sources in a layered order, including base and - /// environment-specific JSON files, environment variables with a specific prefix, and command-line arguments. It is - /// intended to be called during application startup to ensure consistent configuration loading. - /// The host application builder to configure. If null, no configuration is applied and null is returned. - /// The command-line arguments to include as a configuration source. - /// The configured host application builder, or null if the provided host builder is null. - private static IHostApplicationBuilder? ConfigureConfiguration(this IHostApplicationBuilder? hostBuilder, string[] args) - { - if (hostBuilder == null) - { - return null; - } - - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("Configure Host Start"); - hostBuilder.Configuration.SetBasePath(Directory.GetCurrentDirectory()); - hostBuilder.Configuration.AddJsonFile(HostSettingsFile, optional: true); - hostBuilder.Configuration.AddEnvironmentVariables(prefix: Prefix); - hostBuilder.Configuration.AddCommandLine(args); - Console.WriteLine("Configure Host Complete"); - Console.ResetColor(); - - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("Configure App Start"); - hostBuilder.Configuration.AddJsonFile(AppSettingsFilePrefix + ".json", optional: true); - if (!string.IsNullOrEmpty(hostBuilder.Environment.EnvironmentName)) - { - hostBuilder.Configuration.AddJsonFile(AppSettingsFilePrefix + $".{hostBuilder.Environment.EnvironmentName}.json", optional: true); - } - - hostBuilder.Configuration.AddEnvironmentVariables(prefix: Prefix); - hostBuilder.Configuration.AddCommandLine(args); - Console.WriteLine("Configure App Complete"); - Console.ResetColor(); - return hostBuilder; - } } diff --git a/src/Extensions.Hosting.Plugins/Extensions.Hosting.Plugins.csproj b/src/Extensions.Hosting.Plugins/Extensions.Hosting.Plugins.csproj index 7c4502a..c38ad9b 100644 --- a/src/Extensions.Hosting.Plugins/Extensions.Hosting.Plugins.csproj +++ b/src/Extensions.Hosting.Plugins/Extensions.Hosting.Plugins.csproj @@ -1,16 +1,26 @@ - net462;net472;net481;net8.0;net9.0;net10.0 + net462;net472;net48;net481;net8.0;net9.0;net10.0;net11.0 $(NoWarn);IDE0058;CA1822 This extension adds plug-in support to generic host based applications. CP.Extensions.Hosting.Plugins - - - + + + + + + + + + + + + <_Parameter1>Extensions.Hosting.Tests + diff --git a/src/Extensions.Hosting.Plugins/HostBuilderPluginExtensions.cs b/src/Extensions.Hosting.Plugins/HostBuilderPluginExtensions.cs index deac8fe..57442af 100644 --- a/src/Extensions.Hosting.Plugins/HostBuilderPluginExtensions.cs +++ b/src/Extensions.Hosting.Plugins/HostBuilderPluginExtensions.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -13,88 +13,25 @@ namespace ReactiveMarbles.Extensions.Hosting.Plugins; -/// -/// Provides extension methods for configuring and loading plugins in host builder pipelines. -/// +/// Provides extension methods for configuring and loading plugins in host builder pipelines. /// These extension methods enable plugin discovery and configuration for applications using IHostBuilder /// or IHostApplicationBuilder. They allow developers to register, scan, and load plugins as part of the application's /// startup process. Use these methods to integrate plugin-based extensibility into your application's hosting /// lifecycle. public static class HostBuilderPluginExtensions { + /// Stores the plugin builder key value. private const string PluginBuilderKey = nameof(PluginBuilder); - /// - /// Configures plugin support for the specified host builder by invoking the provided configuration action. - /// - /// This method ensures that plugin support is configured only once for the host builder. - /// Subsequent calls will reuse the existing plugin builder instance. Use this method to register or customize - /// plugins before building the host. - /// The host builder to configure with plugin support. Cannot be null. - /// An action that configures the plugin builder. The action receives the current plugin builder instance, or null - /// if plugin support has not yet been initialized. - /// The same instance of with plugin support configured. - /// Thrown if is null. - public static IHostBuilder ConfigurePlugins(this IHostBuilder? hostBuilder, Action configurePlugin) - { - if (hostBuilder == null) - { - throw new ArgumentNullException(nameof(hostBuilder)); - } - - if (!hostBuilder.Properties.TryRetrievePluginBuilder(out var pluginBuilder)) - { - // Configure a single time - ConfigurePluginScanAndLoad(hostBuilder); - } - - configurePlugin?.Invoke(pluginBuilder); - - return hostBuilder; - } - - /// - /// Configures plugins for the application by invoking the specified configuration action on the plugin builder. - /// - /// This method ensures that plugin scanning and loading are configured only once per host - /// builder instance. Subsequent calls will reuse the existing plugin builder. The method is intended to be used as - /// part of the application's startup configuration. - /// The host application builder to configure. Cannot be null. - /// An action that configures the plugin builder. The action receives the current plugin builder instance, or null - /// if not available. - /// The same instance for chaining further configuration. - /// Thrown if is null. - public static IHostApplicationBuilder ConfigurePlugins(this IHostApplicationBuilder? hostBuilder, Action configurePlugin) - { - if (hostBuilder == null) - { - throw new ArgumentNullException(nameof(hostBuilder)); - } - - if (!hostBuilder.Properties.TryRetrievePluginBuilder(out var pluginBuilder)) - { - // Configure a single time - ConfigurePluginScanAndLoad(hostBuilder); - } - - configurePlugin?.Invoke(pluginBuilder); - - return hostBuilder; - } - - /// - /// Attempts to retrieve an existing instance from the specified properties dictionary, - /// or creates and adds a new instance if one does not exist. - /// + /// Attempts to retrieve an existing instance from the specified properties dictionary, or creates and adds a new instance if one does not exist. /// This method ensures that the properties dictionary always contains a valid instance after the call. If no instance was present, a new one is created and stored for /// future retrieval. - /// The dictionary containing property values, used to store and retrieve the instance. - /// Cannot be null. + /// The properties dictionary that stores host builder state. /// When this method returns, contains the retrieved or newly created instance. /// if an existing was found in the dictionary; otherwise, and a new instance is created and added. - private static bool TryRetrievePluginBuilder(this IDictionary properties, out IPluginBuilder pluginBuilder) + private static bool TryRetrievePluginBuilder(IDictionary properties, out IPluginBuilder pluginBuilder) { if (properties.TryGetValue(PluginBuilderKey, out var pluginBuilderObject)) { @@ -107,174 +44,199 @@ private static bool TryRetrievePluginBuilder(this IDictionary pr return false; } - /// - /// Configures the specified host builder to scan for and load plugins from predefined directories during - /// application startup. - /// - /// This method adds services to the dependency injection container by scanning specified - /// framework and plugin directories for assemblies. It invokes each discovered plugin's configuration logic. The - /// method should be called before building the host to ensure all plugins are loaded and configured - /// appropriately. - /// The host builder to configure for plugin scanning and loading. Must not be null. - /// The same instance of with plugin scanning and loading configured. - /// Thrown if no plugins are found and the configuration requires at least one plugin to be present. - private static IHostBuilder ConfigurePluginScanAndLoad(IHostBuilder hostBuilder) => - //// Configure the actual scanning & loading - hostBuilder.ConfigureServices((hostBuilderContext, serviceCollection) => - { - hostBuilder.Properties.TryRetrievePluginBuilder(out var pluginBuilder); + /// Retrieves the order value specified by the applied to the plugin type. + /// The plugin whose order should be retrieved. + /// The order value defined by the on the plugin type; returns 0 if the attribute + /// is not present. + private static int GetOrder(IPlugin plugin) => + plugin.GetType().GetCustomAttribute(false)?.Order ?? 0; - if (pluginBuilder.UseContentRoot) + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHostApplicationBuilder? hostBuilder) + { + /// Configures plugins for the application by invoking the specified configuration action on the plugin builder. + /// This method ensures that plugin scanning and loading are configured only once per host + /// builder instance. Subsequent calls will reuse the existing plugin builder. The method is intended to be used as + /// part of the application's startup configuration. + /// An action that configures the plugin builder. The action receives the current plugin builder instance, or null + /// if not available. + /// The same instance for chaining further configuration. + /// Thrown if is null. + public IHostApplicationBuilder ConfigurePlugins(Action configurePlugin) + { + if (hostBuilder is null) { - var contentRootPath = hostBuilderContext.HostingEnvironment.ContentRootPath; - pluginBuilder.AddScanDirectories(contentRootPath); + throw new ArgumentNullException(nameof(hostBuilder)); } - var scannedAssemblies = new HashSet(); + var alreadyConfigured = TryRetrievePluginBuilder(hostBuilder.Properties, out var pluginBuilder); + configurePlugin?.Invoke(pluginBuilder); - if (pluginBuilder.FrameworkDirectories.Count > 0) + if (!alreadyConfigured) { - foreach (var frameworkScanRoot in pluginBuilder.FrameworkDirectories) - { - // Do the globbing and try to load the framework files into the default AssemblyLoadContext - foreach (var frameworkAssemblyPath in pluginBuilder.FrameworkMatcher.GetResultsInFullPath(frameworkScanRoot)) - { - var frameworkAssemblyName = new AssemblyName(Path.GetFileNameWithoutExtension(frameworkAssemblyPath)); - if (AssemblyLoadContext.Default.TryGetAssembly(frameworkAssemblyName, out var alreadyLoadedAssembly)) - { - scannedAssemblies.Add(alreadyLoadedAssembly); - continue; - } - - // TODO: Log the loading? - var loadedAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(frameworkAssemblyPath); - scannedAssemblies.Add(loadedAssembly); - } - } + _ = ConfigurePluginScanAndLoad(hostBuilder); } - if (pluginBuilder.PluginDirectories.Count > 0) - { - foreach (var pluginScanRootPath in pluginBuilder.PluginDirectories) - { - // Do the globbing and try to load the plug-ins - var pluginPaths = pluginBuilder.PluginMatcher.GetResultsInFullPath(pluginScanRootPath); + return hostBuilder; + } + } - // Use the globbed files, and load the assemblies - var pluginAssemblies = pluginPaths - .Select(s => LoadPlugin(pluginBuilder, s)) - .Where(plugin => plugin != null); - foreach (var pluginAssembly in pluginAssemblies) - { - scannedAssemblies.Add(pluginAssembly); - } - } + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHostBuilder? hostBuilder) + { + /// Configures plugin support for the specified host builder by invoking the provided configuration action. + /// This method ensures that plugin support is configured only once for the host builder. + /// Subsequent calls will reuse the existing plugin builder instance. Use this method to register or customize + /// plugins before building the host. + /// An action that configures the plugin builder. The action receives the current plugin builder instance, or null + /// if plugin support has not yet been initialized. + /// The same instance of with plugin support configured. + /// Thrown if is null. + public IHostBuilder ConfigurePlugins(Action configurePlugin) + { + if (hostBuilder is null) + { + throw new ArgumentNullException(nameof(hostBuilder)); } - var plugins = scannedAssemblies.SelectMany(pluginBuilder.AssemblyScanFunc!).Where(plugin => plugin != null).OrderBy(plugin => plugin?.GetOrder()).ToList(); + var alreadyConfigured = TryRetrievePluginBuilder(hostBuilder.Properties, out var pluginBuilder); + configurePlugin?.Invoke(pluginBuilder); - if (plugins.Count == 0 && pluginBuilder.FailIfNoPlugins) + if (!alreadyConfigured) { - throw new InvalidOperationException("No plugins were found. Set FailIfNoPlugins=false to allow empty plugin sets."); + _ = ConfigurePluginScanAndLoad(hostBuilder); } - foreach (var plugin in plugins) - { - plugin?.ConfigureHost(hostBuilderContext, serviceCollection); - } - }); + return hostBuilder; + } + } - /// - /// Scans for and loads plugin assemblies into the application, configuring each discovered plugin with the provided - /// host builder. - /// - /// This method scans specified directories for both framework and plugin assemblies, loads them, - /// and invokes their configuration logic. The behavior is controlled by plugin builder properties such as scan - /// directories and failure conditions. Plugins are configured in order as determined by their metadata. The method - /// does not modify the original host builder instance beyond plugin configuration. + /// Configures the specified host builder to scan for and load plugins from predefined directories during application startup. + /// The host builder to configure for plugin scanning and loading. Must not be null. + /// The same instance of with plugin scanning and loading configured. + private static IHostBuilder ConfigurePluginScanAndLoad(IHostBuilder hostBuilder) => + hostBuilder.ConfigureServices((hostBuilderContext, serviceCollection) => + ConfigurePlugins( + hostBuilder.Properties, + hostBuilderContext.HostingEnvironment.ContentRootPath, + plugin => plugin.ConfigureHost(hostBuilderContext, serviceCollection))); + + /// Scans for and loads plugin assemblies into the application, configuring each discovered plugin with the provided host builder. /// The application host builder used to configure services and environment settings for plugin discovery and /// initialization. /// The same instance, configured with any discovered plugins. - /// Thrown if no plugins are found and the plugin configuration requires at least one plugin to be present. private static IHostApplicationBuilder ConfigurePluginScanAndLoad(IHostApplicationBuilder hostBuilder) { - //// Configure the actual scanning & loading - hostBuilder.Properties.TryRetrievePluginBuilder(out var pluginBuilder); + ConfigurePlugins( + hostBuilder.Properties, + hostBuilder.Environment.ContentRootPath, + plugin => plugin.ConfigureHost(hostBuilder, hostBuilder.Services)); - if (pluginBuilder.UseContentRoot) - { - var contentRootPath = hostBuilder.Environment.ContentRootPath; - pluginBuilder.AddScanDirectories(contentRootPath); - } + return hostBuilder; + } - var scannedAssemblies = new HashSet(); + /// Scans, loads, orders, and configures plugins from the configured plugin builder. + /// The host builder properties that contain plugin configuration state. + /// The content root used when content-root scanning is enabled. + /// The callback used to configure each discovered plugin. + private static void ConfigurePlugins(IDictionary properties, string contentRootPath, Action configurePlugin) + { + _ = TryRetrievePluginBuilder(properties, out var pluginBuilder); + AddContentRootScanDirectory(pluginBuilder, contentRootPath); - if (pluginBuilder.FrameworkDirectories.Count > 0) - { - foreach (var frameworkScanRoot in pluginBuilder.FrameworkDirectories) - { - // Do the globbing and try to load the framework files into the default AssemblyLoadContext - foreach (var frameworkAssemblyPath in pluginBuilder.FrameworkMatcher.GetResultsInFullPath(frameworkScanRoot)) - { - var frameworkAssemblyName = new AssemblyName(Path.GetFileNameWithoutExtension(frameworkAssemblyPath)); - if (AssemblyLoadContext.Default.TryGetAssembly(frameworkAssemblyName, out var alreadyLoadedAssembly)) - { - scannedAssemblies.Add(alreadyLoadedAssembly); - continue; - } + var scannedAssemblies = LoadAssemblies(pluginBuilder); + var plugins = GetOrderedPlugins(pluginBuilder, scannedAssemblies); - // TODO: Log the loading? - var loadedAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(frameworkAssemblyPath); - scannedAssemblies.Add(loadedAssembly); - } - } - } + if (plugins.Count == 0 && pluginBuilder.FailIfNoPlugins) + { + throw new InvalidOperationException("No plugins were found. Set FailIfNoPlugins=false to allow empty plugin sets."); + } - if (pluginBuilder.PluginDirectories.Count > 0) - { - foreach (var pluginScanRootPath in pluginBuilder.PluginDirectories) - { - // Do the globbing and try to load the plug-ins - var pluginPaths = pluginBuilder.PluginMatcher.GetResultsInFullPath(pluginScanRootPath); + foreach (var plugin in plugins) + { + configurePlugin(plugin); + } + } - // Use the globbed files, and load the assemblies - var pluginAssemblies = pluginPaths - .Select(s => LoadPlugin(pluginBuilder, s)) - .Where(plugin => plugin != null); - foreach (var pluginAssembly in pluginAssemblies) - { - scannedAssemblies.Add(pluginAssembly); - } - } - } + /// Adds the content root to the plugin scan directories when configured. + /// The plugin builder to update. + /// The content root path to add. + private static void AddContentRootScanDirectory(IPluginBuilder pluginBuilder, string contentRootPath) + { + if (!pluginBuilder.UseContentRoot) + { + return; + } - var plugins = scannedAssemblies.SelectMany(pluginBuilder.AssemblyScanFunc!).Where(plugin => plugin != null).OrderBy(plugin => plugin?.GetOrder()).ToList(); + pluginBuilder.AddScanDirectories(contentRootPath); + } - if (plugins.Count == 0 && pluginBuilder.FailIfNoPlugins) + /// Loads all configured framework and plugin assemblies. + /// The plugin builder that contains scan configuration. + /// The loaded assemblies that should be scanned for plugins. + private static HashSet LoadAssemblies(IPluginBuilder pluginBuilder) + { + var scannedAssemblies = new HashSet(); + LoadFrameworkAssemblies(pluginBuilder, scannedAssemblies); + LoadPluginAssemblies(pluginBuilder, scannedAssemblies); + return scannedAssemblies; + } + + /// Loads configured framework assemblies into the default load context. + /// The plugin builder that contains framework scan configuration. + /// The set that receives loaded assemblies. + private static void LoadFrameworkAssemblies(IPluginBuilder pluginBuilder, HashSet scannedAssemblies) + { + foreach (var frameworkScanRoot in pluginBuilder.FrameworkDirectories) + { + foreach (var frameworkAssemblyPath in pluginBuilder.FrameworkMatcher.GetResultsInFullPath(frameworkScanRoot)) { - throw new InvalidOperationException("No plugins were found. Set FailIfNoPlugins=false to allow empty plugin sets."); + var frameworkAssemblyName = new AssemblyName(Path.GetFileNameWithoutExtension(frameworkAssemblyPath)); + if (AssemblyLoadContext.Default.TryGetAssembly(frameworkAssemblyName, out var alreadyLoadedAssembly)) + { + _ = scannedAssemblies.Add(alreadyLoadedAssembly!); + continue; + } + + var loadedAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(frameworkAssemblyPath); + _ = scannedAssemblies.Add(loadedAssembly); } + } + } - foreach (var plugin in plugins) + /// Loads configured plugin assemblies into their plugin load contexts. + /// The plugin builder that contains plugin scan configuration. + /// The set that receives loaded assemblies. + private static void LoadPluginAssemblies(IPluginBuilder pluginBuilder, HashSet scannedAssemblies) + { + foreach (var pluginScanRootPath in pluginBuilder.PluginDirectories) + { + foreach (var pluginAssemblyPath in pluginBuilder.PluginMatcher.GetResultsInFullPath(pluginScanRootPath)) { - plugin?.ConfigureHost(hostBuilder, hostBuilder.Services); + var pluginAssembly = LoadPlugin(pluginBuilder, pluginAssemblyPath); + if (pluginAssembly is not null) + { + _ = scannedAssemblies.Add(pluginAssembly); + } } - - return hostBuilder; + } } - /// - /// Retrieves the order value specified by the applied to the plugin type. - /// - /// The plugin instance whose order value is to be retrieved. Cannot be null. - /// The order value defined by the on the plugin type; returns 0 if the attribute - /// is not present. - private static int GetOrder(this IPlugin plugin) => - plugin.GetType().GetCustomAttribute(false)?.Order ?? 0; - - /// - /// Loads a plugin assembly from the specified location using the provided plugin builder. - /// + /// Scans the loaded assemblies and returns plugins in configured order. + /// The plugin builder that contains the assembly scanning delegate. + /// The assemblies to scan. + /// The ordered plugins discovered from the loaded assemblies. + private static List GetOrderedPlugins(IPluginBuilder pluginBuilder, HashSet scannedAssemblies) => + scannedAssemblies + .SelectMany(assembly => pluginBuilder.AssemblyScanFunc(assembly) ?? Enumerable.Empty()) + .Where(plugin => plugin is not null) + .Cast() + .OrderBy(GetOrder) + .ToList(); + + /// Loads a plugin assembly from the specified location using the provided plugin builder. /// The method performs validation on the plugin assembly before attempting to load it. If the /// assembly is already loaded in the default context, the method returns . /// An instance of used to validate and configure the plugin before loading. @@ -283,26 +245,16 @@ private static int GetOrder(this IPlugin plugin) => /// file does not exist, validation fails, or the assembly is already loaded. private static Assembly? LoadPlugin(IPluginBuilder pluginBuilder, string pluginAssemblyLocation) { - if (!File.Exists(pluginAssemblyLocation)) + if (!File.Exists(pluginAssemblyLocation) || !pluginBuilder.ValidatePlugin(pluginAssemblyLocation)) { - // TODO: Log an error, how to get a logger here? return null; } - // This allows validation like AuthenticodeExaminer - if (!pluginBuilder.ValidatePlugin(pluginAssemblyLocation)) - { - return null; - } - - // TODO: Log verbose that we are loading a plugin var pluginName = Path.GetFileNameWithoutExtension(pluginAssemblyLocation); - - // TODO: Decide if we rather have this to come up with the name: AssemblyName.GetAssemblyName(pluginLocation) var pluginAssemblyName = new AssemblyName(pluginName); if (AssemblyLoadContext.Default.TryGetAssembly(pluginAssemblyName, out _)) { - return null!; + return null; } var loadContext = new PluginLoadContext(pluginAssemblyLocation, pluginName); diff --git a/src/Extensions.Hosting.Plugins/HostedServiceBase.cs b/src/Extensions.Hosting.Plugins/HostedServiceBase.cs index 5f90288..f037491 100644 --- a/src/Extensions.Hosting.Plugins/HostedServiceBase.cs +++ b/src/Extensions.Hosting.Plugins/HostedServiceBase.cs @@ -1,11 +1,8 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; -using System.Reactive; -using System.Reactive.Disposables; -using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; @@ -13,10 +10,7 @@ namespace ReactiveMarbles.Extensions.Hosting.Plugins; -/// -/// Provides a base implementation for a hosted service with support for resource cleanup, logging, and application -/// lifetime events. -/// +/// Provides a base implementation for a hosted service with support for resource cleanup, logging, and application lifetime events. /// This base class integrates with the application's lifetime events to manage service startup, /// shutdown, and resource disposal. It is intended to be inherited by services that require coordinated startup and /// cleanup logic. The class is thread-safe for typical usage scenarios. Derived classes can override the OnStarted, @@ -24,47 +18,50 @@ namespace ReactiveMarbles.Extensions.Hosting.Plugins; /// The type representing the hosted service. Used for logging and identification purposes. /// The logger instance used to record diagnostic and operational information for the hosted service. Cannot be null. /// The application lifetime manager used to register callbacks for application start and stop events. Cannot be null. -public class HostedServiceBase(ILogger logger, IHostApplicationLifetime hostApplicationLifetime) : IHostedService, ICancelable +public class HostedServiceBase(ILogger logger, IHostApplicationLifetime hostApplicationLifetime) : IHostedService, ReactiveUI.Primitives.Disposables.IsDisposed { + /// Logs that the hosted service started. + private static readonly Action _serviceStarted = + LoggerMessage.Define(LogLevel.Information, new EventId(1, nameof(_serviceStarted)), "{ServiceName} Service OnStarted has been called."); + + /// Logs that the hosted service is stopping. + private static readonly Action _serviceStopping = + LoggerMessage.Define(LogLevel.Information, new EventId(2, nameof(_serviceStopping)), "{ServiceName} Service OnStopping has been called."); + + /// Logs that the hosted service stopped. + private static readonly Action _serviceStopped = + LoggerMessage.Define(LogLevel.Information, new EventId(3, nameof(_serviceStopped)), "{ServiceName} Service OnStopped has been called."); + + /// Logs that the hosted service start callback failed. + private static readonly Action _serviceStartFailed = + LoggerMessage.Define(LogLevel.Error, new EventId(4, nameof(_serviceStartFailed)), "{ServiceName} Service OnStarted failed."); + + /// Stores the disposed value. private bool _disposedValue; - /// - /// Gets the collection of disposables that are cleaned up when the owning object is disposed. - /// + /// Gets the collection of disposables that are cleaned up when the owning object is disposed. /// Use this property to register resources or subscriptions that should be disposed together /// with the parent object. Items added to this collection will be disposed automatically when the parent is /// disposed. public CompositeDisposable CleanUp { get; private set; } = []; - /// - /// Gets the logger instance used for recording diagnostic and operational messages. - /// + /// Gets the logger instance used for recording diagnostic and operational messages. public ILogger Logger { get; } = logger; - /// - /// Gets a value indicating whether the object has been disposed. - /// + /// Gets a value indicating whether the object has been disposed. public bool IsDisposed => CleanUp.IsDisposed; /// public Task StartAsync(CancellationToken cancellationToken) { - hostApplicationLifetime.ApplicationStarted.Register(() => + _ = hostApplicationLifetime.ApplicationStarted.Register(() => { - Logger!.LogInformation($"{nameof(T)} Service OnStarted has been called."); + _serviceStarted(Logger, nameof(T), null); CleanUp = []; - - // Create a Task with cancellation on Taskpool call all from inside to ensure teardown. - CleanUp.Add(Observable.Create(async _ => - { - await OnStarted(); - return CleanUp; - }) - .Retry() - .Subscribe()); + _ = RunOnStartedAsync(); }); - hostApplicationLifetime.ApplicationStopping.Register(OnStopping); - hostApplicationLifetime.ApplicationStopped.Register(OnStopped); + _ = hostApplicationLifetime.ApplicationStopping.Register(OnStopping); + _ = hostApplicationLifetime.ApplicationStopped.Register(OnStopped); return Task.CompletedTask; } @@ -73,9 +70,7 @@ public Task StartAsync(CancellationToken cancellationToken) public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method @@ -83,48 +78,55 @@ public void Dispose() GC.SuppressFinalize(this); } - /// - /// Invoked when the associated process or service has started. - /// + /// Invoked when the associated process or service has started. /// Override this method to perform custom actions when the process or service starts. This /// method is called after the start operation has been initiated. /// A task that represents the asynchronous operation. The default implementation returns a completed task. public virtual Task OnStarted() => Task.CompletedTask; - /// - /// Performs tasks required when the service is stopping, such as releasing resources and logging shutdown - /// information. - /// + /// Performs tasks required when the service is stopping, such as releasing resources and logging shutdown information. /// Override this method to implement custom shutdown logic for the service. Call the base /// implementation to ensure standard cleanup is performed. public virtual void OnStopping() { - Logger!.LogInformation($"{nameof(T)} Service OnStopping has been called."); - CleanUp?.Dispose(); + _serviceStopping(Logger, nameof(T), null); + CleanUp.Dispose(); } - /// - /// Called when the service has stopped to perform any necessary cleanup or finalization logic. - /// + /// Called when the service has stopped to perform any necessary cleanup or finalization logic. /// Override this method in a derived class to implement custom actions that should occur when /// the service stops. The base implementation logs a message indicating that the service has stopped. public virtual void OnStopped() => - Logger!.LogInformation($"{nameof(T)} Service OnStopped has been called."); + _serviceStopped(Logger, nameof(T), null); - /// - /// Releases unmanaged and - optionally - managed resources. - /// + /// Releases unmanaged and - optionally - managed resources. /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool disposing) { - if (!_disposedValue) + if (_disposedValue) { - if (disposing) - { - CleanUp.Dispose(); - } + return; + } - _disposedValue = true; + if (disposing) + { + CleanUp.Dispose(); + } + + _disposedValue = true; + } + + /// Runs the started callback and logs any exception without surfacing it on the lifetime callback. + /// A task that represents the asynchronous operation. + private async Task RunOnStartedAsync() + { + try + { + await OnStarted().ConfigureAwait(false); + } + catch (Exception ex) + { + _serviceStartFailed(Logger, nameof(T), ex); } } } diff --git a/src/Extensions.Hosting.Plugins/IPlugin.cs b/src/Extensions.Hosting.Plugins/IPlugin.cs index 347ed76..0fd1d4b 100644 --- a/src/Extensions.Hosting.Plugins/IPlugin.cs +++ b/src/Extensions.Hosting.Plugins/IPlugin.cs @@ -1,22 +1,18 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.Extensions.DependencyInjection; namespace ReactiveMarbles.Extensions.Hosting.Plugins; -/// -/// Defines a contract for plug-ins that can configure services and settings for the application host during startup. -/// +/// Defines a contract for plug-ins that can configure services and settings for the application host during startup. /// Implementations of this interface can be used to extend or modify the application's dependency /// injection container and host configuration. Plug-ins should use the provided context and service collection to /// register services or perform other setup tasks required for their functionality. public interface IPlugin { - /// - /// Configures host-specific services and settings using the provided context and service collection. - /// + /// Configures host-specific services and settings using the provided context and service collection. /// This method is typically called during application startup to customize the host's dependency /// injection container and configuration. The implementation should not assume a specific type for /// hostBuilderContext unless documented by the caller. diff --git a/src/Extensions.Hosting.Plugins/IPluginBuilder.cs b/src/Extensions.Hosting.Plugins/IPluginBuilder.cs index 6a7fdf0..c2b9365 100644 --- a/src/Extensions.Hosting.Plugins/IPluginBuilder.cs +++ b/src/Extensions.Hosting.Plugins/IPluginBuilder.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -9,49 +9,32 @@ namespace ReactiveMarbles.Extensions.Hosting.Plugins; -/// -/// Defines a contract for configuring and building plugin discovery and loading behavior within an application. -/// +/// Defines a contract for configuring and building plugin discovery and loading behavior within an application. /// Implementations of this interface allow customization of plugin and framework assembly scanning, /// validation, and loading strategies. Use this interface to specify directories to scan, matching patterns, validation /// logic, and assembly scanning functions to control how plugins are discovered and loaded at application /// startup. public interface IPluginBuilder { - /// - /// Gets the list of directory paths where plugins are located. - /// + /// Gets the list of directory paths where plugins are located. IList PluginDirectories { get; } - /// - /// Gets the list of directories that contain framework assemblies required for compilation. - /// + /// Gets the list of directories that contain framework assemblies required for compilation. IList FrameworkDirectories { get; } - /// - /// Gets or sets a value indicating whether specify to use the content root for scanning. - /// + /// Gets or sets a value indicating whether specify to use the content root for scanning. bool UseContentRoot { get; set; } - /// - /// Gets or sets a value indicating whether to fail startup when no plugins are loaded. - /// Defaults to false for backwards compatibility. - /// + /// Gets or sets a value indicating whether to fail startup when no plugins are loaded. Defaults to false for backwards compatibility. bool FailIfNoPlugins { get; set; } - /// - /// Gets the matcher used to find all the framework assemblies. - /// + /// Gets the matcher used to find all the framework assemblies. Matcher FrameworkMatcher { get; } - /// - /// Gets the matcher to find all the plugins. - /// + /// Gets the matcher to find all the plugins. Matcher PluginMatcher { get; } - /// - /// Gets or sets specifies a way to validate the plugin file before it's being loaded. - /// + /// Gets or sets specifies a way to validate the plugin file before it's being loaded. Func ValidatePlugin { get; set; } /// diff --git a/src/Extensions.Hosting.Plugins/Internals/AssemblyDependencyResolver.cs b/src/Extensions.Hosting.Plugins/Internals/AssemblyDependencyResolver.cs index 6c0216e..01c7535 100644 --- a/src/Extensions.Hosting.Plugins/Internals/AssemblyDependencyResolver.cs +++ b/src/Extensions.Hosting.Plugins/Internals/AssemblyDependencyResolver.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -8,44 +8,42 @@ namespace ReactiveMarbles.Extensions.Hosting.Plugins.Internals; -/// -/// Provides methods for resolving the paths of managed assemblies and unmanaged DLLs relative to a specified plugin -/// directory. -/// +/// Provides methods for resolving the paths of managed assemblies and unmanaged DLLs relative to a specified plugin directory. /// The absolute path to the root directory containing the plugin and its dependencies. Cannot be null or empty. -public class AssemblyDependencyResolver(string pluginPath) +public sealed class AssemblyDependencyResolver(string pluginPath) { - private readonly string? _pluginPath = Path.GetDirectoryName(pluginPath); + /// Stores the plugin path value. + private readonly string _pluginPath = Path.GetDirectoryName(pluginPath) ?? string.Empty; - /// - /// Resolves the file system path to the assembly file corresponding to the specified assembly name, if it exists in - /// the plugin directory. - /// + /// Resolves the file system path to the assembly file corresponding to the specified assembly name, if it exists in the plugin directory. /// The assembly name to resolve to a file path. Cannot be null. /// The full path to the assembly file if it exists in the plugin directory; otherwise, null. /// Thrown if assemblyName is null. - public string? ResolveAssemblyToPath(AssemblyName assemblyName) - { - if (assemblyName == null) - { - throw new ArgumentNullException(nameof(assemblyName)); - } + public string? ResolveAssemblyToPath(AssemblyName assemblyName) => + assemblyName is null + ? throw new ArgumentNullException(nameof(assemblyName)) + : ResolveExistingPath($"{assemblyName.Name}.dll"); - var assemblyPath = Path.Combine(_pluginPath!, $"{assemblyName.Name}.dll"); - if (File.Exists(assemblyPath)) - { - return assemblyPath; - } + /// Resolves the specified unmanaged DLL name to an absolute file path on disk. + /// The name of the unmanaged DLL to locate. This should not include a path or file extension. + /// The absolute path to the located unmanaged DLL, or null if the DLL cannot be found. + /// Thrown if unmanagedDllName is null, empty, or consists only of white-space characters. + public string? ResolveUnmanagedDllToPath(string unmanagedDllName) + { + var libraryName = !string.IsNullOrWhiteSpace(unmanagedDllName) + ? unmanagedDllName + : throw new ArgumentException("The unmanaged DLL name cannot be null, empty, or consist only of white-space characters.", nameof(unmanagedDllName)); - return null; + var fileName = Path.HasExtension(libraryName) ? libraryName : $"{libraryName}.dll"; + return ResolveExistingPath(fileName); } - /// - /// Resolves the specified unmanaged DLL name to an absolute file path on disk. - /// - /// The name of the unmanaged DLL to locate. This should not include a path or file extension. - /// The absolute path to the located unmanaged DLL, or null if the DLL cannot be found. - /// The method is not implemented. - public string ResolveUnmanagedDllToPath(string unmanagedDllName) => - throw new NotImplementedException(); + /// Resolves a file name relative to the plugin path when it exists. + /// The file name to resolve. + /// The resolved path, or null when the file does not exist. + private string? ResolveExistingPath(string fileName) + { + var path = Path.Combine(_pluginPath, fileName); + return File.Exists(path) ? path : null; + } } diff --git a/src/Extensions.Hosting.Plugins/Internals/AssemblyLoadContext.cs b/src/Extensions.Hosting.Plugins/Internals/AssemblyLoadContext.cs index 1df1c71..cf76cfe 100644 --- a/src/Extensions.Hosting.Plugins/Internals/AssemblyLoadContext.cs +++ b/src/Extensions.Hosting.Plugins/Internals/AssemblyLoadContext.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -8,10 +8,7 @@ namespace ReactiveMarbles.Extensions.Hosting.Plugins.Internals; -/// -/// Provides functionality for loading and managing assemblies in a custom context, enabling isolation and control over -/// assembly loading behavior. -/// +/// Provides functionality for loading and managing assemblies in a custom context, enabling isolation and control over assembly loading behavior. /// AssemblyLoadContext allows applications to load assemblies into isolated contexts, which can be /// useful for plugin architectures, unloading assemblies, or resolving assembly version conflicts. Each context /// maintains its own set of loaded assemblies and can customize how assemblies and unmanaged libraries are resolved. @@ -21,27 +18,19 @@ namespace ReactiveMarbles.Extensions.Hosting.Plugins.Internals; /// purposes and can be null or empty. public class AssemblyLoadContext(string name) { - /// - /// Gets the default assembly load context for the application domain. - /// + /// Gets the default assembly load context for the application domain. /// The default context is used to load assemblies that are part of the application or shared /// framework. Assemblies loaded into the default context are visible to all code in the application domain. Use /// this property to access the standard assembly loading behavior provided by .NET. public static AssemblyLoadContext Default { get; } = new AssemblyLoadContext("default"); - /// - /// Gets the name associated with the current instance. - /// + /// Gets the name associated with the current instance. public string Name { get; } = name; - /// - /// Gets the assemblies that are loaded into the current application domain. - /// + /// Gets the assemblies that are loaded into the current application domain. public IEnumerable Assemblies => AppDomain.CurrentDomain.GetAssemblies(); - /// - /// Loads an assembly given its display name. - /// + /// Loads an assembly given its display name. /// This method loads the assembly into the current load context. If the assembly has already /// been loaded, the existing assembly is returned. /// The assembly name object that specifies the display name of the assembly to load. Cannot be null. @@ -49,7 +38,7 @@ public class AssemblyLoadContext(string name) /// Thrown if is null. public Assembly LoadFromAssemblyName(AssemblyName assemblyName) { - if (assemblyName == null) + if (assemblyName is null) { throw new ArgumentNullException(nameof(assemblyName)); } @@ -58,9 +47,7 @@ public Assembly LoadFromAssemblyName(AssemblyName assemblyName) return Load(assemblyName); } - /// - /// Loads an assembly from the specified file path. - /// + /// Loads an assembly from the specified file path. /// The assembly is loaded into the load-from context. If the assembly has already been loaded, /// this method may return a reference to the existing assembly. This method does not resolve dependencies /// automatically; dependent assemblies must be available to the loader. @@ -68,17 +55,13 @@ public Assembly LoadFromAssemblyName(AssemblyName assemblyName) /// The loaded assembly represented by the specified file path. public Assembly LoadFromAssemblyPath(string assemblyPath) => Assembly.LoadFrom(assemblyPath); - /// - /// Loads the assembly with the specified name. - /// + /// Loads the assembly with the specified name. /// Override this method to implement custom assembly loading logic in a derived class. /// The name of the assembly to load. Cannot be null. /// The loaded assembly, or null if the assembly cannot be found. protected virtual Assembly Load(AssemblyName assemblyName) => null!; - /// - /// Loads an unmanaged dynamic-link library (DLL) from the specified absolute path. - /// + /// Loads an unmanaged dynamic-link library (DLL) from the specified absolute path. /// This method is intended to be called by derived classes to provide custom logic for loading /// unmanaged libraries. The caller is responsible for ensuring that the specified path points to a valid and /// compatible DLL. @@ -86,9 +69,7 @@ public Assembly LoadFromAssemblyName(AssemblyName assemblyName) /// A handle to the loaded unmanaged DLL. Returns if the library could not be loaded. protected IntPtr LoadUnmanagedDllFromPath(string dllPath) => IntPtr.Zero; - /// - /// Loads the specified unmanaged DLL into the process address space. - /// + /// Loads the specified unmanaged DLL into the process address space. /// Override this method to provide custom logic for loading unmanaged libraries when resolving /// native dependencies. The default implementation returns , indicating that the DLL could /// not be loaded. diff --git a/src/Extensions.Hosting.Plugins/Internals/AssemblyLoadContextExtensions.cs b/src/Extensions.Hosting.Plugins/Internals/AssemblyLoadContextExtensions.cs index 38dcecc..40604bb 100644 --- a/src/Extensions.Hosting.Plugins/Internals/AssemblyLoadContextExtensions.cs +++ b/src/Extensions.Hosting.Plugins/Internals/AssemblyLoadContextExtensions.cs @@ -1,57 +1,56 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Reflection; namespace ReactiveMarbles.Extensions.Hosting.Plugins.Internals; -/// -/// Provides extension methods for the AssemblyLoadContext class. -/// +/// Provides extension methods for the AssemblyLoadContext class. /// This static class contains methods that extend the functionality of AssemblyLoadContext, enabling /// additional operations such as searching for loaded assemblies by name. These methods are intended to simplify common /// tasks when working with assembly loading contexts. public static class AssemblyLoadContextExtensions { - /// - /// Attempts to find an assembly with the specified name in the given assembly load context. - /// - /// The search compares only the simple name of the assembly (the Name property) and does not - /// consider version, culture, or public key token. If multiple assemblies with the same name exist in the context, - /// the first match is returned. - /// The assembly load context to search. If null, the method returns false and sets - /// to null. - /// The name of the assembly to locate. Only the property is used for comparison. - /// When this method returns, contains the assembly that matches the specified name, if found; otherwise, null. This - /// parameter is passed uninitialized. - /// if an assembly with the specified name is found; otherwise, . - public static bool TryGetAssembly(this AssemblyLoadContext assemblyLoadContext, AssemblyName assemblyName, out Assembly? foundAssembly) + /// Provides extension members for this receiver. + /// The receiver instance. + extension(AssemblyLoadContext assemblyLoadContext) { - if (assemblyLoadContext == null) + /// Attempts to find an assembly with the specified name in the given assembly load context. + /// The search compares only the simple name of the assembly (the Name property) and does not + /// consider version, culture, or public key token. If multiple assemblies with the same name exist in the context, + /// the first match is returned. + /// The name of the assembly to locate. Only the property is used for comparison. + /// When this method returns, contains the assembly that matches the specified name, if found; otherwise, null. This + /// parameter is passed uninitialized. + /// if an assembly with the specified name is found; otherwise, . + public bool TryGetAssembly(AssemblyName assemblyName, out Assembly? foundAssembly) { - foundAssembly = null; - return false; - } - - foreach (var assembly in assemblyLoadContext.Assemblies) - { - var name = assembly.GetName().Name; - if (name == null) + if (assemblyLoadContext is null) { - continue; + foundAssembly = null; + return false; } - if (!name.Equals(assemblyName?.Name)) + foreach (var assembly in assemblyLoadContext.Assemblies) { - continue; + var name = assembly.GetName().Name; + if (name is null) + { + continue; + } + + if (!name.Equals(assemblyName?.Name)) + { + continue; + } + + foundAssembly = assembly; + return true; } - foundAssembly = assembly; - return true; + foundAssembly = null; + return false; } - - foundAssembly = null; - return false; } } diff --git a/src/Extensions.Hosting.Plugins/Internals/PluginBuilder.cs b/src/Extensions.Hosting.Plugins/Internals/PluginBuilder.cs index 4db800f..b328e16 100644 --- a/src/Extensions.Hosting.Plugins/Internals/PluginBuilder.cs +++ b/src/Extensions.Hosting.Plugins/Internals/PluginBuilder.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -9,14 +9,12 @@ namespace ReactiveMarbles.Extensions.Hosting.Plugins.Internals; -/// -/// Provides configuration and matching logic for plugin discovery and loading within the application. -/// +/// Provides configuration and matching logic for plugin discovery and loading within the application. /// The PluginBuilder class exposes properties and delegates that allow customization of plugin scanning, /// validation, and directory management. It is typically used to configure how plugins are identified and loaded at /// runtime. This class is intended for internal use and is not designed for direct consumption by external /// code. -internal class PluginBuilder : IPluginBuilder +internal sealed class PluginBuilder : IPluginBuilder { /// public Matcher FrameworkMatcher { get; } = new Matcher(); diff --git a/src/Extensions.Hosting.Plugins/Internals/PluginLoadContext.cs b/src/Extensions.Hosting.Plugins/Internals/PluginLoadContext.cs index bba77ba..7437b10 100644 --- a/src/Extensions.Hosting.Plugins/Internals/PluginLoadContext.cs +++ b/src/Extensions.Hosting.Plugins/Internals/PluginLoadContext.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -7,9 +7,7 @@ namespace ReactiveMarbles.Extensions.Hosting.Plugins.Internals; -/// -/// Provides an isolated assembly load context for loading plugins from a specified directory. -/// +/// Provides an isolated assembly load context for loading plugins from a specified directory. /// PluginLoadContext enables loading and resolving assemblies and unmanaged libraries for plugins /// without interfering with the default application context. This allows multiple plugins to be loaded with their own /// dependencies, reducing the risk of version conflicts. Assemblies are resolved relative to the specified plugin @@ -17,45 +15,36 @@ namespace ReactiveMarbles.Extensions.Hosting.Plugins.Internals; /// The file system path to the root directory containing the plugin's assemblies and dependencies. Cannot be null or /// empty. /// The unique name for the assembly load context. Used to identify the context within the application. -internal class PluginLoadContext(string pluginPath, string name) : AssemblyLoadContext(name) +internal sealed class PluginLoadContext(string pluginPath, string name) : AssemblyLoadContext(name) { + /// Stores the resolver value. private readonly AssemblyDependencyResolver _resolver = new(pluginPath); - /// - /// Resolves the file system path to the assembly specified by the given assembly name. - /// + /// Resolves the file system path to the assembly specified by the given assembly name. /// The assembly name to resolve. Cannot be null. /// The full path to the resolved assembly file, or null if the assembly cannot be found. public string? ResolveAssemblyPath(AssemblyName assemblyName) => _resolver.ResolveAssemblyToPath(assemblyName); /// - protected override Assembly Load(AssemblyName assemblyName) - { - // Try to get the assembly from the AssemblyLoadContext.Default, when it is already loaded - if (Default.TryGetAssembly(assemblyName, out var alreadyLoadedAssembly)) - { - return alreadyLoadedAssembly!; - } - - var assemblyPath = ResolveAssemblyPath(assemblyName); - if (assemblyPath == null) - { - return null!; - } - - return LoadFromAssemblyPath(assemblyPath); - } + protected override Assembly Load(AssemblyName assemblyName) => + Default.TryGetAssembly(assemblyName, out var alreadyLoadedAssembly) + ? alreadyLoadedAssembly! + : LoadAssemblyFromResolvedPath(assemblyName)!; /// protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) { var libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName); - if (libraryPath == null) - { - return IntPtr.Zero; - } + return libraryPath is null ? IntPtr.Zero : LoadUnmanagedDllFromPath(libraryPath); + } - return LoadUnmanagedDllFromPath(libraryPath); + /// Loads an assembly from the resolved plugin path when one exists. + /// The assembly name to resolve and load. + /// The loaded assembly, or null when no plugin-local path exists. + private Assembly? LoadAssemblyFromResolvedPath(AssemblyName assemblyName) + { + var assemblyPath = ResolveAssemblyPath(assemblyName); + return assemblyPath is null ? null : LoadFromAssemblyPath(assemblyPath); } } diff --git a/src/Extensions.Hosting.Plugins/PluginBase{T1,T2,T3}.cs b/src/Extensions.Hosting.Plugins/PluginBase{T1,T2,T3}.cs index 5d063af..19f3ff1 100644 --- a/src/Extensions.Hosting.Plugins/PluginBase{T1,T2,T3}.cs +++ b/src/Extensions.Hosting.Plugins/PluginBase{T1,T2,T3}.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.Extensions.DependencyInjection; @@ -7,9 +7,7 @@ namespace ReactiveMarbles.Extensions.Hosting.Plugins; -/// -/// Provides a base implementation for plugins that register three hosted services with the application host. -/// +/// Provides a base implementation for plugins that register three hosted services with the application host. /// This class is intended to be used as a base for plugins that require multiple hosted services to be /// added to the application's dependency injection container. All type parameters must be reference types implementing /// . diff --git a/src/Extensions.Hosting.Plugins/PluginBase{T1,T2}.cs b/src/Extensions.Hosting.Plugins/PluginBase{T1,T2}.cs index 4cc2bb8..7b86b6f 100644 --- a/src/Extensions.Hosting.Plugins/PluginBase{T1,T2}.cs +++ b/src/Extensions.Hosting.Plugins/PluginBase{T1,T2}.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.Extensions.DependencyInjection; @@ -7,9 +7,7 @@ namespace ReactiveMarbles.Extensions.Hosting.Plugins; -/// -/// Provides a base implementation for plugins that register two hosted services with a dependency injection container. -/// +/// Provides a base implementation for plugins that register two hosted services with a dependency injection container. /// This class is intended to be used as a base for plugins that require multiple hosted services to be /// added to the application's service collection. Both type parameters must be reference types implementing . diff --git a/src/Extensions.Hosting.Plugins/PluginBase{T}.cs b/src/Extensions.Hosting.Plugins/PluginBase{T}.cs index b86feba..65a58c8 100644 --- a/src/Extensions.Hosting.Plugins/PluginBase{T}.cs +++ b/src/Extensions.Hosting.Plugins/PluginBase{T}.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.Extensions.DependencyInjection; @@ -7,10 +7,7 @@ namespace ReactiveMarbles.Extensions.Hosting.Plugins; -/// -/// Provides a base implementation for plugins that register a hosted service of the specified type with the -/// application's dependency injection container. -/// +/// Provides a base implementation for plugins that register a hosted service of the specified type with the application's dependency injection container. /// The type of the hosted service to register. Must implement . public class PluginBase : IPlugin where T : class, IHostedService diff --git a/src/Extensions.Hosting.Plugins/PluginBuilderExtensions.cs b/src/Extensions.Hosting.Plugins/PluginBuilderExtensions.cs index d4cb281..229029e 100644 --- a/src/Extensions.Hosting.Plugins/PluginBuilderExtensions.cs +++ b/src/Extensions.Hosting.Plugins/PluginBuilderExtensions.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -7,142 +7,124 @@ namespace ReactiveMarbles.Extensions.Hosting.Plugins; -/// -/// Provides extension methods for configuring plug-in and framework assembly scanning behavior on an instance. -/// +/// Provides extension methods for configuring plug-in and framework assembly scanning behavior on an instance. /// These extension methods allow customization of directory scanning, inclusion and exclusion patterns /// for assemblies, and enforcement of plug-in discovery requirements during application startup. Use these methods to /// control which assemblies are considered as plug-ins or framework components when building a plug-in /// system. public static class PluginBuilderExtensions { - /// - /// Adds one or more directories to the plugin and framework scan paths for the specified plugin builder. - /// - /// Each directory specified is added to both the framework and plugin directory collections of - /// the plugin builder. Duplicate or non-existent directories are not checked by this method. - /// The plugin builder to which the scan directories will be added. - /// An array of directory paths to add to the scan paths. Each path is normalized to its full path before being - /// added. - /// Thrown if is null. - public static void AddScanDirectories(this IPluginBuilder pluginBuilder, params string[] directories) + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IPluginBuilder pluginBuilder) { - if (directories == null) + /// Adds one or more directories to the plugin and framework scan paths for the specified plugin builder. + /// Each directory specified is added to both the framework and plugin directory collections of + /// the plugin builder. Duplicate or non-existent directories are not checked by this method. + /// An array of directory paths to add to the scan paths. Each path is normalized to its full path before being + /// added. + /// Thrown if is null. + public void AddScanDirectories(params string[] directories) { - throw new ArgumentNullException(nameof(directories)); - } + if (directories is null) + { + throw new ArgumentNullException(nameof(directories)); + } - foreach (var directory in directories) - { - var normalizedDirectory = Path.GetFullPath(directory); - pluginBuilder?.FrameworkDirectories.Add(normalizedDirectory); - pluginBuilder?.PluginDirectories.Add(normalizedDirectory); + foreach (var directory in directories) + { + var normalizedDirectory = Path.GetFullPath(directory); + pluginBuilder?.FrameworkDirectories.Add(normalizedDirectory); + pluginBuilder?.PluginDirectories.Add(normalizedDirectory); + } } - } - /// - /// Excludes plug-in targets that match the specified framework glob patterns from the plug-in builder - /// configuration. - /// - /// The plug-in builder to configure with framework exclusions. - /// One or more glob patterns representing frameworks to exclude. Each pattern is applied to filter out matching - /// frameworks from the plug-in targets. - /// Thrown if is . - public static void ExcludeFrameworks(this IPluginBuilder pluginBuilder, params string[] frameworkGlobs) - { - if (frameworkGlobs == null) + /// Excludes plug-in targets that match the specified framework glob patterns from the plug-in builder configuration. + /// One or more glob patterns representing frameworks to exclude. Each pattern is applied to filter out matching + /// frameworks from the plug-in targets. + /// Thrown if is . + public void ExcludeFrameworks(params string[] frameworkGlobs) { - throw new ArgumentNullException(nameof(frameworkGlobs)); - } + if (frameworkGlobs is null) + { + throw new ArgumentNullException(nameof(frameworkGlobs)); + } - foreach (var glob in frameworkGlobs) - { - pluginBuilder?.FrameworkMatcher.AddExclude(glob); + foreach (var glob in frameworkGlobs) + { + pluginBuilder?.FrameworkMatcher.AddExclude(glob); + } } - } - /// - /// Excludes plugins from discovery by specifying one or more glob patterns. - /// - /// Use this method to prevent specific plugins from being loaded by matching their names against - /// the provided glob patterns. This is useful for filtering out plugins that should not be included in the - /// application. - /// The plugin builder to configure with the exclusion patterns. - /// An array of glob patterns that identify plugins to exclude. Each pattern is applied to plugin names during - /// discovery. - /// Thrown if pluginGlobs is null. - public static void ExcludePlugins(this IPluginBuilder pluginBuilder, params string[] pluginGlobs) - { - if (pluginGlobs == null) + /// Excludes plugins from discovery by specifying one or more glob patterns. + /// Use this method to prevent specific plugins from being loaded by matching their names against + /// the provided glob patterns. This is useful for filtering out plugins that should not be included in the + /// application. + /// An array of glob patterns that identify plugins to exclude. Each pattern is applied to plugin names during + /// discovery. + /// Thrown if pluginGlobs is null. + public void ExcludePlugins(params string[] pluginGlobs) { - throw new ArgumentNullException(nameof(pluginGlobs)); - } + if (pluginGlobs is null) + { + throw new ArgumentNullException(nameof(pluginGlobs)); + } - foreach (var glob in pluginGlobs) - { - pluginBuilder?.PluginMatcher.AddExclude(glob); + foreach (var glob in pluginGlobs) + { + pluginBuilder?.PluginMatcher.AddExclude(glob); + } } - } - /// - /// Includes the specified framework glob patterns in the plugin builder's framework matcher, allowing only matching - /// frameworks to be considered during plugin resolution. - /// - /// Use this method to restrict plugin resolution to specific frameworks by providing one or more - /// glob patterns. This is useful when targeting a subset of supported frameworks in multi-targeted plugin - /// scenarios. - /// The plugin builder to configure with the specified framework include patterns. - /// An array of glob patterns that specify which frameworks to include. Each pattern is added to the framework - /// matcher as an inclusion rule. - /// Thrown if is . - public static void IncludeFrameworks(this IPluginBuilder pluginBuilder, params string[] frameworkGlobs) - { - if (frameworkGlobs == null) + /// Includes the specified framework glob patterns in the plugin builder's framework matcher, allowing only matching frameworks to be considered during plugin resolution. + /// Use this method to restrict plugin resolution to specific frameworks by providing one or more + /// glob patterns. This is useful when targeting a subset of supported frameworks in multi-targeted plugin + /// scenarios. + /// An array of glob patterns that specify which frameworks to include. Each pattern is added to the framework + /// matcher as an inclusion rule. + /// Thrown if is . + public void IncludeFrameworks(params string[] frameworkGlobs) { - throw new ArgumentNullException(nameof(frameworkGlobs)); - } + if (frameworkGlobs is null) + { + throw new ArgumentNullException(nameof(frameworkGlobs)); + } - foreach (var glob in frameworkGlobs) - { - pluginBuilder?.FrameworkMatcher.AddInclude(glob); + foreach (var glob in frameworkGlobs) + { + pluginBuilder?.FrameworkMatcher.AddInclude(glob); + } } - } - /// - /// Adds one or more plugin glob patterns to the plugin builder's include list. - /// - /// Each glob pattern in is added to the plugin builder's include - /// matcher. This method can be called multiple times to add additional patterns. - /// The plugin builder to which the include glob patterns will be added. - /// An array of glob patterns specifying which plugins to include. Cannot be null. - /// Thrown if is null. - public static void IncludePlugins(this IPluginBuilder pluginBuilder, params string[] pluginGlobs) - { - if (pluginGlobs == null) + /// Adds one or more plugin glob patterns to the plugin builder's include list. + /// Each glob pattern in is added to the plugin builder's include + /// matcher. This method can be called multiple times to add additional patterns. + /// An array of glob patterns specifying which plugins to include. Cannot be null. + /// Thrown if is null. + public void IncludePlugins(params string[] pluginGlobs) { - throw new ArgumentNullException(nameof(pluginGlobs)); - } + if (pluginGlobs is null) + { + throw new ArgumentNullException(nameof(pluginGlobs)); + } - foreach (var glob in pluginGlobs) - { - pluginBuilder?.PluginMatcher.AddInclude(glob); + foreach (var glob in pluginGlobs) + { + pluginBuilder?.PluginMatcher.AddInclude(glob); + } } - } - /// - /// Configures the plugin builder to require at least one plugin, optionally failing if no plugins are registered. - /// - /// The plugin builder to configure. Cannot be null. - /// true to throw an exception if no plugins are registered; otherwise, false. The default is true. - /// Thrown if pluginBuilder is null. - public static void RequirePlugins(this IPluginBuilder pluginBuilder, bool failIfNone = true) - { - if (pluginBuilder == null) + /// Configures the plugin builder to require at least one plugin, optionally failing if no plugins are registered. + /// true to throw an exception if no plugins are registered; otherwise, false. The default is true. + /// Thrown if pluginBuilder is null. + public void RequirePlugins(bool failIfNone = true) { - throw new ArgumentNullException(nameof(pluginBuilder)); - } + if (pluginBuilder is null) + { + throw new ArgumentNullException(nameof(pluginBuilder)); + } - pluginBuilder.FailIfNoPlugins = failIfNone; + pluginBuilder.FailIfNoPlugins = failIfNone; + } } } diff --git a/src/Extensions.Hosting.Plugins/PluginOrderAttribute.cs b/src/Extensions.Hosting.Plugins/PluginOrderAttribute.cs index 29b2bfe..8176536 100644 --- a/src/Extensions.Hosting.Plugins/PluginOrderAttribute.cs +++ b/src/Extensions.Hosting.Plugins/PluginOrderAttribute.cs @@ -1,36 +1,28 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; namespace ReactiveMarbles.Extensions.Hosting.Plugins; -/// -/// Specifies the initialization order for a plug-in class. -/// +/// Specifies the initialization order for a plug-in class. /// Apply this attribute to a plug-in class to control the sequence in which plug-ins are initialized. /// Lower order values indicate earlier initialization. If multiple plug-ins have the same order, their relative /// initialization order is unspecified. [AttributeUsage(AttributeTargets.Class, Inherited = false)] public sealed class PluginOrderAttribute : Attribute { - /// - /// Initializes a new instance of the class. - /// + /// Initializes a new instance of the class. public PluginOrderAttribute() { } - /// - /// Initializes a new instance of the class with the specified order value. - /// + /// Initializes a new instance of the class with the specified order value. /// The order in which the plugin should be processed. Lower values indicate higher priority. public PluginOrderAttribute(int order) => Order = order; - /// - /// Initializes a new instance of the class using the specified enumeration value. - /// + /// Initializes a new instance of the class using the specified enumeration value. /// This constructor allows specifying the plugin order using an enumeration, which is internally /// converted to an integer. This can improve code readability and maintainability when plugin order is defined by /// an enum. @@ -39,10 +31,12 @@ public PluginOrderAttribute() public PluginOrderAttribute(object enumValue) : this(Convert.ToInt32(enumValue)) { + EnumValue = enumValue; } - /// - /// Gets the order or position of the item within a collection or sequence. - /// + /// Gets the enumeration value supplied to the attribute constructor, if one was supplied. + public object? EnumValue { get; } + + /// Gets the order or position of the item within a collection or sequence. public int Order { get; } } diff --git a/src/Extensions.Hosting.Plugins/PluginScanner.cs b/src/Extensions.Hosting.Plugins/PluginScanner.cs index b04b50d..5064fd3 100644 --- a/src/Extensions.Hosting.Plugins/PluginScanner.cs +++ b/src/Extensions.Hosting.Plugins/PluginScanner.cs @@ -1,26 +1,20 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; using System.Collections.Generic; -using System.Linq; using System.Reflection; namespace ReactiveMarbles.Extensions.Hosting.Plugins; -/// -/// Provides methods for discovering and instantiating plugin implementations from assemblies using naming conventions -/// or type scanning. -/// +/// Provides methods for discovering and instantiating plugin implementations from assemblies using naming conventions or type scanning. /// The PluginScanner class is intended for use in scenarios where plugins implementing the IPlugin /// interface need to be dynamically located and instantiated from assemblies. All methods are static and /// thread-safe. public static class PluginScanner { - /// - /// Discovers and instantiates plugins from the specified assembly using a naming convention. - /// + /// Discovers and instantiates plugins from the specified assembly using a naming convention. /// This method looks for a type named 'Plugin' in the root namespace of the provided assembly. /// The type must implement the IPlugin interface and have a public parameterless constructor. If no such type /// exists, the method returns an empty sequence. @@ -29,7 +23,7 @@ public static class PluginScanner /// Thrown if is null. public static IEnumerable ByNamingConvention(Assembly pluginAssembly) { - if (pluginAssembly == null) + if (pluginAssembly is null) { throw new ArgumentNullException(nameof(pluginAssembly)); } @@ -37,10 +31,7 @@ public static IEnumerable ByNamingConvention(Assembly pluginAssembly) return ByNamingConventionCore(pluginAssembly); } - /// - /// Scans the specified assembly for types that implement the IPlugin interface and creates instances of each - /// discovered plugin type. - /// + /// Scans the specified assembly for types that implement the IPlugin interface and creates instances of each discovered plugin type. /// Only non-abstract, public classes that implement IPlugin are considered. Each type is /// instantiated using its parameterless constructor. If instantiation fails, the corresponding element in the /// returned collection will be null. @@ -50,7 +41,7 @@ public static IEnumerable ByNamingConvention(Assembly pluginAssembly) /// Thrown if is null. public static IEnumerable ScanForPluginInstances(Assembly pluginAssembly) { - if (pluginAssembly == null) + if (pluginAssembly is null) { throw new ArgumentNullException(nameof(pluginAssembly)); } @@ -58,16 +49,24 @@ public static IEnumerable ScanForPluginInstances(Assembly pluginAssembl return ScanForPluginInstancesCore(pluginAssembly); } + /// Discovers a plugin using the conventional root namespace Plugin type name. + /// The assembly to scan. + /// The discovered plugin, or an empty sequence when no conventional plugin type exists. private static IEnumerable ByNamingConventionCore(Assembly pluginAssembly) { var assemblyName = pluginAssembly.GetName().Name; var type = pluginAssembly.GetType($"{assemblyName}.Plugin", false, false); - if (type != null && Activator.CreateInstance(type) is IPlugin plugin) + if (type is null || Activator.CreateInstance(type) is not IPlugin plugin) { - yield return plugin; + yield break; } + + yield return plugin; } + /// Scans the supplied assembly for concrete plugin implementations. + /// The assembly to scan. + /// The plugin instances created from matching exported types. private static IEnumerable ScanForPluginInstancesCore(Assembly pluginAssembly) { var pluginType = typeof(IPlugin); diff --git a/src/Extensions.Hosting.ReactiveUI.Avalonia/Extensions.Hosting.ReactiveUI.Avalonia.csproj b/src/Extensions.Hosting.ReactiveUI.Avalonia/Extensions.Hosting.ReactiveUI.Avalonia.csproj index bd15864..3addb91 100644 --- a/src/Extensions.Hosting.ReactiveUI.Avalonia/Extensions.Hosting.ReactiveUI.Avalonia.csproj +++ b/src/Extensions.Hosting.ReactiveUI.Avalonia/Extensions.Hosting.ReactiveUI.Avalonia.csproj @@ -1,7 +1,7 @@ - + - net8.0;net9.0;net10.0 + net8.0;net9.0;net10.0;net11.0 enable enable CP.Extensions.Hosting.ReactiveUI.Avalonia @@ -12,9 +12,9 @@ - - - + + + diff --git a/src/Extensions.Hosting.ReactiveUI.Avalonia/HostBuilderReactiveUiExtensions.cs b/src/Extensions.Hosting.ReactiveUI.Avalonia/HostBuilderReactiveUiExtensions.cs index 977a8ae..4ca054e 100644 --- a/src/Extensions.Hosting.ReactiveUI.Avalonia/HostBuilderReactiveUiExtensions.cs +++ b/src/Extensions.Hosting.ReactiveUI.Avalonia/HostBuilderReactiveUiExtensions.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.Extensions.Hosting; @@ -10,76 +10,76 @@ namespace ReactiveMarbles.Extensions.Hosting.ReactiveUI; -/// -/// Provides extension methods for configuring ReactiveUI with Microsoft dependency injection in .NET host builder -/// scenarios. -/// +/// 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 /// dependency injection system, supporting both generic host and application host builder patterns. Use these methods /// to set up ReactiveUI infrastructure when building WPF or other .NET applications that rely on dependency /// injection. public static class HostBuilderReactiveUiExtensions { - /// - /// Configures the specified host builder to use Splat with the Microsoft dependency resolver for dependency - /// injection in a WPF application. - /// - /// This method sets up Splat and ReactiveUI to use the Microsoft dependency resolver, enabling - /// seamless integration with the application's dependency injection system. It is intended for use in WPF - /// applications that utilize ReactiveUI and Splat for service location. - /// The host builder to configure with Splat and the Microsoft dependency resolver. Cannot be null. - /// The same instance so that additional configuration calls can be chained. - public static IHostBuilder ConfigureSplatForMicrosoftDependencyResolver(this IHostBuilder hostBuilder) => - hostBuilder.ConfigureServices((serviceCollection) => + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHost host) + { + /// Configures the Microsoft dependency resolver and invokes a custom container factory using the host's service provider. + /// This method enables integration with the Microsoft dependency injection system and allows + /// further customization of the service container. The method returns the same host instance to support fluent + /// configuration. + /// An action that receives the host's service provider for additional container configuration. Cannot be null. + /// The original host instance after configuration, or null if the input host is null. + public IHost? MapSplatLocator(Action containerFactory) { - serviceCollection.UseMicrosoftDependencyResolver(); - AppLocator.CurrentMutable.CreateReactiveUIBuilder() - .WithRegistration(r => r.InitializeSplat()) - .WithAvalonia() - .BuildApp(); - }); + var c = host?.Services; + c?.UseMicrosoftDependencyResolver(); + containerFactory?.Invoke(c); + return host; + } + } - /// - /// Configures Splat to use the Microsoft dependency resolver within the specified host application builder. - /// - /// This method enables Splat and ReactiveUI to resolve dependencies using the - /// Microsoft.Extensions.DependencyInjection container. It should be called during application startup before - /// building the host. - /// The host application builder to configure for Splat and Microsoft dependency injection integration. Cannot be - /// null. - /// The same instance of with Splat configured to use the Microsoft dependency - /// resolver. - /// Thrown if is null. - public static IHostApplicationBuilder ConfigureSplatForMicrosoftDependencyResolver(this IHostApplicationBuilder hostBuilder) + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHostApplicationBuilder hostBuilder) { - if (hostBuilder == null) + /// Configures Splat to use the Microsoft dependency resolver within the specified host application builder. + /// This method enables Splat and ReactiveUI to resolve dependencies using the + /// Microsoft.Extensions.DependencyInjection container. It should be called during application startup before + /// building the host. + /// The same instance of with Splat configured to use the Microsoft dependency + /// resolver. + /// Thrown if is null. + public IHostApplicationBuilder ConfigureSplatForMicrosoftDependencyResolver() { - throw new ArgumentNullException(nameof(hostBuilder)); - } + if (hostBuilder is null) + { + throw new ArgumentNullException(nameof(hostBuilder)); + } - hostBuilder.Services.UseMicrosoftDependencyResolver(); - AppLocator.CurrentMutable.CreateReactiveUIBuilder() - .WithRegistration(r => r.InitializeSplat()) - .WithAvalonia() - .BuildApp(); - return hostBuilder; + hostBuilder.Services.UseMicrosoftDependencyResolver(); + var reactiveUiBuilder = AppLocator.CurrentMutable.CreateReactiveUIBuilder() + .WithRegistration(r => r.InitializeSplat()) + .WithAvalonia(); + _ = reactiveUiBuilder.BuildApp(); + return hostBuilder; + } } - /// - /// Configures the Microsoft dependency resolver and invokes a custom container factory using the host's service - /// provider. - /// - /// This method enables integration with the Microsoft dependency injection system and allows - /// further customization of the service container. The method returns the same host instance to support fluent - /// configuration. - /// The host whose service provider will be used to configure the dependency resolver. Cannot be null. - /// An action that receives the host's service provider for additional container configuration. Cannot be null. - /// The original host instance after configuration, or null if the input host is null. - public static IHost? MapSplatLocator(this IHost host, Action containerFactory) + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHostBuilder hostBuilder) { - var c = host?.Services; - c?.UseMicrosoftDependencyResolver(); - containerFactory?.Invoke(c); - return host; + /// Configures the specified host builder to use Splat with the Microsoft dependency resolver for dependency injection in a WPF application. + /// This method sets up Splat and ReactiveUI to use the Microsoft dependency resolver, enabling + /// seamless integration with the application's dependency injection system. It is intended for use in WPF + /// applications that utilize ReactiveUI and Splat for service location. + /// The same instance so that additional configuration calls can be chained. + public IHostBuilder ConfigureSplatForMicrosoftDependencyResolver() => + hostBuilder.ConfigureServices((serviceCollection) => + { + serviceCollection.UseMicrosoftDependencyResolver(); + var reactiveUiBuilder = AppLocator.CurrentMutable.CreateReactiveUIBuilder() + .WithRegistration(r => r.InitializeSplat()) + .WithAvalonia(); + _ = reactiveUiBuilder.BuildApp(); + }); } } diff --git a/src/Extensions.Hosting.ReactiveUI.Maui/Extensions.Hosting.ReactiveUI.Maui.csproj b/src/Extensions.Hosting.ReactiveUI.Maui/Extensions.Hosting.ReactiveUI.Maui.csproj index 627cea8..93d4700 100644 --- a/src/Extensions.Hosting.ReactiveUI.Maui/Extensions.Hosting.ReactiveUI.Maui.csproj +++ b/src/Extensions.Hosting.ReactiveUI.Maui/Extensions.Hosting.ReactiveUI.Maui.csproj @@ -1,8 +1,8 @@ - + - net9.0;net10.0;net9.0-android;net10.0-android;net9.0-ios;net9.0-tvos;net9.0-macos;net9.0-maccatalyst;net10.0-ios;net10.0-tvos;net10.0-macos;net10.0-maccatalyst; - $(TargetFrameworks);net9.0-windows10.0.19041.0;net10.0-windows10.0.19041.0 + 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 @@ -10,19 +10,19 @@ - - - + + + - - + + - - - + + + diff --git a/src/Extensions.Hosting.ReactiveUI.Maui/HostBuilderReactiveUiExtensions.cs b/src/Extensions.Hosting.ReactiveUI.Maui/HostBuilderReactiveUiExtensions.cs index 69ee118..4edef8d 100644 --- a/src/Extensions.Hosting.ReactiveUI.Maui/HostBuilderReactiveUiExtensions.cs +++ b/src/Extensions.Hosting.ReactiveUI.Maui/HostBuilderReactiveUiExtensions.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.Extensions.Hosting; @@ -9,69 +9,70 @@ namespace ReactiveMarbles.Extensions.Hosting.ReactiveUI; -/// -/// Provides extension methods for configuring ReactiveUI integration with Microsoft dependency injection in .NET host -/// builders. -/// +/// 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 /// dependency injection. Use these methods to configure ReactiveUI services and ensure that Splat uses the Microsoft /// dependency resolver within your application's host configuration pipeline. public static class HostBuilderReactiveUiExtensions { - /// - /// Configures the specified host builder to use the Microsoft dependency resolver for Splat and initializes - /// ReactiveUI integration with .NET MAUI. - /// - /// This method registers the Microsoft dependency resolver for Splat and sets up ReactiveUI for - /// use with .NET MAUI applications. Call this method during application startup to ensure that Splat and ReactiveUI - /// are properly integrated with the dependency injection system. - /// The host builder to configure with Splat and ReactiveUI services. Cannot be null. - /// The same instance so that additional configuration calls can be chained. - public static IHostBuilder ConfigureSplatForMicrosoftDependencyResolver(this IHostBuilder hostBuilder) => - hostBuilder.ConfigureServices((serviceCollection) => + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHost host) + { + /// Configures the dependency resolver for the specified host and invokes a custom container factory action. + /// This method enables integration with the Microsoft dependency resolver and allows for further + /// customization of the service provider through the specified action. The method returns the same host instance to + /// support fluent configuration. + /// An action that receives the host's service provider for additional container configuration. Cannot be null. + /// The original host instance after applying the dependency resolver and container factory configuration. + public IHost? MapSplatLocator(Action containerFactory) { - serviceCollection.UseMicrosoftDependencyResolver(); - AppLocator.CurrentMutable.CreateReactiveUIBuilder() - .WithRegistration(r => r.InitializeSplat()) - .WithMaui() - .BuildApp(); - }); + var c = host?.Services; + c?.UseMicrosoftDependencyResolver(); + containerFactory?.Invoke(c); + return host; + } + } - /// - /// Configures Splat and ReactiveUI to use the Microsoft dependency injection system within the specified host - /// application builder. - /// - /// This method sets up Splat and ReactiveUI to use the Microsoft.Extensions.DependencyInjection - /// service provider, ensuring that dependencies are resolved through the application's DI container. Call this - /// method during application startup before building the host. - /// The host application builder to configure for Splat and ReactiveUI integration. Cannot be null. - /// The same instance, enabling method chaining. - public static IHostApplicationBuilder ConfigureSplatForMicrosoftDependencyResolver(this IHostApplicationBuilder hostBuilder) + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHostApplicationBuilder hostBuilder) { - ArgumentNullException.ThrowIfNull(hostBuilder); + /// Configures Splat and ReactiveUI to use the Microsoft dependency injection system within the specified host application builder. + /// This method sets up Splat and ReactiveUI to use the Microsoft.Extensions.DependencyInjection + /// service provider, ensuring that dependencies are resolved through the application's DI container. Call this + /// method during application startup before building the host. + /// The same instance, enabling method chaining. + public IHostApplicationBuilder ConfigureSplatForMicrosoftDependencyResolver() + { + ArgumentNullException.ThrowIfNull(hostBuilder); - hostBuilder.Services.UseMicrosoftDependencyResolver(); - AppLocator.CurrentMutable.CreateReactiveUIBuilder() - .WithRegistration(r => r.InitializeSplat()) - .WithMaui() - .BuildApp(); - return hostBuilder; + hostBuilder.Services.UseMicrosoftDependencyResolver(); + var reactiveUiBuilder = AppLocator.CurrentMutable.CreateReactiveUIBuilder() + .WithRegistration(r => r.InitializeSplat()) + .WithMaui(); + _ = reactiveUiBuilder.BuildApp(); + return hostBuilder; + } } - /// - /// Configures the dependency resolver for the specified host and invokes a custom container factory action. - /// - /// This method enables integration with the Microsoft dependency resolver and allows for further - /// customization of the service provider through the specified action. The method returns the same host instance to - /// support fluent configuration. - /// The host whose service provider will be used to configure the dependency resolver. Cannot be null. - /// An action that receives the host's service provider for additional container configuration. Cannot be null. - /// The original host instance after applying the dependency resolver and container factory configuration. - public static IHost? MapSplatLocator(this IHost host, Action containerFactory) + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHostBuilder hostBuilder) { - var c = host?.Services; - c?.UseMicrosoftDependencyResolver(); - containerFactory?.Invoke(c); - return host; + /// Configures the specified host builder to use the Microsoft dependency resolver for Splat and initializes ReactiveUI integration with .NET MAUI. + /// This method registers the Microsoft dependency resolver for Splat and sets up ReactiveUI for + /// use with .NET MAUI applications. Call this method during application startup to ensure that Splat and ReactiveUI + /// are properly integrated with the dependency injection system. + /// The same instance so that additional configuration calls can be chained. + public IHostBuilder ConfigureSplatForMicrosoftDependencyResolver() => + hostBuilder.ConfigureServices((serviceCollection) => + { + serviceCollection.UseMicrosoftDependencyResolver(); + var reactiveUiBuilder = AppLocator.CurrentMutable.CreateReactiveUIBuilder() + .WithRegistration(r => r.InitializeSplat()) + .WithMaui(); + _ = reactiveUiBuilder.BuildApp(); + }); } } diff --git a/src/Extensions.Hosting.ReactiveUI.WinForms/Extensions.Hosting.ReactiveUI.WinForms.csproj b/src/Extensions.Hosting.ReactiveUI.WinForms/Extensions.Hosting.ReactiveUI.WinForms.csproj index 52df26c..7874890 100644 --- a/src/Extensions.Hosting.ReactiveUI.WinForms/Extensions.Hosting.ReactiveUI.WinForms.csproj +++ b/src/Extensions.Hosting.ReactiveUI.WinForms/Extensions.Hosting.ReactiveUI.WinForms.csproj @@ -1,8 +1,8 @@ - + - net462;net472;net481;net8.0-windows10.0.19041;net9.0-windows10.0.19041;net10.0-windows10.0.19041 + 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 ReactiveUI support to generic host based dotnet core 8.0 / 9.0 / 10.0 WinForms applications. With this you can enhance your application with a UI, and use all the services provided by the generic host like DI, logging etc, together with this reactive MVVM framework. @@ -10,10 +10,10 @@ - - - - + + + + diff --git a/src/Extensions.Hosting.ReactiveUI.WinForms/HostBuilderReactiveUiExtensions.cs b/src/Extensions.Hosting.ReactiveUI.WinForms/HostBuilderReactiveUiExtensions.cs index bcde208..0aa305c 100644 --- a/src/Extensions.Hosting.ReactiveUI.WinForms/HostBuilderReactiveUiExtensions.cs +++ b/src/Extensions.Hosting.ReactiveUI.WinForms/HostBuilderReactiveUiExtensions.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.Extensions.Hosting; @@ -9,75 +9,78 @@ namespace ReactiveMarbles.Extensions.Hosting.ReactiveUI; -/// -/// Provides extension methods for configuring ReactiveUI with Microsoft dependency injection in .NET host builders. -/// +/// 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 /// Microsoft.Extensions.Hosting infrastructure, allowing applications to use ReactiveUI with standard .NET dependency /// injection. Methods in this class support both IHostBuilder and IHostApplicationBuilder, as well as mapping the Splat /// locator to the application's IServiceProvider. public static class HostBuilderReactiveUiExtensions { - /// - /// Configures the specified host builder to use the Microsoft dependency resolver with Splat and initializes - /// ReactiveUI integration for WinForms applications. - /// - /// This method enables Splat to use the Microsoft dependency injection container and sets up - /// ReactiveUI for WinForms. Call this method before building the host to ensure that Splat and ReactiveUI services - /// are available throughout the application. - /// The host builder to configure with Splat and ReactiveUI services. Cannot be null. - /// The same instance so that additional configuration calls can be chained. - public static IHostBuilder ConfigureSplatForMicrosoftDependencyResolver(this IHostBuilder hostBuilder) => - hostBuilder.ConfigureServices((serviceCollection) => + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHost host) + { + /// Configures the dependency resolver for the specified host and invokes a custom container factory action. + /// This method enables integration with the Microsoft dependency resolver and allows for further + /// customization of the service provider through the specified action. The method does not modify the host instance + /// itself, but may affect global dependency resolution behavior. + /// An action that receives the host's service provider for additional container configuration. Can be null if no + /// additional configuration is required. + /// The original host instance after applying the dependency resolver and invoking the container factory action, or + /// null if the input host is null. + public IHost? MapSplatLocator(Action containerFactory) { - serviceCollection.UseMicrosoftDependencyResolver(); - AppLocator.CurrentMutable.CreateReactiveUIBuilder() - .WithRegistration(r => r.InitializeSplat()) - .WithWinForms() - .BuildApp(); - }); + var c = host?.Services; + c?.UseMicrosoftDependencyResolver(); + containerFactory?.Invoke(c); + return host; + } + } - /// - /// Configures Splat to use the Microsoft dependency resolver within the specified host application builder. - /// - /// This method enables Splat and ReactiveUI to resolve dependencies using the - /// Microsoft.Extensions.DependencyInjection container. Call this method during application startup to ensure that - /// Splat services are registered with the application's dependency injection system. - /// The host application builder to configure for Splat integration. Cannot be null. - /// The same instance, configured to use the Microsoft dependency resolver for - /// Splat. - /// Thrown if is null. - public static IHostApplicationBuilder ConfigureSplatForMicrosoftDependencyResolver(this IHostApplicationBuilder hostBuilder) + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHostApplicationBuilder hostBuilder) { - if (hostBuilder == null) + /// Configures Splat to use the Microsoft dependency resolver within the specified host application builder. + /// This method enables Splat and ReactiveUI to resolve dependencies using the + /// Microsoft.Extensions.DependencyInjection container. Call this method during application startup to ensure that + /// Splat services are registered with the application's dependency injection system. + /// The same instance, configured to use the Microsoft dependency resolver for + /// Splat. + /// Thrown if is null. + public IHostApplicationBuilder ConfigureSplatForMicrosoftDependencyResolver() { - throw new ArgumentNullException(nameof(hostBuilder)); - } + if (hostBuilder is null) + { + throw new ArgumentNullException(nameof(hostBuilder)); + } - hostBuilder.Services.UseMicrosoftDependencyResolver(); - AppLocator.CurrentMutable.CreateReactiveUIBuilder() - .WithRegistration(r => r.InitializeSplat()) - .WithWinForms() - .BuildApp(); - return hostBuilder; + hostBuilder.Services.UseMicrosoftDependencyResolver(); + var reactiveUiBuilder = AppLocator.CurrentMutable.CreateReactiveUIBuilder() + .WithRegistration(r => r.InitializeSplat()) + .WithWinForms(); + _ = reactiveUiBuilder.BuildApp(); + return hostBuilder; + } } - /// - /// Configures the dependency resolver for the specified host and invokes a custom container factory action. - /// - /// This method enables integration with the Microsoft dependency resolver and allows for further - /// customization of the service provider through the specified action. The method does not modify the host instance - /// itself, but may affect global dependency resolution behavior. - /// The host whose service provider will be used to configure the dependency resolver. Cannot be null. - /// An action that receives the host's service provider for additional container configuration. Can be null if no - /// additional configuration is required. - /// The original host instance after applying the dependency resolver and invoking the container factory action, or - /// null if the input host is null. - public static IHost? MapSplatLocator(this IHost host, Action containerFactory) + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHostBuilder hostBuilder) { - var c = host?.Services; - c?.UseMicrosoftDependencyResolver(); - containerFactory?.Invoke(c); - return host; + /// Configures the specified host builder to use the Microsoft dependency resolver with Splat and initializes ReactiveUI integration for WinForms applications. + /// This method enables Splat to use the Microsoft dependency injection container and sets up + /// ReactiveUI for WinForms. Call this method before building the host to ensure that Splat and ReactiveUI services + /// are available throughout the application. + /// The same instance so that additional configuration calls can be chained. + public IHostBuilder ConfigureSplatForMicrosoftDependencyResolver() => + hostBuilder.ConfigureServices((serviceCollection) => + { + serviceCollection.UseMicrosoftDependencyResolver(); + var reactiveUiBuilder = AppLocator.CurrentMutable.CreateReactiveUIBuilder() + .WithRegistration(r => r.InitializeSplat()) + .WithWinForms(); + _ = reactiveUiBuilder.BuildApp(); + }); } } diff --git a/src/Extensions.Hosting.ReactiveUI.WinUI/Extensions.Hosting.ReactiveUI.WinUI.csproj b/src/Extensions.Hosting.ReactiveUI.WinUI/Extensions.Hosting.ReactiveUI.WinUI.csproj index 2d8c356..1386632 100644 --- a/src/Extensions.Hosting.ReactiveUI.WinUI/Extensions.Hosting.ReactiveUI.WinUI.csproj +++ b/src/Extensions.Hosting.ReactiveUI.WinUI/Extensions.Hosting.ReactiveUI.WinUI.csproj @@ -1,8 +1,8 @@ - + - net8.0-windows10.0.19041;net9.0-windows10.0.19041;net10.0-windows10.0.19041 + net8.0-windows10.0.19041;net9.0-windows10.0.19041;net10.0-windows10.0.19041;net11.0-windows10.0.19041 enable enable $(NoWarn);NETSDK1206 @@ -12,9 +12,9 @@ - - - + + + diff --git a/src/Extensions.Hosting.ReactiveUI.WinUI/HostBuilderReactiveUiExtensions.cs b/src/Extensions.Hosting.ReactiveUI.WinUI/HostBuilderReactiveUiExtensions.cs index 55bb67e..1fe75de 100644 --- a/src/Extensions.Hosting.ReactiveUI.WinUI/HostBuilderReactiveUiExtensions.cs +++ b/src/Extensions.Hosting.ReactiveUI.WinUI/HostBuilderReactiveUiExtensions.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.Extensions.Hosting; @@ -9,72 +9,72 @@ namespace ReactiveMarbles.Extensions.Hosting.ReactiveUI; -/// -/// Provides extension methods for configuring ReactiveUI integration with Microsoft dependency injection in .NET host -/// builders. -/// +/// 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 /// dependency injection. They are intended to be used during application startup to ensure that ReactiveUI services are /// properly registered and available throughout the application's lifetime. public static class HostBuilderReactiveUiExtensions { - /// - /// Configures the Splat and ReactiveUI dependency resolvers to use Microsoft.Extensions.DependencyInjection within - /// the specified host builder. - /// - /// Call this method before building the host to ensure that Splat and ReactiveUI services are - /// registered with the Microsoft dependency injection container. This enables seamless integration of ReactiveUI - /// and Splat services with the application's dependency injection system. - /// The host builder to configure with Splat and ReactiveUI dependency resolution using - /// Microsoft.Extensions.DependencyInjection. Cannot be null. - /// The same instance, configured to use Microsoft.Extensions.DependencyInjection for - /// Splat and ReactiveUI dependency resolution. - public static IHostBuilder ConfigureSplatForMicrosoftDependencyResolver(this IHostBuilder hostBuilder) => - hostBuilder.ConfigureServices((serviceCollection) => + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHost host) + { + /// Configures the dependency resolver for the specified host and invokes a custom container factory action. + /// This method enables integration with the Microsoft dependency resolver and allows for further + /// customization of the service provider through the specified action. The method does not modify the host instance + /// itself. + /// An action to perform additional configuration on the service provider. Can be null. + /// The original host instance. Returns null if is null. + public IHost? MapSplatLocator(Action containerFactory) { - serviceCollection.UseMicrosoftDependencyResolver(); - AppLocator.CurrentMutable.CreateReactiveUIBuilder() - .WithRegistration(r => r.InitializeSplat()) - .WithWinUI() - .BuildApp(); - }); + var c = host?.Services; + c?.UseMicrosoftDependencyResolver(); + containerFactory?.Invoke(c); + return host; + } + } - /// - /// Configures Splat and ReactiveUI to use the Microsoft dependency injection system within the specified host - /// builder. - /// - /// This method sets up Splat and ReactiveUI to resolve dependencies using the - /// Microsoft.Extensions.DependencyInjection container. It should be called during application startup before - /// building the host. This configuration is required for proper integration of ReactiveUI and Splat services in - /// applications using Microsoft dependency injection. - /// The host builder to configure for Splat and ReactiveUI integration. Cannot be null. - /// The same instance, enabling method chaining. - public static IHostApplicationBuilder ConfigureSplatForMicrosoftDependencyResolver(this IHostApplicationBuilder hostBuilder) + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHostApplicationBuilder hostBuilder) { - ArgumentNullException.ThrowIfNull(hostBuilder); + /// Configures Splat and ReactiveUI to use the Microsoft dependency injection system within the specified host builder. + /// This method sets up Splat and ReactiveUI to resolve dependencies using the + /// Microsoft.Extensions.DependencyInjection container. It should be called during application startup before + /// building the host. This configuration is required for proper integration of ReactiveUI and Splat services in + /// applications using Microsoft dependency injection. + /// The same instance, enabling method chaining. + public IHostApplicationBuilder ConfigureSplatForMicrosoftDependencyResolver() + { + ArgumentNullException.ThrowIfNull(hostBuilder); - hostBuilder.Services.UseMicrosoftDependencyResolver(); - AppLocator.CurrentMutable.CreateReactiveUIBuilder() - .WithRegistration(r => r.InitializeSplat()) - .WithWinUI() - .BuildApp(); - return hostBuilder; + hostBuilder.Services.UseMicrosoftDependencyResolver(); + var reactiveUiBuilder = AppLocator.CurrentMutable.CreateReactiveUIBuilder() + .WithRegistration(r => r.InitializeSplat()) + .WithWinUI(); + _ = reactiveUiBuilder.BuildApp(); + return hostBuilder; + } } - /// - /// Configures the dependency resolver for the specified host and invokes a custom container factory action. - /// - /// This method enables integration with the Microsoft dependency resolver and allows for further - /// customization of the service provider through the specified action. The method does not modify the host instance - /// itself. - /// The host whose service provider will be used to configure the dependency resolver. Can be null. - /// An action to perform additional configuration on the service provider. Can be null. - /// The original host instance. Returns null if is null. - public static IHost? MapSplatLocator(this IHost host, Action containerFactory) + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHostBuilder hostBuilder) { - var c = host?.Services; - c?.UseMicrosoftDependencyResolver(); - containerFactory?.Invoke(c); - return host; + /// Configures the Splat and ReactiveUI dependency resolvers to use Microsoft.Extensions.DependencyInjection within the specified host builder. + /// Call this method before building the host to ensure that Splat and ReactiveUI services are + /// registered with the Microsoft dependency injection container. This enables seamless integration of ReactiveUI + /// and Splat services with the application's dependency injection system. + /// The same instance, configured to use Microsoft.Extensions.DependencyInjection for + /// Splat and ReactiveUI dependency resolution. + public IHostBuilder ConfigureSplatForMicrosoftDependencyResolver() => + hostBuilder.ConfigureServices((serviceCollection) => + { + serviceCollection.UseMicrosoftDependencyResolver(); + var reactiveUiBuilder = AppLocator.CurrentMutable.CreateReactiveUIBuilder() + .WithRegistration(r => r.InitializeSplat()) + .WithWinUI(); + _ = reactiveUiBuilder.BuildApp(); + }); } } diff --git a/src/Extensions.Hosting.ReactiveUI.Wpf/Extensions.Hosting.ReactiveUI.Wpf.csproj b/src/Extensions.Hosting.ReactiveUI.Wpf/Extensions.Hosting.ReactiveUI.Wpf.csproj index c2637e4..8bb9dd6 100644 --- a/src/Extensions.Hosting.ReactiveUI.Wpf/Extensions.Hosting.ReactiveUI.Wpf.csproj +++ b/src/Extensions.Hosting.ReactiveUI.Wpf/Extensions.Hosting.ReactiveUI.Wpf.csproj @@ -1,8 +1,8 @@ - + - net462;net472;net481;net8.0-windows10.0.19041;net9.0-windows10.0.19041;net10.0-windows10.0.19041 + 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 ReactiveUI support to generic host based dotnet core 8.0 / 9.0 / 10.0 WPF applications. With this you can enhance your application with a UI, and use all the services provided by the generic host like DI, logging etc, together with this reactive MVVM framework. @@ -10,10 +10,10 @@ - - - - + + + + diff --git a/src/Extensions.Hosting.ReactiveUI.Wpf/HostBuilderReactiveUiExtensions.cs b/src/Extensions.Hosting.ReactiveUI.Wpf/HostBuilderReactiveUiExtensions.cs index e1097d9..feb65c3 100644 --- a/src/Extensions.Hosting.ReactiveUI.Wpf/HostBuilderReactiveUiExtensions.cs +++ b/src/Extensions.Hosting.ReactiveUI.Wpf/HostBuilderReactiveUiExtensions.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.Extensions.Hosting; @@ -9,76 +9,76 @@ namespace ReactiveMarbles.Extensions.Hosting.ReactiveUI; -/// -/// Provides extension methods for configuring ReactiveUI with Microsoft dependency injection in .NET host builder -/// scenarios. -/// +/// 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 /// dependency injection system, supporting both generic host and application host builder patterns. Use these methods /// to set up ReactiveUI infrastructure when building WPF or other .NET applications that rely on dependency /// injection. public static class HostBuilderReactiveUiExtensions { - /// - /// Configures the specified host builder to use Splat with the Microsoft dependency resolver for dependency - /// injection in a WPF application. - /// - /// This method sets up Splat and ReactiveUI to use the Microsoft dependency resolver, enabling - /// seamless integration with the application's dependency injection system. It is intended for use in WPF - /// applications that utilize ReactiveUI and Splat for service location. - /// The host builder to configure with Splat and the Microsoft dependency resolver. Cannot be null. - /// The same instance so that additional configuration calls can be chained. - public static IHostBuilder ConfigureSplatForMicrosoftDependencyResolver(this IHostBuilder hostBuilder) => - hostBuilder.ConfigureServices((serviceCollection) => + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHost host) + { + /// Configures the Microsoft dependency resolver and invokes a custom container factory using the host's service provider. + /// This method enables integration with the Microsoft dependency injection system and allows + /// further customization of the service container. The method returns the same host instance to support fluent + /// configuration. + /// An action that receives the host's service provider for additional container configuration. Cannot be null. + /// The original host instance after configuration, or null if the input host is null. + public IHost? MapSplatLocator(Action containerFactory) { - serviceCollection.UseMicrosoftDependencyResolver(); - AppLocator.CurrentMutable.CreateReactiveUIBuilder() - .WithRegistration(r => r.InitializeSplat()) - .WithWpf() - .BuildApp(); - }); + var c = host?.Services; + c?.UseMicrosoftDependencyResolver(); + containerFactory?.Invoke(c); + return host; + } + } - /// - /// Configures Splat to use the Microsoft dependency resolver within the specified host application builder. - /// - /// This method enables Splat and ReactiveUI to resolve dependencies using the - /// Microsoft.Extensions.DependencyInjection container. It should be called during application startup before - /// building the host. - /// The host application builder to configure for Splat and Microsoft dependency injection integration. Cannot be - /// null. - /// The same instance of with Splat configured to use the Microsoft dependency - /// resolver. - /// Thrown if is null. - public static IHostApplicationBuilder ConfigureSplatForMicrosoftDependencyResolver(this IHostApplicationBuilder hostBuilder) + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHostApplicationBuilder hostBuilder) { - if (hostBuilder == null) + /// Configures Splat to use the Microsoft dependency resolver within the specified host application builder. + /// This method enables Splat and ReactiveUI to resolve dependencies using the + /// Microsoft.Extensions.DependencyInjection container. It should be called during application startup before + /// building the host. + /// The same instance of with Splat configured to use the Microsoft dependency + /// resolver. + /// Thrown if is null. + public IHostApplicationBuilder ConfigureSplatForMicrosoftDependencyResolver() { - throw new ArgumentNullException(nameof(hostBuilder)); - } + if (hostBuilder is null) + { + throw new ArgumentNullException(nameof(hostBuilder)); + } - hostBuilder.Services.UseMicrosoftDependencyResolver(); - AppLocator.CurrentMutable.CreateReactiveUIBuilder() - .WithRegistration(r => r.InitializeSplat()) - .WithWpf() - .BuildApp(); - return hostBuilder; + hostBuilder.Services.UseMicrosoftDependencyResolver(); + var reactiveUiBuilder = AppLocator.CurrentMutable.CreateReactiveUIBuilder() + .WithRegistration(r => r.InitializeSplat()) + .WithWpf(); + _ = reactiveUiBuilder.BuildApp(); + return hostBuilder; + } } - /// - /// Configures the Microsoft dependency resolver and invokes a custom container factory using the host's service - /// provider. - /// - /// This method enables integration with the Microsoft dependency injection system and allows - /// further customization of the service container. The method returns the same host instance to support fluent - /// configuration. - /// The host whose service provider will be used to configure the dependency resolver. Cannot be null. - /// An action that receives the host's service provider for additional container configuration. Cannot be null. - /// The original host instance after configuration, or null if the input host is null. - public static IHost? MapSplatLocator(this IHost host, Action containerFactory) + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHostBuilder hostBuilder) { - var c = host?.Services; - c?.UseMicrosoftDependencyResolver(); - containerFactory?.Invoke(c); - return host; + /// Configures the specified host builder to use Splat with the Microsoft dependency resolver for dependency injection in a WPF application. + /// This method sets up Splat and ReactiveUI to use the Microsoft dependency resolver, enabling + /// seamless integration with the application's dependency injection system. It is intended for use in WPF + /// applications that utilize ReactiveUI and Splat for service location. + /// The same instance so that additional configuration calls can be chained. + public IHostBuilder ConfigureSplatForMicrosoftDependencyResolver() => + hostBuilder.ConfigureServices((serviceCollection) => + { + serviceCollection.UseMicrosoftDependencyResolver(); + var reactiveUiBuilder = AppLocator.CurrentMutable.CreateReactiveUIBuilder() + .WithRegistration(r => r.InitializeSplat()) + .WithWpf(); + _ = reactiveUiBuilder.BuildApp(); + }); } } diff --git a/src/Extensions.Hosting.SingleInstance/Extensions.Hosting.SingleInstance.csproj b/src/Extensions.Hosting.SingleInstance/Extensions.Hosting.SingleInstance.csproj index f9ec434..759b77a 100644 --- a/src/Extensions.Hosting.SingleInstance/Extensions.Hosting.SingleInstance.csproj +++ b/src/Extensions.Hosting.SingleInstance/Extensions.Hosting.SingleInstance.csproj @@ -1,13 +1,13 @@ - + - net462;net472;net481;net8.0;net9.0;net10.0 + net462;net472;net48;net481;net8.0;net9.0;net10.0;net11.0 $(NoWarn);CA1812 CP.Extensions.Hosting.SingleInstance - + diff --git a/src/Extensions.Hosting.SingleInstance/HostBuilderApplicationExtensions.cs b/src/Extensions.Hosting.SingleInstance/HostBuilderApplicationExtensions.cs index 9af36e5..4af01d4 100644 --- a/src/Extensions.Hosting.SingleInstance/HostBuilderApplicationExtensions.cs +++ b/src/Extensions.Hosting.SingleInstance/HostBuilderApplicationExtensions.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -10,126 +10,119 @@ namespace ReactiveMarbles.Extensions.Hosting.AppServices; -/// -/// Provides extension methods for configuring a host to ensure that only a single instance of the application runs at a -/// time. -/// +/// Provides extension methods for configuring a host to ensure that only a single instance of the application runs at a time. /// These extension methods add single-instance enforcement to applications built with IHostBuilder or /// IHostApplicationBuilder by configuring a mutex-based lifetime service. This is useful for preventing multiple /// concurrent executions of the same application, such as desktop or service applications that should not be started /// more than once on the same machine. public static class HostBuilderApplicationExtensions { + /// Stores the mutex builder key value. private const string MutexBuilderKey = nameof(MutexBuilder); - /// - /// Configures the host builder to enforce single-instance application behavior using a mutex, and allows additional - /// mutex configuration. - /// - /// This method ensures that only one instance of the application runs at a time by registering a - /// mutex-based lifetime service. Additional mutex options can be configured using the provided . - /// The host builder to configure for single-instance enforcement. Cannot be null. - /// An action to configure the mutex builder. Can be null if no additional configuration is required. - /// The same instance of with single-instance enforcement configured. - /// Thrown if is null. - public static IHostBuilder ConfigureSingleInstance(this IHostBuilder hostBuilder, Action configureAction) + /// Attempts to retrieve an existing instance from the specified properties dictionary, or creates and stores a new one if none exists. + /// The properties dictionary. + /// When this method returns, contains the retrieved or newly created instance. + /// if an existing was found in the dictionary; otherwise, . + private static bool TryRetrieveMutexBuilder(IDictionary properties, out IMutexBuilder mutexBuilder) { - if (hostBuilder is null) + if (properties.TryGetValue(MutexBuilderKey, out var mutexBuilderObject)) { - throw new ArgumentNullException(nameof(hostBuilder)); + mutexBuilder = (IMutexBuilder)mutexBuilderObject; + return true; } - return hostBuilder.ConfigureServices((_, serviceCollection) => + mutexBuilder = new MutexBuilder(); + properties[MutexBuilderKey] = mutexBuilder; + return false; + } + + /// Adds the mutex services to the service collection. + /// The service collection to update. + /// The configured mutex builder. + private static void AddMutexServices(IServiceCollection services, IMutexBuilder mutexBuilder) + { + _ = services + .AddSingleton(mutexBuilder) + .AddHostedService(); + } + + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHostApplicationBuilder hostBuilder) + { + /// Configures the application to enforce single-instance behavior using a mutex and allows customization of the mutex configuration. + /// This method ensures that only one instance of the application runs at a time by registering a + /// mutex and a hosted service to manage application lifetime. Use the parameter + /// to customize mutex options, such as the mutex name or behavior. This method should be called during application + /// startup configuration. + /// An action that configures the mutex builder used to enforce single-instance behavior. This action is invoked + /// with the mutex builder instance. + /// The same instance, enabling further configuration. + /// Thrown if is null. + public IHostApplicationBuilder ConfigureSingleInstance(Action configureAction) { + if (hostBuilder is null) + { + throw new ArgumentNullException(nameof(hostBuilder)); + } + if (!TryRetrieveMutexBuilder(hostBuilder.Properties, out var mutexBuilder)) { - serviceCollection - .AddSingleton(mutexBuilder) - .AddHostedService(); + AddMutexServices(hostBuilder.Services, mutexBuilder); } configureAction?.Invoke(mutexBuilder); - }); - } - /// - /// Configures the application to enforce single-instance behavior using a mutex and allows customization of the - /// mutex configuration. - /// - /// This method ensures that only one instance of the application runs at a time by registering a - /// mutex and a hosted service to manage application lifetime. Use the parameter - /// to customize mutex options, such as the mutex name or behavior. This method should be called during application - /// startup configuration. - /// The host application builder to configure for single-instance enforcement. Cannot be null. - /// An action that configures the mutex builder used to enforce single-instance behavior. This action is invoked - /// with the mutex builder instance. - /// The same instance, enabling further configuration. - /// Thrown if is null. - public static IHostApplicationBuilder ConfigureSingleInstance(this IHostApplicationBuilder hostBuilder, Action configureAction) - { - if (hostBuilder is null) - { - throw new ArgumentNullException(nameof(hostBuilder)); + return hostBuilder; } - if (!TryRetrieveMutexBuilder(hostBuilder.Properties, out var mutexBuilder)) - { - hostBuilder.Services - .AddSingleton(mutexBuilder) - .AddHostedService(); - } - - configureAction?.Invoke(mutexBuilder); - - return hostBuilder; + /// Configures the application to allow only a single instance to run by using a named mutex. + /// Use this method to prevent multiple instances of the application from running simultaneously. + /// The mutex identifier should be unique to the application to avoid conflicts with other applications. + /// The unique identifier for the mutex used to enforce single instance behavior. Cannot be null or empty. + /// The same instance of for chaining further configuration. + public IHostApplicationBuilder ConfigureSingleInstance(string mutexId) => + hostBuilder.ConfigureSingleInstance(builder => builder.MutexId = mutexId); } - /// - /// Configures the host builder to enforce single-instance application behavior using a system-wide mutex. - /// - /// This method ensures that only one instance of the application can run at a time by using a - /// named mutex. If another instance is already running with the same mutex identifier, subsequent instances will - /// not start. Use a unique mutex identifier to avoid conflicts with other applications. - /// The host builder to configure for single-instance enforcement. - /// The unique identifier for the mutex used to ensure only one instance of the application runs. Cannot be null or - /// empty. - /// The same host builder instance, configured for single-instance enforcement. - public static IHostBuilder ConfigureSingleInstance(this IHostBuilder hostBuilder, string mutexId) => - hostBuilder.ConfigureSingleInstance(builder => builder.MutexId = mutexId); - - /// - /// Configures the application to allow only a single instance to run by using a named mutex. - /// - /// Use this method to prevent multiple instances of the application from running simultaneously. - /// The mutex identifier should be unique to the application to avoid conflicts with other applications. - /// The host application builder to configure for single instance enforcement. - /// The unique identifier for the mutex used to enforce single instance behavior. Cannot be null or empty. - /// The same instance of for chaining further configuration. - public static IHostApplicationBuilder ConfigureSingleInstance(this IHostApplicationBuilder hostBuilder, string mutexId) => - hostBuilder.ConfigureSingleInstance(builder => builder.MutexId = mutexId); - - /// - /// Attempts to retrieve an existing instance from the specified properties dictionary, - /// or creates and stores a new one if none exists. - /// - /// If no is present in the dictionary, a new instance is created, - /// added to the dictionary, and returned via the parameter. - /// The dictionary of properties used to store and retrieve the instance. Cannot be - /// null. - /// When this method returns, contains the retrieved or newly created instance. - /// if an existing was found in the dictionary; otherwise, . - private static bool TryRetrieveMutexBuilder(this IDictionary properties, out IMutexBuilder mutexBuilder) + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHostBuilder hostBuilder) { - if (properties.TryGetValue(MutexBuilderKey, out var mutexBuilderObject)) + /// Configures the host builder to enforce single-instance application behavior using a mutex, and allows additional mutex configuration. + /// This method ensures that only one instance of the application runs at a time by registering a + /// mutex-based lifetime service. Additional mutex options can be configured using the provided . + /// An action to configure the mutex builder. Can be null if no additional configuration is required. + /// The same instance of with single-instance enforcement configured. + /// Thrown if is null. + public IHostBuilder ConfigureSingleInstance(Action configureAction) { - mutexBuilder = (IMutexBuilder)mutexBuilderObject; - return true; + if (hostBuilder is null) + { + throw new ArgumentNullException(nameof(hostBuilder)); + } + + return hostBuilder.ConfigureServices((_, serviceCollection) => + { + if (!TryRetrieveMutexBuilder(hostBuilder.Properties, out var mutexBuilder)) + { + AddMutexServices(serviceCollection, mutexBuilder); + } + + configureAction?.Invoke(mutexBuilder); + }); } - mutexBuilder = new MutexBuilder(); - properties[MutexBuilderKey] = mutexBuilder; - return false; + /// Configures the host builder to enforce single-instance application behavior using a system-wide mutex. + /// This method ensures that only one instance of the application can run at a time by using a + /// named mutex. If another instance is already running with the same mutex identifier, subsequent instances will + /// not start. Use a unique mutex identifier to avoid conflicts with other applications. + /// The unique identifier for the mutex used to ensure only one instance of the application runs. Cannot be null or + /// empty. + /// The same host builder instance, configured for single-instance enforcement. + public IHostBuilder ConfigureSingleInstance(string mutexId) => + hostBuilder.ConfigureSingleInstance(builder => builder.MutexId = mutexId); } } diff --git a/src/Extensions.Hosting.SingleInstance/IMutexBuilder.cs b/src/Extensions.Hosting.SingleInstance/IMutexBuilder.cs index 0544512..76d0f48 100644 --- a/src/Extensions.Hosting.SingleInstance/IMutexBuilder.cs +++ b/src/Extensions.Hosting.SingleInstance/IMutexBuilder.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -8,28 +8,19 @@ namespace ReactiveMarbles.Extensions.Hosting.AppServices; -/// -/// Defines a builder for configuring and creating a named mutex, typically used to enforce single-instance application -/// behavior. -/// +/// Defines a builder for configuring and creating a named mutex, typically used to enforce single-instance application behavior. /// Implementations of this interface allow customization of the mutex name, scope (global or local), and /// the action to perform when another instance is already running. This is commonly used in scenarios where only one /// instance of an application should be active at a time. public interface IMutexBuilder { - /// - /// Gets or sets the unique identifier for the mutex associated with the current operation or resource. - /// + /// Gets or sets the unique identifier for the mutex associated with the current operation or resource. string? MutexId { get; set; } - /// - /// Gets or sets a value indicating whether the setting applies globally to all users or contexts. - /// + /// Gets or sets a value indicating whether the setting applies globally to all users or contexts. bool IsGlobal { get; set; } - /// - /// Gets or sets the action to execute when the current process is not the first instance of the application. - /// + /// Gets or sets the action to execute when the current process is not the first instance of the application. /// This action is invoked with the application's host environment and logger as parameters. Use /// this property to define custom behavior, such as notifying the user or logging a message, when a subsequent /// instance of the application is detected. diff --git a/src/Extensions.Hosting.SingleInstance/Internal/MutexBuilder.cs b/src/Extensions.Hosting.SingleInstance/Internal/MutexBuilder.cs index f58e9d6..6f3abee 100644 --- a/src/Extensions.Hosting.SingleInstance/Internal/MutexBuilder.cs +++ b/src/Extensions.Hosting.SingleInstance/Internal/MutexBuilder.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -8,13 +8,11 @@ namespace ReactiveMarbles.Extensions.Hosting.AppServices.Internal; -/// -/// Provides a builder for configuring and creating mutex instances used to coordinate application instance exclusivity. -/// +/// Provides a builder for configuring and creating mutex instances used to coordinate application instance exclusivity. /// This class is intended for internal use to facilitate the construction of mutexes that manage /// single-instance application scenarios. It exposes properties for specifying the mutex identifier, scope, and actions /// to perform when the current process is not the first instance. -internal class MutexBuilder : IMutexBuilder +internal sealed class MutexBuilder : IMutexBuilder { /// public string? MutexId { get; set; } diff --git a/src/Extensions.Hosting.SingleInstance/Internal/MutexLifetimeService.cs b/src/Extensions.Hosting.SingleInstance/Internal/MutexLifetimeService.cs index f1d3be6..b343ca1 100644 --- a/src/Extensions.Hosting.SingleInstance/Internal/MutexLifetimeService.cs +++ b/src/Extensions.Hosting.SingleInstance/Internal/MutexLifetimeService.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -10,10 +10,7 @@ namespace ReactiveMarbles.Extensions.Hosting.AppServices.Internal; -/// -/// Provides an IHostedService implementation that ensures only a single instance of the application runs by acquiring a -/// named mutex at startup. -/// +/// Provides an IHostedService implementation that ensures only a single instance of the application runs by acquiring a named mutex at startup. /// If another instance of the application is already running and holds the mutex, this service will stop /// the current application during startup. This helps prevent multiple instances from running concurrently in /// environments where only one instance should be active. @@ -21,18 +18,32 @@ namespace ReactiveMarbles.Extensions.Hosting.AppServices.Internal; /// The host environment information for the current application instance. /// The application lifetime control used to manage startup and shutdown events. /// The mutex builder that provides configuration for creating and identifying the application mutex. -internal class MutexLifetimeService(ILogger logger, IHostEnvironment hostEnvironment, IHostApplicationLifetime hostApplicationLifetime, IMutexBuilder mutexBuilder) : IHostedService +internal sealed class MutexLifetimeService(ILogger logger, IHostEnvironment hostEnvironment, IHostApplicationLifetime hostApplicationLifetime, IMutexBuilder mutexBuilder) : IHostedService { + /// Logs that another application instance is already running. + private static readonly Action _applicationAlreadyRunning = + LoggerMessage.Define(LogLevel.Debug, new EventId(1, nameof(_applicationAlreadyRunning)), "Application {ApplicationName} already running, stopping application."); + + /// Logs that the mutex is closing during application stopping. + private static readonly Action _closingMutex = + LoggerMessage.Define(LogLevel.Information, new EventId(2, nameof(_closingMutex)), "OnStopping has been called, closing mutex."); + + /// Stores the logger value. private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + /// Stores the host environment value. private readonly IHostEnvironment _hostEnvironment = hostEnvironment ?? throw new ArgumentNullException(nameof(hostEnvironment)); + + /// Stores the host application lifetime value. private readonly IHostApplicationLifetime _hostApplicationLifetime = hostApplicationLifetime ?? throw new ArgumentNullException(nameof(hostApplicationLifetime)); + + /// Stores the mutex builder value. private readonly IMutexBuilder _mutexBuilder = mutexBuilder ?? throw new ArgumentNullException(nameof(mutexBuilder)); + + /// Stores the resource mutex value. private ResourceMutex? _resourceMutex; - /// - /// Initializes the resource mutex and starts the application asynchronously, handling scenarios where another - /// instance may already be running. - /// + /// Initializes the resource mutex and starts the application asynchronously, handling scenarios where another instance may already be running. /// If another instance of the application is already running, the method invokes the configured /// callback for non-primary instances and initiates application shutdown. This method should be called during /// application startup to ensure proper resource locking and instance management. @@ -42,33 +53,29 @@ public Task StartAsync(CancellationToken cancellationToken) { _resourceMutex = ResourceMutex.Create(_logger, _mutexBuilder.MutexId, _hostEnvironment.ApplicationName, _mutexBuilder.IsGlobal); - _hostApplicationLifetime.ApplicationStopping.Register(OnStopping); + _ = _hostApplicationLifetime.ApplicationStopping.Register(OnStopping); if (!_resourceMutex.IsLocked) { _mutexBuilder.WhenNotFirstInstance?.Invoke(_hostEnvironment, _logger); - _logger.LogDebug("Application {applicationName} already running, stopping application.", _hostEnvironment.ApplicationName); + _applicationAlreadyRunning(_logger, _hostEnvironment.ApplicationName, null); _hostApplicationLifetime.StopApplication(); } return Task.CompletedTask; } - /// - /// Attempts to stop the operation asynchronously, honoring cancellation requests. - /// + /// Attempts to stop the operation asynchronously, honoring cancellation requests. /// A token that can be used to request cancellation of the stop operation. /// A task that represents the asynchronous stop operation. public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; - /// - /// Performs cleanup operations when the service is stopping, including releasing resources held by the mutex. - /// + /// Performs cleanup operations when the service is stopping, including releasing resources held by the mutex. /// This method should be called during the service shutdown sequence to ensure that any held /// resources are properly released. Failing to call this method may result in resource leaks or contention issues /// if the mutex is not disposed. private void OnStopping() { - _logger.LogInformation("OnStopping has been called, closing mutex."); + _closingMutex(_logger, null); _resourceMutex?.Dispose(); _resourceMutex = null; } diff --git a/src/Extensions.Hosting.SingleInstance/ResourceMutex.cs b/src/Extensions.Hosting.SingleInstance/ResourceMutex.cs index 1ca3f8d..53b2819 100644 --- a/src/Extensions.Hosting.SingleInstance/ResourceMutex.cs +++ b/src/Extensions.Hosting.SingleInstance/ResourceMutex.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -13,10 +13,7 @@ namespace ReactiveMarbles.Extensions.Hosting.AppServices; -/// -/// Provides a cross-process mutex for synchronizing access to a named resource, ensuring that only one instance can -/// hold the lock at a time across the system or user session. -/// +/// Provides a cross-process mutex for synchronizing access to a named resource, ensuring that only one instance can hold the lock at a time across the system or user session. /// Use this class to prevent multiple instances of an application or process from accessing the same /// resource simultaneously. The mutex can be created as either global (system-wide) or local (per user session), /// depending on the use case. The class is disposable; always call Dispose when the mutex is no longer needed to @@ -24,18 +21,68 @@ namespace ReactiveMarbles.Extensions.Hosting.AppServices; /// ResourceMutex instance. public sealed class ResourceMutex : IDisposable { + /// Logs a mutex acquisition attempt. + private static readonly Action _tryingToGetMutex = + LoggerMessage.Define(LogLevel.Debug, new EventId(1, nameof(_tryingToGetMutex)), "{ResourceName} is trying to get Mutex {MutexId}"); + + /// Logs a mutex owner release timeout. + private static readonly Action _mutexOwnerReleaseTimedOut = + LoggerMessage.Define(LogLevel.Warning, new EventId(2, nameof(_mutexOwnerReleaseTimedOut)), "Timed out waiting for mutex owner thread to release Mutex {MutexId} for {ResourceName}"); + + /// Logs that a mutex is already in use. + private static readonly Action _mutexAlreadyInUse = + LoggerMessage.Define(LogLevel.Warning, new EventId(3, nameof(_mutexAlreadyInUse)), "Mutex {MutexId} is already in use and couldn't be locked for the caller {ResourceName}"); + + /// Logs that an existing mutex was claimed. + private static readonly Action _mutexClaimed = + LoggerMessage.Define(LogLevel.Information, new EventId(4, nameof(_mutexClaimed)), "{ResourceName} has claimed the mutex {MutexId}"); + + /// Logs that a new mutex was created and claimed. + private static readonly Action _mutexCreatedAndClaimed = + LoggerMessage.Define(LogLevel.Information, new EventId(5, nameof(_mutexCreatedAndClaimed)), "{ResourceName} has created & claimed the mutex {MutexId}"); + + /// Logs that an abandoned mutex was recovered. + private static readonly Action _abandonedMutexRecovered = + LoggerMessage.Define(LogLevel.Warning, new EventId(6, nameof(_abandonedMutexRecovered)), "{ResourceName} didn't cleanup correctly, but we got the mutex {MutexId}."); + + /// Logs that mutex access was unauthorized. + private static readonly Action _mutexUnauthorized = + LoggerMessage.Define(LogLevel.Error, new EventId(7, nameof(_mutexUnauthorized)), "{ResourceName} is most likely already running for a different user in the same session, can't create/get mutex {MutexId} due to error."); + + /// Logs that mutex acquisition failed. + private static readonly Action _mutexAcquisitionFailed = + LoggerMessage.Define(LogLevel.Error, new EventId(8, nameof(_mutexAcquisitionFailed)), "Problem obtaining the Mutex {MutexId} for {ResourceName}, assuming it was already taken!"); + + /// Logs that a mutex was released. + private static readonly Action _mutexReleased = + LoggerMessage.Define(LogLevel.Information, new EventId(9, nameof(_mutexReleased)), "Released Mutex {MutexId} for {ResourceName}"); + + /// Logs that mutex release failed. + private static readonly Action _mutexReleaseFailed = + LoggerMessage.Define(LogLevel.Error, new EventId(10, nameof(_mutexReleaseFailed)), "Error releasing Mutex {MutexId} for {ResourceName}"); + + /// Stores the logger value. private readonly ILogger _logger; + + /// Stores the mutex id value. private readonly string _mutexId; + + /// Stores the resource name value. private readonly string _resourceName; + + /// Stores the lock acquired signal value. private readonly ManualResetEventSlim _lockAcquiredSignal = new(initialState: false); + + /// Stores the release signal value. private readonly ManualResetEventSlim _releaseSignal = new(initialState: false); + + /// Stores the disposed value. private bool _disposedValue; + + /// Stores the owner thread value. private Thread? _ownerThread; - /// - /// Initializes a new instance of the class with the specified logger, mutex identifier, and optional. - /// resource name. - /// + /// Initializes a new instance of the class with the specified logger, mutex identifier, and optional. resource name. /// The logger instance used to record diagnostic and operational messages for the mutex. /// The unique identifier for the mutex. Used to distinguish this mutex from others. /// The name of the resource associated with the mutex. If null, the mutex identifier is used as the resource name. @@ -46,14 +93,10 @@ private ResourceMutex(ILogger logger, string mutexId, string? resourceName = nul _resourceName = resourceName ?? mutexId; } - /// - /// Gets a value indicating whether the object is locked. - /// + /// Gets a value indicating whether the object is locked. public bool IsLocked { get; private set; } - /// - /// Creates and acquires a new resource mutex for synchronizing access to a named resource across processes. - /// + /// Creates and acquires a new resource mutex for synchronizing access to a named resource across processes. /// The returned ResourceMutex is locked upon creation. Callers are responsible for releasing the /// mutex when it is no longer needed. If a logger is not provided, a default logger is created /// internally. @@ -63,38 +106,32 @@ private ResourceMutex(ILogger logger, string mutexId, string? resourceName = nul /// true to create a system-wide (global) mutex; false to create a local mutex scoped to the current user session. /// A ResourceMutex instance that is already acquired and can be used to synchronize access to the specified /// resource. - /// Thrown if mutexId is null, empty, or consists only of white-space characters. + /// Thrown if mutexId is null, empty, or consists only of white-space characters. public static ResourceMutex Create(ILogger? logger, string? mutexId, string? resourceName = null, bool global = false) { - if (string.IsNullOrWhiteSpace(mutexId)) - { - throw new ArgumentNullException(nameof(mutexId)); - } + var resourceMutexId = !string.IsNullOrWhiteSpace(mutexId) + ? mutexId + : throw new ArgumentException("The mutex identifier cannot be null, empty, or consist only of white-space characters.", nameof(mutexId)); logger ??= new LoggerFactory().CreateLogger(); - var applicationMutex = new ResourceMutex(logger, (global ? @"Global\" : @"Local\") + mutexId, resourceName); - applicationMutex.Lock(); + var applicationMutex = new ResourceMutex(logger, (global ? @"Global\" : @"Local\") + resourceMutexId, resourceName); + _ = applicationMutex.Lock(); return applicationMutex; } - /// - /// Attempts to acquire an exclusive application-wide mutex to prevent multiple instances from running concurrently. - /// + /// Attempts to acquire an exclusive application-wide mutex to prevent multiple instances from running concurrently. /// This method spawns a dedicated background thread to own the mutex for its entire lifetime. /// This ensures that can safely release the mutex from any calling thread, because - /// is always invoked on the thread that originally + /// is always invoked on the thread that originally /// acquired it. If the mutex is already held by another process or user, the method returns false. The lock /// should be released by calling when no longer needed to avoid resource /// contention. /// true if the mutex was successfully acquired and the lock is held by the current instance; otherwise, false. public bool Lock() { - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("{resourceName} is trying to get Mutex {mutexId}", _resourceName, _mutexId); - } + _tryingToGetMutex(_logger, _resourceName, _mutexId, null); - _ownerThread = new Thread(AcquireAndHoldMutex) + _ownerThread = new(AcquireAndHoldMutex) { IsBackground = true, Name = $"ResourceMutex-{_resourceName}", @@ -107,9 +144,7 @@ public bool Lock() return IsLocked; } - /// - /// Releases all resources used by the current instance and releases the associated application mutex if it is held. - /// + /// Releases all resources used by the current instance and releases the associated application mutex if it is held. /// Call this method when you are finished using the object to ensure that the mutex is properly /// released and resources are freed. After calling Dispose, the object should not be used. This method is safe to /// call multiple times; subsequent calls have no effect. @@ -126,7 +161,7 @@ public void Dispose() _releaseSignal.Set(); if (_ownerThread?.Join(TimeSpan.FromSeconds(5)) == false) { - _logger.LogWarning("Timed out waiting for mutex owner thread to release Mutex {mutexId} for {resourceName}", _mutexId, _resourceName); + _mutexOwnerReleaseTimedOut(_logger, _mutexId, _resourceName, null); } _releaseSignal.Dispose(); @@ -135,7 +170,7 @@ public void Dispose() /// /// Runs on the dedicated owner thread: acquires the mutex, signals the caller, waits for the dispose - /// signal, then releases the mutex — all on the same thread to satisfy + /// signal, then releases the mutex - all on the same thread to satisfy /// thread-affinity requirements. /// private void AcquireAndHoldMutex() @@ -150,16 +185,16 @@ private void AcquireAndHoldMutex() #if NET462 // Added Mutex Security, hopefully this prevents the UnauthorizedAccessException more gracefully var sid = new SecurityIdentifier(WellKnownSidType.WorldSid, null); - var mutexsecurity = new MutexSecurity(); - mutexsecurity.AddAccessRule(new MutexAccessRule(sid, MutexRights.FullControl, AccessControlType.Allow)); - mutexsecurity.AddAccessRule(new MutexAccessRule(sid, MutexRights.ChangePermissions, AccessControlType.Deny)); - mutexsecurity.AddAccessRule(new MutexAccessRule(sid, MutexRights.Delete, AccessControlType.Deny)); + var mutexSecurity = new MutexSecurity(); + mutexSecurity.AddAccessRule(new(sid, MutexRights.FullControl, AccessControlType.Allow)); + mutexSecurity.AddAccessRule(new(sid, MutexRights.ChangePermissions, AccessControlType.Deny)); + mutexSecurity.AddAccessRule(new(sid, MutexRights.Delete, AccessControlType.Deny)); // 1) Create Mutex - applicationMutex = new Mutex(true, _mutexId, out var createdNew, mutexsecurity); + applicationMutex = new(true, _mutexId, out var createdNew, mutexSecurity); #else // 1) Create Mutex - applicationMutex = new Mutex(true, _mutexId, out var createdNew); + applicationMutex = new(true, _mutexId, out var createdNew); #endif // 2) if the mutex wasn't created new get the right to it, this returns false if it's already locked @@ -168,7 +203,7 @@ private void AcquireAndHoldMutex() IsLocked = applicationMutex.WaitOne(2000, false); if (!IsLocked) { - _logger.LogWarning("Mutex {mutexId} is already in use and couldn't be locked for the caller {resourceName}", _mutexId, _resourceName); + _mutexAlreadyInUse(_logger, _mutexId, _resourceName, null); // Clean up applicationMutex.Dispose(); @@ -176,13 +211,13 @@ private void AcquireAndHoldMutex() } else { - _logger.LogInformation("{resourceName} has claimed the mutex {mutexId}", _resourceName, _mutexId); + _mutexClaimed(_logger, _resourceName, _mutexId, null); mutexAcquired = true; } } else { - _logger.LogInformation("{resourceName} has created & claimed the mutex {mutexId}", _resourceName, _mutexId); + _mutexCreatedAndClaimed(_logger, _resourceName, _mutexId, null); mutexAcquired = true; } } @@ -190,21 +225,17 @@ private void AcquireAndHoldMutex() { // Another instance didn't cleanup correctly! // we can ignore the exception, it happened on the "WaitOne" but still the mutex belongs to us - _logger.LogWarning(e, "{resourceName} didn't cleanup correctly, but we got the mutex {mutexId}.", _resourceName, _mutexId); + _abandonedMutexRecovered(_logger, _resourceName, _mutexId, e); mutexAcquired = true; } catch (UnauthorizedAccessException e) { - _logger.LogError( - e, - "{resourceName} is most likely already running for a different user in the same session, can't create/get mutex {mutexId} due to error.", - _resourceName, - _mutexId); + _mutexUnauthorized(_logger, _resourceName, _mutexId, e); IsLocked = false; } catch (Exception ex) { - _logger.LogError(ex, "Problem obtaining the Mutex {mutexId} for {resourceName}, assuming it was already taken!", _resourceName, _mutexId); + _mutexAcquisitionFailed(_logger, _mutexId, _resourceName, ex); IsLocked = false; } finally @@ -221,21 +252,21 @@ private void AcquireAndHoldMutex() // Hold until Dispose() signals that we should release. _releaseSignal.Wait(); - // Release on this thread — the same thread that acquired the mutex — to satisfy Mutex thread-affinity. + // Release on this thread - the same thread that acquired the mutex - to satisfy Mutex thread-affinity. try { - if (IsLocked && applicationMutex != null) + if (IsLocked && applicationMutex is not null) { applicationMutex.ReleaseMutex(); IsLocked = false; - _logger.LogInformation("Released Mutex {mutexId} for {resourceName}", _mutexId, _resourceName); + _mutexReleased(_logger, _mutexId, _resourceName, null); } applicationMutex?.Dispose(); } catch (Exception ex) { - _logger.LogError(ex, "Error releasing Mutex {mutexId} for {resourceName}", _mutexId, _resourceName); + _mutexReleaseFailed(_logger, _mutexId, _resourceName, ex); } } } diff --git a/src/Extensions.Hosting.WinForms/Extensions.Hosting.WinForms.csproj b/src/Extensions.Hosting.WinForms/Extensions.Hosting.WinForms.csproj index f004403..939a917 100644 --- a/src/Extensions.Hosting.WinForms/Extensions.Hosting.WinForms.csproj +++ b/src/Extensions.Hosting.WinForms/Extensions.Hosting.WinForms.csproj @@ -1,7 +1,7 @@ - + - net462;net472;net481;net8.0-windows;net9.0-windows;net10.0-windows + net462;net472;net48;net481;net8.0-windows;net9.0-windows;net10.0-windows;net11.0-windows true true This extension adds WinForms support to generic host based applications. With this you can enhance your application with a UI, and use all the services provided by the generic host like DI, logging etc. diff --git a/src/Extensions.Hosting.WinForms/HostBuilderWinFormsExtensions.cs b/src/Extensions.Hosting.WinForms/HostBuilderWinFormsExtensions.cs index d6ca4ae..b282d0e 100644 --- a/src/Extensions.Hosting.WinForms/HostBuilderWinFormsExtensions.cs +++ b/src/Extensions.Hosting.WinForms/HostBuilderWinFormsExtensions.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -11,10 +11,7 @@ namespace ReactiveMarbles.Extensions.Hosting.WinForms; -/// -/// Provides extension methods for configuring and integrating Windows Forms (WinForms) application lifetime and -/// services with .NET Generic Host builders. -/// +/// Provides extension methods for configuring and integrating Windows Forms (WinForms) application lifetime and services with .NET Generic Host builders. /// These extensions enable seamless integration of WinForms applications with the .NET hosting model, /// allowing developers to configure WinForms services, specify the main form (shell), and control application lifetime /// in relation to the host. Methods are provided for both IHostBuilder and IHostApplicationBuilder to support different @@ -22,211 +19,191 @@ namespace ReactiveMarbles.Extensions.Hosting.WinForms; /// ensure proper startup and shutdown coordination between the WinForms UI and the host. public static class HostBuilderWinFormsExtensions { + /// Stores the win forms context key value. private const string WinFormsContextKey = nameof(WinFormsContext); - /// - /// Configures the host to use Windows Forms lifetime management, enabling the application to start and stop in - /// coordination with the WinForms message loop. - /// - /// This method links the application's lifetime to the Windows Forms message loop, so the host - /// will start when the message loop begins and stop when it ends. Use this method when building WinForms - /// applications that require integration with the generic host infrastructure. - /// The host builder to configure for Windows Forms lifetime integration. Cannot be null. - /// The same instance of for chaining further configuration, or null if is null. - public static IHostBuilder? UseWinFormsLifetime(this IHostBuilder hostBuilder) => - hostBuilder?.ConfigureServices((_, __) => - { - TryRetrieveWinFormsContext(hostBuilder.Properties, out var winFormsContext); - winFormsContext.IsLifetimeLinked = true; - }); - - /// - /// Enables WinForms lifetime management for the application, allowing the host to be controlled by the WinForms - /// message loop. - /// - /// When WinForms lifetime is enabled, the application's lifetime is tied to the WinForms message - /// loop. This is typically used in WinForms applications to ensure that the host shuts down when the main form - /// closes. - /// The host builder to configure with WinForms lifetime support. Cannot be null. - /// The same instance of for chaining further configuration. - /// Thrown if is null. - public static IHostApplicationBuilder UseWinFormsLifetime(this IHostApplicationBuilder hostBuilder) + /// Attempts to retrieve an existing Windows Forms context from the specified property dictionary. + /// If a Windows Forms context does not exist in the dictionary, this method creates a new + /// context, adds it to the dictionary, and returns it in the out parameter. + /// The property dictionary used to store the context. + /// When this method returns, contains the retrieved Windows Forms context if found; otherwise, a new context + /// instance. + /// true if an existing Windows Forms context was found in the dictionary; otherwise, false. + private static bool TryRetrieveWinFormsContext(IDictionary properties, out IWinFormsContext winFormsContext) { - if (hostBuilder == null) + if (properties.TryGetValue(WinFormsContextKey, out var winFormsContextAsObject)) { - throw new ArgumentNullException(nameof(hostBuilder)); + winFormsContext = (IWinFormsContext)winFormsContextAsObject; + return true; } - TryRetrieveWinFormsContext(hostBuilder.Properties, out var winFormsContext); - winFormsContext.IsLifetimeLinked = true; - return hostBuilder; + winFormsContext = new WinFormsContext(); + properties[WinFormsContextKey] = winFormsContext; + return false; } - /// - /// Configures WinForms support for the specified host builder, enabling integration of Windows Forms message loop - /// and services into the application's hosting environment. - /// - /// This method registers the necessary services to run a Windows Forms message loop within a - /// generic host. Call this method before building the host to ensure WinForms integration is available throughout - /// the application's lifetime. - /// The host builder to configure for WinForms support. Cannot be null. - /// An optional delegate to further configure the WinForms context. If null, no additional configuration is - /// performed. - /// The same instance of with WinForms services configured, or null if is null. - public static IHostBuilder? ConfigureWinForms(this IHostBuilder hostBuilder, Action? configureAction = null) => - hostBuilder?.ConfigureServices((_, serviceCollection) => + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHostApplicationBuilder hostBuilder) + { + /// Enables WinForms lifetime management for the application, allowing the host to be controlled by the WinForms message loop. + /// When WinForms lifetime is enabled, the application's lifetime is tied to the WinForms message + /// loop. This is typically used in WinForms applications to ensure that the host shuts down when the main form + /// closes. + /// The same instance of for chaining further configuration. + /// Thrown if is null. + public IHostApplicationBuilder UseWinFormsLifetime() + { + if (hostBuilder is null) + { + throw new ArgumentNullException(nameof(hostBuilder)); + } + + _ = TryRetrieveWinFormsContext(hostBuilder.Properties, out var winFormsContext); + winFormsContext.IsLifetimeLinked = true; + return hostBuilder; + } + + /// Configures WinForms support for the specified host application builder and optionally allows additional WinForms-specific configuration. + /// This method registers the necessary services to enable WinForms integration in a generic host + /// application. If called multiple times, WinForms services are only registered once. Use the parameter to customize the WinForms context as needed. + /// An optional delegate to configure the WinForms context after it is created. If null, no additional configuration + /// is performed. + /// The same instance of with WinForms services configured. + /// Thrown if is null. + public IHostApplicationBuilder ConfigureWinForms(Action? configureAction = null) { + if (hostBuilder is null) + { + throw new ArgumentNullException(nameof(hostBuilder)); + } + if (!TryRetrieveWinFormsContext(hostBuilder.Properties, out var winFormsContext)) { - serviceCollection + _ = hostBuilder.Services .AddSingleton(winFormsContext) .AddSingleton(serviceProvider => new WinFormsThread(serviceProvider)) .AddHostedService(); } configureAction?.Invoke(winFormsContext); - }); - - /// - /// Configures WinForms support for the specified host application builder and optionally allows additional - /// WinForms-specific configuration. - /// - /// This method registers the necessary services to enable WinForms integration in a generic host - /// application. If called multiple times, WinForms services are only registered once. Use the parameter to customize the WinForms context as needed. - /// The host application builder to configure for WinForms support. Cannot be null. - /// An optional delegate to configure the WinForms context after it is created. If null, no additional configuration - /// is performed. - /// The same instance of with WinForms services configured. - /// Thrown if is null. - public static IHostApplicationBuilder ConfigureWinForms(this IHostApplicationBuilder hostBuilder, Action? configureAction = null) - { - if (hostBuilder == null) - { - throw new ArgumentNullException(nameof(hostBuilder)); + return hostBuilder; } - if (!TryRetrieveWinFormsContext(hostBuilder.Properties, out var winFormsContext)) + /// Configures WinForms support for the application and registers the specified main form type as a singleton service. + /// If also implements , it is + /// registered as a singleton service for that interface as well. This enables dependency injection of the main form + /// and shell interface throughout the application. + /// The type of the main form to register. Must inherit from . + /// An optional delegate to configure additional WinForms services or settings. Can be . + /// The same instance for chaining. + /// Thrown if is . + public IHostApplicationBuilder ConfigureWinForms(Action? configureAction = null) + where TView : Form { - hostBuilder.Services - .AddSingleton(winFormsContext) - .AddSingleton(serviceProvider => new WinFormsThread(serviceProvider)) - .AddHostedService(); - } - - configureAction?.Invoke(winFormsContext); - return hostBuilder; - } - - /// - /// Configures WinForms support for the host builder and registers the specified main view form as a singleton - /// service. - /// - /// If also implements , it is - /// registered as a singleton service for that interface as well. This method enables dependency injection and - /// configuration for WinForms applications using the generic host. - /// The type of the main WinForms form to register. Must inherit from . - /// The to configure for WinForms support. - /// An optional action to further configure the WinForms context. May be . - /// The same instance for chaining, or if is . - public static IHostBuilder? ConfigureWinForms(this IHostBuilder hostBuilder, Action? configureAction = null) - where TView : Form - => - hostBuilder? - .ConfigureWinForms(configureAction)? - .ConfigureServices((_, serviceCollection) => + if (hostBuilder is null) { - serviceCollection.AddSingleton(); - - // Check if it also implements IWinFormsShell so we can register it as this - var viewType = typeof(TView); - var shellInterfaceType = typeof(IWinFormsShell); - if (shellInterfaceType.IsAssignableFrom(viewType)) - { - serviceCollection.AddSingleton(shellInterfaceType, serviceProvider => serviceProvider.GetRequiredService()); - } - }); + throw new ArgumentNullException(nameof(hostBuilder)); + } - /// - /// Configures WinForms support for the application and registers the specified main form type as a singleton - /// service. - /// - /// If also implements , it is - /// registered as a singleton service for that interface as well. This enables dependency injection of the main form - /// and shell interface throughout the application. - /// The type of the main form to register. Must inherit from . - /// The host application builder to configure. - /// An optional delegate to configure additional WinForms services or settings. Can be . - /// The same instance for chaining. - /// Thrown if is . - public static IHostApplicationBuilder ConfigureWinForms(this IHostApplicationBuilder hostBuilder, Action? configureAction = null) - where TView : Form - { - if (hostBuilder == null) - { - throw new ArgumentNullException(nameof(hostBuilder)); - } + _ = hostBuilder.ConfigureWinForms(configureAction); - hostBuilder.ConfigureWinForms(configureAction); + _ = hostBuilder.Services.AddSingleton(); - hostBuilder.Services.AddSingleton(); + // Check if it also implements IWinFormsShell so we can register it as this + var viewType = typeof(TView); + var shellInterfaceType = typeof(IWinFormsShell); + if (shellInterfaceType.IsAssignableFrom(viewType)) + { + _ = hostBuilder.Services.AddSingleton(shellInterfaceType, serviceProvider => serviceProvider.GetRequiredService()); + } - // Check if it also implements IWinFormsShell so we can register it as this - var viewType = typeof(TView); - var shellInterfaceType = typeof(IWinFormsShell); - if (shellInterfaceType.IsAssignableFrom(viewType)) - { - hostBuilder.Services.AddSingleton(shellInterfaceType, serviceProvider => serviceProvider.GetRequiredService()); + return hostBuilder; } - return hostBuilder; + /// Configures the WinForms shell for the application using the specified shell form type. + /// The type of the main shell form to use for the application. Must inherit from Form and implement IWinFormsShell. + /// The same IHostApplicationBuilder instance for chaining further configuration. + public IHostApplicationBuilder ConfigureWinFormsShell() + where TShell : Form, IWinFormsShell + => hostBuilder.ConfigureWinForms(); } - /// - /// Configures the specified host builder to use a WinForms shell of the given type as the application's main - /// window. - /// - /// The type of the WinForms shell form to use as the application's main window. Must inherit from and implement . - /// The host builder to configure for WinForms shell integration. Cannot be null. - /// The same instance for chaining, or if is null. - public static IHostBuilder? ConfigureWinFormsShell(this IHostBuilder hostBuilder) - where TShell : Form, IWinFormsShell - => hostBuilder?.ConfigureWinForms(); - - /// - /// Configures the WinForms shell for the application using the specified shell form type. - /// - /// The type of the main shell form to use for the application. Must inherit from Form and implement IWinFormsShell. - /// The host application builder to configure. - /// The same IHostApplicationBuilder instance for chaining further configuration. - public static IHostApplicationBuilder ConfigureWinFormsShell(this IHostApplicationBuilder hostBuilder) - where TShell : Form, IWinFormsShell - => hostBuilder.ConfigureWinForms(); - - /// - /// Attempts to retrieve an existing Windows Forms context from the specified property dictionary. - /// - /// If a Windows Forms context does not exist in the dictionary, this method creates a new - /// context, adds it to the dictionary, and returns it in the out parameter. - /// The dictionary containing property values, which may include a Windows Forms context entry. - /// When this method returns, contains the retrieved Windows Forms context if found; otherwise, a new context - /// instance. - /// true if an existing Windows Forms context was found in the dictionary; otherwise, false. - private static bool TryRetrieveWinFormsContext(this IDictionary properties, out IWinFormsContext winFormsContext) + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHostBuilder hostBuilder) { - if (properties.TryGetValue(WinFormsContextKey, out var winFormsContextAsObject)) - { - winFormsContext = (IWinFormsContext)winFormsContextAsObject; - return true; - } + /// Configures the host to use Windows Forms lifetime management, enabling the application to start and stop in coordination with the WinForms message loop. + /// This method links the application's lifetime to the Windows Forms message loop, so the host + /// will start when the message loop begins and stop when it ends. Use this method when building WinForms + /// applications that require integration with the generic host infrastructure. + /// The same instance of for chaining further configuration, or null if is null. + public IHostBuilder? UseWinFormsLifetime() => + hostBuilder?.ConfigureServices((context, services) => + { + _ = TryRetrieveWinFormsContext(hostBuilder.Properties, out var winFormsContext); + winFormsContext.IsLifetimeLinked = true; + }); - winFormsContext = new WinFormsContext(); - properties[WinFormsContextKey] = winFormsContext; - return false; + /// Configures WinForms support for the specified host builder, enabling integration of Windows Forms message loop and services into the application's hosting environment. + /// This method registers the necessary services to run a Windows Forms message loop within a + /// generic host. Call this method before building the host to ensure WinForms integration is available throughout + /// the application's lifetime. + /// An optional delegate to further configure the WinForms context. If null, no additional configuration is + /// performed. + /// The same instance of with WinForms services configured, or null if is null. + public IHostBuilder? ConfigureWinForms(Action? configureAction = null) => + hostBuilder?.ConfigureServices((context, serviceCollection) => + { + if (!TryRetrieveWinFormsContext(hostBuilder.Properties, out var winFormsContext)) + { + _ = serviceCollection + .AddSingleton(winFormsContext) + .AddSingleton(serviceProvider => new WinFormsThread(serviceProvider)) + .AddHostedService(); + } + + configureAction?.Invoke(winFormsContext); + }); + + /// Configures WinForms support for the host builder and registers the specified main view form as a singleton service. + /// If also implements , it is + /// registered as a singleton service for that interface as well. This method enables dependency injection and + /// configuration for WinForms applications using the generic host. + /// The type of the main WinForms form to register. Must inherit from . + /// An optional action to further configure the WinForms context. May be . + /// The same instance for chaining, or if is . + public IHostBuilder? ConfigureWinForms(Action? configureAction = null) + where TView : Form + => + hostBuilder? + .ConfigureWinForms(configureAction)? + .ConfigureServices((context, serviceCollection) => + { + _ = serviceCollection.AddSingleton(); + + // Check if it also implements IWinFormsShell so we can register it as this + var viewType = typeof(TView); + var shellInterfaceType = typeof(IWinFormsShell); + if (!shellInterfaceType.IsAssignableFrom(viewType)) + { + return; + } + + _ = serviceCollection.AddSingleton(shellInterfaceType, serviceProvider => serviceProvider.GetRequiredService()); + }); + + /// Configures the specified host builder to use a WinForms shell of the given type as the application's main window. + /// The type of the WinForms shell form to use as the application's main window. Must inherit from and implement . + /// The same instance for chaining, or if is null. + public IHostBuilder? ConfigureWinFormsShell() + where TShell : Form, IWinFormsShell + => hostBuilder?.ConfigureWinForms(); } } diff --git a/src/Extensions.Hosting.WinForms/IWinFormsContext.cs b/src/Extensions.Hosting.WinForms/IWinFormsContext.cs index 7af08e4..a759abb 100644 --- a/src/Extensions.Hosting.WinForms/IWinFormsContext.cs +++ b/src/Extensions.Hosting.WinForms/IWinFormsContext.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Windows.Threading; @@ -7,24 +7,17 @@ namespace ReactiveMarbles.Extensions.Hosting.WinForms; -/// -/// Defines a context for Windows Forms operations, providing properties for managing visual styles and dispatching -/// actions to forms. -/// +/// Defines a context for Windows Forms operations, providing properties for managing visual styles and dispatching actions to forms. /// Implementations of this interface enable configuration of UI behavior specific to Windows Forms /// applications, such as enabling visual styles and specifying a dispatcher for thread-safe operations. public interface IWinFormsContext : IUiContext { - /// - /// Gets or sets a value indicating whether visual styles are enabled for the application. - /// + /// Gets or sets a value indicating whether visual styles are enabled for the application. /// When visual styles are enabled, controls are rendered with the current Windows theme, /// providing a modern appearance. Disabling visual styles may cause controls to appear with classic styling. This /// property should typically be set before creating any UI elements. bool EnableVisualStyles { get; set; } - /// - /// Gets or sets the dispatcher used to manage the execution of operations on a specific thread or context. - /// + /// Gets or sets the dispatcher used to manage the execution of operations on a specific thread or context. Dispatcher? Dispatcher { get; set; } } diff --git a/src/Extensions.Hosting.WinForms/IWinFormsService.cs b/src/Extensions.Hosting.WinForms/IWinFormsService.cs index d1ffc4b..741291c 100644 --- a/src/Extensions.Hosting.WinForms/IWinFormsService.cs +++ b/src/Extensions.Hosting.WinForms/IWinFormsService.cs @@ -1,16 +1,12 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. namespace ReactiveMarbles.Extensions.Hosting.WinForms; -/// -/// Defines a service that can be initialized from the Windows Forms UI thread. -/// +/// Defines a service that can be initialized from the Windows Forms UI thread. public interface IWinFormsService { - /// - /// Initializes the component and prepares it for use. - /// + /// Initializes the component and prepares it for use. void Initialize(); } diff --git a/src/Extensions.Hosting.WinForms/IWinFormsShell.cs b/src/Extensions.Hosting.WinForms/IWinFormsShell.cs index ca59b33..20c1963 100644 --- a/src/Extensions.Hosting.WinForms/IWinFormsShell.cs +++ b/src/Extensions.Hosting.WinForms/IWinFormsShell.cs @@ -1,13 +1,13 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using System.ComponentModel; + namespace ReactiveMarbles.Extensions.Hosting.WinForms; -/// -/// Represents a shell interface for hosting and managing Windows Forms-based user interfaces within an application. -/// +/// Represents a shell interface for hosting and managing Windows Forms-based user interfaces within an application. /// Implement this interface to provide integration points for Windows Forms UI components, such as main /// windows, dialogs, or tool windows, within a host application. The specific responsibilities and capabilities of the /// shell are defined by the implementing class. -public interface IWinFormsShell; +public interface IWinFormsShell : IComponent; diff --git a/src/Extensions.Hosting.WinForms/Internals/MultiShellContext.cs b/src/Extensions.Hosting.WinForms/Internals/MultiShellContext.cs index 02d3c31..f20d33f 100644 --- a/src/Extensions.Hosting.WinForms/Internals/MultiShellContext.cs +++ b/src/Extensions.Hosting.WinForms/Internals/MultiShellContext.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Threading; @@ -7,21 +7,16 @@ namespace ReactiveMarbles.Extensions.Hosting.WinForms.Internals; -/// -/// Provides an application context that manages the lifetime of multiple top-level forms, ensuring the application -/// exits when all forms are closed. -/// +/// Provides an application context that manages the lifetime of multiple top-level forms, ensuring the application exits when all forms are closed. /// Use this context to run a Windows Forms application with multiple main forms. The application will /// remain running until all specified forms are closed, at which point the message loop will exit /// automatically. -internal class MultiShellContext : ApplicationContext +internal sealed class MultiShellContext : ApplicationContext { + /// Stores the open forms value. private int _openForms; - /// - /// Initializes a new instance of the class with the specified collection of forms to be managed. - /// as part of the application context. - /// + /// Initializes a new instance of the class with the specified collection of forms to be managed. as part of the application context. /// Each form provided is immediately shown and monitored for closure. The context remains active /// until all managed forms are closed. This constructor is useful for applications that require multiple main /// windows to be open simultaneously. @@ -37,9 +32,7 @@ public MultiShellContext(params Form[] forms) } } - /// - /// Handles the FormClosed event for a form and performs application shutdown if all tracked forms have been closed. - /// + /// Handles the FormClosed event for a form and performs application shutdown if all tracked forms have been closed. /// This method is intended to be used as an event handler for form closure events in /// applications that track multiple startup forms. When the last tracked form is closed, the application thread is /// terminated. @@ -48,9 +41,11 @@ public MultiShellContext(params Form[] forms) private void OnFormClosed(object? s, FormClosedEventArgs args) { // When we have closed the last of the "starting" forms, end the program. - if (Interlocked.Decrement(ref _openForms) == 0) + if (Interlocked.Decrement(ref _openForms) != 0) { - ExitThread(); + return; } + + ExitThread(); } } diff --git a/src/Extensions.Hosting.WinForms/Internals/WinFormsContext.cs b/src/Extensions.Hosting.WinForms/Internals/WinFormsContext.cs index 00326d6..64c2c50 100644 --- a/src/Extensions.Hosting.WinForms/Internals/WinFormsContext.cs +++ b/src/Extensions.Hosting.WinForms/Internals/WinFormsContext.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Windows.Threading; @@ -7,9 +7,7 @@ namespace ReactiveMarbles.Extensions.Hosting.WinForms.Internals; -/// -/// Provides a Windows Forms-specific UI context for managing application-wide settings and services. -/// +/// Provides a Windows Forms-specific UI context for managing application-wide settings and services. /// Use this class to configure and interact with Windows Forms application features, such as enabling /// visual styles and accessing the UI dispatcher. This context is typically used to coordinate UI-related operations in /// a Windows Forms environment. diff --git a/src/Extensions.Hosting.WinForms/Internals/WinFormsThread.cs b/src/Extensions.Hosting.WinForms/Internals/WinFormsThread.cs index 1fee2b1..6a65aa0 100644 --- a/src/Extensions.Hosting.WinForms/Internals/WinFormsThread.cs +++ b/src/Extensions.Hosting.WinForms/Internals/WinFormsThread.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -12,9 +12,7 @@ namespace ReactiveMarbles.Extensions.Hosting.WinForms.Internals; -/// -/// Provides a dedicated UI thread for running Windows Forms applications with dependency injection support. -/// +/// Provides a dedicated UI thread for running Windows Forms applications with dependency injection support. /// This class manages the lifecycle of a Windows Forms message loop on a separate thread, allowing for /// integration with dependency injection and service-based architectures. It is typically used in scenarios where /// Windows Forms UI components need to be hosted or controlled from a background or non-main thread context. The thread @@ -45,8 +43,7 @@ protected override void PreUiThreadStart() protected override void UiThreadStart() { // Use the provided IWinFormsService - var winFormServices = ServiceProvider.GetServices(); - foreach (var winFormService in winFormServices) + foreach (var winFormService in ServiceProvider.GetServices()) { winFormService.Initialize(); } @@ -57,21 +54,27 @@ protected override void UiThreadStart() switch (shells.Length) { case 1: + { Application.Run(shells[0]); break; + } + case 0: + { Application.Run(); break; + } + default: + { var multiShellContext = new MultiShellContext(shells); Application.Run(multiShellContext); break; + } } } - /// - /// Handles the application exit event to perform necessary cleanup operations before the application shuts down. - /// + /// Handles the application exit event to perform necessary cleanup operations before the application shuts down. /// The source of the event, typically the application object. /// An object that contains the event data associated with the application exit event. private void OnApplicationExit(object? sender, EventArgs eventArgs) diff --git a/src/Extensions.Hosting.WinForms/WinFormsHostedService.cs b/src/Extensions.Hosting.WinForms/WinFormsHostedService.cs index 8f4744c..9453689 100644 --- a/src/Extensions.Hosting.WinForms/WinFormsHostedService.cs +++ b/src/Extensions.Hosting.WinForms/WinFormsHostedService.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -13,10 +13,7 @@ namespace ReactiveMarbles.Extensions.Hosting.WinForms; -/// -/// Provides an implementation of IHostedService that manages the lifecycle of a WinForms application within a generic -/// host environment. -/// +/// Provides an implementation of IHostedService that manages the lifecycle of a WinForms application within a generic host environment. /// This service enables integration of a WinForms application into a .NET generic host, allowing the /// application to participate in the host's startup and shutdown processes. It is typically used to coordinate graceful /// startup and shutdown of WinForms UI within background services or desktop applications that leverage dependency @@ -26,6 +23,14 @@ namespace ReactiveMarbles.Extensions.Hosting.WinForms; /// The context that provides access to the WinForms application's state and dispatcher. public class WinFormsHostedService(ILogger logger, WinFormsThread winFormsThread, IWinFormsContext winFormsContext) : IHostedService { + /// Logs when the WinForms application is stopping. + private static readonly Action LogStoppingWinForms = + LoggerMessage.Define(LogLevel.Debug, new EventId(1, nameof(LogStoppingWinForms)), "Stopping WinForms application."); + + /// Logs when a form cannot be cleaned up. + private static readonly Action LogFormCleanupFailed = + LoggerMessage.Define(LogLevel.Warning, new EventId(2, nameof(LogFormCleanupFailed)), "Couldn't cleanup a Form"); + /// public Task StartAsync(CancellationToken cancellationToken) { @@ -41,27 +46,29 @@ public Task StartAsync(CancellationToken cancellationToken) /// public async Task StopAsync(CancellationToken cancellationToken) { - if (winFormsContext.IsRunning) + if (!winFormsContext.IsRunning) + { + return; + } + + LogStoppingWinForms(logger, null); + await winFormsContext.Dispatcher!.InvokeAsync(() => { - logger.LogDebug("Stopping WinForms application."); - await winFormsContext.Dispatcher!.InvokeAsync(() => + // Graceful close, otherwise finalizes try to dispose forms. + foreach (var form in Application.OpenForms.Cast
().ToList()) { - // Graceful close, otherwise finalizes try to dispose forms. - foreach (var form in Application.OpenForms.Cast().ToList()) + try + { + form.Close(); + form.Dispose(); + } + catch (Exception ex) { - try - { - form.Close(); - form.Dispose(); - } - catch (Exception ex) - { - logger.LogWarning(ex, "Couldn't cleanup a Form"); - } + LogFormCleanupFailed(logger, ex); } + } - Application.ExitThread(); - }); - } + Application.ExitThread(); + }); } } diff --git a/src/Extensions.Hosting.WinUI/Extensions.Hosting.WinUI.csproj b/src/Extensions.Hosting.WinUI/Extensions.Hosting.WinUI.csproj index 3e1ad71..16f1fb4 100644 --- a/src/Extensions.Hosting.WinUI/Extensions.Hosting.WinUI.csproj +++ b/src/Extensions.Hosting.WinUI/Extensions.Hosting.WinUI.csproj @@ -1,7 +1,7 @@ - + - net8.0-windows10.0.19041;net9.0-windows10.0.19041;net10.0-windows10.0.19041 + net8.0-windows10.0.19041;net9.0-windows10.0.19041;net10.0-windows10.0.19041;net11.0-windows10.0.19041 10.0.19041.0 win-x64;win-x86;win-arm64 This extension adds WinUI support to generic host applications. With this you can enhance your application with a UI, and use all the services provided by the generic host like DI, logging etc. @@ -11,7 +11,7 @@ - + diff --git a/src/Extensions.Hosting.WinUI/HostBuilderWinUIExtensions.cs b/src/Extensions.Hosting.WinUI/HostBuilderWinUIExtensions.cs index 4d60420..8c4162f 100644 --- a/src/Extensions.Hosting.WinUI/HostBuilderWinUIExtensions.cs +++ b/src/Extensions.Hosting.WinUI/HostBuilderWinUIExtensions.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -11,138 +11,142 @@ namespace ReactiveMarbles.Extensions.Hosting.WinUI; -/// -/// Provides extension methods for configuring WinUI applications with generic host builders. -/// +/// Provides extension methods for configuring WinUI applications with generic host builders. /// These extensions enable integration of WinUI application and window types into the .NET Generic Host /// infrastructure, allowing for dependency injection, service registration, and host-managed application lifetimes. Use /// these methods to set up WinUI applications in a host-based environment, such as when building modern desktop /// applications with dependency injection and background services. public static class HostBuilderWinUIExtensions { + /// Stores the win uicontext key value. private const string WinUIContextKey = nameof(WinUIContext); - /// - /// Configures WinUI support for the specified application and main window types within the host builder. - /// - /// This method registers the WinUI application and main window types with the dependency - /// injection container and links the application lifetime to the host. Call this method before building the host to - /// ensure proper WinUI integration. - /// The type of the WinUI application to configure. Must inherit from . - /// The type of the main window for the application. Must inherit from . - /// The host builder to configure with WinUI services. Cannot be null. - /// The same instance for chaining, or if is null. - /// Thrown if does not inherit from . - public static IHostBuilder? ConfigureWinUI(this IHostBuilder hostBuilder) - where TApp : Application - where TAppWindow : Window + /// Attempts to retrieve an existing WinUI context from the specified property dictionary. + /// If the WinUI context does not exist in the dictionary, this method creates a new instance, + /// assigns it to the out parameter, and adds it to the dictionary for future retrieval. + /// The property dictionary used to store the context. + /// When this method returns, contains the WinUI context retrieved from the dictionary if found; otherwise, a new + /// WinUI context instance. + /// true if an existing WinUI context was found in the dictionary; otherwise, false. + private static bool TryRetrieveWinUIContext(IDictionary properties, out IWinUIContext winUIContext) { - var appType = typeof(TApp); - - hostBuilder?.ConfigureServices((_, serviceCollection) => - { - if (!TryRetrieveWinUIContext(hostBuilder.Properties, out var winUIContext)) - { - serviceCollection.AddSingleton(winUIContext); - serviceCollection.AddSingleton(serviceProvider => new WinUIThread(serviceProvider)); - serviceCollection.AddHostedService(); - } - - winUIContext.AppWindowType = typeof(TAppWindow); - winUIContext.IsLifetimeLinked = true; - }); - - if (appType != null) + if (properties.TryGetValue(WinUIContextKey, out var winUIContextAsObject)) { - var baseApplicationType = typeof(Application); - if (!baseApplicationType.IsAssignableFrom(appType)) - { - throw new ArgumentException("The registered Application type inherit System.Windows.Application", nameof(TApp)); - } - - hostBuilder?.ConfigureServices((_, serviceCollection) => - { - serviceCollection.AddSingleton(); - - if (appType != baseApplicationType) - { - serviceCollection.AddSingleton(services => services.GetRequiredService()); - } - }); + winUIContext = (IWinUIContext)winUIContextAsObject; + return true; } - return hostBuilder; + winUIContext = new WinUIContext(); + properties[WinUIContextKey] = winUIContext; + return false; } - /// - /// Configures WinUI integration for the application by registering the specified application and window types with - /// the host builder. - /// - /// This method registers the WinUI application and window types as singletons in the dependency - /// injection container and sets up the necessary WinUI hosting services. Call this method before building the host - /// to ensure proper WinUI initialization. - /// The type of the WinUI application to register. Must inherit from . - /// The type of the main window to use for the application. Must inherit from . - /// The to configure for WinUI support. - /// The same instance for chaining further configuration. - /// Thrown if is . - /// Thrown if does not inherit from . - public static IHostApplicationBuilder ConfigureWinUI(this IHostApplicationBuilder hostBuilder) + /// Registers the core WinUI hosting services. + /// The service collection to register services into. + /// The WinUI context instance to register. + private static void RegisterWinUIHostingServices(IServiceCollection services, IWinUIContext winUIContext) => + _ = services + .AddSingleton(winUIContext) + .AddSingleton(serviceProvider => new WinUIThread(serviceProvider)) + .AddHostedService(); + + /// Registers the WinUI application type. + /// The WinUI application type to register. + /// The service collection to register services into. + /// Thrown if the application type does not derive from . + private static void RegisterWinUIApplication(IServiceCollection services) where TApp : Application - where TAppWindow : Window { - if (hostBuilder == null) - { - throw new ArgumentNullException(nameof(hostBuilder)); - } - var appType = typeof(TApp); - - if (!TryRetrieveWinUIContext(hostBuilder.Properties, out var winUIContext)) - { - hostBuilder.Services.AddSingleton(winUIContext); - hostBuilder.Services.AddSingleton(serviceProvider => new WinUIThread(serviceProvider)); - hostBuilder.Services.AddHostedService(); - } - - winUIContext.AppWindowType = typeof(TAppWindow); - winUIContext.IsLifetimeLinked = true; - var baseApplicationType = typeof(Application); if (!baseApplicationType.IsAssignableFrom(appType)) { throw new ArgumentException("The registered Application type inherit System.Windows.Application", nameof(TApp)); } - hostBuilder.Services.AddSingleton(); - if (appType != baseApplicationType) + _ = services.AddSingleton(); + if (appType == baseApplicationType) { - hostBuilder.Services.AddSingleton(services => services.GetRequiredService()); + return; } - return hostBuilder; + _ = services.AddSingleton(services => services.GetRequiredService()); } - /// - /// Attempts to retrieve an existing WinUI context from the specified property dictionary. - /// - /// If the WinUI context does not exist in the dictionary, this method creates a new instance, - /// assigns it to the out parameter, and adds it to the dictionary for future retrieval. - /// The dictionary containing property values, which may include a WinUI context entry. - /// When this method returns, contains the WinUI context retrieved from the dictionary if found; otherwise, a new - /// WinUI context instance. - /// true if an existing WinUI context was found in the dictionary; otherwise, false. - private static bool TryRetrieveWinUIContext(this IDictionary properties, out IWinUIContext winUIContext) + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHostApplicationBuilder hostBuilder) { - if (properties.TryGetValue(WinUIContextKey, out var winUIContextAsObject)) + /// Configures WinUI integration for the application by registering the specified application and window types with the host builder. + /// This method registers the WinUI application and window types as singletons in the dependency + /// injection container and sets up the necessary WinUI hosting services. Call this method before building the host + /// to ensure proper WinUI initialization. + /// The type of the WinUI application to register. Must inherit from . + /// The type of the main window to use for the application. Must inherit from . + /// The same instance for chaining further configuration. + /// Thrown if is . + /// Thrown if does not inherit from . + public IHostApplicationBuilder ConfigureWinUI() + where TApp : Application + where TAppWindow : Window { - winUIContext = (IWinUIContext)winUIContextAsObject; - return true; + if (hostBuilder is null) + { + throw new ArgumentNullException(nameof(hostBuilder)); + } + + var appType = typeof(TApp); + + if (!TryRetrieveWinUIContext(hostBuilder.Properties, out var winUIContext)) + { + RegisterWinUIHostingServices(hostBuilder.Services, winUIContext); + } + + winUIContext.AppWindowType = typeof(TAppWindow); + winUIContext.IsLifetimeLinked = true; + + RegisterWinUIApplication(hostBuilder.Services); + + return hostBuilder; } + } - winUIContext = new WinUIContext(); - properties[WinUIContextKey] = winUIContext; - return false; + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHostBuilder hostBuilder) + { + /// Configures WinUI support for the specified application and main window types within the host builder. + /// This method registers the WinUI application and main window types with the dependency + /// injection container and links the application lifetime to the host. Call this method before building the host to + /// ensure proper WinUI integration. + /// The type of the WinUI application to configure. Must inherit from . + /// The type of the main window for the application. Must inherit from . + /// The same instance for chaining, or if is null. + /// Thrown if does not inherit from . + public IHostBuilder? ConfigureWinUI() + where TApp : Application + where TAppWindow : Window + { + if (hostBuilder is null) + { + return null; + } + + _ = hostBuilder.ConfigureServices((context, serviceCollection) => + { + if (!TryRetrieveWinUIContext(hostBuilder.Properties, out var winUIContext)) + { + RegisterWinUIHostingServices(serviceCollection, winUIContext); + } + + winUIContext.AppWindowType = typeof(TAppWindow); + winUIContext.IsLifetimeLinked = true; + }); + + _ = hostBuilder.ConfigureServices((context, serviceCollection) => RegisterWinUIApplication(serviceCollection)); + + return hostBuilder; + } } } diff --git a/src/Extensions.Hosting.WinUI/IWinUIContext.cs b/src/Extensions.Hosting.WinUI/IWinUIContext.cs index 7f25592..3eb8166 100644 --- a/src/Extensions.Hosting.WinUI/IWinUIContext.cs +++ b/src/Extensions.Hosting.WinUI/IWinUIContext.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -9,38 +9,27 @@ namespace ReactiveMarbles.Extensions.Hosting.WinUI; -/// -/// Defines a context for WinUI-based user interface operations, providing access to the application window, dispatcher, -/// and related WinUI components. -/// +/// Defines a context for WinUI-based user interface operations, providing access to the application window, dispatcher, and related WinUI components. /// Implementations of this interface enable interaction with WinUI elements and threading /// infrastructure, facilitating UI operations within a WinUI application environment. This context is typically used to /// coordinate UI actions, access the main application window, and dispatch work to the UI thread. public interface IWinUIContext : IUiContext { - /// - /// Gets or sets the application window associated with the current context. - /// + /// Gets or sets the application window associated with the current context. Window? AppWindow { get; set; } - /// - /// Gets or sets the type of the application window to be created or managed. - /// + /// Gets or sets the type of the application window to be created or managed. /// Set this property to specify a custom window type for the application. If the value is null, /// the default window type will be used. Changing this property may affect how the application window is /// instantiated or displayed. Type? AppWindowType { get; set; } - /// - /// Gets or sets the dispatcher queue associated with the object. - /// + /// Gets or sets the dispatcher queue associated with the object. /// The dispatcher queue is typically used to schedule work to run on a specific thread, such as /// the UI thread in a Windows application. Assigning a dispatcher queue enables thread-safe operations and /// coordination with the application's message loop. DispatcherQueue? Dispatcher { get; set; } - /// - /// Gets or sets the current Windows UI Application instance associated with the host. - /// + /// Gets or sets the current Windows UI Application instance associated with the host. Application? WinUIApplication { get; set; } } diff --git a/src/Extensions.Hosting.WinUI/IWinUIService.cs b/src/Extensions.Hosting.WinUI/IWinUIService.cs index eac77e4..eeb1690 100644 --- a/src/Extensions.Hosting.WinUI/IWinUIService.cs +++ b/src/Extensions.Hosting.WinUI/IWinUIService.cs @@ -1,19 +1,15 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.UI.Xaml; namespace ReactiveMarbles.Extensions.Hosting.WinUI; -/// -/// Defines a service for initializing WinUI components from the UI thread. -/// +/// Defines a service for initializing WinUI components from the UI thread. public interface IWinUIService { - /// - /// Initializes the specified application instance, preparing it for use. - /// + /// Initializes the specified application instance, preparing it for use. /// The application instance to initialize. Cannot be null. void Initialize(Application application); } diff --git a/src/Extensions.Hosting.WinUI/Internals/WinUIContext.cs b/src/Extensions.Hosting.WinUI/Internals/WinUIContext.cs index 3ad3eeb..3315e94 100644 --- a/src/Extensions.Hosting.WinUI/Internals/WinUIContext.cs +++ b/src/Extensions.Hosting.WinUI/Internals/WinUIContext.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -8,10 +8,7 @@ namespace ReactiveMarbles.Extensions.Hosting.WinUI.Internals; -/// -/// Provides context information and access to core WinUI application components, including the main window, dispatcher, -/// and application instance. -/// +/// Provides context information and access to core WinUI application components, including the main window, dispatcher, and application instance. /// Use this class to interact with and manage the state of a WinUI application's main elements. It /// exposes properties for accessing the application window, dispatcher queue, and application instance, as well as /// flags indicating the application's running state and lifetime linkage. This context is typically used to coordinate diff --git a/src/Extensions.Hosting.WinUI/Internals/WinUIThread.cs b/src/Extensions.Hosting.WinUI/Internals/WinUIThread.cs index b2282ff..475b746 100644 --- a/src/Extensions.Hosting.WinUI/Internals/WinUIThread.cs +++ b/src/Extensions.Hosting.WinUI/Internals/WinUIThread.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -12,10 +12,7 @@ namespace ReactiveMarbles.Extensions.Hosting.WinUI.Internals; -/// -/// Provides a UI thread implementation for WinUI applications, enabling initialization and management of WinUI-specific -/// services and application context. -/// +/// Provides a UI thread implementation for WinUI applications, enabling initialization and management of WinUI-specific services and application context. /// This class is intended for scenarios where a dedicated UI thread is required to host a WinUI /// application. It ensures that the necessary WinUI services and synchronization context are initialized on the correct /// thread. The WinUI application and its main window are created and activated as part of the thread startup @@ -40,7 +37,7 @@ protected override void UiThreadStart() // Use the provided IWinUIService var winUIServices = ServiceProvider.GetServices(); - if (winUIServices != null) + if (winUIServices is not null) { foreach (var winUIService in winUIServices) { diff --git a/src/Extensions.Hosting.WinUI/WinUIHostedService.cs b/src/Extensions.Hosting.WinUI/WinUIHostedService.cs index 6e3d1a5..b4ed08e 100644 --- a/src/Extensions.Hosting.WinUI/WinUIHostedService.cs +++ b/src/Extensions.Hosting.WinUI/WinUIHostedService.cs @@ -1,7 +1,8 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; @@ -10,10 +11,7 @@ namespace ReactiveMarbles.Extensions.Hosting.WinUI; -/// -/// Provides an implementation of that manages the lifecycle of a WinUI application within -/// a generic host environment. -/// +/// Provides an implementation of that manages the lifecycle of a WinUI application within a generic host environment. /// This service enables integration of a WinUI application with the .NET Generic Host, allowing the /// application to participate in the host's startup and shutdown processes. It is typically registered as a singleton /// in the application's dependency injection container. @@ -22,6 +20,10 @@ namespace ReactiveMarbles.Extensions.Hosting.WinUI; /// The context that provides access to the WinUI application's dispatcher and lifecycle management. public class WinUIHostedService(ILogger logger, WinUIThread winUIThread, IWinUIContext winUIContext) : IHostedService { + /// Logs when the WinUI application is stopping. + private static readonly Action LogStoppingWinUI = + LoggerMessage.Define(LogLevel.Debug, new EventId(1, nameof(LogStoppingWinUI)), "Stopping WinUI due to application exit."); + /// public Task StartAsync(CancellationToken cancellationToken) { @@ -38,18 +40,20 @@ public Task StartAsync(CancellationToken cancellationToken) /// public async Task StopAsync(CancellationToken cancellationToken) { - if (winUIContext.IsRunning) + if (!winUIContext.IsRunning) { - logger.LogDebug("Stopping WinUI due to application exit."); - - // Stop application - var completion = new TaskCompletionSource(); - winUIContext.Dispatcher?.TryEnqueue(() => - { - winUIContext.WinUIApplication?.Exit(); - completion.SetResult(); - }); - await completion.Task; + return; } + + LogStoppingWinUI(logger, null); + + // Stop application + var completion = new TaskCompletionSource(); + _ = winUIContext.Dispatcher?.TryEnqueue(() => + { + winUIContext.WinUIApplication?.Exit(); + completion.SetResult(); + }); + await completion.Task; } } diff --git a/src/Extensions.Hosting.Wpf/Extensions.Hosting.Wpf.csproj b/src/Extensions.Hosting.Wpf/Extensions.Hosting.Wpf.csproj index 11c4336..f2b3a0c 100644 --- a/src/Extensions.Hosting.Wpf/Extensions.Hosting.Wpf.csproj +++ b/src/Extensions.Hosting.Wpf/Extensions.Hosting.Wpf.csproj @@ -1,7 +1,7 @@ - + - net462;net472;net481;net8.0-windows;net9.0-windows;net10.0-windows + net462;net472;net48;net481;net8.0-windows;net9.0-windows;net10.0-windows;net11.0-windows true This extension adds WPF support to generic host applications. With this you can enhance your application with a UI, and use all the services provided by the generic host like DI, logging etc. CP.Extensions.Hosting.Wpf diff --git a/src/Extensions.Hosting.Wpf/HostBuilderWpfExtensions.cs b/src/Extensions.Hosting.Wpf/HostBuilderWpfExtensions.cs index afc7128..c9ff4c9 100644 --- a/src/Extensions.Hosting.Wpf/HostBuilderWpfExtensions.cs +++ b/src/Extensions.Hosting.Wpf/HostBuilderWpfExtensions.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -11,263 +11,237 @@ namespace ReactiveMarbles.Extensions.Hosting.Wpf; -/// -/// Provides extension methods for configuring and integrating WPF application lifetime and services with .NET Generic -/// Host builders. -/// +/// Provides extension methods for configuring and integrating WPF application lifetime and services with .NET Generic Host builders. /// These extensions enable seamless integration of WPF applications with the hosting infrastructure, /// allowing developers to configure WPF services, link application shutdown to host lifetime, and register WPF windows /// and application types. Methods are provided for both classic and new builder APIs. Ensure that WPF is properly /// configured before using lifetime integration methods. public static class HostBuilderWpfExtensions { + /// Stores the wpf context key value. private const string WpfContextKey = nameof(WpfContext); - /// - /// Enables WPF application lifetime integration for the host, configuring shutdown behavior according to the - /// specified mode. - /// - /// This method should be called after configuring WPF on the host builder. It links the host's - /// lifetime to the WPF application's lifetime, ensuring that the host shuts down according to the specified - /// shutdown mode. - /// The host builder to configure for WPF lifetime integration. Cannot be null. - /// The shutdown mode that determines when the WPF application will exit. The default is - /// ShutdownMode.OnLastWindowClose. - /// The same instance of for chaining further configuration. - /// Thrown if is null. - /// Thrown if WPF has not been configured on the host builder before calling this method. - public static IHostBuilder UseWpfLifetime(this IHostBuilder hostBuilder, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose) + /// Attempts to retrieve an existing IWpfContext instance from the specified properties dictionary, or creates and stores a new instance if one does not exist. + /// If no IWpfContext is present in the dictionary, a new instance is created, added to the + /// dictionary, and returned via the out parameter. Subsequent calls with the same dictionary will retrieve the same + /// instance. + /// The property dictionary used to store the context. + /// When this method returns, contains the IWpfContext instance retrieved from the dictionary if one exists; + /// otherwise, a newly created IWpfContext instance. + /// true if an IWpfContext instance was found in the dictionary; otherwise, false. + private static bool TryRetrieveWpfContext(IDictionary properties, out IWpfContext wpfContext) { - if (hostBuilder == null) + if (properties.TryGetValue(WpfContextKey, out var wpfContextAsObject)) { - throw new ArgumentNullException(nameof(hostBuilder)); + wpfContext = (IWpfContext)wpfContextAsObject; + return true; } - return hostBuilder.ConfigureServices((_, __) => - { - if (!TryRetrieveWpfContext(hostBuilder.Properties, out var wpfContext)) - { - throw new NotSupportedException("Please configure WPF first!"); - } - - wpfContext.ShutdownMode = shutdownMode; - wpfContext.IsLifetimeLinked = true; - }); + wpfContext = new WpfContext(); + properties[WpfContextKey] = wpfContext; + return false; } - /// - /// Enables WPF-specific application lifetime management for the host, configuring how the application shuts down - /// based on the specified shutdown mode. - /// - /// This method links the application's lifetime to the WPF application's lifetime, allowing the - /// host to shut down according to the specified shutdown mode. Call this method after configuring WPF services and - /// before building the host. - /// The host application builder to configure for WPF lifetime management. Cannot be null. - /// The shutdown behavior for the WPF application. Defaults to ShutdownMode.OnLastWindowClose. - /// The same instance of the host application builder, configured to use WPF lifetime management. - /// Thrown if the hostBuilder parameter is null. - /// Thrown if WPF has not been configured on the host builder before calling this method. - public static IHostApplicationBuilder UseWpfLifetime(this IHostApplicationBuilder hostBuilder, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose) + /// Registers the core WPF hosting services. + /// The service collection to register services into. + /// The WPF context instance to register. + private static void RegisterWpfHostingServices(IServiceCollection services, IWpfContext wpfContext) => + _ = services + .AddSingleton(wpfContext) + .AddSingleton(serviceProvider => new WpfThread(serviceProvider)) + .AddHostedService(); + + /// Registers a configured WPF application type or instance. + /// The service collection to register services into. + /// The builder that contains the application configuration. + /// The name of the public parameter used for exception reporting. + /// Thrown if the configured application type does not derive from . + private static void RegisterWpfApplication(IServiceCollection services, WpfBuilder wpfBuilder, string parameterName) { - if (hostBuilder == null) + if (wpfBuilder.ApplicationType is null) { - throw new ArgumentNullException(nameof(hostBuilder)); + return; } - if (!TryRetrieveWpfContext(hostBuilder.Properties, out var wpfContext)) + var baseApplicationType = typeof(Application); + if (!baseApplicationType.IsAssignableFrom(wpfBuilder.ApplicationType)) { - throw new NotSupportedException("Please configure WPF first!"); + throw new ArgumentException("The registered Application type inherit System.Windows.Application", parameterName); } - wpfContext.ShutdownMode = shutdownMode; - wpfContext.IsLifetimeLinked = true; - return hostBuilder; - } + if (wpfBuilder.Application is not null) + { + _ = services.AddSingleton(wpfBuilder.ApplicationType, wpfBuilder.Application); + } + else + { + _ = services.AddSingleton(wpfBuilder.ApplicationType); + } - /// - /// Configures WPF support for the specified host builder, enabling integration of WPF application and window types - /// into the hosting environment. - /// - /// This method registers WPF services and allows customization of the WPF application and main - /// window types through the . It should be called before building the host to - /// ensure WPF integration is properly configured. Only one WPF context is registered per host builder - /// instance. - /// The host builder to configure for WPF support. Cannot be null. - /// An optional delegate to further configure WPF-specific services and application or window types. If null, - /// default WPF configuration is applied. - /// The same instance of for chaining further configuration. - /// Thrown if is null. - /// Thrown if the application type registered via does not inherit from . - public static IHostBuilder ConfigureWpf(this IHostBuilder hostBuilder, Action? configureDelegate = null) - { - if (hostBuilder == null) + if (wpfBuilder.ApplicationType == baseApplicationType) { - throw new ArgumentNullException(nameof(hostBuilder)); + return; } - var wpfBuilder = new WpfBuilder(); - configureDelegate?.Invoke(wpfBuilder); + _ = services.AddSingleton(serviceProvider => (Application)serviceProvider.GetRequiredService(wpfBuilder.ApplicationType)); + } - hostBuilder.ConfigureServices((_, serviceCollection) => + /// Registers configured WPF shell windows. + /// The service collection to register services into. + /// The builder that contains the window configuration. + private static void RegisterWpfWindows(IServiceCollection services, WpfBuilder wpfBuilder) + { + foreach (var wpfWindowType in wpfBuilder.WindowTypes) { - if (!TryRetrieveWpfContext(hostBuilder.Properties, out var wpfContext)) + _ = services.AddSingleton(wpfWindowType); + + var shellInterfaceType = typeof(IWpfShell); + if (!shellInterfaceType.IsAssignableFrom(wpfWindowType)) { - serviceCollection.AddSingleton(wpfContext); - serviceCollection.AddSingleton(serviceProvider => new WpfThread(serviceProvider)); - serviceCollection.AddHostedService(); + continue; } - wpfBuilder.ConfigureContextAction?.Invoke(wpfContext); - }); + _ = services.AddSingleton(shellInterfaceType, serviceProvider => serviceProvider.GetRequiredService(wpfWindowType)); + } + } - if (wpfBuilder.ApplicationType != null) + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHostApplicationBuilder hostBuilder) + { + /// Enables WPF-specific application lifetime management for the host, configuring how the application shuts down based on the specified shutdown mode. + /// This method links the application's lifetime to the WPF application's lifetime, allowing the + /// host to shut down according to the specified shutdown mode. Call this method after configuring WPF services and + /// before building the host. + /// The shutdown behavior for the WPF application. Defaults to ShutdownMode.OnLastWindowClose. + /// The same instance of the host application builder, configured to use WPF lifetime management. + /// Thrown if the hostBuilder parameter is null. + /// Thrown if WPF has not been configured on the host builder before calling this method. + public IHostApplicationBuilder UseWpfLifetime(ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose) { - // Check if the registered application does inherit System.Windows.Application - var baseApplicationType = typeof(Application); - if (!baseApplicationType.IsAssignableFrom(wpfBuilder.ApplicationType)) + if (hostBuilder is null) { - throw new ArgumentException("The registered Application type inherit System.Windows.Application", nameof(configureDelegate)); + throw new ArgumentNullException(nameof(hostBuilder)); } - hostBuilder.ConfigureServices((_, serviceCollection) => + if (!TryRetrieveWpfContext(hostBuilder.Properties, out var wpfContext)) { - if (wpfBuilder.Application != null) - { - // Add existing Application - serviceCollection.AddSingleton(wpfBuilder.ApplicationType, wpfBuilder.Application); - } - else - { - serviceCollection.AddSingleton(wpfBuilder.ApplicationType); - } + throw new NotSupportedException("Please configure WPF first!"); + } - if (wpfBuilder.ApplicationType != baseApplicationType) - { - serviceCollection.AddSingleton(serviceProvider => (Application)serviceProvider.GetRequiredService(wpfBuilder.ApplicationType)); - } - }); + wpfContext.ShutdownMode = shutdownMode; + wpfContext.IsLifetimeLinked = true; + return hostBuilder; } - if (wpfBuilder.WindowTypes.Count > 0) + /// Configures WPF support for the application and registers required WPF services with the host builder. + /// This method adds the necessary services to enable WPF integration in a generic host + /// application. It should be called during application startup before building the host. If an Application type or + /// main window is specified via the configureDelegate, they are registered as singletons in the service + /// container. + /// An optional delegate to further configure WPF-specific options using an IWpfBuilder instance. If null, default + /// configuration is applied. + /// The same IHostApplicationBuilder instance for chaining further configuration. + /// Thrown if hostBuilder is null. + /// Thrown if the Application type registered via configureDelegate does not inherit from + /// System.Windows.Application. + public IHostApplicationBuilder ConfigureWpf(Action? configureDelegate = null) { - hostBuilder.ConfigureServices((_, serviceCollection) => + if (hostBuilder is null) { - foreach (var wpfWindowType in wpfBuilder.WindowTypes) - { - serviceCollection.AddSingleton(wpfWindowType); - - // Check if it also implements IWpfShell so we can register it as this - var shellInterfaceType = typeof(IWpfShell); - if (shellInterfaceType.IsAssignableFrom(wpfWindowType)) - { - serviceCollection.AddSingleton(shellInterfaceType, serviceProvider => serviceProvider.GetRequiredService(wpfWindowType)); - } - } - }); - } + throw new ArgumentNullException(nameof(hostBuilder)); + } - return hostBuilder; - } + var wpfBuilder = new WpfBuilder(); + configureDelegate?.Invoke(wpfBuilder); - /// - /// Configures WPF support for the application and registers required WPF services with the host builder. - /// - /// This method adds the necessary services to enable WPF integration in a generic host - /// application. It should be called during application startup before building the host. If an Application type or - /// main window is specified via the configureDelegate, they are registered as singletons in the service - /// container. - /// The host application builder to configure for WPF support. Cannot be null. - /// An optional delegate to further configure WPF-specific options using an IWpfBuilder instance. If null, default - /// configuration is applied. - /// The same IHostApplicationBuilder instance for chaining further configuration. - /// Thrown if hostBuilder is null. - /// Thrown if the Application type registered via configureDelegate does not inherit from - /// System.Windows.Application. - public static IHostApplicationBuilder ConfigureWpf(this IHostApplicationBuilder hostBuilder, Action? configureDelegate = null) - { - if (hostBuilder == null) - { - throw new ArgumentNullException(nameof(hostBuilder)); - } + if (!TryRetrieveWpfContext(hostBuilder.Properties, out var wpfContext)) + { + RegisterWpfHostingServices(hostBuilder.Services, wpfContext); + } - var wpfBuilder = new WpfBuilder(); - configureDelegate?.Invoke(wpfBuilder); + wpfBuilder.ConfigureContextAction?.Invoke(wpfContext); + RegisterWpfApplication(hostBuilder.Services, wpfBuilder, nameof(configureDelegate)); + RegisterWpfWindows(hostBuilder.Services, wpfBuilder); - if (!TryRetrieveWpfContext(hostBuilder.Properties, out var wpfContext)) - { - hostBuilder.Services.AddSingleton(wpfContext); - hostBuilder.Services.AddSingleton(serviceProvider => new WpfThread(serviceProvider)); - hostBuilder.Services.AddHostedService(); + return hostBuilder; } + } - wpfBuilder.ConfigureContextAction?.Invoke(wpfContext); - - if (wpfBuilder.ApplicationType != null) + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHostBuilder hostBuilder) + { + /// Enables WPF application lifetime integration for the host, configuring shutdown behavior according to the specified mode. + /// This method should be called after configuring WPF on the host builder. It links the host's + /// lifetime to the WPF application's lifetime, ensuring that the host shuts down according to the specified + /// shutdown mode. + /// The shutdown mode that determines when the WPF application will exit. The default is + /// ShutdownMode.OnLastWindowClose. + /// The same instance of for chaining further configuration. + /// Thrown if is null. + /// Thrown if WPF has not been configured on the host builder before calling this method. + public IHostBuilder UseWpfLifetime(ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose) { - // Check if the registered application does inherit System.Windows.Application - var baseApplicationType = typeof(Application); - if (!baseApplicationType.IsAssignableFrom(wpfBuilder.ApplicationType)) + if (hostBuilder is null) { - throw new ArgumentException("The registered Application type inherit System.Windows.Application", nameof(configureDelegate)); + throw new ArgumentNullException(nameof(hostBuilder)); } - if (wpfBuilder.Application != null) - { - // Add existing Application - hostBuilder.Services.AddSingleton(wpfBuilder.ApplicationType, wpfBuilder.Application); - } - else + return hostBuilder.ConfigureServices((context, services) => { - hostBuilder.Services.AddSingleton(wpfBuilder.ApplicationType); - } + if (!TryRetrieveWpfContext(hostBuilder.Properties, out var wpfContext)) + { + throw new NotSupportedException("Please configure WPF first!"); + } - if (wpfBuilder.ApplicationType != baseApplicationType) - { - hostBuilder.Services.AddSingleton(serviceProvider => (Application)serviceProvider.GetRequiredService(wpfBuilder.ApplicationType)); - } + wpfContext.ShutdownMode = shutdownMode; + wpfContext.IsLifetimeLinked = true; + }); } - if (wpfBuilder.WindowTypes.Count > 0) + /// Configures WPF support for the specified host builder, enabling integration of WPF application and window types into the hosting environment. + /// This method registers WPF services and allows customization of the WPF application and main + /// window types through the . It should be called before building the host to + /// ensure WPF integration is properly configured. Only one WPF context is registered per host builder + /// instance. + /// An optional delegate to further configure WPF-specific services and application or window types. If null, + /// default WPF configuration is applied. + /// The same instance of for chaining further configuration. + /// Thrown if is null. + /// Thrown if the application type registered via does not inherit from . + public IHostBuilder ConfigureWpf(Action? configureDelegate = null) { - foreach (var wpfWindowType in wpfBuilder.WindowTypes) + if (hostBuilder is null) { - hostBuilder.Services.AddSingleton(wpfWindowType); + throw new ArgumentNullException(nameof(hostBuilder)); + } + + var wpfBuilder = new WpfBuilder(); + configureDelegate?.Invoke(wpfBuilder); - // Check if it also implements IWpfShell so we can register it as this - var shellInterfaceType = typeof(IWpfShell); - if (shellInterfaceType.IsAssignableFrom(wpfWindowType)) + _ = hostBuilder.ConfigureServices((context, serviceCollection) => + { + if (!TryRetrieveWpfContext(hostBuilder.Properties, out var wpfContext)) { - hostBuilder.Services.AddSingleton(shellInterfaceType, serviceProvider => serviceProvider.GetRequiredService(wpfWindowType)); + RegisterWpfHostingServices(serviceCollection, wpfContext); } + + wpfBuilder.ConfigureContextAction?.Invoke(wpfContext); + }); + + if (wpfBuilder.ApplicationType is not null) + { + _ = hostBuilder.ConfigureServices((context, serviceCollection) => RegisterWpfApplication(serviceCollection, wpfBuilder, nameof(configureDelegate))); } - } - return hostBuilder; - } + if (wpfBuilder.WindowTypes.Count > 0) + { + _ = hostBuilder.ConfigureServices((context, serviceCollection) => RegisterWpfWindows(serviceCollection, wpfBuilder)); + } - /// - /// Attempts to retrieve an existing IWpfContext instance from the specified properties dictionary, or creates and - /// stores a new instance if one does not exist. - /// - /// If no IWpfContext is present in the dictionary, a new instance is created, added to the - /// dictionary, and returned via the out parameter. Subsequent calls with the same dictionary will retrieve the same - /// instance. - /// The dictionary containing property values, typically used to store and retrieve context-specific data. Cannot be - /// null. - /// When this method returns, contains the IWpfContext instance retrieved from the dictionary if one exists; - /// otherwise, a newly created IWpfContext instance. - /// true if an IWpfContext instance was found in the dictionary; otherwise, false. - private static bool TryRetrieveWpfContext(this IDictionary properties, out IWpfContext wpfContext) - { - if (properties.TryGetValue(WpfContextKey, out var wpfContextAsObject)) - { - wpfContext = (IWpfContext)wpfContextAsObject; - return true; + return hostBuilder; } - - wpfContext = new WpfContext(); - properties[WpfContextKey] = wpfContext; - return false; } } diff --git a/src/Extensions.Hosting.Wpf/IWpfBuilder.cs b/src/Extensions.Hosting.Wpf/IWpfBuilder.cs index 5237046..7cb3383 100644 --- a/src/Extensions.Hosting.Wpf/IWpfBuilder.cs +++ b/src/Extensions.Hosting.Wpf/IWpfBuilder.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -8,35 +8,24 @@ namespace ReactiveMarbles.Extensions.Hosting.Wpf; -/// -/// Defines a contract for configuring and building a WPF application, including application type, application instance, -/// window types, and context configuration actions. -/// +/// Defines a contract for configuring and building a WPF application, including application type, application instance, window types, and context configuration actions. /// Implementations of this interface allow customization of WPF application startup and context /// configuration. Use the provided properties to specify the application type or instance, register window types, and /// supply additional context configuration logic as needed. public interface IWpfBuilder { - /// - /// Gets or sets the type of the application associated with this instance. - /// + /// Gets or sets the type of the application associated with this instance. Type? ApplicationType { get; set; } - /// - /// Gets or sets the application associated with the current context. - /// + /// Gets or sets the application associated with the current context. Application? Application { get; set; } - /// - /// Gets the collection of window types supported by the application. - /// + /// Gets the collection of window types supported by the application. /// The returned list contains the types that can be instantiated as windows within the /// application. The order of types in the list may be significant depending on the application's usage. IList WindowTypes { get; } - /// - /// Gets or sets an action that configures the provided WPF context before it is used. - /// + /// Gets or sets an action that configures the provided WPF context before it is used. /// Use this property to supply custom logic for initializing or modifying the WPF context. The /// action is invoked with the context instance, allowing you to set properties or perform setup tasks as needed. If /// not set, no additional configuration is applied. diff --git a/src/Extensions.Hosting.Wpf/IWpfContext.cs b/src/Extensions.Hosting.Wpf/IWpfContext.cs index db281be..52b7997 100644 --- a/src/Extensions.Hosting.Wpf/IWpfContext.cs +++ b/src/Extensions.Hosting.Wpf/IWpfContext.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Windows; @@ -8,31 +8,22 @@ namespace ReactiveMarbles.Extensions.Hosting.Wpf; -/// -/// Defines a context for WPF-based UI operations, providing access to the application, dispatcher, and shutdown -/// behavior. -/// +/// Defines a context for WPF-based UI operations, providing access to the application, dispatcher, and shutdown behavior. /// Use this interface to interact with WPF-specific application features within a UI context /// abstraction. It exposes properties for managing application lifetime, accessing the WPF dispatcher for thread-safe /// UI operations, and referencing the current WPF application instance. public interface IWpfContext : IUiContext { - /// - /// Gets or sets the shutdown behavior for the application. - /// + /// Gets or sets the shutdown behavior for the application. /// The shutdown mode determines when the application will automatically shut down. This setting /// affects how and when the application's main window or all windows are closed, and can be used to control /// application lifetime in different scenarios. ShutdownMode ShutdownMode { get; set; } - /// - /// Gets or sets the current WPF application instance associated with the host. - /// + /// Gets or sets the current WPF application instance associated with the host. Application? WpfApplication { get; set; } - /// - /// Gets the dispatcher associated with the current object. - /// + /// Gets the dispatcher associated with the current object. /// The dispatcher can be used to execute code on the thread that the object is associated with, /// typically the UI thread in applications that use a dispatcher model. This is useful for marshaling calls from /// background threads to the main thread. diff --git a/src/Extensions.Hosting.Wpf/IWpfService.cs b/src/Extensions.Hosting.Wpf/IWpfService.cs index 59ceeec..cb559d9 100644 --- a/src/Extensions.Hosting.Wpf/IWpfService.cs +++ b/src/Extensions.Hosting.Wpf/IWpfService.cs @@ -1,19 +1,15 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Windows; namespace ReactiveMarbles.Extensions.Hosting.Wpf; -/// -/// Defines a service for initializing Windows Presentation Foundation (WPF) components from the UI thread. -/// +/// Defines a service for initializing Windows Presentation Foundation (WPF) components from the UI thread. public interface IWpfService { - /// - /// Initializes the specified application instance, preparing it for use. - /// + /// Initializes the specified application instance, preparing it for use. /// The application instance to initialize. Cannot be null. void Initialize(Application application); } diff --git a/src/Extensions.Hosting.Wpf/IWpfShell.cs b/src/Extensions.Hosting.Wpf/IWpfShell.cs index f4f19ca..8a59053 100644 --- a/src/Extensions.Hosting.Wpf/IWpfShell.cs +++ b/src/Extensions.Hosting.Wpf/IWpfShell.cs @@ -1,13 +1,12 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Windows; + namespace ReactiveMarbles.Extensions.Hosting.Wpf; -/// -/// Defines the contract for a WPF shell, which serves as the main entry point or container for a WPF application's user -/// interface. -/// +/// Defines the contract for a WPF shell, which serves as the main entry point or container for a WPF application's user interface. /// Implementations of this interface typically provide the primary window or host for WPF application /// content. The shell may manage navigation, layout, or other application-wide services. -public interface IWpfShell; +public interface IWpfShell : IInputElement; diff --git a/src/Extensions.Hosting.Wpf/Internals/WpfBuilder.cs b/src/Extensions.Hosting.Wpf/Internals/WpfBuilder.cs index 1dc5ad0..f691d03 100644 --- a/src/Extensions.Hosting.Wpf/Internals/WpfBuilder.cs +++ b/src/Extensions.Hosting.Wpf/Internals/WpfBuilder.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -8,15 +8,12 @@ namespace ReactiveMarbles.Extensions.Hosting.Wpf.Internals; -/// -/// Provides a builder for configuring and initializing WPF application components, including the application type, main -/// application instance, and associated window types. -/// +/// Provides a builder for configuring and initializing WPF application components, including the application type, main application instance, and associated window types. /// Use this class to set up the WPF application environment before launching the application. The /// builder allows customization of the application type, the application instance, and the set of window types to be /// managed. It also supports configuring the WPF context through a delegate. This class is intended for internal use /// within the WPF application infrastructure. -internal class WpfBuilder : IWpfBuilder +internal sealed class WpfBuilder : IWpfBuilder { /// public Type? ApplicationType { get; set; } diff --git a/src/Extensions.Hosting.Wpf/Internals/WpfContext.cs b/src/Extensions.Hosting.Wpf/Internals/WpfContext.cs index a0de68d..436db28 100644 --- a/src/Extensions.Hosting.Wpf/Internals/WpfContext.cs +++ b/src/Extensions.Hosting.Wpf/Internals/WpfContext.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Windows; @@ -7,14 +7,11 @@ namespace ReactiveMarbles.Extensions.Hosting.Wpf.Internals; -/// -/// Provides context and state information for a running WPF application, including access to the application instance, -/// dispatcher, and shutdown behavior. -/// +/// Provides context and state information for a running WPF application, including access to the application instance, dispatcher, and shutdown behavior. /// This class is intended for internal use to coordinate WPF application lifetime and threading. It /// exposes properties for managing shutdown mode, application lifetime linkage, and access to the application's /// dispatcher for thread-safe operations. -internal class WpfContext : IWpfContext +internal sealed class WpfContext : IWpfContext { /// public ShutdownMode ShutdownMode { get; set; } = ShutdownMode.OnLastWindowClose; diff --git a/src/Extensions.Hosting.Wpf/Internals/WpfThread.cs b/src/Extensions.Hosting.Wpf/Internals/WpfThread.cs index d04a11e..2ec6156 100644 --- a/src/Extensions.Hosting.Wpf/Internals/WpfThread.cs +++ b/src/Extensions.Hosting.Wpf/Internals/WpfThread.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -12,10 +12,7 @@ namespace ReactiveMarbles.Extensions.Hosting.Wpf.Internals; -/// -/// Provides a dedicated UI thread for running a Windows Presentation Foundation (WPF) application, managing its -/// lifecycle and synchronization context. -/// +/// Provides a dedicated UI thread for running a Windows Presentation Foundation (WPF) application, managing its lifecycle and synchronization context. /// WpfThread is intended for scenarios where a WPF application needs to be hosted on a separate thread, /// such as in multi-threaded or headless environments. It sets up the necessary synchronization context and manages the /// startup and shutdown of the WPF application. The type expects services implementing IWpfService and IWpfShell to be @@ -38,7 +35,7 @@ protected override void PreUiThreadStart() }; // Register to the WPF application exit to stop the host application - wpfApplication.Dispatcher.InvokeAsync(() => wpfApplication.Exit += (s, e) => HandleApplicationExit()); + _ = wpfApplication.Dispatcher.InvokeAsync(() => wpfApplication.Exit += (s, e) => HandleApplicationExit()); // Store the application for others to interact UiContext!.WpfApplication = wpfApplication; @@ -52,8 +49,7 @@ protected override void UiThreadStart() => UiContext.IsRunning = true; // Use the provided IWpfService - var wpfServices = ServiceProvider.GetServices(); - foreach (var wpfService in wpfServices) + foreach (var wpfService in ServiceProvider.GetServices()) { wpfService.Initialize(UiContext.WpfApplication); } @@ -64,13 +60,14 @@ protected override void UiThreadStart() => switch (shellWindows.Count) { case 1: + { if (UiContext.WpfApplication.Dispatcher.Thread.ThreadState != ThreadState.Running) { - UiContext.WpfApplication.Run(shellWindows[0]); + _ = UiContext.WpfApplication.Run(shellWindows[0]); } - else if (UiContext.WpfApplication.StartupUri != null) + else if (UiContext.WpfApplication.StartupUri is not null) { - MessageBox.Show("Please remove the StartupUri configuration in App.xaml"); + _ = MessageBox.Show("Please remove the StartupUri configuration in App.xaml"); } else { @@ -78,12 +75,15 @@ protected override void UiThreadStart() => } break; + } + case 0: + { if (UiContext.WpfApplication.Dispatcher.Thread.ThreadState != ThreadState.Running) { - UiContext.WpfApplication.Run(); + _ = UiContext.WpfApplication.Run(); } - else if (UiContext.WpfApplication.MainWindow != null) + else if (UiContext.WpfApplication.MainWindow is not null) { // show window if possible UiContext.WpfApplication.MainWindow.Show(); @@ -94,7 +94,10 @@ protected override void UiThreadStart() => } break; + } + default: + { UiContext.WpfApplication.Startup += (sender, args) => { foreach (var window in shellWindows) @@ -105,10 +108,11 @@ protected override void UiThreadStart() => if (UiContext.WpfApplication.Dispatcher.Thread.ThreadState != ThreadState.Running) { - UiContext.WpfApplication.Run(); + _ = UiContext.WpfApplication.Run(); } break; + } } }); } diff --git a/src/Extensions.Hosting.Wpf/WpfBuilderExtensions.cs b/src/Extensions.Hosting.Wpf/WpfBuilderExtensions.cs index e3d0c4d..7d31e0d 100644 --- a/src/Extensions.Hosting.Wpf/WpfBuilderExtensions.cs +++ b/src/Extensions.Hosting.Wpf/WpfBuilderExtensions.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -7,84 +7,75 @@ namespace ReactiveMarbles.Extensions.Hosting.Wpf; -/// -/// Provides extension methods for configuring WPF applications and windows using the IWpfBuilder interface. -/// +/// Provides extension methods for configuring WPF applications and windows using the IWpfBuilder interface. /// These extension methods enable fluent registration and configuration of WPF application types, main /// windows, and context actions within a builder pattern. They are intended to simplify setup and integration of WPF /// components in applications that use dependency injection or modular configuration. public static class WpfBuilderExtensions { - /// - /// Registers the specified window type with the WPF builder for use in the application. - /// - /// The type of the window to register. Must derive from . - /// The WPF builder instance to configure. Cannot be null. - /// The same instance to allow for method chaining, or if is null. - public static IWpfBuilder? UseWindow(this IWpfBuilder wpfBuilder) - where TWindow : Window + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IWpfBuilder wpfBuilder) { - wpfBuilder?.WindowTypes.Add(typeof(TWindow)); - return wpfBuilder; - } - - /// - /// Configures the WPF builder to use the specified application type. - /// - /// The type of the WPF application to use. Must derive from . - /// The WPF builder to configure. Cannot be null. - /// The same instance for method chaining. - /// Thrown if is null. - public static IWpfBuilder UseApplication(this IWpfBuilder wpfBuilder) - where TApplication : Application - { - if (wpfBuilder is null) + /// Registers the specified window type with the WPF builder for use in the application. + /// The type of the window to register. Must derive from . + /// The same instance to allow for method chaining, or if is null. + public IWpfBuilder? UseWindow() + where TWindow : Window { - throw new ArgumentNullException(nameof(wpfBuilder)); + wpfBuilder?.WindowTypes.Add(typeof(TWindow)); + return wpfBuilder; } - wpfBuilder.ApplicationType = typeof(TApplication); - return wpfBuilder; - } - - /// - /// Configures the WPF builder to use the specified application instance as the current application. - /// - /// The type of the application to use. Must derive from . - /// The WPF builder to configure. Cannot be null. - /// The application instance to set as the current application. Must not be null. - /// The same instance for method chaining. - /// Thrown if is null. - public static IWpfBuilder UseCurrentApplication(this IWpfBuilder wpfBuilder, TApplication currentApplication) - where TApplication : Application - { - if (wpfBuilder is null) + /// Configures the WPF builder to use the specified application type. + /// The type of the WPF application to use. Must derive from . + /// The same instance for method chaining. + /// Thrown if is null. + public IWpfBuilder UseApplication() + where TApplication : Application { - throw new ArgumentNullException(nameof(wpfBuilder)); - } + if (wpfBuilder is null) + { + throw new ArgumentNullException(nameof(wpfBuilder)); + } - wpfBuilder.ApplicationType = typeof(TApplication); - wpfBuilder.Application = currentApplication; - return wpfBuilder; - } + wpfBuilder.ApplicationType = typeof(TApplication); + return wpfBuilder; + } - /// - /// Configures the WPF context by specifying an action to be executed during context setup. - /// - /// The WPF builder instance to configure. Cannot be null. - /// An action to perform additional configuration on the WPF context. This action is invoked during context setup - /// and can be null if no additional configuration is required. - /// The same instance, enabling method chaining. - /// Thrown if is null. - public static IWpfBuilder ConfigureContext(this IWpfBuilder wpfBuilder, Action configureAction) - { - if (wpfBuilder is null) + /// Configures the WPF builder to use the specified application instance as the current application. + /// The type of the application to use. Must derive from . + /// The application instance to set as the current application. Must not be null. + /// The same instance for method chaining. + /// Thrown if is null. + public IWpfBuilder UseCurrentApplication(TApplication currentApplication) + where TApplication : Application { - throw new ArgumentNullException(nameof(wpfBuilder)); + if (wpfBuilder is null) + { + throw new ArgumentNullException(nameof(wpfBuilder)); + } + + wpfBuilder.ApplicationType = typeof(TApplication); + wpfBuilder.Application = currentApplication; + return wpfBuilder; } - wpfBuilder.ConfigureContextAction = configureAction; - return wpfBuilder; + /// Configures the WPF context by specifying an action to be executed during context setup. + /// An action to perform additional configuration on the WPF context. This action is invoked during context setup + /// and can be null if no additional configuration is required. + /// The same instance, enabling method chaining. + /// Thrown if is null. + public IWpfBuilder ConfigureContext(Action configureAction) + { + if (wpfBuilder is null) + { + throw new ArgumentNullException(nameof(wpfBuilder)); + } + + wpfBuilder.ConfigureContextAction = configureAction; + return wpfBuilder; + } } } diff --git a/src/Extensions.Hosting.Wpf/WpfHostedService.cs b/src/Extensions.Hosting.Wpf/WpfHostedService.cs index d3c735b..46c5f85 100644 --- a/src/Extensions.Hosting.Wpf/WpfHostedService.cs +++ b/src/Extensions.Hosting.Wpf/WpfHostedService.cs @@ -1,7 +1,8 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; @@ -10,10 +11,7 @@ namespace ReactiveMarbles.Extensions.Hosting.Wpf; -/// -/// Provides an implementation of IHostedService that manages the lifetime of a WPF application within a generic host -/// environment. -/// +/// Provides an implementation of IHostedService that manages the lifetime of a WPF application within a generic host environment. /// This service enables integration of a WPF application's main loop with the .NET Generic Host, /// allowing WPF applications to participate in host-managed startup and shutdown sequences. It is typically registered /// as a singleton in the application's dependency injection container. @@ -22,6 +20,10 @@ namespace ReactiveMarbles.Extensions.Hosting.Wpf; /// The IWpfContext that provides access to the WPF application's context and dispatcher. public class WpfHostedService(ILogger logger, WpfThread wpfThread, IWpfContext wpfContext) : IHostedService { + /// Logs when the WPF application is stopping. + private static readonly Action LogStoppingWpf = + LoggerMessage.Define(LogLevel.Debug, new EventId(1, nameof(LogStoppingWpf)), "Stopping WPF due to application exit."); + /// public Task StartAsync(CancellationToken cancellationToken) { @@ -38,12 +40,14 @@ public Task StartAsync(CancellationToken cancellationToken) /// public async Task StopAsync(CancellationToken cancellationToken) { - if (wpfContext.IsRunning) + if (!wpfContext.IsRunning) { - logger.LogDebug("Stopping WPF due to application exit."); - - // Stop application - await wpfContext.Dispatcher.InvokeAsync(() => wpfContext.WpfApplication?.Shutdown()); + return; } + + LogStoppingWpf(logger, null); + + // Stop application + await wpfContext.Dispatcher.InvokeAsync(() => wpfContext.WpfApplication?.Shutdown()); } } diff --git a/src/Extensions.Hosting.slnx b/src/Extensions.Hosting.slnx index 530470c..c4f0e88 100644 --- a/src/Extensions.Hosting.slnx +++ b/src/Extensions.Hosting.slnx @@ -16,6 +16,7 @@ + diff --git a/src/Extensions.Logging.Log4Net/Entities/MessageCandidate.cs b/src/Extensions.Logging.Log4Net/Entities/MessageCandidate.cs index 0a958d7..daa05f1 100644 --- a/src/Extensions.Logging.Log4Net/Entities/MessageCandidate.cs +++ b/src/Extensions.Logging.Log4Net/Entities/MessageCandidate.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -7,10 +7,7 @@ namespace ReactiveMarbles.Extensions.Logging.Log4Net.Entities; -/// -/// Represents a candidate log message, including its log level, event identifier, state, optional exception, and a -/// formatter function for generating the message text. -/// +/// Represents a candidate log message, including its log level, event identifier, state, optional exception, and a formatter function for generating the message text. /// Use this struct to encapsulate all information required to format and emit a log message. The /// formatter function can be used to produce the final log message string based on the provided state and exception. /// This type is typically used in logging frameworks to defer message formatting until it is needed. @@ -18,11 +15,7 @@ namespace ReactiveMarbles.Extensions.Logging.Log4Net.Entities; /// the log entry. public readonly record struct MessageCandidate { - /// - /// Initializes a new instance of the struct. - /// Initializes a new instance of the MessageCandidate class with the specified log level, event identifier, - /// state, exception, and formatter. - /// + /// Initializes a new instance of the struct. Initializes a new instance of the MessageCandidate class with the specified log level, event identifier, state, exception, and formatter. /// The severity level of the log entry. /// The identifier for the event associated with the log entry. /// The state object to be logged. Represents the primary content or context for the log entry. @@ -37,28 +30,18 @@ public MessageCandidate(LogLevel logLevel, EventId eventId, TState state, Except Formatter = formatter; } - /// - /// Gets the log level the message should be printed with. - /// + /// Gets the log level the message should be printed with. public LogLevel LogLevel { get; } - /// - /// Gets the event id of the message. - /// + /// Gets the event id of the message. public EventId EventId { get; } - /// - /// Gets the message state. Can be provided to the formatter to generate the string representation of the error message. - /// + /// Gets the message state. Can be provided to the formatter to generate the string representation of the error message. public TState State { get; } - /// - /// Gets exception that should be printed with the message. Null if the log message has no corrosponding exception. - /// + /// Gets exception that should be printed with the message. Null if the log message has no corrosponding exception. public Exception? Exception { get; } - /// - /// Gets the message formatter. Can be called with the state and exception to generate the string representation of the error message. - /// + /// Gets the message formatter. Can be called with the state and exception to generate the string representation of the error message. public Func Formatter { get; } } diff --git a/src/Extensions.Logging.Log4Net/Entities/NodeInfo.cs b/src/Extensions.Logging.Log4Net/Entities/NodeInfo.cs index 4743098..e00378e 100644 --- a/src/Extensions.Logging.Log4Net/Entities/NodeInfo.cs +++ b/src/Extensions.Logging.Log4Net/Entities/NodeInfo.cs @@ -1,35 +1,27 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Collections.Generic; namespace ReactiveMarbles.Extensions.Logging.Log4Net.Entities; -/// -/// Class to store information of a log4net xml config file node. -/// +/// Class to store information of a log4net xml config file node. public class NodeInfo { - /// - /// Gets or sets the x path to find the node to override. - /// + /// Gets or sets the x path to find the node to override. /// /// The x path. /// public string? XPath { get; set; } - /// - /// Gets or sets the content of the node. - /// + /// Gets or sets the content of the node. /// /// The content of the node. /// public string? NodeContent { get; set; } - /// - /// Gets or sets the attributes. - /// + /// Gets or sets the attributes. /// /// The attributes. /// diff --git a/src/Extensions.Logging.Log4Net/Extensions.Logging.Log4Net.csproj b/src/Extensions.Logging.Log4Net/Extensions.Logging.Log4Net.csproj index bff5063..cc8b2bd 100644 --- a/src/Extensions.Logging.Log4Net/Extensions.Logging.Log4Net.csproj +++ b/src/Extensions.Logging.Log4Net/Extensions.Logging.Log4Net.csproj @@ -1,6 +1,6 @@ - + - netstandard2.0;net8.0;net9.0;net10.0 + net462;net472;net48;net481;net8.0;net9.0;net10.0;net11.0 CP.Extensions.Logging.Log4Net @@ -8,30 +8,36 @@ snupkg - + - - - - - + + + + + - - - - + + + + - - - - + + + + - - - - + + + + - \ No newline at end of file + + + + + + + diff --git a/src/Extensions.Logging.Log4Net/Extensions/DocumentExtensions.cs b/src/Extensions.Logging.Log4Net/Extensions/DocumentExtensions.cs index 9b5bbfb..47abe98 100644 --- a/src/Extensions.Logging.Log4Net/Extensions/DocumentExtensions.cs +++ b/src/Extensions.Logging.Log4Net/Extensions/DocumentExtensions.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.IO; @@ -8,40 +8,54 @@ namespace ReactiveMarbles.Extensions.Logging.Log4Net.Extensions; -/// -/// Class with XmlDocument and XDocument extensions. -/// +/// Class with XmlDocument and XDocument extensions. internal static class DocumentExtensions { - /// - /// Converts a XmlDocument object into xDocument. - /// - /// The XML document. - /// The XmlDocument converted to XDocument. - public static XDocument ToXDocument(this XmlDocument xmlDocument) + /// Provides extension members for this receiver. + /// The receiver instance. + extension(XDocument document) { - using (var memoryStream = new MemoryStream()) + /// Converts a XDocument object into XmlDocument. + /// The XDocument converted to XmlDocument. + public XmlDocument ToXmlDocument() { - xmlDocument.Save(memoryStream); - memoryStream.Seek(0, SeekOrigin.Begin); - return XDocument.Load(memoryStream); + using var memoryStream = new MemoryStream(); + document.Save(memoryStream); + _ = memoryStream.Seek(0, SeekOrigin.Begin); + + var xmlDocument = new XmlDocument + { + XmlResolver = null, + }; + using var reader = XmlReader.Create(memoryStream, CreateSecureXmlReaderSettings()); + xmlDocument.Load(reader); + return xmlDocument; } } - /// - /// Converts a XDocument object into XmlDocument. - /// - /// The x document. - /// The XDocument converted to XmlDocument. - public static XmlDocument ToXmlDocument(this XDocument xDocument) + /// Provides extension members for this receiver. + /// The receiver instance. + extension(XmlDocument xmlDocument) { - using (var memoryStream = new MemoryStream()) + /// Converts a XmlDocument object into xDocument. + /// The XmlDocument converted to XDocument. + public XDocument ToXDocument() { - xDocument.Save(memoryStream); - memoryStream.Seek(0, SeekOrigin.Begin); - var xmlDoc = new XmlDocument(); - xmlDoc.Load(memoryStream); - return xmlDoc; + using var memoryStream = new MemoryStream(); + xmlDocument.Save(memoryStream); + _ = memoryStream.Seek(0, SeekOrigin.Begin); + + using var reader = XmlReader.Create(memoryStream, CreateSecureXmlReaderSettings()); + return XDocument.Load(reader); } } + + /// Creates XML reader settings that prevent DTD processing. + /// The secure XML reader settings. + private static XmlReaderSettings CreateSecureXmlReaderSettings() => + new() + { + DtdProcessing = DtdProcessing.Prohibit, + XmlResolver = null, + }; } diff --git a/src/Extensions.Logging.Log4Net/Extensions/Log4NetProviderExtensions.cs b/src/Extensions.Logging.Log4Net/Extensions/Log4NetProviderExtensions.cs index 476fd86..1835264 100644 --- a/src/Extensions.Logging.Log4Net/Extensions/Log4NetProviderExtensions.cs +++ b/src/Extensions.Logging.Log4Net/Extensions/Log4NetProviderExtensions.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -7,35 +7,35 @@ namespace ReactiveMarbles.Extensions.Logging.Log4Net.Extensions; -/// -/// Log4Net provider extensions. -/// +/// Log4Net provider extensions. public static class Log4NetProviderExtensions { - /// - /// Creates a logger instance for the specified category type using the provided logger provider. - /// - /// This extension method simplifies logger creation by associating the logger with the - /// full name of the specified category type. Only logger providers of type Log4NetProvider are - /// supported. - /// The category type for which to create a logger. Typically, this is the class type that will use the logger. - /// The logger provider used to create the logger. Must be of type Log4NetProvider. - /// An ILogger instance associated with the specified category type. - /// Thrown if the logger provider is null. - /// Thrown if the logger provider is not of type Log4NetProvider. - public static ILogger CreateLogger(this ILoggerProvider self) - where TName : class + /// Provides extension members for this receiver. + /// The receiver instance. + extension(ILoggerProvider self) { - if (self == null) + /// Creates a logger instance for the specified category type using the provided logger provider. + /// This extension method simplifies logger creation by associating the logger with the + /// full name of the specified category type. Only logger providers of type Log4NetProvider are + /// supported. + /// The category type for which to create a logger. Typically, this is the class type that will use the logger. + /// An ILogger instance associated with the specified category type. + /// Thrown if the logger provider is null. + /// Thrown if the logger provider is not of type Log4NetProvider. + public ILogger CreateLogger() + where TName : class { - throw new ArgumentNullException(nameof(self)); - } + if (self is null) + { + throw new ArgumentNullException(nameof(self)); + } - if (!self.GetType().IsAssignableFrom(typeof(Log4NetProvider))) - { - throw new ArgumentOutOfRangeException(nameof(self), "The ILoggerProvider should be of type Log4NetProvider."); - } + if (self is not Log4NetProvider) + { + throw new ArgumentOutOfRangeException(nameof(self), "The ILoggerProvider should be of type Log4NetProvider."); + } - return self.CreateLogger(typeof(TName).FullName!); + return self.CreateLogger(typeof(TName).FullName!); + } } } diff --git a/src/Extensions.Logging.Log4Net/Extensions/LogExtensions.cs b/src/Extensions.Logging.Log4Net/Extensions/LogExtensions.cs index 21ebd82..79c9eb8 100644 --- a/src/Extensions.Logging.Log4Net/Extensions/LogExtensions.cs +++ b/src/Extensions.Logging.Log4Net/Extensions/LogExtensions.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -7,29 +7,25 @@ namespace ReactiveMarbles.Extensions.Logging.Log4Net.Extensions; -/// -/// Provides extension methods for the ILog interface to support logging messages at custom log levels such as Critical -/// and Trace. -/// +/// Provides extension methods for the ILog interface to support logging messages at custom log levels such as Critical and Trace. /// These extension methods enable logging at additional severity levels not present in the standard ILog /// interface. Use these methods to log messages with Critical or Trace importance when using log4net. public static class LogExtensions { - /// - /// Criticals the specified message. - /// - /// The log. - /// The message. - /// The exception. - public static void Critical(this ILog log, object message, Exception exception) - => log?.Logger.Log(null!, log4net.Core.Level.Critical, message, exception); + /// Provides extension members for this receiver. + /// The receiver instance. + extension(ILog log) + { + /// Criticals the specified message. + /// The message. + /// The exception. + public void Critical(object message, Exception exception) + => log?.Logger.Log(null!, log4net.Core.Level.Critical, message, exception); - /// - /// Traces the specified message. - /// - /// The log. - /// The message. - /// The exception. - public static void Trace(this ILog log, object message, Exception exception) - => log?.Logger.Log(null!, log4net.Core.Level.Trace, message, exception); + /// Traces the specified message. + /// The message. + /// The exception. + public void Trace(object message, Exception exception) + => log?.Logger.Log(null!, log4net.Core.Level.Trace, message, exception); + } } diff --git a/src/Extensions.Logging.Log4Net/ILog4NetLogLevelTranslator.cs b/src/Extensions.Logging.Log4Net/ILog4NetLogLevelTranslator.cs index 93e062c..984d5dc 100644 --- a/src/Extensions.Logging.Log4Net/ILog4NetLogLevelTranslator.cs +++ b/src/Extensions.Logging.Log4Net/ILog4NetLogLevelTranslator.cs @@ -1,19 +1,15 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.Extensions.Logging; namespace ReactiveMarbles.Extensions.Logging; -/// -/// Represents a log level translator between the different logging systems. -/// +/// Represents a log level translator between the different logging systems. public interface ILog4NetLogLevelTranslator { - /// - /// Translates a to a log4net based on the provided options. - /// + /// Translates a to a log4net based on the provided options. /// The log level to translate. /// The log4net provider options influencing the translation. /// The corresponding log level for log4net. diff --git a/src/Extensions.Logging.Log4Net/ILog4NetLoggingEventFactory.cs b/src/Extensions.Logging.Log4Net/ILog4NetLoggingEventFactory.cs index f31c834..510e23d 100644 --- a/src/Extensions.Logging.Log4Net/ILog4NetLoggingEventFactory.cs +++ b/src/Extensions.Logging.Log4Net/ILog4NetLoggingEventFactory.cs @@ -1,22 +1,18 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. -using log4net.Core; using Microsoft.Extensions.Logging; using ReactiveMarbles.Extensions.Logging.Log4Net.Entities; +using Log4NetCoreLogger = log4net.Core.ILogger; +using LoggingEvent = log4net.Core.LoggingEvent; namespace ReactiveMarbles.Extensions.Logging; -/// -/// Represents a factory that creates the log4net from a . -/// +/// Represents a factory that creates the log4net from a . public interface ILog4NetLoggingEventFactory { - /// - /// Creates a new log4net LoggingEvent instance using the specified message candidate, logger, provider options, - /// and external scope provider. - /// + /// Creates a new log4net LoggingEvent instance using the specified message candidate, logger, provider options, and external scope provider. /// The type of the state object associated with the log message. /// The candidate message containing the log state and related metadata to be included in the logging event. /// The log4net logger to associate with the created logging event. Cannot be null. @@ -26,7 +22,7 @@ public interface ILog4NetLoggingEventFactory /// A LoggingEvent instance populated with the provided message, logger, options, and scope information. LoggingEvent? CreateLoggingEvent( in MessageCandidate messageCandidate, - log4net.Core.ILogger logger, + Log4NetCoreLogger logger, Log4NetProviderOptions options, IExternalScopeProvider scopeProvider); } diff --git a/src/Extensions.Logging.Log4Net/Log4NetExtensions.cs b/src/Extensions.Logging.Log4Net/Log4NetExtensions.cs index 6c12afc..ad4826e 100644 --- a/src/Extensions.Logging.Log4Net/Log4NetExtensions.cs +++ b/src/Extensions.Logging.Log4Net/Log4NetExtensions.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -8,109 +8,93 @@ namespace ReactiveMarbles.Extensions.Logging; -/// -/// The log4net extensions class. -/// +/// The log4net extensions class. public static class Log4NetExtensions { - /// - /// Adds the log4net. - /// - /// The factory. - /// The with added Log4Net provider. - public static ILoggerFactory AddLog4Net(this ILoggerFactory factory) - => factory.AddLog4Net(new Log4NetProviderOptions()); + /// Provides extension members for this receiver. + /// The receiver instance. + extension(ILoggerFactory factory) + { + /// Adds the log4net. + /// The with added Log4Net provider. + public ILoggerFactory AddLog4Net() + => factory.AddLog4Net(new Log4NetProviderOptions()); - /// - /// Adds the log4net. - /// - /// The factory. - /// The log4net Config File. - /// The after adding the log4net provider. - public static ILoggerFactory AddLog4Net(this ILoggerFactory factory, string log4NetConfigFile) - => factory.AddLog4Net(log4NetConfigFile, false); + /// Adds the log4net. + /// The log4net Config File. + /// The after adding the log4net provider. + public ILoggerFactory AddLog4Net(string log4NetConfigFile) + => factory.AddLog4Net(log4NetConfigFile, false); - /// - /// Adds the log4net logging provider. - /// - /// The factory. - /// The log4 net configuration file. - /// if set to true [watch]. - /// The after adding the log4net provider. - public static ILoggerFactory AddLog4Net(this ILoggerFactory factory, string log4NetConfigFile, bool watch) - => factory.AddLog4Net(new Log4NetProviderOptions(log4NetConfigFile, watch)); + /// Adds the log4net logging provider. + /// The log4 net configuration file. + /// if set to true [watch]. + /// The after adding the log4net provider. + public ILoggerFactory AddLog4Net(string log4NetConfigFile, bool watch) + => factory.AddLog4Net(new Log4NetProviderOptions(log4NetConfigFile, watch)); - /// - /// Adds the log4net logging provider. - /// - /// The logger factory. - /// The options for log4net provider. - /// The after adding the log4net provider. - public static ILoggerFactory AddLog4Net(this ILoggerFactory factory, Log4NetProviderOptions options) - { - if (factory == null) + /// Adds the log4net logging provider. + /// The options for log4net provider. + /// The after adding the log4net provider. + public ILoggerFactory AddLog4Net(Log4NetProviderOptions options) { - throw new ArgumentNullException(nameof(factory)); - } - - factory.AddProvider(new Log4NetProvider(options)); - return factory; - } + if (factory is null) + { + throw new ArgumentNullException(nameof(factory)); + } - /// - /// Adds the log4net logging provider. - /// - /// The logging builder instance. - /// The passed as parameter with the new provider registered. - public static ILoggingBuilder AddLog4Net(this ILoggingBuilder builder) - { - var options = new Log4NetProviderOptions(); - return builder.AddLog4Net(options); + factory.AddProvider(new Log4NetProvider(options)); + return factory; + } } - /// - /// Adds the log4net logging provider. - /// - /// The logging builder instance. - /// The log4net Config File. - /// The passed as parameter with the new provider registered. - public static ILoggingBuilder AddLog4Net(this ILoggingBuilder builder, string log4NetConfigFile) + /// Provides extension members for this receiver. + /// The receiver instance. + extension(ILoggingBuilder builder) { - var options = new Log4NetProviderOptions(log4NetConfigFile); - return builder.AddLog4Net(options); - } + /// Adds the log4net logging provider. + /// The passed as parameter with the new provider registered. + public ILoggingBuilder AddLog4Net() + { + var options = new Log4NetProviderOptions(); + return builder.AddLog4Net(options); + } - /// - /// Adds the log4net logging provider. - /// - /// The logging builder instance. - /// The log4net Config File. - /// if set to true, the configuration will be reloaded when the xml configuration file changes. - /// - /// The passed as parameter with the new provider registered. - /// - public static ILoggingBuilder AddLog4Net(this ILoggingBuilder builder, string log4NetConfigFile, bool watch) - { - var options = new Log4NetProviderOptions(log4NetConfigFile, watch); - return builder.AddLog4Net(options); - } + /// Adds the log4net logging provider. + /// The log4net Config File. + /// The passed as parameter with the new provider registered. + public ILoggingBuilder AddLog4Net(string log4NetConfigFile) + { + var options = new Log4NetProviderOptions(log4NetConfigFile); + return builder.AddLog4Net(options); + } - /// - /// Adds a Log4Net-based logging provider to the specified logging builder. - /// - /// Use this method to enable Log4Net logging in an application's logging pipeline. This method - /// registers the Log4Net provider as a singleton service. - /// The logging builder to which the Log4Net provider will be added. - /// The options used to configure the Log4Net provider. Cannot be null. - /// The same instance of for chaining. - public static ILoggingBuilder AddLog4Net(this ILoggingBuilder builder, Log4NetProviderOptions options) - { - if (builder == null) + /// Adds the log4net logging provider. + /// The log4net Config File. + /// if set to true, the configuration will be reloaded when the xml configuration file changes. + /// + /// The passed as parameter with the new provider registered. + /// + public ILoggingBuilder AddLog4Net(string log4NetConfigFile, bool watch) { - throw new ArgumentNullException(nameof(builder)); + var options = new Log4NetProviderOptions(log4NetConfigFile, watch); + return builder.AddLog4Net(options); } - builder.Services.AddSingleton(new Log4NetProvider(options)); - return builder; + /// Adds a Log4Net-based logging provider to the specified logging builder. + /// Use this method to enable Log4Net logging in an application's logging pipeline. This method + /// registers the Log4Net provider as a singleton service. + /// The options used to configure the Log4Net provider. Cannot be null. + /// The same instance of for chaining. + public ILoggingBuilder AddLog4Net(Log4NetProviderOptions options) + { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + + _ = builder.Services.AddSingleton(new Log4NetProvider(options)); + return builder; + } } } diff --git a/src/Extensions.Logging.Log4Net/Log4NetLogLevelTranslator.cs b/src/Extensions.Logging.Log4Net/Log4NetLogLevelTranslator.cs index ea8287f..667de92 100644 --- a/src/Extensions.Logging.Log4Net/Log4NetLogLevelTranslator.cs +++ b/src/Extensions.Logging.Log4Net/Log4NetLogLevelTranslator.cs @@ -1,10 +1,10 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; -using log4net.Core; using Microsoft.Extensions.Logging; +using Level = log4net.Core.Level; namespace ReactiveMarbles.Extensions.Logging; @@ -12,33 +12,36 @@ namespace ReactiveMarbles.Extensions.Logging; public sealed class Log4NetLogLevelTranslator : ILog4NetLogLevelTranslator { /// - public Level? TranslateLogLevel(LogLevel logLevel, Log4NetProviderOptions options) - { - switch (logLevel) + public Level? TranslateLogLevel(LogLevel logLevel, Log4NetProviderOptions options) => + logLevel switch { - case LogLevel.Critical: - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } + LogLevel.Critical => TranslateCriticalLevel(options), + LogLevel.Debug => Level.Debug, + LogLevel.Error => Level.Error, + LogLevel.Information => Level.Info, + LogLevel.Warning => Level.Warn, + LogLevel.Trace => Level.Trace, + LogLevel.None => null, + _ => null, + }; - var overrideCriticalLevelWith = options.OverrideCriticalLevelWith; - return !string.IsNullOrEmpty(overrideCriticalLevelWith) - && overrideCriticalLevelWith.Equals(LogLevel.Critical.ToString(), StringComparison.OrdinalIgnoreCase) - ? Level.Critical - : Level.Fatal; - case LogLevel.Debug: - return Level.Debug; - case LogLevel.Error: - return Level.Error; - case LogLevel.Information: - return Level.Info; - case LogLevel.Warning: - return Level.Warn; - case LogLevel.Trace: - return Level.Trace; + /// Translates the critical log level using the configured override value. + /// The log4net provider options. + /// The log4net level to use for critical messages. + /// Thrown when options is null. + private static Level TranslateCriticalLevel(Log4NetProviderOptions options) + { + if (options is null) + { + throw new ArgumentNullException(nameof(options)); } - return null; + var overrideCriticalLevelWith = options.OverrideCriticalLevelWith; + var useCriticalLevel = !string.IsNullOrEmpty(overrideCriticalLevelWith) + && overrideCriticalLevelWith.Equals(nameof(LogLevel.Critical), StringComparison.OrdinalIgnoreCase); + + return useCriticalLevel + ? Level.Critical + : Level.Fatal; } } diff --git a/src/Extensions.Logging.Log4Net/Log4NetLogger.cs b/src/Extensions.Logging.Log4Net/Log4NetLogger.cs index aaf1eb3..e0f5c3e 100644 --- a/src/Extensions.Logging.Log4Net/Log4NetLogger.cs +++ b/src/Extensions.Logging.Log4Net/Log4NetLogger.cs @@ -1,30 +1,25 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; -using log4net; using Microsoft.Extensions.Logging; using ReactiveMarbles.Extensions.Logging.Log4Net.Entities; +using Log4NetCoreLogger = log4net.Core.ILogger; +using LogManager = log4net.LogManager; namespace ReactiveMarbles.Extensions.Logging; -/// -/// The log4net logger class. -/// +/// The log4net logger class. public class Log4NetLogger : ILogger { + /// Stores the external scope provider value. private readonly IExternalScopeProvider _externalScopeProvider; - /// - /// The log. - /// - private readonly log4net.Core.ILogger _logger; + /// The log. + private readonly Log4NetCoreLogger _logger; - /// - /// Initializes a new instance of the class using the specified provider options and external scope. - /// provider. - /// + /// Initializes a new instance of the class using the specified provider options and external scope. provider. /// The options used to configure the logger, including repository and logger name information. Cannot be null. /// The external scope provider used to capture and manage logging scopes. Cannot be null. /// Thrown if either options or externalScopeProvider is null. @@ -32,42 +27,37 @@ public Log4NetLogger(Log4NetProviderOptions options, IExternalScopeProvider exte { Options = options ?? throw new ArgumentNullException(nameof(options)); _externalScopeProvider = externalScopeProvider ?? throw new ArgumentNullException(nameof(externalScopeProvider)); - _logger = LogManager.GetLogger(options.LoggerRepository, options.Name).Logger; + var loggerRepository = options.LoggerRepository; + _logger = string.IsNullOrWhiteSpace(loggerRepository) + ? LogManager.GetLogger(options.Name).Logger + : LogManager.GetLogger(loggerRepository!, options.Name).Logger; } - /// - /// Gets the name. - /// + /// Gets the name. public string Name => _logger.Name; - /// - /// Gets a get-only property for accessing the - /// within the instance. - /// + /// Gets a get-only property for accessing the within the instance. internal Log4NetProviderOptions Options { get; } - /// - /// Begins a logical operation scope. - /// + /// Begins a logical operation scope. /// The type of the state. /// The identifier for the scope. /// /// An IDisposable that ends the logical operation scope on dispose. /// - public IDisposable BeginScope(TState state) + public IDisposable? BeginScope(TState state) + where TState : notnull => _externalScopeProvider.Push(state); - /// - /// Determines whether the logging level is enabled. - /// + /// Determines whether the logging level is enabled. /// The log level. /// The value indicating whether the logging level is enabled. /// Throws when is outside allowed range. public bool IsEnabled(LogLevel logLevel) { var translatedLogLevel = Options.LogLevelTranslator?.TranslateLogLevel(logLevel, Options); - if (translatedLogLevel != null) + if (translatedLogLevel is not null) { return _logger.IsEnabledFor(translatedLogLevel); } @@ -80,9 +70,7 @@ public bool IsEnabled(LogLevel logLevel) throw new ArgumentOutOfRangeException(nameof(logLevel)); } - /// - /// Logs an exception into the log. - /// + /// Logs an exception into the log. /// The log level. /// The event Id. /// The state. @@ -108,7 +96,7 @@ public void Log( var loggingEvent = Options.LoggingEventFactory?.CreateLoggingEvent(in candidate, _logger, Options, _externalScopeProvider); - if (loggingEvent == null) + if (loggingEvent is null) { return; } @@ -116,11 +104,10 @@ public void Log( _logger.Log(loggingEvent); } - private static void EnsureValidFormatter(Func formatter) - { - if (formatter == null) - { - throw new ArgumentNullException(nameof(formatter)); - } - } + /// Validates that a formatter delegate was supplied. + /// The type of the state. + /// The formatter to validate. + /// Thrown when formatter is null. + private static void EnsureValidFormatter(Func formatter) => + _ = formatter ?? throw new ArgumentNullException(nameof(formatter)); } diff --git a/src/Extensions.Logging.Log4Net/Log4NetLoggingEventFactory.cs b/src/Extensions.Logging.Log4Net/Log4NetLoggingEventFactory.cs index d6c66d5..ee79058 100644 --- a/src/Extensions.Logging.Log4Net/Log4NetLoggingEventFactory.cs +++ b/src/Extensions.Logging.Log4Net/Log4NetLoggingEventFactory.cs @@ -1,42 +1,58 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; using System.Collections; using System.Collections.Generic; using System.Globalization; -using log4net.Core; using Microsoft.Extensions.Logging; using ReactiveMarbles.Extensions.Logging.Log4Net.Entities; +using Log4NetCoreLogger = log4net.Core.ILogger; +using LoggingEvent = log4net.Core.LoggingEvent; namespace ReactiveMarbles.Extensions.Logging; /// -public class Log4NetLoggingEventFactory - : ILog4NetLoggingEventFactory +public class Log4NetLoggingEventFactory : ILog4NetLoggingEventFactory { - /// - /// The default property name for scopes that don't provide their own property name by implementing - /// an where T is and where TKey - /// is . - /// + /// The default property name for scopes that don't provide their own property name by implementing an where T is and where TKey is . protected const string DefaultScopeProperty = "scope"; + + /// Stores the event id property value. private const string EventIdProperty = "eventId"; + /// Stores tuple scope handlers for supported tuple value types. + private static readonly Func[] _tupleScopeHandlers = + [ + TryAddTupleScope, + TryAddTupleScope, + TryAddTupleScope, + TryAddTupleScope, + TryAddTupleScope, + TryAddTupleScope, + TryAddTupleScope, + TryAddTupleScope, + TryAddTupleScope, + TryAddTupleScope, + TryAddTupleScope, + TryAddTupleScope, + TryAddTupleScope, + ]; + /// public LoggingEvent? CreateLoggingEvent( in MessageCandidate messageCandidate, - log4net.Core.ILogger logger, + Log4NetCoreLogger logger, Log4NetProviderOptions options, IExternalScopeProvider scopeProvider) { - if (options == null) + if (options is null) { throw new ArgumentNullException(nameof(options)); } - if (logger == null) + if (logger is null) { throw new ArgumentNullException(nameof(logger)); } @@ -45,7 +61,7 @@ public class Log4NetLoggingEventFactory var message = messageCandidate.Formatter(messageCandidate.State, messageCandidate.Exception); var logLevel = options.LogLevelTranslator?.TranslateLogLevel(messageCandidate.LogLevel, options); - if (logLevel == null || (string.IsNullOrEmpty(message) && messageCandidate.Exception == null)) + if (logLevel is null || (string.IsNullOrEmpty(message) && messageCandidate.Exception is null)) { return null; } @@ -78,148 +94,104 @@ public class Log4NetLoggingEventFactory /// /// The the scope information will be added to. /// The external provider for the current logging scope. - protected virtual void EnrichWithScopes(LoggingEvent loggingEvent, IExternalScopeProvider scopeProvider) => scopeProvider?.ForEachScope( - (scope, @event) => - { - // This function will add the scopes in the legacy way they were added before the IExternalScopeProvider was introduced, - // to maintain backwards compatibility. - // This pretty much means that we are emulating a LogicalThreadContextStack, which is a stack, that allows pushing - // strings on to it, which will be concatenated with space as a separator. - // See: https://github.com/apache/logging-log4net/blob/47aaf46d5f031ea29d781bac4617bd1bb9446215/src/log4net/Util/LogicalThreadContextStack.cs#L343 - - // Because string implements IEnumerable we first need to check for string. - if (scope is string) - { - var previousValue = @event.Properties[DefaultScopeProperty] as string; - - @event.Properties[DefaultScopeProperty] = JoinOldAndNewValue(previousValue, scope.ToString()); - return; - } + protected virtual void EnrichWithScopes(LoggingEvent loggingEvent, IExternalScopeProvider scopeProvider) => + scopeProvider?.ForEachScope(AddScopeProperties, loggingEvent); - if (scope is IEnumerable col) - { - foreach (var item in col) - { - if (item is KeyValuePair keyValuePair3) - { - var keyValuePair = keyValuePair3; - var previousValue = @event.Properties[keyValuePair.Key] as string; - @event.Properties[keyValuePair.Key] = JoinOldAndNewValue(previousValue, keyValuePair.Value); - continue; - } - - if (item is KeyValuePair keyValuePair2) - { - var keyValuePair = keyValuePair2; - var previousValue = @event.Properties[keyValuePair.Key] as string; - - // The current culture should not influence how integers/floats/... are displayed in logging, - // so we are using Convert.ToString which will convert IConvertible and IFormattable with - // the specified IFormatProvider. - var additionalValue = Convert.ToString(keyValuePair.Value, CultureInfo.InvariantCulture); - @event.Properties[keyValuePair.Key] = JoinOldAndNewValue(previousValue, additionalValue); - continue; - } - } - - return; - } - - if (FromValueTuple()) - { - return; - } - - if (FromValueTuple()) - { - return; - } - - if (FromValueTuple()) - { - return; - } - - if (FromValueTuple()) - { - return; - } - - if (FromValueTuple()) - { - return; - } - - if (FromValueTuple()) - { - return; - } - - if (FromValueTuple()) - { - return; - } + /// Adds a scope value to the logging event properties. + /// The scope value supplied by Microsoft.Extensions.Logging. + /// The logging event to enrich. + private static void AddScopeProperties(object? scope, LoggingEvent loggingEvent) + { + if (scope is string scopeText) + { + AppendProperty(loggingEvent, DefaultScopeProperty, scopeText); + return; + } - if (FromValueTuple()) - { - return; - } + if (scope is null || TryAddEnumerableScope(scope, loggingEvent) || TryAddTupleScope(scope, loggingEvent)) + { + return; + } - if (FromValueTuple()) - { - return; - } + AppendProperty(loggingEvent, DefaultScopeProperty, Convert.ToString(scope, CultureInfo.InvariantCulture)); + } - if (FromValueTuple()) - { - return; - } + /// Adds key/value scope items from enumerable scope state. + /// The scope object to inspect. + /// The logging event to enrich. + /// true when the scope was an enumerable scope; otherwise, false. + private static bool TryAddEnumerableScope(object scope, LoggingEvent loggingEvent) + { + if (scope is not IEnumerable collection) + { + return false; + } - if (FromValueTuple()) + foreach (var item in collection) + { + if (item is KeyValuePair stringPair) { - return; + AppendProperty(loggingEvent, stringPair.Key, stringPair.Value); + continue; } - if (FromValueTuple()) + if (item is KeyValuePair objectPair) { - return; + AppendProperty(loggingEvent, objectPair.Key, Convert.ToString(objectPair.Value, CultureInfo.InvariantCulture)); } + } - if (FromValueTuple()) - { - return; - } + return true; + } - if (scope is not null) + /// Adds key/value scope items from tuple scope state. + /// The scope object to inspect. + /// The logging event to enrich. + /// true when the scope was a supported tuple scope; otherwise, false. + private static bool TryAddTupleScope(object scope, LoggingEvent loggingEvent) + { + foreach (var tupleScopeHandler in _tupleScopeHandlers) + { + if (tupleScopeHandler(scope, loggingEvent)) { - var previousValue = @event.Properties[DefaultScopeProperty] as string; - var additionalValue = Convert.ToString(scope, CultureInfo.InvariantCulture); - @event.Properties[DefaultScopeProperty] = JoinOldAndNewValue(previousValue, additionalValue); - return; + return true; } + } - bool FromValueTuple() - { - if (scope is ValueTuple valueTuple) - { - var previousValue = @event.Properties[valueTuple.Item1] as string; - var additionalValue = Convert.ToString(valueTuple.Item2, CultureInfo.InvariantCulture); - @event.Properties[valueTuple.Item1] = JoinOldAndNewValue(previousValue, additionalValue); - return true; - } - - return false; - } - }, - loggingEvent); + return false; + } - private static string? JoinOldAndNewValue(string? previousValue, string? newValue) + /// Adds a key/value scope item from tuple scope state. + /// The tuple value type to match. + /// The scope object to inspect. + /// The logging event to enrich. + /// true when the scope matched the tuple value type; otherwise, false. + private static bool TryAddTupleScope(object scope, LoggingEvent loggingEvent) { - if (string.IsNullOrEmpty(previousValue)) + if (scope.GetType() != typeof((string Key, T Value))) { - return newValue; + return false; } - return previousValue + " " + newValue; + var valueTuple = ((string Key, T Value))scope; + AppendProperty(loggingEvent, valueTuple.Key, Convert.ToString(valueTuple.Value, CultureInfo.InvariantCulture)); + return true; } + + /// Appends a log4net property value using the legacy scope separator. + /// The logging event to enrich. + /// The property name to update. + /// The value to append. + private static void AppendProperty(LoggingEvent loggingEvent, string propertyName, string? newValue) + { + var previousValue = loggingEvent.Properties[propertyName] as string; + loggingEvent.Properties[propertyName] = JoinOldAndNewValue(previousValue, newValue); + } + + /// Joins an existing property value with a new value using the legacy scope separator. + /// The existing property value. + /// The new property value. + /// The combined value. + private static string? JoinOldAndNewValue(string? previousValue, string? newValue) => + string.IsNullOrEmpty(previousValue) ? newValue : previousValue + " " + newValue; } diff --git a/src/Extensions.Logging.Log4Net/Log4NetProvider.cs b/src/Extensions.Logging.Log4Net/Log4NetProvider.cs index 61fbcb9..eb0f739 100644 --- a/src/Extensions.Logging.Log4Net/Log4NetProvider.cs +++ b/src/Extensions.Logging.Log4Net/Log4NetProvider.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -11,62 +11,48 @@ using System.Xml; using System.Xml.Linq; using System.Xml.XPath; -using log4net; -using log4net.Config; -using log4net.Repository; using Microsoft.Extensions.Logging; using ReactiveMarbles.Extensions.Logging.Log4Net.Entities; using ReactiveMarbles.Extensions.Logging.Log4Net.Extensions; using ReactiveMarbles.Extensions.Logging.Log4Net.Scope; +using Hierarchy = log4net.Repository.Hierarchy.Hierarchy; +using ILoggerRepository = log4net.Repository.ILoggerRepository; +using LogException = log4net.Core.LogException; +using LogManager = log4net.LogManager; +using XmlConfigurator = log4net.Config.XmlConfigurator; namespace ReactiveMarbles.Extensions.Logging; -/// -/// The log4net provider class. -/// +/// The log4net provider class. /// public class Log4NetProvider : ILoggerProvider, ISupportExternalScope { - /// - /// The loggers collection. - /// - private readonly ConcurrentDictionary _loggers = new ConcurrentDictionary(); - - /// - /// Prevents to dispose the object more than single time. - /// + /// The loggers collection. + private readonly ConcurrentDictionary _loggers = new(); + + /// Prevents to dispose the object more than single time. private bool _disposedValue; - /// - /// The log4net repository. - /// + /// The log4net repository. private ILoggerRepository? _loggerRepository; - /// - /// The provider options. - /// + /// The provider options. private Log4NetProviderOptions? _options; - /// - /// Initializes a new instance of the class. - /// + /// Initializes a new instance of the class. public Log4NetProvider() : this(new Log4NetProviderOptions()) { } - /// - /// Initializes a new instance of the class. - /// + /// Initializes a new instance of the class. /// The log4NetConfigFile. public Log4NetProvider(string log4NetConfigFileName) : this(new Log4NetProviderOptions(log4NetConfigFileName)) { } - /// - /// Initializes a new instance of the class. - /// + /// Initializes a new instance of the class. /// The options. /// options. /// Wach cannot be true when you are overwriting config file values with values from configuration section. @@ -80,17 +66,13 @@ public Log4NetProvider(Log4NetProviderOptions options) ConfigureLog4NetLibrary(); } - /// - /// Finalizes an instance of the class. - /// + /// Finalizes an instance of the class. ~Log4NetProvider() { Dispose(false); } - /// - /// Gets the external logging scope provider. - /// + /// Gets the external logging scope provider. /// /// Reading the offical logging implementations, it seems like we need to handle the case that this might never be set. /// We handle it with a NullScopeProvider instead of null checks, to make the process of implementing interfaces like @@ -98,141 +80,126 @@ public Log4NetProvider(Log4NetProviderOptions options) /// public IExternalScopeProvider ExternalScopeProvider { get; private set; } = NullScopeProvider.Instance; - /// - /// Creates the logger. - /// + /// Creates the logger. /// An instance of the . public ILogger CreateLogger() => CreateLogger(_options?.Name ?? string.Empty); - /// - /// Creates the logger. - /// + /// Creates the logger. /// The category name. /// An instance of the . public ILogger CreateLogger(string categoryName) => _loggers.GetOrAdd(categoryName, CreateLoggerImplementation); - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } - /// - /// Sets the external scope provider to be used for logging operations. - /// + /// Sets the external scope provider to be used for logging operations. /// Use this method to configure how scope information is managed and included in log messages. /// Setting the scope provider to null disables external scoping support. /// The external scope provider that supplies scope information for log entries. Can be null to disable external /// scoping. public void SetScopeProvider(IExternalScopeProvider scopeProvider) => ExternalScopeProvider = scopeProvider; - /// - /// Releases unmanaged and - optionally - managed resources. - /// + /// Releases unmanaged and - optionally - managed resources. /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool disposing) { - if (!_disposedValue) + if (_disposedValue) { - if (disposing) - { - _loggerRepository?.Shutdown(); - _loggers.Clear(); - } + return; + } - _disposedValue = true; + if (disposing) + { + _loggerRepository?.Shutdown(); + _loggers.Clear(); } + + _disposedValue = true; } - /// - /// Updates configuration nodes overriding values if required. - /// + /// Updates configuration nodes overriding values if required. /// The configuration file XML document. /// The overriding values available. /// An within the overriding values replaced. private static XmlDocument UpdateNodesWithOverridingValues(XmlDocument configXmlDocument, IEnumerable overridingNodes) { - var additionalConfig = overridingNodes; - if (additionalConfig != null) + if (overridingNodes is null) { - var configXDoc = configXmlDocument.ToXDocument(); - foreach (var nodeInfo in additionalConfig) + return configXmlDocument; + } + + var configDocument = configXmlDocument.ToXDocument(); + foreach (var nodeInfo in overridingNodes) + { + var node = configDocument.XPathSelectElement(nodeInfo.XPath!); + if (node is null) { - var node = configXDoc.XPathSelectElement(nodeInfo.XPath!); - if (node != null) - { - if (nodeInfo.NodeContent != null) - { - node.Value = nodeInfo.NodeContent; - } - - AddOrUpdateAttributes(node, nodeInfo); - } + continue; } - return configXDoc.ToXmlDocument(); + if (nodeInfo.NodeContent is not null) + { + node.Value = nodeInfo.NodeContent; + } + + AddOrUpdateAttributes(node, nodeInfo); } - return configXmlDocument; + return configDocument.ToXmlDocument(); } - /// - /// Adds or updates the attributes specified in the node information. - /// + /// Adds or updates the attributes specified in the node information. /// The node. /// The node information. private static void AddOrUpdateAttributes(XElement node, NodeInfo nodeInfo) { - if (nodeInfo?.Attributes != null) + if (nodeInfo.Attributes is null) { - foreach (var attribute in nodeInfo.Attributes) + return; + } + + foreach (var attribute in nodeInfo.Attributes) + { + var nodeAttribute = node.Attributes() + .FirstOrDefault(a => a.Name.LocalName.Equals(attribute.Key, StringComparison.OrdinalIgnoreCase)); + if (nodeAttribute is not null) { - var nodeAttribute = node.Attributes() - .FirstOrDefault(a => a.Name.LocalName.Equals(attribute.Key, StringComparison.OrdinalIgnoreCase)); - if (nodeAttribute != null) - { - nodeAttribute.Value = attribute.Value; - } - else - { - node.SetAttributeValue(attribute.Key, attribute.Value); - } + nodeAttribute.Value = attribute.Value; + continue; } + + node.SetAttributeValue(attribute.Key, attribute.Value); } } - /// - /// Parses log4net config file. - /// + /// Parses log4net config file. /// The filename. /// The with the log4net XML element. private static XmlDocument ParseLog4NetConfigFile(string filename) { - using (var stream = File.OpenRead(filename)) + using var stream = File.OpenRead(filename); + var settings = new XmlReaderSettings { - var settings = new XmlReaderSettings - { - DtdProcessing = DtdProcessing.Prohibit - }; - - var log4netConfig = new XmlDocument(); - using (var reader = XmlReader.Create(stream, settings)) - { - log4netConfig.Load(reader); - } + DtdProcessing = DtdProcessing.Prohibit, + XmlResolver = null, + }; - return log4netConfig; - } + var log4netConfig = new XmlDocument + { + XmlResolver = null, + }; + using var reader = XmlReader.Create(stream, settings); + log4netConfig.Load(reader); + return log4netConfig; } - /// - /// Creates the logger implementation. - /// + /// Creates the logger implementation. /// The name. /// The instance. private Log4NetLogger CreateLoggerImplementation(string name) @@ -249,15 +216,11 @@ private Log4NetLogger CreateLoggerImplementation(string name) return new Log4NetLogger(loggerOptions, ExternalScopeProvider); } - /// - /// Gets the current executing assembly considering the target framework. - /// + /// Gets the current executing assembly considering the target framework. /// The assembly to be used as the reference logging assembly. private Assembly GetLoggingReferenceAssembly() => _options?.ConfigurationAssembly ?? Assembly.GetExecutingAssembly(); - /// - /// Ensures that provided options combinations are valid, and sets the class field if everything is ok. - /// + /// Ensures that provided options combinations are valid, and sets the class field if everything is ok. /// The options to validate. /// /// Throws when the Watch option is set and there are properties to override. @@ -267,7 +230,7 @@ private Log4NetLogger CreateLoggerImplementation(string name) /// private void SetOptionsIfValid(Log4NetProviderOptions options) { - if (options == null) + if (options is null) { throw new ArgumentNullException(nameof(options)); } @@ -281,88 +244,84 @@ private void SetOptionsIfValid(Log4NetProviderOptions options) _options = options; } - /// - /// Configures the log4net library using the available configuration data. - /// + /// Configures the log4net library using the available configuration data. private void ConfigureLog4NetLibrary() { if (_options?.UseWebOrAppConfig == true) { - XmlConfigurator.Configure(_loggerRepository!); + _ = XmlConfigurator.Configure(_loggerRepository!); return; } - if (_options?.ExternalConfigurationSetup == false) + if (_options?.ExternalConfigurationSetup != false) { - var fileNamePath = CreateLog4NetFilePath(); - if (_options.Watch) - { - XmlConfigurator.ConfigureAndWatch( - _loggerRepository!, - new FileInfo(fileNamePath)); - } - else - { - var configXml = ParseLog4NetConfigFile(fileNamePath); - if (_options.PropertyOverrides?.Count > 0) - { - configXml = UpdateNodesWithOverridingValues( - configXml, - _options.PropertyOverrides); - } - - XmlConfigurator.Configure(_loggerRepository!, configXml.DocumentElement!); - } + return; } + + var fileNamePath = CreateLog4NetFilePath(); + if (_options.Watch) + { + _ = XmlConfigurator.ConfigureAndWatch(_loggerRepository!, new FileInfo(fileNamePath)); + return; + } + + var configXml = ParseLog4NetConfigFile(fileNamePath); + if (_options.PropertyOverrides.Count > 0) + { + configXml = UpdateNodesWithOverridingValues(configXml, _options.PropertyOverrides); + } + + _ = XmlConfigurator.Configure(_loggerRepository!, configXml.DocumentElement!); } - /// - /// Creates the log4net.config file path. - /// + /// Creates the log4net.config file path. /// The full path to the log4net.config file. private string CreateLog4NetFilePath() { - var fileNamePath = _options?.Log4NetConfigFileName; + var fileNamePath = _options?.Log4NetConfigFileName ?? string.Empty; if (!Path.IsPathRooted(fileNamePath)) { - fileNamePath = Path.Combine(AppContext.BaseDirectory, fileNamePath!); + fileNamePath = Path.Combine(AppContext.BaseDirectory, fileNamePath); } return Path.GetFullPath(fileNamePath); } - /// - /// Gets or creates the logger repository using the given assembly. - /// + /// Gets or creates the logger repository using the given assembly. /// The assembly to be used to create the repository. private void CreateLoggerRepository(Assembly assembly) { - var repositoryType = typeof(log4net.Repository.Hierarchy.Hierarchy); + var repositoryType = typeof(Hierarchy); + var repositoryName = _options!.LoggerRepository; - if (!string.IsNullOrEmpty(_options!.LoggerRepository)) + if (string.IsNullOrEmpty(repositoryName)) { - try - { - _loggerRepository = LogManager.GetRepository(_options.LoggerRepository!); - if (_options.ExternalConfigurationSetup) - { - // The logger repository is already configured. We can exit here. - return; - } - } - catch (log4net.Core.LogException) - { - // The logger repository is not defined outside the extension. - _loggerRepository = null; - } + _loggerRepository = LogManager.CreateRepository(assembly, repositoryType); + return; + } - _loggerRepository ??= - LogManager.CreateRepository(_options.LoggerRepository!, repositoryType); + if (TryUseExistingRepository(repositoryName!)) + { + return; + } + + _loggerRepository ??= LogManager.CreateRepository(repositoryName!, repositoryType); + } + + /// Attempts to use an existing log4net repository for the configured repository name. + /// The repository name to resolve. + /// true when the repository was found and external configuration means no further setup is required; otherwise, false. + private bool TryUseExistingRepository(string repositoryName) + { + try + { + _loggerRepository = LogManager.GetRepository(repositoryName); + return _options!.ExternalConfigurationSetup; } - else + catch (LogException) { - _loggerRepository = - LogManager.CreateRepository(assembly, repositoryType); + _loggerRepository = null; + return false; } } } diff --git a/src/Extensions.Logging.Log4Net/Log4NetProviderOptions.cs b/src/Extensions.Logging.Log4Net/Log4NetProviderOptions.cs index 43dde6e..b6a44e9 100644 --- a/src/Extensions.Logging.Log4Net/Log4NetProviderOptions.cs +++ b/src/Extensions.Logging.Log4Net/Log4NetProviderOptions.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Collections.Generic; @@ -9,9 +9,7 @@ namespace ReactiveMarbles.Extensions.Logging; -/// -/// The log4Net provider options. -/// +/// The log4Net provider options. /// /// Initializes a new instance of the class with the specified configuration file name and. /// watch setting. @@ -20,80 +18,52 @@ namespace ReactiveMarbles.Extensions.Logging; /// true to enable watching the configuration file for changes; otherwise, false. public sealed class Log4NetProviderOptions(string log4NetConfigFileName, bool watch) { - /// - /// The default log4 net file name. - /// + /// The default log4 net file name. private const string DefaultLog4NetFileName = "log4net.config"; - /// - /// Initializes a new instance of the class. - /// + /// Initializes a new instance of the class. public Log4NetProviderOptions() : this(DefaultLog4NetFileName) { } - /// - /// Initializes a new instance of the class. - /// + /// Initializes a new instance of the class. /// Name of the log4 net configuration file. public Log4NetProviderOptions(string log4NetConfigFileName) : this(log4NetConfigFileName, false) { } - /// - /// Gets or sets the name. - /// + /// Gets or sets the name. public string Name { get; set; } = string.Empty; - /// - /// Gets or sets the name of the log file. - /// + /// Gets or sets the name of the log file. public string Log4NetConfigFileName { get; set; } = log4NetConfigFileName; - /// - /// Gets or sets the logger repository. - /// + /// Gets or sets the logger repository. public string? LoggerRepository { get; set; } - /// - /// Gets or sets the level value that should be used to override default's critical level. - /// + /// Gets or sets the level value that should be used to override default's critical level. public string OverrideCriticalLevelWith { get; set; } = string.Empty; - /// - /// Gets or sets the property overrides. - /// - public List PropertyOverrides { get; set; } = new List(); + /// Gets or sets the property overrides. + public List PropertyOverrides { get; set; } = new(); - /// - /// Gets or sets a value indicating whether this is watch. - /// + /// Gets or sets a value indicating whether this is watch. public bool Watch { get; set; } = watch; - /// - /// Gets or sets a value indicating whether let user setup log4net externally. - /// + /// Gets or sets a value indicating whether let user setup log4net externally. public bool ExternalConfigurationSetup { get; set; } - /// - /// Gets or sets a value indicating whether let user setup log4net from web.config / app.config. - /// + /// Gets or sets a value indicating whether let user setup log4net from web.config / app.config. public bool UseWebOrAppConfig { get; set; } - /// - /// Gets or sets the assembly used to create the log4net repository identity when is not provided. - /// + /// Gets or sets the assembly used to create the log4net repository identity when is not provided. public Assembly? ConfigurationAssembly { get; set; } - /// - /// Gets or sets the factory for the log4net . - /// + /// Gets or sets the factory for the log4net . public ILog4NetLoggingEventFactory? LoggingEventFactory { get; set; } - /// - /// Gets or sets the translator between the and the log4net . - /// + /// Gets or sets the translator between the and the log4net . public ILog4NetLogLevelTranslator? LogLevelTranslator { get; set; } } diff --git a/src/Extensions.Logging.Log4Net/Scope/NullScope.cs b/src/Extensions.Logging.Log4Net/Scope/NullScope.cs index 9453daf..2070a15 100644 --- a/src/Extensions.Logging.Log4Net/Scope/NullScope.cs +++ b/src/Extensions.Logging.Log4Net/Scope/NullScope.cs @@ -1,27 +1,20 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; namespace ReactiveMarbles.Extensions.Logging.Log4Net.Scope; -/// -/// A logger scope that does not save any information and does not need to be disposed. -/// +/// A logger scope that does not save any information and does not need to be disposed. internal sealed class NullScope : IDisposable { - /// - /// Initializes a new instance of the class. - /// Constructor that prevents external instantiation. - /// + /// Initializes a new instance of the class. Constructor that prevents external instantiation. private NullScope() { } - /// - /// Gets the singleton instance that represent every . - /// + /// Gets the singleton instance that represent every . internal static NullScope Instance { get; } = new NullScope(); /// diff --git a/src/Extensions.Logging.Log4Net/Scope/NullScopeProvider.cs b/src/Extensions.Logging.Log4Net/Scope/NullScopeProvider.cs index f1196fb..45b0998 100644 --- a/src/Extensions.Logging.Log4Net/Scope/NullScopeProvider.cs +++ b/src/Extensions.Logging.Log4Net/Scope/NullScopeProvider.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -7,22 +7,15 @@ namespace ReactiveMarbles.Extensions.Logging.Log4Net.Scope; -/// -/// A that will not save nor return scopes. -/// +/// A that will not save nor return scopes. internal sealed class NullScopeProvider : IExternalScopeProvider { - /// - /// Initializes a new instance of the class. - /// Constructor that prevents external instantiation. - /// + /// Initializes a new instance of the class. Constructor that prevents external instantiation. private NullScopeProvider() { } - /// - /// Gets the singleton instance that represents every . - /// + /// Gets the singleton instance that represents every . internal static NullScopeProvider Instance { get; } = new NullScopeProvider(); /// diff --git a/src/examples/Extensions.Hosting.Avalonia.Example/App.axaml.cs b/src/examples/Extensions.Hosting.Avalonia.Example/App.axaml.cs index ccac2e3..fb07a96 100644 --- a/src/examples/Extensions.Hosting.Avalonia.Example/App.axaml.cs +++ b/src/examples/Extensions.Hosting.Avalonia.Example/App.axaml.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Avalonia; @@ -8,18 +8,13 @@ namespace Extensions.Hosting.Avalonia.Example; -/// -/// Represents the main application class for the Avalonia application. Provides the entry point for application -/// initialization and resource loading. -/// +/// Represents the main application class for the Avalonia application. Provides the entry point for application initialization and resource loading. /// Overrides the Initialize method to load XAML resources required for the application's user interface. /// Ensure that all necessary XAML files are included in the project to enable proper resource loading and application /// startup. public class App : Application { - /// - /// Initializes the component by loading its associated XAML resources. - /// + /// Initializes the component by loading its associated XAML resources. /// This method should be called during the component's initialization phase to ensure that all /// XAML-defined resources and controls are properly loaded and available for use. public override void Initialize() => AvaloniaXamlLoader.Load(this); diff --git a/src/examples/Extensions.Hosting.Avalonia.Example/Extensions.Hosting.Avalonia.Example.csproj b/src/examples/Extensions.Hosting.Avalonia.Example/Extensions.Hosting.Avalonia.Example.csproj index 66ff73e..708269c 100644 --- a/src/examples/Extensions.Hosting.Avalonia.Example/Extensions.Hosting.Avalonia.Example.csproj +++ b/src/examples/Extensions.Hosting.Avalonia.Example/Extensions.Hosting.Avalonia.Example.csproj @@ -2,16 +2,16 @@ WinExe - net8.0;net9.0;net10.0 + net8.0;net9.0;net10.0;net11.0 AnyCPU;x64 false - - - - + + + + diff --git a/src/examples/Extensions.Hosting.Avalonia.Example/HostBuilderExtensions.cs b/src/examples/Extensions.Hosting.Avalonia.Example/HostBuilderExtensions.cs new file mode 100644 index 0000000..81221ea --- /dev/null +++ b/src/examples/Extensions.Hosting.Avalonia.Example/HostBuilderExtensions.cs @@ -0,0 +1,63 @@ +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.IO; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Extensions.Hosting.Avalonia.Example; + +/// Provides host builder extension methods for the sample Avalonia application. +public static class HostBuilderExtensions +{ + /// Stores the app settings file prefix value. + private const string AppSettingsFilePrefix = "appsettings"; + + /// Stores the host settings file value. + private const string HostSettingsFile = "hostsettings.json"; + + /// Stores the prefix value. + private const string Prefix = "PREFIX_"; + + /// Provides extension members for this receiver. + /// The receiver instance. + extension(IHostBuilder hostBuilder) + { + /// Configures logging for the host builder using settings from the application's configuration. + /// This method adds logging configuration from the "Logging" section of the application's + /// configuration and enables both console and debug logging providers. + /// The same host builder instance, enabling method chaining. + internal IHostBuilder ConfigureLogging() => + hostBuilder.ConfigureLogging((hostContext, configLogging) => + configLogging + .AddConfiguration(hostContext.Configuration.GetSection("Logging")) + .AddConsole() + .AddDebug()); + + /// Configures the host and application settings for the host builder using JSON files, environment variables, and command-line arguments. + /// This method sets the base path for configuration files to the current directory and + /// supports optional JSON files for both host and application settings based on the hosting + /// environment. + /// An array of command-line arguments that can influence the configuration of the application. + /// The configured host builder instance, allowing for further configuration or execution. + internal IHostBuilder ConfigureConfiguration(string[] args) => + hostBuilder.ConfigureHostConfiguration(configHost => + configHost.SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile(HostSettingsFile, optional: true) + .AddEnvironmentVariables(prefix: Prefix) + .AddCommandLine(args)) + .ConfigureAppConfiguration((hostContext, configApp) => + { + _ = configApp.AddJsonFile(AppSettingsFilePrefix + ".json", optional: true); + if (!string.IsNullOrEmpty(hostContext.HostingEnvironment.EnvironmentName)) + { + _ = configApp.AddJsonFile(AppSettingsFilePrefix + $".{hostContext.HostingEnvironment.EnvironmentName}.json", optional: true); + } + + _ = configApp.AddEnvironmentVariables(prefix: Prefix) + .AddCommandLine(args); + }); + } +} diff --git a/src/examples/Extensions.Hosting.Avalonia.Example/MainWindow.axaml.cs b/src/examples/Extensions.Hosting.Avalonia.Example/MainWindow.axaml.cs index 89bfbc3..1249178 100644 --- a/src/examples/Extensions.Hosting.Avalonia.Example/MainWindow.axaml.cs +++ b/src/examples/Extensions.Hosting.Avalonia.Example/MainWindow.axaml.cs @@ -1,7 +1,8 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using System; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; @@ -11,16 +12,17 @@ namespace Extensions.Hosting.Avalonia.Example; -/// -/// A simple example Avalonia window. -/// +/// A simple example Avalonia window. public partial class MainWindow : Window, IAvaloniaShell { + /// Logs exit button clicks. + private static readonly Action ExitButtonClicked = + LoggerMessage.Define(LogLevel.Information, new EventId(1, nameof(ExitButton_Click)), "Exit-Button was clicked!"); + + /// Stores the logger value. private readonly ILogger _logger; - /// - /// Initializes a new instance of the class and sets up the user interface and event handlers. - /// + /// Initializes a new instance of the class and sets up the user interface and event handlers. /// This constructor initializes the UI components and attaches the Click event handler for the /// Exit button, enabling the application to close when the button is clicked. /// The logger used to record events and errors within the MainWindow. @@ -34,14 +36,19 @@ public MainWindow(ILogger logger) exitButton?.Click += ExitButton_Click; } - private void ExitButton_Click(object? sender, RoutedEventArgs e) + /// Handles exit button clicks. + /// The event sender. + /// The event arguments. + protected void ExitButton_Click(object? sender, RoutedEventArgs e) { - _logger.LogInformation("Exit-Button was clicked!"); + ExitButtonClicked(_logger, null); // Close the application - if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) + if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime lifetime) { - lifetime.Shutdown(); + return; } + + lifetime.Shutdown(); } } diff --git a/src/examples/Extensions.Hosting.Avalonia.Example/Program.cs b/src/examples/Extensions.Hosting.Avalonia.Example/Program.cs index 28b781c..55cd0e9 100644 --- a/src/examples/Extensions.Hosting.Avalonia.Example/Program.cs +++ b/src/examples/Extensions.Hosting.Avalonia.Example/Program.cs @@ -1,35 +1,22 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; -using System.IO; using Avalonia; -using Avalonia.Controls; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; using ReactiveMarbles.Extensions.Hosting.Avalonia; namespace Extensions.Hosting.Avalonia.Example; -/// -/// Provides the entry point for the application and contains methods for configuring the host, logging, and application -/// settings. -/// +/// Provides the entry point for the application and contains methods for configuring the host, logging, and application settings. /// The Program class is responsible for initializing and starting the Avalonia application using a /// generic host. It configures logging, application configuration sources, and sets up the Avalonia application /// lifecycle, including the main window. This class is intended to be the starting point for the application and should /// not be instantiated. public static class Program { - private const string AppSettingsFilePrefix = "appsettings"; - private const string HostSettingsFile = "hostsettings.json"; - private const string Prefix = "PREFIX_"; - - /// - /// Initializes and runs the Avalonia application with configured logging, application settings, and main window. - /// + /// Initializes and runs the Avalonia application with configured logging, application settings, and main window. /// This method sets up the application host with logging, configuration, and Avalonia-specific /// settings, including the main application and window. The application runs until it is closed or /// terminated. @@ -44,10 +31,10 @@ public static int Main(string[] args) .ConfigureAvalonia(avaloniaBuilder => { // Use the custom application - avaloniaBuilder.UseApplication(); + _ = avaloniaBuilder.UseApplication(); // Use the main window - avaloniaBuilder.UseWindow(); + _ = avaloniaBuilder.UseWindow(); }) .UseAvaloniaLifetime() .UseConsoleLifetime() @@ -59,46 +46,4 @@ public static int Main(string[] args) return 0; } - - /// - /// Configures logging for the host builder using settings from the application's configuration. - /// - /// This method adds logging configuration from the "Logging" section of the application's - /// configuration and enables both console and debug logging providers. - /// The host builder to configure logging for. This parameter cannot be null. - /// The same host builder instance, enabling method chaining. - private static IHostBuilder ConfigureLogging(this IHostBuilder hostBuilder) => - hostBuilder.ConfigureLogging((hostContext, configLogging) => - configLogging - .AddConfiguration(hostContext.Configuration.GetSection("Logging")) - .AddConsole() - .AddDebug()); - - /// - /// Configures the host and application settings for the host builder using JSON files, environment variables, - /// and command-line arguments. - /// - /// This method sets the base path for configuration files to the current directory and - /// supports optional JSON files for both host and application settings based on the hosting - /// environment. - /// The host builder instance to configure with the specified settings. - /// An array of command-line arguments that can influence the configuration of the application. - /// The configured host builder instance, allowing for further configuration or execution. - private static IHostBuilder ConfigureConfiguration(this IHostBuilder hostBuilder, string[] args) => - hostBuilder.ConfigureHostConfiguration(configHost => - configHost.SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile(HostSettingsFile, optional: true) - .AddEnvironmentVariables(prefix: Prefix) - .AddCommandLine(args)) - .ConfigureAppConfiguration((hostContext, configApp) => - { - configApp.AddJsonFile(AppSettingsFilePrefix + ".json", optional: true); - if (!string.IsNullOrEmpty(hostContext.HostingEnvironment.EnvironmentName)) - { - configApp.AddJsonFile(AppSettingsFilePrefix + $".{hostContext.HostingEnvironment.EnvironmentName}.json", optional: true); - } - - configApp.AddEnvironmentVariables(prefix: Prefix) - .AddCommandLine(args); - }); } diff --git a/src/examples/Extensions.Hosting.Avalonia.Example/ViewLocator.cs b/src/examples/Extensions.Hosting.Avalonia.Example/ViewLocator.cs index 951d825..b74af52 100644 --- a/src/examples/Extensions.Hosting.Avalonia.Example/ViewLocator.cs +++ b/src/examples/Extensions.Hosting.Avalonia.Example/ViewLocator.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; @@ -8,32 +8,27 @@ namespace Dapplo.Hosting.Sample.AvaloniaDemo; -/// -/// A simple ViewLocator to locate views based on ViewModel type names. -/// This is optional and can be used if you follow MVVM pattern. -/// +/// A simple ViewLocator to locate views based on ViewModel type names. This is optional and can be used if you follow MVVM pattern. public class ViewLocator : IDataTemplate { - /// - /// Creates a new instance of a Control that corresponds to the type of the provided data object. - /// + /// Creates a new instance of a Control that corresponds to the type of the provided data object. /// This method infers the Control type by replacing 'ViewModel' with 'View' in the data object's /// type name. If the type cannot be resolved or instantiation fails, a TextBlock with an error message is returned /// instead. - /// The data object used to determine which Control type to instantiate. Cannot be null. + /// The data object used to determine which Control type to instantiate. Cannot be null. /// A Control instance that matches the type of the data object, or null if the data is null. If the corresponding /// Control type cannot be found or instantiated, returns a TextBlock indicating the error. - public Control? Build(object? data) + public Control? Build(object? param) { - if (data is null) + if (param is null) { return null; } - var name = data.GetType().FullName!.Replace("ViewModel", "View"); + var name = param.GetType().FullName!.Replace("ViewModel", "View"); var type = Type.GetType(name); - if (type != null) + if (type is not null) { try { @@ -48,9 +43,7 @@ public class ViewLocator : IDataTemplate return new TextBlock { Text = "Not Found: " + name }; } - /// - /// Determines whether the specified data object is not null. - /// + /// Determines whether the specified data object is not null. /// The data object to evaluate for nullity. This parameter can be null. /// true if the data object is not null; otherwise, false. public bool Match(object? data) => data is { }; diff --git a/src/examples/Extensions.Hosting.Console.Example/Extensions.Hosting.Console.Example.csproj b/src/examples/Extensions.Hosting.Console.Example/Extensions.Hosting.Console.Example.csproj index 45b5950..9f3a049 100644 --- a/src/examples/Extensions.Hosting.Console.Example/Extensions.Hosting.Console.Example.csproj +++ b/src/examples/Extensions.Hosting.Console.Example/Extensions.Hosting.Console.Example.csproj @@ -2,7 +2,7 @@ Exe - net10.0-windows;net481 + net10.0-windows;net11.0-windows;net481 x64 true enable diff --git a/src/examples/Extensions.Hosting.Console.Example/Program.cs b/src/examples/Extensions.Hosting.Console.Example/Program.cs index d80a204..a7feae4 100644 --- a/src/examples/Extensions.Hosting.Console.Example/Program.cs +++ b/src/examples/Extensions.Hosting.Console.Example/Program.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using ReactiveMarbles.Extensions.Hosting.PluginService; diff --git a/src/examples/Extensions.Hosting.Maui.Example/App.xaml.cs b/src/examples/Extensions.Hosting.Maui.Example/App.xaml.cs index 7d4a25a..ad49874 100644 --- a/src/examples/Extensions.Hosting.Maui.Example/App.xaml.cs +++ b/src/examples/Extensions.Hosting.Maui.Example/App.xaml.cs @@ -1,29 +1,23 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. namespace Extensions.Hosting.Maui.Example; -/// -/// App. -/// +/// Represents the sample MAUI application. /// public partial class App : Application { - /// - /// Initializes a new instance of the class. - /// + /// Initializes a new instance of the class. public App() { InitializeComponent(); // Start the host - MauiProgram.HostApp.StartAsync(); + _ = MauiProgram.HostApp.StartAsync(); } - /// - /// Creates the window. - /// + /// Creates the window. /// State of the activation. /// A Window. protected override Window CreateWindow(IActivationState? activationState) => new(new AppShell()); diff --git a/src/examples/Extensions.Hosting.Maui.Example/AppShell.xaml.cs b/src/examples/Extensions.Hosting.Maui.Example/AppShell.xaml.cs index 67ac278..5bc221d 100644 --- a/src/examples/Extensions.Hosting.Maui.Example/AppShell.xaml.cs +++ b/src/examples/Extensions.Hosting.Maui.Example/AppShell.xaml.cs @@ -1,17 +1,13 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. namespace Extensions.Hosting.Maui.Example; -/// -/// AppShell. -/// +/// Represents the sample MAUI shell. /// public partial class AppShell : Shell { - /// - /// Initializes a new instance of the class. - /// + /// Initializes a new instance of the class. public AppShell() => InitializeComponent(); } diff --git a/src/examples/Extensions.Hosting.Maui.Example/ExampleMauiService.cs b/src/examples/Extensions.Hosting.Maui.Example/ExampleMauiService.cs index 9abf93d..5288158 100644 --- a/src/examples/Extensions.Hosting.Maui.Example/ExampleMauiService.cs +++ b/src/examples/Extensions.Hosting.Maui.Example/ExampleMauiService.cs @@ -1,23 +1,25 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using Microsoft.Extensions.Logging; using ReactiveMarbles.Extensions.Hosting.Maui; +using Application = Microsoft.Maui.Controls.Application; namespace Extensions.Hosting.Maui.Example; -/// -/// Example MAUI service. -/// -public class ExampleMauiService : IMauiService +/// Example MAUI service. +/// The logger used to record service initialization. +public class ExampleMauiService(ILogger logger) : IMauiService { - /// - /// Initializes the specified application. - /// + /// Logs that the MAUI application was initialized. + private static readonly Action ApplicationInitialized = + LoggerMessage.Define(LogLevel.Information, new EventId(1, nameof(ApplicationInitialized)), "MAUI application initialized via IMauiService"); + + /// Initializes the specified application. /// The application. - public void Initialize(Microsoft.Maui.Controls.Application application) + public void Initialize(Application application) { - // Example initialization - Console.WriteLine("MAUI application initialized via IMauiService"); + ApplicationInitialized(logger, null); } } diff --git a/src/examples/Extensions.Hosting.Maui.Example/Extensions.Hosting.Maui.Example.csproj b/src/examples/Extensions.Hosting.Maui.Example/Extensions.Hosting.Maui.Example.csproj index 7af9c08..d52ff40 100644 --- a/src/examples/Extensions.Hosting.Maui.Example/Extensions.Hosting.Maui.Example.csproj +++ b/src/examples/Extensions.Hosting.Maui.Example/Extensions.Hosting.Maui.Example.csproj @@ -1,9 +1,9 @@ - + false - net10.0-android; - $(TargetFrameworks);net10.0-windows10.0.19041.0 + net10.0-android;net11.0-android; + $(TargetFrameworks);net10.0-windows10.0.19041.0;net11.0-windows10.0.19041.0 @@ -30,16 +30,7 @@ - - - - - - - - - diff --git a/src/tests/Extensions.Hosting.Tests/HostBuilderApplicationExtensionsTests.cs b/src/tests/Extensions.Hosting.Tests/HostBuilderApplicationExtensionsTests.cs index 9a9c571..301033a 100644 --- a/src/tests/Extensions.Hosting.Tests/HostBuilderApplicationExtensionsTests.cs +++ b/src/tests/Extensions.Hosting.Tests/HostBuilderApplicationExtensionsTests.cs @@ -1,21 +1,18 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging.Abstractions; using ReactiveMarbles.Extensions.Hosting.AppServices; namespace Extensions.Hosting.Tests; -/// -/// Contains unit tests for the HostBuilderApplicationExtensions class (SingleInstance functionality). -/// +/// Contains unit tests for the HostBuilderApplicationExtensions class (SingleInstance functionality). public class HostBuilderApplicationExtensionsTests { - /// - /// Verifies that ConfigureSingleInstance with IHostBuilder throws when hostBuilder is null. - /// + /// Verifies that ConfigureSingleInstance with IHostBuilder throws when hostBuilder is null. /// A task that represents the asynchronous test operation. [Test] public async Task ConfigureSingleInstance_IHostBuilder_WithNullHostBuilder_ThrowsArgumentNullException() @@ -25,9 +22,7 @@ public async Task ConfigureSingleInstance_IHostBuilder_WithNullHostBuilder_Throw await Assert.That(act).Throws(); } - /// - /// Verifies that ConfigureSingleInstance with IHostApplicationBuilder throws when hostBuilder is null. - /// + /// Verifies that ConfigureSingleInstance with IHostApplicationBuilder throws when hostBuilder is null. /// A task that represents the asynchronous test operation. [Test] public async Task ConfigureSingleInstance_IHostApplicationBuilder_WithNullHostBuilder_ThrowsArgumentNullException() @@ -37,9 +32,7 @@ public async Task ConfigureSingleInstance_IHostApplicationBuilder_WithNullHostBu await Assert.That(act).Throws(); } - /// - /// Verifies that ConfigureSingleInstance with mutexId returns the same IHostBuilder for chaining. - /// + /// Verifies that ConfigureSingleInstance with mutexId returns the same IHostBuilder for chaining. /// A task that represents the asynchronous test operation. [Test] public async Task ConfigureSingleInstance_WithMutexId_ReturnsHostBuilder() @@ -50,9 +43,45 @@ public async Task ConfigureSingleInstance_WithMutexId_ReturnsHostBuilder() await Assert.That(result).IsEqualTo(hostBuilder); } - /// - /// Verifies that ConfigureSingleInstance with configure action returns the same IHostBuilder for chaining. - /// + /// Verifies that ConfigureSingleInstance with IHostApplicationBuilder returns the same builder and registers the mutex builder. + /// A task that represents the asynchronous test operation. + [Test] + public async Task ConfigureSingleInstance_IHostApplicationBuilder_WithMutexId_ReturnsBuilderAndRegistersMutexBuilder() + { + var hostBuilder = Host.CreateApplicationBuilder(); + var mutexId = "test-mutex-" + Guid.NewGuid().ToString("N"); + + var result = hostBuilder.ConfigureSingleInstance(mutexId); + + await Assert.That(result).IsEqualTo(hostBuilder); + await using var serviceProvider = hostBuilder.Services.BuildServiceProvider(); + var mutexBuilder = serviceProvider.GetRequiredService(); + await Assert.That(mutexBuilder.MutexId).IsEqualTo(mutexId); + } + + /// Verifies that multiple IHostApplicationBuilder calls reuse the same mutex builder. + /// A task that represents the asynchronous test operation. + [Test] + public async Task ConfigureSingleInstance_IHostApplicationBuilder_MultipleCalls_UsesSameMutexBuilder() + { + IMutexBuilder? firstBuilder = null; + IMutexBuilder? secondBuilder = null; + var hostBuilder = Host.CreateApplicationBuilder(); + + _ = hostBuilder.ConfigureSingleInstance(builder => + { + firstBuilder = builder; + builder.MutexId = "test-mutex-" + Guid.NewGuid().ToString("N"); + }); + + _ = hostBuilder.ConfigureSingleInstance(builder => secondBuilder = builder); + + await Assert.That(firstBuilder).IsNotNull(); + await Assert.That(secondBuilder).IsNotNull(); + await Assert.That(firstBuilder).IsEqualTo(secondBuilder); + } + + /// Verifies that ConfigureSingleInstance with configure action returns the same IHostBuilder for chaining. /// A task that represents the asynchronous test operation. [Test] public async Task ConfigureSingleInstance_WithConfigureAction_ReturnsHostBuilder() @@ -67,16 +96,14 @@ public async Task ConfigureSingleInstance_WithConfigureAction_ReturnsHostBuilder await Assert.That(result).IsEqualTo(hostBuilder); } - /// - /// Verifies that ConfigureSingleInstance configure action is invoked. - /// + /// Verifies that ConfigureSingleInstance configure action is invoked. /// A task that represents the asynchronous test operation. [Test] public async Task ConfigureSingleInstance_ConfigureActionIsInvoked() { var wasInvoked = false; var hostBuilder = Host.CreateDefaultBuilder(); - hostBuilder.ConfigureSingleInstance(builder => + _ = hostBuilder.ConfigureSingleInstance(builder => { wasInvoked = true; builder.MutexId = "test-mutex-" + Guid.NewGuid().ToString("N"); @@ -87,24 +114,20 @@ public async Task ConfigureSingleInstance_ConfigureActionIsInvoked() await Assert.That(wasInvoked).IsTrue(); } - /// - /// Verifies that ConfigureSingleInstance registers the MutexBuilder service. - /// + /// Verifies that ConfigureSingleInstance registers the MutexBuilder service. /// A task that represents the asynchronous test operation. [Test] public async Task ConfigureSingleInstance_RegistersMutexBuilder() { var hostBuilder = Host.CreateDefaultBuilder(); - hostBuilder.ConfigureSingleInstance(builder => builder.MutexId = "test-mutex-" + Guid.NewGuid().ToString("N")); + _ = hostBuilder.ConfigureSingleInstance(builder => builder.MutexId = "test-mutex-" + Guid.NewGuid().ToString("N")); using var host = hostBuilder.Build(); var mutexBuilder = host.Services.GetService(); await Assert.That(mutexBuilder).IsNotNull(); } - /// - /// Verifies that multiple calls to ConfigureSingleInstance use the same MutexBuilder instance. - /// + /// Verifies that multiple calls to ConfigureSingleInstance use the same MutexBuilder instance. /// A task that represents the asynchronous test operation. [Test] public async Task ConfigureSingleInstance_MultipleCalls_UsesSameMutexBuilder() @@ -113,15 +136,13 @@ public async Task ConfigureSingleInstance_MultipleCalls_UsesSameMutexBuilder() IMutexBuilder? secondBuilder = null; var hostBuilder = Host.CreateDefaultBuilder(); - hostBuilder.ConfigureSingleInstance(builder => + _ = hostBuilder.ConfigureSingleInstance(builder => { firstBuilder = builder; builder.MutexId = "test-mutex-" + Guid.NewGuid().ToString("N"); }); - hostBuilder.ConfigureSingleInstance(builder => - { - secondBuilder = builder; - }); + + _ = hostBuilder.ConfigureSingleInstance(builder => secondBuilder = builder); using var host = hostBuilder.Build(); await Assert.That(firstBuilder).IsNotNull(); @@ -129,15 +150,13 @@ public async Task ConfigureSingleInstance_MultipleCalls_UsesSameMutexBuilder() await Assert.That(firstBuilder).IsEqualTo(secondBuilder); } - /// - /// Verifies that ConfigureSingleInstance allows setting IsGlobal property. - /// + /// Verifies that ConfigureSingleInstance allows setting IsGlobal property. /// A task that represents the asynchronous test operation. [Test] public async Task ConfigureSingleInstance_CanSetIsGlobal() { var hostBuilder = Host.CreateDefaultBuilder(); - hostBuilder.ConfigureSingleInstance(builder => + _ = hostBuilder.ConfigureSingleInstance(builder => { builder.MutexId = "test-mutex-" + Guid.NewGuid().ToString("N"); builder.IsGlobal = true; @@ -148,16 +167,14 @@ public async Task ConfigureSingleInstance_CanSetIsGlobal() await Assert.That(mutexBuilder.IsGlobal).IsTrue(); } - /// - /// Verifies that ConfigureSingleInstance allows setting WhenNotFirstInstance callback. - /// + /// Verifies that ConfigureSingleInstance allows setting WhenNotFirstInstance callback. /// A task that represents the asynchronous test operation. [Test] public async Task ConfigureSingleInstance_CanSetWhenNotFirstInstanceCallback() { var callbackSet = false; var hostBuilder = Host.CreateDefaultBuilder(); - hostBuilder.ConfigureSingleInstance(builder => + _ = hostBuilder.ConfigureSingleInstance(builder => { builder.MutexId = "test-mutex-" + Guid.NewGuid().ToString("N"); builder.WhenNotFirstInstance = (_, _) => callbackSet = true; @@ -168,7 +185,52 @@ public async Task ConfigureSingleInstance_CanSetWhenNotFirstInstanceCallback() await Assert.That(mutexBuilder.WhenNotFirstInstance).IsNotNull(); // Invoke the callback to verify it was set correctly - mutexBuilder.WhenNotFirstInstance?.Invoke(null!, null!); + mutexBuilder.WhenNotFirstInstance!.Invoke(null!, null!); + await Assert.That(callbackSet).IsTrue(); + } + + /// Verifies that starting and stopping the host releases the configured single-instance mutex. + /// A task that represents the asynchronous test operation. + [Test] + public async Task ConfigureSingleInstance_StartAndStop_ReleasesMutex() + { + var mutexId = "test-mutex-" + Guid.NewGuid().ToString("N"); + using var host = Host.CreateDefaultBuilder() + .ConfigureSingleInstance(builder => + { + builder.MutexId = mutexId; + builder.IsGlobal = false; + }) + .Build(); + + await host.StartAsync(); + await host.StopAsync(); + + using var mutex = ResourceMutex.Create(NullLogger.Instance, mutexId, "verification", false); + await Assert.That(mutex.IsLocked).IsTrue(); + } + + /// Verifies that a second instance invokes the configured callback and requests application stop. + /// A task that represents the asynchronous test operation. + [Test] + public async Task ConfigureSingleInstance_StartAsync_WhenMutexAlreadyLocked_InvokesCallback() + { + var callbackSet = false; + var mutexId = "test-mutex-" + Guid.NewGuid().ToString("N"); + using var primaryMutex = ResourceMutex.Create(NullLogger.Instance, mutexId, "primary", false); + using var host = Host.CreateDefaultBuilder() + .ConfigureSingleInstance(builder => + { + builder.MutexId = mutexId; + builder.IsGlobal = false; + builder.WhenNotFirstInstance = (_, _) => callbackSet = true; + }) + .Build(); + + await host.StartAsync(); + await host.StopAsync(); + + await Assert.That(primaryMutex.IsLocked).IsTrue(); await Assert.That(callbackSet).IsTrue(); } } diff --git a/src/tests/Extensions.Hosting.Tests/HostBuilderPluginExtensionsTests.cs b/src/tests/Extensions.Hosting.Tests/HostBuilderPluginExtensionsTests.cs new file mode 100644 index 0000000..3cdd6d1 --- /dev/null +++ b/src/tests/Extensions.Hosting.Tests/HostBuilderPluginExtensionsTests.cs @@ -0,0 +1,320 @@ +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.IO; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using ReactiveMarbles.Extensions.Hosting.Plugins; + +namespace Extensions.Hosting.Tests; + +/// Contains tests for host builder plugin configuration extensions. +public class HostBuilderPluginExtensionsTests +{ + /// Verifies that ConfigurePlugins with a null IHostBuilder throws. + /// A task that represents the asynchronous test operation. + [Test] + public async Task ConfigurePlugins_IHostBuilder_WithNullHostBuilder_ThrowsArgumentNullException() + { + IHostBuilder? hostBuilder = null; + var act = () => hostBuilder!.ConfigurePlugins(_ => { }); + await Assert.That(act).Throws(); + } + + /// Verifies that ConfigurePlugins with a null IHostApplicationBuilder throws. + /// A task that represents the asynchronous test operation. + [Test] + public async Task ConfigurePlugins_IHostApplicationBuilder_WithNullHostBuilder_ThrowsArgumentNullException() + { + IHostApplicationBuilder? hostBuilder = null; + var act = () => hostBuilder!.ConfigurePlugins(_ => { }); + await Assert.That(act).Throws(); + } + + /// Verifies that IHostBuilder plugin configuration scans and configures ordered plugins. + /// A task that represents the asynchronous test operation. + [Test] + public async Task ConfigurePlugins_IHostBuilder_LoadsConfiguredPluginsInOrder() + { + var configuredPlugins = new List(); + var hostBuilder = Host.CreateDefaultBuilder(); + + _ = hostBuilder.ConfigurePlugins(builder => + { + ArgumentNullException.ThrowIfNull(builder); + AddCurrentAssemblyAsFramework(builder); + builder.AssemblyScanFunc = _ => + [ + new LaterRecordingPlugin(configuredPlugins), + null, + new EarlierRecordingPlugin(configuredPlugins), + ]; + }); + + using var host = hostBuilder.Build(); + + await Assert.That(configuredPlugins.Count).IsEqualTo(2); + await Assert.That(configuredPlugins[0]).IsEqualTo(EarlierRecordingPlugin.Name); + await Assert.That(configuredPlugins[1]).IsEqualTo(LaterRecordingPlugin.Name); + } + + /// Verifies that IHostApplicationBuilder applies caller configuration before scanning for plugins. + /// A task that represents the asynchronous test operation. + [Test] + public async Task ConfigurePlugins_IHostApplicationBuilder_AppliesConfigurationBeforeScanning() + { + var configuredPlugins = new List(); + var hostBuilder = Host.CreateApplicationBuilder(); + + var result = hostBuilder.ConfigurePlugins(builder => + { + ArgumentNullException.ThrowIfNull(builder); + AddCurrentAssemblyAsFramework(builder); + builder.AssemblyScanFunc = _ => [new EarlierRecordingPlugin(configuredPlugins)]; + }); + + await Assert.That(result).IsEqualTo(hostBuilder); + await Assert.That(configuredPlugins.Count).IsEqualTo(1); + await Assert.That(configuredPlugins[0]).IsEqualTo(EarlierRecordingPlugin.Name); + } + + /// Verifies that repeated IHostBuilder plugin configuration calls reuse the same builder instance. + /// A task that represents the asynchronous test operation. + [Test] + public async Task ConfigurePlugins_IHostBuilder_MultipleCalls_ReusePluginBuilder() + { + IPluginBuilder? firstBuilder = null; + IPluginBuilder? secondBuilder = null; + var hostBuilder = Host.CreateDefaultBuilder(); + + _ = hostBuilder.ConfigurePlugins(builder => firstBuilder = builder); + _ = hostBuilder.ConfigurePlugins(builder => secondBuilder = builder); + + await Assert.That(firstBuilder).IsNotNull(); + await Assert.That(secondBuilder).IsNotNull(); + await Assert.That(firstBuilder).IsEqualTo(secondBuilder); + } + + /// Verifies that content-root scanning can provide the assembly used for plugin discovery. + /// A task that represents the asynchronous test operation. + [Test] + public async Task ConfigurePlugins_IHostBuilder_WithContentRootScan_LoadsConfiguredPlugin() + { + var configuredPlugins = new List(); + var assemblyPath = typeof(HostBuilderPluginExtensionsTests).Assembly.Location; + var hostBuilder = Host.CreateDefaultBuilder() + .UseContentRoot(Path.GetDirectoryName(assemblyPath)!); + + _ = hostBuilder.ConfigurePlugins(builder => + { + ArgumentNullException.ThrowIfNull(builder); + builder.UseContentRoot = true; + _ = builder.FrameworkMatcher.AddInclude(Path.GetFileName(assemblyPath)); + builder.AssemblyScanFunc = _ => [new EarlierRecordingPlugin(configuredPlugins)]; + }); + + using var host = hostBuilder.Build(); + + await Assert.That(configuredPlugins.Count).IsEqualTo(1); + await Assert.That(configuredPlugins[0]).IsEqualTo(EarlierRecordingPlugin.Name); + } + + /// Verifies that invalid plugin files discovered by the plugin matcher are ignored. + /// A task that represents the asynchronous test operation. + [Test] + public async Task ConfigurePlugins_IHostBuilder_WithInvalidPluginFile_IgnoresPlugin() + { + var tempDirectory = CreateTemporaryDirectory(); + try + { + var pluginPath = Path.Combine(tempDirectory, "InvalidPlugin.dll"); + await File.WriteAllTextAsync(pluginPath, string.Empty); + var hostBuilder = Host.CreateDefaultBuilder(); + + _ = hostBuilder.ConfigurePlugins(builder => + { + ArgumentNullException.ThrowIfNull(builder); + builder.PluginDirectories.Add(tempDirectory); + _ = builder.PluginMatcher.AddInclude(Path.GetFileName(pluginPath)); + builder.ValidatePlugin = _ => false; + }); + + using var host = hostBuilder.Build(); + await Assert.That(host).IsNotNull(); + } + finally + { + Directory.Delete(tempDirectory, recursive: true); + } + } + + /// Verifies that plugin files already loaded in the default context are ignored. + /// A task that represents the asynchronous test operation. + [Test] + public async Task ConfigurePlugins_IHostBuilder_WithAlreadyLoadedPluginFile_IgnoresPlugin() + { + var assemblyPath = typeof(HostBuilderPluginExtensionsTests).Assembly.Location; + var hostBuilder = Host.CreateDefaultBuilder(); + + _ = hostBuilder.ConfigurePlugins(builder => + { + ArgumentNullException.ThrowIfNull(builder); + builder.PluginDirectories.Add(Path.GetDirectoryName(assemblyPath)!); + _ = builder.PluginMatcher.AddInclude(Path.GetFileName(assemblyPath)); + }); + + using var host = hostBuilder.Build(); + await Assert.That(host).IsNotNull(); + } + + /// Verifies that framework assemblies not yet loaded in the default context are loaded and scanned. + /// A task that represents the asynchronous test operation. + [Test] + public async Task ConfigurePlugins_IHostBuilder_WithUnloadedFrameworkAssembly_LoadsAndScansAssembly() + { + var configuredPlugins = new List(); + var assemblyPath = GetUnloadedAssemblyPath(); + var assemblyName = Path.GetFileNameWithoutExtension(assemblyPath); + var hostBuilder = Host.CreateDefaultBuilder(); + + _ = hostBuilder.ConfigurePlugins(builder => + { + ArgumentNullException.ThrowIfNull(builder); + builder.FrameworkDirectories.Add(Path.GetDirectoryName(assemblyPath)!); + _ = builder.FrameworkMatcher.AddInclude(Path.GetFileName(assemblyPath)); + builder.AssemblyScanFunc = assembly => + assembly.GetName().Name == assemblyName + ? [new EarlierRecordingPlugin(configuredPlugins)] + : []; + }); + + using var host = hostBuilder.Build(); + + await Assert.That(configuredPlugins.Count).IsEqualTo(1); + await Assert.That(configuredPlugins[0]).IsEqualTo(EarlierRecordingPlugin.Name); + } + + /// Verifies that plugin assemblies not yet loaded in the default context are loaded and scanned. + /// A task that represents the asynchronous test operation. + [Test] + public async Task ConfigurePlugins_IHostBuilder_WithUnloadedPluginAssembly_LoadsAndScansAssembly() + { + var configuredPlugins = new List(); + var assemblyPath = GetUnloadedAssemblyPath(); + var assemblyName = Path.GetFileNameWithoutExtension(assemblyPath); + var hostBuilder = Host.CreateDefaultBuilder(); + + _ = hostBuilder.ConfigurePlugins(builder => + { + ArgumentNullException.ThrowIfNull(builder); + builder.PluginDirectories.Add(Path.GetDirectoryName(assemblyPath)!); + _ = builder.PluginMatcher.AddInclude(Path.GetFileName(assemblyPath)); + builder.AssemblyScanFunc = assembly => + assembly.GetName().Name == assemblyName + ? [new EarlierRecordingPlugin(configuredPlugins)] + : []; + }); + + using var host = hostBuilder.Build(); + + await Assert.That(configuredPlugins.Count).IsEqualTo(1); + await Assert.That(configuredPlugins[0]).IsEqualTo(EarlierRecordingPlugin.Name); + } + + /// Verifies that required plugin configuration fails when no plugins are discovered. + /// A task that represents the asynchronous test operation. + [Test] + public async Task ConfigurePlugins_IHostBuilder_WithRequiredPluginsAndNoPlugins_ThrowsInvalidOperationException() + { + var hostBuilder = Host.CreateDefaultBuilder(); + _ = hostBuilder.ConfigurePlugins(builder => builder.RequirePlugins()); + + var act = () => hostBuilder.Build(); + await Assert.That(act).Throws(); + } + + /// Verifies that repeated IHostApplicationBuilder plugin configuration calls reuse the same builder instance. + /// A task that represents the asynchronous test operation. + [Test] + public async Task ConfigurePlugins_IHostApplicationBuilder_MultipleCalls_ReusePluginBuilder() + { + IPluginBuilder? firstBuilder = null; + IPluginBuilder? secondBuilder = null; + var hostBuilder = Host.CreateApplicationBuilder(); + + _ = hostBuilder.ConfigurePlugins(builder => firstBuilder = builder); + _ = hostBuilder.ConfigurePlugins(builder => secondBuilder = builder); + + await Assert.That(firstBuilder).IsNotNull(); + await Assert.That(secondBuilder).IsNotNull(); + await Assert.That(firstBuilder).IsEqualTo(secondBuilder); + } + + /// Adds the current test assembly to the framework scan set. + /// The plugin builder to configure. + private static void AddCurrentAssemblyAsFramework(IPluginBuilder pluginBuilder) + { + var assemblyPath = typeof(HostBuilderPluginExtensionsTests).Assembly.Location; + pluginBuilder.FrameworkDirectories.Add(Path.GetDirectoryName(assemblyPath)!); + _ = pluginBuilder.FrameworkMatcher.AddInclude(Path.GetFileName(assemblyPath)); + } + + /// Creates a temporary directory for plugin scanning tests. + /// The created temporary directory. + private static string CreateTemporaryDirectory() + { + var tempDirectory = Path.Combine(Path.GetTempPath(), "Extensions.Hosting.Tests", Guid.NewGuid().ToString("N")); + _ = Directory.CreateDirectory(tempDirectory); + return tempDirectory; + } + + /// Finds a managed assembly in the test output that has not yet been loaded. + /// The path to an unloaded managed assembly. + private static string GetUnloadedAssemblyPath() + { + var assemblyDirectory = Path.GetDirectoryName(typeof(HostBuilderPluginExtensionsTests).Assembly.Location)!; + var loadedAssemblyNames = AppDomain.CurrentDomain.GetAssemblies() + .Select(assembly => assembly.GetName().Name) + .Where(name => name is not null) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var assemblyPath in Directory.EnumerateFiles(assemblyDirectory, "*.dll")) + { + var assemblyName = Path.GetFileNameWithoutExtension(assemblyPath); + if (!loadedAssemblyNames.Contains(assemblyName) && + !assemblyName.StartsWith("Extensions.Hosting", StringComparison.OrdinalIgnoreCase)) + { + return assemblyPath; + } + } + + throw new InvalidOperationException("No unloaded assembly was available in the test output directory."); + } + + /// Records configuration with an earlier plugin order. + /// The configured plugin log. + [PluginOrder(-1)] + private sealed class EarlierRecordingPlugin(List configuredPlugins) : IPlugin + { + /// Stores the plugin name. + public const string Name = "Earlier"; + + /// + public void ConfigureHost(object hostBuilderContext, IServiceCollection serviceCollection) => + configuredPlugins.Add(Name); + } + + /// Records configuration with a later plugin order. + /// The configured plugin log. + [PluginOrder(10)] + private sealed class LaterRecordingPlugin(List configuredPlugins) : IPlugin + { + /// Stores the plugin name. + public const string Name = "Later"; + + /// + public void ConfigureHost(object hostBuilderContext, IServiceCollection serviceCollection) => + configuredPlugins.Add(Name); + } +} diff --git a/src/tests/Extensions.Hosting.Tests/HostedServiceBaseTests.cs b/src/tests/Extensions.Hosting.Tests/HostedServiceBaseTests.cs new file mode 100644 index 0000000..a2c4886 --- /dev/null +++ b/src/tests/Extensions.Hosting.Tests/HostedServiceBaseTests.cs @@ -0,0 +1,222 @@ +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging.Abstractions; +using ReactiveMarbles.Extensions.Hosting.Plugins; + +namespace Extensions.Hosting.Tests; + +/// Contains unit tests for the hosted service base lifecycle implementation. +public class HostedServiceBaseTests +{ + /// Verifies that lifetime callbacks invoke the overridable lifecycle methods. + /// A task that represents the asynchronous test operation. + [Test] + public async Task LifetimeCallbacks_InvokeLifecycleMethods() + { + using var lifetime = new TestHostApplicationLifetime(); + var service = new TestHostedService(lifetime); + + await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + lifetime.TriggerStarted(); + await service.Started.Task.WaitAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(false); + + await Assert.That(service.StartedCount).IsEqualTo(1); + await Assert.That(service.CleanupDisposable).IsNotNull(); + await Assert.That(service.CleanupDisposable!.IsDisposed).IsFalse(); + + lifetime.TriggerStopping(); + await Assert.That(service.StoppingCount).IsEqualTo(1); + await Assert.That(service.CleanupDisposable.IsDisposed).IsTrue(); + await Assert.That(service.IsDisposed).IsTrue(); + + lifetime.TriggerStopped(); + await Assert.That(service.StoppedCount).IsEqualTo(1); + } + + /// Verifies that disposing the service is idempotent and disposes cleanup resources. + /// A task that represents the asynchronous test operation. + [Test] + public async Task Dispose_IsIdempotentAndDisposesCleanup() + { + using var lifetime = new TestHostApplicationLifetime(); + var service = new TestHostedService(lifetime); + + await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + lifetime.TriggerStarted(); + await service.Started.Task.WaitAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(false); + + service.Dispose(); + service.Dispose(); + + await Assert.That(service.CleanupDisposable).IsNotNull(); + await Assert.That(service.CleanupDisposable!.IsDisposed).IsTrue(); + await Assert.That(service.IsDisposed).IsTrue(); + } + + /// Verifies that StopAsync returns a completed task. + /// A task that represents the asynchronous test operation. + [Test] + public async Task StopAsync_ReturnsCompletedTask() + { + using var lifetime = new TestHostApplicationLifetime(); + var service = new DefaultHostedService(lifetime); + + var stopTask = service.StopAsync(CancellationToken.None); + + await Assert.That(stopTask.IsCompletedSuccessfully).IsTrue(); + } + + /// Verifies that the default OnStarted implementation completes successfully. + /// A task that represents the asynchronous test operation. + [Test] + public async Task OnStarted_DefaultImplementation_CompletesSuccessfully() + { + using var lifetime = new TestHostApplicationLifetime(); + var service = new DefaultHostedService(lifetime); + + await service.OnStarted().ConfigureAwait(false); + + await Assert.That(service).IsNotNull(); + } + + /// Verifies that an OnStarted exception is observed and contained by the lifetime callback. + /// A task that represents the asynchronous test operation. + [Test] + public async Task LifetimeStarted_WhenOnStartedThrows_DoesNotThrowFromCallback() + { + using var lifetime = new TestHostApplicationLifetime(); + var service = new ThrowingStartedHostedService(lifetime); + + await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + lifetime.TriggerStarted(); + await service.Started.Task.WaitAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(false); + await Task.Delay(TimeSpan.FromMilliseconds(50)).ConfigureAwait(false); + + await Assert.That(service.StartedCount).IsEqualTo(1); + } + + /// Test hosted service used to observe lifecycle callback execution. + /// The test lifetime source. + private sealed class TestHostedService(IHostApplicationLifetime lifetime) + : HostedServiceBase(NullLogger.Instance, lifetime) + { + /// Gets the started signal. + public TaskCompletionSource Started { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); + + /// Gets the cleanup disposable. + public TrackingDisposable? CleanupDisposable { get; private set; } + + /// Gets the number of started callbacks. + public int StartedCount { get; private set; } + + /// Gets the number of stopping callbacks. + public int StoppingCount { get; private set; } + + /// Gets the number of stopped callbacks. + public int StoppedCount { get; private set; } + + /// + public override Task OnStarted() + { + StartedCount++; + CleanupDisposable = new(); + CleanUp.Add(CleanupDisposable); + _ = Started.TrySetResult(true); + return Task.CompletedTask; + } + + /// + public override void OnStopping() + { + StoppingCount++; + base.OnStopping(); + } + + /// + public override void OnStopped() + { + StoppedCount++; + base.OnStopped(); + } + } + + /// Hosted service that uses the base lifecycle implementations. + /// The test lifetime source. + private sealed class DefaultHostedService(IHostApplicationLifetime lifetime) + : HostedServiceBase(NullLogger.Instance, lifetime); + + /// Hosted service that throws from OnStarted. + /// The test lifetime source. + private sealed class ThrowingStartedHostedService(IHostApplicationLifetime lifetime) + : HostedServiceBase(NullLogger.Instance, lifetime) + { + /// Gets the started signal. + public TaskCompletionSource Started { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); + + /// Gets the number of started callbacks. + public int StartedCount { get; private set; } + + /// + public override Task OnStarted() + { + StartedCount++; + _ = Started.TrySetResult(true); + throw new InvalidOperationException("Start failure for test coverage."); + } + } + + /// Disposable used to verify cleanup disposal. + private sealed class TrackingDisposable : IDisposable + { + /// Gets a value indicating whether this instance was disposed. + public bool IsDisposed { get; private set; } + + /// + public void Dispose() => IsDisposed = true; + } + + /// Test application lifetime that exposes manual trigger methods. + private sealed class TestHostApplicationLifetime : IHostApplicationLifetime, IDisposable + { + /// Stores the started token source. + private readonly CancellationTokenSource _started = new(); + + /// Stores the stopping token source. + private readonly CancellationTokenSource _stopping = new(); + + /// Stores the stopped token source. + private readonly CancellationTokenSource _stopped = new(); + + /// + public CancellationToken ApplicationStarted => _started.Token; + + /// + public CancellationToken ApplicationStopping => _stopping.Token; + + /// + public CancellationToken ApplicationStopped => _stopped.Token; + + /// Triggers the started token. + public void TriggerStarted() => _started.Cancel(); + + /// Triggers the stopping token. + public void TriggerStopping() => _stopping.Cancel(); + + /// Triggers the stopped token. + public void TriggerStopped() => _stopped.Cancel(); + + /// + public void StopApplication() => TriggerStopping(); + + /// + public void Dispose() + { + _started.Dispose(); + _stopping.Dispose(); + _stopped.Dispose(); + } + } +} diff --git a/src/tests/Extensions.Hosting.Tests/MutexBuilderTests.cs b/src/tests/Extensions.Hosting.Tests/MutexBuilderTests.cs index e1f9fa0..e91ab78 100644 --- a/src/tests/Extensions.Hosting.Tests/MutexBuilderTests.cs +++ b/src/tests/Extensions.Hosting.Tests/MutexBuilderTests.cs @@ -1,17 +1,13 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. namespace Extensions.Hosting.Tests; -/// -/// Contains unit tests for the IMutexBuilder interface and its implementation. -/// +/// Contains unit tests for the IMutexBuilder interface and its implementation. public class MutexBuilderTests { - /// - /// Verifies that TestMutexBuilder has null MutexId by default. - /// + /// Verifies that TestMutexBuilder has null MutexId by default. /// A task that represents the asynchronous test operation. [Test] public async Task MutexBuilder_MutexId_DefaultsToNull() @@ -20,21 +16,20 @@ public async Task MutexBuilder_MutexId_DefaultsToNull() await Assert.That(builder.MutexId).IsNull(); } - /// - /// Verifies that MutexId can be set and retrieved. - /// + /// Verifies that MutexId can be set and retrieved. /// A task that represents the asynchronous test operation. [Test] public async Task MutexBuilder_MutexId_CanBeSetAndRetrieved() { - var builder = new TestMutexBuilder(); - builder.MutexId = "test-mutex-id"; + var builder = new TestMutexBuilder + { + MutexId = "test-mutex-id" + }; + await Assert.That(builder.MutexId).IsEqualTo("test-mutex-id"); } - /// - /// Verifies that IsGlobal defaults to false. - /// + /// Verifies that IsGlobal defaults to false. /// A task that represents the asynchronous test operation. [Test] public async Task MutexBuilder_IsGlobal_DefaultsToFalse() @@ -43,21 +38,20 @@ public async Task MutexBuilder_IsGlobal_DefaultsToFalse() await Assert.That(builder.IsGlobal).IsFalse(); } - /// - /// Verifies that IsGlobal can be set and retrieved. - /// + /// Verifies that IsGlobal can be set and retrieved. /// A task that represents the asynchronous test operation. [Test] public async Task MutexBuilder_IsGlobal_CanBeSetAndRetrieved() { - var builder = new TestMutexBuilder(); - builder.IsGlobal = true; + var builder = new TestMutexBuilder + { + IsGlobal = true + }; + await Assert.That(builder.IsGlobal).IsTrue(); } - /// - /// Verifies that WhenNotFirstInstance defaults to null. - /// + /// Verifies that WhenNotFirstInstance defaults to null. /// A task that represents the asynchronous test operation. [Test] public async Task MutexBuilder_WhenNotFirstInstance_DefaultsToNull() @@ -66,9 +60,7 @@ public async Task MutexBuilder_WhenNotFirstInstance_DefaultsToNull() await Assert.That(builder.WhenNotFirstInstance).IsNull(); } - /// - /// Verifies that WhenNotFirstInstance can be set and invoked. - /// + /// Verifies that WhenNotFirstInstance can be set and invoked. /// A task that represents the asynchronous test operation. [Test] public async Task MutexBuilder_WhenNotFirstInstance_CanBeSetAndInvoked() @@ -77,14 +69,12 @@ public async Task MutexBuilder_WhenNotFirstInstance_CanBeSetAndInvoked() var wasInvoked = false; builder.WhenNotFirstInstance = (_, _) => wasInvoked = true; - builder.WhenNotFirstInstance?.Invoke(null!, null!); + builder.WhenNotFirstInstance.Invoke(null!, null!); await Assert.That(wasInvoked).IsTrue(); } - /// - /// Verifies that all properties can be configured together. - /// + /// Verifies that all properties can be configured together. /// A task that represents the asynchronous test operation. [Test] public async Task MutexBuilder_AllProperties_CanBeConfiguredTogether() diff --git a/src/tests/Extensions.Hosting.Tests/OrderedTestPlugin.cs b/src/tests/Extensions.Hosting.Tests/OrderedTestPlugin.cs index a1cd68b..a11d94e 100644 --- a/src/tests/Extensions.Hosting.Tests/OrderedTestPlugin.cs +++ b/src/tests/Extensions.Hosting.Tests/OrderedTestPlugin.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.Extensions.DependencyInjection; @@ -7,9 +7,7 @@ namespace Extensions.Hosting.Tests; -/// -/// A plugin with a custom order attribute for testing plugin ordering. -/// +/// A plugin with a custom order attribute for testing plugin ordering. [PluginOrder(100)] public class OrderedTestPlugin : IPlugin { diff --git a/src/tests/Extensions.Hosting.Tests/PluginBaseTests.cs b/src/tests/Extensions.Hosting.Tests/PluginBaseTests.cs new file mode 100644 index 0000000..c531c57 --- /dev/null +++ b/src/tests/Extensions.Hosting.Tests/PluginBaseTests.cs @@ -0,0 +1,101 @@ +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using ReactiveMarbles.Extensions.Hosting.Plugins; + +namespace Extensions.Hosting.Tests; + +/// Contains tests for plugin base hosted-service registration helpers. +public class PluginBaseTests +{ + /// Verifies that PluginBase with one hosted service registers that service. + /// A task that represents the asynchronous test operation. + [Test] + public async Task ConfigureHost_WithOneHostedService_RegistersService() + { + var services = new ServiceCollection(); + var plugin = new PluginBase(); + + plugin.ConfigureHost(new object(), services); + + await Assert.That(services.Any(IsFirstHostedServiceRegistration)).IsTrue(); + } + + /// Verifies that PluginBase with two hosted services registers both services. + /// A task that represents the asynchronous test operation. + [Test] + public async Task ConfigureHost_WithTwoHostedServices_RegistersServices() + { + var services = new ServiceCollection(); + var plugin = new PluginBase(); + + plugin.ConfigureHost(new object(), services); + + await Assert.That(services.Any(IsFirstHostedServiceRegistration)).IsTrue(); + await Assert.That(services.Any(IsSecondHostedServiceRegistration)).IsTrue(); + } + + /// Verifies that PluginBase with three hosted services registers all services. + /// A task that represents the asynchronous test operation. + [Test] + public async Task ConfigureHost_WithThreeHostedServices_RegistersServices() + { + var services = new ServiceCollection(); + var plugin = new PluginBase(); + + plugin.ConfigureHost(new object(), services); + + await Assert.That(services.Any(IsFirstHostedServiceRegistration)).IsTrue(); + await Assert.That(services.Any(IsSecondHostedServiceRegistration)).IsTrue(); + await Assert.That(services.Any(IsThirdHostedServiceRegistration)).IsTrue(); + } + + /// Returns a value indicating whether the descriptor registers the first hosted service. + /// The service descriptor to inspect. + /// True when the descriptor registers the first hosted service. + private static bool IsFirstHostedServiceRegistration(ServiceDescriptor serviceDescriptor) => + IsHostedServiceRegistration(serviceDescriptor); + + /// Returns a value indicating whether the descriptor registers the second hosted service. + /// The service descriptor to inspect. + /// True when the descriptor registers the second hosted service. + private static bool IsSecondHostedServiceRegistration(ServiceDescriptor serviceDescriptor) => + IsHostedServiceRegistration(serviceDescriptor); + + /// Returns a value indicating whether the descriptor registers the third hosted service. + /// The service descriptor to inspect. + /// True when the descriptor registers the third hosted service. + private static bool IsThirdHostedServiceRegistration(ServiceDescriptor serviceDescriptor) => + IsHostedServiceRegistration(serviceDescriptor); + + /// Returns a value indicating whether the descriptor registers the requested hosted service type. + /// The hosted service implementation type. + /// The service descriptor to inspect. + /// True when the descriptor registers the requested hosted service type. + private static bool IsHostedServiceRegistration(ServiceDescriptor serviceDescriptor) + where T : class, IHostedService => + serviceDescriptor.ServiceType == typeof(IHostedService) && + serviceDescriptor.ImplementationType == typeof(T); + + /// First hosted service used for registration tests. + public sealed class FirstHostedService : TestHostedService; + + /// Second hosted service used for registration tests. + public sealed class SecondHostedService : TestHostedService; + + /// Third hosted service used for registration tests. + public sealed class ThirdHostedService : TestHostedService; + + /// Base hosted service used for registration tests. + public abstract class TestHostedService : IHostedService + { + /// + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + /// + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } +} diff --git a/src/tests/Extensions.Hosting.Tests/PluginBuilderExtensionsTests.cs b/src/tests/Extensions.Hosting.Tests/PluginBuilderExtensionsTests.cs index 5d1c454..c1216b4 100644 --- a/src/tests/Extensions.Hosting.Tests/PluginBuilderExtensionsTests.cs +++ b/src/tests/Extensions.Hosting.Tests/PluginBuilderExtensionsTests.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.IO; @@ -7,14 +7,10 @@ namespace Extensions.Hosting.Tests; -/// -/// Contains unit tests for the PluginBuilderExtensions class. -/// +/// Contains unit tests for the PluginBuilderExtensions class. public class PluginBuilderExtensionsTests { - /// - /// Verifies that AddScanDirectories throws when directories is null. - /// + /// Verifies that AddScanDirectories throws when directories is null. /// A task that represents the asynchronous test operation. [Test] public async Task AddScanDirectories_WithNullDirectories_ThrowsArgumentNullException() @@ -24,9 +20,7 @@ public async Task AddScanDirectories_WithNullDirectories_ThrowsArgumentNullExcep await Assert.That(Act).Throws(); } - /// - /// Verifies that AddScanDirectories adds directories to both collections. - /// + /// Verifies that AddScanDirectories adds directories to both collections. /// A task that represents the asynchronous test operation. [Test] public async Task AddScanDirectories_WithValidDirectories_AddsToBothCollections() @@ -40,9 +34,7 @@ public async Task AddScanDirectories_WithValidDirectories_AddsToBothCollections( await Assert.That(builder.FrameworkDirectories.Count).IsEqualTo(1); } - /// - /// Verifies that ExcludeFrameworks throws when frameworkGlobs is null. - /// + /// Verifies that ExcludeFrameworks throws when frameworkGlobs is null. /// A task that represents the asynchronous test operation. [Test] public async Task ExcludeFrameworks_WithNullGlobs_ThrowsArgumentNullException() @@ -52,9 +44,7 @@ public async Task ExcludeFrameworks_WithNullGlobs_ThrowsArgumentNullException() await Assert.That(Act).Throws(); } - /// - /// Verifies that ExcludeFrameworks does not throw with valid input. - /// + /// Verifies that ExcludeFrameworks does not throw with valid input. /// A task that represents the asynchronous test operation. [Test] public async Task ExcludeFrameworks_WithValidGlobs_DoesNotThrow() @@ -73,9 +63,7 @@ public async Task ExcludeFrameworks_WithValidGlobs_DoesNotThrow() await Assert.That(exception).IsNull(); } - /// - /// Verifies that ExcludePlugins throws when pluginGlobs is null. - /// + /// Verifies that ExcludePlugins throws when pluginGlobs is null. /// A task that represents the asynchronous test operation. [Test] public async Task ExcludePlugins_WithNullGlobs_ThrowsArgumentNullException() @@ -85,9 +73,7 @@ public async Task ExcludePlugins_WithNullGlobs_ThrowsArgumentNullException() await Assert.That(Act).Throws(); } - /// - /// Verifies that ExcludePlugins does not throw with valid input. - /// + /// Verifies that ExcludePlugins does not throw with valid input. /// A task that represents the asynchronous test operation. [Test] public async Task ExcludePlugins_WithValidGlobs_DoesNotThrow() @@ -106,9 +92,7 @@ public async Task ExcludePlugins_WithValidGlobs_DoesNotThrow() await Assert.That(exception).IsNull(); } - /// - /// Verifies that IncludeFrameworks throws when frameworkGlobs is null. - /// + /// Verifies that IncludeFrameworks throws when frameworkGlobs is null. /// A task that represents the asynchronous test operation. [Test] public async Task IncludeFrameworks_WithNullGlobs_ThrowsArgumentNullException() @@ -118,9 +102,7 @@ public async Task IncludeFrameworks_WithNullGlobs_ThrowsArgumentNullException() await Assert.That(Act).Throws(); } - /// - /// Verifies that IncludeFrameworks does not throw with valid input. - /// + /// Verifies that IncludeFrameworks does not throw with valid input. /// A task that represents the asynchronous test operation. [Test] public async Task IncludeFrameworks_WithValidGlobs_DoesNotThrow() @@ -139,9 +121,7 @@ public async Task IncludeFrameworks_WithValidGlobs_DoesNotThrow() await Assert.That(exception).IsNull(); } - /// - /// Verifies that IncludePlugins throws when pluginGlobs is null. - /// + /// Verifies that IncludePlugins throws when pluginGlobs is null. /// A task that represents the asynchronous test operation. [Test] public async Task IncludePlugins_WithNullGlobs_ThrowsArgumentNullException() @@ -151,9 +131,7 @@ public async Task IncludePlugins_WithNullGlobs_ThrowsArgumentNullException() await Assert.That(Act).Throws(); } - /// - /// Verifies that IncludePlugins does not throw with valid input. - /// + /// Verifies that IncludePlugins does not throw with valid input. /// A task that represents the asynchronous test operation. [Test] public async Task IncludePlugins_WithValidGlobs_DoesNotThrow() @@ -172,9 +150,7 @@ public async Task IncludePlugins_WithValidGlobs_DoesNotThrow() await Assert.That(exception).IsNull(); } - /// - /// Verifies that RequirePlugins throws when pluginBuilder is null. - /// + /// Verifies that RequirePlugins throws when pluginBuilder is null. /// A task that represents the asynchronous test operation. [Test] public async Task RequirePlugins_WithNullBuilder_ThrowsArgumentNullException() @@ -183,9 +159,7 @@ public async Task RequirePlugins_WithNullBuilder_ThrowsArgumentNullException() await Assert.That(Act).Throws(); } - /// - /// Verifies that RequirePlugins sets FailIfNoPlugins to true by default. - /// + /// Verifies that RequirePlugins sets FailIfNoPlugins to true by default. /// A task that represents the asynchronous test operation. [Test] public async Task RequirePlugins_Default_SetsFailIfNoPluginsTrue() @@ -195,22 +169,21 @@ public async Task RequirePlugins_Default_SetsFailIfNoPluginsTrue() await Assert.That(builder.FailIfNoPlugins).IsTrue(); } - /// - /// Verifies that RequirePlugins sets FailIfNoPlugins to false when specified. - /// + /// Verifies that RequirePlugins sets FailIfNoPlugins to false when specified. /// A task that represents the asynchronous test operation. [Test] public async Task RequirePlugins_WithFalse_SetsFailIfNoPluginsFalse() { - var builder = new TestPluginBuilder(); - builder.FailIfNoPlugins = true; + var builder = new TestPluginBuilder + { + FailIfNoPlugins = true + }; + builder.RequirePlugins(false); await Assert.That(builder.FailIfNoPlugins).IsFalse(); } - /// - /// Verifies that multiple directories can be added. - /// + /// Verifies that multiple directories can be added. /// A task that represents the asynchronous test operation. [Test] public async Task AddScanDirectories_MultipleDirectories_AddsAll() diff --git a/src/tests/Extensions.Hosting.Tests/PluginOrderAttributeTests.cs b/src/tests/Extensions.Hosting.Tests/PluginOrderAttributeTests.cs index 712b197..d3fdc54 100644 --- a/src/tests/Extensions.Hosting.Tests/PluginOrderAttributeTests.cs +++ b/src/tests/Extensions.Hosting.Tests/PluginOrderAttributeTests.cs @@ -1,19 +1,15 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using ReactiveMarbles.Extensions.Hosting.Plugins; namespace Extensions.Hosting.Tests; -/// -/// Contains unit tests for the PluginOrderAttribute class. -/// +/// Contains unit tests for the PluginOrderAttribute class. public class PluginOrderAttributeTests { - /// - /// Verifies that the default constructor sets Order to 0. - /// + /// Verifies that the default constructor sets Order to 0. /// A task that represents the asynchronous test operation. [Test] public async Task DefaultConstructor_SetsOrderToZero() @@ -22,9 +18,7 @@ public async Task DefaultConstructor_SetsOrderToZero() await Assert.That(attribute.Order).IsEqualTo(0); } - /// - /// Verifies that the int constructor sets the Order property correctly. - /// + /// Verifies that the int constructor sets the Order property correctly. /// A task that represents the asynchronous test operation. [Test] public async Task IntConstructor_SetsOrderCorrectly() @@ -33,9 +27,7 @@ public async Task IntConstructor_SetsOrderCorrectly() await Assert.That(attribute.Order).IsEqualTo(42); } - /// - /// Verifies that the int constructor handles negative values. - /// + /// Verifies that the int constructor handles negative values. /// A task that represents the asynchronous test operation. [Test] public async Task IntConstructor_WithNegativeValue_SetsOrderCorrectly() @@ -44,9 +36,7 @@ public async Task IntConstructor_WithNegativeValue_SetsOrderCorrectly() await Assert.That(attribute.Order).IsEqualTo(-10); } - /// - /// Verifies that the enum constructor converts the enum to its int value. - /// + /// Verifies that the enum constructor converts the enum to its int value. /// A task that represents the asynchronous test operation. [Test] public async Task EnumConstructor_ConvertsToIntValue() @@ -55,9 +45,7 @@ public async Task EnumConstructor_ConvertsToIntValue() await Assert.That(attribute.Order).IsEqualTo((int)TestOrder.High); } - /// - /// Verifies that the attribute can be retrieved from a decorated class. - /// + /// Verifies that the attribute can be retrieved from a decorated class. /// A task that represents the asynchronous test operation. [Test] public async Task Attribute_CanBeRetrievedFromClass() diff --git a/src/tests/Extensions.Hosting.Tests/PluginScannerTests.cs b/src/tests/Extensions.Hosting.Tests/PluginScannerTests.cs index 84dd79c..61a0785 100644 --- a/src/tests/Extensions.Hosting.Tests/PluginScannerTests.cs +++ b/src/tests/Extensions.Hosting.Tests/PluginScannerTests.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.Extensions.DependencyInjection; @@ -7,40 +7,30 @@ namespace Extensions.Hosting.Tests; -/// -/// Contains unit tests for the PluginScanner class, verifying its behavior when scanning for plugin instances and -/// handling invalid input. -/// +/// Contains unit tests for the PluginScanner class, verifying its behavior when scanning for plugin instances and handling invalid input. /// These tests ensure that PluginScanner methods correctly handle null arguments and return expected /// results when no plugins are found. The class uses the TUnit testing framework. public class PluginScannerTests { - /// - /// Verifies that ScanForPluginInstances throws an ArgumentNullException when passed a null argument. - /// + /// Verifies that ScanForPluginInstances throws an ArgumentNullException when passed a null argument. /// A task that represents the asynchronous test operation. [Test] public async Task ScanForPluginInstances_WithNull_ThrowsArgumentNullException() { - static System.Collections.Generic.IEnumerable Act() => PluginScanner.ScanForPluginInstances(null!); + static IEnumerable Act() => PluginScanner.ScanForPluginInstances(null!); await Assert.That(Act).Throws(); } - /// - /// Verifies that ByNamingConvention throws an ArgumentNullException when passed a null argument. - /// + /// Verifies that ByNamingConvention throws an ArgumentNullException when passed a null argument. /// A task that represents the asynchronous test operation. [Test] public async Task ByNamingConvention_WithNull_ThrowsArgumentNullException() { - static System.Collections.Generic.IEnumerable Act() => PluginScanner.ByNamingConvention(null!); + static IEnumerable Act() => PluginScanner.ByNamingConvention(null!); await Assert.That(Act).Throws(); } - /// - /// Verifies that the plugin scanner returns an empty collection when no plugins are found by naming convention in - /// the current assembly. - /// + /// Verifies that the plugin scanner returns an empty collection when no plugins are found by naming convention in the current assembly. /// A task that represents the asynchronous test operation. [Test] public async Task ByNamingConvention_FindsNoPlugin_ReturnsEmpty() @@ -51,9 +41,7 @@ public async Task ByNamingConvention_FindsNoPlugin_ReturnsEmpty() await Assert.That(plugins.Any()).IsFalse(); } - /// - /// Verifies that ScanForPluginInstances returns a non-null collection for a valid assembly. - /// + /// Verifies that ScanForPluginInstances returns a non-null collection for a valid assembly. /// A task that represents the asynchronous test operation. [Test] public async Task ScanForPluginInstances_WithValidAssembly_ReturnsNonNullCollection() @@ -63,9 +51,7 @@ public async Task ScanForPluginInstances_WithValidAssembly_ReturnsNonNullCollect await Assert.That(plugins).IsNotNull(); } - /// - /// Verifies that ScanForPluginInstances discovers the TestPlugin class. - /// + /// Verifies that ScanForPluginInstances discovers the TestPlugin class. /// A task that represents the asynchronous test operation. [Test] public async Task ScanForPluginInstances_FindsTestPlugin() @@ -76,9 +62,7 @@ public async Task ScanForPluginInstances_FindsTestPlugin() await Assert.That(plugins.Any(p => p is TestPlugin)).IsTrue(); } - /// - /// Verifies that ScanForPluginInstances does not include abstract plugin classes. - /// + /// Verifies that ScanForPluginInstances does not include abstract plugin classes. /// A task that represents the asynchronous test operation. [Test] public async Task ScanForPluginInstances_DoesNotIncludeAbstractPlugins() @@ -88,9 +72,7 @@ public async Task ScanForPluginInstances_DoesNotIncludeAbstractPlugins() await Assert.That(plugins.Any(p => p.GetType() == typeof(AbstractTestPlugin))).IsFalse(); } - /// - /// Verifies that the test plugin can be configured via ConfigureHost. - /// + /// Verifies that the test plugin can be configured via ConfigureHost. /// A task that represents the asynchronous test operation. [Test] public async Task TestPlugin_ConfigureHost_DoesNotThrow() diff --git a/src/tests/Extensions.Hosting.Tests/ResourceMutexTests.cs b/src/tests/Extensions.Hosting.Tests/ResourceMutexTests.cs index 5fd0f3a..e35868a 100644 --- a/src/tests/Extensions.Hosting.Tests/ResourceMutexTests.cs +++ b/src/tests/Extensions.Hosting.Tests/ResourceMutexTests.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.Extensions.Logging.Abstractions; @@ -7,50 +7,40 @@ namespace Extensions.Hosting.Tests; -/// -/// Contains unit tests for the ResourceMutex class to verify its behavior and exception handling. -/// +/// Contains unit tests for the ResourceMutex class to verify its behavior and exception handling. /// These tests ensure that ResourceMutex correctly throws exceptions for invalid input and that its /// locking mechanism functions as expected. The tests are intended to validate the public API and usage scenarios of /// ResourceMutex. public class ResourceMutexTests { - /// - /// Verifies that calling ResourceMutex.Create with a null mutex ID throws an ArgumentNullException. - /// + /// Verifies that calling ResourceMutex.Create with a null mutex ID throws an ArgumentException. /// A task that represents the asynchronous test operation. [Test] public async Task Create_WithNullMutexId_ThrowsArgumentNullException() { static ResourceMutex Act() => ResourceMutex.Create(null, null); - await Assert.That(Act).Throws(); + await Assert.That(Act).Throws(); } - /// - /// Verifies that calling ResourceMutex.Create with an empty mutex ID throws an ArgumentNullException. - /// + /// Verifies that calling ResourceMutex.Create with an empty mutex ID throws an ArgumentException. /// A task that represents the asynchronous test operation. [Test] public async Task Create_WithEmptyMutexId_ThrowsArgumentNullException() { static ResourceMutex Act() => ResourceMutex.Create(NullLogger.Instance, string.Empty); - await Assert.That(Act).Throws(); + await Assert.That(Act).Throws(); } - /// - /// Verifies that calling ResourceMutex.Create with a whitespace mutex ID throws an ArgumentNullException. - /// + /// Verifies that calling ResourceMutex.Create with a whitespace mutex ID throws an ArgumentException. /// A task that represents the asynchronous test operation. [Test] public async Task Create_WithWhitespaceMutexId_ThrowsArgumentNullException() { static ResourceMutex Act() => ResourceMutex.Create(NullLogger.Instance, " "); - await Assert.That(Act).Throws(); + await Assert.That(Act).Throws(); } - /// - /// Verifies that a ResourceMutex instance is locked upon creation and is properly disposed. - /// + /// Verifies that a ResourceMutex instance is locked upon creation and is properly disposed. /// A task that represents the asynchronous test operation. [Test] public async Task Create_ValidMutexId_IsLocked() @@ -62,9 +52,7 @@ public async Task Create_ValidMutexId_IsLocked() await Assert.That(mutex.IsLocked).IsTrue(); } - /// - /// Verifies that ResourceMutex can be created without a logger (null logger). - /// + /// Verifies that ResourceMutex can be created without a logger (null logger). /// A task that represents the asynchronous test operation. [Test] public async Task Create_WithNullLogger_CreatesDefaultLogger() @@ -76,9 +64,7 @@ public async Task Create_WithNullLogger_CreatesDefaultLogger() await Assert.That(mutex.IsLocked).IsTrue(); } - /// - /// Verifies that ResourceMutex can be created with a custom resource name. - /// + /// Verifies that ResourceMutex can be created with a custom resource name. /// A task that represents the asynchronous test operation. [Test] public async Task Create_WithResourceName_Succeeds() @@ -91,9 +77,7 @@ public async Task Create_WithResourceName_Succeeds() await Assert.That(mutex.IsLocked).IsTrue(); } - /// - /// Verifies that ResourceMutex can be created as a local mutex. - /// + /// Verifies that ResourceMutex can be created as a local mutex. /// A task that represents the asynchronous test operation. [Test] public async Task Create_LocalMutex_Succeeds() @@ -105,9 +89,22 @@ public async Task Create_LocalMutex_Succeeds() await Assert.That(mutex.IsLocked).IsTrue(); } - /// - /// Verifies that ResourceMutex can be created as a global mutex. - /// + /// Verifies that a second local mutex with the same identifier cannot acquire the lock. + /// A task that represents the asynchronous test operation. + [Test] + public async Task Create_SecondLocalMutexWithSameId_IsNotLocked() + { + var logger = NullLogger.Instance; + var id = "test-mutex-contention-" + Guid.NewGuid().ToString("N"); + + using var first = ResourceMutex.Create(logger, id); + using var second = ResourceMutex.Create(logger, id); + + await Assert.That(first.IsLocked).IsTrue(); + await Assert.That(second.IsLocked).IsFalse(); + } + + /// Verifies that ResourceMutex can be created as a global mutex. /// A task that represents the asynchronous test operation. [Test] public async Task Create_GlobalMutex_Succeeds() @@ -119,9 +116,7 @@ public async Task Create_GlobalMutex_Succeeds() await Assert.That(mutex.IsLocked).IsTrue(); } - /// - /// Verifies that disposing a ResourceMutex does not throw. - /// + /// Verifies that disposing a ResourceMutex does not throw. /// A task that represents the asynchronous test operation. [Test] public async Task Dispose_DoesNotThrow() @@ -143,9 +138,7 @@ public async Task Dispose_DoesNotThrow() await Assert.That(exception).IsNull(); } - /// - /// Verifies that disposing a ResourceMutex multiple times does not throw. - /// + /// Verifies that disposing a ResourceMutex multiple times does not throw. /// A task that represents the asynchronous test operation. [Test] public async Task Dispose_MultipleTimes_DoesNotThrow() diff --git a/src/tests/Extensions.Hosting.Tests/TestMutexBuilder.cs b/src/tests/Extensions.Hosting.Tests/TestMutexBuilder.cs index 43795ed..145f950 100644 --- a/src/tests/Extensions.Hosting.Tests/TestMutexBuilder.cs +++ b/src/tests/Extensions.Hosting.Tests/TestMutexBuilder.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.Extensions.Hosting; @@ -8,9 +8,7 @@ namespace Extensions.Hosting.Tests; -/// -/// Test implementation of IMutexBuilder for unit testing. -/// +/// Test implementation of IMutexBuilder for unit testing. public class TestMutexBuilder : IMutexBuilder { /// diff --git a/src/tests/Extensions.Hosting.Tests/TestOrder.cs b/src/tests/Extensions.Hosting.Tests/TestOrder.cs index e23fb0b..02468f4 100644 --- a/src/tests/Extensions.Hosting.Tests/TestOrder.cs +++ b/src/tests/Extensions.Hosting.Tests/TestOrder.cs @@ -1,12 +1,10 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. namespace Extensions.Hosting.Tests; -/// -/// Test enumeration for plugin ordering. -/// +/// Test enumeration for plugin ordering. public enum TestOrder { /// Low priority. diff --git a/src/tests/Extensions.Hosting.Tests/TestPlugin.cs b/src/tests/Extensions.Hosting.Tests/TestPlugin.cs index bfbd500..628637c 100644 --- a/src/tests/Extensions.Hosting.Tests/TestPlugin.cs +++ b/src/tests/Extensions.Hosting.Tests/TestPlugin.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using Microsoft.Extensions.DependencyInjection; @@ -7,14 +7,10 @@ namespace Extensions.Hosting.Tests; -/// -/// A test plugin implementation for unit testing purposes. -/// +/// A test plugin implementation for unit testing purposes. public class TestPlugin : IPlugin { - /// - /// Gets a value indicating whether ConfigureHost was called. - /// + /// Gets a value indicating whether ConfigureHost was called. public bool WasConfigured { get; private set; } /// diff --git a/src/tests/Extensions.Hosting.Tests/TestPluginBuilder.cs b/src/tests/Extensions.Hosting.Tests/TestPluginBuilder.cs index 435eced..f878480 100644 --- a/src/tests/Extensions.Hosting.Tests/TestPluginBuilder.cs +++ b/src/tests/Extensions.Hosting.Tests/TestPluginBuilder.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// Copyright (c) 2016-2026 ReactiveUI and Contributors. All rights reserved. +// ReactiveUI and Contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System.Reflection; @@ -8,9 +8,7 @@ namespace Extensions.Hosting.Tests; -/// -/// Test implementation of IPluginBuilder for unit testing. -/// +/// Test implementation of IPluginBuilder for unit testing. public class TestPluginBuilder : IPluginBuilder { /// diff --git a/stylecop.json b/stylecop.json deleted file mode 100644 index 85e9732..0000000 --- a/stylecop.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", - "settings": { - "indentation": { - "useTabs": false, - "indentationSize": 4 - }, - "documentationRules": { - "documentExposedElements": true, - "documentInternalElements": false, - "documentPrivateElements": false, - "documentInterfaces": true, - "documentPrivateFields": false, - "documentationCulture": "en-US", - "companyName": "ReactiveUI Association Incorporated", - "copyrightText": "Copyright (c) 2019-2025 {companyName}. All rights reserved.\n{companyName} licenses this file to you under the {licenseName} license.\nSee the {licenseFile} file in the project root for full license information.", - "variables": { - "licenseName": "MIT", - "licenseFile": "LICENSE" - }, - "xmlHeader": false - }, - "layoutRules": { - "newlineAtEndOfFile": "allow", - "allowConsecutiveUsings": true - }, - "maintainabilityRules": { - "topLevelTypes": [ - "class", - "interface", - "struct", - "enum", - "delegate" - ] - }, - "orderingRules": { - "usingDirectivesPlacement": "outsideNamespace", - "systemUsingDirectivesFirst": true - } - } -} From 55f4829d68471f7c3ea5cac3f06668bb13f15c9f Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Sun, 5 Jul 2026 17:05:18 +0100 Subject: [PATCH 2/4] fix: resolve NUKE build analyzer failures Seal the NUKE Build type so CA1852 is satisfied when the build project is compiled in CI. Add a FromConfiguration conversion alternative for Configuration to satisfy CA2225 without suppressing analyzer diagnostics. Verification: dotnet build build/_build.csproj -c Release /warnaserror; dotnet build src/Extensions.Hosting.slnx -c Release --no-restore /m:1 /p:BuildInParallel=false. --- build/Build.cs | 2 +- build/Configuration.cs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/build/Build.cs b/build/Build.cs index dbc8147..e52c4ea 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -24,7 +24,7 @@ //// FetchDepth = 0, //// ImportSecrets = new[] { nameof(NuGetApiKey) }, //// InvokedTargets = new[] { nameof(Compile), nameof(Deploy) })] -partial class Build : NukeBuild +sealed partial class Build : NukeBuild { public static int Main() => Execute(x => x.Pack); diff --git a/build/Configuration.cs b/build/Configuration.cs index a0e0498..1c0af0c 100644 --- a/build/Configuration.cs +++ b/build/Configuration.cs @@ -9,4 +9,7 @@ public class Configuration : Enumeration public static implicit operator string(Configuration configuration) => configuration?.Value; + + public static string FromConfiguration(Configuration configuration) => + configuration?.Value; } From 74d732e9dbe7879d1a442f928dbba9b414c516dc Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Sun, 5 Jul 2026 17:07:16 +0100 Subject: [PATCH 3/4] ci: install .NET 11 preview SDK Add 11.0.x to setup-dotnet in BuildOnly and BuildDeploy so CI can compile the new net11.0 target frameworks. Verification: git diff --check. --- .github/workflows/BuildDeploy.yml | 1 + .github/workflows/BuildOnly.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/BuildDeploy.yml b/.github/workflows/BuildDeploy.yml index 1c048e1..72b1887 100644 --- a/.github/workflows/BuildDeploy.yml +++ b/.github/workflows/BuildDeploy.yml @@ -44,6 +44,7 @@ jobs: 8.0.x 9.0.x 10.0.x + 11.0.x dotnet-quality: 'preview' cache: true cache-dependency-path: | diff --git a/.github/workflows/BuildOnly.yml b/.github/workflows/BuildOnly.yml index 4065df2..1c81954 100644 --- a/.github/workflows/BuildOnly.yml +++ b/.github/workflows/BuildOnly.yml @@ -37,6 +37,7 @@ jobs: 8.0.x 9.0.x 10.0.x + 11.0.x dotnet-quality: 'preview' cache: true cache-dependency-path: | From c8e080bd05caff293c54654c627484daf3c6df22 Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Sun, 5 Jul 2026 17:44:35 +0100 Subject: [PATCH 4/4] ci: avoid duplicate dotnet package caching Remove setup-dotnet package caching now that the workflows already use a dedicated actions/cache step for NuGet packages and NUKE temp data. This prevents the setup-dotnet post step from spending excessive time saving duplicate cache data after successful build and test steps. Verification: git diff --check. --- .github/workflows/BuildDeploy.yml | 9 +-------- .github/workflows/BuildOnly.yml | 9 +-------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/.github/workflows/BuildDeploy.yml b/.github/workflows/BuildDeploy.yml index 72b1887..c7d86de 100644 --- a/.github/workflows/BuildDeploy.yml +++ b/.github/workflows/BuildDeploy.yml @@ -37,7 +37,7 @@ jobs: with: fetch-depth: 0 - - name: Setup .NET (With cache) + - name: Setup .NET uses: actions/setup-dotnet@v5.3.0 with: dotnet-version: | @@ -46,13 +46,6 @@ jobs: 10.0.x 11.0.x dotnet-quality: 'preview' - cache: true - cache-dependency-path: | - **/Directory.Packages.props - **/*.slnx - **/*.csproj - **/global.json - **/nuget.config - name: NBGV id: nbgv diff --git a/.github/workflows/BuildOnly.yml b/.github/workflows/BuildOnly.yml index 1c81954..2270fdb 100644 --- a/.github/workflows/BuildOnly.yml +++ b/.github/workflows/BuildOnly.yml @@ -30,7 +30,7 @@ jobs: with: fetch-depth: 0 - - name: Setup .NET (With cache) + - name: Setup .NET uses: actions/setup-dotnet@v5.3.0 with: dotnet-version: | @@ -39,13 +39,6 @@ jobs: 10.0.x 11.0.x dotnet-quality: 'preview' - cache: true - cache-dependency-path: | - **/Directory.Packages.props - **/*.slnx - **/*.csproj - **/global.json - **/nuget.config - name: NBGV id: nbgv