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
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();
+ }
+}
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..7972601
--- /dev/null
+++ b/src/Lookups/Cache/LookupHydrationCache.cs
@@ -0,0 +1,156 @@
+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) && 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