Introduction
Recently, a friend and I were trying to implement the Identity pattern from ASP.NET Core 1.1 on a different database schema and with custom tables from a legacy application and through some trial and error, we managed to do it.
In this post, I will share the steps we took and how we managed to solve it. I hope you find it useful :D.
The Setup
First off, we started with the old database we had and added a new schema to separate the business entities from the security ones. Here’s a script of how the tables were setup in 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
Now, if we look at this schema, we can see it’s identical to the normal identity table except they have different table names, they belong to a different database schema (as they should) and the *Client
*table (which represents the former AspNetUsers
table) has 3 new columns needed in our application.
So we made the changes to the database first, since that was the easiest to modify in this application migration, next we create a new ASP.NET Core 1.1 solution and set the authentication to Individual User Accounts.
The Changes
Next, we deleted the migrations the template project come with that are found under Data-> Migrations to get those from interfering with our work.
So now, we have an old database that was modified for our new requests and a brand new created ASP.NET Core 1.1 application, in which we want to do Code-First from an existing database. So then, we did the following steps:
- Opened up the Package Manager console
- Ran the following command by switching the “” part with the connection string for our old database
Scaffold-DbContext -Connection -Provider "Microsoft.EntityFrameworkCore.SqlServer" -o "Models\" -Schemas "dbo","Security”
-
Removed the class ApplicationUser
since we will be using our own class for this purpose.
Now that we have the models created into our Models folder, we need to do the following updates, we’re going to go through each class one by one from how it looked when it was imported to how it will be when we’re done.
Client.cs
When we imported our models, the Client
class ended up looking like this:
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; }
}
}
But like we said before, this will be the model for our users, as such the class needs to implement the IdentityUser
interface, but since we’re creating all of the tables, we will also need to specify the types of the other dependent classes and the type of the Id
property. At the end, the class looked like this:
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; }
}
}
Note that the only properties remaining from the old implementation are the custom columns that were added to the user table and the new navigation properties for the custom types.
ClientClaim.cs
This class (and some of the other supporting classes) doesn’t hold any changes from what is the default implemented into the framework with the exception of being called differently.
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; }
}
}
Got changed to this:
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
From this:
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; }
}
}
To this:
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
From this:
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; }
}
}
To this:
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
namespace WebApplication1.Models
{
public class ClientToken : IdentityUserToken<string>
{
public string ClientId { get; set; }
}
}
ExternalLogin.cs
From this:
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; }
}
}
To this:
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
From this:
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; }
}
}
To this:
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; }
}
}
Note that in this case, just like for the client, we’re not just implementing IdentityRole
but we’re using the form with generics for its dependent classes.
And finally...
RoleClaims.cs
From this:
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; }
}
}
To this:
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
namespace WebApplication1.Models
{
public class RoleClaim : IdentityRoleClaim<string>
{
public virtual Role Role { get; set; }
}
}
Pfffewww…But we’re not done yet. When we did the import of the models, a new database context was created which if you used the command we did earlier, it will be called the name of the database suffixed with Context
and be placed inside the same models folder.
Updating the Database Context
What we need to do now are the following:
- Move all the properties (the
DbSet
s) created in this context and move them in the default database context that came with the project template, called ApplicationDbContext
. - Move the
OnModelCreating
method entirely. - Delete the created context since we got everything we needed from it (we can ignore the
OnConfiguring
method since the ApplicationDbContext
already receives the connection string in Startup.cs).
Now we need to do some updates in the *ApplicationDbContext.cs *file, initially it looked like this:
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);
}
}
}
Besides the updates we added from the generated context earlier, we also need to change the interface from IdentityDbContext
to a more explicit interface declaring all of our custom classes, and in the end, it will look like this:
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);
});
}
}
}
One thing to note which was a big headache for us is in the OnModelCreating
method, and that was the default line base.OnModelCreating(builder)
. The reason this was a headache was that we grew accustomed to always call on the base implementations of virtual methods and not ask why, and every time we ran the migration for the new database, we would end up not only with our own tables, but also the old AspNetXXX tables because this is what runs inside the OnModelCreating
override in the IdentityDbContext
class:
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");
}));
}
So in our implementation, we needed to remove the call to the base implementation so that it doesn’t interfere with our migration.
Running the Application?
So after all these changes, we hit Build and yep, we found the build was failing in all places because we removed the ApplicationUser
class (to make things easier, we could have just moved the Client
implementation in that class and then renamed it, but that would be a bit tangled), we went to each location where *ApplicationUser
*was used and replaced it with our own Client
class.
Ok, now we build and everything seems ok, right? Wrong… We run the application and we get the following error:
GenericArguments\[0], ‘WebApplication1.Models.Client’,
on ‘Microsoft.AspNetCore.Identity.EntityFrameworkCore.UserStore\`4\[TUser,TRole,TContext,TKey]’
violates the constraint of type ‘TUser’.
When trying to run the following line in the *Startup.cs *file:
services.AddIdentity<Client, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
This is where we got stuck for a while (actually, we got stuck in quite a few of the previous steps since we were doing something for which we had no proper documentation), so we started looking into the .NET Core documentation, and StackOverflow and such, but no clean-cut fixes for why this is happening, let alone how to fix it (you know my policy, understand first, fix after).
So I took a look into the source code of the AddEntityFrameworkStores()
method. And I found this:
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;
}
Which then later got me to look into the definition of the UserStore
to see what constraint we were violating. Well, it didn’t seem very obvious but the default UserStore
and RoleStore
implementations did now know how to use our custom classes (after some later research into the Github repo, I found this issue and they mention this should be a lot easier to implement in the .NET Core 2 iteration, if you’re curious, the issue can be found here). To fix it, we needed to implement our own stores and managers.
Implementing the “Fix”
So this is what we did next, we updated the startup line as follows:
services.AddIdentity<Client, Role>()
.AddUserStore<ApplicationUserStore>()
.AddUserManager<ApplicationUserManager>()
.AddRoleStore<ApplicationRoleStore>()
.AddRoleManager<ApplicationRoleManager>()
.AddSignInManager<ApplicationSignInManager>()
.AddDefaultTokenProviders();
As you can see, we needed to add custom stores and managers (we will show you their implementation next) and commented out the AddEntityFrameworkStores
line (kept here to emphasize that it needed to be removed).
Next, this is the implementation for each one of those custom classes:
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
};
}
}
}
Will It Work Now?
Well, the answer is yes and no.
Yes, the application ran without any issue, but we forgot one more step. Since we only used our old database for scaffolding and by default, a new application creates its own database, we need to add a migration for all the changes we made so that they can be applied to any new database we connect to.
Creating the Migration
So we opened up the Package Manager Console again and typed in cd ./WebApplication1
(the reason for this is because we need to be in the project folder if we want to run any dotnet command lines) then dotnet ef migrations add Initial -o "Data\Migrations”
.
This created our migration in the Data->Migrations folder just like it was when we first created the project, and for those curious as to how such a migration looks like after all our changes, this is what is outputted:
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");
}
}
}
How About Now? Will It Work Now?
Again yes and no again, we’re getting closer though, final push.
The application ran, we went to register a user, we got prompted to apply the migration, we applied it and it worked, what’s missing?
In our Client
* model, we added 2 new fields that are mandatory. Those are FirstName and LastName. This is our **Register method inside the *AccountController
.
[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);
}
As you can see, this is where our Client
is created, since we were losing our patience and we didn’t want to start changing the register form, we just hardcoded a few values like this:
var user = new Client { UserName = model.Email, Email = model.Email,
FirstName = "RandomFirstName", LastName = "RandomLastName"};
Classy, right?
And guess what? IT WORKED !!! WOOHOO.
Excuse my enthusiasm, but we were finally through this ordeal, all that was left now was to migrate the rest of the application (this was the hard part), update the register form of course and continue on our way.
Conclusion
I hope you liked our adventures, and I also hope it helped you if you, by chance, encountered this issue.
Thank you and see you next time.
CodeProject