Added support for custom repositories
This commit is contained in:
@@ -2,7 +2,13 @@
|
||||
|
||||
namespace HopFrame.Core.Config;
|
||||
|
||||
public class DbContextConfig {
|
||||
public interface ITableGroupConfig {
|
||||
public Type ContextType { get; }
|
||||
public List<TableConfig> Tables { get; }
|
||||
public HopFrameConfig ParentConfig { get; }
|
||||
}
|
||||
|
||||
public class DbContextConfig : ITableGroupConfig {
|
||||
public Type ContextType { get; }
|
||||
public List<TableConfig> Tables { get; } = new();
|
||||
public HopFrameConfig ParentConfig { get; }
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using HopFrame.Core.Callbacks;
|
||||
using System.Linq.Expressions;
|
||||
using HopFrame.Core.Callbacks;
|
||||
using HopFrame.Core.Repositories;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace HopFrame.Core.Config;
|
||||
|
||||
public class HopFrameConfig {
|
||||
public List<DbContextConfig> Contexts { get; } = new();
|
||||
public List<ITableGroupConfig> Contexts { get; } = new();
|
||||
public bool DisplayUserInfo { get; set; } = true;
|
||||
public string? BasePolicy { get; set; }
|
||||
public string? LoginPageRewrite { get; set; }
|
||||
@@ -48,6 +50,36 @@ public sealed class HopFrameConfigurator(HopFrameConfig config, IServiceCollecti
|
||||
return new DbContextConfigurator<TDbContext>(context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a table of the desired type and configures it to use a custom repository
|
||||
/// </summary>
|
||||
/// <param name="keyExpression">The key of the model</param>
|
||||
/// <param name="configurator">The configurator used for configuring the table page</param>
|
||||
/// <typeparam name="TRepository">The repository class that inherits from the <see cref="IHopFrameRepository{TModel,TKey}"/> (needs to be registered as a service)</typeparam>
|
||||
/// <typeparam name="TModel">The model of the table</typeparam>
|
||||
/// <typeparam name="TKey">The type of the primary key</typeparam>
|
||||
public HopFrameConfigurator AddCustomRepository<TRepository, TModel, TKey>(Expression<Func<TModel, TKey>> keyExpression, Action<TableConfigurator<TModel>> configurator) {
|
||||
var context = AddCustomRepository<TRepository, TModel, TKey>(keyExpression);
|
||||
configurator.Invoke(context);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a table of the desired type and configures it to use a custom repository
|
||||
/// </summary>
|
||||
/// <param name="keyExpression">The key of the model</param>
|
||||
/// <typeparam name="TRepository">The repository class that inherits from the <see cref="IHopFrameRepository{TModel,TKey}"/> (needs to be registered as a service)</typeparam>
|
||||
/// <typeparam name="TModel">The model of the table</typeparam>
|
||||
/// <typeparam name="TKey">The type of the primary key</typeparam>
|
||||
/// <returns>The configurator used for configuring the table page</returns>
|
||||
public TableConfigurator<TModel> AddCustomRepository<TRepository, TModel, TKey>(Expression<Func<TModel, TKey>> keyExpression) {
|
||||
var keyProperty = TableConfigurator<TModel>.GetPropertyInfo(keyExpression);
|
||||
var context = new RepositoryGroupConfig(typeof(TRepository), keyProperty, InnerConfig);
|
||||
context.Tables.Add(new TableConfig(context, typeof(TModel), typeof(TRepository).Name, 0));
|
||||
InnerConfig.Contexts.Add(context);
|
||||
return new TableConfigurator<TModel>(context.Tables[0]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a context is already registered in the HopFrame
|
||||
/// </summary>
|
||||
@@ -64,6 +96,7 @@ public sealed class HopFrameConfigurator(HopFrameConfig config, IServiceCollecti
|
||||
/// <returns>The configurator of the context if it already was defined, null if not</returns>
|
||||
public DbContextConfigurator<TDbContext>? GetDbContext<TDbContext>() where TDbContext : DbContext {
|
||||
var config = InnerConfig.Contexts
|
||||
.OfType<DbContextConfig>()
|
||||
.SingleOrDefault(context => context.ContextType == typeof(TDbContext));
|
||||
if (config is null) return null;
|
||||
|
||||
|
||||
13
src/HopFrame.Core/Config/RepositoryGroupConfig.cs
Normal file
13
src/HopFrame.Core/Config/RepositoryGroupConfig.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace HopFrame.Core.Config;
|
||||
|
||||
public class RepositoryGroupConfig(Type repoType, PropertyInfo keyProperty, HopFrameConfig config) : ITableGroupConfig {
|
||||
public Type ContextType { get; } = repoType;
|
||||
|
||||
public List<TableConfig> Tables { get; } = new();
|
||||
|
||||
public HopFrameConfig ParentConfig { get; } = config;
|
||||
|
||||
public PropertyInfo KeyProperty { get; } = keyProperty;
|
||||
}
|
||||
@@ -11,7 +11,7 @@ public class TableConfig {
|
||||
public string PropertyName { get; }
|
||||
public string DisplayName { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public DbContextConfig ContextConfig { get; }
|
||||
public ITableGroupConfig ContextConfig { get; }
|
||||
public bool Ignored { get; set; }
|
||||
public int Order { get; set; }
|
||||
internal bool Seeded { get; set; }
|
||||
@@ -23,7 +23,7 @@ public class TableConfig {
|
||||
|
||||
public List<PropertyConfig> Properties { get; } = new();
|
||||
|
||||
public TableConfig(DbContextConfig config, Type tableType, string propertyName, int nthTable) {
|
||||
public TableConfig(ITableGroupConfig config, Type tableType, string propertyName, int nthTable) {
|
||||
TableType = tableType;
|
||||
PropertyName = propertyName;
|
||||
ContextConfig = config;
|
||||
|
||||
24
src/HopFrame.Core/Repositories/IHopFrameRepository.cs
Normal file
24
src/HopFrame.Core/Repositories/IHopFrameRepository.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
namespace HopFrame.Core.Repositories;
|
||||
|
||||
public interface IHopFrameRepository<TModel, in TKey> where TModel : class {
|
||||
|
||||
Task<IEnumerable<TModel>> LoadPage(int page, int perPage);
|
||||
|
||||
Task<SearchResult<TModel>> Search(string searchTerm, int page, int perPage);
|
||||
|
||||
Task<int> GetTotalPageCount(int perPage);
|
||||
|
||||
Task CreateItem(TModel item);
|
||||
|
||||
Task EditItem(TModel item);
|
||||
|
||||
Task DeleteItem(TModel item);
|
||||
|
||||
Task<TModel?> GetOne(TKey key);
|
||||
|
||||
}
|
||||
|
||||
public readonly struct SearchResult<TModel>(IEnumerable<TModel> items, int pageCount) {
|
||||
public IEnumerable<TModel> Items { get; init; } = items;
|
||||
public int PageCount { get; init; } = pageCount;
|
||||
}
|
||||
@@ -4,7 +4,7 @@ using HopFrame.Core.Config;
|
||||
namespace HopFrame.Core.Services;
|
||||
|
||||
public interface ITableManager {
|
||||
public IQueryable<object> LoadPage(int page, int perPage = 20);
|
||||
public Task<IEnumerable<object>> LoadPage(int page, int perPage = 20);
|
||||
public Task<(IEnumerable<object>, int)> Search(string searchTerm, int page = 0, int perPage = 20);
|
||||
public Task<int> TotalPages(int perPage = 20);
|
||||
public Task DeleteItem(object item);
|
||||
|
||||
@@ -45,11 +45,18 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
|
||||
var table = context.Tables.FirstOrDefault(table => table.PropertyName == tablePropertyName);
|
||||
if (table is null) continue;
|
||||
|
||||
var dbContext = provider.GetService(context.ContextType) as DbContext;
|
||||
if (dbContext is null) return null;
|
||||
var repo = provider.GetService(context.ContextType);
|
||||
if (repo is null) return null;
|
||||
|
||||
var type = typeof(TableManager<>).MakeGenericType(table.TableType);
|
||||
return Activator.CreateInstance(type, dbContext, table, this, provider) as ITableManager;
|
||||
if (context is DbContextConfig) {
|
||||
var type = typeof(TableManager<>).MakeGenericType(table.TableType);
|
||||
return Activator.CreateInstance(type, (DbContext)repo, table, this, provider) as ITableManager;
|
||||
}
|
||||
|
||||
if (context is RepositoryGroupConfig repoConfig) {
|
||||
var type = typeof(RepositoryTableManager<,>).MakeGenericType(table.TableType, repoConfig.KeyProperty.PropertyType);
|
||||
return Activator.CreateInstance(type, repo, this, provider) as ITableManager;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -60,11 +67,18 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
|
||||
var table = context.Tables.FirstOrDefault(table => table.TableType == tableType);
|
||||
if (table is null) continue;
|
||||
|
||||
var dbContext = provider.GetService(context.ContextType) as DbContext;
|
||||
if (dbContext is null) return null;
|
||||
var repo = provider.GetService(context.ContextType);
|
||||
if (repo is null) return null;
|
||||
|
||||
var type = typeof(TableManager<>).MakeGenericType(table.TableType);
|
||||
return Activator.CreateInstance(type, dbContext, table, this, provider) as ITableManager;
|
||||
if (context is DbContextConfig) {
|
||||
var type = typeof(TableManager<>).MakeGenericType(table.TableType);
|
||||
return Activator.CreateInstance(type, (DbContext)repo, table, this, provider) as ITableManager;
|
||||
}
|
||||
|
||||
if (context is RepositoryGroupConfig repoConfig) {
|
||||
var type = typeof(RepositoryTableManager<,>).MakeGenericType(table.TableType, repoConfig.KeyProperty.PropertyType);
|
||||
return Activator.CreateInstance(type, repo, this, provider) as ITableManager;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -72,6 +86,7 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
|
||||
|
||||
private void SeedTableData(TableConfig table) {
|
||||
if (table.Seeded) return;
|
||||
if (table.ContextConfig is not DbContextConfig) return;
|
||||
var dbContext = (provider.GetRequiredService(table.ContextConfig.ContextType) as DbContext)!;
|
||||
var entity = dbContext.Model.FindEntityType(table.TableType)!;
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
using HopFrame.Core.Config;
|
||||
using HopFrame.Core.Repositories;
|
||||
|
||||
namespace HopFrame.Core.Services.Implementations;
|
||||
|
||||
public class RepositoryTableManager<TModel, TKey>(IHopFrameRepository<TModel, TKey> repo, IContextExplorer explorer, IServiceProvider provider) : ITableManager where TModel : class {
|
||||
public async Task<IEnumerable<object>> LoadPage(int page, int perPage = 20) {
|
||||
return await repo.LoadPage(page, perPage);
|
||||
}
|
||||
public async Task<(IEnumerable<object>, int)> Search(string searchTerm, int page = 0, int perPage = 20) {
|
||||
var result = await repo.Search(searchTerm, page, perPage);
|
||||
return (result.Items, result.PageCount);
|
||||
}
|
||||
public Task<int> TotalPages(int perPage = 20) {
|
||||
return repo.GetTotalPageCount(perPage);
|
||||
}
|
||||
public Task DeleteItem(object item) {
|
||||
return repo.DeleteItem((TModel)item);
|
||||
}
|
||||
public Task EditItem(object item) {
|
||||
return repo.EditItem((TModel)item);
|
||||
}
|
||||
public Task AddItem(object item) {
|
||||
return repo.CreateItem((TModel)item);
|
||||
}
|
||||
public Task AddAll(IEnumerable<object> items) {
|
||||
var tasks = items
|
||||
.Select(item => repo.CreateItem((TModel)item))
|
||||
.ToList();
|
||||
|
||||
return Task.WhenAll(tasks);
|
||||
}
|
||||
public async Task<object?> GetOne(object key) {
|
||||
return await repo.GetOne((TKey)key);
|
||||
}
|
||||
public async Task<string> DisplayProperty(object? item, PropertyConfig prop, object? value = null, object? enumerableValue = null) {
|
||||
var manager = new TableManager<TModel>(null!, null!, explorer, provider);
|
||||
return await manager.DisplayProperty(item, prop, value, enumerableValue);
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,13 @@ namespace HopFrame.Core.Services.Implementations;
|
||||
|
||||
internal sealed class TableManager<TModel>(DbContext context, TableConfig config, IContextExplorer explorer, IServiceProvider provider) : ITableManager where TModel : class {
|
||||
|
||||
public IQueryable<object> LoadPage(int page, int perPage = 20) {
|
||||
public async Task<IEnumerable<object>> LoadPage(int page, int perPage = 20) {
|
||||
var table = context.Set<TModel>();
|
||||
var data = IncludeForeignKeys(table);
|
||||
return data
|
||||
return await data
|
||||
.Skip(page * perPage)
|
||||
.Take(perPage);
|
||||
.Take(perPage)
|
||||
.ToArrayAsync();
|
||||
}
|
||||
|
||||
public Task<(IEnumerable<object>, int)> Search(string searchTerm, int page = 0, int perPage = 20) {
|
||||
|
||||
@@ -340,7 +340,7 @@
|
||||
|
||||
var relationType = config.Info.PropertyType;
|
||||
if (config.IsEnumerable) {
|
||||
relationType = config.Info.PropertyType.GetGenericArguments().First();
|
||||
relationType = relationType.GetGenericArguments().First();
|
||||
}
|
||||
|
||||
var relationTable = Explorer.GetTable(relationType);
|
||||
|
||||
@@ -231,7 +231,7 @@
|
||||
_hasCreatePolicy = await Handler.IsAuthenticatedAsync(_config?.CreatePolicy);
|
||||
|
||||
_manager ??= Explorer.GetTableManager(_config!.PropertyName);
|
||||
CurrentlyDisplayedModels = await _manager!.LoadPage(_currentPage, PerPage).ToArrayAsync();
|
||||
CurrentlyDisplayedModels = (await _manager!.LoadPage(_currentPage, PerPage)).ToArray();
|
||||
_totalPages = await _manager.TotalPages(PerPage);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IToastService to
|
||||
|
||||
[EventHandler]
|
||||
public void OnInit(TableInitializedEvent e) {
|
||||
if (e.Sender.DialogData is not null) return;
|
||||
|
||||
e.AddPageButton("Export", () => Export(e.Table), icon: new Microsoft.FluentUI.AspNetCore.Components.Icons.Regular.Size20.ArrowUpload());
|
||||
e.AddPageButton("Import", () => Import(e.Table, e.Sender), icon: new Microsoft.FluentUI.AspNetCore.Components.Icons.Regular.Size20.ArrowDownload());
|
||||
}
|
||||
@@ -29,8 +31,7 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IToastService to
|
||||
}
|
||||
|
||||
var data = await manager
|
||||
.LoadPage(0, int.MaxValue)
|
||||
.ToArrayAsync();
|
||||
.LoadPage(0, int.MaxValue);
|
||||
|
||||
var properties = table.Properties.Where(prop => !prop.IsVirtualProperty).ToArray();
|
||||
|
||||
@@ -81,6 +82,7 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IToastService to
|
||||
if (property is null) continue;
|
||||
|
||||
object? value = rowValues[i];
|
||||
if (string.IsNullOrWhiteSpace((string)value)) continue;
|
||||
|
||||
if (property.IsEnumerable) {
|
||||
if (!property.Info.PropertyType.IsGenericType) continue;
|
||||
@@ -102,7 +104,7 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IToastService to
|
||||
|
||||
var enumerable = Activator.CreateInstance(property.Info.PropertyType);
|
||||
foreach (var key in values) {
|
||||
var entry = await relationManager.GetOne(ParseString(key, primaryKeyType));
|
||||
var entry = await relationManager.GetOne(ParseString(key, primaryKeyType)!);
|
||||
if (entry is null) continue;
|
||||
|
||||
addMethod.Invoke(enumerable, [entry]);
|
||||
@@ -116,7 +118,7 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IToastService to
|
||||
var relationManager = explorer.GetTableManager(property.Info.PropertyType);
|
||||
var relationPrimaryKeyType = GetPrimaryKeyType(property.Info.PropertyType);
|
||||
if (relationManager is null || relationPrimaryKeyType is null) continue;
|
||||
value = await relationManager.GetOne(ParseString((string)value, relationPrimaryKeyType));
|
||||
value = await relationManager.GetOne(ParseString((string)value, relationPrimaryKeyType)!);
|
||||
}
|
||||
else if (property.Info.PropertyType == typeof(Guid)) {
|
||||
var success = Guid.TryParse((string)value, out var guid);
|
||||
@@ -151,13 +153,20 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IToastService to
|
||||
|
||||
if (property.IsEnumerable) {
|
||||
var enumerable = (IEnumerable)value;
|
||||
return '[' + string.Join(',', enumerable.OfType<object>().Select(o => SelectPrimaryKey(o) ?? o.ToString())) + ']';
|
||||
return '[' + string.Join(',', enumerable.OfType<object>().Select(o => SelectPrimaryKey(o, property) ?? o.ToString())) + ']';
|
||||
}
|
||||
|
||||
return SelectPrimaryKey(value) ?? value.ToString() ?? string.Empty;
|
||||
return SelectPrimaryKey(value, property) ?? value.ToString() ?? string.Empty;
|
||||
}
|
||||
|
||||
private string? SelectPrimaryKey(object entity) {
|
||||
private string? SelectPrimaryKey(object entity, PropertyConfig config) {
|
||||
if (config.IsRelation) {
|
||||
var table = explorer.GetTable(entity.GetType());
|
||||
if (table?.ContextConfig is RepositoryGroupConfig repoConfig) {
|
||||
return repoConfig.KeyProperty.GetValue(entity)?.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
return entity
|
||||
.GetType()
|
||||
.GetProperties()
|
||||
@@ -169,6 +178,11 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IToastService to
|
||||
}
|
||||
|
||||
private Type? GetPrimaryKeyType(Type tableType) {
|
||||
var table = explorer.GetTable(tableType);
|
||||
if (table?.ContextConfig is RepositoryGroupConfig repoConfig) {
|
||||
return repoConfig.KeyProperty.PropertyType;
|
||||
}
|
||||
|
||||
return tableType
|
||||
.GetProperties()
|
||||
.FirstOrDefault(prop => prop
|
||||
|
||||
Reference in New Issue
Block a user