From 3a3ed0083d9162dc0476ae6309fcd2ff03bb3d13 Mon Sep 17 00:00:00 2001 From: ysmoradi Date: Sat, 29 Nov 2025 01:21:43 +0100 Subject: [PATCH 1/7] fix --- .../AutoInject/AutoInjectClassType.cs | 7 -- .../AutoInjectRazorComponentHandler.cs | 66 ------------------- .../AutoInject/AutoInjectSourceGenerator.cs | 44 +------------ 3 files changed, 1 insertion(+), 116 deletions(-) delete mode 100644 src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectClassType.cs delete mode 100644 src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectRazorComponentHandler.cs diff --git a/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectClassType.cs b/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectClassType.cs deleted file mode 100644 index c6bc601524..0000000000 --- a/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectClassType.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Bit.SourceGenerators; - -public enum AutoInjectClassType -{ - RazorComponent, - NormalClass -} diff --git a/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectRazorComponentHandler.cs b/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectRazorComponentHandler.cs deleted file mode 100644 index fd1c072cd2..0000000000 --- a/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectRazorComponentHandler.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Microsoft.CodeAnalysis; - -namespace Bit.SourceGenerators; - -public static class AutoInjectRazorComponentHandler -{ - public static string? Generate(INamedTypeSymbol? classSymbol, IReadOnlyCollection eligibleMembers) - { - if (classSymbol is null) - { - return null; - } - - if (AutoInjectHelper.IsContainingSymbolEqualToContainingNamespace(classSymbol) is false) - { - return null; - } - - string classNamespace = classSymbol.ContainingNamespace.ToDisplayString(); - - IReadOnlyCollection sortedMembers = eligibleMembers.OrderBy(o => o.Name).ToList(); - - string source = $@" -using Microsoft.AspNetCore.Components; -using System.ComponentModel; - -namespace {classNamespace} -{{ - public partial class {AutoInjectHelper.GenerateClassName(classSymbol)} - {{ - {GenerateInjectableProperties(sortedMembers)} - }} -}}"; - return source; - } - - private static string GenerateInjectableProperties(IReadOnlyCollection eligibleMembers) - { - StringBuilder stringBuilder = new StringBuilder(); - - foreach (ISymbol member in eligibleMembers) - { - if (member is IFieldSymbol fieldSymbol) - stringBuilder.Append(GenerateProperty(fieldSymbol.Type, fieldSymbol.Name)); - - if (member is IPropertySymbol propertySymbol) - stringBuilder.Append(GenerateProperty(propertySymbol.Type, propertySymbol.Name)); - } - - return stringBuilder.ToString(); - } - - private static string GenerateProperty(ITypeSymbol @type, string name) - { - return $@" - [global::System.CodeDom.Compiler.GeneratedCode(""Bit.SourceGenerators"",""{BitSourceGeneratorUtil.GetPackageVersion()}"")] - [global::System.Diagnostics.DebuggerNonUserCode] - [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] -{"\t\t"}[Inject] -{"\t\t"}[EditorBrowsable(EditorBrowsableState.Never)] -{"\t\t"}private {@type} ____{AutoInjectHelper.FormatMemberName(name)} {{ get => {name}; set => {name} = value; }}"; - } -} diff --git a/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectSourceGenerator.cs b/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectSourceGenerator.cs index 17f697ff45..a967524d6c 100644 --- a/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectSourceGenerator.cs +++ b/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectSourceGenerator.cs @@ -82,48 +82,6 @@ private static bool IsClassIsPartial(GeneratorExecutionContext context, INamedTy private static string? GenerateSource(INamedTypeSymbol? attributeSymbol, INamedTypeSymbol? classSymbol, IReadOnlyCollection eligibleMembers) { - AutoInjectClassType env = FigureOutTypeOfEnvironment(classSymbol); - return env switch - { - AutoInjectClassType.NormalClass => AutoInjectNormalClassHandler.Generate(attributeSymbol, classSymbol, eligibleMembers), - AutoInjectClassType.RazorComponent => AutoInjectRazorComponentHandler.Generate(classSymbol, eligibleMembers), - _ => string.Empty - }; - } - - private static AutoInjectClassType FigureOutTypeOfEnvironment(INamedTypeSymbol? @class) - { - if (@class is null) - throw new ArgumentNullException(nameof(@class)); - - if (IsClassIsRazorComponent(@class)) - return AutoInjectClassType.RazorComponent; - else - return AutoInjectClassType.NormalClass; - } - - private static bool IsClassIsRazorComponent(INamedTypeSymbol @class) - { - bool isInheritIComponent = @class.AllInterfaces.Any(o => o.ToDisplayString() == "Microsoft.AspNetCore.Components.IComponent"); - - if (isInheritIComponent) - return true; - - var classFilePaths = @class.Locations - .Where(o => o.SourceTree is not null) - .Select(o => o.SourceTree?.FilePath) - .ToList(); - - string razorFileName = $"{@class.Name}.razor"; - - foreach (var path in classFilePaths) - { - string directoryPath = Path.GetDirectoryName(path) ?? string.Empty; - string filePath = Path.Combine(directoryPath, razorFileName); - if (File.Exists(filePath)) - return true; - } - - return false; + return AutoInjectNormalClassHandler.Generate(attributeSymbol, classSymbol, eligibleMembers); } } From 31c4e5dbe186e44a316c33e3d69f8e9d5d22e17f Mon Sep 17 00:00:00 2001 From: ysmoradi Date: Sat, 29 Nov 2025 01:55:16 +0100 Subject: [PATCH 2/7] fix --- .../Components/Pages/TodoPage.razor.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/TodoPage.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/TodoPage.razor.cs index 4943e9244c..a5f7078a0a 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/TodoPage.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/TodoPage.razor.cs @@ -5,9 +5,19 @@ namespace Boilerplate.Client.Core.Components.Pages; public partial class TodoPage { - [AutoInject] Keyboard keyboard = default!; [AutoInject] ITodoItemController todoItemController = default!; + /// + /// By default, services remain in Blazor's scope for the lifetime of the app: + /// 1- Blazor Server: When the user closes the browser tab or gets disconnected. + /// 2- Blazor WebAssembly and Hybrid: When the app is closed. + /// This raises no issue with most services, specially those that are registered as singletons, + /// but if you prefer the service to be disposed when the component is disposed, use `ScopedServices` instead of AutoInject. + /// The following code demonstrates this by injecting the `Keyboard` service using `ScopedServices`, + /// this means there's no need to manually dispose it in the `DisposeAsync` method. + /// + Keyboard keyboard => field ??= ScopedServices.GetRequiredService(); + private bool isLoading; private string? searchText; private string? selectedSort; @@ -193,14 +203,4 @@ private async Task UpdateTodoItem(TodoItemDto todoItem) viewTodoItems.Remove(todoItem); } } - - protected override async ValueTask DisposeAsync(bool disposing) - { - await base.DisposeAsync(true); - - if (disposing) - { - await keyboard.DisposeAsync(); - } - } } From 03c0e8c2894621cd721ba2916068cf18693ffcbf Mon Sep 17 00:00:00 2001 From: ysmoradi Date: Sat, 29 Nov 2025 02:07:35 +0100 Subject: [PATCH 3/7] fix --- ...ndency Injection & Service Registration.md | 32 +++++++++++++++++++ .../Components/Pages/TodoPage.razor.cs | 10 +----- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.docs/09- Dependency Injection & Service Registration.md b/src/Templates/Boilerplate/Bit.Boilerplate/.docs/09- Dependency Injection & Service Registration.md index 1e6b43b5c8..b65ab0e4a2 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/.docs/09- Dependency Injection & Service Registration.md +++ b/src/Templates/Boilerplate/Bit.Boilerplate/.docs/09- Dependency Injection & Service Registration.md @@ -250,6 +250,38 @@ public partial class FeedbackPage : AppPageBase } ``` +## Owned services + +### Default Service Lifetime in Blazor Components + +By default, services injected in Blazor components remain tied to the application scope for the entire lifetime: + +- **Blazor Server**: Until the user closes the browser tab or the browser gets disconnected. +- **Blazor WebAssembly / Blazor Hybrid**: Until the browser tab or app is closed. + +This is perfectly fine for most services (especially singletons or stateless ones), but services that hold resources (timers, event subscriptions, native handlers, etc.) may need to be disposed when their associated component is destroyed. + +### Using ScopedServices for Automatic Disposal + +To achieve automatic disposal when the component is disposed, inject the service via `ScopedServices` instead of using `[AutoInject]`. This creates a scoped service instance that gets disposed along with the component. + +**Example:** + +```csharp +Keyboard keyboard => field ??= ScopedServices.GetRequiredService(); +``` + +Insetad of + +```csharp +[AutoInject] private Keyboard keyboard = default!; + +protected override async ValueTask DisposeAsync(bool disposing) +{ + await keyboard.DisposeAsync(); + await base.DisposeAsync(disposing); +} +``` --- ### AI Wiki: Answered Questions diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/TodoPage.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/TodoPage.razor.cs index a5f7078a0a..65cff8d402 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/TodoPage.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/TodoPage.razor.cs @@ -7,15 +7,7 @@ public partial class TodoPage { [AutoInject] ITodoItemController todoItemController = default!; - /// - /// By default, services remain in Blazor's scope for the lifetime of the app: - /// 1- Blazor Server: When the user closes the browser tab or gets disconnected. - /// 2- Blazor WebAssembly and Hybrid: When the app is closed. - /// This raises no issue with most services, specially those that are registered as singletons, - /// but if you prefer the service to be disposed when the component is disposed, use `ScopedServices` instead of AutoInject. - /// The following code demonstrates this by injecting the `Keyboard` service using `ScopedServices`, - /// this means there's no need to manually dispose it in the `DisposeAsync` method. - /// + // Refer to .docs/09- Dependency Injection & Service Registration.md 's Owned services section for more information about ScopedServices Keyboard keyboard => field ??= ScopedServices.GetRequiredService(); private bool isLoading; From 32cc58c30c45f7b7514591d6e2accfa43f0e21c7 Mon Sep 17 00:00:00 2001 From: ysmoradi Date: Sat, 29 Nov 2025 03:00:18 +0100 Subject: [PATCH 4/7] fix --- .../Bit.SourceGenerators/AutoInject/AutoInjectHelper.cs | 9 ++++----- .../AutoInject/AutoInjectNormalClassHandler.cs | 2 +- .../AutoInject/AutoInjectSourceGenerator.cs | 6 +++--- .../09- Dependency Injection & Service Registration.md | 2 +- .../Components/AppClientCoordinator.cs | 4 +--- 5 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectHelper.cs b/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectHelper.cs index 588cffeb1e..330cd9b6d6 100644 --- a/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectHelper.cs +++ b/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectHelper.cs @@ -20,7 +20,7 @@ public static IReadOnlyCollection GetBaseClassEligibleMembers(INamedTyp bool hasBase = false; - List result = new List(); + List result = []; INamedTypeSymbol? currentClass = classSymbol; do @@ -28,8 +28,7 @@ public static IReadOnlyCollection GetBaseClassEligibleMembers(INamedTyp if (currentClass.BaseType is not null) { INamedTypeSymbol baseType = currentClass.BaseType; - string baseMetadataName = baseType.ToDisplayString(); - if (baseMetadataName != "System.Object") + if (baseType.SpecialType != SpecialType.System_Object) { var baseEligibleFields = baseType .GetMembers() @@ -65,7 +64,7 @@ public static IReadOnlyCollection GetBaseClassEligibleMembers(INamedTyp } } while (hasBase); - return result.OrderBy(o => o.Name).ToList(); + return [.. result.OrderBy(o => o.Name)]; } public static string FormatMemberName(string? memberName) @@ -80,7 +79,7 @@ public static string FormatMemberName(string? memberName) if (memberName.Length == 1) return memberName.ToUpper(CultureInfo.InvariantCulture); - return memberName.Substring(0, 1).ToUpper(CultureInfo.InvariantCulture) + memberName.Substring(1); + return memberName[..1].ToUpper(CultureInfo.InvariantCulture) + memberName[1..]; } public static bool IsContainingSymbolEqualToContainingNamespace(INamedTypeSymbol? @class) diff --git a/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectNormalClassHandler.cs b/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectNormalClassHandler.cs index d6c01d9396..1cf71533ae 100644 --- a/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectNormalClassHandler.cs +++ b/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectNormalClassHandler.cs @@ -22,7 +22,7 @@ public static class AutoInjectNormalClassHandler string classNamespace = classSymbol.ContainingNamespace.ToDisplayString(); IReadOnlyCollection baseEligibleMembers = AutoInjectHelper.GetBaseClassEligibleMembers(classSymbol, attributeSymbol); - IReadOnlyCollection sortedMembers = eligibleMembers.OrderBy(o => o.Name).ToList(); + IReadOnlyCollection sortedMembers = [.. eligibleMembers.OrderBy(o => o.Name)]; string source = $@" namespace {classNamespace} diff --git a/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectSourceGenerator.cs b/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectSourceGenerator.cs index a967524d6c..acf9a5be0b 100644 --- a/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectSourceGenerator.cs +++ b/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectSourceGenerator.cs @@ -14,7 +14,7 @@ namespace Bit.SourceGenerators; public class AutoInjectSourceGenerator : ISourceGenerator { private static int counter; - private static readonly DiagnosticDescriptor NonPartialClassError = new DiagnosticDescriptor(id: "BITGEN001", + private static readonly DiagnosticDescriptor NonPartialClassError = new(id: "BITGEN001", title: "The class needs to be partial", messageFormat: "{0} is not partial. The AutoInject attribute needs to be used only in partial classes.", category: "Bit.SourceGenerators", @@ -38,7 +38,7 @@ public void Execute(GeneratorExecutionContext context) if (IsClassIsPartial(context, group.Key) is false) return; - string? partialClassSource = GenerateSource(attributeSymbol, group.Key, group.ToList()); + string? partialClassSource = GenerateSource(attributeSymbol, group.Key, [.. group]); if (string.IsNullOrEmpty(partialClassSource) is false) { @@ -54,7 +54,7 @@ public void Execute(GeneratorExecutionContext context) if (IsClassIsPartial(context, @class.BaseType!) is false) return; - string? partialClassSource = GenerateSource(attributeSymbol, @class, new List()); + string? partialClassSource = GenerateSource(attributeSymbol, @class, []); if (string.IsNullOrEmpty(partialClassSource) is false) { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.docs/09- Dependency Injection & Service Registration.md b/src/Templates/Boilerplate/Bit.Boilerplate/.docs/09- Dependency Injection & Service Registration.md index b65ab0e4a2..5c70c1efe2 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/.docs/09- Dependency Injection & Service Registration.md +++ b/src/Templates/Boilerplate/Bit.Boilerplate/.docs/09- Dependency Injection & Service Registration.md @@ -271,7 +271,7 @@ To achieve automatic disposal when the component is disposed, inject the service Keyboard keyboard => field ??= ScopedServices.GetRequiredService(); ``` -Insetad of +Instead of ```csharp [AutoInject] private Keyboard keyboard = default!; diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppClientCoordinator.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppClientCoordinator.cs index a2226521ed..6ddb844e96 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppClientCoordinator.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppClientCoordinator.cs @@ -33,11 +33,9 @@ public partial class AppClientCoordinator : AppComponentBase [AutoInject] private UserAgent userAgent = default!; [AutoInject] private IJSRuntime jsRuntime = default!; [AutoInject] private IUserController userController = default!; - [AutoInject] private IStorageService storageService = default!; [AutoInject] private ILogger authLogger = default!; [AutoInject] private ILogger navigatorLogger = default!; [AutoInject] private ILogger logger = default!; - [AutoInject] private IBitDeviceCoordinator bitDeviceCoordinator = default!; //#if (notification == true) [AutoInject] private IPushNotificationService pushNotificationService = default!; //#endif @@ -362,7 +360,7 @@ private async Task ConfigureUISetup() if (CultureInfoManager.InvariantGlobalization is false) { CultureInfoManager.SetCurrentCulture(new Uri(NavigationManager.Uri).GetCulture() ?? // 1- Culture query string OR Route data request culture - await storageService.GetItem("Culture") ?? // 2- User settings + await StorageService.GetItem("Culture") ?? // 2- User settings CultureInfo.CurrentUICulture.Name); // 3- OS settings } } From 227e1a4bc801c8157c6ecb1b207842805d57698a Mon Sep 17 00:00:00 2001 From: ysmoradi Date: Sat, 29 Nov 2025 03:05:22 +0100 Subject: [PATCH 5/7] fix --- .../AutoInject/AutoInjectClassType.cs | 7 ++ .../AutoInject/AutoInjectHelper.cs | 9 +-- .../AutoInjectNormalClassHandler.cs | 2 +- .../AutoInjectRazorComponentHandler.cs | 66 +++++++++++++++++++ .../AutoInject/AutoInjectSourceGenerator.cs | 50 ++++++++++++-- 5 files changed, 125 insertions(+), 9 deletions(-) create mode 100644 src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectClassType.cs create mode 100644 src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectRazorComponentHandler.cs diff --git a/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectClassType.cs b/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectClassType.cs new file mode 100644 index 0000000000..c6bc601524 --- /dev/null +++ b/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectClassType.cs @@ -0,0 +1,7 @@ +namespace Bit.SourceGenerators; + +public enum AutoInjectClassType +{ + RazorComponent, + NormalClass +} diff --git a/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectHelper.cs b/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectHelper.cs index 330cd9b6d6..588cffeb1e 100644 --- a/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectHelper.cs +++ b/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectHelper.cs @@ -20,7 +20,7 @@ public static IReadOnlyCollection GetBaseClassEligibleMembers(INamedTyp bool hasBase = false; - List result = []; + List result = new List(); INamedTypeSymbol? currentClass = classSymbol; do @@ -28,7 +28,8 @@ public static IReadOnlyCollection GetBaseClassEligibleMembers(INamedTyp if (currentClass.BaseType is not null) { INamedTypeSymbol baseType = currentClass.BaseType; - if (baseType.SpecialType != SpecialType.System_Object) + string baseMetadataName = baseType.ToDisplayString(); + if (baseMetadataName != "System.Object") { var baseEligibleFields = baseType .GetMembers() @@ -64,7 +65,7 @@ public static IReadOnlyCollection GetBaseClassEligibleMembers(INamedTyp } } while (hasBase); - return [.. result.OrderBy(o => o.Name)]; + return result.OrderBy(o => o.Name).ToList(); } public static string FormatMemberName(string? memberName) @@ -79,7 +80,7 @@ public static string FormatMemberName(string? memberName) if (memberName.Length == 1) return memberName.ToUpper(CultureInfo.InvariantCulture); - return memberName[..1].ToUpper(CultureInfo.InvariantCulture) + memberName[1..]; + return memberName.Substring(0, 1).ToUpper(CultureInfo.InvariantCulture) + memberName.Substring(1); } public static bool IsContainingSymbolEqualToContainingNamespace(INamedTypeSymbol? @class) diff --git a/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectNormalClassHandler.cs b/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectNormalClassHandler.cs index 1cf71533ae..d6c01d9396 100644 --- a/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectNormalClassHandler.cs +++ b/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectNormalClassHandler.cs @@ -22,7 +22,7 @@ public static class AutoInjectNormalClassHandler string classNamespace = classSymbol.ContainingNamespace.ToDisplayString(); IReadOnlyCollection baseEligibleMembers = AutoInjectHelper.GetBaseClassEligibleMembers(classSymbol, attributeSymbol); - IReadOnlyCollection sortedMembers = [.. eligibleMembers.OrderBy(o => o.Name)]; + IReadOnlyCollection sortedMembers = eligibleMembers.OrderBy(o => o.Name).ToList(); string source = $@" namespace {classNamespace} diff --git a/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectRazorComponentHandler.cs b/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectRazorComponentHandler.cs new file mode 100644 index 0000000000..fd1c072cd2 --- /dev/null +++ b/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectRazorComponentHandler.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace Bit.SourceGenerators; + +public static class AutoInjectRazorComponentHandler +{ + public static string? Generate(INamedTypeSymbol? classSymbol, IReadOnlyCollection eligibleMembers) + { + if (classSymbol is null) + { + return null; + } + + if (AutoInjectHelper.IsContainingSymbolEqualToContainingNamespace(classSymbol) is false) + { + return null; + } + + string classNamespace = classSymbol.ContainingNamespace.ToDisplayString(); + + IReadOnlyCollection sortedMembers = eligibleMembers.OrderBy(o => o.Name).ToList(); + + string source = $@" +using Microsoft.AspNetCore.Components; +using System.ComponentModel; + +namespace {classNamespace} +{{ + public partial class {AutoInjectHelper.GenerateClassName(classSymbol)} + {{ + {GenerateInjectableProperties(sortedMembers)} + }} +}}"; + return source; + } + + private static string GenerateInjectableProperties(IReadOnlyCollection eligibleMembers) + { + StringBuilder stringBuilder = new StringBuilder(); + + foreach (ISymbol member in eligibleMembers) + { + if (member is IFieldSymbol fieldSymbol) + stringBuilder.Append(GenerateProperty(fieldSymbol.Type, fieldSymbol.Name)); + + if (member is IPropertySymbol propertySymbol) + stringBuilder.Append(GenerateProperty(propertySymbol.Type, propertySymbol.Name)); + } + + return stringBuilder.ToString(); + } + + private static string GenerateProperty(ITypeSymbol @type, string name) + { + return $@" + [global::System.CodeDom.Compiler.GeneratedCode(""Bit.SourceGenerators"",""{BitSourceGeneratorUtil.GetPackageVersion()}"")] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +{"\t\t"}[Inject] +{"\t\t"}[EditorBrowsable(EditorBrowsableState.Never)] +{"\t\t"}private {@type} ____{AutoInjectHelper.FormatMemberName(name)} {{ get => {name}; set => {name} = value; }}"; + } +} diff --git a/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectSourceGenerator.cs b/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectSourceGenerator.cs index acf9a5be0b..17f697ff45 100644 --- a/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectSourceGenerator.cs +++ b/src/SourceGenerators/Bit.SourceGenerators/AutoInject/AutoInjectSourceGenerator.cs @@ -14,7 +14,7 @@ namespace Bit.SourceGenerators; public class AutoInjectSourceGenerator : ISourceGenerator { private static int counter; - private static readonly DiagnosticDescriptor NonPartialClassError = new(id: "BITGEN001", + private static readonly DiagnosticDescriptor NonPartialClassError = new DiagnosticDescriptor(id: "BITGEN001", title: "The class needs to be partial", messageFormat: "{0} is not partial. The AutoInject attribute needs to be used only in partial classes.", category: "Bit.SourceGenerators", @@ -38,7 +38,7 @@ public void Execute(GeneratorExecutionContext context) if (IsClassIsPartial(context, group.Key) is false) return; - string? partialClassSource = GenerateSource(attributeSymbol, group.Key, [.. group]); + string? partialClassSource = GenerateSource(attributeSymbol, group.Key, group.ToList()); if (string.IsNullOrEmpty(partialClassSource) is false) { @@ -54,7 +54,7 @@ public void Execute(GeneratorExecutionContext context) if (IsClassIsPartial(context, @class.BaseType!) is false) return; - string? partialClassSource = GenerateSource(attributeSymbol, @class, []); + string? partialClassSource = GenerateSource(attributeSymbol, @class, new List()); if (string.IsNullOrEmpty(partialClassSource) is false) { @@ -82,6 +82,48 @@ private static bool IsClassIsPartial(GeneratorExecutionContext context, INamedTy private static string? GenerateSource(INamedTypeSymbol? attributeSymbol, INamedTypeSymbol? classSymbol, IReadOnlyCollection eligibleMembers) { - return AutoInjectNormalClassHandler.Generate(attributeSymbol, classSymbol, eligibleMembers); + AutoInjectClassType env = FigureOutTypeOfEnvironment(classSymbol); + return env switch + { + AutoInjectClassType.NormalClass => AutoInjectNormalClassHandler.Generate(attributeSymbol, classSymbol, eligibleMembers), + AutoInjectClassType.RazorComponent => AutoInjectRazorComponentHandler.Generate(classSymbol, eligibleMembers), + _ => string.Empty + }; + } + + private static AutoInjectClassType FigureOutTypeOfEnvironment(INamedTypeSymbol? @class) + { + if (@class is null) + throw new ArgumentNullException(nameof(@class)); + + if (IsClassIsRazorComponent(@class)) + return AutoInjectClassType.RazorComponent; + else + return AutoInjectClassType.NormalClass; + } + + private static bool IsClassIsRazorComponent(INamedTypeSymbol @class) + { + bool isInheritIComponent = @class.AllInterfaces.Any(o => o.ToDisplayString() == "Microsoft.AspNetCore.Components.IComponent"); + + if (isInheritIComponent) + return true; + + var classFilePaths = @class.Locations + .Where(o => o.SourceTree is not null) + .Select(o => o.SourceTree?.FilePath) + .ToList(); + + string razorFileName = $"{@class.Name}.razor"; + + foreach (var path in classFilePaths) + { + string directoryPath = Path.GetDirectoryName(path) ?? string.Empty; + string filePath = Path.Combine(directoryPath, razorFileName); + if (File.Exists(filePath)) + return true; + } + + return false; } } From f967d588af196049cf32a1f624740d81b8cc7114 Mon Sep 17 00:00:00 2001 From: ysmoradi Date: Sat, 29 Nov 2025 10:17:25 +0100 Subject: [PATCH 6/7] fix --- ...ependency Injection & Service Registration.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.docs/09- Dependency Injection & Service Registration.md b/src/Templates/Boilerplate/Bit.Boilerplate/.docs/09- Dependency Injection & Service Registration.md index 5c70c1efe2..c037d21a6c 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/.docs/09- Dependency Injection & Service Registration.md +++ b/src/Templates/Boilerplate/Bit.Boilerplate/.docs/09- Dependency Injection & Service Registration.md @@ -268,7 +268,14 @@ To achieve automatic disposal when the component is disposed, inject the service **Example:** ```csharp -Keyboard keyboard => field ??= ScopedServices.GetRequiredService(); +Keyboard keyboard => field ??= ScopedServices.GetRequiredService(); // ??= Lazy initialization. It means the service gets resolved when accessed. + +protected override async Task OnAfterFirstRenderAsync() +{ + await keyboard.Add(ButilKeyCodes.KeyF, () => searchBox.FocusAsync(), ButilModifiers.Ctrl); // Handles keyboard shortcuts + + await base.OnAfterFirstRenderAsync(); +} ``` Instead of @@ -276,6 +283,13 @@ Instead of ```csharp [AutoInject] private Keyboard keyboard = default!; +protected override async Task OnAfterFirstRenderAsync() +{ + await keyboard.Add(ButilKeyCodes.KeyF, () => searchBox.FocusAsync(), ButilModifiers.Ctrl); // Handles keyboard shortcuts + + await base.OnAfterFirstRenderAsync(); +} + protected override async ValueTask DisposeAsync(bool disposing) { await keyboard.DisposeAsync(); From 4fd6e6830541cc4fc4f15c66e813a5063a743045 Mon Sep 17 00:00:00 2001 From: ysmoradi Date: Sat, 29 Nov 2025 10:18:58 +0100 Subject: [PATCH 7/7] fix --- .../.docs/09- Dependency Injection & Service Registration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.docs/09- Dependency Injection & Service Registration.md b/src/Templates/Boilerplate/Bit.Boilerplate/.docs/09- Dependency Injection & Service Registration.md index c037d21a6c..3ff858322c 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/.docs/09- Dependency Injection & Service Registration.md +++ b/src/Templates/Boilerplate/Bit.Boilerplate/.docs/09- Dependency Injection & Service Registration.md @@ -268,7 +268,7 @@ To achieve automatic disposal when the component is disposed, inject the service **Example:** ```csharp -Keyboard keyboard => field ??= ScopedServices.GetRequiredService(); // ??= Lazy initialization. It means the service gets resolved when accessed. +Keyboard keyboard => field ??= ScopedServices.GetRequiredService(); // ??= means the service gets resolved when accessed, results into better performance. protected override async Task OnAfterFirstRenderAsync() {