2

I am building an ASP.NET MVC 5 multi-tenant solution and have a slight problem when it comes to roles. I have created a custom role entity as follows:

public class ApplicationRole : IdentityRole, ITenantEntity
    {
        public ApplicationRole()
            : base()
        {
        }

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

        public int? TenantId { get; set; }
    }

And done everything else needed.. it's all working nicely, except for one thing...; when a tenant admin tries to add a new role and if that role's name is already being used by a role created by another tenant, he will get the following error:

Name Administrators is already taken.

Obviously there is some underlying check for role names to be unique in ASP.NET Identity somewhere. Is there some way to change this so that I can make it look for uniqueness by "TenantId + Name", instead of Name only?

UPDATE

Using dotPeek to decompile the DLLs, I have found that I need to create my own implementation of IIdentityValidator and of course modify my RoleManager. So, here's my role validator:

public class TenantRoleValidator : IIdentityValidator<ApplicationRole>
    {
        private RoleManager<ApplicationRole, string> Manager { get; set; }

        /// <summary>Constructor</summary>
        /// <param name="manager"></param>
        public TenantRoleValidator(RoleManager<ApplicationRole, string> manager)
        {
            if (manager == null)
            {
                throw new ArgumentNullException("manager");
            }

            this.Manager = manager;
        }

        /// <summary>Validates a role before saving</summary>
        /// <param name="item"></param>
        /// <returns></returns>
        public virtual async Task<IdentityResult> ValidateAsync(ApplicationRole item)
        {
            if ((object)item == null)
            {
                throw new ArgumentNullException("item");
            }

            var errors = new List<string>();
            await this.ValidateRoleName(item, errors);
            return errors.Count <= 0 ? IdentityResult.Success : IdentityResult.Failed(errors.ToArray());
        }

        private async Task ValidateRoleName(ApplicationRole role, List<string> errors)
        {
            if (string.IsNullOrWhiteSpace(role.Name))
            {
                errors.Add("Name cannot be null or empty.");
            }
            else
            {
                var existingRole = await this.Manager.Roles.FirstOrDefaultAsync(x => x.TenantId == role.TenantId && x.Name == role.Name);
                if (existingRole == null)
                {
                    return;
                }

                errors.Add(string.Format("{0} is already taken.", role.Name));
            }
        }
    }

And my role manager:

public class ApplicationRoleManager : RoleManager<ApplicationRole>
    {
        public ApplicationRoleManager(IRoleStore<ApplicationRole, string> store)
            : base(store)
        {
            this.RoleValidator = new TenantRoleValidator(this);
        }

        public static ApplicationRoleManager Create(IdentityFactoryOptions<ApplicationRoleManager> options, IOwinContext context)
        {
            return new ApplicationRoleManager(
                new RoleStore<ApplicationRole>(context.Get<ApplicationDbContext>()));
        }
    }

However, I am now getting a new error:

Cannot insert duplicate key row in object 'dbo.AspNetRoles' with unique index 'RoleNameIndex'. The duplicate key value is (Administrators). The statement has been terminated

I could just modify the db to change the indexes I suppose, but I need it to be correct on installation because the solution I am building is a CMS and will be used for many installations in future...

My first thought is I somehow need to modify the EntityTypeConfiguration<T> for the ApplicationRole entity. But of course I don't have immediate access to that... it just gets auto created by the ApplicationDbContext because it inherits from IdentityDbContext<ApplicationUser>. I will have to delve deeper into the disassembled code and see what I can find...

UPDATE 2

OK, I was using base.OnModelCreating(modelBuilder); to get the configurations for the identity membership tables. I removed that line and copied the decompiled code to my OnModelCreating method, but removed the part for creating the index. This (and removing the index in the db) solved that error I had before.. however, I have 1 more error and I am totally stumped now...

I get an error message as follows:

Cannot insert the value NULL into column 'Name', table 'dbo.AspNetRoles'; column does not allow nulls. INSERT fails. The statement has been terminated.

This makes no sense, because when debugging, I can clearly see I am passing the Name and the TenantId in the role I am trying to create. This is my code:

var result = await roleManager.CreateAsync(new ApplicationRole
            {
                TenantId = tenantId,
                Name = role.Name
            });

Those values are not null, so I don't know what's going on here anymore. Any help would be most appreciated.

UPDATE 3

I created my own RoleStore, which inherits from RoleStore<ApplicationRole> and I overrode the CreateAsync((ApplicationRole role) method so I can debug this part and see what's happening. See below:

enter image description here

After continuing to run the code, I still get the following error on the yellow screen of death:

enter image description here

Someone, anyone, please help shed some light on what's happening here and if it's at all possible to fix this.

UPDATE 4

OK, I'm closer to the answer now.. I created a new db from scratch (allowing EF to create it) and I noticed that the Name column does not get created... only Id and TenantId.. this means the previous error is because my existing DB had the Name column already and was set to NOT NULL.. and EF is ignoring the Name column for my role entity for some reason, which I assume has something to do with it inheriting from IdentityRole.

This is the model configuration I have:

var rolesTable = modelBuilder.Entity<ApplicationRole>().ToTable("AspNetRoles");

            rolesTable.Property(x => x.TenantId)
                .HasColumnAnnotation("Index", new IndexAnnotation(new IndexAttribute("RoleNameIndex") { IsUnique = true, Order = 1 }));

            rolesTable.Property(x => x.Name)
                .IsRequired()
                .HasMaxLength(256)
                .HasColumnAnnotation("Index", new IndexAnnotation(new IndexAttribute("RoleNameIndex") { IsUnique = true, Order = 2 }));

            rolesTable.HasMany(x => x.Users).WithRequired().HasForeignKey(x => x.RoleId);

I thought it was maybe something to do with the index config, so I just removed both of those (TenantId and Name) and replaced it with this:

rolesTable.Property(x => x.Name)
                .IsRequired()
                .HasMaxLength(256);

However, the Name column was still not created. The only difference between now and before, is that I am using modelBuilder.Entity<ApplicationRole>() whereas the default would have been modelBuilder.Entity<IdentityRole>() I suppose...

How can I get EF to recognize the both Name property from the base class, IdentityRole and the TenantId property from the derived class ApplicationRole?

Matt
  • 6,787
  • 11
  • 65
  • 112

1 Answers1

4

OK I've solved this. The answer is to firsrtly follow all the updates I added in my original post and then the final thing to do was make my ApplicationDbContext inherit from IdentityDbContext<ApplicationUser, ApplicationRole, string, IdentityUserLogin, IdentityUserRole, IdentityUserClaim> instead of just IdentityDbContext<ApplicationUser>

Matt
  • 6,787
  • 11
  • 65
  • 112
  • 1
    would be great if you could post your final version to answer. Not sure what is the latest version after reading those updates. And were you happy with that implementation? Did it serve as expected? I see its being almost 7 years, so wonder if you would do it same way today? Thanks in advance – curiousBoy Apr 06 '23 at 07:46
  • 1
    @curiousBoy It's indeed been a while. Looks like something I was doing for my CMS. If you want to take a look, you can find it here: https://github.com/gordon-matt/MantleCMS – Matt Aug 24 '23 at 09:18