c# - EF : manual (non autogenerated) keys require ad hoc handling of new entities

标签 c# wpf entity-framework mvvm entity-framework-core

在使用EF Core作为ORM的MVVM应用程序中,我决定使用手动插入的(文本主键)对表进行建模。
这是因为在这个特定的应用程序中,我宁愿使用有意义的键而不是无意义的整数id,至少对于简单的键值表(例如世界各国的表)而言。
我有类似的东西:

 Id   | Description  
 -----|--------------------------
 USA  | United States of America  
 ITA  | Italy   
 etc. etc.
因此,实体为:
public class Country
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public string Id { get; set; }
    public string Description { get; set; }
}
这是我的 View 模型。它仅是ObservableCollection of States的容器。实际上,它是从存储库加载的。这很简单,我在结尾处包含了整个代码。这不是很相关,我也可以只使用DbContext。但是我想显示所有层以查看解决方案所属的位置。哦,是的,然后它包含实际上冒犯了EF Core的同步代码。
public class CountriesViewModel
{
    //CountryRepository normally would be injected
    public CountryRepository CountryRepository { get; set; } = new CountryRepository(new AppDbContext());
    public ObservableCollection<Country> Countries {get; set;}

    public CountriesViewModel()
    {
        Countries = new ObservableCollection<Country>();
        Countries.CollectionChanged += Countries_CollectionChanged;
    }

    private void Countries_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        foreach (Country c in e.NewItems)
        {
            CountryRepository.Add(c);
        }
    }
}
在我的MainWindow中,我只有:
 <Window.DataContext>
    <local:CountriesViewModel/>
</Window.DataContext>

<DockPanel>
    <DataGrid ItemsSource="{Binding Countries}"/>
</DockPanel>
问题与疑问
现在,这不起作用。当我们尝试插入新记录时,在这种情况下,我使用DataGrid的自动功能进行了操作,我得到了:
System.InvalidOperationException: 'Unable to track an entity of type 'Country'
because primary key property 'Id' is null.'
每次我将新记录添加到ObservableCollection时,我也尝试将其添加回存储库中,这又将其添加到不接受具有null键的实体的EF DbContext上。
那么我在这里有什么选择?
一种是将新记录的添加推迟到插入Id为止。正如我所展示的那样,这并非微不足道,但这不是问题。最糟糕的是,这样我将拥有一些由EF跟踪的记录(更新和删除以及分配了pk的新记录)和一些由 View 模型跟踪的记录(尚未分配键的新记录)。
另一种是使用备用键。我会有一个整数,自动生成的主键,ITA,USA等代码将是备用键,也将在关系中使用。从简单程度来看还不错,但是我想要一个仅应用程序的解决方案。
我在寻找什么
我正在这里寻找一种整洁的解决方案,一种在出现此问题时都可以使用的模式,该模式在MVVM/EF应用程序的上下文中可以很好地发挥作用。
当然,我也可以朝 View 事件的方向看,即迫使用户在触发插入的某个事件之前插入键。我认为它是第二类解决方案,因为它取决于 View 。
剩余代码
为了完整起见,如果您要运行代码,这里是剩余的代码。
DbContext
(为postgres配置)
public class AppDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
    optionsBuilder.UseNpgsql("Host=localhost;Database=WpfApp1;Username=postgres;Password=postgres");
    }
    public DbSet<Country> Countries { get;set; }
}
资料库
之所以为这样一个简单的示例实现存储库,是因为我认为可能的解决方案可能是在存储库中而不是在 View 模型中包括新的无键记录管理。我仍然希望有人提出一个更简单的解决方案。
public class CountryRepository
{
    private AppDbContext AppDbContext { get; set; }
    public CountryRepository(AppDbContext appDbContext) => AppDbContext = appDbContext;
    public IEnumerable<Country> All() => AppDbContext.Countries.ToList();
    public void Add(Country country) => AppDbContext.Add(country);

    //ususally we don't have a save here, it's in a Unit of Work; 
    //for the example purpose it's ok
    public int Save() => AppDbContext.SaveChanges(); 
}

最佳答案

解决EF Core中上述问题的最干净的方法可能是利用临时 value generation on add。为此,您需要一个自定义的ValueGenerator,如下所示:

using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.ValueGeneration;

public class TemporaryStringValueGenerator : ValueGenerator<string>
{
    public override bool GeneratesTemporaryValues => true; // <-- essential
    public override string Next(EntityEntry entry) => Guid.NewGuid().ToString();
}
流利的配置与此类似:
modelBuilder.Entity<Country>().Property(e => e.Id)
    .HasValueGenerator<TemporaryStringValueGenerator>()
    .ValueGeneratedOnAdd();
潜在的缺点是:
  • 在EF Core 3.0之前的版本中,生成的临时值设置在实体实例上,因此将在UI中可见。 EF Core 3.0中已修复此问题,因此现在Temporary key values are no longer set onto entity instances
  • 即使该属性看起来为空(空)并且是必需的(主键/备用键的默认值),如果您不提供显式值,EF Core也会尝试发出INSERT命令并从类似的数据库中读取“实际”值标识和其他数据库生成的值,在这种情况下将导致非用户友好的数据库生成的运行时异常。但是EF Core通常不会进行验证,因此不会有太大不同-您必须在相应的层中添加并验证属性必需的规则。
  • 关于c# - EF : manual (non autogenerated) keys require ad hoc handling of new entities,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62899114/

    相关文章:

    c# - 将参数传递给 .fromsql

    linq - 错误 6046 : Unable to generate function import return type of the store function

    c# - 修改 Entity Framework 的表达式树,尽可能接近 T-SQL translation\execution

    c# - 以编程方式为数据库中的所有对象生成脚本

    c# - WPF:当 DependencyProperty 更改时运行代码

    c# - 制作 DevExpress BarButtonItem 内容两行

    javascript - 如何通过使用 EF 单击从右侧到左侧更改数据

    c# - 如何实现 Struct 用作​​字典键

    c# - 如何合并两个 JObject?

    wpf - 无法对 DependencyProperty 进行数据绑定(bind)