Warning: file_get_contents(/data/phpspider/zhask/data//catemap/2/csharp/306.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
C# EF:手动(非自动生成)键需要特别处理新实体_C#_Wpf_Entity Framework_Mvvm_Entity Framework Core - Fatal编程技术网

C# EF:手动(非自动生成)键需要特别处理新实体

C# EF:手动(非自动生成)键需要特别处理新实体,c#,wpf,entity-framework,mvvm,entity-framework-core,C#,Wpf,Entity Framework,Mvvm,Entity Framework Core,在一个以EF核心作为ORM的MVVM应用程序中,我决定使用手动插入的文本主键对一个表进行建模 这是因为在这个特定的应用程序中,我宁愿使用有意义的键而不是无意义的整数ID,至少对于简单的键值表,如世界各国表。 我有点像: Id | Description -----|-------------------------- USA | United States of America ITA | Italy etc. etc. 因此,该实体是: public clas

在一个以EF核心作为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; }
}
这是我的viewmodel。它只不过是一个可以观察到的收集国家的容器。实际上,它是从存储库加载的。这很简单,我在最后包含了整个代码。这并不真正相关,我也可以只使用DbContext。但我想显示所有层,以查看解决方案属于何处。哦,是的,那么它包含的同步代码实际上违反了EF核心

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);
        }
    }
}
每次我向ObservableCollection添加新记录时,我也会尝试将其添加回存储库,而存储库又会将其添加到EF DbContext中,EF DbContext不接受具有null键的实体。

那么我的选择是什么?
一种是推迟添加新记录,直到插入Id为止。这并不像我所展示的集合处理那样微不足道,但这不是问题所在。最糟糕的是,通过这种方式,我会有一些记录被EF跟踪(更新、删除和分配了pk的新记录),还有一些记录被视图模型跟踪(尚未分配密钥的新记录)

另一种是使用备用键;我将有一个整数,自动生成的主键和ITA,USA等代码将是一个备用键,也将用于关系。从简单性来看,这并不是那么糟糕,但我想要一个只适用于应用程序的解决方案

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(); 
}
我在找什么 我在这里寻找一个简洁的解决方案,一个在出现此问题时使用的模式,它在MVVM/EF应用程序的上下文中运行良好。
当然,我也可以查看查看事件的方向,即强制用户在触发插入的特定事件之前插入键。我认为它是一种二级解决方案,因为它是依赖于视图的。 剩余代码 为了完整起见,如果您想运行代码,下面是剩余的代码

DbContext (为postgres配置)

公共类AppDbContext:DbContext
{
配置时受保护的覆盖无效(DBContextOptions Builder Options Builder)
{
optionsBuilder.UseNpgsql(“主机=localhost;数据库=WpfApp1;用户名=postgres;密码=postgres”);
}
公共数据库集国家{get;set;}
}
存储库 我之所以为这样一个简单的示例实现存储库,是因为我认为一个可能的解决方案可能是在存储库中而不是在viewmodel中包含新的无关键记录管理。我仍然希望有人能拿出一个更简单的解决方案

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(); 
}
公共类存储库
{
私有AppDbContext AppDbContext{get;set;}
公共CountryRepository(AppDbContext AppDbContext)=>AppDbContext=AppDbContext;
public IEnumerable All()=>AppDbContext.Countries.ToList();
public void Add(国家/地区)=>AppDbContext.Add(国家/地区);
//通常我们这里没有储蓄,而是在一个单位的工作中;
//举个例子,没关系
public int Save()=>AppDbContext.SaveChanges();
}

在EF Core中解决上述问题最干净的方法可能是利用临时。要做到这一点,您需要这样的定制:

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();
}
使用Microsoft.EntityFrameworkCore.ChangeTracking;
使用Microsoft.EntityFrameworkCore.ValueGeneration;
公共类临时StringValueGenerator:ValueGenerator
{
public override bool GeneratesTemporaryValues=>true;//Guid.NewGuid().ToString();
}
与此类似的配置:

modelBuilder.Entity<Country>().Property(e => e.Id)
    .HasValueGenerator<TemporaryStringValueGenerator>()
    .ValueGeneratedOnAdd();
modelBuilder.Entity().Property(e=>e.Id)
.HasValueGenerator()
.ValueGeneratedOnAdd();
潜在的缺点是:

  • 在pre-EF Core 3.0中,生成的临时值设置在实体实例上,因此在UI中可见。这在EF Core 3.0中已经修复,所以现在

  • 即使属性看起来是空的(null)并且是必需的(主键/备用键的默认值),如果您不提供显式值,EF Core将尝试发出
    INSERT
    命令,并从数据库中读取“实际”值,类似于identity和其他数据库生成的值,在这种情况下,这将导致非用户友好的数据库生成的运行时异常。但EF Core通常不进行验证,所以这不会有太大的不同-您必须在相应的层中添加并验证属性必需规则


为什么要使用
[数据库生成(DatabaseGeneratedOption.None)]
?另外,您应该有一个
CountryViewModel
,就像您有一个
CountriesViewModel
一样。为了简单起见,我排除了CountryViewModel,因为它不会给示例增加太多内容。DatabaseGenerated(DatabaseGeneratedAction.None)是因为我手动插入了密钥,问题在于这些。这将是一个很好的解决方案。我迫不及待地想尝试一下。不幸的是,我使用的是EF Core v 2.2.4,我必须迁移它,这反过来需要一些测试。。。
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();