
«Из коробки» провайдером данных для ASP.NET Identity является MSSQL, но поскольку система авторизация Identity может взаимодействовать с любой другой реляционной СУБД, мы исследовали и реализовали эту возможность для InterSystems Caché.
Во первых, для чего все это? Представим, что ваш проект использует СУБД Caché на .NET и вам потребовалась полноценная и надежная система авторизации. Писать такую систему с нуля руками крайне нецелесообразно, естественно, что вы захотите воспользоваться существующим аналогом в .NET — ASP.NET Identity. Но в чистом виде фреймворк способен работать только со своей нативной СУБД от Microsoft — MS SQL. Наша задача состояла в том, чтобы реализовать адаптер, который позволит легким движением руки портировать Identity на СУБД Intersystems Cache. Поставленная задача была реализована в ASP.NET Identity Caché Provider.
Суть проекта ASP.NET Identity Caché Provider заключается в имплиментации провайдера данных Caché для ASP.NET Idenity. Основная задача заключалась в хранении и предоставлении доступа к таблицам AspNetRoles, AspNetUserClaims, AspNetUserLogins, AspNetUserRoles и AspNetUsers, не нарушая стандартной логики работы с данными таблицами.

Каждый пользователь для UserManager’а предоставляет объект интерфейса IUser. При этом все операции по управлению пользователями производятся через хранилище, представленное объектом IUserStore. Каждая роль представляет реализацию интерфейса IRole, а манипуляции с ролями (добавление, изменение, удаление) осуществляются посредством RoleManager. Непосредственную реализацию интерфейсов IUser, IRole, IUserStore и IRoleStore предоставляет пространство имен Microsoft.AspNet.Identity EntityFramework, где для использования доступны такие классы как IdentityUser, UserStore, IdentityRole, RoleStore, IdentityDbContext.

Если необходимо хранить дополнительную информацию о пользователе, которой нет в указанных таблицах по умолчанию, существует класс IdentityUserClaim (клеймы), который позволяет добавлять необходимые поля и потом использовать их, например, при регистрации пользователя.
Перейдем к рассмотрению реализации провайдера данных Caché для ASP.NET Identity. Она проходила в два этапа:
− Имплементация классов хранения данных (которые будут отвечать за хранение состояния) и класса IdentityDbContext, который инкапсулирует всю низкоуровневую логику работы с хранилищем данных. Также был имплементирован класс IdentityDbInitializer, который проводит адаптацию базы данных Caché для работы с Identity.
− Имплементация классов UserStore и RoleStore (вместе с интеграционными
тестами). Демонстрационный проект.
В ходе первого этапа были имплеменированы следующие классы:
− IdentityUser — имплементация интерфейса IUser.
− IdentityUserRole — ассоциативная сущность для связи User–Role.
− IdentityUserLogin — данные о логинах пользователя.
Расширяемая версия класса UserLoginInfo.
− IdentityUserClaim — данные о клеймах пользователя.
− IdentityDbContext<TUser, TRole, TKey, TUserLogin, TUserRole, TUserClaim> — контекст базы данных Entity Framework.
Рассмотрим более подробно сущность IdentityUser, которая представляет собой хранилище для пользователей, ролей, логинов, клеймов и связей пользователь-роль. Пример имплементации обычного и обобщенного варианта IdentityUser.
namespace InterSystems.AspNet.Identity.Cache { /// <summary> /// IUser implementation /// </summary> public class IdentityUser : IdentityUser<string, IdentityUserLogin, IdentityUserRole, IdentityUserClaim>, IUser { /// <summary> /// Constructor which creates a new Guid for the Id /// </summary> public IdentityUser() { Id = Guid.NewGuid().ToString(); } /// <summary> /// Constructor that takes a userName /// </summary> /// <param name="userName"></param> public IdentityUser(string userName) : this() { UserName = userName; } } /// <summary> /// IUser implementation /// </summary> /// <typeparam name="TKey"></typeparam> /// <typeparam name="TLogin"></typeparam> /// <typeparam name="TRole"></typeparam> /// <typeparam name="TClaim"></typeparam> public class IdentityUser<TKey, TLogin, TRole, TClaim> : IUser<TKey> where TLogin : IdentityUserLogin<TKey> where TRole : IdentityUserRole<TKey> where TClaim : IdentityUserClaim<TKey> { /// <summary> /// Constructor /// </summary> public IdentityUser() { Claims = new List<TClaim>(); Roles = new List<TRole>(); Logins = new List<TLogin>(); } /// <summary> /// Email /// </summary> public virtual string Email { get; set; }
Для реализации ограничения прав доступа в Identity предназначены специальные объекты – Роли. Роль в конфигурации может соответствовать должностям или видам деятельности различных групп пользователей.
namespace InterSystems.AspNet.Identity.Cache { /// <summary> /// EntityType that represents a user belonging to a role /// </summary> public class IdentityUserRole : IdentityUserRole<string> { } /// <summary> /// EntityType that represents a user belonging to a role /// </summary> /// <typeparam name="TKey"></typeparam> public class IdentityUserRole<TKey> { /// <summary> /// UserId for the user that is in the role /// </summary> public virtual TKey UserId { get; set; } /// <summary> /// RoleId for the role /// </summary> public virtual TKey RoleId { get; set; } } }
IdentityDbContext – сущность, инкапсулирующая в себе создание подключения, загрузку сущностей из базы данных, валидацию соответствия пользовательских объектов структуре связанных таблиц и значений полей. В качестве примера рассмотрим метод OnModelCreating, который валидирует таблицы в соответствии с требованиями Identity.
protected override void OnModelCreating(DbModelBuilder modelBuilder) { // Mapping and configuring identity entities according to the Cache tables var user = modelBuilder.Entity<TUser>() .ToTable("AspNetUsers"); user.HasMany(u => u.Roles).WithRequired().HasForeignKey(ur => ur.UserId); user.HasMany(u => u.Claims).WithRequired().HasForeignKey(uc => uc.UserId); user.HasMany(u => u.Logins).WithRequired().HasForeignKey(ul => ul.UserId); user.Property(u => u.UserName) .IsRequired() .HasMaxLength(256) .HasColumnAnnotation("Index", new IndexAnnotation(new IndexAttribute("UserNameIndex") { IsUnique = true })); user.Property(u => u.Email).HasMaxLength(256); modelBuilder.Entity<TUserRole>() .HasKey(r => new { r.UserId, r.RoleId }) .ToTable("AspNetUserRoles"); modelBuilder.Entity<TUserLogin>() .HasKey(l => new { l.LoginProvider, l.ProviderKey, l.UserId }) .ToTable("AspNetUserLogins"); modelBuilder.Entity<TUserClaim>() .ToTable("AspNetUserClaims"); var role = modelBuilder.Entity<TRole>() .ToTable("AspNetRoles"); role.Property(r => r.Name) .IsRequired() .HasMaxLength(256) .HasColumnAnnotation("Index", new IndexAnnotation(new IndexAttribute("RoleNameIndex") { IsUnique = true })); role.HasMany(r => r.Users).WithRequired().HasForeignKey(ur => ur.RoleId); }
DbModelBuilder служит для сопоставления классов CLR со схемой базы данных. Этот ориентированный на код подход к построению модели EDM называется Code First. DbModelBuilder обычно используется для настройки модели путем переопределения OnModelCreating(DbModelBuilder). Однако DbModelBuilder можно также использовать независимо от DbContext для сборки модели и последующего конструирования DbContext или ObjectContext.
Класс IdentityDbInitializer подготавливает базу данных Caché для использовани Identity.
public void InitializeDatabase(DbContext context) { using (var connection = BuildConnection(context)) { var tables = GetExistingTables(connection); CreateTableIfNotExists(tables, AspNetUsers, connection); CreateTableIfNotExists(tables, AspNetRoles, connection); CreateTableIfNotExists(tables, AspNetUserRoles, connection); CreateTableIfNotExists(tables, AspNetUserClaims, connection); CreateTableIfNotExists(tables, AspNetUserLogins, connection); CreateIndexesIfNotExist(connection); } }
Методы CreateTableIfNotExists создают необходимые таблицы, если таких еще не существует. Проверка на существование таблицы делается посредством выполнения запроса к таблице Cache — Dictionary.CompiledClass, в которой хранится информация об уже существующих таблицах. В случае если какая-либо таблица еще не создана, она создается.
На втором этапе были реализованы такие сущности как IdentityUserStore и IdentityRoleStore, которые инкапсулируют в себе логику добавления, редактирования и удаления пользователей, и ролей. Для этих сущностей требовалось стопроцентное покрытие юнит-тестами.
Подведем итоги: был реализован провайдер данных для работы СУБД Caché с Entity Framework в контексте технологии ASP.NET Identity. Приложение оформлено в отдельный Nuget-пакет, и теперь при необходимости работать с СУБД Caché, и при этом использовать стандартную авторизацию от Microsoft, достаточно просто внедрить сборку Identity Caché Provider в проект через Nuget Package Manager.
Реализация проекта с исходным кодом, примером и тестами выложена на GitHub.

