Added relation support

This commit is contained in:
Leon Hoppe
2025-01-15 08:02:23 +01:00
parent c4c0424559
commit ad4d9c65d6
12 changed files with 93 additions and 15 deletions

View File

@@ -1,4 +1,5 @@
using System.Reflection; using System.Linq.Expressions;
using System.Reflection;
namespace HopFrame.Core.Config; namespace HopFrame.Core.Config;
@@ -8,6 +9,7 @@ public class PropertyConfig(PropertyInfo info) {
public bool List { get; set; } = true; public bool List { get; set; } = true;
public bool Sortable { get; set; } = true; public bool Sortable { get; set; } = true;
public bool Searchable { get; set; } = true; public bool Searchable { get; set; } = true;
public PropertyInfo? DisplayedProperty { get; set; }
} }
public class PropertyConfig<TProp>(PropertyConfig config) { public class PropertyConfig<TProp>(PropertyConfig config) {
@@ -33,4 +35,9 @@ public class PropertyConfig<TProp>(PropertyConfig config) {
return this; return this;
} }
public PropertyConfig<TProp> DisplayedProperty<TInnerProp>(Expression<Func<TProp, TInnerProp>> propertyExpression) {
config.DisplayedProperty = TableConfig<TProp>.GetPropertyInfo(propertyExpression);
return this;
}
} }

View File

@@ -42,7 +42,7 @@ public class TableConfig<TModel>(TableConfig innerConfig) {
return this; return this;
} }
private static PropertyInfo GetPropertyInfo<TSource, TProperty>(Expression<Func<TSource, TProperty>> propertyLambda) { internal static PropertyInfo GetPropertyInfo<TSource, TProperty>(Expression<Func<TSource, TProperty>> propertyLambda) {
if (propertyLambda.Body is not MemberExpression member) { if (propertyLambda.Body is not MemberExpression member) {
throw new ArgumentException($"Expression '{propertyLambda}' refers to a method, not a property."); throw new ArgumentException($"Expression '{propertyLambda}' refers to a method, not a property.");
} }

View File

@@ -5,5 +5,6 @@ namespace HopFrame.Core.Services;
public interface IContextExplorer { public interface IContextExplorer {
public IEnumerable<string> GetTableNames(); public IEnumerable<string> GetTableNames();
public TableConfig? GetTable(string tableName); public TableConfig? GetTable(string tableName);
public TableConfig? GetTable(Type tableEntity);
public ITableManager? GetTableManager(string tableName); public ITableManager? GetTableManager(string tableName);
} }

View File

@@ -1,8 +1,13 @@
namespace HopFrame.Core.Services; using System.Reflection;
using HopFrame.Core.Config;
namespace HopFrame.Core.Services;
public interface ITableManager { public interface ITableManager {
public IQueryable<object> LoadPage(int page, int perPage = 20); public IQueryable<object> LoadPage(int page, int perPage = 20);
public (IEnumerable<object>, int) Search(string searchTerm, int page = 0, int perPage = 20); public (IEnumerable<object>, int) Search(string searchTerm, int page = 0, int perPage = 20);
public int TotalPages(int perPage = 20); public int TotalPages(int perPage = 20);
public Task DeleteItem(object item); public Task DeleteItem(object item);
public string DisplayProperty(object item, PropertyInfo info, TableConfig? tableConfig);
} }

View File

@@ -23,6 +23,16 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
return null; return null;
} }
public TableConfig? GetTable(Type tableEntity) {
foreach (var context in config.Contexts) {
var table = context.Tables.FirstOrDefault(table => table.TableType == tableEntity);
if (table is not null)
return table;
}
return null;
}
public ITableManager? GetTableManager(string tableName) { public ITableManager? GetTableManager(string tableName) {
foreach (var context in config.Contexts) { foreach (var context in config.Contexts) {
var table = context.Tables.FirstOrDefault(table => table.PropertyName == tableName); var table = context.Tables.FirstOrDefault(table => table.PropertyName == tableName);
@@ -32,9 +42,10 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
if (dbContext is null) return null; if (dbContext is null) return null;
var type = typeof(TableManager<>).MakeGenericType(table.TableType); var type = typeof(TableManager<>).MakeGenericType(table.TableType);
return Activator.CreateInstance(type, dbContext, table) as ITableManager; return Activator.CreateInstance(type, dbContext, table, this) as ITableManager;
} }
return null; return null;
} }
} }

View File

@@ -1,20 +1,23 @@
using HopFrame.Core.Config; using System.ComponentModel.DataAnnotations.Schema;
using System.Reflection;
using HopFrame.Core.Config;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace HopFrame.Core.Services.Implementations; namespace HopFrame.Core.Services.Implementations;
internal sealed class TableManager<TModel>(DbContext context, TableConfig config) : ITableManager where TModel : class { internal sealed class TableManager<TModel>(DbContext context, TableConfig config, IContextExplorer explorer) : ITableManager where TModel : class {
public IQueryable<object> LoadPage(int page, int perPage = 20) { public IQueryable<object> LoadPage(int page, int perPage = 20) {
var table = context.Set<TModel>(); var table = context.Set<TModel>();
return table var data = IncludeForgeinKeys(table);
return data
.Skip(page * perPage) .Skip(page * perPage)
.Take(perPage); .Take(perPage);
} }
public (IEnumerable<object>, int) Search(string searchTerm, int page = 0, int perPage = 20) { public (IEnumerable<object>, int) Search(string searchTerm, int page = 0, int perPage = 20) {
var table = context.Set<TModel>(); var table = context.Set<TModel>();
var all = table var all = IncludeForgeinKeys(table)
.AsEnumerable() .AsEnumerable()
.Where(item => ItemSearched(item, searchTerm)) .Where(item => ItemSearched(item, searchTerm))
.ToList(); .ToList();
@@ -46,4 +49,34 @@ internal sealed class TableManager<TModel>(DbContext context, TableConfig config
return false; return false;
} }
public string DisplayProperty(object item, PropertyInfo info, TableConfig? tableConfig) {
if (item is null) return string.Empty;
var prop = tableConfig?.Properties.Find(prop => prop.Info.Name == info.Name);
if (prop is null) return item.ToString() ?? string.Empty;
var propValue = prop.Info.GetValue(item);
if (propValue is null || prop.DisplayedProperty is null)
return propValue?.ToString() ?? string.Empty;
var innerConfig = explorer.GetTable(propValue.GetType());
return DisplayProperty(propValue, prop.DisplayedProperty, innerConfig);
}
private IQueryable<TModel> IncludeForgeinKeys(IQueryable<TModel> query) {
var pendingQuery = query;
foreach (var property in config.Properties) {
var attr = property.Info
.GetCustomAttributes(true)
.FirstOrDefault(att => att is ForeignKeyAttribute) as ForeignKeyAttribute;
if (attr is null) continue;
pendingQuery = pendingQuery.Include(property.Info.Name);
}
return pendingQuery;
}
} }

View File

@@ -18,7 +18,6 @@
private string? _displayName; private string? _displayName;
private string? _initials; private string? _initials;
private string? _searchValue;
protected override async Task OnInitializedAsync() { protected override async Task OnInitializedAsync() {
if (Config.DisplayUserInfo) { if (Config.DisplayUserInfo) {

View File

@@ -18,7 +18,7 @@
<FluentDataGrid Items="_currentlyDisplayedModels?.AsQueryable()"> <FluentDataGrid Items="_currentlyDisplayedModels?.AsQueryable()">
@{ var dataIndex = 0; } @{ var dataIndex = 0; }
@foreach (var property in _config!.Properties.Where(prop => prop.List)) { @foreach (var property in _config!.Properties.Where(prop => prop.List)) {
<PropertyColumn Title="@property.Name" Property="o => property.Info.GetValue(o)" Style="min-width: max-content; min-height: 43px" Sortable="@property.Sortable"/> <PropertyColumn Title="@property.Name" Property="o => _manager!.DisplayProperty(o, property.Info, _config)" Style="min-width: max-content; min-height: 43px" Sortable="@property.Sortable"/>
} }
<TemplateColumn Title="Actions" Align="@Align.End" Style="min-height: 43px; min-width: max-content"> <TemplateColumn Title="Actions" Align="@Align.End" Style="min-height: 43px; min-width: max-content">
@@ -27,7 +27,7 @@
<FluentIcon Value="@(new Icons.Regular.Size16.Edit())"/> <FluentIcon Value="@(new Icons.Regular.Size16.Edit())"/>
</FluentButton> </FluentButton>
<FluentButton aria-label="Delete entry" OnClick="() => { DeleteEntry(currentElement); }"> <FluentButton aria-label="Delete entry" OnClick="async () => { await DeleteEntry(currentElement); }">
<FluentIcon Value="@(new Icons.Regular.Size16.Delete())" Color="Color.Warning"/> <FluentIcon Value="@(new Icons.Regular.Size16.Delete())" Color="Color.Warning"/>
</FluentButton> </FluentButton>

View File

@@ -1,4 +1,5 @@
@page "/" @page "/"
@using HopFrame.Testing.Models
<PageTitle>Home</PageTitle> <PageTitle>Home</PageTitle>
@@ -11,20 +12,32 @@ Welcome to your new Fluent Blazor app.
@code { @code {
protected override async Task OnInitializedAsync() { protected override async Task OnInitializedAsync() {
User? user = null;
for (int i = 0; i < 100; i++) { for (int i = 0; i < 100; i++) {
var first = GenerateName(Random.Shared.Next(4, 6)); var first = GenerateName(Random.Shared.Next(4, 6));
var last = GenerateName(Random.Shared.Next(4, 6)); var last = GenerateName(Random.Shared.Next(4, 6));
var username = $"{first}.{last}"; var username = $"{first}.{last}";
Context.Users.Add(new() {
user = new() {
Email = $"{username}-{Random.Shared.Next(0, 20)}@gmail.com", Email = $"{username}-{Random.Shared.Next(0, 20)}@gmail.com",
Id = Guid.CreateVersion7(), Id = Guid.CreateVersion7(),
FirstName = first, FirstName = first,
LastName = last, LastName = last,
Username = username Username = username
}); };
Context.Users.Add(user);
} }
await Context.SaveChangesAsync(); await Context.SaveChangesAsync();
Context.Posts.Add(new() {
Caption = "Cool Post",
Content = "This post is cool",
Author = user
});
await Context.SaveChangesAsync();
} }
public static string GenerateName(int len) { public static string GenerateName(int len) {
@@ -46,3 +59,5 @@ Welcome to your new Fluent Blazor app.
} }
} }
using HopFrame.Testing.Models;
using System.Runtime.InteropServices;

View File

@@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations.Schema;
namespace HopFrame.Testing.Models; namespace HopFrame.Testing.Models;
public class Post { public class Post {
[DatabaseGenerated(DatabaseGeneratedOption.Identity)] [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; } public int Id { get; set; }
[MaxLength(255)] [MaxLength(255)]

View File

@@ -1,6 +1,9 @@
namespace HopFrame.Testing.Models; using System.ComponentModel.DataAnnotations;
namespace HopFrame.Testing.Models;
public class User { public class User {
[Key]
public required Guid Id { get; init; } public required Guid Id { get; init; }
public required string Email { get; init; } public required string Email { get; init; }
public string? Username { get; set; } public string? Username { get; set; }

View File

@@ -35,6 +35,10 @@ builder.Services.AddHopFrame(options => {
table.Property(u => u.Id) table.Property(u => u.Id)
.Sortable(false); .Sortable(false);
}); });
context.Table<Post>()
.Property(p => p.Author)
.DisplayedProperty(u => u!.Username);
}); });
}); });