【问题标题】:Mimicking an Enum List in Entity Framework using code first and DDD使用代码优先和 DDD 在实体框架中模拟枚举列表
【发布时间】:2017-11-10 12:46:36
【问题描述】:

我正在尝试根据this answer 的想法实现一个枚举列表。我的目标是能够在我的域中使用枚举,并在从数据库中保存和检索它时将其转换为类实例。

按原样使用代码(来源如下),我得到一个带有消息的DbUpdateException

违反主键约束“PK_dbo.Faculty”。无法在对象“dbo.Faculty”中插入重复键。重复键值为 (0)。 声明已终止。

这是意料之中的,因为我正在更新 Faculty 的每个实例。

为了解决这个问题,我尝试了解决方案 few questions on this, 没有成功。他们建议附加实体或将其状态设置为未更改。所以我尝试覆盖SaveChanges() 并使用:

ChangeTracker.Entries<Faculty>().ToList().ForEach(x => x.State = EntityState.Unchanged);

ChangeTracker.Entries<Faculty>().ToList()
    .ForEach(x => Entry(x.Entity).State = EntityState.Unchanged);

甚至

ChangeTracker.Entries<Department>().ToList().ForEach(department =>
{
    foreach (var faculty in department.Entity.Faculties)
    {
        Entry(faculty).State = EntityState.Unchanged;
    }
});

但是他们都抛出了InvalidOperationException 的消息:

附加信息:保存或接受更改失败,因为多个“TestEnum.Entities.Faculty”类型的实体具有相同的主键值。确保显式设置的主键值是唯一的。确保在数据库和实体框架模型中正确配置了数据库生成的主键。使用实体设计器进行数据库优先/模型优先配置。使用“HasDatabaseGeneratedOption”流式 API 或“DatabaseGeneratedAttribute”进行 Code First 配置。

如何指示 EF 不要尝试将它们插入数据库?我需要此实现在 SaveChanges() 内工作,因为我遵循 DDD 设计规则并将基础架构与域逻辑分开。

代码如下:

class Program
{
    static void Main(string[] args)
    {
        using (var dbContext = new MyContext())
        {
            var example = new Department();
            example.AddFaculty(FacultyEnum.Eng);
            example.AddFaculty(FacultyEnum.Math);

            dbContext.Department.Add(example);

            var example2 = new Department();
            example2.AddFaculty(FacultyEnum.Math);

            dbContext.Department.Add(example2);

            dbContext.SaveChanges();

            var exampleFromDb1 = dbContext.Department.Find(1);
            var exampleFromDb2 = dbContext.Department.Find(2);
        }
    }
}

public enum FacultyEnum
{
    [Description("English")]
    Eng,
    [Description("Mathematics")]
    Math,
    [Description("Economy")]
    Eco,
}

public class Department
{
    public int Id { get; set; }
    public virtual ICollection<Faculty> Faculties { get; set; }

    public Department()
    {
        Faculties = new List<Faculty>();
    }

    public void AddFaculty(FacultyEnum faculty)
    {
        Faculties.Add(faculty);
    }
}

public class Faculty
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }

    private Faculty(FacultyEnum @enum)
    {
        Id = (int)@enum;
        Name = @enum.ToString();
        Description = @enum.GetEnumDescription();
    }

    protected Faculty() { } //For EF

    public static implicit operator Faculty(FacultyEnum @enum) => new Faculty(@enum);

    public static implicit operator FacultyEnum(Faculty faculty) => (FacultyEnum)faculty.Id;
}

public class MyContext : DbContext
{
    public DbSet<Department> Department { get; set; }
    public DbSet<Faculty> Faculty { get; set; }

    public MyContext()
        : base(nameOrConnectionString: GetConnectionString())
    {
        Database.SetInitializer(new MyDbInitializer());
    }

    public int SaveSeed()
    {
        return base.SaveChanges();
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
        modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
        modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>();

        modelBuilder.Properties<string>()
            .Configure(p => p.HasMaxLength(100));

        modelBuilder.Configurations.Add(new DepartmentConfiguration());
        modelBuilder.Configurations.Add(new FacultyConfiguration());

        base.OnModelCreating(modelBuilder);
    }

    private static string GetConnectionString()
    {
        return @"Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=TestEnum;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=True;ApplicationIntent=ReadWrite;MultiSubnetFailover=False;MultipleActiveResultSets=true;";
    }
}

public class MyDbInitializer : DropCreateDatabaseIfModelChanges<MyContext>
{
    protected override void Seed(MyContext context)
    {
        context.Faculty.SeedEnumValues<Faculty, FacultyEnum>(@enum => @enum);
        context.SaveSeed();
    }
}

public class DepartmentConfiguration : EntityTypeConfiguration<Department>
{
    public DepartmentConfiguration()
    {
        HasMany(x => x.Faculties)
            .WithMany();
    }
}

public class FacultyConfiguration : EntityTypeConfiguration<Faculty>
{
    public FacultyConfiguration()
    {
        Property(x => x.Id)
            .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
    }
}

public static class Extensions
{
    public static string GetEnumDescription<TEnum>(this TEnum item)
        => item.GetType()
               .GetField(item.ToString())
               .GetCustomAttributes(typeof(DescriptionAttribute), false)
               .Cast<DescriptionAttribute>()
               .FirstOrDefault()?.Description ?? string.Empty;

    public static void SeedEnumValues<T, TEnum>(this IDbSet<T> dbSet, Func<TEnum, T> converter)
        where T : class => Enum.GetValues(typeof(TEnum))
                               .Cast<object>()
                               .Select(value => converter((TEnum)value))
                               .ToList()
                               .ForEach(instance => dbSet.AddOrUpdate(instance));
}

【问题讨论】:

    标签: c# entity-framework domain-driven-design


    【解决方案1】:

    当我们使用一个类的新实例时,EF 总是会尝试将它插入到数据库中。解决此问题的一种方法是“撤消”存储在 ChangeTracker 中的内容,因此它只使用从数据库加载的实体。

    所以我所做的是覆盖SaveChanges() 方法,循环遍历ChangeTracker 中的每个Department,获取每个Ids 的教师列表,清除所有Faculty 对象的ChangeTracker 条目和然后将它们再次添加到他们的部门,但现在使用在 ChangeTracker 或通过 Find() 找到的实体。

    它看起来有点低效,所以我运行了一个测试,一个使用这种方法,另一个在每次运行时从 Db 加载每个 Faculty。我跑了 10000 次,结果是平均跑了 3 次:

    Enum list:    77642 ms
    Normal class: 70619 ms
    

    如您所见,使用此方法会导致大约 10% 的速度损失,因此您可以自行决定应用程序的成本/收益。对我来说,增加的表现力弥补了成本,因为我的应用程序不会接近那么多操作。

    除了MyContext,还有一些其他的类在原帖的基础上发生了变化,我还扩展了控制台测试以涵盖所有用例,所以我在下面发布了完整的解决方案。

    class Program
    {
        static void Main(string[] args)
        {
            var id = 0;
    
            using (var dbContext = new MyContext())
            {
                var department = new Department();
                department.AddFaculty(FacultyEnum.Eng);
                department.AddFaculty(FacultyEnum.Math);
                dbContext.Department.Add(department);
    
                var department2 = new Department();
                department2.AddFaculty(FacultyEnum.Math);
                dbContext.Department.Add(department2);
    
                dbContext.SaveChanges();
                id = department.Id;
            }
    
            using (var dbContext = new MyContext())
            {
                var department = dbContext.Department.Find(id);
                department.AddFaculty(FacultyEnum.Eco);
                dbContext.SaveChanges();
            }
    
            using (var dbContext = new MyContext())
            {
                var department = dbContext.Department.Find(id);
                var faculty = department.Faculties.Where(x => x.Id == (int)FacultyEnum.Eng).FirstOrDefault();
                department.Faculties.Remove(faculty);
                dbContext.SaveChanges();
            }
    
            using (var dbContext = new MyContext())
            {
                var department = dbContext.Department.Find(id);
                Console.WriteLine($"Department Id {department.Id} has these faculties:");
                foreach (var faculty in department.Faculties)
                {
                    Console.WriteLine($"- {faculty.Id}");
                }
            }
    
            Console.ReadKey();
        }
    }
    
    public class MyContext : DbContext
    {
        public DbSet<Department> Department { get; set; }
        public DbSet<Faculty> Faculty { get; set; }
    
        public MyContext()
            : base(nameOrConnectionString: GetConnectionString())
        {
            Database.SetInitializer(new MyDbInitializer());
        }
    
        public override int SaveChanges()
        {
            CleanUpFaculties();
    
            return base.SaveChanges();
        }
    
        private void CleanUpFaculties()
        {
            var departments = ChangeTracker
                .Entries<Department>()
                .Select(x => x.Entity)
                .ToList();
    
            var cachedDataToReload = departments
                .Select(department => new
                {
                    Department = department,
                    FacultyIds = department.Faculties.Select(faculty => faculty.Id).ToList(),
                })
                .ToList();
    
            CleanUpFacultiesOnChangeTracker();
    
            foreach (var item in cachedDataToReload)
            {
                var faculties = LoadFacultiesFromDb(item.FacultyIds);
    
                typeof(Department).GetProperty("Faculties")
                    .SetValue(item.Department, faculties);
            }
        }
    
        private void CleanUpFacultiesOnChangeTracker()
        {
            var changedEntries = ChangeTracker.Entries<Faculty>().Where(x => x.State != EntityState.Unchanged).ToList();
    
            foreach (var entry in changedEntries)
            {
                switch (entry.State)
                {
                    case EntityState.Modified:
                        entry.CurrentValues.SetValues(entry.OriginalValues);
                        entry.State = EntityState.Unchanged;
                        break;
                    case EntityState.Added:
                        entry.State = EntityState.Detached;
                        break;
                    case EntityState.Deleted:
                        entry.State = EntityState.Unchanged;
                        break;
                }
            }
        }
    
        private ICollection<Faculty> LoadFacultiesFromDb(IEnumerable<FacultyEnum> facultyIds)
        {
            var destination = new List<Faculty>();
    
            foreach (var id in facultyIds)
            {
                var newFaculty = ChangeTracker
                    .Entries<Faculty>()
                    .Where(x => x.State == EntityState.Unchanged && x.Entity.Id == id)
                    .FirstOrDefault()
                    ?.Entity;
    
                if (newFaculty == null)
                {
                    newFaculty = Set<Faculty>().Find(id) ?? id;
                }
    
                destination.Add(newFaculty);
            }
    
            return destination;
        }
    
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
            modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
            modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>();
    
            modelBuilder.Properties<string>()
                .Configure(p => p.HasMaxLength(100));
    
            modelBuilder.Configurations.Add(new DepartmentConfiguration());
            modelBuilder.Configurations.Add(new FacultyConfiguration());
    
            base.OnModelCreating(modelBuilder);
        }
    
        private static string GetConnectionString()
        {
            return @"Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=TestEnum;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=True;ApplicationIntent=ReadWrite;MultiSubnetFailover=False;MultipleActiveResultSets=true;";
        }
    }
    
    public class MyDbInitializer : DropCreateDatabaseIfModelChanges<MyContext>
    {
        protected override void Seed(MyContext context)
        {
            context.Faculty.SeedEnumValues<Faculty, FacultyEnum>(theEnum => theEnum);
            context.SaveChanges();
    
            base.Seed(context);
        }
    }
    
    public class DepartmentConfiguration : EntityTypeConfiguration<Department>
    {
        public DepartmentConfiguration()
        {
            HasMany(x => x.Faculties)
                .WithMany();
        }
    }
    
    public class FacultyConfiguration : EntityTypeConfiguration<Faculty>
    {
        public FacultyConfiguration()
        {
            Property(x => x.Id)
                .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
        }
    }
    
    public class Department
    {
        public int Id { get; private set; }
        public virtual ICollection<Faculty> Faculties { get; private set; }
    
        public Department()
        {
            Faculties = new List<Faculty>();
        }
    
        public void AddFaculty(FacultyEnum faculty)
        {
            Faculties.Add(faculty);
        }
    }
    
    public class Faculty
    {
        public FacultyEnum Id { get; private set; }
        public string Name { get; private set; }
        public string Description { get; private set; }
    
        private Faculty(FacultyEnum theEnum)
        {
            Id = theEnum;
            Name = theEnum.ToString();
            Description = theEnum.Description();
        }
    
        protected Faculty() { } //For EF
    
        public static implicit operator Faculty(FacultyEnum theEnum) => new Faculty(theEnum);
    
        public static implicit operator FacultyEnum(Faculty faculty) => faculty.Id;
    }
    
    public enum FacultyEnum
    {
        [Description("English")]
        Eng,
        [Description("Mathematics")]
        Math,
        [Description("Economy")]
        Eco,
    }
    
    public static class Extensions
    {
        public static string Description<TEnum>(this TEnum item)
            => item.GetType()
                   .GetField(item.ToString())
                   .GetCustomAttributes(typeof(DescriptionAttribute), false)
                   .Cast<DescriptionAttribute>()
                   .FirstOrDefault()?.Description ?? string.Empty;
    
        public static void SeedEnumValues<T, TEnum>(this IDbSet<T> dbSet, Func<TEnum, T> converter)
            where T : class => Enum.GetValues(typeof(TEnum))
                                   .Cast<object>()
                                   .Select(value => converter((TEnum)value))
                                   .ToList()
                                   .ForEach(instance => dbSet.AddOrUpdate(instance));
    }
    

    输出将是:

    // Department Id 1 has these faculties: 
    // - Math
    // - Eco
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-12-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多