Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Avolutions.Baf.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<Nullable>enable</Nullable>

<PackageId>Avolutions.Baf.Core</PackageId>
<Version>0.17.0</Version>
<Version>0.18.0</Version>

<Title>Avolutions BAF Core</Title>
<Company>Avolutions</Company>
Expand Down
8 changes: 8 additions & 0 deletions src/Loading/Abstractions/IBlockingLoadingService.cs
Original file line number Diff line number Diff line change
@@ -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);
}
9 changes: 9 additions & 0 deletions src/Loading/Abstractions/ILoadingService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Avolutions.Baf.Core.Loading.Abstractions;

public interface ILoadingService
{
bool IsLoading { get; }
event Action? OnLoadingChanged;
void StartLoading();
void StopLoading();
}
6 changes: 4 additions & 2 deletions src/Loading/LoadingModule.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -8,6 +9,7 @@ public class LoadingModule : IFeatureModule
{
public void Register(IServiceCollection services)
{
services.AddScoped<LoadingService>();
services.AddSingleton<ILoadingService, LoadingService>();
services.AddSingleton<IBlockingLoadingService, BlockingLoadingService>();
}
}
32 changes: 32 additions & 0 deletions src/Loading/Services/BlockingLoadingService.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
27 changes: 20 additions & 7 deletions src/Loading/Services/LoadingService.cs
Original file line number Diff line number Diff line change
@@ -1,26 +1,39 @@
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;

public bool IsLoading => _isLoading;

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();
}
}

protected void NotifyStateChanged()
{
OnLoadingChanged?.Invoke();
}
}
9 changes: 9 additions & 0 deletions src/Lookups/Abstractions/ILookupHydrationCache.cs
Original file line number Diff line number Diff line change
@@ -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);
}
8 changes: 8 additions & 0 deletions src/Lookups/Abstractions/ILookupHydrator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Avolutions.Baf.Core.Entity.Abstractions;

namespace Avolutions.Baf.Core.Lookups.Abstractions;

public interface ILookupHydrator
{
void Hydrate(IEntity entity);
}
7 changes: 7 additions & 0 deletions src/Lookups/Attributes/LookupAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Avolutions.Baf.Core.Lookups.Attributes;

[AttributeUsage(AttributeTargets.Property)]
public class LookupAttribute : Attribute
{

}
156 changes: 156 additions & 0 deletions src/Lookups/Cache/LookupHydrationCache.cs
Original file line number Diff line number Diff line change
@@ -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<Type, LookupPropertyMetadata[]> _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<DbContext>();

var newCache = new ConcurrentDictionary<Type, LookupPropertyMetadata[]>();

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<LookupPropertyMetadata>();
var properties = entityType.GetProperties();

foreach (var property in properties)
{
if (property.GetCustomAttribute<LookupAttribute>() == 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();
}

/// <summary>
/// Builds a compiled delegate to get the Id property value from an entity.
/// For an Article with QuantityUnitId, this compiles to:
/// <code>(object entity) => ((Article)entity).QuantityUnitId</code>
/// </summary>
private static Func<object, Guid> BuildGetIdDelegate(Type entityType, PropertyInfo idProperty)
{
var parameter = Expression.Parameter(typeof(object), "entity");
var cast = Expression.Convert(parameter, entityType);
var propertyAccess = Expression.Property(cast, idProperty);

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<Func<object, Guid>>(result, parameter);
return lambda.Compile();
}

/// <summary>
/// Builds a compiled delegate to set the lookup property on an entity.
/// For an Article with QuantityUnit, this compiles to:
/// <code>(object entity, object? value) => ((Article)entity).QuantityUnit = (QuantityUnit)value</code>
/// </summary>
private static Action<object, object?> 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<Action<object, object?>>(assign, entityParam, valueParam);
return lambda.Compile();
}

/// <summary>
/// Builds a delegate to resolve a lookup from cache by Id.
/// This compiles to:
/// <code>(Guid id) => cache.GetByIdAsync(id, CancellationToken.None).Result</code>
/// </summary>
private static Func<Guid, object?> 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);
};
}
}
25 changes: 25 additions & 0 deletions src/Lookups/Interceptors/LookupHydrationInterceptor.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
49 changes: 49 additions & 0 deletions src/Lookups/Interceptors/LookupSaveChangesInterceptor.cs
Original file line number Diff line number Diff line change
@@ -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<int> 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);
}
}
}
Loading