From 2e2c4fe0c13ca97b7fa18cf54562c963976953a7 Mon Sep 17 00:00:00 2001 From: damyanpetev Date: Wed, 21 Oct 2026 20:04:27 +0300 Subject: [PATCH 1/3] refactor: expose the JS runtime on IIgniteUIBlazor An IIgniteUIBlazor registered without AddIgniteUIBlazor gets a runtime of its own, with a one-time deprecation warning, until such registrations are dropped. Co-Authored-By: Claude Opus 5.5 --- src/componentsBase/IIgniteUIBlazorRuntime.cs | 30 +++++++++++++--- src/componentsBase/IgbComponentBase.cs | 6 ++++ .../ServiceRegistrationTests.cs | 36 +++++++++++++++++++ 3 files changed, 67 insertions(+), 5 deletions(-) diff --git a/src/componentsBase/IIgniteUIBlazorRuntime.cs b/src/componentsBase/IIgniteUIBlazorRuntime.cs index fb991f99..809aec0a 100644 --- a/src/componentsBase/IIgniteUIBlazorRuntime.cs +++ b/src/componentsBase/IIgniteUIBlazorRuntime.cs @@ -1,9 +1,10 @@ +using System.Runtime.CompilerServices; using Microsoft.JSInterop; namespace IgniteUI.Blazor.Controls { /// - /// The runtime behind the marker; the components and + /// The runtime behind ; the components and /// reach it through . /// internal interface IIgniteUIBlazorRuntime @@ -19,14 +20,33 @@ internal interface IIgniteUIBlazorRuntime internal static class IgniteUIBlazorRuntimeExtensions { + // Temporary, for apps that register an IIgniteUIBlazor implementation themselves instead of calling + // AddIgniteUIBlazor, such as the full IgniteUI.Blazor package's deprecated IgniteUIBlazor class. + private static readonly ConditionalWeakTable _adopted = new(); + private static int _adoptionReported; + /// - /// The single conversion from the public marker to the runtime it is implemented by. + /// The single conversion from the public service to the runtime it is implemented by. + /// Another implementation gets a runtime of its own, one per instance and so one per scope. /// internal static IIgniteUIBlazorRuntime AsRuntime(this IIgniteUIBlazor service) { - return service as IIgniteUIBlazorRuntime - ?? throw new InvalidOperationException( - $"The registered {nameof(IIgniteUIBlazor)} instance is of type {service?.GetType().Name ?? "null"}, which is not the runtime registered by AddIgniteUIBlazor."); + if (service is IIgniteUIBlazorRuntime runtime) + { + return runtime; + } + return _adopted.GetValue(service, Adopt); + } + + private static IgniteUIBlazor Adopt(IIgniteUIBlazor service) + { + if (Interlocked.Exchange(ref _adoptionReported, 1) == 0) + { + Console.WriteLine( + $"Warning: {nameof(IIgniteUIBlazor)} is registered as {service.GetType().FullName} instead of through AddIgniteUIBlazor. " + + "This is deprecated and will stop working in a future release; call AddIgniteUIBlazor instead."); + } + return new IgniteUIBlazor(service.JsRuntime); } } } diff --git a/src/componentsBase/IgbComponentBase.cs b/src/componentsBase/IgbComponentBase.cs index 87d3caa4..f51a04f7 100644 --- a/src/componentsBase/IgbComponentBase.cs +++ b/src/componentsBase/IgbComponentBase.cs @@ -3857,6 +3857,12 @@ internal IgniteUIBlazorSettings(IIgniteUIBlazorSettings settings) /// public interface IIgniteUIBlazor { + /// + /// The JS runtime the client resources load through. Supports the product infrastructure and is not + /// intended to be used directly from your code. + /// + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] + IJSRuntime JsRuntime { get; } } internal class IgniteUIBlazor : IIgniteUIBlazor, IIgniteUIBlazorRuntime diff --git a/tests/IgniteUI.Blazor.Tests/ServiceRegistrationTests.cs b/tests/IgniteUI.Blazor.Tests/ServiceRegistrationTests.cs index d24c32b7..d6b73eef 100644 --- a/tests/IgniteUI.Blazor.Tests/ServiceRegistrationTests.cs +++ b/tests/IgniteUI.Blazor.Tests/ServiceRegistrationTests.cs @@ -1,3 +1,4 @@ +using Bunit; using IgniteUI.Blazor.Controls; using Microsoft.Extensions.DependencyInjection; using Microsoft.JSInterop; @@ -180,6 +181,41 @@ public void AddIgniteUIBlazor_MutatingOriginalArray_DoesNotAffectRegisteredModul Assert.Equal([typeof(FirstModule)], SettingsOf(host).ModulesToLoad); } + /// + /// The full IgniteUI.Blazor package requests its preloaded modules through this JS runtime + /// when the runtime invokes their Register, the only time it has the instance. + /// + [Fact] + public void Runtime_ExposesTheRegisteredJsRuntime() + { + using var host = new Host(s => s.AddIgniteUIBlazor()); + + Assert.Same(host.Services.GetRequiredService(), host.Services.GetRequiredService().JsRuntime); + } + + /// + /// An registered without AddIgniteUIBlazor, as the full + /// IgniteUI.Blazor package's deprecated IgniteUIBlazor is, gets a runtime of its own; + /// without it every component throws while being injected. + /// + [Fact] + public void Component_WithHandRegisteredService_RequestsItsModuleThroughThatJsRuntime() + { + using var context = new BunitContext(); + context.JSInterop.Mode = JSRuntimeMode.Loose; + context.Services.AddScoped(sp => new HandRegisteredService(sp.GetRequiredService())); + + context.Render(); + + var load = Assert.Single(context.JSInterop.Invocations, v => v.Identifier == "igRequestLoad"); + Assert.Equal("WebButtonModule", load.Arguments[0]); + } + + private sealed class HandRegisteredService(IJSRuntime jsRuntime) : IIgniteUIBlazor + { + public IJSRuntime JsRuntime { get; } = jsRuntime; + } + [Fact] public void Runtime_IsScoped_SoEachScopeRegistersItsOwn() { From 88337d6e84281d65f17665e47dfb379286bcd27e Mon Sep 17 00:00:00 2001 From: damyanpetev Date: Thu, 22 Oct 2026 10:29:36 +0300 Subject: [PATCH 2/3] refactor: bridge only the full package's IgniteUIBlazor Co-Authored-By: Claude Opus 5.5 --- src/componentsBase/IIgniteUIBlazorRuntime.cs | 24 ++++----- .../ServiceRegistrationTests.cs | 53 ++++++++++++++++--- 2 files changed, 57 insertions(+), 20 deletions(-) diff --git a/src/componentsBase/IIgniteUIBlazorRuntime.cs b/src/componentsBase/IIgniteUIBlazorRuntime.cs index 809aec0a..f6d57cab 100644 --- a/src/componentsBase/IIgniteUIBlazorRuntime.cs +++ b/src/componentsBase/IIgniteUIBlazorRuntime.cs @@ -20,14 +20,10 @@ internal interface IIgniteUIBlazorRuntime internal static class IgniteUIBlazorRuntimeExtensions { - // Temporary, for apps that register an IIgniteUIBlazor implementation themselves instead of calling - // AddIgniteUIBlazor, such as the full IgniteUI.Blazor package's deprecated IgniteUIBlazor class. private static readonly ConditionalWeakTable _adopted = new(); - private static int _adoptionReported; /// /// The single conversion from the public service to the runtime it is implemented by. - /// Another implementation gets a runtime of its own, one per instance and so one per scope. /// internal static IIgniteUIBlazorRuntime AsRuntime(this IIgniteUIBlazor service) { @@ -35,18 +31,18 @@ internal static IIgniteUIBlazorRuntime AsRuntime(this IIgniteUIBlazor service) { return runtime; } - return _adopted.GetValue(service, Adopt); - } - - private static IgniteUIBlazor Adopt(IIgniteUIBlazor service) - { - if (Interlocked.Exchange(ref _adoptionReported, 1) == 0) + if (service != null && IsFullPackageRuntime(service.GetType())) { - Console.WriteLine( - $"Warning: {nameof(IIgniteUIBlazor)} is registered as {service.GetType().FullName} instead of through AddIgniteUIBlazor. " + - "This is deprecated and will stop working in a future release; call AddIgniteUIBlazor instead."); + return _adopted.GetValue(service, s => new IgniteUIBlazor(s.JsRuntime)); } - return new IgniteUIBlazor(service.JsRuntime); + throw new InvalidOperationException( + $"The registered {nameof(IIgniteUIBlazor)} instance is of type {service?.GetType().Name ?? "null"}, which is not the runtime registered by AddIgniteUIBlazor."); } + + // Temporary, while the full IgniteUI.Blazor package deprecates its IgniteUIBlazor class: an app that registers + // that class instead of calling AddIgniteUIBlazor gets a runtime of its own, one per instance and so one per scope. + // Matched by name because that package depends on this one. + private static bool IsFullPackageRuntime(Type type) => + type.FullName == "IgniteUI.Blazor.Controls.IgniteUIBlazor" && type.Assembly.GetName().Name == "IgniteUI.Blazor"; } } diff --git a/tests/IgniteUI.Blazor.Tests/ServiceRegistrationTests.cs b/tests/IgniteUI.Blazor.Tests/ServiceRegistrationTests.cs index d6b73eef..a6383976 100644 --- a/tests/IgniteUI.Blazor.Tests/ServiceRegistrationTests.cs +++ b/tests/IgniteUI.Blazor.Tests/ServiceRegistrationTests.cs @@ -1,3 +1,5 @@ +using System.Reflection; +using System.Reflection.Emit; using Bunit; using IgniteUI.Blazor.Controls; using Microsoft.Extensions.DependencyInjection; @@ -194,16 +196,15 @@ public void Runtime_ExposesTheRegisteredJsRuntime() } /// - /// An registered without AddIgniteUIBlazor, as the full - /// IgniteUI.Blazor package's deprecated IgniteUIBlazor is, gets a runtime of its own; - /// without it every component throws while being injected. + /// The full IgniteUI.Blazor package's deprecated IgniteUIBlazor, registered in place of + /// AddIgniteUIBlazor, gets a runtime of its own; without it every component throws while being injected. /// [Fact] - public void Component_WithHandRegisteredService_RequestsItsModuleThroughThatJsRuntime() + public void Component_WithFullPackageRuntimeRegistered_RequestsItsModuleThroughItsJsRuntime() { using var context = new BunitContext(); context.JSInterop.Mode = JSRuntimeMode.Loose; - context.Services.AddScoped(sp => new HandRegisteredService(sp.GetRequiredService())); + context.Services.AddScoped(sp => FullPackageRuntime.Create(sp.GetRequiredService())); context.Render(); @@ -211,11 +212,51 @@ public void Component_WithHandRegisteredService_RequestsItsModuleThroughThatJsRu Assert.Equal("WebButtonModule", load.Arguments[0]); } - private sealed class HandRegisteredService(IJSRuntime jsRuntime) : IIgniteUIBlazor + /// Only the full package's class is bridged; any other implementation is still rejected. + [Fact] + public void Component_WithOtherRuntimeRegistered_Throws() + { + using var context = new BunitContext(); + context.JSInterop.Mode = JSRuntimeMode.Loose; + context.Services.AddScoped(sp => new OtherRuntime(sp.GetRequiredService())); + + var error = Assert.Throws(() => context.Render()); + Assert.Contains("not the runtime registered by AddIgniteUIBlazor", error.Message); + } + + private sealed class OtherRuntime(IJSRuntime jsRuntime) : IIgniteUIBlazor { public IJSRuntime JsRuntime { get; } = jsRuntime; } + /// + /// Stand-in for the full package's IgniteUIBlazor, which this repository cannot reference: a type of + /// that full name, emitted into an assembly of that name, deriving from this class for its members. + /// + public abstract class FullPackageRuntime : IIgniteUIBlazor + { + private static readonly Type _emitted = Emit(); + private IJSRuntime? _jsRuntime; + + public IJSRuntime JsRuntime => _jsRuntime ?? throw new InvalidOperationException("Create sets the JS runtime."); + + public static IIgniteUIBlazor Create(IJSRuntime jsRuntime) + { + var runtime = (FullPackageRuntime)(Activator.CreateInstance(_emitted) ?? throw new InvalidOperationException()); + runtime._jsRuntime = jsRuntime; + return runtime; + } + + private static Type Emit() + { + var assembly = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("IgniteUI.Blazor"), AssemblyBuilderAccess.Run); + var type = assembly.DefineDynamicModule("IgniteUI.Blazor") + .DefineType("IgniteUI.Blazor.Controls.IgniteUIBlazor", TypeAttributes.Public | TypeAttributes.Sealed, typeof(FullPackageRuntime)); + type.DefineDefaultConstructor(MethodAttributes.Public); + return type.CreateType(); + } + } + [Fact] public void Runtime_IsScoped_SoEachScopeRegistersItsOwn() { From 6a9e26f7dd3402f5a722a8d469ded1246bae66eb Mon Sep 17 00:00:00 2001 From: damyanpetev Date: Thu, 22 Oct 2026 10:55:58 +0300 Subject: [PATCH 3/3] test: trim the runtime bridge tests Co-Authored-By: Claude Opus 5.5 --- .../ServiceRegistrationTests.cs | 56 +++++-------------- 1 file changed, 15 insertions(+), 41 deletions(-) diff --git a/tests/IgniteUI.Blazor.Tests/ServiceRegistrationTests.cs b/tests/IgniteUI.Blazor.Tests/ServiceRegistrationTests.cs index a6383976..e9cf564d 100644 --- a/tests/IgniteUI.Blazor.Tests/ServiceRegistrationTests.cs +++ b/tests/IgniteUI.Blazor.Tests/ServiceRegistrationTests.cs @@ -183,10 +183,6 @@ public void AddIgniteUIBlazor_MutatingOriginalArray_DoesNotAffectRegisteredModul Assert.Equal([typeof(FirstModule)], SettingsOf(host).ModulesToLoad); } - /// - /// The full IgniteUI.Blazor package requests its preloaded modules through this JS runtime - /// when the runtime invokes their Register, the only time it has the instance. - /// [Fact] public void Runtime_ExposesTheRegisteredJsRuntime() { @@ -195,16 +191,18 @@ public void Runtime_ExposesTheRegisteredJsRuntime() Assert.Same(host.Services.GetRequiredService(), host.Services.GetRequiredService().JsRuntime); } - /// - /// The full IgniteUI.Blazor package's deprecated IgniteUIBlazor, registered in place of - /// AddIgniteUIBlazor, gets a runtime of its own; without it every component throws while being injected. - /// + /// The full IgniteUI.Blazor package's deprecated IgniteUIBlazor, registered in place of AddIgniteUIBlazor. [Fact] public void Component_WithFullPackageRuntimeRegistered_RequestsItsModuleThroughItsJsRuntime() { using var context = new BunitContext(); context.JSInterop.Mode = JSRuntimeMode.Loose; - context.Services.AddScoped(sp => FullPackageRuntime.Create(sp.GetRequiredService())); + context.Services.AddScoped(sp => + { + var runtime = (HandRegisteredRuntime)Activator.CreateInstance(_fullPackageRuntimeType)!; + runtime.JsRuntime = sp.GetRequiredService(); + return runtime; + }); context.Render(); @@ -212,50 +210,26 @@ public void Component_WithFullPackageRuntimeRegistered_RequestsItsModuleThroughI Assert.Equal("WebButtonModule", load.Arguments[0]); } - /// Only the full package's class is bridged; any other implementation is still rejected. [Fact] public void Component_WithOtherRuntimeRegistered_Throws() { using var context = new BunitContext(); - context.JSInterop.Mode = JSRuntimeMode.Loose; - context.Services.AddScoped(sp => new OtherRuntime(sp.GetRequiredService())); + context.Services.AddScoped(_ => new HandRegisteredRuntime()); var error = Assert.Throws(() => context.Render()); Assert.Contains("not the runtime registered by AddIgniteUIBlazor", error.Message); } - private sealed class OtherRuntime(IJSRuntime jsRuntime) : IIgniteUIBlazor + public class HandRegisteredRuntime : IIgniteUIBlazor { - public IJSRuntime JsRuntime { get; } = jsRuntime; + public IJSRuntime JsRuntime { get; set; } = null!; } - /// - /// Stand-in for the full package's IgniteUIBlazor, which this repository cannot reference: a type of - /// that full name, emitted into an assembly of that name, deriving from this class for its members. - /// - public abstract class FullPackageRuntime : IIgniteUIBlazor - { - private static readonly Type _emitted = Emit(); - private IJSRuntime? _jsRuntime; - - public IJSRuntime JsRuntime => _jsRuntime ?? throw new InvalidOperationException("Create sets the JS runtime."); - - public static IIgniteUIBlazor Create(IJSRuntime jsRuntime) - { - var runtime = (FullPackageRuntime)(Activator.CreateInstance(_emitted) ?? throw new InvalidOperationException()); - runtime._jsRuntime = jsRuntime; - return runtime; - } - - private static Type Emit() - { - var assembly = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("IgniteUI.Blazor"), AssemblyBuilderAccess.Run); - var type = assembly.DefineDynamicModule("IgniteUI.Blazor") - .DefineType("IgniteUI.Blazor.Controls.IgniteUIBlazor", TypeAttributes.Public | TypeAttributes.Sealed, typeof(FullPackageRuntime)); - type.DefineDefaultConstructor(MethodAttributes.Public); - return type.CreateType(); - } - } + // HandRegisteredRuntime under the full package's type and assembly name; this repository cannot reference that package. + private static readonly Type _fullPackageRuntimeType = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("IgniteUI.Blazor"), AssemblyBuilderAccess.Run) + .DefineDynamicModule("IgniteUI.Blazor") + .DefineType("IgniteUI.Blazor.Controls.IgniteUIBlazor", TypeAttributes.Public, typeof(HandRegisteredRuntime)) + .CreateType(); [Fact] public void Runtime_IsScoped_SoEachScopeRegistersItsOwn()