Off late, there has been a lot of discussion on the latest iteration of dotnet core aka dotnet core 3.0. Although released around a month ago, its until recently that the market has started to embrace the third major release of the open source dotnet framework, which brings in some major changes internally. While the introduction of desktop application frameworks like the WPF to dotnet core ecosystem has been the salient feature, there have also been other changes such as introduction of framework dependent .exe file generated when published, incorporation of the C# 8.0 along with its features such as the Indexer and asynchronous stream features to name a few. And recently i got a chance to look for myself all the major and minor changes that have been put to force in this version. I was asked to upgrade an existing asp.net core mvc application written in dotnet core 2.2 to the latest version aka the dotnet core 3.0. And let’s talk about how I was able to upgrade my application while having a first hand experience of the new framework at hand.
Setting up the Context – The Existing Readers Application in dotnet core 2.2:
The existing application was a ReadersApi application which is built on the dotnet core 2.2 open source framework. The application has an API for data operations on Readers which is secured by JWT Token Authorization. Further the attributes for the token configuration is fetched from the configuration by means of a strongly typed configuration class which is consumed during the app startup. Also the application has SwaggerUI configured for API documentation. The startup class for the application is as shown below:
namespace ReadersApi
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IConfigManager, ConfigManager>();
services.AddSingleton<ITokenManager, TokenManager>();
services.AddSingleton<IReaderRepo, ReaderRepo>();
//build the service pipeline and fetch the service instance of
//ConfigManager to pass to the JWT Authentication Middleware
var sp = services.BuildServiceProvider();
var config = sp.GetRequiredService<IConfigManager>();
services.AddBearerAuthentication(config);
services.AddDbContext<MyContext>(builder =>
{
builder.UseSqlServer(config.ConnectionString);
});
services.AddMvc();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info { Title = "Reader API", Version = "V1" });
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
app.UseMvc();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
}
}
}
And the AddBearerAuthentication middleware is configured as below:
namespace ReadersApi
{
static class AuthorizationExtension
{
public static IServiceCollection AddBearerAuthentication(
this IServiceCollection services, IConfigManager configManager)
{
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateAudience = true,
ValidateIssuer = true,
ValidateLifetime = true,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(configManager.key)),
ValidIssuer = configManager.Issuer,
ValidAudience = configManager.Audience
};
options.Events = new JwtBearerEvents()
{
OnAuthenticationFailed = (context) =>
{
Console.WriteLine(context.Exception);
return Task.CompletedTask;
},
OnMessageReceived = (context) =>
{
return Task.CompletedTask;
},
OnTokenValidated = (context) =>
{
return Task.CompletedTask;
}
};
});
return services;
}
}
}
The ConfigManager is defined as below:
namespace ReadersApi.Providers
{
public interface IConfigManager
{
string Issuer { get; }
string Audience { get; }
int ExpiryInMinutes { get; }
string key { get; }
string ConnectionString { get; }
}
public class ConfigManager : IConfigManager
{
private readonly IConfiguration _config;
public ConfigManager(IConfiguration config)
{
_config = config;
}
public string Issuer => _config["Issuer"];
public string Audience => _config["Audience"];
public int ExpiryInMinutes => int.Parse(_config["ExpiryInMinutes"]);
public string key => _config["key"];
public string ConnectionString =>
_config.GetConnectionString("DefaultConnection");
}
}
and finally the csproj file:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.2.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.2.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="2.2.2" />
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0"
PrivateAssets="All" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="4.0.1" />
</ItemGroup>
</Project>
Now this entire setup is in ASP.NET Core 2.2 as already mentioned. Let’s begin by installing the dotnet core 3.0 sdk from the official microsoft downloads page.
Once installed, let’s quickly create a sample mvc project using the dotnet core CLI, to see if there’s any change in the sample project boilerplate code.
dotnet new webapi --name netcore3app
The resultant application is an mvc app with views and controllers along with the assets in wwwroot folder.
If we look at the startup.cs generated:
namespace dotnetcore3app
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime.
// Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
}
// This method gets called by the runtime.
// Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
Comparing it with the startup class of the existing ReadersApi project, observe that a lot has changed in the default setup code for inducting Mvc. Instead of the services.AddMvc() and app.UseMvc() method calls under ConfigureServices() and Confiugre() methods, we now have services.AddControllers() and app.UseRouting() together with app.UseEndpoints() which replace the functionalities of the former set. We now have endpoint based routing configurations with corresponding changes in CORS policies.
Further on observing the csproj file of the sample project, we see that it’s
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
</ItemGroup>
</Project>
empty! There’s no reference to Microsoft.AspNetCore.App or Microsoft.AspNetCore.Razor.Design which were a prerequisite for the former version.
Done with the comparison, let’s begin making changes to the ReadersApi in line with the newer version.
Step 1: The csproj:
Begin by changing the TargetFramework value from netcoreapp2.2 to netcoreapp3.0 in the PropertyGroup at the beginning. Next, remove the unnecessary Microsoft.AspNetCore.App package reference from the xml. Also, update all the nuget packages as needed.
"dotnetcore 3.0 doesn’t need Microsoft.AspNetCore.App nuget as it was in dotnetcore 2.2"
Step 2: Program.cs:
One major difference in the latest version of dotnet core is the inclusion of a generic IHost and IHostBuilder interfaces in place of the IWebHost and IWebHostBuilder present in the previous iteration. The older interfaces are still supported in dotnet core 3.0, but its better to move on to the newer implementations available. The major difference in the IHostBuilder is that it no longer supports constructor injection of Startup class with custom services, which was possible in IWebHostBuilder. Reason is simple – performance. The IWebHostBuilder, because of its provision for partial injection of services resulted in generation of service pipeline twice; one when the injections are made via the IWebHost and second when the entire ConfigureServices method is executed. To avoid this, the IHostBuilder no longer supports all this. The previous Host building logic is now replaced as below:
The Program.cs of the aspnetcore2.2 app
namespace ReadersApi
{
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
}
The Program.cs of the newer aspnetcore3.0 app
namespace dotnetcore3app
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
"dotnetcore 3.0 uses the newer IHostBuilder interface to bootstrap the application in place of IWebHost which removes Startup constructor injection ability."
Step 3: The Startup class:
As mentioned earlier, there’s no way we can inject custom dependencies into the Startup constructor apart from IConfiguration and IWebHostEnvironment. Observe that we’re talking about IWebHostEnvironment interface and not IHostingEnvironment available in the previous version. And there’s no way we can be able to build and use any services in ConfigureServices() like we did earlier. In the case of ReadersApi we require ConfigManager instance be passed onto the Authentication middleware for reading the token validation parameters.
var sp = services.BuildServiceProvider();
var config = sp.GetRequiredService<IConfigManager>();
services.AddBearerAuthentication(config);
Also observe the code snippet for setting up the DbContext using EF Core.
services.AddDbContext<MyContext>(builder =>
{
builder.UseSqlServer(config.ConnectionString);
});
This is to be avoided in the latest version. One workaround for this is by lazy loading the Authentication module onto the pipeline after all the injectables have been loaded. This can be achieved by means of NamedOptions. We would need to bring in similar changes for the DbContext which uses the same object for connection string access. Also, we would need to look at what changes we need to bring in to implement SwaggerUI in the latest setup.
We’ll look at these things in the second part of this article.