ASP.NET Core Identity, part 1 - understanding Identity and extending to support non-string primary keys

08 September 2017

ASP.NET Core Identity is described by Microsoft as "a membership system which allows you to add login functionality to your application"; it brings a huge amount of user account functionality to our web applications with almost no effort on the part of the developer.

We are going to look at what happens when you add Identity to a project, how to customise the type of the primary key and how to implement a multi-tenant Identity solution.


Understanding how Identity changes a project

Creating a new "ASP.NET Core Web Application" project in Visual Studio offers the option to "Change Authentication" to "Individual User Accounts"; doing so will add all the code required to support Identity. Whilst this is fantastically easy for the developer, having all the work done for us does tend to make it more difficult to understand how it actually works, which can make it difficult to understand how to customise or extend Identity when it doesn't do exactly what we need out of the box.

Before we progress with some of the common customisations we have required at Future Shock, it helps to understand what happens when Identity is added to a project:

  1. Firstly a number of NuGet packages are added to our app to support both Identity and EntityFramework (the technology which will manage our database).
  2. appsettings.json will be modified to add a connection string for a SQL Server database (using LocalDB on the development machine, you don't need to install SQL Server); Identity will use this to store user account data.
  3. A Data folder will be added containing:
    1. ApplicationDbContext.cs - a class which represents your database within your web application.
    2. A Migrations folder containing an initial migration class - a migration class is created whenever structural changes are saved to a database; this one adds all the tables required by Identity.
  4. Startup.cs is modified to add the database context (ApplicationDbContext) and Identity with the default settings.
  5. Models, View and Controllers will be added to provide the actual account login, register and management web pages; along with some simple Services to handle automated sending of emails/SMS messages (using placeholder methods in the Services/MessageServices.cs class - you will need to implement these yourself).

The really important bits here are 3a and 4; these are the areas we will need to tweak if we wish to customise our Identity configuration, so we will quickly examine these in a bit more detail.

Startup.cs

This is where the overall configuration of our application occurs, a number of lines have been added to support Identity mostly in the ConfigureServices method:

  1. services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
    This adds the database definition class to the list of services supported by our application, and tells the service to use the database connection string added in the appsettings.json. Simply adding this wires an application up to use a database (assuming the Microsoft.EntityFrameworkCore NuGet package is installed, your ApplicationDbContext could derive from the DbContext class in EntityFrameworkCore - for more info see the official ASP.NET Core docs).

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

    This adds the Identity service, telling it to use the class in Models/ApplicationUser.cs to define the IdentityUser, to use the default IdentityRole class, to use the database context we created in step 1, and to add all the remaining defaults for Identity using AddDefaultTokenProviders. This is the key configuration point for customising Identity. Note that in theory the custom class ApplicationUser could be removed and the IdentityUser class supplied with Identity used instead, but you would need to edit many of the MVC files to do this; its simpler to use the ApplicationUser class and which will be required for many of the more common simple customisations of Identity (e.g. adding other fields to your user account than simply those supplied to support login).

  3. services.AddTransient<IEmailSender, AuthMessageSender>(); and services.AddTransient<ISmsSender, AuthMessageSender>();
    These add the services for sending emails and text messages.

The Configure method has simply gained the code app.UseIdentity(); in ASP.NET Core 1 (app.UseAuthentication(); in ASP.NET Core 2) to actually enable Identity in the application pipeline.

Knowing what has been automatically added to support Identity, and having identified the key points of customisation, we can now look at a few commonly required changes.


Changing the primary key type

The primary key created automatically for the User table in our database will be of type string by default; many of us will require something different to use in relationships as we implement the feature set our application. Generally we use int, long or guid types for this purpose; changing this in versions of Identity before Core could be quite a lot of work, thankfully it has become much easier now.

We are giving an example of changing to a long but you can use any other type in its place.

You can find more information on this in the official ASP.NET Core documentation but below is the most simplified process we have found for both current versions of ASP.NET Core.

ASP.NET Core 1

We have found it only necessary to amend the IdentityUser, there is no need to customise IdentityRole to change its key type.

  1. Open the Models/ApplicationUser.cs file and change the class definition:
    from public class ApplicationUser : IdentityUser
    to public class ApplicationUser : IdentityUser<long>
    This derives the class to from the generic IdentityUser<TKey> rather than the non-generic default IdentityUser which uses a string key.
  2. Open the Models/ApplicationDbContext.cs file and change the class definition:
    from  public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    to public class ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityRole<long>, long>
    This tells the class that the user key will be a long and that IdentityRole also needs to use a corresponding long key.
  3. Open the Startup.cs file and change the ConfigureServices method from:
    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    to:
    services.AddIdentity<ApplicationUser, IdentityRole<long>>()
        .AddEntityFrameworkStores<ApplicationDbContext, long>()
        .AddDefaultTokenProviders();

    This amends the default Framework Stores to use long keys for Users and Roles.
  4. Amend your Controllers/AccountController.cs and Controllers/ManageController.cs to fix build errors where method calls are expecting strings instead of longs; simply change user.Id to user.Id.ToString() on the four lines that cause an error.
  5. Update the database structure. Unfortunately adding a migration won't work as SQL Server will reject the update with a message that it cannot change the type of an identity column. The simplest solution at this stage (configuring identity should be one of the first things you do in a new web application) is to:
    1. If you have already run the application then delete any existing database (you can use the SQL Server Object Explorer in Visual Studio to do this).
    2. Delete all the files from within the Data/Migrations folder.
    3. Open the Package Manager Console in Visual Studio then run the commands:

      add-migration SetupDatabase -OutputDir Data\Migrations 
      Followed by:
      update-database

ASP.NET Core 2

The only difference from the ASP.NET Core version 1 instructions above is in step 3 where the AddEntityFrameworkStores line has changed to not require or accept a second parameter, it should now be amended to:

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

Also version 2 no longer provides the Entity Framework(EF) Core navigation properties of the base IdentityUser (e.g. IdentityUser.Roles, IdentityUser.Claims and IdentityUser.Logins) ; if your 1.x project used these properties, manually add them back to the 2.0 project (as per Microsoft docs):

  1. Amend your Models/ApplicationUser.cs file to include any of the following you require within your class definition:

    // Navigation property for the roles this user belongs to
    public virtual ICollection<IdentityUserRole<int>> Roles { get; } = new List<IdentityUserRole<int>>();

    // Navigation property for the claims this user possesses.
    public virtual ICollection<IdentityUserClaim<int>> Claims { get; } = new List<IdentityUserClaim<int>>();

    // Navigation property for this users login accounts.
    public virtual ICollection<IdentityUserLogin<int>> Logins { get; } = new List<IdentityUserLogin<int>>();

  2. Edit your Models/ApplicationDbContext.cs file, adding any of the following  to your OnModelCreating method:
    builder.Entity<ApplicationUser>().HasMany(e => e.Roles).WithOne().HasForeignKey(e => e.UserId).IsRequired();
    builder.Entity<ApplicationUser>().HasMany(e => e.Claims).WithOne().HasForeignKey(e => e.UserId).IsRequired().OnDelete(DeleteBehavior.Cascade);
    builder.Entity<ApplicationUser>().HasMany(e => e.Logins).WithOne().HasForeignKey(e => e.UserId).IsRequired().OnDelete(DeleteBehavior.Cascade);

  3. Add a new migration and update your database

Implementing muli-tenancy

As this is a fairly in-depth topic we have published this in a separate article