Matt Matt - 1 month ago 19
C# Question

IdentityRole in multi-tenant application

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
?

Answer

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>

Comments