38

How would one turn the enums used in an EF Core database context into lookup tables and add the relevant foreign keys?


Tim Abell
  • 11,186
  • 8
  • 79
  • 110
  • Why not use this answer which will work with EF Core perfectly without any issue => https://stackoverflow.com/a/15065439/797882 – CodeNotFound May 16 '18 at 16:17

3 Answers3

62

You can use an enum in your code and have a lookup table in your db by using a combination of these two EF Core features:


Here below a data model example:

public class Wine
{
    public int WineId { get; set; }
    public string Name { get; set; }

    public WineVariantId WineVariantId { get; set; }
    public WineVariant WineVariant { get; set; }
}

public enum WineVariantId : int
{
    Red = 0,
    White = 1,
    Rose = 2
}

public class WineVariant
{
    public WineVariantId WineVariantId { get; set; }
    public string Name { get; set; }

    public List<Wine> Wines { get; set; }
}

Here the DbContext where you configure value conversions and data seeding:

public class WineContext : DbContext
{
    public DbSet<Wine> Wines { get; set; }
    public DbSet<WineVariant> WineVariants { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlite("Data Source=wines.db");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder
            .Entity<Wine>()
            .Property(e => e.WineVariantId)
            .HasConversion<int>();

        modelBuilder
            .Entity<WineVariant>()
            .Property(e => e.WineVariantId)
            .HasConversion<int>();

        modelBuilder
            .Entity<WineVariant>().HasData(
                Enum.GetValues(typeof(WineVariantId))
                    .Cast<WineVariantId>()
                    .Select(e => new WineVariant()
                    {
                        WineVariantId = e,
                        Name = e.ToString()
                    })
            );
    }
}

Then you can use the enum values in your code as follow:

db.Wines.Add(new Wine
{
    Name = "Gutturnio",
    WineVariantId = WineVariantId.Red,
});

db.Wines.Add(new Wine
{
    Name = "Ortrugo",
    WineVariantId = WineVariantId.White,
});

Here is what your db will contain:

WineVariants table

Wines table

I published the complete example as a gist: https://gist.github.com/paolofulgoni/825bef5cd6cd92c4f9bbf33f603af4ff

Paolo Fulgoni
  • 5,208
  • 3
  • 39
  • 55
  • 4
    How do you account for changes in the Enum in future migrations? Say you were to insert a record into the enum position 3 (currently rose) so that wine.rose = 4 and wine.blend = 3. The Migration created updates the column so all my rose wines now are blends and rose is added at the end. :( – Itanex Nov 07 '19 at 23:04
  • 5
    @Itanex if you change ID of the elements on purpose, I think it's fair to manually change the migration. If you want to avoid accidental changes, then using explicit values in the enum may help, I'm going to update the example accordingly – Paolo Fulgoni Nov 08 '19 at 22:22
  • Thanks for the tip. I was leaning this way. Just hoped there was a better way. – Itanex Nov 08 '19 at 22:53
  • @PaoloFulgoni, In database first, How can we do that? By running scaffolding, the dbContext file will be overwritten. – Mohammad Taherian Jun 09 '20 at 17:26
  • @MohammadTaherian I'm sorry but I don't have any idea regarding the database-first scenario – Paolo Fulgoni Jun 11 '20 at 13:39
  • @PaoloFulgoni I liked the way how you suffixed the enum class with `Id`. An enum is nothing but a type of identifier. – Kahan Bhalani May 11 '23 at 11:32
6

Here is another example :

public class Weather {
        public int Id { get; init; }
        public WeatherType Type { get; init; }
    }

public enum WeatherType {
        Cloudy = 1,
        Sunny = 2,
        Rainy = 3,
    }

And you can add HasConversion in a seperate class like this :

public class WeatherEntityTypeConfiguration : IEntityTypeConfiguration<Weather>
    {
        public void Configure(EntityTypeBuilder<Weather> builder)
        {
            builder.ToTable("Weather").HasKey(k => k.Id);

            builder.Property(p => p.Id).IsRequired();
            builder.Property(p => p.Type).HasConversion<int>().IsRequired(); 
            // builder.Property(p => p.Type).HasConversion<string>().IsRequired();
          
        }
    }

Note : If you use HasConversion<int>() data would be stored in database as an integer but if you use HasConversion<string>() data would be stored as string (in this example : Cloudy, Sunny or Rainy )

Kaveh Naseri
  • 1,102
  • 2
  • 15
  • 24
  • I like this solution. The weather only has the property for the enum, it doesn't need to know if there is and ID or not behind the enumeration. This is configured in entity core with has conversion. – Álvaro García Jun 16 '23 at 14:26
0

In addition to @PaoloFulgoni, here is how you'd do it if you want a many-to-many relationship with enums i.e. you want many user roles or wine variants and work with enums moreover, you can't store it as a flag because you need to know about the roles/privileges without source code(on db side).
TLDR ;) You'd have to create a join table which contains about about who has what privilege(or roles if you want).
There is a Users table which has a list of privileges, a privilege table which has privilege definition i.e. Id, name. And a Join table which will have User and Privilege as it's key. If an entry against this user/privilege combination is present that means this user has this privilege/role.

The code:

//for enum
public enum UserPrivilegeId : int
{
    AddProject = 0,
    ModifyProject = 1,
    DeleteProject = 2,
    AddUser = 3,
    ModifyUser = 4,
    DeleteUser = 5
}

//User class
public record User
{
    public User()
    {
        Privileges = new HashSet<Privilege>();
    }

    public int Id { get; set; }
    public string Username { get; set; }
    public string PasswordHash { get; set; }

    public virtual ICollection<Privilege> Privileges { get; set; }
    public virtual List<UserPrivilege> UserPrivileges { get; set; }
}

//Privilege Class
public record Privilege //note record is IMPORTANT here, because this forces it to compare by value, if you want to *use a class*, then make sure to override GetHashCode and Equals
{
    public Privilege()
    {
        Users = new HashSet<User>();
    }

    public Privilege(UserPrivilegeId privilegeId, string privilegeName)
    {
        PrivilegeId = privilegeId;
        PrivilegeName = privilegeName;
        Users = new HashSet<User>();
    }

    [Key]
    public UserPrivilegeId PrivilegeId { get; set; }
    public string PrivilegeName { get; set; }
    public virtual ICollection<User> Users { get; set; }
    public virtual List<UserPrivilege> UserPrivileges { get; set; } 
}

//and finally the UserPrivilege join class
public record UserPrivilege
{
    public UserPrivilegeId PrivilageId { get; set; }
    public Privilege Privilage { get; set; }

    public int UserId { get; set; }
    public User User { get; set; }
}

//The set-up in dbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Privilege>()
                .HasKey(p => p.PrivilegeId);

    modelBuilder.Entity<Privilege>()
                .Property(p => p.PrivilegeId)
                .HasConversion<int>();


    modelBuilder.Entity<User>()
                .HasMany(user => user.Privileges)
                .WithMany(privilege => privilege.Users)
                .UsingEntity<UserPrivilege>(
                    j => j
                            .HasOne(up => up.Privilage)
                            .WithMany(u => u.UserPrivileges)
                            .HasForeignKey(up => up.PrivilageId),
                    j => j
                            .HasOne(up => up.User)
                            .WithMany(p => p.UserPrivileges)
                            .HasForeignKey(up => up.UserId),
                    j =>
                    {
                        j.Property(u => u.PrivilageId).HasConversion<int>();
                        j.HasKey(u => new { u.PrivilageId, u.UserId });
                    });
    //this adds definitions of privileges to the table
    modelBuilder.Entity<Privilege>()
                .HasData(
                    Enum.GetValues(typeof(UserPrivilegeId))
                    .Cast<UserPrivilegeId>()
                    .Select(p => new Privilege(p, p.ToString())));

    base.OnModelCreating(modelBuilder);
}

Use it by creating a wrapper around it with a boolean on IsActive like this:

public class UserPrivelegesDTO
{
    public UserPrivelegesDTO(UserPrivilegeId privilege, bool isActive)
    {
        this.PrivilegeId = privilege;
        this.PrivilegeName = privilege.ToString();
        this.IsActive = isActive;
    }

    public UserPrivilegeId PrivilegeId { get; set; }
    public string PrivilegeName { get; set; }
    public bool IsActive { get; set; }
}

If you want to convert from List<Privileges> to List<UserPrivilegeDTO>, you can

return await _context.Privileges.OrderBy(x => x.PrivilegeId).ToListAsync(cancellationToken);

To Convert back to List<Privileges>, simply

var privileges = _userPrivilegesViewModel.Privileges.Where(x => x.IsActive).Select(x => new Privilege(x.PrivilegeId, x.PrivilegeName));

If you want to check if the user has privilege

var user = _context.Users.Include(x => x.Privileges).FirstAsync(x => x.Id == 1);
if (request.Editor.Privileges.Any(p => p.PrivilegeId == UserPrivilegeId.ModifyUser))
    return true;

When you want to update privileges

var PrivilegeChangeUser = await 
    _context.Users
            .Include(user => user.Privileges)
            .Include(user => user.UserPrivileges)
            .FirstOrDefaultAsync(user => user.Id == request.UserId);
//**NOTE**you *need* to include the join table i.e. UserPrivileges in order replace the privileges, if you do not include it EF will try to add the privileges which already exist :(

//To update the privileges from an IEnumerable<UserPrivilegeIdEnum>
//first get the privileges objects and add that to users
var AllPrivileges = 
            await _context.Privileges
                .Include(x => x.UserPrivileges)
                .Include(x => x.Users)
                .Where(x => 
                    request.Privileges
                    .Contains(x.PrivilegeId)
            ).ToListAsync(cancellationToken);

PrivilegeChangeUser.Privileges = AllPrivileges;
AMunim
  • 992
  • 6
  • 13