Updating project to ASP.NET Core 2.0

We have upgraded Visual Studio 2017 version 15.3 and also installed .NET Core SDK 2.0. Let’s upgrade the project to ASP.NET Core 2.0. Actually we wanted to achieve 3 goals:

  1. Change the target of libraries to .NET Standard 2.0, instead of .NET 4.7, so that we can easily switch between full .NET Framework and .NET Core.
  2. Upgrade NuGet references to ASP.NET Core 2.0.
  3. Change target of the web application to .NET Core 2.0, so that it can run on multiple platforms.

Change target of libraries and web application

Visual Studio project properties page

There is no UI functionality in Visual Studio 2017 to change target of a project from .NET 4.7 to .NET Standard. It is possible to switch easily between different versions of the same framework. For example it is possible to change the target from .NET 4.6.1 to .NET 4.7, but not to .NET Core 2.0.
However, it is possible to do it directly by editing .csproj file. Actually it is even faster, when using Visual Studio Code.

  1. Simply open the folder with your solution in Visual Studio Code.
  2. Then open each .csproj file excluding test projects and web application project. Btw, web project is minimal in our solution, because all controllers and middleware are in separate assembly.
    1. Find element TargetFramework and change value from net47 to netstandard2.0.
  3. Open web project and change value of element TargetFramework from net47 to netcoreapp2.0.

Example of .csproj file:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

  <PropertyGroup>
    <TreatWarningsAsErrors>True</TreatWarningsAsErrors>
    <TreatSpecificWarningsAsErrors />
  </PropertyGroup>

  ...

</Project>

We decided to stay with target net47 for test projects, because tests were not discovered by VSTest in TFS build. At the moment we don’t need to run tests on multiple platforms, so targeting .NET Framework 4.7 is sufficient. If you need to run tests on multiple platforms very simple solution might be to change the element name to TargetFrameworks (notice plural) and specify value with multiple targets like net47;netcoreapp2.0. For example:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFrameworks>net47;netcoreapp2.0</TargetFrameworks>
  </PropertyGroup>

  <PropertyGroup>
    <TreatWarningsAsErrors>True</TreatWarningsAsErrors>
    <TreatSpecificWarningsAsErrors />
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0" />
    <PackageReference Include="xunit" Version="2.2.0" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
    <!-- other packages -->
  </ItemGroup>

  ...

</Project>

Notice reference to NuGet Microsoft.NET.Test.Sdk. Without this reference, your tests would not be dicoverable in .NET Core. Purpose of this is to speedup the test discovery. More information can be found in the post about Test Container.

Then I opened solution in Visual Studio 2017 and it compiled smoothly with new targets. And the tests passed too.

Upgrade ASP.NET Core 2.0 NuGet packages

Migration of the project from ASP.NET Core 1.1 to 2.0 is described in the Microsoft documentation. I followed the documentation more or less with few specialities in our solution. The solution is a Web API project and does not include any Razor views. All controllers are in separate assembly. And we implemented custom cookie verification in authentication module.

In my opinion, it would be much slower to update package references in Visual Studio. So I opened the solution folder in Visual Studio Code. Then I opened each .csproj file and updated all package references related to ASP.NET Core 2.0. I simply changed Version attribute to value 2.0.0 in PackageReference elements. Some examples of the package references:

  • <PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.0.0" /> - this package is referenced by project with Controllers. It includes some MVC base classes and HTTP abstractions. It is not needed to reference the full ASP.NET Core (MVC).
  • <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.0.0" /> - we use these interfaces for logging.

In the web application project I removed all package references related to ASP.NET Core 2.0 and added single NuGet package <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />. Microsoft.AspNetCore.All is a metapackage that includes everything related to ASP.NET Core 2.0 (MVC, HTTP server, Identity, IIS integration, Entity Framework Core, and extensions like dependency injection, configuration, etc.)

Then I opened solution in Visual Studio 2017, but this time I got some compilation errors.

Change of authentication in ASP.NET Core 2.0

In ASP.NET Core 2.0 authentication architecture was changed from version 1.1. In version 1.x there was separate middleware for each type of authentication (cookie, OpenID, etc.) In ASP.NET Core 2.0, however, there is single authentication middleware and each type of authentication is handled by a service injected using dependency injection. Again migration of authentication in ASP.NET Core 2.0 is documented in the Microsoft documentation.

The first step was to change extension method to register the authentication service. In ASP.NET Core 1.0 this extension method extended IApplicationBuilder interface. In ASP.NET Core 2.0 it has to extend IServiceCollection interface and it should register authentication service instead of middlewear. Example of the extension method:

public static class ApplicationAuthentication
{
    public static AuthenticationBuilder AddApplicationAuthentication(this IServiceCollection services)
    {
        if (services == null)
        {
            throw new ArgumentNullException(nameof(services));
        }

        return services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
            .AddCookie(
            o =>
            {
                o.Cookie.Name = "AspNetCore.MyApp.Default";
                o.CookieManager = new HttpHeaderCookieManager();
                o.EventsType = typeof(ApplicationAuthenticationEvents);
            });
    }
}

Unfortunately, it is not possible to use custom Authentication Scheme name. It is because Authentication Scheme is not a simple string value anymore, but it is coupled with .NET type that implements the authentication. Therefore, I used CookieAuthenticationDefaults.AuthenticationScheme as the schema name. And additionally I had to specify the cookie name. By default, the CookieName is same as the Authentication Schema. I wanted to keep the same cookie name for compatibility, even though the schema name was changed.

Nice new feature is that CookieAuthenticationOptions class has EventsType property. Previously it was necessary to provide an instance of class implementing ICookieAuthenticationEvents interface. Now it is possible to specify the type only and it is then resolved using service provider (Dependency Injection in ASP.NET Core 2.0).

Another error was the missing interface ICookieAuthenticationEvents. A nice surprise was that it was actually replaced by CookieAuthenticationEvents abstract class with default implementation. So it is not necessary to implement all interface methods, even when they were empty. So I deleted all empty methods and kept only ValidatePrincipal.

public class ApplicationAuthenticationEvents : CookieAuthenticationEvents
{
    private readonly IEntityQueryById<Account> _accountQuery;

    public ApplicationAuthenticationEvents(IEntityQueryById<Account> accountQuery)
    {
        _accountQuery = accountQuery ?? throw new ArgumentNullException(nameof(accountQuery));
    }

    public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (context.Principal == null)
        {
            return;
        }

        var identity = context.Principal.Identity;
        if (!identity.IsAuthenticated)
        {
            context.RejectPrincipal();
            return;
        }

        var identityId = identity.Name;
        var account = await _accountQuery.ExecuteAsync(identityId);

        if (account == null)
        {
            context.RejectPrincipal();
            return;
        }

        // add some other claims to identity Claim (e.g. roles, employee ID)
    }
}

Then there were some errors about calling obsolete methods. HttpContext does not have Authentication property and methods like SignInAsync or SignOutAsync are extension methods of HttpContext.
So the fix was simple change of line from

await HttpContext.Authentication.SignInAsync(Middleware.ApplicationAuthentication.AuthenticationScheme, claims.Value);

to line

await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claims.Value);

And the last step was to correctly register the services and authentication middleware in the application Startup class. In ConfigureServices method I called AddApplicationAuthentication. And in Configure method I called UseAuthentication. In the end this was the only change in the startup procedure.

public class Startup : IStartup
{
    public Startup(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);

        builder.AddEnvironmentVariables();
        Configuration = builder.Build();
    }

    public IConfigurationRoot Configuration { get; }

    public IServiceProvider ConfigureServices(IServiceCollection services)
    {
        services.AddMvc().AddApplicationPart(typeof(Rest.Controllers.AccountController).Assembly);

        services.AddApplicationAuthentication();

        var builder = new ContainerBuilder();
        builder.RegisterModule<ApplicationModule>();
        builder.Populate(services);
        var container = builder.Build();
        return new AutofacServiceProvider(container);
    }

    public void Configure(IApplicationBuilder app)
    {
        var serviceProvider = app.ApplicationServices;
        var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();

        loggerFactory.AddConsole(Configuration.GetSection("Logging"));
        loggerFactory.AddDebug();

        // Configure settings like database connection strings

        app.UseAuthentication();
        app.UseMvc();
    }
}

Some last fine tuning

As we converted all projects from .NET Framework to .NET Core, we could delete all App.config files. In our case, there was no application configuration, but configuration of runtime like Garbage Collector in Server mode or some assembly bindings.

In Microsoft migration documentation it is suggested to simplify Main entry point by using WebHost.CreateDefaultBuilder method. However, in some specific cases we host the application on specific ports and need to setup special HTTP bindings. This is configured in hosting.json file. Therefore we kept the original Main entry point.