I have a number of different projects that all implement the same schema for configuration, security and audit and are looking for a pattern that would allow me to put these schema definitions in an abstract classes (entity, configuration and dbcontext) that can be extended in the concrete implementations if needed. My current POC fails when the base configurations is applied. I get:
A key cannot be configured on 'UserRole' because it is a derived type. The key must be configured on the root type.
Any help / pointers will be greatly appreciated!
I have the following code samples....
Abstract base classes
RoleBase
public abstract class RoleBase
{
public RoleBase()
{
this.UserRoles = new List<UserRoles>();
}
public long Id { get; set; }
public string Name { get; set; }
public virtual IEnumerable<UserRoleBase> UserRoles { get; set; }
}
UserBase
public abstract class UserBase
{
public long Id { get; set; }
public string Username { get; set; }
public string Email { get; set; }
public virtual ICollection<UserRoleBase> UserRoles { get; set; }
}
UserRoleBase
public abstract class UserRoleBase
{
public long Id { get; set; }
public long RoleId { get; set; }
public long UserId { get; set; }
public bool Deleted { get; set; }
public virtual RoleBase Role { get; set; }
public virtual UserBase User { get; set; }
}
Each of these have an abstract configuration class for the base class...
RoleBase Configuration
public abstract class RoleConfiguration<T> : IEntityTypeConfiguration<T>
where T : RoleBase
{
public virtual void Configure(EntityTypeBuilder<T> builder)
{
// Primary Key
builder.HasKey(t => t.Id);
// Properties
builder.Property(t => t.Name)
.IsRequired()
.HasMaxLength(50);
// Table & Column Mappings
builder.ToTable("Role", "Security");
builder.Property(t => t.Id).HasColumnName("Id");
builder.Property(t => t.Name).HasColumnName("Name");
}
}
UserBase Configuration
public abstract class UserConfiguration<TBase> : IEntityTypeConfiguration<TBase>
where TBase : UserBase
{
public virtual void Configure(EntityTypeBuilder<TBase> builder)
{
// Primary Key
builder.HasKey(t => t.Id);
// Properties
builder.Property(t => t.Username).IsRequired().HasMaxLength(255);
builder.Property(t => t.Email).IsRequired().HasMaxLength(255);
// Table & Column Mappings
builder.ToTable("User", "Security");
builder.Property(t => t.Id).HasColumnName("Id");
builder.Property(t => t.Username).HasColumnName("Username");
builder.Property(t => t.Email).HasColumnName("Email");
}
}
UserRoleBase Configuration
public abstract class UserRoleConfiguration<T> : IEntityTypeConfiguration<T>
where T : UserRoleBase
{
public virtual void Configure(EntityTypeBuilder<T> builder)
{
// Primary Key
builder.HasKey(t => t.Id);
// Properties
builder.Property(t => t.RoleId).IsRequired();
builder.Property(t => t.UserId).IsRequired();
builder.Property(t => t.Deleted).IsRequired();
// Table & Column Mappings
builder.ToTable("UserRole", "Security");
builder.Property(t => t.Id).HasColumnName("Id");
builder.Property(t => t.RoleId).HasColumnName("RoleId");
builder.Property(t => t.UserId).HasColumnName("UserId");
builder.Property(t => t.Deleted).HasColumnName("Deleted");
// Relationships
builder.HasOne(t => t.Role)
.WithMany(t => (ICollection<TBase>)t.UserRoles)
.HasForeignKey(d => d.RoleId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(t => t.UserDetail)
.WithMany(t => (ICollection<TBase>)t.UserRoles)
.HasForeignKey(d => d.UserDetailId)
.OnDelete(DeleteBehavior.Restrict);
}
And a concrete implementation of the base classes:
Role
public class Role : RoleBase
{
}
User
public class User : UserBase
{
// Extension properties
public string FirstName { get; set; }
public string LastName { get; set; }
public string Phone { get; set; }
public string Mobile { get; set; }
}
UserRole
public class UserRole : UserRoleBase
{
}
And a concrete implementation of the configuration
RoleConfiguration
public class RoleConfiguration : Base.Configurations.RoleConfiguration<Role>
{
public override void Configure(EntityTypeBuilder<Role> builder)
{
base.Configure(builder);
this.ConfigureEntity(builder);
}
private void ConfigureEntity(EntityTypeBuilder<Role> builder)
{
}
}
UserConfiguration
public class UserConfiguration : Base.Configurations.UserConfiguration<User>
{
public override void Configure(EntityTypeBuilder<User> builder)
{
base.Configure(builder);
this.ConfigureEntity(builder);
}
private void ConfigureEntity(EntityTypeBuilder<User> builder)
{
//Registration of extension properties
builder.Property(t => t.FirstName).HasColumnName("FirstName");
builder.Property(t => t.LastName).HasColumnName("LastName");
builder.Property(t => t.Phone).HasColumnName("Phone");
builder.Property(t => t.Mobile).HasColumnName("Mobile");
}
}
UserRoleConfiguration
public class UserRoleConfiguration : Base.Configurations.UserRoleConfiguration<UserRole>
{
public override void Configure(EntityTypeBuilder<UserRole> builder)
{
base.Configure(builder);
this.ConfigureEntity(builder);
}
private void ConfigureEntity(EntityTypeBuilder<UserRole> builder)
{
}
}
And the base context
public abstract class BaseDbContext: DbContext
{
public BaseDbContext(DbContextOptions<BaseDbContext> options)
: base(options)
{
}
// https://github.com/aspnet/EntityFramework.Docs/issues/594
protected BaseDbContext(DbContextOptions options)
: base(options)
{
}
public DbSet<RoleBase> Roles { get; set; }
public DbSet<UserBase> Users { get; set; }
public DbSet<UserRoleBase> UserRoles { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
}
}
And the concrete context
public class MyDbContext: BaseDbContext
{
public MyDbContext(DbContextOptions<MyDbContext> options)
:base(options)
{
}
protected MyDbContext(DbContextOptions options)
: base(options)
{
}
public new DbSet<Role> Roles { get; set; }
public new DbSet<User> Users { get; set; }
public new DbSet<UserRole> UserRoles { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new RoleConfiguration());
modelBuilder.ApplyConfiguration(new UserConfiguration());
modelBuilder.ApplyConfiguration(new UserRoleConfiguration());
base.OnModelCreating(modelBuilder);
}
}
So all this works for items that does not have navigation properties and migrates to the database fine as long as there is no navigation properties. I can see the extension properties on User being done as long as I comment out all the navigation properties.
With the navigation properties present, I get an error on the base configuration class. after concrete implementation called base.Configure(builder);
I get the following error message on builder.HasKey(t => t.Id); and for the above sample code it would be on...
public abstract class UserRoleConfiguration<T> : IEntityTypeConfiguration<T>
where T : UserRoleBase
{
public virtual void Configure(EntityTypeBuilder<T> builder)
{
// Primary Key
builder.HasKey(t => t.Id);
System.InvalidOperationException: 'A key cannot be configured on 'UserRole' because it is a derived type. The key must be configured on the root type 'UserRoleBase'. If you did not intend for 'UserRoleBase' to be included in the model, ensure that it is not included in a DbSet property on your context, referenced in a configuration call to ModelBuilder, or referenced from a navigation property on a type that is included in the model.'
Is there a way in which I can keep these relational configuration in the abstract base class so that I would not need to replicate it in each concrete implementation of the base classes? Or is there a different approach that can be followed to overcome this issue?