IdentityRole в мультитенантном приложении
Я строю мультитенантное решение ASP.NET MVC 5 и имею небольшую проблему, когда дело касается ролей. Я создал пользовательский объект роли следующим образом:
public class ApplicationRole : IdentityRole, ITenantEntity
{
public ApplicationRole()
: base()
{
}
public ApplicationRole(string roleName)
: base(roleName)
{
}
public int? TenantId { get; set; }
}
И сделал все остальное, что нужно.. все работает хорошо, за исключением одного...; когда администратор клиента пытается добавить новую роль и имя этой роли уже используется ролью, созданной другим арендатором, он получит следующую ошибку:
Имя Администратора уже занято.
Очевидно, что есть некоторая базовая проверка, чтобы имена ролей были уникальными в ASP.NET Identity где-то. Есть ли способ изменить это так, чтобы я мог искать уникальность по "TenantId + Name", а не только по имени?
ОБНОВИТЬ
Используя dotPeek для декомпиляции библиотек DLL, я обнаружил, что мне нужно создать свою собственную реализацию IIdentityValidator и, конечно же, изменить мой RoleManager. Итак, вот мой валидатор роли:
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));
}
}
}
И мой менеджер роли:
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>()));
}
}
Однако теперь я получаю новую ошибку:
Невозможно вставить повторяющуюся строку ключа в объект "dbo.AspNetRoles" с уникальным индексом "RoleNameIndex". Значение дубликата ключа (Администраторы). Заявление было прекращено
Я мог бы просто изменить базу данных, чтобы изменить индексы, я полагаю, но мне нужно, чтобы она была правильной при установке, потому что решение, которое я создаю, - это CMS и будет использоваться для многих установок в будущем...
Моя первая мысль: мне нужно как-то изменить EntityTypeConfiguration<T>
для ApplicationRole
юридическое лицо. Но, конечно, у меня нет немедленного доступа к этому... он просто автоматически создается ApplicationDbContext
потому что он наследует от IdentityDbContext<ApplicationUser>
, Мне придется глубже вникать в разобранный код и посмотреть, что я могу найти...
ОБНОВЛЕНИЕ 2
Хорошо я использовал base.OnModelCreating(modelBuilder);
чтобы получить конфигурации для таблиц членства идентичности. Я удалил эту строку и скопировал декомпилированный код в мой OnModelCreating
метод, но удалил часть для создания индекса. Это (и удаление индекса в БД) решило ту ошибку, которая у меня была раньше... однако, у меня есть еще 1 ошибка, и я теперь совершенно тупик...
Я получаю сообщение об ошибке следующим образом:
Невозможно вставить значение NULL в столбец "Имя", таблица "dbo.AspNetRoles"; столбец не допускает нулевые значения. Вставить не удается. Заявление было прекращено.
Это не имеет смысла, потому что при отладке я ясно вижу, что передаю Имя и TenantId в роли, которую пытаюсь создать. Это мой код:
var result = await roleManager.CreateAsync(new ApplicationRole
{
TenantId = tenantId,
Name = role.Name
});
Эти значения не равны нулю, поэтому я не знаю, что здесь происходит. Любая помощь будет наиболее ценной.
ОБНОВЛЕНИЕ 3
Я создал свой собственный RoleStore, который наследуется от RoleStore<ApplicationRole>
и я преодолел CreateAsync((ApplicationRole role)
метод, чтобы я мог отладить эту часть и посмотреть, что происходит. Увидеть ниже:
Продолжая запускать код, я все еще получаю следующую ошибку на желтом экране смерти:
Кто-нибудь, кто-нибудь, пожалуйста, помогите пролить свет на то, что здесь происходит, и если это вообще возможно исправить.
ОБНОВЛЕНИЕ 4
Хорошо, теперь я ближе к ответу.. Я создал новую базу данных с нуля (позволяя EF создавать ее), и я заметил, что столбец Имя не создается... только Id и TenantId.. это означает, что предыдущий ошибка в том, что моя существующая БД уже имела столбец Имя и была установлена на NOT NULL.. и EF по какой-то причине игнорирует столбец Имя для моей сущности роли, которая, как я полагаю, имеет какое-то отношение к ней, наследуемой от IdentityRole.
Это модель конфигурации у меня есть:
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);
Я подумал, что это может быть связано с конфигурацией индекса, поэтому я просто удалил оба из них (TenantId и Name) и заменил это следующим:
rolesTable.Property(x => x.Name)
.IsRequired()
.HasMaxLength(256);
Однако столбец Имя все еще не был создан. Единственная разница между сейчас и до того, что я использую modelBuilder.Entity<ApplicationRole>()
тогда как по умолчанию было бы modelBuilder.Entity<IdentityRole>()
Я полагаю...
Как я могу заставить EF распознавать оба свойства Name из базового класса, IdentityRole
и свойство TenantId из производного класса ApplicationRole
?
1 ответ
ОК, я решил это. Ответ заключается в том, чтобы в первую очередь следить за всеми обновлениями, которые я добавил в исходное сообщение, а затем последнее, что нужно сделать, это сделать мой ApplicationDbContext
наследовать от IdentityDbContext<ApplicationUser, ApplicationRole, string, IdentityUserLogin, IdentityUserRole, IdentityUserClaim>
вместо просто IdentityDbContext<ApplicationUser>