3

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?

Kiewiet
  • 85
  • 1
  • 5

1 Answers1

1

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.'

From the error, you could use Key attribute on the id of the base model to specify the primary key .

From breaking changes included in EF Core 3.0 , ToTable on a derived type throws an exception , currently it isn't valid to map a derived type to a different table. This change avoids breaking in the future when it becomes a valid thing to do.

You could use Data Annotations on the base model to configure the table that a type maps to:

[Table("Role", Schema = "Security")]
public abstract class RoleBase
{
    public RoleBase()
    {
        this.UserRoles = new List<UserRoles>();
    }
    [Key]
    public long Id { get; set; }
    public string Name { get; set; }
    public virtual ICollection<UserRoleBase> UserRoles { get; set; }
}
Xueli Chen
  • 11,987
  • 3
  • 25
  • 36
  • Thanks @xueli-chen this solved it for me. I added [Table("xxxx", Schema = "xxxx")] to all the abstract base classes with [Key] on the the Id as suggested. and removed builder.HasKey(t => t.Id); and builder.ToTable("xxxx", "xxxx");from the abstract configuration class. Thanks for the help! – Kiewiet Sep 20 '19 at 13:21
  • @Kiewiet , glad that you resolved the issue , if you find my reply is helpful , could you mark my reply as the answer . This will help other people who meet the same or similar issue to find the answer quickly . Thanks a lot ! – Xueli Chen Sep 23 '19 at 01:58