Unable to create migration in EF Core 6 using custom Identity & custom ApiAuthorizationDbContext

584 views Asked by At

I've been trying for the last few weeks to get a Solution setup based on the Jason Taylor's 'CleanArchitecture' on GitHub. My Solution is not forked from his, but built from scratch to mimic his in an attempt to fully understand it.

However, when getting to the ApplicationDbContext, I realized that I needed to customize the ApiAuthorizationDbContext to allow me to use int as the primary key for my identity models - as this project will be inheriting an existing database where users are setup with int based id's.

The problem is whenever I try to create the intial migration, I get the following error:

The entity type 'RoleClaims' requires a primary key to be defined. If you intended to use a keyless entity type, call 'HasNoKey' in 'OnModelCreating'.

I've never had an issue with this before, as the IdentityDbContext usually sorts all that out in the base.OnModelCreating function, then I just rename the tables using configuration files, but this is not currently working.

I tried manually setting the PKey of the ApplicationRoleClaims in it's configuration file, but that resulting in the following error:

A key cannot be configured on 'RoleClaims' because it is a derived type. The key must be configured on the root type 'IdentityRoleClaim'.

So I settled on trying to configure the Identity models in the base ApiAuthorizationDbContext OnModelCreating, thinking perhaps something was messing up in the main ApplicationDbContext inheriting it, but that has made no difference.

Here is my implementation of the ApiAuthorizationDbContext:

    public class ApiAuthorizationDbContext<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TRoleClaim, TUserToken> : IdentityDbContext<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TRoleClaim, TUserToken>, IPersistedGrantDbContext
        where TUser : IdentityUser<TKey>
        where TRole : IdentityRole<TKey>
        where TKey : IEquatable<TKey>
        where TUserClaim : IdentityUserClaim<TKey>
        where TUserRole : IdentityUserRole<TKey>
        where TUserLogin : IdentityUserLogin<TKey>
        where TRoleClaim : IdentityRoleClaim<TKey>
        where TUserToken : IdentityUserToken<TKey>
    {
        private readonly IOptions<OperationalStoreOptions> _operationalStoreOptions;

        /// <summary>
        /// Initializes a new instance of <see cref="KeyApiAuthorizationDbContext{TUser}"/>.
        /// </summary>
        /// <param name="options">The <see cref="DbContextOptions"/>.</param>
        /// <param name="operationalStoreOptions">The <see cref="IOptions{OperationalStoreOptions}"/>.</param>
        public ApiAuthorizationDbContext(
            DbContextOptions options,
            IOptions<OperationalStoreOptions> operationalStoreOptions)
            : base(options)
        {
            _operationalStoreOptions = operationalStoreOptions;
        }

        /// <summary>
        /// Gets or sets the <see cref="DbSet{PersistedGrant}"/>.
        /// </summary>
        public DbSet<PersistedGrant> PersistedGrants { get; set; }

        /// <summary>
        /// Gets or sets the <see cref="DbSet{DeviceFlowCodes}"/>.
        /// </summary>
        public DbSet<DeviceFlowCodes> DeviceFlowCodes { get; set; }

        /// <summary>
        /// Gets or sets the <see cref="DbSet{Key}"/>.
        /// </summary>
        public DbSet<Key> Keys { get; set; }

        Task<int> IPersistedGrantDbContext.SaveChangesAsync() => base.SaveChangesAsync();

        /// <inheritdoc />
        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);

            builder.Entity<TUser>()
                .ToTable("Users");

            builder.Entity<TUserClaim>()
                .ToTable("UserClaims");

            builder.Entity<TUserLogin>()
                .ToTable("UserLogins");

            builder.Entity<TUserRole>()
                .ToTable("UserRoles");

            builder.Entity<TUserToken>()
                .ToTable("UserTokens");

            builder.Entity<TRole>()
                .ToTable("Roles");

            builder.Entity<TRoleClaim>()
                .ToTable("RoleClaims");
            builder.Entity<TRoleClaim>()
                .HasKey(c => c.Id);

            builder.ConfigurePersistedGrantContext(_operationalStoreOptions.Value);
        }
    }

My ApplicationDbContext:

    public class ApplicationDbContext : ApiAuthorizationDbContext<ApplicationUser, ApplicationRole, int, ApplicationUserClaim, ApplicationUserRole, ApplicationUserLogin, ApplicationRoleClaim, ApplicationUserToken>, IApplicationDbContext
    {
        private readonly ICurrentUserService _currentUserService;

        public ApplicationDbContext(
            DbContextOptions<ApplicationDbContext> options,
            IOptions<OperationalStoreOptions> operationalStoreOptions,
            ICurrentUserService currentUserService) : base(options, operationalStoreOptions)
        {
            _currentUserService = currentUserService;
        }

        public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken())
        {
            foreach (var entry in ChangeTracker.Entries<AuditableEntity>())
            {
                switch (entry.State)
                {
                    case EntityState.Added:
                        entry.Entity.CreatedBy = int.Parse(_currentUserService?.UserId);
                        entry.Entity.Created = DateTime.Now;
                        break;

                    case EntityState.Modified:
                        entry.Entity.LastModifiedBy = int.Parse(_currentUserService?.UserId);
                        entry.Entity.LastModified = DateTime.Now;
                        break;
                }
            }

            var result = await base.SaveChangesAsync(cancellationToken);

            return result;
        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);

            builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
        }
    }

My Identity models:

    public class ApplicationRole : IdentityRole<int>
    {
        public ApplicationRole()
        {
        }

        public ApplicationRole(string roleName) : base(roleName)
        {
        }

        public virtual ICollection<ApplicationUserRole> UserRoles { get; set; }
        public virtual ICollection<ApplicationRoleClaim> RoleClaims { get; set; }
    }

    public class ApplicationRoleClaim : IdentityRoleClaim<int>
    {
    }

    public class ApplicationUser : IdentityUser<int>
    {
        public ApplicationUser()
        {
        }

        public ApplicationUser(string userName) : base(userName)
        {
        }

        DbSet<ApplicationRole> Roles { get; set; }
        DbSet<ApplicationUserClaim> Claims { get; set; }
        DbSet<ApplicationUserLogin> Logins { get; set; }
        DbSet<ApplicationUserToken> Tokens { get; set; }
    }

    public class ApplicationUserClaim : IdentityUserClaim<int>
    {
    }

    public class ApplicationUserLogin : IdentityUserLogin<int>
    {
    }

    public class ApplicationUserRole : IdentityUserRole<int>
    {
        public virtual ApplicationUser User { get; set; }
        public virtual ApplicationRole Role { get; set; }
    }

    public class ApplicationUserToken : IdentityUserToken<int>
    {
    }

UPDATE 1.0

This appears to be unrelated to the ApiAuthorizationDbContext, the same issue occurs when directly inheriting IdentityDbContext, such as below:

    public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, int, ApplicationUserClaim, ApplicationUserRole, ApplicationUserLogin, ApplicationRoleClaim, ApplicationUserToken>, IApplicationDbContext
    {
        private readonly ICurrentUserService _currentUserService;

        public ApplicationDbContext(
            DbContextOptions<ApplicationDbContext> options,
            IOptions<OperationalStoreOptions> operationalStoreOptions) : base(options)
        {
        }

        public ApplicationDbContext(
            DbContextOptions<ApplicationDbContext> options,
            IOptions<OperationalStoreOptions> operationalStoreOptions,
            ICurrentUserService currentUserService) : base(options)
        {
            _currentUserService = currentUserService;
        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);

            var roleClaim = builder.Model.FindEntityType("MyNamespace_Core.Infrastructure.Identity.ApplicationRoleClaim");

            roleClaim.RemoveForeignKey(roleClaim.FindForeignKeys(roleClaim.GetProperty("RoleId1"))?.First());
            roleClaim.RemoveProperty("RoleId1");
            Console.WriteLine(string.Join(',', builder.Model.FindEntityType("MyNamespace_Core.Infrastructure.Identity.ApplicationRoleClaim")?.GetProperties()));

            builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
        }
    }

After printing the ModelBuilder.Model.ToDebugString to the console during the migration, I found that the ApplicationRoleClaim entity is duplicating the RoleId foreign key.. I'm unaware if this is the issue, or simply a related issue, but I am unable to resolve it.

I have attempted to remove the erroneous FK on the ApplicationRoleClaim entity after the base.OnModelCreating function is called like below:

roleClaim.RemoveForeignKey(roleClaim.FindForeignKeys(roleClaim.GetProperty("RoleId1"))?.First());
roleClaim.RemoveProperty("RoleId1");

However, this then produces a new FK named 'RoleId2'....

So it seems that there is some fundamental issue with EF Core 6 and Identity that I'm not aware of.. Or I'm missing something incredibly obvious.

0

There are 0 answers