Миграция ASP.NET MVC Identity на ASP.NET Core Identity
                        
                    При попытке мигрировать один мой проект на .NET core я столкнулся с тем, что достаточно мало толковых статей про то, как перенести пользователей и роли на .NET Core 3.0.
Из того полезного, что я нашел было только несколько обзорных статей и советов:
- https://stackoverflow.com/questions/53878000/how-to-migrate-identity-users-from-a-mvc5-app-to-a-asp-net-core-2-2-app
 - https://docs.microsoft.com/en-us/aspnet/core/migration/proper-to-2x/membership-to-core-identity?view=aspnetcore-2.2
 
В этой статье я разберу как за 10 шагов мигрировать свое ASP.NET MVC приложение на ASP.NET Core.
Подготовка тестового проекта
В качестве проекта который я буду мигрировать, я возьму свой старый MVC проект. Давайте для того, чтобы немного усложнить задачу, добавим к пользователю еще одно entity – hobby и сделаем связь many-to-many.
public class Hobby : BaseEntity
    {
        public string Name { get; set; }
        public ICollection<ApplicationUser> Users{ get; set; }
    }
Entity User'a я модифицирую следующим образом:
public class ApplicationUser : IdentityUser
    {
        public ICollection<Hobby> Hobbies { get; set; }
        public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
        {
            // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
            var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
            // Add custom user claims here
            return userIdentity;
        }
    }
Для того, чтобы в базе были хоть какие-то данные по юзеру и его хобби, добавим Seed данных:
protected override void Seed(ApplicationDbContext context)
        {
            var userManager = new ApplicationUserManager(new UserStore<ApplicationUser>(context));
            var roleManager = new ApplicationRoleManager(new RoleStore<IdentityRole>(context));
            const string adminRoleName = "Administrator";
            var roles = new List<IdentityRole>
            {
                new IdentityRole
                {
                    Name = adminRoleName
                },
                new IdentityRole
                {
                    Name = "User"
                }
            };
            foreach (var identityRole in roles)
            {
                var existingRole = roleManager.FindByName(identityRole.Name);
                if (existingRole == null)
                {
                    context.Roles.Add(identityRole);
                }
            }
            var hobbies = new List<Hobby>
            {
                new Hobby
                {
                    Name = "Танцы"
                },
                new Hobby
                {
                    Name = "Рисование"
                }
            };
            context.Hobbies.AddRange(hobbies);
            var user = new ApplicationUser
            {
                UserName = "test@test.com",
                Email = "test@test.com",
                Hobbies = hobbies
            };         
            userManager.Create(user, "123456");
            base.Seed(context);
        }
Так как в моем проекте уже была миграция, то я сначала применю ее для новой базы выполнив команду update-database в package manager console.
Теперь добавим новую миграцию и выполняем ее:
add-migration AddHobbies
update-database
Открыв вашу базу данных в SQL Management studio вы должны увидеть следующее:

У вас должна появиться таблица с хобби и таблица для связки many to many хобби и юзера.
Давайте попробуем теперь залогиниться с использованием нашего логина и пароля:

Как видим, мы успешно залогинились:

Миграция ASP.NET MVC приложения на ASP.NET Core
Шаг 1: Создание Web проекта
Создаем новый solution с ASP.NET Core веб проектом

и выбираем имя проекта и solution'а

В качестве шаблона я выбираю Web Application (MVC)

Готово, веб проект создан.
Шаг 2: Создание Data проекта
Приступим к созданию Data проекта, где будет хранится наш контекст и Entities.
Создаем class library проект:

Называем наш проект "data"

Шаг 3: Генерация DB контекста
Теперь нам нужно используя Package Manger Console сгенерировать контекст который нами будет использоваться в качестве "черновика".
Для этого мы воспользуемся командой c PMC. Но перед этим нам нужно установить необходимые nuget пакеты:

Install-Package Microsoft.EntityFrameworkCore.Tools
Install-Package Microsoft.EntityFrameworkCore.SqlServer
Install-Package Microsoft.AspNetCore.Identity.EntityFrameworkCore
После установки этих пакетов, выполним генерацию Db контекста и наших моделей с помощью команды:
Scaffold-DbContext "Data Source=localhost;Initial Catalog=migration-test;Integrated Security=True;MultipleActiveResultSets=True" Microsoft.EntityFrameworkCore.SqlServer -OutputDir MigrationEntities
где Data Source это connectionString к нашей локальной базе, которая использовалась в MVC проекте.
В результате у нас должен был сгенерироваться контекст и entities.

Шаг 4: Nuget пакеты для веб проекта
Добавим в конфигурацию connectionstrings для нашей базы
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Initial Catalog=migration-test;Integrated Security=True;MultipleActiveResultSets=True"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}
И установим недостающие nuget пакеты:
Install-Package Microsoft.AspNetCore
Install-Package Microsoft.AspNetCore.Identity
Install-Package Microsoft.EntityFrameworkCore
Install-Package Microsoft.AspNetCore.Identity.EntityFrameworkCore
Install-Package Microsoft.EntityFrameworkCore.SqlServer
Install-Package Microsoft.EntityFrameworkCore.Design
Шаг 5: Перенос Entities и DB контекста в дата проекте
Начинаем переносить Entity в новый проект. Просто скопируем папку Entities с нашего старого проекта, в наш новый .NET Core дата проект
Удаляем GenerateUserIdentityAsync метод с  ApplicationUser класса и пока закомментируем Hobbies Entity
public class ApplicationUser : IdentityUser
    {
        //public ICollection<Hobby> Hobbies { get; set; }
    }
Переносим наш Db контекст с старого проекта и модифицируем его, чтобы он выглядел таким образом:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityRole, string>
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.Entity<ApplicationUser>(entity =>
            {
                entity.Property(e => e.UserName)
                    .IsRequired()
                    .HasMaxLength(256);
            });
        }
    }
Для того, чтобы старые пользователи корректно логинились при новой схеме. Нужно добавить обработчик старого хэша и мигратор на новый тип хэша.
Создадим класс:
public class OldMvcPasswordHasher : PasswordHasher<ApplicationUser>
    {
        public override PasswordVerificationResult VerifyHashedPassword(ApplicationUser user, string hashedPassword, string providedPassword)
        {
            // if it's the new algorithm version, delegate the call to parent class
            if (user.HashVersion == PasswordHashVersion.Core)
                return base.VerifyHashedPassword(user, hashedPassword, providedPassword);
            byte[] buffer4;
            if (hashedPassword == null)
            {
                return PasswordVerificationResult.Failed;
            }
            if (providedPassword == null)
            {
                throw new ArgumentNullException("providedPassword");
            }
            byte[] src = Convert.FromBase64String(hashedPassword);
            if ((src.Length != 0x31) || (src[0] != 0))
            {
                return PasswordVerificationResult.Failed;
            }
            byte[] dst = new byte[0x10];
            Buffer.BlockCopy(src, 1, dst, 0, 0x10);
            byte[] buffer3 = new byte[0x20];
            Buffer.BlockCopy(src, 0x11, buffer3, 0, 0x20);
            using (Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(providedPassword, dst, 0x3e8))
            {
                buffer4 = bytes.GetBytes(0x20);
            }
            if (AreHashesEqual(buffer3, buffer4))
            {
                user.HashVersion = PasswordHashVersion.Core;
                return PasswordVerificationResult.SuccessRehashNeeded;
            }
            return PasswordVerificationResult.Failed;
        }
        private bool AreHashesEqual(byte[] firstHash, byte[] secondHash)
        {
            int minHashLength = firstHash.Length <= secondHash.Length ? firstHash.Length : secondHash.Length;
            var xor = firstHash.Length ^ secondHash.Length;
            for (int i = 0; i < minHashLength; i++)
                xor |= firstHash[i] ^ secondHash[i];
            return 0 == xor;
        }
    }
И Enum
public enum PasswordHashVersion
    {
        OldMvc,
        Core
    }
Теперь добавим в ApplicationUser новое проперти:
public class ApplicationUser : IdentityUser
    {
        public PasswordHashVersion HashVersion { get; set; }
        //public ICollection<Hobby> Hobbies { get; set; }
    }
Шаг 6: Конфигурация web проекта
Конфигурируем наш Startup.cs:
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
        public IConfiguration Configuration { get; }
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<CookiePolicyOptions>(options =>
            {
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(
                    Configuration.GetConnectionString("DefaultConnection")));
            services.AddIdentity<ApplicationUser, IdentityRole>(options =>
            {
                options.Password.RequireDigit = true;
                options.Password.RequiredLength = 6;
                options.Password.RequireNonAlphanumeric = false;
                options.Password.RequireUppercase = false;
                options.Password.RequireLowercase = false;
                options.User.AllowedUserNameCharacters =
                    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
                options.User.RequireUniqueEmail = true;
            }).AddEntityFrameworkStores<ApplicationDbContext>();
            services.AddScoped<SignInManager<ApplicationUser>, SignInManager<ApplicationUser>>();
            services.Replace(new ServiceDescriptor(
                serviceType: typeof(IPasswordHasher<ApplicationUser>),
                implementationType: typeof(OldMvcPasswordHasher),
                ServiceLifetime.Scoped));
            services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie();
            services.AddMvc(options => options.EnableEndpointRouting = false).SetCompatibilityVersion(CompatibilityVersion.Latest);
        }
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                app.UseHsts();
            }
            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseAuthentication();
            app.UseAuthorization();
            app.UseRouting();
           
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
Program.cs
public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }
        public static IWebHostBuilder CreateHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseIISIntegration()
                .UseStartup<Startup>();
    }
Шаг 7: Миграция базы данных
Следующим шагом у нас будет миграция нашей базы данных, для этого нам нужно выполнить несколько sql скриптов:
Добавляем нужные таблицы:
/*
	Generate new tables AspNetRoleClaims,AspNetUserTokens
*/
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[AspNetRoleClaims](
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[ClaimType] [nvarchar](max) NULL,
	[ClaimValue] [nvarchar](max) NULL,
	[RoleId] [nvarchar](450) NOT NULL,
 CONSTRAINT [PK_AspNetRoleClaims] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
/****** Object:  Table [dbo].[AspNetUserTokens]    Script Date: 17/5/2018 7:25:29 AM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[AspNetUserTokens](
	[UserId] [nvarchar](450) NOT NULL,
	[LoginProvider] [nvarchar](450) NOT NULL,
	[Name] [nvarchar](450) NOT NULL,
	[Value] [nvarchar](max) NULL,
 CONSTRAINT [PK_AspNetUserTokens] PRIMARY KEY CLUSTERED 
(
	[UserId] ASC,
	[LoginProvider] ASC,
	[Name] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
Добавляем таблицу миграций .NET Core и первую миграцию:
GO
/* Object:  Table [dbo].[__EFMigrationsHistory]   */
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[__EFMigrationsHistory](
	[MigrationId] [nvarchar](150) NOT NULL,
	[ProductVersion] [nvarchar](32) NOT NULL,
 CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY CLUSTERED 
(
	[MigrationId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
INSERT [dbo].[__EFMigrationsHistory] ([MigrationId], [ProductVersion]) VALUES (N'00000000000000_CreateIdentitySchema', N'2.0.2-rtm-10011')
GO
Меняем структуру таблиц пользователей, и ролей:
/*
	AspNetUsers table changes
*/ 
/*
	AspNetUsers table changes
*/
GO
DROP INDEX [UserNameIndex] ON [AspNetUsers];
GO
EXEC sp_rename N'AspNetUsers.LockoutEndDateUtc', N'LockoutEnd', N'COLUMN';
GO
ALTER TABLE [AspNetUsers] ADD [ConcurrencyStamp] nvarchar(max) NULL;
GO
ALTER TABLE [AspNetUsers] ADD [NormalizedEmail] nvarchar(256) NULL;
GO
ALTER TABLE [AspNetUsers] ADD [NormalizedUserName] nvarchar(256) NULL;
GO
drop index [EmailIndex] on [AspNetUsers]
CREATE INDEX [EmailIndex] ON [AspNetUsers] ([NormalizedEmail]);
GO
drop index [UserNameIndex] on [AspNetUsers]
CREATE UNIQUE INDEX [UserNameIndex] ON [AspNetUsers] ([NormalizedUserName]) WHERE [NormalizedUserName] IS NOT NULL;
GO
/*
	AspNetRoles table changes
*/
GO
DROP INDEX [RoleNameIndex] ON [AspNetRoles];
GO
ALTER TABLE [AspNetRoles] ADD [ConcurrencyStamp] nvarchar(max) NULL;
GO
ALTER TABLE [AspNetRoles] ADD [NormalizedName] nvarchar(256) NULL;
GO
drop index [RoleNameIndex] on [AspNetRoles]
CREATE UNIQUE INDEX [RoleNameIndex] ON [AspNetRoles] ([NormalizedName]) WHERE [NormalizedName] IS NOT NULL;
GO
И последним скриптом обновляем пользователей:
UPDATE AspNetUsers SET NormalizedEmail = UPPER(Email), NormalizedUserName = UPPER(username)
ALTER TABLE [AspNetUsers]
ADD HashVersion int NOT NULL DEFAULT(0)
Шаг 8: Вход пользователей
Добавим контроллер для входа пользователей:
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using data.Entities;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
namespace web.Controllers
{
    public class AccountController : Microsoft.AspNetCore.Mvc.Controller
    {
        private readonly SignInManager<ApplicationUser> _signInManager;
        public AccountController(SignInManager<ApplicationUser> signInManager)
        {
            _signInManager = signInManager;
        }
        [HttpGet]
        public IActionResult Login(string returnUrl = null)
        {
            return View(new LoginModel { ReturnUrl = returnUrl });
        }
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Login(LoginModel model)
        {
            if (ModelState.IsValid)
            {
                var result =
                    await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, false);
                if (result.Succeeded)
                {
                    if (model.ReturnUrl != null)
                    {
                        return LocalRedirect(model.ReturnUrl);
                    }
                    return RedirectToAction("Index", "Home");
                }
                ModelState.AddModelError("", "Неправильный логин и (или) пароль");
            }
            return View(model);
        }
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Logout()
        {
            await _signInManager.SignOutAsync();
            return RedirectToAction("Index", "Home");
        }
    }
    public class LoginModel
    {
        [Required]
        [Display(Name = "Email")]
        public string Email { get; set; }
        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Пароль")]
        public string Password { get; set; }
        [Display(Name = "Запомнить меня?")]
        public bool RememberMe { get; set; }
        public string ReturnUrl { get; set; }
    }
}
Создадим View для логина:
@model web.Controllers.LoginModel
<partial name="_ValidationScriptsPartial"/>
<h2>Вход в приложение</h2>
<form method="post" asp-controller="Account" asp-action="Login"
      asp-route-returnUrl="@Model.ReturnUrl">
    <div asp-validation-summary="ModelOnly"></div>
    <div>
        <label asp-for="Email"></label><br />
        <input asp-for="Email" />
        <span asp-validation-for="Email"></span>
    </div>
    <div>
        <label asp-for="Password"></label><br />
        <input asp-for="Password" />
        <span asp-validation-for="Password"></span>
    </div>
    <div>
        <label asp-for="RememberMe"></label><br />
        <input asp-for="RememberMe" />
    </div>
    <div>
        <input type="submit" value="Войти" />
    </div>
</form>
Модифицируем Layout, добавим в него индикатор того, что юзер успешно залогинился в систему:
  @if(User.Identity.IsAuthenticated)
    {
        <p>@User.Identity.Name</p>
        <form method="post" asp-controller="Account" asp-action="Logout">
            <input type="submit" value="Выход" />
        </form>
    }
    else
    {
        <a asp-controller="Account" asp-action="Login">Вход</a>
        <a asp-controller="Account" asp-action="Register">Регистрация</a>
    } 
Запускаем проект и проверяем работает ли вход.

Вводим данные которые мы использовали для входа пользователя в Old MVC приложении

Отлично, как видим все заработало.
Шаг 9: Миграция Many-to-Many
Для того, чтобы мигрировать many-to-many таблицы, необходимо сделать несколько манипуляций, EntityFrameworkCore уже не работает по той же схеме как и EF с .NET framework. Теперь для этого нужно создавать отдельную Entity. Обратимся к папке MigrationEntities, которую мы сгенерировали на шаге 3. Возмем оттуда файл ApplicationUserHobbies.cs и модифицируем его следующим образом:
public  class ApplicationUserHobbies
    {
        public string ApplicationUserId { get; set; }
        public Guid HobbyId { get; set; }
        public virtual ApplicationUser ApplicationUser { get; set; }
        public virtual Hobby Hobby { get; set; }
    }
А entity хобби так:
public class Hobby : BaseEntity
    {
        public string Name { get; set; }
        public virtual ICollection<ApplicationUserHobbies> ApplicationUserHobbies { get; set; }
    }
ApplicationUser:
public class ApplicationUser : IdentityUser
    {
        public PasswordHashVersion HashVersion { get; set; }
        public ICollection<ApplicationUserHobbies> ApplicationUserHobbies { get; set; }
    }
Теперь добавим таблицы и модифицируем OnModelCreating с помощью контекста. который нам автоматически сгенерировал EF на шаге 3.
Наш ApplicationDbContext должен выглядеть следующим образом:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityRole, string>
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }
        public virtual DbSet<Hobby> Hobbies { get; set; }
        public virtual DbSet<ApplicationUserHobbies> ApplicationUserHobbies { get; set; }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.Entity<ApplicationUser>(entity =>
            {
                entity.Property(e => e.UserName)
                    .IsRequired()
                    .HasMaxLength(256);
            });
            modelBuilder.Entity<ApplicationUserHobbies>(entity =>
            {
                entity.HasKey(e => new { e.ApplicationUserId, e.HobbyId })
                    .HasName("PK_dbo.ApplicationUserHobbies");
                entity.HasIndex(e => e.ApplicationUserId)
                    .HasName("IX_ApplicationUser_Id");
                entity.HasIndex(e => e.HobbyId)
                    .HasName("IX_Hobby_Id");
                entity.Property(e => e.ApplicationUserId)
                    .HasColumnName("ApplicationUser_Id")
                    .HasMaxLength(128);
                entity.Property(e => e.HobbyId).HasColumnName("Hobby_Id");
                entity.HasOne(d => d.ApplicationUser)
                    .WithMany(p => p.ApplicationUserHobbies)
                    .HasForeignKey(d => d.ApplicationUserId)
                    .HasConstraintName("FK_dbo.ApplicationUserHobbies_dbo.AspNetUsers_ApplicationUser_Id");
                entity.HasOne(d => d.Hobby)
                    .WithMany(p => p.ApplicationUserHobbies)
                    .HasForeignKey(d => d.HobbyId)
                    .HasConstraintName("FK_dbo.ApplicationUserHobbies_dbo.Hobbies_Hobby_Id");
            });
            modelBuilder.Entity<Hobbies>(entity =>
            {
                entity.Property(e => e.Id).ValueGeneratedNever();
                entity.Property(e => e.AddedDate).HasColumnType("datetime");
            });
        }
    }
Шаг 10: Работа с Many-to-Many
Для того, чтобы проверить работает ли контекст после наших манипуляций, давайте создадим контроллер, в котором выведем хобби пользователя:
[Authorize]
    public class HobbiesController : Controller
    {
        private readonly ApplicationDbContext _context;
        public HobbiesController(ApplicationDbContext context)
        {
            _context = context;
        }
        public IActionResult Index()
        {
            var hobbies =  _context.ApplicationUserHobbies
                .Include(userHobbies => userHobbies.Hobby)
                .Include(userHobbies => userHobbies.ApplicationUser)
                .Where(userHobbies => userHobbies.ApplicationUser.UserName == User.Identity.Name)
                .Select(userHobbies => new HobbyDto
                {
                    Id = userHobbies.Hobby.Id,
                    Name =  userHobbies.Hobby.Name
                }).ToList();
            return View(hobbies);
        }
    }
    public class HobbyDto
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
    }
View
@model System.Collections.Generic.List<web.Controllers.HobbyDto>
<h1>Hobbies:</h1>
@foreach (var item in Model)
{
    <p>@item.Name</p>
}
Запускаем проект и переходим на страницу /hobbies

Как видим, все работает, путем нехитрых манипуляций мы мигрировали пользователей и роли ASP.NET MVC на ASP.NET Core 3.0.
Исходники проекта можно найти тут.