C# 使用代码优先和DDD在实体框架中模拟枚举列表

C# 使用代码优先和DDD在实体框架中模拟枚举列表,c#,entity-framework,domain-driven-design,C#,Entity Framework,Domain Driven Design,我正试图实现一个枚举列表,该列表基于。我的目标是能够在我的域中使用枚举,并在从数据库保存和检索枚举时将其转换为类实例 按原样使用代码(下面的源代码),我得到了一条消息: 违反主键约束“PK_dbo.Faculty”。无法在对象“dbo.Faculty”中插入重复键。重复的键值为(0)。 声明已终止 这是意料之中的,因为我正在更新每一个教员的实例 为了解决这个问题,我试着从一个 , 没有成功。他们建议要么附加实体,要么将其状态设置为未更改。因此,我尝试重写SaveChanges()并使用: Cha

我正试图实现一个枚举列表,该列表基于。我的目标是能够在我的域中使用枚举,并在从数据库保存和检索枚举时将其转换为类实例

按原样使用代码(下面的源代码),我得到了一条消息:

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

这是意料之中的,因为我正在更新每一个教员的实例

为了解决这个问题,我试着从一个 , 没有成功。他们建议要么附加实体,要么将其状态设置为未更改。因此,我尝试重写
SaveChanges()
并使用:

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

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

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

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

枚举列表:77642毫秒
正常等级:70619毫秒
如您所见,使用此方法会导致大约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));
}
ChangeTracker.Entries<Department>().ToList().ForEach(department =>
{
    foreach (var faculty in department.Entity.Faculties)
    {
        Entry(faculty).State = EntityState.Unchanged;
    }
});
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));
}
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