From c8cf6442de8a830c41c74b5005ed8300487d84dd Mon Sep 17 00:00:00 2001 From: AlxndrVgt Date: Thu, 11 Dec 2025 10:08:56 +0100 Subject: [PATCH 1/7] feat: add lookup hydration --- .../Abstractions/ILookupHydrationCache.cs | 9 ++ src/Lookups/Abstractions/ILookupHydrator.cs | 8 + src/Lookups/Attributes/LookupAttribute.cs | 7 + src/Lookups/Cache/LookupHydrationCache.cs | 144 ++++++++++++++++++ .../LookupHydrationInterceptor.cs | 25 +++ .../LookupSaveChangesInterceptor.cs | 49 ++++++ src/Lookups/Lookups.cs | 21 +++ src/Lookups/Models/LookupPropertyMetadata.cs | 8 + src/Lookups/Services/LookupHydrator.cs | 34 +++++ .../Extensions/ServiceCollectionExtensions.cs | 5 +- 10 files changed, 309 insertions(+), 1 deletion(-) create mode 100644 src/Lookups/Abstractions/ILookupHydrationCache.cs create mode 100644 src/Lookups/Abstractions/ILookupHydrator.cs create mode 100644 src/Lookups/Attributes/LookupAttribute.cs create mode 100644 src/Lookups/Cache/LookupHydrationCache.cs create mode 100644 src/Lookups/Interceptors/LookupHydrationInterceptor.cs create mode 100644 src/Lookups/Interceptors/LookupSaveChangesInterceptor.cs create mode 100644 src/Lookups/Lookups.cs create mode 100644 src/Lookups/Models/LookupPropertyMetadata.cs create mode 100644 src/Lookups/Services/LookupHydrator.cs diff --git a/src/Lookups/Abstractions/ILookupHydrationCache.cs b/src/Lookups/Abstractions/ILookupHydrationCache.cs new file mode 100644 index 0000000..54f2241 --- /dev/null +++ b/src/Lookups/Abstractions/ILookupHydrationCache.cs @@ -0,0 +1,9 @@ +using Avolutions.Baf.Core.Caching.Abstractions; +using Avolutions.Baf.Core.Lookups.Models; + +namespace Avolutions.Baf.Core.Lookups.Abstractions; + +public interface ILookupHydrationCache : ICache +{ + LookupPropertyMetadata[]? GetMetadata(Type entityType); +} \ No newline at end of file diff --git a/src/Lookups/Abstractions/ILookupHydrator.cs b/src/Lookups/Abstractions/ILookupHydrator.cs new file mode 100644 index 0000000..a01cf63 --- /dev/null +++ b/src/Lookups/Abstractions/ILookupHydrator.cs @@ -0,0 +1,8 @@ +using Avolutions.Baf.Core.Entity.Abstractions; + +namespace Avolutions.Baf.Core.Lookups.Abstractions; + +public interface ILookupHydrator +{ + void Hydrate(IEntity entity); +} \ No newline at end of file diff --git a/src/Lookups/Attributes/LookupAttribute.cs b/src/Lookups/Attributes/LookupAttribute.cs new file mode 100644 index 0000000..63344f1 --- /dev/null +++ b/src/Lookups/Attributes/LookupAttribute.cs @@ -0,0 +1,7 @@ +namespace Avolutions.Baf.Core.Lookups.Attributes; + +[AttributeUsage(AttributeTargets.Property)] +public class LookupAttribute : Attribute +{ + +} \ No newline at end of file diff --git a/src/Lookups/Cache/LookupHydrationCache.cs b/src/Lookups/Cache/LookupHydrationCache.cs new file mode 100644 index 0000000..78e4335 --- /dev/null +++ b/src/Lookups/Cache/LookupHydrationCache.cs @@ -0,0 +1,144 @@ +using System.Collections.Concurrent; +using System.Linq.Expressions; +using System.Reflection; +using Avolutions.Baf.Core.Caching.Abstractions; +using Avolutions.Baf.Core.Lookups.Abstractions; +using Avolutions.Baf.Core.Lookups.Attributes; +using Avolutions.Baf.Core.Lookups.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Avolutions.Baf.Core.Lookups.Cache; + +public class LookupHydrationCache : ILookupHydrationCache +{ + private readonly IServiceScopeFactory _scopeFactory; + private readonly IServiceProvider _serviceProvider; + private ConcurrentDictionary _cache = new(); + + public LookupHydrationCache(IServiceScopeFactory scopeFactory, IServiceProvider serviceProvider) + { + _scopeFactory = scopeFactory; + _serviceProvider = serviceProvider; + } + + public LookupPropertyMetadata[]? GetMetadata(Type entityType) + { + return _cache.TryGetValue(entityType, out var metadata) ? metadata : null; + } + + public Task RefreshAsync(CancellationToken cancellationToken = default) + { + using var scope = _scopeFactory.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + + var newCache = new ConcurrentDictionary(); + + foreach (var entityType in context.Model.GetEntityTypes()) + { + var clrType = entityType.ClrType; + var metadata = BuildMetadata(clrType); + + if (metadata.Length > 0) + { + newCache[clrType] = metadata; + } + } + + _cache = newCache; + return Task.CompletedTask; + } + + private LookupPropertyMetadata[] BuildMetadata(Type entityType) + { + var result = new List(); + var properties = entityType.GetProperties(); + + foreach (var property in properties) + { + if (property.GetCustomAttribute() == null) + { + continue; + } + + var idPropertyName = $"{property.Name}Id"; + var idProperty = entityType.GetProperty(idPropertyName); + + if (idProperty == null || idProperty.PropertyType != typeof(Guid)) + { + continue; + } + + var cacheType = typeof(ILookupCache<>).MakeGenericType(property.PropertyType); + var cache = _serviceProvider.GetService(cacheType); + + if (cache == null) + { + continue; + } + + var baseCacheType = typeof(ICache<>).MakeGenericType(property.PropertyType); + var getByIdMethod = baseCacheType.GetMethod("GetByIdAsync", [typeof(Guid), typeof(CancellationToken)]); + + if (getByIdMethod == null) + { + continue; + } + + result.Add(new LookupPropertyMetadata + { + GetId = BuildGetIdDelegate(entityType, idProperty), + SetLookup = BuildSetLookupDelegate(entityType, property), + GetFromCache = BuildCacheResolver(cache, getByIdMethod) + }); + } + + return result.ToArray(); + } + + /// + /// Builds a compiled delegate to get the Id property value from an entity. + /// For an Article with QuantityUnitId, this compiles to: + /// (object entity) => ((Article)entity).QuantityUnitId + /// + private static Func BuildGetIdDelegate(Type entityType, PropertyInfo idProperty) + { + var parameter = Expression.Parameter(typeof(object), "entity"); + var cast = Expression.Convert(parameter, entityType); + var propertyAccess = Expression.Property(cast, idProperty); + var lambda = Expression.Lambda>(propertyAccess, parameter); + return lambda.Compile(); + } + + /// + /// Builds a compiled delegate to set the lookup property on an entity. + /// For an Article with QuantityUnit, this compiles to: + /// (object entity, object? value) => ((Article)entity).QuantityUnit = (QuantityUnit)value + /// + private static Action BuildSetLookupDelegate(Type entityType, PropertyInfo lookupProperty) + { + var entityParam = Expression.Parameter(typeof(object), "entity"); + var valueParam = Expression.Parameter(typeof(object), "value"); + var castEntity = Expression.Convert(entityParam, entityType); + var castValue = Expression.Convert(valueParam, lookupProperty.PropertyType); + var propertyAccess = Expression.Property(castEntity, lookupProperty); + var assign = Expression.Assign(propertyAccess, castValue); + var lambda = Expression.Lambda>(assign, entityParam, valueParam); + return lambda.Compile(); + } + + /// + /// Builds a delegate to resolve a lookup from cache by Id. + /// This compiles to: + /// (Guid id) => cache.GetByIdAsync(id, CancellationToken.None).Result + /// + private static Func BuildCacheResolver(object cache, MethodInfo getByIdMethod) + { + return id => + { + var task = (Task)getByIdMethod.Invoke(cache, [id, CancellationToken.None])!; + task.GetAwaiter().GetResult(); + return task.GetType().GetProperty("Result")!.GetValue(task); + }; + } +} \ No newline at end of file diff --git a/src/Lookups/Interceptors/LookupHydrationInterceptor.cs b/src/Lookups/Interceptors/LookupHydrationInterceptor.cs new file mode 100644 index 0000000..1bdb28d --- /dev/null +++ b/src/Lookups/Interceptors/LookupHydrationInterceptor.cs @@ -0,0 +1,25 @@ +using Avolutions.Baf.Core.Entity.Abstractions; +using Avolutions.Baf.Core.Lookups.Abstractions; +using Microsoft.EntityFrameworkCore.Diagnostics; + +namespace Avolutions.Baf.Core.Lookups.Interceptors; + +public class LookupHydrationInterceptor : IMaterializationInterceptor +{ + private readonly ILookupHydrator _hydrator; + + public LookupHydrationInterceptor(ILookupHydrator hydrator) + { + _hydrator = hydrator; + } + + public object InitializedInstance(MaterializationInterceptionData materializationData, object entity) + { + if (entity is IEntity entityBase) + { + _hydrator.Hydrate(entityBase); + } + + return entity; + } +} \ No newline at end of file diff --git a/src/Lookups/Interceptors/LookupSaveChangesInterceptor.cs b/src/Lookups/Interceptors/LookupSaveChangesInterceptor.cs new file mode 100644 index 0000000..8d73485 --- /dev/null +++ b/src/Lookups/Interceptors/LookupSaveChangesInterceptor.cs @@ -0,0 +1,49 @@ +using Avolutions.Baf.Core.Entity.Abstractions; +using Avolutions.Baf.Core.Lookups.Abstractions; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; + +namespace Avolutions.Baf.Core.Lookups.Interceptors; + +public class LookupSaveChangesInterceptor : SaveChangesInterceptor +{ + private readonly ILookupHydrator _hydrator; + + public LookupSaveChangesInterceptor(ILookupHydrator hydrator) + { + _hydrator = hydrator; + } + + public override int SavedChanges(SaveChangesCompletedEventData eventData, int result) + { + HydrateEntities(eventData.Context); + return base.SavedChanges(eventData, result); + } + + public override ValueTask SavedChangesAsync( + SaveChangesCompletedEventData eventData, + int result, + CancellationToken cancellationToken = default) + { + HydrateEntities(eventData.Context); + return base.SavedChangesAsync(eventData, result, cancellationToken); + } + + private void HydrateEntities(DbContext? context) + { + if (context == null) + { + return; + } + + var entries = context.ChangeTracker.Entries() + .Where(e => e.Entity is IEntity) + .Where(e => e.Entity is not ILookup) + .Where(e => e.State is EntityState.Added or EntityState.Modified); + + foreach (var entry in entries) + { + _hydrator.Hydrate((IEntity)entry.Entity); + } + } +} \ No newline at end of file diff --git a/src/Lookups/Lookups.cs b/src/Lookups/Lookups.cs new file mode 100644 index 0000000..d287322 --- /dev/null +++ b/src/Lookups/Lookups.cs @@ -0,0 +1,21 @@ +using Avolutions.Baf.Core.Caching.Abstractions; +using Avolutions.Baf.Core.Lookups.Abstractions; +using Avolutions.Baf.Core.Lookups.Cache; +using Avolutions.Baf.Core.Lookups.Interceptors; +using Avolutions.Baf.Core.Lookups.Services; +using Avolutions.Baf.Core.Module.Abstractions; +using Microsoft.Extensions.DependencyInjection; + +namespace Avolutions.Baf.Core.Lookups; + +public class Lookups : IFeatureModule +{ + public void Register(IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(sp => sp.GetRequiredService()); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + } +} \ No newline at end of file diff --git a/src/Lookups/Models/LookupPropertyMetadata.cs b/src/Lookups/Models/LookupPropertyMetadata.cs new file mode 100644 index 0000000..a302396 --- /dev/null +++ b/src/Lookups/Models/LookupPropertyMetadata.cs @@ -0,0 +1,8 @@ +namespace Avolutions.Baf.Core.Lookups.Models; + +public class LookupPropertyMetadata +{ + public required Func GetId { get; init; } + public required Action SetLookup { get; init; } + public required Func GetFromCache { get; init; } +} \ No newline at end of file diff --git a/src/Lookups/Services/LookupHydrator.cs b/src/Lookups/Services/LookupHydrator.cs new file mode 100644 index 0000000..c456b79 --- /dev/null +++ b/src/Lookups/Services/LookupHydrator.cs @@ -0,0 +1,34 @@ +using Avolutions.Baf.Core.Entity.Abstractions; +using Avolutions.Baf.Core.Lookups.Abstractions; + +namespace Avolutions.Baf.Core.Lookups.Services; + +public class LookupHydrator : ILookupHydrator +{ + private readonly ILookupHydrationCache _hydrationCache; + + public LookupHydrator(ILookupHydrationCache hydrationCache) + { + _hydrationCache = hydrationCache; + } + + public void Hydrate(IEntity entity) + { + var metadata = _hydrationCache.GetMetadata(entity.GetType()); + if (metadata == null) + return; + + foreach (var prop in metadata) + { + var id = prop.GetId(entity); + if (id == Guid.Empty) + continue; + + var lookup = prop.GetFromCache(id); + if (lookup != null) + { + prop.SetLookup(entity, lookup); + } + } + } +} \ No newline at end of file diff --git a/src/Module/Extensions/ServiceCollectionExtensions.cs b/src/Module/Extensions/ServiceCollectionExtensions.cs index fcd57e2..10236b5 100644 --- a/src/Module/Extensions/ServiceCollectionExtensions.cs +++ b/src/Module/Extensions/ServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using System.Reflection; using Avolutions.Baf.Core.Audit.Interceptors; using Avolutions.Baf.Core.Entity.Interceptors; +using Avolutions.Baf.Core.Lookups.Interceptors; using Avolutions.Baf.Core.Module.Abstractions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -51,7 +52,9 @@ public static IServiceCollection AddBafCore(this IServiceCollection se { options.AddInterceptors( sp.GetRequiredService(), - sp.GetRequiredService() + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService() ); }); From da3b8c201c0651b638ca05d8d781a85b3a05c65a Mon Sep 17 00:00:00 2001 From: AlxndrVgt Date: Fri, 12 Dec 2025 16:03:25 +0100 Subject: [PATCH 2/7] fix: nullable lookup ids were not hydrated --- src/Lookups/Cache/LookupHydrationCache.cs | 16 ++++++++++++++-- src/Lookups/Services/LookupHydrator.cs | 4 ++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Lookups/Cache/LookupHydrationCache.cs b/src/Lookups/Cache/LookupHydrationCache.cs index 78e4335..7972601 100644 --- a/src/Lookups/Cache/LookupHydrationCache.cs +++ b/src/Lookups/Cache/LookupHydrationCache.cs @@ -64,7 +64,7 @@ private LookupPropertyMetadata[] BuildMetadata(Type entityType) var idPropertyName = $"{property.Name}Id"; var idProperty = entityType.GetProperty(idPropertyName); - if (idProperty == null || idProperty.PropertyType != typeof(Guid)) + if (idProperty == null || (idProperty.PropertyType != typeof(Guid) && idProperty.PropertyType != typeof(Guid?))) { continue; } @@ -106,7 +106,19 @@ private static Func BuildGetIdDelegate(Type entityType, PropertyIn var parameter = Expression.Parameter(typeof(object), "entity"); var cast = Expression.Convert(parameter, entityType); var propertyAccess = Expression.Property(cast, idProperty); - var lambda = Expression.Lambda>(propertyAccess, parameter); + + Expression result; + if (idProperty.PropertyType == typeof(Guid?)) + { + var emptyGuid = Expression.Constant(Guid.Empty); + result = Expression.Coalesce(propertyAccess, emptyGuid); + } + else + { + result = propertyAccess; + } + + var lambda = Expression.Lambda>(result, parameter); return lambda.Compile(); } diff --git a/src/Lookups/Services/LookupHydrator.cs b/src/Lookups/Services/LookupHydrator.cs index c456b79..d525493 100644 --- a/src/Lookups/Services/LookupHydrator.cs +++ b/src/Lookups/Services/LookupHydrator.cs @@ -16,13 +16,17 @@ public void Hydrate(IEntity entity) { var metadata = _hydrationCache.GetMetadata(entity.GetType()); if (metadata == null) + { return; + } foreach (var prop in metadata) { var id = prop.GetId(entity); if (id == Guid.Empty) + { continue; + } var lookup = prop.GetFromCache(id); if (lookup != null) From b5183c1d2b2634708ec535a32038ed711bdc4d6f Mon Sep 17 00:00:00 2001 From: AlxndrVgt Date: Fri, 12 Dec 2025 16:37:26 +0100 Subject: [PATCH 3/7] feat: add method to get field names from templates --- .../Services/HandlebarsTemplateService.cs | 7 ++- src/Template/Services/PdfTemplateService.cs | 9 +++- src/Template/Services/TemplateService.cs | 13 +++--- src/Template/Services/WordTemplateService.cs | 44 +++++++++++++++++-- 4 files changed, 61 insertions(+), 12 deletions(-) diff --git a/src/Template/Services/HandlebarsTemplateService.cs b/src/Template/Services/HandlebarsTemplateService.cs index e313821..9cfcfd4 100644 --- a/src/Template/Services/HandlebarsTemplateService.cs +++ b/src/Template/Services/HandlebarsTemplateService.cs @@ -4,7 +4,12 @@ namespace Avolutions.Baf.Core.Template.Services; public class HandlebarsTemplateService : TemplateService { - protected override Task ApplyValuesToTemplateAsync(string template, IDictionary values, CancellationToken ct) + public override IReadOnlyList ExtractFieldNames(Stream template) + { + throw new NotImplementedException(); + } + + public override Task ApplyValuesToTemplateAsync(string template, IDictionary values, CancellationToken ct) { var compiledTemplate = Handlebars.Compile(template); var result = compiledTemplate(values); diff --git a/src/Template/Services/PdfTemplateService.cs b/src/Template/Services/PdfTemplateService.cs index 0e03127..4a5a897 100644 --- a/src/Template/Services/PdfTemplateService.cs +++ b/src/Template/Services/PdfTemplateService.cs @@ -18,8 +18,13 @@ public PdfTemplateService() _fontsConfigured = true; } } - - protected override Task ApplyValuesToTemplateAsync(Stream template, IDictionary values, CancellationToken ct) + + public override IReadOnlyList ExtractFieldNames(Stream template) + { + throw new NotImplementedException(); + } + + public override Task ApplyValuesToTemplateAsync(Stream template, IDictionary values, CancellationToken ct) { using var templateBuffer = new MemoryStream(); template.CopyTo(templateBuffer); diff --git a/src/Template/Services/TemplateService.cs b/src/Template/Services/TemplateService.cs index 8c94104..ae6740d 100644 --- a/src/Template/Services/TemplateService.cs +++ b/src/Template/Services/TemplateService.cs @@ -15,18 +15,21 @@ public Task ApplyModelToTemplateAsync(TTemplate template, object model, return ApplyValuesToTemplateAsync(template, values, ct); } - - protected abstract Task ApplyValuesToTemplateAsync( + + public abstract IReadOnlyList ExtractFieldNames(Stream template); + + public abstract Task ApplyValuesToTemplateAsync( TTemplate template, IDictionary values, CancellationToken ct); - protected Dictionary BuildValueDictionary(object model) + protected virtual Dictionary BuildValueDictionary(object model) { - var result = new Dictionary(StringComparer.OrdinalIgnoreCase); var type = model.GetType(); + var properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public); + var result = new Dictionary(properties.Length, StringComparer.OrdinalIgnoreCase); - foreach (var property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public)) + foreach (var property in properties) { if (!property.CanRead) { diff --git a/src/Template/Services/WordTemplateService.cs b/src/Template/Services/WordTemplateService.cs index 8fbb727..705d0f3 100644 --- a/src/Template/Services/WordTemplateService.cs +++ b/src/Template/Services/WordTemplateService.cs @@ -6,10 +6,48 @@ namespace Avolutions.Baf.Core.Template.Services; public class WordTemplateService : TemplateService { - protected override Task ApplyValuesToTemplateAsync(Stream template, IDictionary values, CancellationToken ct) + public override IReadOnlyList ExtractFieldNames(Stream template) + { + var fieldNames = new HashSet(StringComparer.OrdinalIgnoreCase); + + using var document = WordprocessingDocument.Open(template, false); + + var body = document.MainDocumentPart?.Document.Body; + if (body != null) + { + ExtractMergeFieldNames(body, fieldNames); + } + + return fieldNames.ToList(); + } + + private static void ExtractMergeFieldNames(OpenXmlElement root, HashSet fieldNames) + { + foreach (var fieldCode in root.Descendants()) + { + var instruction = fieldCode.Text; + if (string.IsNullOrWhiteSpace(instruction)) + { + continue; + } + + if (!instruction.Contains("MERGEFIELD", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var fieldName = ExtractMergeFieldName(instruction); + if (fieldName != null) + { + fieldNames.Add(fieldName); + } + } + } + + public override Task ApplyValuesToTemplateAsync(Stream template, IDictionary values, CancellationToken ct) { // Copy to a writable, seekable stream - var output = new MemoryStream(); + using var output = new MemoryStream(); template.CopyTo(output); output.Position = 0; @@ -21,8 +59,6 @@ protected override Task ApplyValuesToTemplateAsync(Stream template, IDic ReplaceMergeFields(body, values); } } - - output.Position = 0; return Task.FromResult(output.ToArray()); } From 182eecd8f5202542a95575b9234f10f37f89782d Mon Sep 17 00:00:00 2001 From: AlxndrVgt Date: Fri, 12 Dec 2025 16:37:43 +0100 Subject: [PATCH 4/7] feat: add method to get field names from template --- src/Template/Abstractions/ITemplateService.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Template/Abstractions/ITemplateService.cs b/src/Template/Abstractions/ITemplateService.cs index 80276ba..80a6b09 100644 --- a/src/Template/Abstractions/ITemplateService.cs +++ b/src/Template/Abstractions/ITemplateService.cs @@ -3,4 +3,5 @@ public interface ITemplateService { Task ApplyModelToTemplateAsync(TTemplate template, object model, CancellationToken ct); + IReadOnlyList ExtractFieldNames(Stream template); } \ No newline at end of file From b0548f3cfeab169527b59e75eaea0d9d887e204c Mon Sep 17 00:00:00 2001 From: AlxndrVgt Date: Sun, 14 Dec 2025 15:59:14 +0100 Subject: [PATCH 5/7] refactor: use correct name for lookups module --- src/Lookups/{Lookups.cs => LookupsModule.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/Lookups/{Lookups.cs => LookupsModule.cs} (94%) diff --git a/src/Lookups/Lookups.cs b/src/Lookups/LookupsModule.cs similarity index 94% rename from src/Lookups/Lookups.cs rename to src/Lookups/LookupsModule.cs index d287322..d7574a4 100644 --- a/src/Lookups/Lookups.cs +++ b/src/Lookups/LookupsModule.cs @@ -8,7 +8,7 @@ namespace Avolutions.Baf.Core.Lookups; -public class Lookups : IFeatureModule +public class LookupsModule : IFeatureModule { public void Register(IServiceCollection services) { From daee1ab4cb53a207ffcb32a3179f0f124ca4d7bb Mon Sep 17 00:00:00 2001 From: AlxndrVgt Date: Sun, 14 Dec 2025 18:57:08 +0100 Subject: [PATCH 6/7] feat: add blocking loading service --- .../Abstractions/IBlockingLoadingService.cs | 8 +++++ src/Loading/Abstractions/ILoadingService.cs | 9 ++++++ src/Loading/LoadingModule.cs | 6 ++-- .../Services/BlockingLoadingService.cs | 32 +++++++++++++++++++ src/Loading/Services/LoadingService.cs | 27 ++++++++++++---- 5 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 src/Loading/Abstractions/IBlockingLoadingService.cs create mode 100644 src/Loading/Abstractions/ILoadingService.cs create mode 100644 src/Loading/Services/BlockingLoadingService.cs diff --git a/src/Loading/Abstractions/IBlockingLoadingService.cs b/src/Loading/Abstractions/IBlockingLoadingService.cs new file mode 100644 index 0000000..61b25ae --- /dev/null +++ b/src/Loading/Abstractions/IBlockingLoadingService.cs @@ -0,0 +1,8 @@ +namespace Avolutions.Baf.Core.Loading.Abstractions; + +public interface IBlockingLoadingService : ILoadingService +{ + string LoadingText { get; } + void StartLoading(string text); + void UpdateText(string text); +} \ No newline at end of file diff --git a/src/Loading/Abstractions/ILoadingService.cs b/src/Loading/Abstractions/ILoadingService.cs new file mode 100644 index 0000000..eb6a626 --- /dev/null +++ b/src/Loading/Abstractions/ILoadingService.cs @@ -0,0 +1,9 @@ +namespace Avolutions.Baf.Core.Loading.Abstractions; + +public interface ILoadingService +{ + bool IsLoading { get; } + event Action? OnLoadingChanged; + void StartLoading(); + void StopLoading(); +} \ No newline at end of file diff --git a/src/Loading/LoadingModule.cs b/src/Loading/LoadingModule.cs index cf51347..ff941bd 100644 --- a/src/Loading/LoadingModule.cs +++ b/src/Loading/LoadingModule.cs @@ -1,4 +1,5 @@ -using Avolutions.Baf.Core.Loading.Services; +using Avolutions.Baf.Core.Loading.Abstractions; +using Avolutions.Baf.Core.Loading.Services; using Avolutions.Baf.Core.Module.Abstractions; using Microsoft.Extensions.DependencyInjection; @@ -8,6 +9,7 @@ public class LoadingModule : IFeatureModule { public void Register(IServiceCollection services) { - services.AddScoped(); + services.AddSingleton(); + services.AddSingleton(); } } \ No newline at end of file diff --git a/src/Loading/Services/BlockingLoadingService.cs b/src/Loading/Services/BlockingLoadingService.cs new file mode 100644 index 0000000..3375df0 --- /dev/null +++ b/src/Loading/Services/BlockingLoadingService.cs @@ -0,0 +1,32 @@ +using Avolutions.Baf.Core.Loading.Abstractions; + +namespace Avolutions.Baf.Core.Loading.Services; + +public class BlockingLoadingService : LoadingService, IBlockingLoadingService +{ + private string _loadingText = string.Empty; + public string LoadingText => _loadingText; + + public void StartLoading(string text) + { + _loadingText = text; + base.StartLoading(); + } + + public void UpdateText(string text) + { + if (!IsLoading) + { + return; + } + + _loadingText = text; + NotifyStateChanged(); + } + + public override void StopLoading() + { + base.StopLoading(); + _loadingText = string.Empty; + } +} diff --git a/src/Loading/Services/LoadingService.cs b/src/Loading/Services/LoadingService.cs index 854d6ea..ab9371b 100644 --- a/src/Loading/Services/LoadingService.cs +++ b/src/Loading/Services/LoadingService.cs @@ -1,6 +1,8 @@ -namespace Avolutions.Baf.Core.Loading.Services; +using Avolutions.Baf.Core.Loading.Abstractions; -public class LoadingService +namespace Avolutions.Baf.Core.Loading.Services; + +public class LoadingService : ILoadingService { private bool _isLoading; @@ -8,19 +10,30 @@ public class LoadingService public event Action? OnLoadingChanged; - public void StartLoading() + public virtual void StartLoading() { - if (_isLoading) return; + if (_isLoading) + { + return; + } _isLoading = true; OnLoadingChanged?.Invoke(); } - public void StopLoading() + public virtual void StopLoading() { - if (!_isLoading) return; + if (!_isLoading) + { + return; + } _isLoading = false; OnLoadingChanged?.Invoke(); } -} \ No newline at end of file + + protected void NotifyStateChanged() + { + OnLoadingChanged?.Invoke(); + } +} From ef6f741c87ee5962f132549c832c20c8aa10718c Mon Sep 17 00:00:00 2001 From: AlxndrVgt Date: Tue, 16 Dec 2025 12:17:02 +0100 Subject: [PATCH 7/7] chore: set version to 0.18.0 --- src/Avolutions.Baf.Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avolutions.Baf.Core.csproj b/src/Avolutions.Baf.Core.csproj index 53f898c..d1622ae 100644 --- a/src/Avolutions.Baf.Core.csproj +++ b/src/Avolutions.Baf.Core.csproj @@ -6,7 +6,7 @@ enable Avolutions.Baf.Core - 0.17.0 + 0.18.0 Avolutions BAF Core Avolutions