Migrating from .NET Core 1.1 to 2.0

Home / Migrating from .NET Core 1.1 to 2.0

There are many subtle changes in moving from .NET Core 1.1 to .NET Core 2.0. In this post, I cover some of the breaking changes that I discovered while migrating a recent .NET Core 1.1 application.


Auth changes .. there are many!

First, calls to “context.Authenciate.SignInAsync|SignOutAsync|ChallengeAsync” are now simply against the context: “context.SignInAsync|SignOutAsync|ChallengeAsync”

Configuration of the middleware handlers has moved from IAppBuilder “Use” extensions to IServiceCollection “Add” extensions. That is to say, many things moved from Middleware to being service-based.

For exmaple, configuring the Cookie middleware previously looked like this:

*** UPDATE ***

Since writing this post, which was based on .NET Core 2.0 preview, Authentication is more consolidated in the 2.0 RTM. With .NET Core 2.0 final, all authentication is attached to the IServiceCollection’s extension “AddAuthentication.” Additionally, the “CookieBuilder” object was thrown into the mix to consolidate Cookie settings.

This change is referenced here: https://github.com/aspnet/Announcements/issues/262

// Add authentication
services
	.AddAuthentication(o =>
	{
		o.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
		o.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
		o.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
	})
	.AddCookie(o =>
	{
		o.LoginPath = new PathString(oauthSettings.LoginPath);
		o.LogoutPath = new PathString(oauthSettings.LogoutPath);
		o.Cookie = new CookieBuilder()
		{
			Name = oauthSettings.CookieName,
			Domain = string.IsNullOrWhiteSpace(oauthSettings.CookieDomain) ? null : oauthSettings.CookieDomain,
			Path = oauthSettings.CookiePath,
			Expiration = TimeSpan.FromMinutes(oauthSettings.ExpireMinutes),
			SecurePolicy = CookieSecurePolicy.Always,
			HttpOnly = true
		};
		o.SlidingExpiration = oauthSettings.SlidingExpiration;
		o.ExpireTimeSpan = TimeSpan.FromMinutes(oauthSettings.ExpireMinutes);
		o.Events = new CookieAuthenticationEvents()
		{
			OnValidatePrincipal = async context => { await ValidateAsync(context); }
		},
		o.TicketDataFormat = new TicketDataFormat(dataProtector)
	})
	.AddOAuth(oauthSettings.AuthType, o =>
	{
		OAuthMiddlewareOptions.SetOptions(o, oauthSettings);
	});

And, there is a new IApplicationBuilder extention that must be called in your startup too.

// We still have to use it after adding.. :/
app.UseAuthentication();

.. Previous post resumed

Old:

// Add cookie middleware
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
	AutomaticAuthenticate = true,
	AutomaticChallenge = true,
	LoginPath = new PathString(oauthSettings.LoginPath),
	LogoutPath = new PathString(oauthSettings.LogoutPath),
	CookieName = oauthSettings.CookieName,
	CookieDomain = oauthSettings.CookieDomain,
	CookiePath = oauthSettings.CookiePath,
	SlidingExpiration = oauthSettings.SlidingExpiration,
	ExpireTimeSpan = TimeSpan.FromMinutes(oauthSettings.ExpireMinutes),
	Events = new CookieAuthenticationEvents()
	{
		OnValidatePrincipal = async context => { await ValidateAsync(context); }
	}
});

New:

// Add cookie middleware
services.AddCookieAuthentication(o =>
{            
    o.LoginPath = new PathString(oauthSettings.LoginPath);
    o.LogoutPath = new PathString(oauthSettings.LogoutPath);
    o.CookieName = oauthSettings.CookieName;
    o.CookieDomain = oauthSettings.CookieDomain;
    o.CookiePath = oauthSettings.CookiePath;
    o.SlidingExpiration = oauthSettings.SlidingExpiration;
    o.ExpireTimeSpan = TimeSpan.FromMinutes(oauthSettings.ExpireMinutes);
    o.Events = new CookieAuthenticationEvents()
    {
        OnValidatePrincipal = async context => { await ValidateAsync(context); }
    };
});

Similarly, adding OAuth middleware is different. I was using a static class to return a new instance of OAuthOptions:

// Add the OAuth2 middleware
app.UseOAuthAuthentication(MyMiddlewareOptions.GetOptions(oauthSettings));

private class MyMiddlewareOptions
{
	public static OAuthOptions GetOptions(OAuthSettings oauthSettings)
	{
		var options = new OAuthOptions
		{
			AuthenticationScheme = oauthSettings.AuthType,
			ClientId = oauthSettings.ClientId,
			ClientSecret = oauthSettings.ClientSecret,
			CallbackPath = new PathString(oauthSettings.CallbackPath),
			AuthorizationEndpoint = $"{oauthSettings.AuthServer}{oauthSettings.AuthPath}",
			TokenEndpoint = $"{oauthSettings.AuthServer}{oauthSettings.AuthTokenPath}",
			UserInformationEndpoint = $"{oauthSettings.AuthServer}/api/user",

			Scope = { "identity", "roles" },
			Events = new OAuthEvents
			{
				OnCreatingTicket = async context => { await CreateAuthTicket(context, oauthSettings); },
				OnTicketReceived = async context => { await TicketReceived(context); }
			}
		};

		return options;
	}
}

New way:

// Add the OAuth2 middleware
services.AddOAuthAuthentication(oauthSettings.AuthType, o =>
{
    MyMiddlewareOptions.SetOptions(o, oauthSettings);
});

private class MyMiddlewareOptions
{
    public static OAuthOptions SetOptions(OAuthOptions options, OAuthSettings oauthSettings)
    {
        options.SignInScheme = oauthSettings.AuthType;
        options.ClientId = oauthSettings.ClientId;
        options.ClientSecret = oauthSettings.ClientSecret;
        options.CallbackPath = new PathString(oauthSettings.CallbackPath);
        options.AuthorizationEndpoint = $"{oauthSettings.AuthServer}{oauthSettings.AuthPath}";
        options.TokenEndpoint = $"{oauthSettings.AuthServer}{oauthSettings.AuthTokenPath}";
        options.UserInformationEndpoint = $"{oauthSettings.AuthServer}/api/user";

        options.Scope.Add("identity");
        options.Scope.Add("roles");

        options.Events = new OAuthEvents
        {
            OnCreatingTicket = async context => { await CreateAuthTicket(context, oauthSettings); },
            OnTicketReceived = async context => { await TicketReceived(context); }
        };

        return options;
    }
}

And then we have enabling of the authentication pipeline that has changed.

Old:

// Add authentication
services.AddAuthentication(o => o.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme);

The SignInScheme was removed from the options and now we can define defaults in the new options:

// Add authentication
services.AddAuthentication(o =>
{
	o.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
	o.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
	o.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
});

Logging is significantly different. Previously, you would inject and ILoggerFactory into your Startup.cs’s “Configure” method and setup logging there. This method of configuration has been deprecated. Logging is configured via the WebHostBuilder now.

Old in Startup.cs:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    loggerFactory.AddDebug(LogLevel.Information);
    loggerFactory.AddDebug(LogLevel.Debug);
    loggerFactory.AddDebug(LogLevel.Error);

    // Add custom provider
    Func<IRepository<AppLogModel>> appLogRepoFactory = () => ServiceProviderFactory.ServiceProvider
         .GetService(typeof(IRepository<AppLogModel>)) as IRepository<AppLogModel>;            
    ((LoggerFactory)loggerFactory).AddProvider("myProvider", new AppLoggerProvider(appLogRepoFactory));

New in Program.cs:

var host = new WebHostBuilder()
    .UseConfiguration(config)
    .ConfigureLogging(factory =>
    {
        factory.UseConfiguration(appSettings.GetSection("Logging"));
        factory.AddConsole();
        factory.AddDebug();

        Func<IRepository<AppLogModel>> appLogRepoFactory = () => ServiceProviderFactory.ServiceProvider
             .GetService(typeof(IRepository<AppLogModel>)) as IRepository<AppLogModel>;
        factory.AddProvider("myProvider", new AppLoggerProvider(appLogRepoFactory));
    })

Notice how we no longer add filter levels in the extension methods (.AddDebug(LogLevel.Error) for example). Filters can be defined in our appSettings by name such as:

"Logging": {
  "IncludeScopes": false,
  "LogLevel": {
    "Default": "Debug",
    "System": "Information",
    "Microsoft": "Information"
  },
  "Debug": {
    "LogLevel": {
      "System": "Error",
      "Microsoft": "Information",
      "Microsoft.Extensions": "Warning"
    }
  }
}

Some of these details are on github:

https://github.com/aspnet/Announcements/issues/238

Kestrel hosting is different. I’ve previously blogged about getting HTTPS working with Kestrel.

Old code:

string env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
var config = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile(@"Properties/hosting.json", optional: false, reloadOnChange: true)
    .Build();

var host = new WebHostBuilder()
.UseConfiguration(config)
.UseKestrel(options => options.UseHttps(new X509Certificate2("testCert.pfx", "testPassword")));

New code (defining the certificate with the Listen extension):

var regEx = new Regex(@"((http[s]?):\/\/)([\w\d\.]*)(?:\:\d+)");
var rootUrl = regEx.Match(config["urls"]).Value;
var rootUrlPort = new Uri(rootUrl).Port;

var host = new WebHostBuilder()
.UseConfiguration(config)
.UseKestrel(options => options.Listen(IPAddress.Loopback, rootUrlPort, listenOptions =>
{
    listenOptions.UseHttps(new X509Certificate2("testCert.pfx", "testPassword"));
}));

Issue and sample on github:

https://github.com/aspnet/Announcements/issues/230
https://github.com/aspnet/KestrelHttpServer/blob/dev/samples/SampleApp/Startup.cs#L37-L43

Other random tidbits… I noticed that once I enabled “Information” logging with my Entity Framework based-logger, I got into a weird infinite loop. Entity Framework would log database commands. The logger would then execute an Entity Framework context save, which would trigger another Entity Framework logging request.

Fortunately, if you inspect the “TState state” on the Log<TState> ILogger execution, we can cast it it to a DbCommandLogData object and check its CommandText. In my case, I am building out my INSERT statement manually (to avoid loading of @SCOPE_IDENTITY needlessly) into the Log4Net schema which made the comparison easy.

Here’s the full method below. It does use some extension methods that I have blogged about previously to get various metadata. Also, for the full code, look here.

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
    try
    {
        if (!IsEnabled(logLevel))
        {
            return;
        }

        if (formatter == null)
        {
            throw new ArgumentNullException(nameof(formatter));
        }

        var message = exception == null ?
            formatter(state, exception) :
            $"{formatter(state, exception)}{Environment.NewLine}{Environment.NewLine}{exception}";

        if (string.IsNullOrWhiteSpace(message))
        {
            return;
        }

        var hostAddress = Dns.GetHostName();
        var hostEntry = Dns.GetHostEntryAsync(hostAddress).Result;
        var ipAddress = hostEntry
            .AddressList.Where(x => x.AddressFamily == AddressFamily.InterNetwork)
            .FirstOrDefault();

        var model = new AppLogModel()
        {
            Date = DateTime.UtcNow,
            Application = Trim($"{_name}", AppLogModel.MaxApplicationLength),
            Message = Trim(message, AppLogModel.MaximumMessageLength),
            Exception = Trim($"{exception}", AppLogModel.MaximumExceptionLength),
            Level = Trim($"{logLevel}", AppLogModel.MaxLoggerLength),
            Logger = Trim($"{_name}", AppLogModel.MaxLoggerLength),
            Thread = Trim($"{eventId}", AppLogModel.MaxThreadLength),
            IpAddress = Trim($"{ipAddress}", AppLogModel.MaxIpAddressLength),
            HostAddress = Trim($"{hostAddress}", AppLogModel.MaxHostLength)
        };

        // Use Raw SQL to avoid pulling back scope_identity
        var repo = _repoFactory();
        var tableName = repo.GetTableName<AppLogModel>();
        var columnNames = repo.GetColumnNames<AppLogModel>();
        StaticHelpers.GetPropertyName<AppLogModel>(x => x.Date);
        var sb = new StringBuilder($"INSERT [{repo.DbSchema}].[{tableName}] (");
        sb.Append($"{columnNames[StaticHelpers.GetPropertyName<AppLogModel>(x => x.Date)]}");
        sb.Append($",{columnNames[StaticHelpers.GetPropertyName<AppLogModel>(x => x.Application)]}");
        sb.Append($",{columnNames[StaticHelpers.GetPropertyName<AppLogModel>(x => x.Message)]}");
        sb.Append($",{columnNames[StaticHelpers.GetPropertyName<AppLogModel>(x => x.Exception)]}");
        sb.Append($",{columnNames[StaticHelpers.GetPropertyName<AppLogModel>(x => x.Level)]}");
        sb.Append($",{columnNames[StaticHelpers.GetPropertyName<AppLogModel>(x => x.Logger)]}");
        sb.Append($",{columnNames[StaticHelpers.GetPropertyName<AppLogModel>(x => x.Thread)]}");
        sb.Append($",{columnNames[StaticHelpers.GetPropertyName<AppLogModel>(x => x.IpAddress)]}");
        sb.Append($",{columnNames[StaticHelpers.GetPropertyName<AppLogModel>(x => x.HostAddress)]}");
        sb.Append($") VALUES (@Date, @Application, @Message, @Exception, @Level, @Logger, @Thread, @IpAddress, @HostAddress)");

        var commandText = sb.ToString();

        if ((state as Microsoft.EntityFrameworkCore.Storage.DbCommandLogData)?.CommandText == commandText)
        {
            return;
        }
  
        var sqlParams = new List<SqlParameter>()
        {
            new SqlParameter("@Date", model.Date),
            new SqlParameter("@Application", model.Application),
            new SqlParameter("@Message", model.Message),
            new SqlParameter("@Exception", model.Exception),
            new SqlParameter("@Level", model.Level),
            new SqlParameter("@Logger", model.Logger),
            new SqlParameter("@Thread", model.Thread),
            new SqlParameter("@IpAddress", model.IpAddress),
            new SqlParameter("@HostAddress", model.HostAddress)
        };

        repo.DbContext.Database.ExecuteSqlCommand(sb.ToString(), sqlParams);
        //repo.Add(model);
        //repo.Save();
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex);
    }
}

Leave a Reply