65.9K
CodeProject 正在变化。 阅读更多。
Home

ASP.NET Core 1.1 和 Identity 的探索之旅

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2018年11月13日

CPOL

8分钟阅读

viewsIcon

5701

ASP.NET Core 1.1 和 Identity 的探索之旅

引言

最近,我和一位朋友尝试在不同的数据库模式下,并结合一个遗留应用程序的自定义表来实现 ASP.NET Core 1.1 的 Identity 模式,经过一些反复试验,我们成功了。

在这篇文章中,我将分享我们采取的步骤以及我们是如何解决这个问题的。希望对您有所帮助 :D。

设置

首先,我们从现有的旧数据库开始,并添加了一个新的模式来区分业务实体和安全实体。以下是在 SQL Server 中设置表的脚本。

CREATE SCHEMA [Security]
GO

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [Security].[Client](
    [ID] [nvarchar](450) NOT NULL,
    [AccessFailedCount] [int] NOT NULL,
    [CNP] [nvarchar](30) NULL,
    [ConcurrencyStamp] [nvarchar](max) NULL,
    [Email] [nvarchar](256) NULL,
    [EmailConfirmed] [bit] NOT NULL,
    [FirstName] [nvarchar](50) NOT NULL,
    [LastName] [nvarchar](50) NOT NULL,
    [LockoutEnabled] [bit] NOT NULL,
    [LockoutEnd] [datetimeoffset](7) NULL,
    [NormalizedEmail] [nvarchar](256) NULL,
    [NormalizedUserName] [nvarchar](256) NULL,
    [NumarCI] [nvarchar](20) NULL,
    [PasswordHash] [nvarchar](max) NULL,
    [PhoneNumber] [nvarchar](max) NULL,
    [PhoneNumberConfirmed] [bit] NOT NULL,
    [RegDate] [datetime2](7) NOT NULL,
    [SecurityStamp] [nvarchar](max) NULL,
    [SerieCI] [nvarchar](2) NULL,
    [TwoFactorEnabled] [bit] NOT NULL,
    [UserName] [nvarchar](256) NULL,
 CONSTRAINT [PK_Client] 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

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [Security].[ClientClaim](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [ClaimType] [nvarchar](max) NULL,
    [ClaimValue] [nvarchar](max) NULL,
    [ClientID] [nvarchar](450) NOT NULL,
 CONSTRAINT [PK_ClientClaim] 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

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [Security].[ClientRole](
    [ClientID] [nvarchar](450) NOT NULL,
    [RoleID] [nvarchar](450) NOT NULL,
 CONSTRAINT [PK_ClientRole] PRIMARY KEY CLUSTERED 
(
    [ClientID] ASC,
    [RoleID] 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

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [Security].[ClientToken](
    [ClientID] [nvarchar](450) NOT NULL,
    [LoginProvider] [nvarchar](450) NOT NULL,
    [Name] [nvarchar](450) NOT NULL,
    [Value] [nvarchar](max) NULL,
 CONSTRAINT [PK_ClientToken] PRIMARY KEY CLUSTERED 
(
    [ClientID] 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

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [Security].[ExternalLogin](
    [LoginProvider] [nvarchar](450) NOT NULL,
    [ProviderKey] [nvarchar](450) NOT NULL,
    [ProviderDisplayName] [nvarchar](max) NULL,
    [ClientID] [nvarchar](450) NOT NULL,
 CONSTRAINT [PK_ExternalLogin] PRIMARY KEY CLUSTERED 
(
    [LoginProvider] ASC,
    [ProviderKey] 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

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [Security].[Role](
    [ID] [nvarchar](450) NOT NULL,
    [ConcurrencyStamp] [nvarchar](max) NULL,
    [Name] [nvarchar](256) NULL,
    [NormalizedName] [nvarchar](256) NULL,
 CONSTRAINT [PK_Role] 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

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [Security].[RoleClaim](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [ClaimType] [nvarchar](max) NULL,
    [ClaimValue] [nvarchar](max) NULL,
    [RoleID] [nvarchar](450) NOT NULL,
 CONSTRAINT [PK_RoleClaim] 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
SET ANSI_PADDING ON

GO

CREATE NONCLUSTERED INDEX [EmailIndex] ON [Security].[Client]
(
    [NormalizedEmail] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, _
 DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
SET ANSI_PADDING ON

GO

CREATE UNIQUE NONCLUSTERED INDEX [UserNameIndex] ON [Security].[Client]
(
    [NormalizedUserName] ASC
)
WHERE ([NormalizedUserName] IS NOT NULL)
WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, _
 IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, _
 ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
SET ANSI_PADDING ON

GO

CREATE NONCLUSTERED INDEX [IX_ClientClaim_ClientID] ON [Security].[ClientClaim]
(
    [ClientID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, _
 DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
SET ANSI_PADDING ON

GO

CREATE NONCLUSTERED INDEX [IX_ClientRole_RoleID] ON [Security].[ClientRole]
(
    [RoleID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, _
 DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
SET ANSI_PADDING ON

GO

CREATE NONCLUSTERED INDEX [IX_ExternalLogin_ClientID] ON [Security].[ExternalLogin]
(
    [ClientID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, _
 DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
SET ANSI_PADDING ON

GO

CREATE UNIQUE NONCLUSTERED INDEX [RoleNameIndex] ON [Security].[Role]
(
    [NormalizedName] ASC
)
WHERE ([NormalizedName] IS NOT NULL)
WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, _
 IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, _
 ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
SET ANSI_PADDING ON

GO

CREATE NONCLUSTERED INDEX [IX_RoleClaim_RoleID] ON [Security].[RoleClaim]
(
    [RoleID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, _
 DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
ALTER TABLE [Security].[Client] ADD  DEFAULT (getdate()) FOR [RegDate]
GO
ALTER TABLE [Security].[ClientClaim]  WITH CHECK ADD  _
 CONSTRAINT [FK_ClientClaim_Client_ClientID] FOREIGN KEY([ClientID])
REFERENCES [Security].[Client] ([ID])
ON DELETE CASCADE
GO
ALTER TABLE [Security].[ClientClaim] CHECK CONSTRAINT [FK_ClientClaim_Client_ClientID]
GO
ALTER TABLE [Security].[ClientRole]  WITH CHECK ADD  _
 CONSTRAINT [FK_ClientRole_Client_ClientID] FOREIGN KEY([ClientID])
REFERENCES [Security].[Client] ([ID])
ON DELETE CASCADE
GO
ALTER TABLE [Security].[ClientRole] CHECK CONSTRAINT [FK_ClientRole_Client_ClientID]
GO
ALTER TABLE [Security].[ClientRole]  WITH CHECK ADD  _
 CONSTRAINT [FK_ClientRole_Role_RoleID] FOREIGN KEY([RoleID])
REFERENCES [Security].[Role] ([ID])
ON DELETE CASCADE
GO
ALTER TABLE [Security].[ClientRole] CHECK CONSTRAINT [FK_ClientRole_Role_RoleID]
GO
ALTER TABLE [Security].[ExternalLogin]  WITH CHECK ADD  _
 CONSTRAINT [FK_ExternalLogin_Client_ClientID] FOREIGN KEY([ClientID])
REFERENCES [Security].[Client] ([ID])
ON DELETE CASCADE
GO
ALTER TABLE [Security].[ExternalLogin] CHECK CONSTRAINT [FK_ExternalLogin_Client_ClientID]
GO
ALTER TABLE [Security].[RoleClaim]  WITH CHECK ADD  _
 CONSTRAINT [FK_RoleClaim_Role_RoleID] FOREIGN KEY([RoleID])
REFERENCES [Security].[Role] ([ID])
ON DELETE CASCADE
GO
ALTER TABLE [Security].[RoleClaim] CHECK CONSTRAINT [FK_RoleClaim_Role_RoleID]
GO

现在,如果我们查看这个模式,可以看到它与普通的 identity 表是相同的,只是它们有不同的表名,它们属于不同的数据库模式(正如应该的那样),并且 `Client` 表(代表以前的 `AspNetUsers` 表)有 3 个应用程序所需的全新列。

因此,我们首先对数据库进行了更改,因为在这次应用程序迁移中,这是最容易修改的部分。接下来,我们创建了一个新的 ASP.NET Core 1.1 解决方案,并将身份验证设置为 **Individual User Accounts**。

更改

接下来,我们删除了模板项目自带的位于 **Data->Migrations** 目录下的迁移文件,以防止它们干扰我们的工作。

所以现在,我们有了一个为新需求修改过的旧数据库,以及一个全新创建的 ASP.NET Core 1.1 应用程序。在这个应用程序中,我们想从现有数据库进行 Code-First 开发。于是,我们执行了以下步骤:

  1. 打开了程序包管理器控制台
  2. 运行了以下命令,将 "" 部分替换为我们旧数据库的连接字符串:**Scaffold-DbContext -Connection -Provider "Microsoft.EntityFrameworkCore.SqlServer" -o "Models\" -Schemas "dbo","Security”**
  3. 删除了 `ApplicationUser` 类,因为我们将为此目的使用我们自己的类。

现在模型已经创建到我们的 `Models` 文件夹中,我们需要进行以下更新。我们将逐个类地进行,展示从导入时的样子到完成后会是什么样子。

Client.cs

当我们导入模型时,`Client` 类最终看起来是这样的:

using System;
using System.Collections.Generic;

namespace WebApplication1.Models
{
    public partial class Client
    {
        public Client()
        {
            ClientClaim = new HashSet<ClientClaim>();
            ClientRole = new HashSet<ClientRole>();
            ExternalLogin = new HashSet<ExternalLogin>();
        }

        public string Id { get; set; }
        public int AccessFailedCount { get; set; }
        public string Cnp { get; set; }
        public string ConcurrencyStamp { get; set; }
        public string Email { get; set; }
        public bool EmailConfirmed { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public bool LockoutEnabled { get; set; }
        public DateTimeOffset? LockoutEnd { get; set; }
        public string NormalizedEmail { get; set; }
        public string NormalizedUserName { get; set; }
        public string NumarCi { get; set; }
        public string PasswordHash { get; set; }
        public string PhoneNumber { get; set; }
        public bool PhoneNumberConfirmed { get; set; }
        public DateTime RegDate { get; set; }
        public string SecurityStamp { get; set; }
        public string SerieCi { get; set; }
        public bool TwoFactorEnabled { get; set; }
        public string UserName { get; set; }

        public virtual ICollection<ClientClaim> ClientClaim { get; set; }
        public virtual ICollection<ClientRole> ClientRole { get; set; }
        public virtual ICollection<ExternalLogin> ExternalLogin { get; set; }
    }
}

但正如我们之前所说,这将是我们用户的模型,因此该类需要实现 `IdentityUser` 接口。但是,由于我们正在创建所有表,我们还需要指定其他依赖类的类型以及 `Id` 属性的类型。最终,该类看起来是这样的:

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;

namespace WebApplication1.Models
{
    public class Client : IdentityUser<string, ClientClaim, ClientRole, ExternalLogin>
    {
        public Client()
        {
            ClientClaim = new HashSet<ClientClaim>();
            ClientRole = new HashSet<ClientRole>();
            ExternalLogin = new HashSet<ExternalLogin>();
        }

        public string Cnp { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string NumarCi { get; set; }
        public DateTime RegDate { get; set; }
        public string SerieCi { get; set; }

        public virtual ICollection<ClientClaim> ClientClaim { get; set; }
        public virtual ICollection<ClientRole> ClientRole { get; set; }
        public virtual ICollection<ExternalLogin> ExternalLogin { get; set; }
    }
}

请注意,从旧实现中保留的唯一属性是我们添加到用户表中的自定义列以及新自定义类型的导航属性。

ClientClaim.cs

这个类(以及其他一些支持类)除了名称不同之外,与框架中默认实现的没有任何变化。

using System;
using System.Collections.Generic;

namespace WebApplication1.Models
{
    public partial class ClientClaim
    {
        public int Id { get; set; }
        public string ClaimType { get; set; }
        public string ClaimValue { get; set; }
        public string ClientId { get; set; }

        public virtual Client Client { get; set; }
    }
}

更改为这样:

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;

namespace WebApplication1.Models
{
    public class ClientClaim : IdentityUserClaim<string>
    {
        public string ClientId { get; set; }

        public virtual Client Client { get; set; }
    }
}

ClientRole.cs

从这个:

using System;
using System.Collections.Generic;

namespace WebApplication1.Models
{
    public partial class ClientRole
    {
        public string ClientId { get; set; }
        public string RoleId { get; set; }

        public virtual Client Client { get; set; }
        public virtual Role Role { get; set; }
    }
}

变为这样:

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;

namespace WebApplication1.Models
{
    public class ClientRole : IdentityUserRole<string>
    {
        public string ClientId { get; set; }

        public virtual Client Client { get; set; }
        public virtual Role Role { get; set; }
    }
}

ClientToken.cs

从这个:

using System;
using System.Collections.Generic;

namespace WebApplication1.Models
{
    public partial class ClientToken
    {
        public string ClientId { get; set; }
        public string LoginProvider { get; set; }
        public string Name { get; set; }
        public string Value { get; set; }
    }
}

变为这样:

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;

namespace WebApplication1.Models
{
    public class ClientToken : IdentityUserToken<string>
    {
        public string ClientId { get; set; }
    }
}

ExternalLogin.cs

从这个:

using System;
using System.Collections.Generic;

namespace WebApplication1.Models
{
    public partial class ExternalLogin
    {
        public string LoginProvider { get; set; }
        public string ProviderKey { get; set; }
        public string ProviderDisplayName { get; set; }
        public string ClientId { get; set; }

        public virtual Client Client { get; set; }
    }
}

变为这样:

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;

namespace WebApplication1.Models
{
    public class ExternalLogin : IdentityUserLogin<string>
    {
        public string ClientId { get; set; }

        public virtual Client Client { get; set; }
    }
}

Role.cs

从这个:

using System;
using System.Collections.Generic;

namespace WebApplication1.Models
{
    public partial class Role
    {
        public Role()
        {
            ClientRole = new HashSet<ClientRole>();
            RoleClaim = new HashSet<RoleClaim>();
        }

        public string Id { get; set; }
        public string ConcurrencyStamp { get; set; }
        public string Name { get; set; }
        public string NormalizedName { get; set; }

        public virtual ICollection<ClientRole> ClientRole { get; set; }
        public virtual ICollection<RoleClaim> RoleClaim { get; set; }
    }
}

变为这样:

using System.Collections.Generic;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;

namespace WebApplication1.Models
{
    public class Role : IdentityRole<string, ClientRole, RoleClaim>
    {
        public Role()
        {
            ClientRole = new HashSet<ClientRole>();
            RoleClaim = new HashSet<RoleClaim>();
        }

        public virtual ICollection<ClientRole> ClientRole { get; set; }
        public virtual ICollection<RoleClaim> RoleClaim { get; set; }
    }
}

请注意,在这种情况下,就像对于 `Client` 一样,我们不仅实现了 `IdentityRole`,而且使用了带有泛型的形式来处理其依赖类。

最后...

RoleClaims.cs

从这个:

using System;
using System.Collections.Generic;

namespace WebApplication1.Models
{
    public partial class RoleClaim
    {
        public int Id { get; set; }
        public string ClaimType { get; set; }
        public string ClaimValue { get; set; }
        public string RoleId { get; set; }

        public virtual Role Role { get; set; }
    }
}

变为这样:

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;

namespace WebApplication1.Models
{
    public class RoleClaim : IdentityRoleClaim<string>
    {
        public virtual Role Role { get; set; }
    }
}

呼——!但我们还没完成。当我们导入模型时,创建了一个新的数据库上下文。如果您使用了我们之前的命令,它将被命名为数据库名称加上 `Context` 后缀,并放在同一个 `models` 文件夹中。

更新数据库上下文

现在我们需要做以下事情:

  • 将此上下文中创建的所有属性(`DbSet`)移动到项目中模板自带的默认数据库上下文 `ApplicationDbContext` 中。
  • 完全移动 `OnModelCreating` 方法。
  • 删除创建的上下文,因为我们已经从中获得了所有需要的内容(可以忽略 `OnConfiguring` 方法,因为 `ApplicationDbContext` 已经在 `Startup.cs` 中接收连接字符串)。

现在我们需要对 `ApplicationDbContext.cs` 文件进行一些更新。最初它看起来是这样的:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using WebApplication1.Models;

namespace WebApplication1.Data
{
    public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
            // Customize the ASP.NET Identity model and override the defaults if needed.
            // For example, you can rename the ASP.NET Identity table names and more.
            // Add your customizations after calling base.OnModelCreating(builder);
        }
    }
}

除了我们从生成的上下文中添加的更新外,我们还需要将接口从 `IdentityDbContext` 更改为更明确的接口,声明我们所有的自定义类。最终,它看起来会是这样的:

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using WebApplication1.Models;

namespace WebApplication1.Data
{
    public class ApplicationDbContext : IdentityDbContext<Client, Role, 
           string, ClientClaim, ClientRole, ExternalLogin, RoleClaim, ClientToken>
    {
        public virtual DbSet<Client> Client { get; set; }
        public virtual DbSet<ClientClaim> ClientClaim { get; set; }
        public virtual DbSet<ClientRole> ClientRole { get; set; }
        public virtual DbSet<ClientToken> ClientToken { get; set; }
        public virtual DbSet<ExternalLogin> ExternalLogin { get; set; }
        public virtual DbSet<Role> Role { get; set; }
        public virtual DbSet<RoleClaim> RoleClaim { get; set; }


        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Client>(entity =>
            {
                entity.ToTable("Client", "Security");

                entity.HasIndex(e => e.NormalizedEmail)
                    .HasName("EmailIndex");

                entity.HasIndex(e => e.NormalizedUserName)
                    .HasName("UserNameIndex")
                    .IsUnique();

                entity.Property(e => e.Id)
                    .HasColumnName("ID")
                    .HasMaxLength(450);

                entity.Property(e => e.Cnp)
                    .HasColumnName("CNP")
                    .HasMaxLength(30);

                entity.Property(e => e.Email).HasMaxLength(256);

                entity.Property(e => e.FirstName)
                    .IsRequired()
                    .HasMaxLength(50);

                entity.Property(e => e.LastName)
                    .IsRequired()
                    .HasMaxLength(50);

                entity.Property(e => e.NormalizedEmail).HasMaxLength(256);

                entity.Property(e => e.NormalizedUserName)
                    .IsRequired()
                    .HasMaxLength(256);

                entity.Property(e => e.NumarCi)
                    .HasColumnName("NumarCI")
                    .HasMaxLength(20);

                entity.Property(e => e.RegDate).HasDefaultValueSql("getdate()");

                entity.Property(e => e.SerieCi)
                    .HasColumnName("SerieCI")
                    .HasMaxLength(2);

                entity.Property(e => e.UserName).HasMaxLength(256);
            });

            modelBuilder.Entity<ClientClaim>(entity =>
            {
                entity.ToTable("ClientClaim", "Security");

                entity.HasIndex(e => e.ClientId)
                    .HasName("IX_ClientClaim_ClientID");

                entity.Property(e => e.Id).HasColumnName("ID");

                entity.Property(e => e.ClientId)
                    .IsRequired()
                    .HasColumnName("ClientID")
                    .HasMaxLength(450);

                entity.HasOne(d => d.Client)
                    .WithMany(p => p.ClientClaim)
                    .HasForeignKey(d => d.ClientId);
            });

            modelBuilder.Entity<ClientRole>(entity =>
            {
                entity.HasKey(e => new { e.ClientId, e.RoleId })
                    .HasName("PK_ClientRole");

                entity.ToTable("ClientRole", "Security");

                entity.HasIndex(e => e.RoleId)
                    .HasName("IX_ClientRole_RoleID");

                entity.Property(e => e.ClientId)
                    .HasColumnName("ClientID")
                    .HasMaxLength(450);

                entity.Property(e => e.RoleId)
                    .HasColumnName("RoleID")
                    .HasMaxLength(450);

                entity.HasOne(d => d.Client)
                    .WithMany(p => p.ClientRole)
                    .HasForeignKey(d => d.ClientId);

                entity.HasOne(d => d.Role)
                    .WithMany(p => p.ClientRole)
                    .HasForeignKey(d => d.RoleId);
            });

            modelBuilder.Entity<ClientToken>(entity =>
            {
                entity.HasKey(e => new { e.ClientId, e.LoginProvider, e.Name })
                    .HasName("PK_ClientToken");

                entity.ToTable("ClientToken", "Security");

                entity.Property(e => e.ClientId)
                    .HasColumnName("ClientID")
                    .HasMaxLength(450);

                entity.Property(e => e.LoginProvider).HasMaxLength(450);

                entity.Property(e => e.Name).HasMaxLength(450);
            });

            modelBuilder.Entity<ExternalLogin>(entity =>
            {
                entity.HasKey(e => new { e.LoginProvider, e.ProviderKey })
                    .HasName("PK_ExternalLogin");

                entity.ToTable("ExternalLogin", "Security");

                entity.HasIndex(e => e.ClientId)
                    .HasName("IX_ExternalLogin_ClientID");

                entity.Property(e => e.LoginProvider).HasMaxLength(450);

                entity.Property(e => e.ProviderKey).HasMaxLength(450);

                entity.Property(e => e.ClientId)
                    .IsRequired()
                    .HasColumnName("ClientID")
                    .HasMaxLength(450);

                entity.HasOne(d => d.Client)
                    .WithMany(p => p.ExternalLogin)
                    .HasForeignKey(d => d.ClientId);
            });

            modelBuilder.Entity<Role>(entity =>
            {
                entity.ToTable("Role", "Security");

                entity.HasIndex(e => e.NormalizedName)
                    .HasName("RoleNameIndex")
                    .IsUnique();

                entity.Property(e => e.Id)
                    .HasColumnName("ID")
                    .HasMaxLength(450);

                entity.Property(e => e.Name).HasMaxLength(256);

                entity.Property(e => e.NormalizedName)
                    .IsRequired()
                    .HasMaxLength(256);
            });

            modelBuilder.Entity<RoleClaim>(entity =>
            {
                entity.ToTable("RoleClaim", "Security");

                entity.HasIndex(e => e.RoleId)
                    .HasName("IX_RoleClaim_RoleID");

                entity.Property(e => e.Id).HasColumnName("ID");

                entity.Property(e => e.RoleId)
                    .IsRequired()
                    .HasColumnName("RoleID")
                    .HasMaxLength(450);

                entity.HasOne(d => d.Role)
                    .WithMany(p => p.RoleClaim)
                    .HasForeignKey(d => d.RoleId);
            });
        }

    }
}

需要注意的一点是,这给我们带来了很大的麻烦,那就是在 `OnModelCreating` 方法中的默认行 `base.OnModelCreating(builder)`。这之所以是个麻烦,是因为我们习惯了总是调用基类的虚拟方法实现,而不去深究原因。每次我们为新数据库运行迁移时,不仅会出现我们自己的表,还会出现旧的 `AspNetXXX` 表,因为这就是在 `IdentityDbContext` 类的 `OnModelCreating` 覆盖中执行的操作。

protected override void OnModelCreating(ModelBuilder builder)
    {
      builder.Entity<TUser>((Action<EntityTypeBuilder<TUser>>) (b =>
      {
        b.HasKey((Expression<Func<TUser, object>>) (u => (object) u.Id));
        b.HasIndex((Expression<Func<TUser, object>>) 
                 (u => u.NormalizedUserName)).HasName("UserNameIndex").IsUnique(true);
        b.HasIndex((Expression<Func<TUser, object>>) (u => u.NormalizedEmail)).HasName("EmailIndex");
        b.ToTable<TUser>("AspNetUsers");
        b.Property<string>((Expression<Func<TUser, string>>) 
                       (u => u.ConcurrencyStamp)).IsConcurrencyToken(true);
        b.Property<string>((Expression<Func<TUser, string>>) 
                       (u => u.UserName)).HasMaxLength(256);
        b.Property<string>((Expression<Func<TUser, string>>) 
                       (u => u.NormalizedUserName)).HasMaxLength(256);
        b.Property<string>((Expression<Func<TUser, string>>) (u => u.Email)).HasMaxLength(256);
        b.Property<string>((Expression<Func<TUser, string>>) 
                       (u => u.NormalizedEmail)).HasMaxLength(256);
        b.HasMany<TUserClaim>((Expression<Func<TUser, IEnumerable<TUserClaim>>>) 
                       (u => u.Claims)).WithOne((string) null).HasForeignKey
                       ((Expression<Func<TUserClaim, object>>) 
                       (uc => (object) uc.UserId)).IsRequired(true);
        b.HasMany<TUserLogin>((Expression<Func<TUser, IEnumerable<TUserLogin>>>) 
                       (u => u.Logins)).WithOne((string) null).HasForeignKey
                       ((Expression<Func<TUserLogin, object>>) 
                       (ul => (object) ul.UserId)).IsRequired(true);
        b.HasMany<TUserRole>((Expression<Func<TUser, IEnumerable<TUserRole>>>) 
                       (u => u.Roles)).WithOne((string) null).HasForeignKey
                       ((Expression<Func<TUserRole, object>>) 
                       (ur => (object) ur.UserId)).IsRequired(true);
      }));
      builder.Entity<TRole>((Action<EntityTypeBuilder<TRole>>) (b =>
      {
        b.HasKey((Expression<Func<TRole, object>>) (r => (object) r.Id));
        b.HasIndex((Expression<Func<TRole, object>>) 
                      (r => r.NormalizedName)).HasName("RoleNameIndex").IsUnique(true);
        b.ToTable<TRole>("AspNetRoles");
        b.Property<string>((Expression<Func<TRole, string>>) 
                      (r => r.ConcurrencyStamp)).IsConcurrencyToken(true);
        b.Property<string>((Expression<Func<TRole, string>>) (u => u.Name)).HasMaxLength(256);
        b.Property<string>((Expression<Func<TRole, string>>) 
                      (u => u.NormalizedName)).HasMaxLength(256);
        b.HasMany<TUserRole>((Expression<Func<TRole, IEnumerable<TUserRole>>>) 
                      (r => r.Users)).WithOne((string) null).HasForeignKey
                      ((Expression<Func<TUserRole, object>>) 
                      (ur => (object) ur.RoleId)).IsRequired(true);
        b.HasMany<TRoleClaim>((Expression<Func<TRole, IEnumerable<TRoleClaim>>>) 
                      (r => r.Claims)).WithOne((string) null).HasForeignKey
                      ((Expression<Func<TRoleClaim, object>>) 
                      (rc => (object) rc.RoleId)).IsRequired(true);
      }));
      builder.Entity<TUserClaim>((Action<EntityTypeBuilder<TUserClaim>>) (b =>
      {
        b.HasKey((Expression<Func<TUserClaim, object>>) (uc => (object) uc.Id));
        b.ToTable<TUserClaim>("AspNetUserClaims");
      }));
      builder.Entity<TRoleClaim>((Action<EntityTypeBuilder<TRoleClaim>>) (b =>
      {
        b.HasKey((Expression<Func<TRoleClaim, object>>) (rc => (object) rc.Id));
        b.ToTable<TRoleClaim>("AspNetRoleClaims");
      }));
      builder.Entity<TUserRole>((Action<EntityTypeBuilder<TUserRole>>) (b =>
      {
        b.HasKey((Expression<Func<TUserRole, object>>) (r => new
        {
          UserId = r.UserId,
          RoleId = r.RoleId
        }));
        b.ToTable<TUserRole>("AspNetUserRoles");
      }));
      builder.Entity<TUserLogin>((Action<EntityTypeBuilder<TUserLogin>>) (b =>
      {
        b.HasKey((Expression<Func<TUserLogin, object>>) (l => new
        {
          LoginProvider = l.LoginProvider,
          ProviderKey = l.ProviderKey
        }));
        b.ToTable<TUserLogin>("AspNetUserLogins");
      }));
      builder.Entity<TUserToken>((Action<EntityTypeBuilder<TUserToken>>) (b =>
      {
        b.HasKey((Expression<Func<TUserToken, object>>) (l => new
        {
          UserId = l.UserId,
          LoginProvider = l.LoginProvider,
          Name = l.Name
        }));
        b.ToTable<TUserToken>("AspNetUserTokens");
      }));
    }

所以在我们的实现中,我们需要删除对基类实现的调用,以免干扰我们的迁移。

运行应用程序?

经过所有这些更改后,我们点击 **Build**,果然,在所有地方都出现了构建失败,因为我们删除了 `ApplicationUser` 类(为了简化,我们可以将 `Client` 的实现移到那个类中然后重命名,但这会有点混乱)。我们去到每个使用 `ApplicationUser` 的地方,并用我们自己的 `Client` 类替换它。

好的,现在我们构建,一切似乎都正常了,对吧?错了……我们运行应用程序,得到以下错误:

GenericArguments\[0], ‘WebApplication1.Models.Client’, 
on ‘Microsoft.AspNetCore.Identity.EntityFrameworkCore.UserStore\`4\[TUser,TRole,TContext,TKey]’ 
violates the constraint of type ‘TUser’.

当尝试在 `Startup.cs` 文件中运行以下行时:

services.AddIdentity<Client, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();

我们在这里卡了一段时间(实际上,我们在之前的几个步骤中都遇到了困难,因为我们正在做一些没有 proper 文档支持的事情),所以我们开始查阅 .NET Core 文档、StackOverflow 等,但没有找到直接的修复方法来解释为什么会发生这种情况,更不用说如何修复它了(你知道我的原则,先理解,后修复)。

所以我查看了 `AddEntityFrameworkStores()` 方法的源代码。我发现了这个:

private static IServiceCollection GetDefaultServices
   (Type userType, Type roleType, Type contextType, Type keyType = null)
    {
      Type type = keyType;
      if ((object) type == null)
        type = typeof (string);
      keyType = type;
      Type implementationType1 = typeof (UserStore<,,,>).MakeGenericType
                 (userType, roleType, contextType, keyType);
      Type implementationType2 = typeof (RoleStore<,,>).MakeGenericType(roleType, contextType, keyType);
      ServiceCollection services = new ServiceCollection();
      services.AddScoped(typeof (IUserStore<>).MakeGenericType(userType), implementationType1);
      services.AddScoped(typeof (IRoleStore<>).MakeGenericType(roleType), implementationType2);
      return (IServiceCollection) services;
    }

然后我继续查看 `UserStore` 的定义,看看我们违反了什么约束。嗯,这似乎并不明显,但默认的 `UserStore` 和 `RoleStore` 实现并不知道如何使用我们的自定义类(在后来对 Github 仓库的一些研究中,我发现了这个问题,他们提到在 .NET Core 2 的迭代中实现起来会容易得多。如果你好奇,可以在这里找到这个问题)。为了修复它,我们需要实现自己的 stores 和 managers。

实现“修复”

所以,我们接下来这样做,我们将 startup 行更新如下:

services.AddIdentity<Client, Role>()
    .AddUserStore<ApplicationUserStore>()
    .AddUserManager<ApplicationUserManager>()
    .AddRoleStore<ApplicationRoleStore>()
    .AddRoleManager<ApplicationRoleManager>()
    .AddSignInManager<ApplicationSignInManager>()
    //.AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();

正如你所见,我们需要添加自定义的 stores 和 managers(我们将在下一步展示它们的实现),并注释掉了 `AddEntityFrameworkStores` 行(保留在这里是为了强调需要删除它)。

接下来,这是每个自定义类的实现:

using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using WebApplication1.Models;

namespace WebApplication1.Data
{
    public class ApplicationRoleManager : RoleManager<Role>
    {
        public ApplicationRoleManager(IRoleStore<Role> store, 
            IEnumerable<IRoleValidator<Role>> roleValidators, 
            ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, 
            ILogger<RoleManager<Role>> logger, IHttpContextAccessor contextAccessor) 
            : base(store, roleValidators, keyNormalizer, errors, logger, contextAccessor)
        {
        }
    }
}
using System.Security.Claims;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using WebApplication1.Models;

namespace WebApplication1.Data
{
    public class ApplicationRoleStore : RoleStore<Role, ApplicationDbContext, 
                      string, ClientRole, RoleClaim>
    {
        public ApplicationRoleStore(ApplicationDbContext context, 
                      IdentityErrorDescriber describer = null) : base(context, describer)
        {
        }

        protected override RoleClaim CreateRoleClaim(Role role, Claim claim)
        {
            var roleClaim = new RoleClaim
            {
                Role = role,
                RoleId = role.Id
            };
            roleClaim.InitializeFromClaim(claim);
            return roleClaim;
        }
    }
}
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using WebApplication1.Models;

namespace WebApplication1.Data
{
    public class ApplicationSignInManager : SignInManager<Client>
    {
        public ApplicationSignInManager(UserManager<Client> userManager, 
                 IHttpContextAccessor contextAccessor, 
                 IUserClaimsPrincipalFactory<Client> claimsFactory, 
                 IOptions<IdentityOptions> optionsAccessor, ILogger<SignInManager<Client>> logger) 
            : base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger)
        {
        }
    }
}
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using WebApplication1.Models;

namespace WebApplication1.Data
{
    public class ApplicationUserManager : UserManager<Client>
    {
        public ApplicationUserManager(IUserStore<Client> store, 
              IOptions<IdentityOptions> optionsAccessor, 
              IPasswordHasher<Client> passwordHasher, 
              IEnumerable<IUserValidator<Client>> userValidators, 
              IEnumerable<IPasswordValidator<Client>> passwordValidators, 
              ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, 
              IServiceProvider services, ILogger<UserManager<Client>> logger) 
            : base(store, optionsAccessor, passwordHasher, userValidators, 
                     passwordValidators, keyNormalizer, errors, services, logger)
        {
        }
    }
}
using System.Security.Claims;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using WebApplication1.Models;

namespace WebApplication1.Data
{
    public class ApplicationUserStore : UserStore<Client, Role, ApplicationDbContext, 
             string, ClientClaim, ClientRole, ExternalLogin, ClientToken, RoleClaim>
    {
        public ApplicationUserStore(ApplicationDbContext context, 
                      IdentityErrorDescriber describer = null)
            : base(context, describer)
        {
        }

        protected override ClientRole CreateUserRole(Client user, Role role)
        {
            return new ClientRole
            {
                Client = user,
                Role = role,
                ClientId = user.Id,
                RoleId = role.Id,
                UserId = user.Id
            };
        }

        protected override ClientClaim CreateUserClaim(Client user, Claim claim)
        {
            var clientClaim = new ClientClaim
            {
                Client = user,
                ClientId = user.Id,
                UserId = user.Id,
            };
            clientClaim.InitializeFromClaim(claim);
            return clientClaim;
        }

        protected override ExternalLogin CreateUserLogin(Client user, UserLoginInfo login)
        {
            return new ExternalLogin
            {
                Client = user,
                ClientId = user.Id,
                UserId = user.Id,
                LoginProvider = login.LoginProvider,
                ProviderDisplayName = login.ProviderDisplayName,
                ProviderKey = login.ProviderKey
            };
        }

        protected override ClientToken CreateUserToken
                   (Client user, string loginProvider, string name, string value)
        {
            return new ClientToken
            {
                ClientId = user.Id,
                UserId = user.Id,
                LoginProvider = loginProvider,
                Value = value,
                Name = name
            };
        }
    }
}

现在会工作吗?

嗯,答案是“是”和“否”。

是的,应用程序运行没有问题,但我们忘记了一个步骤。由于我们只使用旧数据库进行脚手架生成,并且默认情况下,新应用程序会创建自己的数据库,我们需要为我们所做的所有更改添加一个迁移,以便它们可以应用于我们连接到的任何新数据库。

创建迁移

所以我们再次打开了程序包管理器控制台,并输入 `cd ./WebApplication1`(之所以这样做是因为我们需要在项目文件夹中才能运行任何 dotnet 命令行),然后输入 `dotnet ef migrations add Initial -o "Data\Migrations”`。

这会在 `Data->Migrations` 文件夹中创建我们的迁移,就像项目刚创建时一样。对于那些好奇这样的迁移在经历了我们所有的更改后会是什么样子的人,这就是输出:

using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Metadata;

namespace WebApplication1.Data.Migrations
{
    public partial class Initial : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.EnsureSchema(
                name: "Security");

            migrationBuilder.CreateTable(
                name: "Client",
                schema: "Security",
                columns: table => new
                {
                    ID = table.Column<string>(maxLength: 450, nullable: false),
                    AccessFailedCount = table.Column<int>(nullable: false),
                    CNP = table.Column<string>(maxLength: 30, nullable: true),
                    ConcurrencyStamp = table.Column<string>(nullable: true),
                    Email = table.Column<string>(maxLength: 256, nullable: true),
                    EmailConfirmed = table.Column<bool>(nullable: false),
                    FirstName = table.Column<string>(maxLength: 50, nullable: false),
                    LastName = table.Column<string>(maxLength: 50, nullable: false),
                    LockoutEnabled = table.Column<bool>(nullable: false),
                    LockoutEnd = table.Column<DateTimeOffset>(nullable: true),
                    NormalizedEmail = table.Column<string>(maxLength: 256, nullable: true),
                    NormalizedUserName = table.Column<string>(maxLength: 256, nullable: false),
                    NumarCI = table.Column<string>(maxLength: 20, nullable: true),
                    PasswordHash = table.Column<string>(nullable: true),
                    PhoneNumber = table.Column<string>(nullable: true),
                    PhoneNumberConfirmed = table.Column<bool>(nullable: false),
                    RegDate = table.Column<DateTime>(nullable: false, defaultValueSql: "getdate()"),
                    SecurityStamp = table.Column<string>(nullable: true),
                    SerieCI = table.Column<string>(maxLength: 2, nullable: true),
                    TwoFactorEnabled = table.Column<bool>(nullable: false),
                    UserName = table.Column<string>(maxLength: 256, nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Client", x => x.ID);
                });

            migrationBuilder.CreateTable(
                name: "ClientToken",
                schema: "Security",
                columns: table => new
                {
                    ClientID = table.Column<string>(maxLength: 450, nullable: false),
                    LoginProvider = table.Column<string>(maxLength: 450, nullable: false),
                    Name = table.Column<string>(maxLength: 450, nullable: false),
                    UserId = table.Column<string>(nullable: true),
                    Value = table.Column<string>(nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_ClientToken", x => new { x.ClientID, x.LoginProvider, x.Name });
                });

            migrationBuilder.CreateTable(
                name: "Role",
                schema: "Security",
                columns: table => new
                {
                    ID = table.Column<string>(maxLength: 450, nullable: false),
                    ConcurrencyStamp = table.Column<string>(nullable: true),
                    Name = table.Column<string>(maxLength: 256, nullable: true),
                    NormalizedName = table.Column<string>(maxLength: 256, nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Role", x => x.ID);
                });

            migrationBuilder.CreateTable(
                name: "ClientClaim",
                schema: "Security",
                columns: table => new
                {
                    ID = table.Column<int>(nullable: false)
                        .Annotation("SqlServer:ValueGenerationStrategy", 
                                     SqlServerValueGenerationStrategy.IdentityColumn),
                    ClaimType = table.Column<string>(nullable: true),
                    ClaimValue = table.Column<string>(nullable: true),
                    ClientID = table.Column<string>(maxLength: 450, nullable: false),
                    ClientId1 = table.Column<string>(nullable: true),
                    UserId = table.Column<string>(nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_ClientClaim", x => x.ID);
                    table.ForeignKey(
                        name: "FK_ClientClaim_Client_ClientID",
                        column: x => x.ClientID,
                        principalSchema: "Security",
                        principalTable: "Client",
                        principalColumn: "ID",
                        onDelete: ReferentialAction.Cascade);
                    table.ForeignKey(
                        name: "FK_ClientClaim_Client_ClientId1",
                        column: x => x.ClientId1,
                        principalSchema: "Security",
                        principalTable: "Client",
                        principalColumn: "ID",
                        onDelete: ReferentialAction.Restrict);
                });

            migrationBuilder.CreateTable(
                name: "ExternalLogin",
                schema: "Security",
                columns: table => new
                {
                    LoginProvider = table.Column<string>(maxLength: 450, nullable: false),
                    ProviderKey = table.Column<string>(maxLength: 450, nullable: false),
                    ClientID = table.Column<string>(maxLength: 450, nullable: false),
                    ClientId1 = table.Column<string>(nullable: true),
                    ProviderDisplayName = table.Column<string>(nullable: true),
                    UserId = table.Column<string>(nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_ExternalLogin", x => new { x.LoginProvider, x.ProviderKey });
                    table.ForeignKey(
                        name: "FK_ExternalLogin_Client_ClientID",
                        column: x => x.ClientID,
                        principalSchema: "Security",
                        principalTable: "Client",
                        principalColumn: "ID",
                        onDelete: ReferentialAction.Cascade);
                    table.ForeignKey(
                        name: "FK_ExternalLogin_Client_ClientId1",
                        column: x => x.ClientId1,
                        principalSchema: "Security",
                        principalTable: "Client",
                        principalColumn: "ID",
                        onDelete: ReferentialAction.Restrict);
                });

            migrationBuilder.CreateTable(
                name: "ClientRole",
                schema: "Security",
                columns: table => new
                {
                    ClientID = table.Column<string>(maxLength: 450, nullable: false),
                    RoleID = table.Column<string>(maxLength: 450, nullable: false),
                    ClientId1 = table.Column<string>(nullable: true),
                    RoleId1 = table.Column<string>(nullable: true),
                    UserId = table.Column<string>(nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_ClientRole", x => new { x.ClientID, x.RoleID });
                    table.ForeignKey(
                        name: "FK_ClientRole_Client_ClientID",
                        column: x => x.ClientID,
                        principalSchema: "Security",
                        principalTable: "Client",
                        principalColumn: "ID",
                        onDelete: ReferentialAction.Cascade);
                    table.ForeignKey(
                        name: "FK_ClientRole_Client_ClientId1",
                        column: x => x.ClientId1,
                        principalSchema: "Security",
                        principalTable: "Client",
                        principalColumn: "ID",
                        onDelete: ReferentialAction.Restrict);
                    table.ForeignKey(
                        name: "FK_ClientRole_Role_RoleID",
                        column: x => x.RoleID,
                        principalSchema: "Security",
                        principalTable: "Role",
                        principalColumn: "ID",
                        onDelete: ReferentialAction.Cascade);
                    table.ForeignKey(
                        name: "FK_ClientRole_Role_RoleId1",
                        column: x => x.RoleId1,
                        principalSchema: "Security",
                        principalTable: "Role",
                        principalColumn: "ID",
                        onDelete: ReferentialAction.Restrict);
                });

            migrationBuilder.CreateTable(
                name: "RoleClaim",
                schema: "Security",
                columns: table => new
                {
                    ID = table.Column<int>(nullable: false)
                        .Annotation("SqlServer:ValueGenerationStrategy", 
                                     SqlServerValueGenerationStrategy.IdentityColumn),
                    ClaimType = table.Column<string>(nullable: true),
                    ClaimValue = table.Column<string>(nullable: true),
                    RoleID = table.Column<string>(maxLength: 450, nullable: false),
                    RoleId1 = table.Column<string>(nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_RoleClaim", x => x.ID);
                    table.ForeignKey(
                        name: "FK_RoleClaim_Role_RoleID",
                        column: x => x.RoleID,
                        principalSchema: "Security",
                        principalTable: "Role",
                        principalColumn: "ID",
                        onDelete: ReferentialAction.Cascade);
                    table.ForeignKey(
                        name: "FK_RoleClaim_Role_RoleId1",
                        column: x => x.RoleId1,
                        principalSchema: "Security",
                        principalTable: "Role",
                        principalColumn: "ID",
                        onDelete: ReferentialAction.Restrict);
                });

            migrationBuilder.CreateIndex(
                name: "EmailIndex",
                schema: "Security",
                table: "Client",
                column: "NormalizedEmail");

            migrationBuilder.CreateIndex(
                name: "UserNameIndex",
                schema: "Security",
                table: "Client",
                column: "NormalizedUserName",
                unique: true);

            migrationBuilder.CreateIndex(
                name: "IX_ClientClaim_ClientID",
                schema: "Security",
                table: "ClientClaim",
                column: "ClientID");

            migrationBuilder.CreateIndex(
                name: "IX_ClientClaim_ClientId1",
                schema: "Security",
                table: "ClientClaim",
                column: "ClientId1");

            migrationBuilder.CreateIndex(
                name: "IX_ClientRole_ClientId1",
                schema: "Security",
                table: "ClientRole",
                column: "ClientId1");

            migrationBuilder.CreateIndex(
                name: "IX_ClientRole_RoleID",
                schema: "Security",
                table: "ClientRole",
                column: "RoleID");

            migrationBuilder.CreateIndex(
                name: "IX_ClientRole_RoleId1",
                schema: "Security",
                table: "ClientRole",
                column: "RoleId1");

            migrationBuilder.CreateIndex(
                name: "IX_ExternalLogin_ClientID",
                schema: "Security",
                table: "ExternalLogin",
                column: "ClientID");

            migrationBuilder.CreateIndex(
                name: "IX_ExternalLogin_ClientId1",
                schema: "Security",
                table: "ExternalLogin",
                column: "ClientId1");

            migrationBuilder.CreateIndex(
                name: "RoleNameIndex",
                schema: "Security",
                table: "Role",
                column: "NormalizedName",
                unique: true);

            migrationBuilder.CreateIndex(
                name: "IX_RoleClaim_RoleID",
                schema: "Security",
                table: "RoleClaim",
                column: "RoleID");

            migrationBuilder.CreateIndex(
                name: "IX_RoleClaim_RoleId1",
                schema: "Security",
                table: "RoleClaim",
                column: "RoleId1");
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "ClientClaim",
                schema: "Security");

            migrationBuilder.DropTable(
                name: "ClientRole",
                schema: "Security");

            migrationBuilder.DropTable(
                name: "ClientToken",
                schema: "Security");

            migrationBuilder.DropTable(
                name: "ExternalLogin",
                schema: "Security");

            migrationBuilder.DropTable(
                name: "RoleClaim",
                schema: "Security");

            migrationBuilder.DropTable(
                name: "Client",
                schema: "Security");

            migrationBuilder.DropTable(
                name: "Role",
                schema: "Security");
        }
    }
}

现在呢?现在会工作吗?

又是“是”和“否”,不过我们越来越接近了,最后冲刺。

应用程序运行了,我们去注册用户,被提示应用迁移,我们应用了它,它工作了,还有什么遗漏的?

在我们的 `Client` 模型中,我们添加了 2 个新的必填字段:`FirstName` 和 `LastName`。这是我们 `AccountController` 中的 **Register** 方法:

// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null)
{
    ViewData["ReturnUrl"] = returnUrl;
    if (ModelState.IsValid)
    {
        var user = new Client { UserName = model.Email, Email = model.Email };
        var result = await _userManager.CreateAsync(user, model.Password);
        if (result.Succeeded)
        {
            await _signInManager.SignInAsync(user, isPersistent: false);
            _logger.LogInformation(3, "User created a new account with password.");
            return RedirectToLocal(returnUrl);
        }
        AddErrors(result);
    }
    return View(model);
}

正如你所见,这是创建 `Client` 的地方。由于我们已经失去了耐心,并且不想开始修改注册表单,所以我们只是硬编码了一些值,如下所示:

 var user = new Client { UserName = model.Email, Email = model.Email, 
                         FirstName = "RandomFirstName", LastName = "RandomLastName"};

够酷吧?

你猜怎么着?它成功了!!!耶!

请原谅我的激动,但我们终于熬过了这场磨难。现在剩下的是迁移应用程序的其余部分(这是困难的部分),当然要更新注册表单,然后继续前进。

结论

希望你喜欢我们的这次探索之旅,也希望它能帮助你,如果你碰巧遇到了这个问题。

谢谢,下次再见。

ASP.NET Core 1.1 和 Identity 的探索之旅 - CodeProject - 代码之家
© . All rights reserved.