ASP.NET Core 3.1 - Rename Related Entities

Ken Haggerty
Created 11/24/2020 - Updated 02/23/2021 23:57

This article will demonstrate a process for renaming related EF Core entities and database objects without dropping data. I will assume you have downloaded the ASP.NET Core 3.1 - Users Without Identity Project or created a new ASP.NET Core 3.1 Razor Pages project. See Tutorial: Get started with Razor Pages in ASP.NET Core. You should review the earlier articles of the Users Without Identity Project series.

Users Without Identity Project and Article Series

I prototyped an application for a prospect with a simple user management system which I published to demo.kenhaggerty.com. I named the user entity User. When I created the Users Without Identity Project (UWIP) I named the user entity AppUser. When I implemented 2FA, the demo site used a UserToken entity and UWIP used AppUserToken.

I recently updated almost every page in the Account and Account / Mange folders of the UWIP. To implement all the updates to the demo site, I decided to copy the folders and rename the user and user token entities to AppUser and AppUserToken.

Demo site - User.cs:
public class User
{
    [Key]
    public int Id { get; set; }
    [Required]
    [Display(Name = "Login Name")]
    [StringLength(32, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
    public string LoginName { get; set; }
    [Required]
    [StringLength(32)]
    public string LoginNameUppercase { get; set; }
    [Required]
    [MaxLength(84, ErrorMessage = "The {0} is max {1} characters long.")]
    public string PasswordHash { get; set; }
    [Display(Name = "MCP")]
    public bool MustChangePassword { get; set; }
    [Display(Name = "2FA")]
    public bool TwoFactorEnabled { get; set; }
    [Display(Name = "Admin")]
    public bool IsAdmin { get; set; }
    [Timestamp]
    public byte[] RowVersion { get; set; }

    public virtual ICollection<UserToken> UserTokens { get; set; }
}
Demo site - UserToken.cs:
public class UserToken
{
    [Required]
    public int UserId { get; set; }
    [Required]
    [StringLength(128)]
    public string Name { get; set; }
    public string Value { get; set; }

    public virtual User User { get; set; }
}

Notice the virtual UserTokens navigational property on User and the virtual User navigational property on UserToken. I implemented a unique constraint on LoginNameUppercase for the User and a non-clustered composite primary key for the UserToken in the ApplicationDbContext > OnModelCreating method.

Demo site - ApplicationDbContext.cs:
protected override void OnModelCreating(ModelBuilder builder)
{
    base.OnModelCreating(builder);

    // Set a unique constraint for LoginNameUppercase
    builder.Entity<User>().HasIndex(u => u.LoginNameUppercase).IsUnique();

    // Composite primary key consisting of the UserId and Name
    builder.Entity<UserToken>().HasKey(t => new { t.UserId, t.Name }).IsClustered(false);
}

After renaming the entities, navigational properties, and references to AppUser and AppUserToken along with the AppUserToken.AppUserId property, the Package Manager Console add migration command results in dropping the tables with existing data and creating new empty tables.

AppUserRename.cs:
public partial class AppUserRename : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(
            name: "UserTokens");

        migrationBuilder.DropTable(
            name: "Users");

        migrationBuilder.CreateTable(
            name: "AppUsers",
            columns: table => new
            {
                Id = table.Column<int>(nullable: false)
                    .Annotation("SqlServer:Identity", "1, 1"),
                LoginName = table.Column<string>(maxLength: 32, nullable: false),
                LoginNameUppercase = table.Column<string>(maxLength: 32, nullable: false),
                PasswordHash = table.Column<string>(maxLength: 128, nullable: false),
                IsAdmin = table.Column<bool>(nullable: false),
                MustChangePassword = table.Column<bool>(nullable: false),
                TwoFactorEnabled = table.Column<bool>(nullable: false),
                IsDisabled = table.Column<bool>(nullable: false),
                RowVersion = table.Column<byte[]>(rowVersion: true, nullable: true)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_AppUsers", x => x.Id);
            });

        migrationBuilder.CreateTable(
            name: "AppUserTokens",
            columns: table => new
            {
                AppUserId = table.Column<int>(nullable: false),
                Name = table.Column<string>(maxLength: 128, nullable: false),
                Value = table.Column<string>(nullable: true)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_AppUserTokens", x => new { x.AppUserId, x.Name })
                    .Annotation("SqlServer:Clustered", false);
                table.ForeignKey(
                    name: "FK_AppUserTokens_AppUsers_AppUserId",
                    column: x => x.AppUserId,
                    principalTable: "AppUsers",
                    principalColumn: "Id",
                    onDelete: ReferentialAction.Cascade);
            });

        migrationBuilder.CreateIndex(
            name: "IX_AppUsers_LoginNameUppercase",
            table: "AppUsers",
            column: "LoginNameUppercase",
            unique: true);
    }
}

I had a similar issue when I refactored KenHaggerty.Com's FrameworkEnum. See ASP.NET Core 5.0 - Framework Enum. My research found Stack Overflow - Entity Framework Migrations renaming tables and columns which I used to rename the TargetFrameworkEnum to FrameworkEnum. Renaming the user and user token entities is a little more complicated. The solution drops all the foreign and primary keys along with the unique constraint before renaming the tables and column then adding back the keys and constraint.

public partial class AppUserRename : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropForeignKey(name: "FK_UserTokens_Users_UserId", table: "UserTokens");

        migrationBuilder.DropUniqueConstraint(name: "IX_Users_LoginNameUppercase", table: "Users");

        migrationBuilder.DropPrimaryKey(name: "PK_Users", table: "Users");

        migrationBuilder.DropPrimaryKey(name: "PK_UserTokens", table: "UserTokens");

        migrationBuilder.RenameTable(name: "Users", newName: "AppUsers");

        migrationBuilder.AddPrimaryKey(name: "PK_AppUsers", table: "AppUsers", column: "Id");

        migrationBuilder.AddUniqueConstraint(name: "IX_AppUsers_LoginNameUppercase", table: "AppUsers",
            column: "LoginNameUppercase");

        migrationBuilder.RenameTable(name: "UserTokens", newName: "AppUserTokens");

        migrationBuilder.RenameColumn(name: "UserId", table: "AppUserTokens", newName: "AppUserId");

        migrationBuilder.AddPrimaryKey(name: "PK_AppUserTokens", table: "AppUserTokens",
            columns: new string[] { "AppUserId", "Name" })
            .Annotation("SqlServer:Clustered", false);

        migrationBuilder.AddForeignKey(name: "FK_AppUserTokens_AppUsers_AppUserId", table: "AppUserTokens",
            column: "AppUserId", principalTable: "AppUsers", principalColumn: "Id", onDelete: ReferentialAction.Cascade);
    }
}
Update 02/23/2021

I added the Enhanced User Series' article links.

Article Tags:

2FA Authorization EF Core

Comment Count = 0

Please log in to comment.

Login Register
Logged in users receive web notifications.
Web Notifications