Added ef core integration
This commit is contained in:
20
debug/TestApplication/DatabaseContext.cs
Normal file
20
debug/TestApplication/DatabaseContext.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TestApplication.Models;
|
||||||
|
|
||||||
|
namespace TestApplication;
|
||||||
|
|
||||||
|
public class DatabaseContext(DbContextOptions<DatabaseContext> options) : DbContext(options) {
|
||||||
|
|
||||||
|
public DbSet<User> Users { get; set; }
|
||||||
|
|
||||||
|
public DbSet<Post> Posts { get; set; }
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder) {
|
||||||
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity<User>()
|
||||||
|
.HasMany(u => u.Posts)
|
||||||
|
.WithOne(p => p.Sender)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
debug/TestApplication/Models/Post.cs
Normal file
13
debug/TestApplication/Models/Post.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace TestApplication.Models;
|
||||||
|
|
||||||
|
public class Post {
|
||||||
|
[Key]
|
||||||
|
public Guid Id { get; } = Guid.CreateVersion7();
|
||||||
|
|
||||||
|
public required User Sender { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(5000)]
|
||||||
|
public required string Message { get; set; }
|
||||||
|
}
|
||||||
30
debug/TestApplication/Models/User.cs
Normal file
30
debug/TestApplication/Models/User.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace TestApplication.Models;
|
||||||
|
|
||||||
|
public class User {
|
||||||
|
[Key]
|
||||||
|
public Guid Id { get; } = Guid.CreateVersion7();
|
||||||
|
|
||||||
|
[EmailAddress, MaxLength(25)]
|
||||||
|
public required string Email { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(25)]
|
||||||
|
public required string Username { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(64)]
|
||||||
|
public required string Password { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(25)]
|
||||||
|
public required string FirstName { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(25)]
|
||||||
|
public required string LastName { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(255)]
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
public required DateOnly Birth { get; set; } = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
|
||||||
|
public List<Post> Posts { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
using HopFrame.Core;
|
||||||
|
using HopFrame.Core.EFCore;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TestApplication;
|
||||||
using TestApplication.Components;
|
using TestApplication.Components;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
@@ -6,6 +10,15 @@ var builder = WebApplication.CreateBuilder(args);
|
|||||||
builder.Services.AddRazorComponents()
|
builder.Services.AddRazorComponents()
|
||||||
.AddInteractiveServerComponents();
|
.AddInteractiveServerComponents();
|
||||||
|
|
||||||
|
builder.Services.AddEntityFrameworkInMemoryDatabase();
|
||||||
|
builder.Services.AddDbContext<DatabaseContext>(options => {
|
||||||
|
options.UseInMemoryDatabase("testing");
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddHopFrame(config => {
|
||||||
|
config.AddDbContext<DatabaseContext>();
|
||||||
|
});
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"http": {
|
"http": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": true,
|
"launchBrowser": false,
|
||||||
"applicationUrl": "http://localhost:5281",
|
"applicationUrl": "http://localhost:5281",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"https": {
|
"https": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": true,
|
"launchBrowser": false,
|
||||||
"applicationUrl": "https://localhost:7126;http://localhost:5281",
|
"applicationUrl": "https://localhost:7126;http://localhost:5281",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
|||||||
@@ -11,4 +11,8 @@
|
|||||||
<ProjectReference Include="..\..\src\HopFrame.Core\HopFrame.Core.csproj" />
|
<ProjectReference Include="..\..\src\HopFrame.Core\HopFrame.Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -4,15 +4,18 @@
|
|||||||
* The configuration for a single property
|
* The configuration for a single property
|
||||||
*/
|
*/
|
||||||
public class PropertyConfig {
|
public class PropertyConfig {
|
||||||
/** The unique identifier for the property (usually the real property name in the model) */
|
/** [GENERATED] The unique identifier for the property (usually the real property name in the model) */
|
||||||
public required string Identifier { get; init; }
|
public required string Identifier { get; init; }
|
||||||
|
|
||||||
/** The displayed name of the Property */
|
/** [GENERATED] The displayed name of the Property */
|
||||||
public required string DisplayName { get; set; }
|
public required string DisplayName { get; set; }
|
||||||
|
|
||||||
/** The type of the property */
|
/** [GENERATED] The real type of the property */
|
||||||
public required Type Type { get; set; }
|
public required Type Type { get; set; }
|
||||||
|
|
||||||
|
/** [GENERATED] The type as wich the property should be treated */
|
||||||
|
public required PropertyType PropertyType { get; set; }
|
||||||
|
|
||||||
/** Determines if the property will appear in the table */
|
/** Determines if the property will appear in the table */
|
||||||
public bool Listable { get; set; } = true;
|
public bool Listable { get; set; } = true;
|
||||||
|
|
||||||
@@ -31,11 +34,57 @@ public class PropertyConfig {
|
|||||||
/** Determines if the property is visible in the creation or edit dialog */
|
/** Determines if the property is visible in the creation or edit dialog */
|
||||||
public bool Creatable { get; set; } = true;
|
public bool Creatable { get; set; } = true;
|
||||||
|
|
||||||
/** Determines if the actual value should be displayed (useful for passwords) */
|
/** [GENERATED] The place (from left to right) that the property will appear in the table and editor */
|
||||||
public bool DisplayValue { get; set; } = true;
|
|
||||||
|
|
||||||
/** The place (from left to right) that the property will appear in the table and editor */
|
|
||||||
public int OrderIndex { get; set; }
|
public int OrderIndex { get; set; }
|
||||||
|
|
||||||
internal PropertyConfig() {}
|
internal PropertyConfig() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used to distinguish between different input types in the frontend. <br/>
|
||||||
|
/// Binary Format: First byte is used for additional properties, second byte identifies the real type
|
||||||
|
/// </summary>
|
||||||
|
[Flags]
|
||||||
|
public enum PropertyType : byte {
|
||||||
|
/** Used together with another type to indicate that the value can be null */
|
||||||
|
Nullable = 0b10000000,
|
||||||
|
|
||||||
|
/** Used together with another type to indicate that the property is a relation */
|
||||||
|
Relation = 0b01000000,
|
||||||
|
|
||||||
|
/** Used together with another type to indicate that the value is enumerable */
|
||||||
|
List = 0b00100000,
|
||||||
|
|
||||||
|
/** Indicates that the value is numeric */
|
||||||
|
Numeric = 0x01,
|
||||||
|
|
||||||
|
/** Indicates that the value is a boolean */
|
||||||
|
Boolean = 0x02,
|
||||||
|
|
||||||
|
/** Indicates that the value is a timestamp */
|
||||||
|
DateTime = 0x03,
|
||||||
|
|
||||||
|
/** Indicates that the value is a date */
|
||||||
|
DateOnly = 0x04,
|
||||||
|
|
||||||
|
/** Indicates that the value is a time of day */
|
||||||
|
TimeOnly = 0x05,
|
||||||
|
|
||||||
|
/** Indicates that the value is a list of fixed values */
|
||||||
|
Enum = 0x06,
|
||||||
|
|
||||||
|
/** Indicates that the value is a string */
|
||||||
|
Text = 0x07,
|
||||||
|
|
||||||
|
/** Indicates that the value is an email */
|
||||||
|
Email = 0x08,
|
||||||
|
|
||||||
|
/** Indicates that the value is a long string */
|
||||||
|
TextArea = 0x09,
|
||||||
|
|
||||||
|
/** Indicates that the value should be hidden */
|
||||||
|
Password = 0x0A,
|
||||||
|
|
||||||
|
/** Indicates that the value is a phone number */
|
||||||
|
PhoneNumber = 0x0B
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,28 +4,28 @@
|
|||||||
* The configuration for a table
|
* The configuration for a table
|
||||||
*/
|
*/
|
||||||
public class TableConfig {
|
public class TableConfig {
|
||||||
/** The unique identifier for the table (usually the name of the model) */
|
/** [GENERATED] The unique identifier for the table (usually the name of the model) */
|
||||||
public required string Identifier { get; init; }
|
public required string Identifier { get; init; }
|
||||||
|
|
||||||
/** The configurations for the properties of the model */
|
/** [GENERATED] The configurations for the properties of the model */
|
||||||
public IList<PropertyConfig> Properties { get; set; } = new List<PropertyConfig>();
|
public IList<PropertyConfig> Properties { get; set; } = new List<PropertyConfig>();
|
||||||
|
|
||||||
/** The type of the model */
|
/** [GENERATED] The type of the model */
|
||||||
public required Type TableType { get; set; }
|
public required Type TableType { get; set; }
|
||||||
|
|
||||||
/** The type identifier for the repository */
|
/** [GENERATED] The type identifier for the repository */
|
||||||
public required Type RepositoryType { get; set; }
|
public required Type RepositoryType { get; set; }
|
||||||
|
|
||||||
/** the url of the table page */
|
/** [GENERATED] the url of the table page */
|
||||||
public required string Route { get; set; }
|
public required string Route { get; set; }
|
||||||
|
|
||||||
/** The displayed name of the table */
|
/** [GENERATED] The displayed name of the table */
|
||||||
public required string DisplayName { get; set; }
|
public required string DisplayName { get; set; }
|
||||||
|
|
||||||
/** A short description for the table */
|
/** A short description for the table */
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
|
|
||||||
/** The place (from top to bottom) that the table will appear in on the sidebar */
|
/** [GENERATED] The place (from top to bottom) that the table will appear in on the sidebar */
|
||||||
public int OrderIndex { get; set; }
|
public int OrderIndex { get; set; }
|
||||||
|
|
||||||
internal TableConfig() {}
|
internal TableConfig() {}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ public class HopFrameConfigurator(HopFrameConfig config, IServiceCollection serv
|
|||||||
/** The internal config that is modified */
|
/** The internal config that is modified */
|
||||||
public HopFrameConfig Config { get; } = config;
|
public HopFrameConfig Config { get; } = config;
|
||||||
|
|
||||||
|
internal IServiceCollection Services { get; } = services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a new table to the configuration based on the provided repository
|
/// Adds a new table to the configuration based on the provided repository
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -22,7 +24,7 @@ public class HopFrameConfigurator(HopFrameConfig config, IServiceCollection serv
|
|||||||
public HopFrameConfigurator AddRepository<TRepository, TModel>(Action<TableConfigurator<TModel>>? configurator = null) where TRepository : IHopFrameRepository where TModel : notnull {
|
public HopFrameConfigurator AddRepository<TRepository, TModel>(Action<TableConfigurator<TModel>>? configurator = null) where TRepository : IHopFrameRepository where TModel : notnull {
|
||||||
var table = ConfigurationHelper.InitializeTable(Config, typeof(TRepository), typeof(TModel));
|
var table = ConfigurationHelper.InitializeTable(Config, typeof(TRepository), typeof(TModel));
|
||||||
Config.Tables.Add(table);
|
Config.Tables.Add(table);
|
||||||
services.TryAddScoped(typeof(TRepository));
|
Services.TryAddScoped(typeof(TRepository));
|
||||||
configurator?.Invoke(new TableConfigurator<TModel>(table));
|
configurator?.Invoke(new TableConfigurator<TModel>(table));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -44,7 +46,7 @@ public class HopFrameConfigurator(HopFrameConfig config, IServiceCollection serv
|
|||||||
throw new ArgumentException($"Table '{config.Identifier}' has some validation errors:\n\t{string.Join("\n\t", errors)}");
|
throw new ArgumentException($"Table '{config.Identifier}' has some validation errors:\n\t{string.Join("\n\t", errors)}");
|
||||||
|
|
||||||
Config.Tables.Add(config);
|
Config.Tables.Add(config);
|
||||||
services.TryAddScoped(config.RepositoryType);
|
Services.TryAddScoped(config.RepositoryType);
|
||||||
configurator?.Invoke(new TableConfigurator<TModel>(config));
|
configurator?.Invoke(new TableConfigurator<TModel>(config));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,15 +45,18 @@ public class PropertyConfigurator(PropertyConfig config) {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** <inheritdoc cref="PropertyConfig.DisplayValue" /> */
|
|
||||||
public PropertyConfigurator DisplayValue(bool displayValue) {
|
|
||||||
Config.DisplayValue = displayValue;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** <inheritdoc cref="PropertyConfig.OrderIndex" /> */
|
/** <inheritdoc cref="PropertyConfig.OrderIndex" /> */
|
||||||
public PropertyConfigurator SetOrderIndex(int index) {
|
public PropertyConfigurator SetOrderIndex(int index) {
|
||||||
Config.OrderIndex = index;
|
Config.OrderIndex = index;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the property type. The predefined modifiers (like nullable) persist.
|
||||||
|
/// If the property is a list or any other generic type, please use the enumerated type.
|
||||||
|
/// </summary>
|
||||||
|
public PropertyConfigurator SetType(PropertyType type) {
|
||||||
|
Config.PropertyType = (PropertyType)(((byte)Config.PropertyType & 0xF0) | ((byte)type & 0x0F));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
34
src/HopFrame.Core/EFCore/DbConfigPopulator.cs
Normal file
34
src/HopFrame.Core/EFCore/DbConfigPopulator.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using HopFrame.Core.Configuration;
|
||||||
|
using HopFrame.Core.Helpers;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace HopFrame.Core.EFCore;
|
||||||
|
|
||||||
|
internal static class DbConfigPopulator {
|
||||||
|
|
||||||
|
public static string ConfigureRepository(HopFrameConfig global, IServiceCollection services, Type contextType, PropertyInfo tableProperty) {
|
||||||
|
var modelType = tableProperty.PropertyType.GenericTypeArguments.First();
|
||||||
|
var repoType = typeof(EfCoreRepository<,>).MakeGenericType(modelType, contextType);
|
||||||
|
|
||||||
|
services.AddScoped(repoType);
|
||||||
|
|
||||||
|
var table = ConfigurationHelper.InitializeTable(global, repoType, modelType);
|
||||||
|
global.Tables.Add(table);
|
||||||
|
return table.Identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CheckForRelations(HopFrameConfig global, TableConfig table) {
|
||||||
|
foreach (var property in table.Properties) {
|
||||||
|
var type = property.Type;
|
||||||
|
|
||||||
|
if ((property.PropertyType & PropertyType.List) != 0 && type.IsGenericType) {
|
||||||
|
type = type.GenericTypeArguments.First();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (global.Tables.Any(t => t.TableType == type))
|
||||||
|
property.PropertyType |= PropertyType.Relation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
47
src/HopFrame.Core/EFCore/EfCoreRepository.cs
Normal file
47
src/HopFrame.Core/EFCore/EfCoreRepository.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
using HopFrame.Core.Repositories;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace HopFrame.Core.EFCore;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The generic repository that handles data source communication for managed tables
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TModel">The model that is managed by the repo</typeparam>
|
||||||
|
/// <typeparam name="TContext">The underlying context that handles database communication</typeparam>
|
||||||
|
public class EfCoreRepository<TModel, TContext>(TContext context) : HopFrameRepository<TModel> where TModel : class where TContext : DbContext {
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override Task<IEnumerable<TModel>> LoadPageAsync(int page, int perPage, CancellationToken ct = default) {
|
||||||
|
throw new NotImplementedException(); //TODO: Implement loading functionality
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override async Task<int> CountAsync(CancellationToken ct = default) {
|
||||||
|
var table = context.Set<TModel>();
|
||||||
|
return await table.CountAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override Task<IEnumerable<TModel>> SearchAsync(string searchTerm, int page, int perPage, CancellationToken ct = default) {
|
||||||
|
throw new NotImplementedException(); //TODO: Implement search functionality
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override async Task CreateAsync(TModel entry, CancellationToken ct = default) {
|
||||||
|
await context.AddAsync(entry, ct);
|
||||||
|
await context.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override async Task UpdateAsync(TModel entry, CancellationToken ct = default) {
|
||||||
|
context.Update(entry);
|
||||||
|
await context.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override async Task DeleteAsync(TModel entry, CancellationToken ct = default) {
|
||||||
|
context.Remove(entry);
|
||||||
|
await context.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
44
src/HopFrame.Core/EFCore/HopFrameConfiguratorExtensions.cs
Normal file
44
src/HopFrame.Core/EFCore/HopFrameConfiguratorExtensions.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
using System.Linq.Expressions;
|
||||||
|
using HopFrame.Core.Configuration;
|
||||||
|
using HopFrame.Core.Configurators;
|
||||||
|
using HopFrame.Core.Helpers;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace HopFrame.Core.EFCore;
|
||||||
|
|
||||||
|
/** Adds useful extensions to the <see cref="HopFrameConfigurator"/> to add managed <see cref="DbContext"/> repositories */
|
||||||
|
public static class HopFrameConfiguratorExtensions {
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds managed repositories for the selected (or all if none provided) tables
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configurator">The configurator for the current <see cref="HopFrameConfig"/></param>
|
||||||
|
/// <param name="includedTables">The tables that should be configured (if none are provided, all tables will be added)</param>
|
||||||
|
/// <typeparam name="TDbContext">The already configured and injectable database context</typeparam>
|
||||||
|
public static HopFrameConfigurator AddDbContext<TDbContext>(this HopFrameConfigurator configurator, params Expression<Func<TDbContext, object>>[] includedTables) where TDbContext : DbContext {
|
||||||
|
var contextType = typeof(TDbContext);
|
||||||
|
var properties = contextType.GetProperties()
|
||||||
|
.Where(p => p.PropertyType.IsGenericType)
|
||||||
|
.Where(p => p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>));
|
||||||
|
|
||||||
|
if (includedTables.Length != 0) {
|
||||||
|
properties = includedTables.Select(ExpressionHelper.GetPropertyInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
var tableIdentifiers = new List<string>();
|
||||||
|
foreach (var tableProperty in properties) {
|
||||||
|
var identifier = DbConfigPopulator.ConfigureRepository(configurator.Config, configurator.Services, contextType, tableProperty);
|
||||||
|
tableIdentifiers.Add(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
var createdTables = configurator.Config.Tables
|
||||||
|
.Where(t => tableIdentifiers.Contains(t.Identifier))
|
||||||
|
.ToArray();
|
||||||
|
foreach (var createdTable in createdTables) {
|
||||||
|
DbConfigPopulator.CheckForRelations(configurator.Config, createdTable);
|
||||||
|
}
|
||||||
|
|
||||||
|
return configurator;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
using System.Reflection;
|
using System.Collections;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using HopFrame.Core.Configuration;
|
using HopFrame.Core.Configuration;
|
||||||
// ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
|
// ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
|
||||||
|
|
||||||
@@ -28,7 +31,7 @@ internal static class ConfigurationHelper {
|
|||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PropertyConfig InitializeProperty(TableConfig table, PropertyInfo property) {
|
public static PropertyConfig InitializeProperty(TableConfig table, PropertyInfo property) {
|
||||||
var identifier = property.Name;
|
var identifier = property.Name;
|
||||||
|
|
||||||
if (table.Properties.Any(p => p.Identifier == identifier))
|
if (table.Properties.Any(p => p.Identifier == identifier))
|
||||||
@@ -38,12 +41,60 @@ internal static class ConfigurationHelper {
|
|||||||
Identifier = identifier,
|
Identifier = identifier,
|
||||||
Type = property.PropertyType,
|
Type = property.PropertyType,
|
||||||
DisplayName = property.Name,
|
DisplayName = property.Name,
|
||||||
OrderIndex = table.Properties.Count
|
OrderIndex = table.Properties.Count,
|
||||||
|
PropertyType = InferPropertyType(property.PropertyType, property)
|
||||||
};
|
};
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static PropertyType InferPropertyType(Type realType, PropertyInfo info) {
|
||||||
|
byte modifiers = 0;
|
||||||
|
|
||||||
|
if (Nullable.GetUnderlyingType(realType) != null) {
|
||||||
|
modifiers |= (byte)PropertyType.Nullable;
|
||||||
|
realType = Nullable.GetUnderlyingType(realType)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.CustomAttributes.Any(a => a.AttributeType == typeof(NullableAttribute))) {
|
||||||
|
modifiers |= (byte)PropertyType.Nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((realType.IsAssignableTo(typeof(IEnumerable)) || realType.IsAssignableTo(typeof(IEnumerable<>))) && realType != typeof(string)) {
|
||||||
|
modifiers |= (byte)PropertyType.List;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (realType.IsGenericType) {
|
||||||
|
realType = realType.GenericTypeArguments.First();
|
||||||
|
}
|
||||||
|
|
||||||
|
var type = PropertyType.Text;
|
||||||
|
var realTypeCode = Type.GetTypeCode(realType);
|
||||||
|
|
||||||
|
if (realTypeCode == TypeCode.Boolean)
|
||||||
|
type = PropertyType.Boolean;
|
||||||
|
|
||||||
|
if (realTypeCode == TypeCode.DateTime)
|
||||||
|
type = PropertyType.DateTime;
|
||||||
|
|
||||||
|
if (realType == typeof(DateOnly))
|
||||||
|
type = PropertyType.DateOnly;
|
||||||
|
|
||||||
|
if (realType == typeof(TimeOnly))
|
||||||
|
type = PropertyType.TimeOnly;
|
||||||
|
|
||||||
|
if (realType.IsEnum)
|
||||||
|
type = PropertyType.Enum;
|
||||||
|
|
||||||
|
if (realType.IsNumeric())
|
||||||
|
type = PropertyType.Numeric;
|
||||||
|
|
||||||
|
if (info.CustomAttributes.Any(a => a.AttributeType == typeof(EmailAddressAttribute)))
|
||||||
|
type = PropertyType.Email;
|
||||||
|
|
||||||
|
return (PropertyType)((byte)type | modifiers);
|
||||||
|
}
|
||||||
|
|
||||||
public static IEnumerable<string> ValidateTable(HopFrameConfig global, TableConfig config) {
|
public static IEnumerable<string> ValidateTable(HopFrameConfig global, TableConfig config) {
|
||||||
if (global.Tables.Any(t => t.Identifier == config.Identifier))
|
if (global.Tables.Any(t => t.Identifier == config.Identifier))
|
||||||
yield return $"Table identifier '{config.Identifier}' is not unique";
|
yield return $"Table identifier '{config.Identifier}' is not unique";
|
||||||
|
|||||||
23
src/HopFrame.Core/Helpers/TypeExtensions.cs
Normal file
23
src/HopFrame.Core/Helpers/TypeExtensions.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
namespace HopFrame.Core.Helpers;
|
||||||
|
|
||||||
|
internal static class TypeExtensions {
|
||||||
|
public static bool IsNumeric(this Type o) {
|
||||||
|
if (o.IsEnum) return false;
|
||||||
|
switch (Type.GetTypeCode(o)) {
|
||||||
|
case TypeCode.Byte:
|
||||||
|
case TypeCode.SByte:
|
||||||
|
case TypeCode.UInt16:
|
||||||
|
case TypeCode.UInt32:
|
||||||
|
case TypeCode.UInt64:
|
||||||
|
case TypeCode.Int16:
|
||||||
|
case TypeCode.Int32:
|
||||||
|
case TypeCode.Int64:
|
||||||
|
case TypeCode.Decimal:
|
||||||
|
case TypeCode.Double:
|
||||||
|
case TypeCode.Single:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.3" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace HopFrame.Core.Repositories;
|
namespace HopFrame.Core.Repositories;
|
||||||
|
|
||||||
/** The base repository that provides access to the model dataset */
|
/** The base repository that provides access to the model dataset */
|
||||||
public abstract class HopFrameRepository<TModel> : IHopFrameRepository where TModel : notnull {
|
public abstract class HopFrameRepository<TModel> : IHopFrameRepository where TModel : class {
|
||||||
|
|
||||||
/** <inheritdoc cref="LoadPageGenericAsync"/> */
|
/** <inheritdoc cref="LoadPageGenericAsync"/> */
|
||||||
public abstract Task<IEnumerable<TModel>> LoadPageAsync(int page, int perPage, CancellationToken ct = default);
|
public abstract Task<IEnumerable<TModel>> LoadPageAsync(int page, int perPage, CancellationToken ct = default);
|
||||||
@@ -15,18 +15,21 @@ public abstract class HopFrameRepository<TModel> : IHopFrameRepository where TMo
|
|||||||
public abstract Task<IEnumerable<TModel>> SearchAsync(string searchTerm, int page, int perPage, CancellationToken ct = default);
|
public abstract Task<IEnumerable<TModel>> SearchAsync(string searchTerm, int page, int perPage, CancellationToken ct = default);
|
||||||
|
|
||||||
/** <inheritdoc cref="CreateGenericAsync"/> */
|
/** <inheritdoc cref="CreateGenericAsync"/> */
|
||||||
public abstract Task CreateAsync(TModel entry, CancellationToken ct);
|
public abstract Task CreateAsync(TModel entry, CancellationToken ct = default);
|
||||||
|
|
||||||
|
/** <inheritdoc cref="UpdateGenericAsync"/> */
|
||||||
|
public abstract Task UpdateAsync(TModel entry, CancellationToken ct = default);
|
||||||
|
|
||||||
/** <inheritdoc cref="DeleteGenericAsync"/> */
|
/** <inheritdoc cref="DeleteGenericAsync"/> */
|
||||||
public abstract Task DeleteAsync(TModel entry, CancellationToken ct);
|
public abstract Task DeleteAsync(TModel entry, CancellationToken ct = default);
|
||||||
|
|
||||||
/** <inheritdoc/> */
|
/** <inheritdoc/> */
|
||||||
public async Task<IEnumerable> LoadPageGenericAsync(int page, int perPage, CancellationToken ct = default) {
|
public async Task<IEnumerable> LoadPageGenericAsync(int page, int perPage, CancellationToken ct) {
|
||||||
return await LoadPageAsync(page, perPage, ct);
|
return await LoadPageAsync(page, perPage, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** <inheritdoc/> */
|
/** <inheritdoc/> */
|
||||||
public async Task<IEnumerable> SearchGenericAsync(string searchTerm, int page, int perPage, CancellationToken ct = default) {
|
public async Task<IEnumerable> SearchGenericAsync(string searchTerm, int page, int perPage, CancellationToken ct) {
|
||||||
return await SearchAsync(searchTerm, page, perPage, ct);
|
return await SearchAsync(searchTerm, page, perPage, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,6 +38,11 @@ public abstract class HopFrameRepository<TModel> : IHopFrameRepository where TMo
|
|||||||
return CreateAsync((TModel)entry, ct);
|
return CreateAsync((TModel)entry, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** <inheritdoc/> */
|
||||||
|
public Task UpdateGenericAsync(object entry, CancellationToken ct) {
|
||||||
|
return UpdateAsync((TModel)entry, ct);
|
||||||
|
}
|
||||||
|
|
||||||
/** <inheritdoc/> */
|
/** <inheritdoc/> */
|
||||||
public Task DeleteGenericAsync(object entry, CancellationToken ct) {
|
public Task DeleteGenericAsync(object entry, CancellationToken ct) {
|
||||||
return DeleteAsync((TModel)entry, ct);
|
return DeleteAsync((TModel)entry, ct);
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ public interface IHopFrameRepository {
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="page">The index of the current page (starts at 0)</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>
|
/// <param name="perPage">The amount of entries that should be loaded</param>
|
||||||
public Task<IEnumerable> LoadPageGenericAsync(int page, int perPage, CancellationToken ct = default);
|
public Task<IEnumerable> LoadPageGenericAsync(int page, int perPage, CancellationToken ct);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the total amount of entries in the dataset
|
/// Returns the total amount of entries in the dataset
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Task<int> CountAsync(CancellationToken ct = default);
|
public Task<int> CountAsync(CancellationToken ct);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Searches through the whole dataset and returns a page of matching entries
|
/// Searches through the whole dataset and returns a page of matching entries
|
||||||
@@ -24,7 +24,7 @@ public interface IHopFrameRepository {
|
|||||||
/// <param name="searchTerm">The search text provided by the user</param>
|
/// <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="page">The index of the current page (starts at 0)</param>
|
||||||
/// <param name="perPage">The amount of entries that should be loaded</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 = default);
|
public Task<IEnumerable> SearchGenericAsync(string searchTerm, int page, int perPage, CancellationToken ct);
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -33,6 +33,12 @@ public interface IHopFrameRepository {
|
|||||||
/// <param name="entry">The entry that needs to be saved</param>
|
/// <param name="entry">The entry that needs to be saved</param>
|
||||||
public Task CreateGenericAsync(object entry, CancellationToken ct);
|
public Task CreateGenericAsync(object entry, CancellationToken ct);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Saves the changes made to the entry to the dataset
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entry">The modified entry</param>
|
||||||
|
public Task UpdateGenericAsync(object entry, CancellationToken ct);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deletes the provided entry from the dataset
|
/// Deletes the provided entry from the dataset
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -8,18 +8,23 @@ namespace HopFrame.Tests.Core.Configurators;
|
|||||||
|
|
||||||
public class HopFrameConfiguratorTests {
|
public class HopFrameConfiguratorTests {
|
||||||
private class TestRepository : IHopFrameRepository {
|
private class TestRepository : IHopFrameRepository {
|
||||||
public Task<IEnumerable> LoadPageGenericAsync(int page, int perPage, CancellationToken ct = default) {
|
public Task<IEnumerable> LoadPageGenericAsync(int page, int perPage, CancellationToken ct) {
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
public Task<int> CountAsync(CancellationToken ct = default) {
|
public Task<int> CountAsync(CancellationToken ct) {
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
public Task<IEnumerable> SearchGenericAsync(string searchTerm, int page, int perPage, CancellationToken ct = default) {
|
public Task<IEnumerable> SearchGenericAsync(string searchTerm, int page, int perPage, CancellationToken ct) {
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
public Task CreateGenericAsync(object entry, CancellationToken ct) {
|
public Task CreateGenericAsync(object entry, CancellationToken ct) {
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task UpdateGenericAsync(object entry, CancellationToken ct) {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
public Task DeleteGenericAsync(object entry, CancellationToken ct) {
|
public Task DeleteGenericAsync(object entry, CancellationToken ct) {
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
@@ -41,7 +46,8 @@ public class HopFrameConfiguratorTests {
|
|||||||
Identifier = "Id",
|
Identifier = "Id",
|
||||||
DisplayName = "Id",
|
DisplayName = "Id",
|
||||||
Type = typeof(int),
|
Type = typeof(int),
|
||||||
OrderIndex = 0
|
OrderIndex = 0,
|
||||||
|
PropertyType = PropertyType.Numeric
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
using HopFrame.Core.Configuration;
|
||||||
|
using HopFrame.Core.Configurators;
|
||||||
|
|
||||||
|
namespace HopFrame.Tests.Core.Configurators;
|
||||||
|
|
||||||
|
public class PropertyConfiguratorTests {
|
||||||
|
private PropertyConfig CreateConfig(PropertyType type)
|
||||||
|
=> new PropertyConfig {
|
||||||
|
Identifier = "Test",
|
||||||
|
DisplayName = "Test",
|
||||||
|
Type = typeof(string),
|
||||||
|
OrderIndex = 0,
|
||||||
|
PropertyType = type
|
||||||
|
};
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetType_ReplacesBaseType_AndPreservesModifiers() {
|
||||||
|
// Arrange: Nullable + List + Numeric
|
||||||
|
var original = PropertyType.Numeric | PropertyType.Nullable | PropertyType.List;
|
||||||
|
var config = CreateConfig(original);
|
||||||
|
|
||||||
|
var configurator = new PropertyConfigurator(config);
|
||||||
|
|
||||||
|
// Act: change base type to Text
|
||||||
|
configurator.SetType(PropertyType.Text);
|
||||||
|
|
||||||
|
// Assert: modifiers remain, base type replaced
|
||||||
|
Assert.Equal(
|
||||||
|
PropertyType.Text | PropertyType.Nullable | PropertyType.List,
|
||||||
|
config.PropertyType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetType_DoesNotAffectModifiers_WhenSettingSameBaseType() {
|
||||||
|
var original = PropertyType.Boolean | PropertyType.Nullable;
|
||||||
|
var config = CreateConfig(original);
|
||||||
|
|
||||||
|
var configurator = new PropertyConfigurator(config);
|
||||||
|
|
||||||
|
configurator.SetType(PropertyType.Boolean);
|
||||||
|
|
||||||
|
Assert.Equal(original, config.PropertyType);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetType_CanChangeEnumToNumeric_WhileKeepingModifiers() {
|
||||||
|
var original = PropertyType.Enum | PropertyType.List;
|
||||||
|
var config = CreateConfig(original);
|
||||||
|
|
||||||
|
var configurator = new PropertyConfigurator(config);
|
||||||
|
|
||||||
|
configurator.SetType(PropertyType.Numeric);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
PropertyType.Numeric | PropertyType.List,
|
||||||
|
config.PropertyType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetType_CanChangeToEmail_AndPreserveNullable() {
|
||||||
|
var original = PropertyType.Text | PropertyType.Nullable;
|
||||||
|
var config = CreateConfig(original);
|
||||||
|
|
||||||
|
var configurator = new PropertyConfigurator(config);
|
||||||
|
|
||||||
|
configurator.SetType(PropertyType.Email);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
PropertyType.Email | PropertyType.Nullable,
|
||||||
|
config.PropertyType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetType_ReturnsConfigurator_ForFluentApi() {
|
||||||
|
var config = CreateConfig(PropertyType.Text);
|
||||||
|
var configurator = new PropertyConfigurator(config);
|
||||||
|
|
||||||
|
var result = configurator.SetType(PropertyType.Numeric);
|
||||||
|
|
||||||
|
Assert.Same(configurator, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
147
tests/HopFrame.Tests.Core/EFCore/DbConfigPopulatorTests.cs
Normal file
147
tests/HopFrame.Tests.Core/EFCore/DbConfigPopulatorTests.cs
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
using HopFrame.Core.Configuration;
|
||||||
|
using HopFrame.Core.EFCore;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace HopFrame.Tests.Core.EFCore;
|
||||||
|
|
||||||
|
public class DbConfigPopulatorTests {
|
||||||
|
private class TestContext { }
|
||||||
|
|
||||||
|
private class TestModel {
|
||||||
|
public int Id { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class OtherModel {
|
||||||
|
public int X { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private HopFrameConfig CreateConfig(params TableConfig[] tables)
|
||||||
|
=> new HopFrameConfig { Tables = tables.ToList() };
|
||||||
|
|
||||||
|
private TableConfig CreateTable(Type modelType)
|
||||||
|
=> new TableConfig {
|
||||||
|
Identifier = modelType.Name,
|
||||||
|
TableType = modelType,
|
||||||
|
RepositoryType = typeof(object),
|
||||||
|
Route = modelType.Name.ToLower(),
|
||||||
|
DisplayName = modelType.Name,
|
||||||
|
OrderIndex = 0,
|
||||||
|
Properties = new List<PropertyConfig> {
|
||||||
|
new PropertyConfig {
|
||||||
|
Identifier = "Id",
|
||||||
|
DisplayName = "Id",
|
||||||
|
Type = typeof(int),
|
||||||
|
PropertyType = PropertyType.Numeric
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// ConfigureRepository
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConfigureRepository_RegistersRepositoryType() {
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
var global = CreateConfig();
|
||||||
|
|
||||||
|
var prop = typeof(TestDbContext).GetProperty(nameof(TestDbContext.TestModels))!;
|
||||||
|
|
||||||
|
var identifier = DbConfigPopulator.ConfigureRepository(global, services, typeof(TestDbContext), prop);
|
||||||
|
|
||||||
|
var repoType = typeof(EfCoreRepository<TestModel, TestDbContext>);
|
||||||
|
|
||||||
|
Assert.Contains(services, d => d.ServiceType == repoType);
|
||||||
|
Assert.Single(global.Tables);
|
||||||
|
Assert.Equal(identifier, global.Tables[0].Identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConfigureRepository_AddsTableToGlobalConfig() {
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
var global = CreateConfig();
|
||||||
|
|
||||||
|
var prop = typeof(TestDbContext).GetProperty(nameof(TestDbContext.TestModels))!;
|
||||||
|
|
||||||
|
DbConfigPopulator.ConfigureRepository(global, services, typeof(TestDbContext), prop);
|
||||||
|
|
||||||
|
Assert.Single(global.Tables);
|
||||||
|
Assert.Equal(typeof(TestModel), global.Tables[0].TableType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestDbContext : DbContext {
|
||||||
|
public List<TestModel> TestModels { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// CheckForRelations
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
|
||||||
|
private class RelationModel {
|
||||||
|
public OtherModel Single { get; set; } = new();
|
||||||
|
public List<OtherModel> Many { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CheckForRelations_SetsRelationFlag_ForSingleReference() {
|
||||||
|
var otherTable = CreateTable(typeof(OtherModel));
|
||||||
|
var relationTable = CreateTable(typeof(RelationModel));
|
||||||
|
|
||||||
|
relationTable.Properties = new List<PropertyConfig> {
|
||||||
|
new PropertyConfig {
|
||||||
|
Identifier = "Single",
|
||||||
|
DisplayName = "Single",
|
||||||
|
Type = typeof(OtherModel),
|
||||||
|
PropertyType = PropertyType.Text
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var global = CreateConfig(otherTable, relationTable);
|
||||||
|
|
||||||
|
DbConfigPopulator.CheckForRelations(global, relationTable);
|
||||||
|
|
||||||
|
Assert.True((relationTable.Properties[0].PropertyType & PropertyType.Relation) != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CheckForRelations_SetsRelationFlag_ForListReference() {
|
||||||
|
var otherTable = CreateTable(typeof(OtherModel));
|
||||||
|
var relationTable = CreateTable(typeof(RelationModel));
|
||||||
|
|
||||||
|
relationTable.Properties = new List<PropertyConfig> {
|
||||||
|
new PropertyConfig {
|
||||||
|
Identifier = "Many",
|
||||||
|
DisplayName = "Many",
|
||||||
|
Type = typeof(List<OtherModel>),
|
||||||
|
PropertyType = PropertyType.List
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var global = CreateConfig(otherTable, relationTable);
|
||||||
|
|
||||||
|
DbConfigPopulator.CheckForRelations(global, relationTable);
|
||||||
|
|
||||||
|
Assert.True((relationTable.Properties[0].PropertyType & PropertyType.Relation) != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CheckForRelations_DoesNotSetRelationFlag_WhenNoMatchingTableExists() {
|
||||||
|
var relationTable = CreateTable(typeof(RelationModel));
|
||||||
|
|
||||||
|
relationTable.Properties = new List<PropertyConfig> {
|
||||||
|
new PropertyConfig {
|
||||||
|
Identifier = "Single",
|
||||||
|
DisplayName = "Single",
|
||||||
|
Type = typeof(OtherModel),
|
||||||
|
PropertyType = PropertyType.Text
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var global = CreateConfig(relationTable);
|
||||||
|
|
||||||
|
DbConfigPopulator.CheckForRelations(global, relationTable);
|
||||||
|
|
||||||
|
Assert.False((relationTable.Properties[0].PropertyType & PropertyType.Relation) != 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
using HopFrame.Core.Configuration;
|
||||||
|
using HopFrame.Core.Configurators;
|
||||||
|
using HopFrame.Core.EFCore;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace HopFrame.Tests.Core.EFCore;
|
||||||
|
|
||||||
|
public class HopFrameConfiguratorExtensionsTests {
|
||||||
|
private class TestModelA {
|
||||||
|
public int Id { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestModelB {
|
||||||
|
public int Id { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestDbContext : DbContext {
|
||||||
|
public DbSet<TestModelA> A { get; set; } = null!;
|
||||||
|
public DbSet<TestModelB> B { get; set; } = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HopFrameConfig CreateConfig()
|
||||||
|
=> new HopFrameConfig { Tables = new List<TableConfig>() };
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// AddDbContext - all tables
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddDbContext_AddsAllDbSets_WhenNoFilterProvided() {
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
var config = CreateConfig();
|
||||||
|
var configurator = new HopFrameConfigurator(config, services);
|
||||||
|
|
||||||
|
configurator.AddDbContext<TestDbContext>();
|
||||||
|
|
||||||
|
Assert.Equal(2, config.Tables.Count);
|
||||||
|
Assert.Contains(config.Tables, t => t.TableType == typeof(TestModelA));
|
||||||
|
Assert.Contains(config.Tables, t => t.TableType == typeof(TestModelB));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddDbContext_RegistersRepositoriesForAllDbSets() {
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
var config = CreateConfig();
|
||||||
|
var configurator = new HopFrameConfigurator(config, services);
|
||||||
|
|
||||||
|
configurator.AddDbContext<TestDbContext>();
|
||||||
|
|
||||||
|
Assert.Contains(services, d => d.ServiceType == typeof(EfCoreRepository<TestModelA, TestDbContext>));
|
||||||
|
Assert.Contains(services, d => d.ServiceType == typeof(EfCoreRepository<TestModelB, TestDbContext>));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// AddDbContext - filtered tables
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddDbContext_UsesOnlyIncludedTables_WhenFilterProvided() {
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
var config = CreateConfig();
|
||||||
|
var configurator = new HopFrameConfigurator(config, services);
|
||||||
|
|
||||||
|
configurator.AddDbContext<TestDbContext>(ctx => ctx.A
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.Single(config.Tables);
|
||||||
|
Assert.Equal(typeof(TestModelA), config.Tables[0].TableType);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddDbContext_RegistersOnlyFilteredRepositories() {
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
var config = CreateConfig();
|
||||||
|
var configurator = new HopFrameConfigurator(config, services);
|
||||||
|
|
||||||
|
configurator.AddDbContext<TestDbContext>(ctx => ctx.A
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.Contains(services, d => d.ServiceType == typeof(EfCoreRepository<TestModelA, TestDbContext>));
|
||||||
|
Assert.DoesNotContain(services, d => d.ServiceType == typeof(EfCoreRepository<TestModelB, TestDbContext>));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// Relation detection
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
|
||||||
|
private class RelationModel {
|
||||||
|
public TestModelA Single { get; set; } = null!;
|
||||||
|
public List<TestModelB> Many { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class RelationDbContext : DbContext {
|
||||||
|
public DbSet<TestModelA> A { get; set; } = null!;
|
||||||
|
public DbSet<TestModelB> B { get; set; } = null!;
|
||||||
|
public DbSet<RelationModel> R { get; set; } = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddDbContext_ChecksRelationsForCreatedTables() {
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
var config = CreateConfig();
|
||||||
|
var configurator = new HopFrameConfigurator(config, services);
|
||||||
|
|
||||||
|
configurator.AddDbContext<RelationDbContext>();
|
||||||
|
|
||||||
|
var relationTable = config.Tables.Single(t => t.TableType == typeof(RelationModel));
|
||||||
|
|
||||||
|
// At least one property should have the Relation flag
|
||||||
|
Assert.Contains(relationTable.Properties, p => (p.PropertyType & PropertyType.Relation) != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// Fluent API
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddDbContext_ReturnsConfigurator() {
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
var config = CreateConfig();
|
||||||
|
var configurator = new HopFrameConfigurator(config, services);
|
||||||
|
|
||||||
|
var result = configurator.AddDbContext<TestDbContext>();
|
||||||
|
|
||||||
|
Assert.Same(configurator, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Reflection;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using HopFrame.Core.Configuration;
|
using HopFrame.Core.Configuration;
|
||||||
using HopFrame.Core.Helpers;
|
using HopFrame.Core.Helpers;
|
||||||
|
|
||||||
@@ -7,6 +7,28 @@ using HopFrame.Core.Helpers;
|
|||||||
namespace HopFrame.Tests.Core.Helpers;
|
namespace HopFrame.Tests.Core.Helpers;
|
||||||
|
|
||||||
public class ConfigurationHelperTests {
|
public class ConfigurationHelperTests {
|
||||||
|
private class PropertyTypeModel {
|
||||||
|
public int Number { get; set; }
|
||||||
|
public int? NullableNumber { get; set; }
|
||||||
|
public string Text { get; set; } = "";
|
||||||
|
public bool Flag { get; set; }
|
||||||
|
public DateTime Timestamp { get; set; }
|
||||||
|
public DateOnly Date { get; set; }
|
||||||
|
public TimeOnly Time { get; set; }
|
||||||
|
public TestEnum EnumValue { get; set; }
|
||||||
|
public List<int> Numbers { get; set; } = new();
|
||||||
|
public IEnumerable<string> Strings { get; set; } = new List<string>();
|
||||||
|
|
||||||
|
[EmailAddress]
|
||||||
|
public string Email { get; set; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum TestEnum {
|
||||||
|
A,
|
||||||
|
B
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private HopFrameConfig CreateGlobal(params TableConfig[] tables)
|
private HopFrameConfig CreateGlobal(params TableConfig[] tables)
|
||||||
=> new HopFrameConfig { Tables = tables.ToList() };
|
=> new HopFrameConfig { Tables = tables.ToList() };
|
||||||
|
|
||||||
@@ -21,7 +43,8 @@ public class ConfigurationHelperTests {
|
|||||||
new PropertyConfig {
|
new PropertyConfig {
|
||||||
Identifier = "Prop1",
|
Identifier = "Prop1",
|
||||||
DisplayName = "Property 1",
|
DisplayName = "Property 1",
|
||||||
Type = typeof(int)
|
Type = typeof(int),
|
||||||
|
PropertyType = PropertyType.Numeric
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -92,12 +115,30 @@ public class ConfigurationHelperTests {
|
|||||||
Assert.Contains(config.Properties, p => p.Identifier == "Name");
|
Assert.Contains(config.Properties, p => p.Identifier == "Name");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InitializeTable_SetsPropertyTypes_ForAllProperties() {
|
||||||
|
var global = CreateGlobal();
|
||||||
|
|
||||||
|
var config = ConfigurationHelper.InitializeTable(global, typeof(string), typeof(PropertyTypeModel));
|
||||||
|
|
||||||
|
Assert.Contains(config.Properties, p => p.PropertyType == PropertyType.Numeric);
|
||||||
|
Assert.Contains(config.Properties, p => p.PropertyType == (PropertyType.Numeric | PropertyType.Nullable));
|
||||||
|
Assert.Contains(config.Properties, p => p.PropertyType == PropertyType.Boolean);
|
||||||
|
Assert.Contains(config.Properties, p => p.PropertyType == PropertyType.DateTime);
|
||||||
|
Assert.Contains(config.Properties, p => p.PropertyType == PropertyType.DateOnly);
|
||||||
|
Assert.Contains(config.Properties, p => p.PropertyType == PropertyType.TimeOnly);
|
||||||
|
Assert.Contains(config.Properties, p => p.PropertyType == PropertyType.Enum);
|
||||||
|
Assert.Contains(config.Properties, p => p.PropertyType == (PropertyType.Numeric | PropertyType.List));
|
||||||
|
Assert.Contains(config.Properties, p => p.PropertyType == (PropertyType.Text | PropertyType.List));
|
||||||
|
Assert.Contains(config.Properties, p => p.PropertyType == PropertyType.Email);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void InitializeProperty_UsesPropertyNameAsIdentifier_WhenUnique() {
|
public void InitializeProperty_UsesPropertyNameAsIdentifier_WhenUnique() {
|
||||||
var table = CreateDummyTable("T");
|
var table = CreateDummyTable("T");
|
||||||
var property = typeof(TestModel).GetProperty(nameof(TestModel.Id))!;
|
var property = typeof(TestModel).GetProperty(nameof(TestModel.Id))!;
|
||||||
|
|
||||||
var config = InvokeInitializeProperty(table, property);
|
var config = ConfigurationHelper.InitializeProperty(table, property);
|
||||||
|
|
||||||
Assert.Equal("Id", config.Identifier);
|
Assert.Equal("Id", config.Identifier);
|
||||||
}
|
}
|
||||||
@@ -105,12 +146,14 @@ public class ConfigurationHelperTests {
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void InitializeProperty_GeneratesGuid_WhenIdentifierAlreadyExists() {
|
public void InitializeProperty_GeneratesGuid_WhenIdentifierAlreadyExists() {
|
||||||
var table = CreateDummyTable("T");
|
var table = CreateDummyTable("T");
|
||||||
table.Properties.Add(new PropertyConfig
|
table.Properties.Add(new PropertyConfig {
|
||||||
{ Identifier = "Id", Type = typeof(int), DisplayName = "Id", OrderIndex = 0 });
|
Identifier = "Id", Type = typeof(int), DisplayName = "Id", OrderIndex = 0,
|
||||||
|
PropertyType = PropertyType.Numeric
|
||||||
|
});
|
||||||
|
|
||||||
var property = typeof(TestModel).GetProperty(nameof(TestModel.Id))!;
|
var property = typeof(TestModel).GetProperty(nameof(TestModel.Id))!;
|
||||||
|
|
||||||
var config = InvokeInitializeProperty(table, property);
|
var config = ConfigurationHelper.InitializeProperty(table, property);
|
||||||
|
|
||||||
Assert.NotEqual("Id", config.Identifier);
|
Assert.NotEqual("Id", config.Identifier);
|
||||||
Assert.True(Guid.TryParse(config.Identifier, out _));
|
Assert.True(Guid.TryParse(config.Identifier, out _));
|
||||||
@@ -121,7 +164,7 @@ public class ConfigurationHelperTests {
|
|||||||
var table = CreateDummyTable("T");
|
var table = CreateDummyTable("T");
|
||||||
var property = typeof(TestModel).GetProperty(nameof(TestModel.Name))!;
|
var property = typeof(TestModel).GetProperty(nameof(TestModel.Name))!;
|
||||||
|
|
||||||
var config = InvokeInitializeProperty(table, property);
|
var config = ConfigurationHelper.InitializeProperty(table, property);
|
||||||
|
|
||||||
Assert.Equal("Name", config.DisplayName);
|
Assert.Equal("Name", config.DisplayName);
|
||||||
Assert.Equal(typeof(string), config.Type);
|
Assert.Equal(typeof(string), config.Type);
|
||||||
@@ -131,23 +174,28 @@ public class ConfigurationHelperTests {
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void InitializeProperty_SetsOrderIndex_ToCurrentPropertyCount() {
|
public void InitializeProperty_SetsOrderIndex_ToCurrentPropertyCount() {
|
||||||
var table = CreateDummyTable("T");
|
var table = CreateDummyTable("T");
|
||||||
table.Properties.Add(new PropertyConfig
|
table.Properties.Add(new PropertyConfig {
|
||||||
{ Identifier = "X", Type = typeof(int), DisplayName = "X", OrderIndex = 0 });
|
Identifier = "X", Type = typeof(int), DisplayName = "X", OrderIndex = 0, PropertyType = PropertyType.Numeric
|
||||||
|
});
|
||||||
|
|
||||||
var property = typeof(TestModel).GetProperty(nameof(TestModel.Name))!;
|
var property = typeof(TestModel).GetProperty(nameof(TestModel.Name))!;
|
||||||
|
|
||||||
var config = InvokeInitializeProperty(table, property);
|
var config = ConfigurationHelper.InitializeProperty(table, property);
|
||||||
|
|
||||||
Assert.Equal(1, config.OrderIndex);
|
Assert.Equal(1, config.OrderIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
private PropertyConfig InvokeInitializeProperty(TableConfig table, PropertyInfo property) {
|
[Fact]
|
||||||
var method = typeof(ConfigurationHelper)
|
public void InitializeProperty_SetsPropertyType_FromInferPropertyType() {
|
||||||
.GetMethod("InitializeProperty", BindingFlags.NonPublic | BindingFlags.Static)!;
|
var table = CreateDummyTable("T");
|
||||||
|
var prop = typeof(PropertyTypeModel).GetProperty(nameof(PropertyTypeModel.Number))!;
|
||||||
|
|
||||||
return (PropertyConfig)method.Invoke(null, [table, property])!;
|
var config = ConfigurationHelper.InitializeProperty(table, prop);
|
||||||
|
|
||||||
|
Assert.Equal(PropertyType.Numeric, config.PropertyType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ValidateTable_ReturnsError_WhenIdentifierNotUnique() {
|
public void ValidateTable_ReturnsError_WhenIdentifierNotUnique() {
|
||||||
var config = CreateValidTable();
|
var config = CreateValidTable();
|
||||||
@@ -210,7 +258,8 @@ public class ConfigurationHelperTests {
|
|||||||
config.Properties.Add(new PropertyConfig {
|
config.Properties.Add(new PropertyConfig {
|
||||||
Identifier = "Prop1",
|
Identifier = "Prop1",
|
||||||
DisplayName = "Duplicate",
|
DisplayName = "Duplicate",
|
||||||
Type = typeof(int)
|
Type = typeof(int),
|
||||||
|
PropertyType = PropertyType.Numeric
|
||||||
});
|
});
|
||||||
|
|
||||||
var result = ConfigurationHelper.ValidateTable(CreateGlobal(), config).ToList();
|
var result = ConfigurationHelper.ValidateTable(CreateGlobal(), config).ToList();
|
||||||
@@ -246,4 +295,84 @@ public class ConfigurationHelperTests {
|
|||||||
|
|
||||||
Assert.Empty(result);
|
Assert.Empty(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InferPropertyType_RecognizesNumeric() {
|
||||||
|
var prop = typeof(PropertyTypeModel).GetProperty(nameof(PropertyTypeModel.Number))!;
|
||||||
|
var result = ConfigurationHelper.InferPropertyType(typeof(int), prop);
|
||||||
|
|
||||||
|
Assert.Equal(PropertyType.Numeric, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InferPropertyType_RecognizesNullableNumeric() {
|
||||||
|
var prop = typeof(PropertyTypeModel).GetProperty(nameof(PropertyTypeModel.NullableNumber))!;
|
||||||
|
var result = ConfigurationHelper.InferPropertyType(typeof(int?), prop);
|
||||||
|
|
||||||
|
Assert.Equal(PropertyType.Numeric | PropertyType.Nullable, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InferPropertyType_RecognizesBoolean() {
|
||||||
|
var prop = typeof(PropertyTypeModel).GetProperty(nameof(PropertyTypeModel.Flag))!;
|
||||||
|
var result = ConfigurationHelper.InferPropertyType(typeof(bool), prop);
|
||||||
|
|
||||||
|
Assert.Equal(PropertyType.Boolean, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InferPropertyType_RecognizesDateTime() {
|
||||||
|
var prop = typeof(PropertyTypeModel).GetProperty(nameof(PropertyTypeModel.Timestamp))!;
|
||||||
|
var result = ConfigurationHelper.InferPropertyType(typeof(DateTime), prop);
|
||||||
|
|
||||||
|
Assert.Equal(PropertyType.DateTime, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InferPropertyType_RecognizesDateOnly() {
|
||||||
|
var prop = typeof(PropertyTypeModel).GetProperty(nameof(PropertyTypeModel.Date))!;
|
||||||
|
var result = ConfigurationHelper.InferPropertyType(typeof(DateOnly), prop);
|
||||||
|
|
||||||
|
Assert.Equal(PropertyType.DateOnly, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InferPropertyType_RecognizesTimeOnly() {
|
||||||
|
var prop = typeof(PropertyTypeModel).GetProperty(nameof(PropertyTypeModel.Time))!;
|
||||||
|
var result = ConfigurationHelper.InferPropertyType(typeof(TimeOnly), prop);
|
||||||
|
|
||||||
|
Assert.Equal(PropertyType.TimeOnly, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InferPropertyType_RecognizesEnum() {
|
||||||
|
var prop = typeof(PropertyTypeModel).GetProperty(nameof(PropertyTypeModel.EnumValue))!;
|
||||||
|
var result = ConfigurationHelper.InferPropertyType(typeof(TestEnum), prop);
|
||||||
|
|
||||||
|
Assert.Equal(PropertyType.Enum, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InferPropertyType_RecognizesList() {
|
||||||
|
var prop = typeof(PropertyTypeModel).GetProperty(nameof(PropertyTypeModel.Numbers))!;
|
||||||
|
var result = ConfigurationHelper.InferPropertyType(typeof(List<int>), prop);
|
||||||
|
|
||||||
|
Assert.Equal(PropertyType.Numeric | PropertyType.List, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InferPropertyType_RecognizesEnumerable() {
|
||||||
|
var prop = typeof(PropertyTypeModel).GetProperty(nameof(PropertyTypeModel.Strings))!;
|
||||||
|
var result = ConfigurationHelper.InferPropertyType(typeof(IEnumerable<string>), prop);
|
||||||
|
|
||||||
|
Assert.Equal(PropertyType.Text | PropertyType.List, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InferPropertyType_RecognizesEmail() {
|
||||||
|
var prop = typeof(PropertyTypeModel).GetProperty(nameof(PropertyTypeModel.Email))!;
|
||||||
|
var result = ConfigurationHelper.InferPropertyType(typeof(string), prop);
|
||||||
|
|
||||||
|
Assert.Equal(PropertyType.Email, result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,6 @@ using Moq;
|
|||||||
namespace HopFrame.Tests.Core.Repositories;
|
namespace HopFrame.Tests.Core.Repositories;
|
||||||
|
|
||||||
public class HopFrameRepositoryTests {
|
public class HopFrameRepositoryTests {
|
||||||
|
|
||||||
private Mock<HopFrameRepository<TestModel>> CreateMock()
|
private Mock<HopFrameRepository<TestModel>> CreateMock()
|
||||||
=> new(MockBehavior.Strict);
|
=> new(MockBehavior.Strict);
|
||||||
|
|
||||||
@@ -21,7 +20,7 @@ public class HopFrameRepositoryTests {
|
|||||||
mock.Setup(r => r.LoadPageAsync(2, 10, It.IsAny<CancellationToken>()))
|
mock.Setup(r => r.LoadPageAsync(2, 10, It.IsAny<CancellationToken>()))
|
||||||
.ReturnsAsync(expected);
|
.ReturnsAsync(expected);
|
||||||
|
|
||||||
var result = await mock.Object.LoadPageGenericAsync(2, 10);
|
var result = await mock.Object.LoadPageGenericAsync(2, 10, CancellationToken.None);
|
||||||
|
|
||||||
Assert.Equal(expected, result);
|
Assert.Equal(expected, result);
|
||||||
}
|
}
|
||||||
@@ -39,7 +38,7 @@ public class HopFrameRepositoryTests {
|
|||||||
mock.Setup(r => r.SearchAsync("abc", 1, 20, It.IsAny<CancellationToken>()))
|
mock.Setup(r => r.SearchAsync("abc", 1, 20, It.IsAny<CancellationToken>()))
|
||||||
.ReturnsAsync(expected);
|
.ReturnsAsync(expected);
|
||||||
|
|
||||||
var result = await mock.Object.SearchGenericAsync("abc", 1, 20);
|
var result = await mock.Object.SearchGenericAsync("abc", 1, 20, CancellationToken.None);
|
||||||
|
|
||||||
Assert.Equal(expected, result);
|
Assert.Equal(expected, result);
|
||||||
}
|
}
|
||||||
@@ -62,6 +61,24 @@ public class HopFrameRepositoryTests {
|
|||||||
mock.Verify(r => r.CreateAsync(model, It.IsAny<CancellationToken>()), Times.Once);
|
mock.Verify(r => r.CreateAsync(model, It.IsAny<CancellationToken>()), Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// UpdateGenericAsync
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateGenericAsync_CastsAndDelegates() {
|
||||||
|
var mock = CreateMock();
|
||||||
|
|
||||||
|
var model = new TestModel { Id = 77 };
|
||||||
|
|
||||||
|
mock.Setup(r => r.UpdateAsync(model, It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
await mock.Object.UpdateGenericAsync(model, CancellationToken.None);
|
||||||
|
|
||||||
|
mock.Verify(r => r.UpdateAsync(model, It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
// DeleteGenericAsync
|
// DeleteGenericAsync
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
|
|||||||
@@ -9,18 +9,23 @@ namespace HopFrame.Tests.Core.Services.Implementation;
|
|||||||
|
|
||||||
public class ConfigAccessorTests {
|
public class ConfigAccessorTests {
|
||||||
private class TestRepository : IHopFrameRepository {
|
private class TestRepository : IHopFrameRepository {
|
||||||
public Task<IEnumerable> LoadPageGenericAsync(int page, int perPage, CancellationToken ct = default) {
|
public Task<IEnumerable> LoadPageGenericAsync(int page, int perPage, CancellationToken ct) {
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
public Task<int> CountAsync(CancellationToken ct = default) {
|
public Task<int> CountAsync(CancellationToken ct) {
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
public Task<IEnumerable> SearchGenericAsync(string searchTerm, int page, int perPage, CancellationToken ct = default) {
|
public Task<IEnumerable> SearchGenericAsync(string searchTerm, int page, int perPage, CancellationToken ct) {
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
public Task CreateGenericAsync(object entry, CancellationToken ct) {
|
public Task CreateGenericAsync(object entry, CancellationToken ct) {
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task UpdateGenericAsync(object entry, CancellationToken ct) {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
public Task DeleteGenericAsync(object entry, CancellationToken ct) {
|
public Task DeleteGenericAsync(object entry, CancellationToken ct) {
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
@@ -39,7 +44,8 @@ public class ConfigAccessorTests {
|
|||||||
Identifier = "Id",
|
Identifier = "Id",
|
||||||
DisplayName = "Id",
|
DisplayName = "Id",
|
||||||
Type = typeof(int),
|
Type = typeof(int),
|
||||||
OrderIndex = 0
|
OrderIndex = 0,
|
||||||
|
PropertyType = PropertyType.Numeric
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user