Added sorting
This commit is contained in:
@@ -5,6 +5,8 @@ namespace TestApplication.Models;
|
||||
public class User {
|
||||
[Key]
|
||||
public Guid Id { get; } = Guid.CreateVersion7();
|
||||
|
||||
public int Index { get; set; }
|
||||
|
||||
[EmailAddress, MaxLength(25)]
|
||||
public required string Email { get; set; }
|
||||
|
||||
@@ -23,6 +23,8 @@ builder.Services.AddHopFrame(config => {
|
||||
|
||||
table.Property(u => u.Password)
|
||||
.Listable(false);
|
||||
|
||||
table.SetPreferredProperty(u => u.Username);
|
||||
});
|
||||
|
||||
config.Table<Post>(table => {
|
||||
@@ -42,7 +44,7 @@ if (!app.Environment.IsDevelopment()) {
|
||||
await using (var scope = app.Services.CreateAsyncScope()) {
|
||||
var context = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
|
||||
|
||||
foreach (var _ in Enumerable.Range(1, 100)) {
|
||||
foreach (var i in Enumerable.Range(1, 100)) {
|
||||
var firstName = Faker.Name.First();
|
||||
var lastName = Faker.Name.Last();
|
||||
|
||||
@@ -53,9 +55,22 @@ await using (var scope = app.Services.CreateAsyncScope()) {
|
||||
LastName = lastName,
|
||||
Description = Faker.Lorem.Paragraph(),
|
||||
Birth = DateOnly.FromDateTime(Faker.Identification.DateOfBirth()),
|
||||
Password = Faker.RandomNumber.Next(100000L, 99999999999999999L).ToString()
|
||||
Password = Faker.RandomNumber.Next(100000L, 99999999999999999L).ToString(),
|
||||
Index = i
|
||||
});
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
context.Posts.Add(new() {
|
||||
Message = Faker.Lorem.Paragraph(),
|
||||
Sender = context.Users.First()
|
||||
});
|
||||
|
||||
context.Posts.Add(new() {
|
||||
Message = Faker.Lorem.Paragraph(),
|
||||
Sender = context.Users.Skip(1).First()
|
||||
});
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
@@ -36,6 +36,9 @@ public class PropertyConfig {
|
||||
|
||||
/// [GENERATED] The place (from left to right) that the property will appear in the table and editor
|
||||
public int OrderIndex { get; set; }
|
||||
|
||||
/// [GENERATED] The table that owns this property
|
||||
public TableConfig Table { get; set; }
|
||||
|
||||
internal PropertyConfig() {}
|
||||
}
|
||||
|
||||
@@ -28,5 +28,8 @@ public class TableConfig {
|
||||
/// [GENERATED] The place (from top to bottom) that the table will appear in on the sidebar
|
||||
public int OrderIndex { get; set; }
|
||||
|
||||
/// [GENERATED] The identifier of the property that should be displayed if the model is used as a relation
|
||||
public string? PreferredProperty { get; set; }
|
||||
|
||||
internal TableConfig() {}
|
||||
}
|
||||
@@ -46,8 +46,8 @@ public class TableConfigurator<TModel>(TableConfig config) where TModel : class
|
||||
return new PropertyConfigurator(prop);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Property"/>
|
||||
public PropertyConfigurator Property<TProp>(Expression<Func<TModel, TProp>> propertyExpression) {
|
||||
/// <inheritdoc cref="Property(string)"/>
|
||||
public PropertyConfigurator Property(Expression<Func<TModel, object>> propertyExpression) {
|
||||
var propertyName = ExpressionHelper.GetPropertyInfo(propertyExpression).Name;
|
||||
var prop = Config.Properties.FirstOrDefault(p => p.Identifier == propertyName);
|
||||
|
||||
@@ -56,4 +56,16 @@ public class TableConfigurator<TModel>(TableConfig config) where TModel : class
|
||||
|
||||
return new PropertyConfigurator(prop);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="TableConfig.PreferredProperty"/>
|
||||
public TableConfigurator<TModel> SetPreferredProperty(Expression<Func<TModel, object>> propertyExpression) {
|
||||
var propertyName = ExpressionHelper.GetPropertyInfo(propertyExpression).Name;
|
||||
var prop = Config.Properties.FirstOrDefault(p => p.Identifier == propertyName);
|
||||
|
||||
if (prop is null)
|
||||
throw new ArgumentException($"No attribute '{propertyName}' found in '{Config.Identifier}'!");
|
||||
|
||||
Config.PreferredProperty = prop.Identifier;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,29 @@
|
||||
using HopFrame.Core.Repositories;
|
||||
using HopFrame.Core.Configuration;
|
||||
using HopFrame.Core.Repositories;
|
||||
using HopFrame.Core.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace HopFrame.Core.EFCore;
|
||||
|
||||
internal class EfCoreRepository<TModel, TContext>(TContext context) : HopFrameRepository<TModel> where TModel : class where TContext : DbContext {
|
||||
internal class EfCoreRepository<TModel, TContext>(TContext context, IConfigAccessor accessor) : HopFrameRepository<TModel> where TModel : class where TContext : DbContext {
|
||||
|
||||
public override async Task<IEnumerable<TModel>> LoadPageAsync(int page, int perPage, CancellationToken ct = default) {
|
||||
var set = context.Set<TModel>();
|
||||
return await set
|
||||
var query = set
|
||||
.AsNoTracking()
|
||||
.Skip(page * perPage)
|
||||
.Take(perPage)
|
||||
.ToArrayAsync(ct); //TODO: Implement FK loading
|
||||
.Take(perPage);
|
||||
|
||||
return await IncludeForeignKeys(query)
|
||||
.ToArrayAsync(ct);
|
||||
}
|
||||
|
||||
private IQueryable<TModel> IncludeForeignKeys(IQueryable<TModel> query) {
|
||||
var table = accessor.GetTableByType(typeof(TModel))!;
|
||||
|
||||
return table.Properties
|
||||
.Where(p => (p.PropertyType & PropertyType.Relation) != 0)
|
||||
.Aggregate(query, (current, property) => current.Include(property.Identifier));
|
||||
}
|
||||
|
||||
public override async Task<int> CountAsync(CancellationToken ct = default) {
|
||||
|
||||
@@ -42,9 +42,13 @@ internal static class ConfigurationHelper {
|
||||
Type = property.PropertyType,
|
||||
DisplayName = property.Name,
|
||||
OrderIndex = table.Properties.Count,
|
||||
PropertyType = InferPropertyType(property.PropertyType, property)
|
||||
PropertyType = InferPropertyType(property.PropertyType, property),
|
||||
Table = table
|
||||
};
|
||||
|
||||
if (property.CustomAttributes.Any(a => a.AttributeType == typeof(KeyAttribute)))
|
||||
table.PreferredProperty = config.Identifier;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,12 +24,12 @@ public abstract class HopFrameRepository<TModel> : IHopFrameRepository where TMo
|
||||
public abstract Task DeleteAsync(TModel entry, CancellationToken ct = default);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<IEnumerable> LoadPageGenericAsync(int page, int perPage, CancellationToken ct) {
|
||||
public async Task<IEnumerable<object>> LoadPageGenericAsync(int page, int perPage, CancellationToken ct) {
|
||||
return await LoadPageAsync(page, perPage, ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<IEnumerable> SearchGenericAsync(string searchTerm, int page, int perPage, CancellationToken ct) {
|
||||
public async Task<IEnumerable<object>> SearchGenericAsync(string searchTerm, int page, int perPage, CancellationToken ct) {
|
||||
return await SearchAsync(searchTerm, page, perPage, ct);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System.Collections;
|
||||
|
||||
#pragma warning disable CS1573 // Parameter has no matching param tag in the XML comment (but other parameters do)
|
||||
#pragma warning disable CS1573 // Parameter has no matching param tag in the XML comment (but other parameters do)
|
||||
namespace HopFrame.Core.Repositories;
|
||||
|
||||
/// The generic repository that provides access to the model dataset
|
||||
@@ -11,7 +9,7 @@ public interface IHopFrameRepository {
|
||||
/// </summary>
|
||||
/// <param name="page">The index of the current page (starts at 0)</param>
|
||||
/// <param name="perPage">The amount of entries that should be loaded</param>
|
||||
public Task<IEnumerable> LoadPageGenericAsync(int page, int perPage, CancellationToken ct);
|
||||
public Task<IEnumerable<object>> LoadPageGenericAsync(int page, int perPage, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the total amount of entries in the dataset
|
||||
@@ -24,7 +22,7 @@ public interface IHopFrameRepository {
|
||||
/// <param name="searchTerm">The search text provided by the user</param>
|
||||
/// <param name="page">The index of the current page (starts at 0)</param>
|
||||
/// <param name="perPage">The amount of entries that should be loaded</param>
|
||||
public Task<IEnumerable> SearchGenericAsync(string searchTerm, int page, int perPage, CancellationToken ct);
|
||||
public Task<IEnumerable<object>> SearchGenericAsync(string searchTerm, int page, int perPage, CancellationToken ct);
|
||||
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -10,7 +10,14 @@ public interface IEntityAccessor {
|
||||
/// </summary>
|
||||
/// <param name="model">The model to pull the property from</param>
|
||||
/// <param name="property">The property that shall be extracted</param>
|
||||
public Task<string?> GetValue(object model, PropertyConfig property);
|
||||
public string? GetValue(object model, PropertyConfig property);
|
||||
|
||||
/// <summary>
|
||||
/// Formats the property to be displayed properly
|
||||
/// </summary>
|
||||
/// <param name="value">The value of the property</param>
|
||||
/// <param name="property">The property that shall be extracted</param>
|
||||
public string? FormatValue(object? value, PropertyConfig property);
|
||||
|
||||
/// <summary>
|
||||
/// Properly formats and sets the new value of the property
|
||||
@@ -18,6 +25,14 @@ public interface IEntityAccessor {
|
||||
/// <param name="model">The model to save the property to</param>
|
||||
/// <param name="property">The property that shall be modified</param>
|
||||
/// <param name="value">The new value of the property</param>
|
||||
public Task SetValue(object model, PropertyConfig property, object value);
|
||||
public void SetValue(object model, PropertyConfig property, object value);
|
||||
|
||||
/// <summary>
|
||||
/// Sorts the provided dataset by the specified property
|
||||
/// </summary>
|
||||
/// <param name="data">The dataset that needs to be sorted</param>
|
||||
/// <param name="property">The property that defines the sort order</param>
|
||||
/// <param name="descending">Determines if the resulting order should be flipped</param>
|
||||
public IEnumerable<object> SortDataByProperty(IEnumerable<object> data, PropertyConfig property, bool descending = false);
|
||||
|
||||
}
|
||||
@@ -1,25 +1,91 @@
|
||||
using HopFrame.Core.Configuration;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using HopFrame.Core.Configuration;
|
||||
|
||||
namespace HopFrame.Core.Services.Implementation;
|
||||
|
||||
internal class EntityAccessor : IEntityAccessor {
|
||||
internal class EntityAccessor(IConfigAccessor accessor) : IEntityAccessor {
|
||||
|
||||
public async Task<string?> GetValue(object model, PropertyConfig property) {
|
||||
public string? GetValue(object model, PropertyConfig property) {
|
||||
var prop = model.GetType().GetProperty(property.Identifier);
|
||||
|
||||
if (prop is null)
|
||||
return null;
|
||||
|
||||
return prop.GetValue(model)?.ToString();
|
||||
var value = prop.GetValue(model);
|
||||
return FormatValue(value, property);
|
||||
}
|
||||
|
||||
public async Task SetValue(object model, PropertyConfig property, object value) {
|
||||
var prop = model.GetType().GetProperty(property.Identifier);
|
||||
|
||||
public string? FormatValue(object? value, PropertyConfig property) {
|
||||
if (value is null)
|
||||
return null;
|
||||
|
||||
if ((property.PropertyType & PropertyType.List) != 0) {
|
||||
return (value as IEnumerable<object>)!.Count().ToString();
|
||||
}
|
||||
|
||||
if ((property.PropertyType & PropertyType.Relation) != 0) {
|
||||
var table = accessor.GetTableByType(property.Type);
|
||||
if (table?.PreferredProperty != null) {
|
||||
var tableProp = table.Properties.First(p => p.Identifier == table.PreferredProperty);
|
||||
return GetValue(value, tableProp);
|
||||
}
|
||||
}
|
||||
|
||||
return value.ToString();
|
||||
}
|
||||
|
||||
public void SetValue(object model, PropertyConfig property, object value) {
|
||||
var prop = model.GetType().GetProperty(property.Identifier);
|
||||
if (prop is null)
|
||||
return;
|
||||
|
||||
if (value.GetType() != property.Type)
|
||||
value = Convert.ChangeType(value, property.Type);
|
||||
|
||||
prop.SetValue(model, Convert.ChangeType(value, property.Type));
|
||||
prop.SetValue(model, value);
|
||||
}
|
||||
|
||||
public IEnumerable<object> SortDataByProperty(IEnumerable<object> data, PropertyConfig property, bool descending = false) {
|
||||
var prop = property.Table.TableType.GetProperty(property.Identifier);
|
||||
if (prop is null)
|
||||
return data;
|
||||
|
||||
var parameter = Expression.Parameter(property.Table.TableType);
|
||||
Expression expression = Expression.Property(parameter, prop);
|
||||
var targetType = prop.PropertyType;
|
||||
|
||||
if ((property.PropertyType & PropertyType.Relation) != 0) {
|
||||
var relationTable = accessor.GetTableByType(property.Type);
|
||||
PropertyInfo? relationPropInfo = null;
|
||||
|
||||
if (relationTable?.PreferredProperty != null) {
|
||||
var relationProp = relationTable.Properties.First(p => p.Identifier == relationTable.PreferredProperty);
|
||||
|
||||
if ((relationProp.PropertyType & PropertyType.List) == 0)
|
||||
relationPropInfo = relationProp.Type.GetProperty(relationProp.Identifier);
|
||||
}
|
||||
|
||||
if (relationPropInfo == null) {
|
||||
var formatMethod = GetType().GetMethod(nameof(FormatValue))!;
|
||||
targetType = typeof(string);
|
||||
expression = Expression.Call(Expression.Constant(this), formatMethod, expression, Expression.Constant(property));
|
||||
}
|
||||
else {
|
||||
targetType = relationPropInfo.PropertyType;
|
||||
expression = Expression.Property(expression, relationPropInfo);
|
||||
}
|
||||
}
|
||||
|
||||
var lambda = Expression.Lambda(expression, parameter);
|
||||
|
||||
var methodName = descending ? nameof(Enumerable.OrderByDescending) : nameof(Enumerable.OrderBy);
|
||||
var method = typeof(Enumerable)
|
||||
.GetMethods()
|
||||
.Single(m => m.Name == methodName && m.GetParameters().Length == 2)
|
||||
.MakeGenericMethod(property.Table.TableType, targetType);
|
||||
|
||||
var result = method.Invoke(null, [data, lambda.Compile()]);
|
||||
return (IEnumerable<object>)result!;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12,7 +12,8 @@
|
||||
<ToolBarContent>
|
||||
<MudText Typo="Typo.h6">@Config.DisplayName</MudText>
|
||||
<MudSpacer />
|
||||
<MudStack Row="true" Spacing="2" Style="min-width: 500px">
|
||||
<MudStack Row="true" Spacing="2" Style="min-width: 600px">
|
||||
<MudButton OnClick="@(Manager.ReloadServerData)">Reload</MudButton>
|
||||
<MudTextField
|
||||
T="string"
|
||||
Placeholder="Search"
|
||||
@@ -30,12 +31,13 @@
|
||||
<HeaderContent>
|
||||
@foreach (var prop in OrderedProperties) {
|
||||
<MudTh>
|
||||
@if (prop.Sortable) {
|
||||
<MudTableSortLabel SortLabel="@prop.Identifier" T="string">@prop.DisplayName</MudTableSortLabel>
|
||||
}
|
||||
else {
|
||||
<MudTableSortLabel
|
||||
T="object"
|
||||
@ref="SortDirections[prop.Identifier]"
|
||||
SortDirectionChanged="@(dir => OnSort(prop, dir))"
|
||||
Enabled="@prop.Sortable">
|
||||
@prop.DisplayName
|
||||
}
|
||||
</MudTableSortLabel>
|
||||
</MudTh>
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,12 @@ public partial class Table(IEntityAccessor accessor, IConfigAccessor configAcces
|
||||
|
||||
private MudTable<Dictionary<string, string>> Manager { get; set; } = null!;
|
||||
|
||||
private Dictionary<string, MudTableSortLabel<object>> SortDirections { get; set; } = new();
|
||||
|
||||
private KeyValuePair<string, SortDirection>? _currentSort;
|
||||
|
||||
private string _searchText = string.Empty;
|
||||
|
||||
protected override void OnInitialized() {
|
||||
base.OnInitialized();
|
||||
|
||||
@@ -26,22 +32,19 @@ public partial class Table(IEntityAccessor accessor, IConfigAccessor configAcces
|
||||
.Where(p => p.Listable)
|
||||
.OrderBy(p => p.OrderIndex)
|
||||
.ToArray();
|
||||
|
||||
foreach (var property in OrderedProperties) {
|
||||
SortDirections.Add(property.Identifier, null!);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<Dictionary<string, string>>> PrepareData(object[] entries) {
|
||||
private List<Dictionary<string, string>> PrepareData(object[] entries) {
|
||||
var list = new List<Dictionary<string, string>>();
|
||||
|
||||
foreach (var entry in entries) {
|
||||
var taskDict = new Dictionary<string, Task<string?>>();
|
||||
foreach (var prop in OrderedProperties) {
|
||||
taskDict.Add(prop.Identifier, accessor.GetValue(entry, prop));
|
||||
}
|
||||
|
||||
await Task.WhenAll(taskDict.Values);
|
||||
|
||||
var dict = new Dictionary<string, string>();
|
||||
foreach (var prop in taskDict) {
|
||||
dict.Add(prop.Key, prop.Value.Result ?? string.Empty);
|
||||
foreach (var prop in OrderedProperties) {
|
||||
dict.Add(prop.Identifier, accessor.GetValue(entry, prop) ?? string.Empty);
|
||||
}
|
||||
|
||||
list.Add(dict);
|
||||
@@ -51,8 +54,19 @@ public partial class Table(IEntityAccessor accessor, IConfigAccessor configAcces
|
||||
}
|
||||
|
||||
private async Task<TableData<Dictionary<string, string>>> Reload(TableState state, CancellationToken ct) {
|
||||
var entries = await Repository.LoadPageGenericAsync(state.Page, state.PageSize, ct);
|
||||
var data = await PrepareData(entries.Cast<object>().ToArray());
|
||||
IEnumerable<object> entries;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_searchText))
|
||||
entries = await Repository.LoadPageGenericAsync(state.Page, state.PageSize, ct);
|
||||
else
|
||||
entries = await Repository.SearchGenericAsync(_searchText, state.Page, state.PageSize, ct);
|
||||
|
||||
if (_currentSort.HasValue) {
|
||||
var sortProp = Config.Properties.First(p => p.Identifier == _currentSort.Value.Key);
|
||||
entries = accessor.SortDataByProperty(entries, sortProp, _currentSort.Value.Value == SortDirection.Descending);
|
||||
}
|
||||
|
||||
var data = PrepareData(entries.ToArray());
|
||||
var total = await Repository.CountAsync(ct);
|
||||
|
||||
return new TableData<Dictionary<string, string>> {
|
||||
@@ -62,7 +76,27 @@ public partial class Table(IEntityAccessor accessor, IConfigAccessor configAcces
|
||||
}
|
||||
|
||||
private async Task OnSearch(string searchText) {
|
||||
Console.WriteLine(searchText);
|
||||
_searchText = searchText;
|
||||
await Manager.ReloadServerData();
|
||||
}
|
||||
|
||||
private async Task OnSort(PropertyConfig property, SortDirection direction) {
|
||||
if (direction != SortDirection.None) {
|
||||
foreach (var reference in SortDirections
|
||||
.Where(d => d.Key != property.Identifier)) {
|
||||
#pragma warning disable BL0005
|
||||
reference.Value.SortDirection = SortDirection.None;
|
||||
#pragma warning restore BL0005
|
||||
}
|
||||
}
|
||||
|
||||
if (direction == SortDirection.None) {
|
||||
_currentSort = null;
|
||||
}
|
||||
else {
|
||||
_currentSort = new(property.Identifier, direction);
|
||||
}
|
||||
|
||||
await Manager.ReloadServerData();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
@page "/admin/{TableRoute}"
|
||||
@using HopFrame.Web.Components.Components
|
||||
@inherits CancellableComponent
|
||||
@rendermode InteractiveServer
|
||||
@layout HopFrameLayout
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace HopFrame.Web.Components.Pages;
|
||||
|
||||
public partial class TablePage(IConfigAccessor accessor, NavigationManager navigator) : CancellableComponent {
|
||||
public partial class TablePage(IConfigAccessor accessor, NavigationManager navigator) : ComponentBase {
|
||||
private const int PerPage = 25;
|
||||
|
||||
[Parameter]
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Collections;
|
||||
using HopFrame.Core.Configuration;
|
||||
using HopFrame.Core.Configuration;
|
||||
using HopFrame.Core.Configurators;
|
||||
using HopFrame.Core.Repositories;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -8,13 +7,13 @@ namespace HopFrame.Tests.Core.Configurators;
|
||||
|
||||
public class HopFrameConfiguratorTests {
|
||||
private class TestRepository : IHopFrameRepository {
|
||||
public Task<IEnumerable> LoadPageGenericAsync(int page, int perPage, CancellationToken ct) {
|
||||
public Task<IEnumerable<object>> LoadPageGenericAsync(int page, int perPage, CancellationToken ct) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public Task<int> CountAsync(CancellationToken ct) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public Task<IEnumerable> SearchGenericAsync(string searchTerm, int page, int perPage, CancellationToken ct) {
|
||||
public Task<IEnumerable<object>> SearchGenericAsync(string searchTerm, int page, int perPage, CancellationToken ct) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public Task CreateGenericAsync(object entry, CancellationToken ct) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Collections;
|
||||
using HopFrame.Core.Configuration;
|
||||
using HopFrame.Core.Configuration;
|
||||
using HopFrame.Core.Repositories;
|
||||
using HopFrame.Core.Services.Implementation;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -9,13 +8,13 @@ namespace HopFrame.Tests.Core.Services.Implementation;
|
||||
|
||||
public class ConfigAccessorTests {
|
||||
private class TestRepository : IHopFrameRepository {
|
||||
public Task<IEnumerable> LoadPageGenericAsync(int page, int perPage, CancellationToken ct) {
|
||||
public Task<IEnumerable<object>> LoadPageGenericAsync(int page, int perPage, CancellationToken ct) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public Task<int> CountAsync(CancellationToken ct) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public Task<IEnumerable> SearchGenericAsync(string searchTerm, int page, int perPage, CancellationToken ct) {
|
||||
public Task<IEnumerable<object>> SearchGenericAsync(string searchTerm, int page, int perPage, CancellationToken ct) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public Task CreateGenericAsync(object entry, CancellationToken ct) {
|
||||
|
||||
Reference in New Issue
Block a user