Entity Framework Role Manager error

Migrating from MariaDB to MongoDB using EF Core Provider

Hello! I am trying to migrate from MariaDB to MongoDB using MongoDB EF Core Provider. When attempting to use RoleManager.AddClaimAsync(TRole role, Claim claim) I get a [Microsoft.AspNetCore.Identity.RoleManager] Role Manager error.

Stack Trace

The property ‘IdentityRoleClaim.Id’ does not have a value set and no value generator is available for properties of type ‘int’. Either set a value for the property before adding the entity or configure a value generator for properties of type ‘int’ in ‘OnModelCreating’.

Here is the stack trace:

at Microsoft.EntityFrameworkCore.ValueGeneration.ValueGeneratorSelector.Create(IProperty property, ITypeBase typeBase)
   at Microsoft.EntityFrameworkCore.ValueGeneration.ValueGeneratorSelector.<Select>b__6_0(IProperty p, ITypeBase t)
   at Microsoft.EntityFrameworkCore.ValueGeneration.ValueGeneratorCache.<>c.<GetOrAdd>b__7_0(CacheKey ck, ValueTuple`3 p)
   at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd[TArg](TKey key, Func`3 valueFactory, TArg factoryArgument)
   at Microsoft.EntityFrameworkCore.ValueGeneration.ValueGeneratorCache.GetOrAdd(IProperty property, ITypeBase typeBase, Func`3 factory)
   at Microsoft.EntityFrameworkCore.ValueGeneration.ValueGeneratorSelector.Select(IProperty property, ITypeBase typeBase)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ValueGenerationManager.GetValueGenerator(IProperty property)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ValueGenerationManager.Generate(InternalEntityEntry entry, Boolean includePrimaryKey)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState entityState, Boolean acceptChanges, Boolean modifyProperties, Nullable`1 forceStateWhenUnknownKey, Nullable`1 fallbackState)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.PaintAction(EntityEntryGraphNode`1 node)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityEntryGraphIterator.TraverseGraph[TState](EntityEntryGraphNode`1 node, Func`2 handleNode)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.AttachGraph(InternalEntityEntry rootEntry, EntityState targetState, EntityState storeGeneratedWithKeySetTargetState, Boolean forceStateWhenUnknownKey)
   at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.SetEntityState(InternalEntityEntry entry, EntityState entityState)
   at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.Add(TEntity entity)
   at Microsoft.AspNetCore.Identity.EntityFrameworkCore.RoleStore`5.AddClaimAsync(TRole role, Claim claim, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Identity.RoleManager`1.AddClaimAsync(TRole role, Claim claim)

DbContext Implementation

Here is my DbContext implementation:

public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, string> , IApplicationDbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

    public DbSet<FidoStoredCredential> FidoStoredCredential => Set<FidoStoredCredential>();

    protected override void OnModelCreating(ModelBuilder builder)
    {
        builder.Entity<FidoStoredCredential>().HasKey(m => m.Id);

        base.OnModelCreating(builder);
    }
}

Identity Registration Code

Here is my identity registration code:

services.AddDbContextFactory<ApplicationDbContext>(options =>
                    options.UseMongoDB(_mongoClient, "DatabaseName")
                    .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));

services.AddIdentity<ApplicationUser, ApplicationRole>(options => {
    options.SignIn.RequireConfirmedAccount = false;
    options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
})
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddRoleManager<RoleManagerScopedService<ApplicationRole>>()
    .AddUserManager<UserManagerScopedService<ApplicationUser>>()
    .AddDefaultTokenProviders()
    .AddTokenProvider<Fido2UserTwoFactorTokenProvider<ApplicationUser>>("FIDO2")
    ;

User and Role Models

My IdentityUser and IdentityRole implementations aren’t very sophisticated, I simply extend IdentityUser and IdentityRole adding a few properties:

public class ApplicationUser : IdentityUser
{
    public ApplicationUser() : base() { }

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

    public string FullName { get; set; } = string.Empty;

    /// <summary> Profile picture. </summary>
    public byte[] Picture { get; set; } = Array.Empty<byte>();

    /// <summary> Indicates if this account belong to a human 
    /// or is it some kind of service account. </summary>
    public bool IsHuman { get; set; } = true;

    /// <summary> Indicates if interactive login is allowed for this account. </summary>
    public bool IsLoginAllowed { get; set; } = true;

    /// <summary> Indicates if user can be deleted </summary>
    public bool IsDeleteAllowed { get; set; } = true;

    /// <summary> The last time user was active. </summary>
    public DateTimeOffset LastActive { get; set; } = DateTimeOffset.UtcNow;

    /// <summary> Indicates if user should be logged out upon their session timeout. </summary>
    public bool IsSessionTimeoutAllowed { get; set; } = true;

    public DateTimeOffset LastPasswordChangeDate { get; set; } = DateTimeOffset.UtcNow;
}


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

    [Required]
    public override string Name { get => base.Name; set => base.Name = value; }

    /// <summary> Description of the role </summary>
    public string Description { get;set;} = string.Empty;

    /// <summary> Indicates if role is enabled. </summary>
    public bool Enabled { get;set;} = true;

    /// <summary> Indicates if role can be deleted. </summary>
    public bool IsDeleteAllowed { get; set; } = true;

    /// <summary> Indicates if role can be disabled. </summary>
    public bool IsDisableAllowed { get; set; } = true;

    public byte[] MappedAdGroupSid { get; set; } = Array.Empty<byte>();

    public string MappedAdGroupName { get; set; } = string.Empty;

    public uint DisplayPriority { get; set; } = 0;

    /// <summary> Role picture. </summary>
    public byte[] Picture { get; set; } = Array.Empty<byte>();
}

Hi Alexey.

The error message you’re seeing there is because IdentityRoleClaim.Id is an integer key and so needs a value. I would imagine the MariaDB provider you were using is able to generate one as it is a relational non-replicated server.

We don’t have the facility to provide auto-incremented integers as they would not scale well in high-volume environments with multiple servers.

If you have the ability to manually set it in your code by checking some reliable source for an unused integer you could try that.

1 Like

Hi Damien,

Thank you for the reply. Is there any chance this could be automated? Perhaps in the OnModelCreating() method in the DbContext class? The matter is that I never set IdentityRoleClaim.Id in the code explicitly, I tend to use EF Core Role Manager to create new roles. I really appreciate any help you can provide.

You could register a random integer generator but I don’t think it’s something we would enable by default given the potential issues with replicated scenarios.

Thank you for the suggestion. I appreciate the help. What I ended up doing was registering a custom ValueGenerator which returns the next index (not random, though, consecutive one):

public class CustomIdentityRoleClaimIdValueGenerator : ValueGenerator<int>
{
    public override bool GeneratesTemporaryValues => false;

    public override int Next(EntityEntry entry)
    {
        var currInd = entry.Context.Set<IdentityRoleClaim<string>>().Count();

        return currInd + 1;
    }
}

Registration code in DbContext implementation inside OnModelCreating() method:

builder.Entity< IdentityRoleClaim<string> >().Property(m => m.Id).HasValueGenerator<CustomIdentityRoleClaimIdValueGenerator>();

This seems to solve the issue. However, I am trying to utilize UserManager.GetClaimsAsync(TUser user) method and getting the exception:

MongoDB.Driver.Linq.ExpressionNotSupportedException: Expression not supported: i.ToClaim().

If I understand this correctly, the UserManager.GetClaimsAsync() is not supported by the Mongo EF Core DB provider. Or is there any way to bypass this limitation? Thank you very much in advance!

The full stack trace is:

 [Microsoft.EntityFrameworkCore.Query] An exception occurred while iterating over the results of a query for context type '"NextGenHMI.Core.Libraries.AAA.Data.ApplicationDbContext"'."\r\n""MongoDB.Driver.Linq.ExpressionNotSupportedException: Expression not supported: i.ToClaim().\r\n   at MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.MethodCallExpressionToAggregationExpressionTranslator.Translate(TranslationContext context, MethodCallExpression expression)\r\n   at MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.ExpressionToAggregationExpressionTranslator.Translate(TranslationContext context, Expression expression)\r\n   at MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.ExpressionToAggregationExpressionTranslator.TranslateLambdaBody(TranslationContext context, LambdaExpression lambdaExpression, IBsonSerializer parameterSerializer, Boolean asRoot)\r\n   at MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToPipelineTranslators.SelectMethodToPipelineTranslator.Translate(TranslationContext context, MethodCallExpression expression)\r\n   at MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToPipelineTranslators.ExpressionToPipelineTranslator.Translate(TranslationContext context, Expression expression)\r\n   at MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToExecutableQueryTranslators.ExpressionToExecutableQueryTranslator.Translate[TDocument,TOutput](MongoQueryProvider`1 provider, Expression expression)\r\n   at MongoDB.Driver.Linq.Linq3Implementation.MongoQuery`2.Execute()\r\n   at MongoDB.Driver.Linq.Linq3Implementation.MongoQuery`2.GetEnumerator()\r\n   at MongoDB.EntityFrameworkCore.Query.QueryingEnumerable`2.Enumerator.MoveNextHelper()\r\n   at MongoDB.EntityFrameworkCore.Query.QueryingEnumerable`2.Enumerator.MoveNextAsync()"
MongoDB.Driver.Linq.ExpressionNotSupportedException: Expression not supported: i.ToClaim().
   at MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.MethodCallExpressionToAggregationExpressionTranslator.Translate(TranslationContext context, MethodCallExpression expression)
   at MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.ExpressionToAggregationExpressionTranslator.Translate(TranslationContext context, Expression expression)
   at MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.ExpressionToAggregationExpressionTranslator.TranslateLambdaBody(TranslationContext context, LambdaExpression lambdaExpression, IBsonSerializer parameterSerializer, Boolean asRoot)
   at MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToPipelineTranslators.SelectMethodToPipelineTranslator.Translate(TranslationContext context, MethodCallExpression expression)
   at MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToPipelineTranslators.ExpressionToPipelineTranslator.Translate(TranslationContext context, Expression expression)
   at MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToExecutableQueryTranslators.ExpressionToExecutableQueryTranslator.Translate[TDocument,TOutput](MongoQueryProvider`1 provider, Expression expression)
   at MongoDB.Driver.Linq.Linq3Implementation.MongoQuery`2.Execute()
   at MongoDB.Driver.Linq.Linq3Implementation.MongoQuery`2.GetEnumerator()
   at MongoDB.EntityFrameworkCore.Query.QueryingEnumerable`2.Enumerator.MoveNextHelper()
   at MongoDB.EntityFrameworkCore.Query.QueryingEnumerable`2.Enumerator.MoveNextAsync()
   at System.Runtime.CompilerServices.ConfiguredCancelableAsyncEnumerable`1.Enumerator.MoveNextAsync()
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Identity.EntityFrameworkCore.UserStore`9.GetClaimsAsync(TUser user, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Identity.UserManager`1.GetClaimsAsync(TUser user)

I think you’re probably running into the lack of certain Select projection support right now in the EF and LINQ providers we have. It’s on the backlog of things to tackle but I don’t have an ETA. Specifically I think it’s this line:

It might be possible to subclass that RoleStore and override that method to put an AsEnumerable() between the Where and the Select to force the select projection to happen entirely in LINQ to Objects side for now. That said there could be other issues like this preventing the use of the ASP.NET Identity module with the MongoDB EF Core Provider.