在使用EF Core作为ORM的MVVM应用程序中,我决定使用手动插入的(文本主键)对表进行建模。
这是因为在这个特定的应用程序中,我宁愿使用有意义的键而不是无意义的整数id,至少对于简单的键值表(例如世界各国的表)而言。
我有类似的东西:
因此,实体为: Id | Description
-----|--------------------------
USA | United States of America
ITA | Italy
etc. etc.
这是我的 View 模型。它仅是ObservableCollection of States的容器。实际上,它是从存储库加载的。这很简单,我在结尾处包含了整个代码。这不是很相关,我也可以只使用DbContext。但是我想显示所有层以查看解决方案所属的位置。哦,是的,然后它包含实际上冒犯了EF Core的同步代码。public class Country
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public string Id { get; set; }
public string Description { get; set; }
}
在我的MainWindow中,我只有: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);
}
}
}
问题与疑问 <Window.DataContext>
<local:CountriesViewModel/>
</Window.DataContext>
<DockPanel>
<DataGrid ItemsSource="{Binding Countries}"/>
</DockPanel>
现在,这不起作用。当我们尝试插入新记录时,在这种情况下,我使用DataGrid的自动功能进行了操作,我得到了:
每次我将新记录添加到ObservableCollection时,我也尝试将其添加回存储库中,这又将其添加到不接受具有null键的实体的EF DbContext上。 System.InvalidOperationException: 'Unable to track an entity of type 'Country'
because primary key property 'Id' is null.'
那么我在这里有什么选择?
一种是将新记录的添加推迟到插入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();
潜在的缺点是:INSERT
命令并从类似的数据库中读取“实际”值标识和其他数据库生成的值,在这种情况下将导致非用户友好的数据库生成的运行时异常。但是EF Core通常不会进行验证,因此不会有太大不同-您必须在相应的层中添加并验证属性必需的规则。关于c# - EF : manual (non autogenerated) keys require ad hoc handling of new entities,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62899114/