Identity Server: Migration to ASP.NET Core 2

The Identity App that is part of my IdentityServer sample project is the last application I have on GitHub (of the ones that will get upgraded) that needs an upgrade to ASP.NET Core. The starting point of the project before any changes can be found here. This post assumes that you have already followed my generic ASP.NET Core 2 migration post, which can be found here, on the project you are migrating. One final note of caution this post has been written using the RC1 version of the Identity Server NuGet packages and then moved to the final version so there will be two different related pull requests that will have to be looked at to get the full picture of all the changes.

Package Changes

The first change is to get a version of the Identity Server packages what will work from ASP.NET Core 2.

Before:
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="1.0.1" />
<PackageReference Include="IdentityServer4.EntityFramework" Version="1.0.1" />

After:
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="2.0.0" />
<PackageReference Include="IdentityServer4.EntityFramework" Version="2.0.0" />

Database Initialization

I wasted a lot of time on finding out this was an issue when I was trying to create Entity Framework migrations and kept getting Unable to create an object of type ‘ApplicationDbContext’. Add an implementation of ‘IDesignTimeDbContextFactory‘ errors. The gist is database initialization needs to be moved out of Startup and context constructors.

Let’s start with the ApplicationDbContext and remove the following code from the constructor as well as the associated property.

if (_migrated) return;
Database.Migrate();
_migrated = true;

Next, in the Configure function of the Startup class remove the following line.

IdentityServerDatabaseInitialization.InitializeDatabase(app);

We still need the database initialization code to run, but where should that be done? In the Main function of the Program class seems to be the new recommended location. The following is the new Main function.

public static void Main(string[] args)
{
    var host = BuildWebHost(args);

    using (var scope = host.Services.CreateScope())
    {
        var services = scope.ServiceProvider;

        try
        {
            IdentityServerDatabaseInitialization.InitializeDatabase(services);
        }
        catch (Exception ex)
        {
            var logger = services.GetRequiredService<ILogger<Program>>();
            logger.LogError(ex, "An error occurred Initializing the DB.");
        }
    }

    host.Run();
}

InitializeDatabase now needs to take an IServiceProvider instead of an IApplicationBuilder. This forced a lot of lines to change so the following is the full class.

public static class IdentityServerDatabaseInitialization
{
    public static void InitializeDatabase(IServiceProvider services)
    {
        PerformMigrations(services);
        SeedData(services);

    }

    private static void PerformMigrations(IServiceProvider services)
    {
        services
          .GetRequiredService<ApplicationDbContext>()
          .Database.Migrate();
        services
          .GetRequiredService<ConfigurationDbContext>()
          .Database.Migrate();
        services
          .GetRequiredService<PersistedGrantDbContext>()
          .Database.Migrate();
    }

    private static void SeedData(IServiceProvider services)
    {
        var context = services.GetRequiredService<ConfigurationDbContext>();

        if (!context.Clients.Any())
        {
            foreach (var client in Config.GetClients())
            {
                context.Clients.Add(client.ToEntity());
            }
            context.SaveChanges();
        }

        if (!context.IdentityResources.Any())
        {
            foreach (var resource in Config.GetIdentityResources())
            {
                context.IdentityResources.Add(resource.ToEntity());
            }
            context.SaveChanges();
        }

        if (!context.ApiResources.Any())
        {
            foreach (var resource in Config.GetApiResources())
            {
                context.ApiResources.Add(resource.ToEntity());
            }
            context.SaveChanges();
        }
    }
}

Startup Changes

Most of the changes to the Startup class are in the ConfigureServices function, but some cross with the Configure function as well. The existing AddIdentityServer extension has multiple changes especially if you are using Entity Framework for your configuration data. AddTemporarySigningCredential is now AddDeveloperSigningCredential. The following is the new version including configuration data.

services.AddIdentityServer()
    .AddDeveloperSigningCredential()
    .AddAspNetIdentity<ApplicationUser>()
    .AddConfigurationStore(options =>
    {
      options.ConfigureDbContext = builder =>                 
        builder.UseSqlite(Configuration
                          .GetConnectionString("DefaultConnection"),
                          db => db.MigrationsAssembly(migrationsAssembly));
    })
    .AddOperationalStore(options =>
    {
      options.ConfigureDbContext = builder =>
        builder.UseSqlite(Configuration
                          .GetConnectionString("DefaultConnection"),
                          db => db.MigrationsAssembly(migrationsAssembly));
    });

The way to handle registration of external authentication has changed as well. For example, this application uses Twitter. The UseTwitterAuthentication call in the Configure function needs to be removed. The following added to the bottom of the ConfigureServices is now the proper way to add external authentication providers.

services.AddAuthentication().AddTwitter(twitterOptions =>
{
    twitterOptions.ConsumerKey = 
         Configuration["Authentication:Twitter:ConsumerKey"];
    twitterOptions.ConsumerSecret = 
         Configuration["Authentication:Twitter:ConsumerSecret"];
});

Entity Framework

The new changes in Identity from the ASP.NET Core team included a new foreign key which is one of the things that Sqlite migrations can’t actually do. Since I don’t really have any data I care about I just deleted the database and the existing migrations and snapshots and regenerated everything. If you are using Sqlite and this isn’t an option for you check out this post for some options. If you aren’t using Sqlite then the migrations should work fine. The following are the commands to generate migrations for the 3 contexts that the Identity Application uses.

dotnet ef migrations add InitConfigration -c ConfigurationDbContext -o Data/Migrations/IdentityServer/Configuration

dotnet ef migrations add InitPersistedGrant -c PersistedGrantDbContext -o Data/Migrations/IdentityServer/PersistedGrant

dotnet ef migrations add InitApplication -c ApplicationDbContext -o Data/Migrations

Quick Start UI Changes

As part of going from the RC1 version to the Final version, the Identity Server team updated the UI and related bits to be in line with the new features added in the ASP.NET Core 2.0 release. Turns out that resulted in a lot of changes. Since I haven’t done any custom work in this area of my Identity Application I deleted the related files in my local project and pulled from the ASP.NET and Entity Framework Combined sample. I am going to give a good idea of all the files I replace, but in case I miss something GitHub will have the full story.

In the Controllers folder replace AccountController.cs and ManageController.cs. Add or replace the following folders:  ExtensionsModelsQuickstartServices, and Views.

Application Insights Error

I ran into the following error.

System.InvalidOperationException: No service for type ‘Microsoft.ApplicationInsights.AspNetCore.JavaScriptSnippet’ has been registered.

You may or may not see it, but if you do open the _Layout.cshtml and remove the following two lines.

@inject Microsoft.ApplicationInsights.AspNetCore.JavaScriptSnippet JavaScriptSnippet


@Html.Raw(JavaScriptSnippet.FullScript)

Wrapping up

If you hit any issues not covered above make sure and check out the breaking changes issue. The completed code can be found here for part 1 and here for part 2.


Also published on Medium.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.