实体:
using CommonSolution.Entities.Task;
using Microsoft.EntityFrameworkCore;
namespace EntityFramework.Models;
[System.ComponentModel.DataAnnotations.Schema.Table("tasks_custom_folders")]
[Microsoft.EntityFrameworkCore.EntityTypeConfiguration(typeof(TaskCustomFolderModel.Configuration))]
public class TaskCustomFolderModel
{
/* [ Theory ] `Guid.NewGuid()` return the string of 36 characters. See https://stackoverflow.com/a/4458925/4818123 */
[System.ComponentModel.DataAnnotations.Key]
[System.ComponentModel.DataAnnotations.MaxLength(36)]
public string ID { get; set; } = Guid.NewGuid().ToString();
/* ━━━ Title ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
[System.ComponentModel.DataAnnotations.MaxLength(TaskCustomFolder.Title.MAXIMAL_CHARACTERS_COUNT)]
public string Title { get; set; } = null!;
/* ━━━ Relatives ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
/* ─── Parent ───────────────────────────────────────────────────────────────────────────────────────────────────── */
/* [ Theory ] Although generally the explicit specifying of navigation property is optional, it is required for seeding. */
/* [ Theory ] `Guid.NewGuid()` return the string of 36 characters. See https://stackoverflow.com/a/4458925/4818123 */
[System.ComponentModel.DataAnnotations.MaxLength(36)]
public string? ParentID { get; set; }
public TaskCustomFolderModel? Parent { get; set; }
/* ─── Children ─────────────────────────────────────────────────────────────────────────────────────────────────── */
public List<TaskCustomFolderModel> Children { get; set; } = [];
/* ━━━ Order Among Siblings ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
public uint OrderAmongSiblings { get; set; }
/* ━━━ Configuration ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
public class Configuration : Microsoft.EntityFrameworkCore.IEntityTypeConfiguration<TaskCustomFolderModel>
{
private const string TABLE_NAME = "TasksCustomFolders";
public void Configure(
Microsoft.EntityFrameworkCore.Metadata.Builders.EntityTypeBuilder<TaskCustomFolderModel> builder)
{
builder.ToTable(Configuration.TABLE_NAME);
builder.
Property(taskCustomFolderModel => taskCustomFolderModel.Title).
IsRequired(TaskCustomFolder.Title.IS_REQUIRED);
builder.
Property(taskCustomFolderModel => taskCustomFolderModel.Parent).
IsRequired(TaskCustomFolder.Parent.IS_REQUIRED);
builder.
Property(taskCustomFolderModel => taskCustomFolderModel.ParentID).
IsRequired(TaskCustomFolder.Parent.IS_REQUIRED);
builder.
Property(taskCustomFolderModel => taskCustomFolderModel.Children).
IsRequired(TaskCustomFolder.Children.IS_REQUIRED);
builder.
Property(taskCustomFolderModel => taskCustomFolderModel.OrderAmongSiblings).
IsRequired(TaskCustomFolder.OrderAmongSiblings.IS_REQUIRED);
}
}
}
当我尝试通过调用
base.Database.EnsureCreated();
为数据库播种时出错:
System.InvalidOperationException:''列表' 无法映射属性“TaskCustomFolderModel.Children”,因为数据库提供程序不支持此类型。考虑使用值转换器将属性值转换为数据库支持的类型。有关更多信息,请参阅 https://aka.ms/efcore-docs-value-converters。或者,使用“[NotMapped]”属性或使用“OnModelCreating”中的“EntityTypeBuilder.Ignore”从模型中排除该属性。'
根据错误,数据库提供程序不支持某些内容,但是我用作参考并且也有关系的下面的示例工作正常!
public class MenuItem
{
public int Id { get; set; }
public string? Title { get; set; }
public int? ParentId { get; set; }
public MenuItem? Parent { get; set; }
public List<MenuItem> Children { get; set; } = new();
}
using Microsoft.EntityFrameworkCore;
public class ApplicationContext : DbContext
{
public DbSet<MenuItem> MenuItems { get; set; } = null!;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlite("Data Source=helloapp.db");
}
}
using (ApplicationContext db = new ApplicationContext())
{
db.Database.EnsureDeleted();
db.Database.EnsureCreated();
MenuItem file = new MenuItem { Title = "File" };
MenuItem edit = new MenuItem { Title = "Edit" };
MenuItem open = new MenuItem { Title = "Open", Parent = file };
MenuItem save = new MenuItem { Title = "Save", Parent = file };
MenuItem copy = new MenuItem { Title = "Copy", Parent = edit };
MenuItem paste = new MenuItem { Title = "Paste", Parent = edit };
db.MenuItems.AddRange(file, edit, open, save, copy, paste);
db.SaveChanges();
}
using (ApplicationContext db = new ApplicationContext())
{
var menuItems = db.MenuItems.ToList();
Console.WriteLine("All Menu:");
foreach (MenuItem m in menuItems)
{
Console.WriteLine(m.Title);
}
Console.WriteLine();
var fileMenu = db.MenuItems.FirstOrDefault(m => m.Title == "File");
if (fileMenu != null)
{
Console.WriteLine(fileMenu.Title);
foreach (var m in fileMenu.Children)
{
Console.WriteLine($"---{m.Title}");
}
}
}
所以看起来问题出在其他地方,所以现在我不会说我正在使用哪个数据库提供商。
带有种子代码的数据库上下文类:
public class RemoteDatabaseContext : DatabaseContext
{
public RemoteDatabaseContext()
{
// base.Database.EnsureDeleted();
base.Database.EnsureCreated();
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder.UseNpgsql("Host=localhost;Port=5432;Username=postgres;Password=pass1234");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.
Entity<TaskCustomFolderModel>().
HasData(TaskCustomFolderModel.FromBusinessRulesEntities(SampleTasksCustomFoldersRepository.TaskCustomFolders));
}
}
public abstract class DatabaseContext : Microsoft.EntityFrameworkCore.DbContext
{
public DbSet<TaskModel> TasksModels { get; internal set; } = null!;
public DbSet<TaskCustomFolderModel> TaskCustomFoldersModels { get; internal set; } = null!;
public DbSet<LocationModel> LocationModels { get; internal set; } = null!;
public DbSet<PersonModel> PeopleModels { get; internal set; } = null!;
}
为什么您通过 Fluent API 设置
IsRequired
,而通过属性设置最大字符数?
简短的回答:由于干净的架构,并且因为
TaskCustomFolderModel
和其他实体框架模型必须由于业务规则的更改而更改,而无需手动代码编辑。
长答案:
根据清洁架构,业务规则不得依赖于任何框架,而
TaskCustomFolderModel
则依赖于任何框架。因此,除了 TaskCustomFolderModel
类之外,还有来自业务规则的类:
public class TaskCustomFolder
{
public required string ID { get; init; }
/* ━━━ Title ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
public required string title { get; set; }
public abstract record Title
{
public const bool IS_REQUIRED = true;
public const byte MINIMAL_CHARACTERS_COUNT = 1;
public const byte MAXIMAL_CHARACTERS_COUNT = Byte.MaxValue;
}
/* ━━━ Relatives ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
/* ─── Parent ───────────────────────────────────────────────────────────────────────────────────────────────────── */
public TaskCustomFolder? parent { get; set; }
public abstract record Parent
{
public const bool IS_REQUIRED = false;
}
/* ─── Children ─────────────────────────────────────────────────────────────────────────────────────────────────── */
public List<TaskCustomFolder> children { get; set; } = [];
public abstract record Children
{
public const bool IS_REQUIRED = false;
}
/* ━━━ Order Among Siblings ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
public required uint orderAmongSiblings { get; set; }
public abstract record OrderAmongSiblings
{
public const bool IS_REQUIRED = true;
public const uint MINIMAL_VALUE = 1;
public const uint MAXIMAL_VALUE = UInt32.MaxValue;
}
/* ━━━ Path ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
public string path
{
get
{
TaskCustomFolder currentDepthLevel = this;
List<string> pathSegments = [ currentDepthLevel.title ];
while (currentDepthLevel.parent is not null)
{
currentDepthLevel = currentDepthLevel.parent;
pathSegments.Add(currentDepthLevel.title);
}
return String.Join("/", pathSegments);
}
}
}
如果我们要更改某些属性的
MAXIMAL_CHARACTERS_COUNT
,则无需编辑TaskCustomFolderModel
,因为它通过属性引用业务规则:
public class TaskCustomFolderModel
{
// ... [System.ComponentModel.DataAnnotations.MaxLength(TaskCustomFolder.Title.MAXIMAL_CHARACTERS_COUNT)]
public string Title { get; set; } = null!;
}
但是,没有像
Required
这样接受参数的 TaskCustomFolder.Children.IS_REQUIRED
属性。因此只能通过流畅的 API(或创建新属性)来完成。
实际上很简单,但是异常消息可能会更有帮助。
异常原因是
builder. Property(taskCustomFolderModel => taskCustomFolderModel.Children). IsRequired(TaskCustomFolder.Children.IS_REQUIRED);
这使得 EF 将
Children
视为另一种类型的引用属性,就像 Parent
一样。但是, List<TaskCustomFolderModel>
不是映射类型。尽管这很明显,但异常消息可能会指出 Required
不能应用于集合属性。
如果您从集合属性中删除
IsRequired
调用,配置将有效。
您可能会反对该属性应该是必需的,但从数据建模的角度来看,这个要求没有意义。无法在基于 SQL 的数据库中强制执行 1:1..n 关系,因此 EF 不提供配置语句来支持它。使集合不为空只是为了应用程序方便。至于存储的数据,空集合和
null
集合之间没有区别。