I have a web application running .net core 2.x (just upgraded from .net core 1.x finally) and I've gotten most everything ported from .net core 1.x to 2.x. I have however hit a brick wall with my Identity implementation. It worked fine with .net core 1.x but now it refuses.
The implementation is running on Entity Framework Core and is built Database first (implemented in a pre-existing database).
My problem is that when I try to log in now with .net core 2.x I get an error message that states:
InvalidOperationException: The relationship from 'AspNetUserRole.AspNetRole' to 'AspNetRole.AspNetUserRoles' with foreign key properties {'RoleId' : int} cannot target the primary key {'Id' : int} because it is not compatible. Configure a principal key or a set of compatible foreign key properties for this relationship.
Which, to me, makes absolutely no sense. How can an int
foreign key be incompatible with an int
primary key?
The actual implementation of the context and the classes are as follow (it's a stupidly simple implementation):
public partial class AspNetUser : IdentityUser<int>
{ }
public partial class AspNetRole : IdentityRole<int>
{ }
public partial class AspNetRoleClaim : IdentityRoleClaim<int>
{ }
public partial class AspNetUserClaim : IdentityUserClaim<int>
{ }
public partial class AspNetUserRole : IdentityUserRole<int>
{ }
public partial class AspNetUserToken : IdentityUserToken<int>
{ }
public partial class AspNetUserLogin : IdentityUserLogin<int>
{ }
public class IdentityDataContext : IdentityDbContext<AspNetUser, AspNetRole, int, AspNetUserClaim, AspNetUserRole, AspNetUserLogin, AspNetRoleClaim, AspNetUserToken>
{
public IdentityDataContext(DbContextOptions<IdentityDataContext> options) : base(options)
{ }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<AspNetUser>()
.HasMany(e => e.AspNetUserClaims)
.WithOne()
.HasForeignKey(e => e.UserId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.Entity<AspNetUserClaim>()
.HasOne(x => x.AspNetUser)
.WithMany(x => x.AspNetUserClaims)
.HasForeignKey(x => x.UserId);
builder.Entity<AspNetUser>()
.HasMany(e => e.AspNetUserLogins)
.WithOne()
.HasForeignKey(e => e.UserId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.Entity<AspNetUserLogin>()
.HasOne(x => x.AspNetUser)
.WithMany(x => x.AspNetUserLogins)
.HasForeignKey(x => x.UserId);
builder.Entity<AspNetUser>()
.HasMany(e => e.AspNetUserRoles)
.WithOne()
.HasForeignKey(e => e.UserId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.Entity<AspNetUserRole>()
.HasOne(x => x.AspNetUser)
.WithMany(x => x.AspNetUserRoles)
.HasForeignKey(x => x.UserId);
builder.Entity<AspNetRole>()
.HasMany(e => e.AspNetUserRoles)
.WithOne()
.HasForeignKey(e => e.RoleId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.Entity<AspNetUserRole>()
.HasOne(x => x.AspNetRole)
.WithMany(x => x.AspNetUserRoles)
.HasForeignKey(x => x.RoleId);
builder.Entity<AspNetUserRole>()
.HasOne(x => x.AspNetUser)
.WithMany(x => x.AspNetUserRoles)
.HasForeignKey(x => x.UserId);
builder.Entity<AspNetRole>()
.HasMany(e => e.AspNetRoleClaims)
.WithOne()
.HasForeignKey(e => e.RoleId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.Entity<AspNetRoleClaim>()
.HasOne(x => x.AspNetRole)
.WithMany(x => x.AspNetRoleClaims)
.HasForeignKey(x => x.RoleId);
builder.Entity<AspNetUser>()
.HasMany(e => e.AspNetUserTokens)
.WithOne()
.HasForeignKey(e => e.UserId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.Entity<AspNetUserToken>()
.HasOne(x => x.AspNetUser)
.WithMany(x => x.AspNetUserTokens)
.HasForeignKey(x => x.UserId);
}
}
and the two classes it complains about are defined as:
[Table("AspNetUserRoles")]
public partial class AspNetUserRole
{
[Key]
public int Id { get; set; }
[ForeignKey("AspNetUser")]
public override int UserId { get; set; }
[ForeignKey("AspNetRole")]
public override int RoleId { get; set; }
public string ConcurrencyStamp { get; set; }
public int CreatedById { get; set; }
public System.DateTime CreatedDate { get; set; }
public Nullable<int> ChangedById { get; set; }
public Nullable<System.DateTime> ChangedDate { get; set; }
public bool IsDisabled { get; set; }
[JsonIgnore]
public virtual AspNetRole AspNetRole { get; set; }
[JsonIgnore]
public virtual AspNetUser AspNetUser { get; set; }
}
[Table("AspNetRoles")]
public partial class AspNetRole
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public AspNetRole()
{
this.AspNetRoleClaims = new HashSet<AspNetRoleClaim>();
this.AspNetUserRoles = new HashSet<AspNetUserRole>();
}
[Key]
public override int Id { get; set; }
public override string Name { get; set; }
public override string NormalizedName { get; set; }
public override string ConcurrencyStamp { get; set; }
public int CreatedById { get; set; }
public System.DateTime CreatedDate { get; set; }
public Nullable<int> ChangedById { get; set; }
public Nullable<System.DateTime> ChangedDate { get; set; }
public bool IsDisabled { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<AspNetRoleClaim> AspNetRoleClaims { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<AspNetUserRole> AspNetUserRoles { get; set; }
}
This actually has me completely stumped.
EDIT:
When breaking and trying to evaluate a DbSet
where it throws the error, I get a stacktrace that points to the ModelEvaluator
. Which, I'm going to be honest, helps me very little.
at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.ValidateNoShadowKeys(IModel model)\r\n at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.Validate(IModel model)\r\n at Microsoft.EntityFrameworkCore.Infrastructure.RelationalModelValidator.Validate(IModel model)\r\n at Microsoft.EntityFrameworkCore.Internal.SqlServerModelValidator.Validate(IModel model)\r\n at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.CreateModel(DbContext context, IConventionSetBuilder conventionSetBuilder, IModelValidator validator)\r\n at System.Lazy'1.ViaFactory(LazyThreadSafetyMode mode)\r\n--- End of stack trace from previous location where exception was thrown ---\r\n at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\r\n
at System.Lazy'1.CreateValue()\r\n at Microsoft.EntityFrameworkCore.Internal.DbContextServices.CreateModel()\r\n at Microsoft.EntityFrameworkCore.Internal.DbContextServices.get_Model()\r\n at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScoped(ScopedCallSite scopedCallSite, ServiceProviderEngineScope scope)\r\n at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, ServiceProviderEngineScope scope)\r\n at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScoped(ScopedCallSite scopedCallSite, ServiceProviderEngineScope scope)\r\n at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)\r\n at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)\r\n at Microsoft.EntityFrameworkCore.DbContext.get_DbContextDependencies()\r\n at Microsoft.EntityFrameworkCore.DbContext.get_Model()\r\n at Microsoft.EntityFrameworkCore.Internal.InternalDbSet'1.get_EntityType()\r\n at Microsoft.EntityFrameworkCore.Internal.InternalDbSet'1.get_EntityQueryable()\r\n at Microsoft.EntityFrameworkCore.Internal.InternalDbSet'1.System.Collections.Generic.IEnumerable.GetEnumerator()\r\n at System.Collections.Generic.LargeArrayBuilder'1.AddRange(IEnumerable'1 items)\r\n at System.Collections.Generic.EnumerableHelpers.ToArray[T](IEnumerable'1 source)\r\n at System.Linq.Enumerable.ToArray[TSource](IEnumerable'1 source)\r\n at System.Linq.SystemCore_EnumerableDebugView'1.get_Items()
EDIT 2:
On recommendation, tried adding a OnModelCreating
in where I define the different foreign keys (see the definition of my IdentityDataContext above) - no luck.
EDIT 3:
The OnModelCreating
WAS the answer: I had just missed the "reversed" definitions, if you will. For example, defining
builder.Entity<AspNetUser>()
.HasMany(e => e.AspNetUserRoles)
.WithOne()
.HasForeignKey(e => e.UserId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.Entity<AspNetRole>()
.HasMany(e => e.AspNetUserRoles)
.WithOne()
.HasForeignKey(e => e.RoleId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
Is not enough - you have to also add the reverse:
builder.Entity<AspNetUserRole>()
.HasOne(x => x.AspNetUser)
.WithMany(x => x.AspNetUserRoles)
.HasForeignKey(x => x.UserId);
builder.Entity<AspNetUserRole>()
.HasOne(x => x.AspNetRole)
.WithMany(x => x.AspNetUserRoles)
.HasForeignKey(x => x.RoleId);
OnModelCreating
) of yourIdentityDataContext
? – PrescienceIdentityDataContext
is only what's in the code above. The partial classes (AspNetRole
,AspNetUserRole
and so on) are generated from an edmx (Database First). – PhotoreconnaissanceIdentityDbContext
fluent configuration, which takes precedence over data annotations / conventions. Since your models have navigation properties, you have to override that after calling base implementation. See for instance #45864022. – PrescienceOnModelCreating
, in where I define the different foreign keys (see the definition of my IdentityDataContext above) - no luck. Same error. – PhotoreconnaissanceWithOne
are missing the inverse navigation property, e.g.WithOne(e => e.AspNetUser)
,WithOne(e => e.AspNetRole)
. Also theAspNetUserRole
PK -builder.Entity<AspNetUserRole>().HasKey(e => e.Id)
. Basically everything that is different from the base identity context configuration. – Prescience