4 minute read

I’ve been working on a project the past few months in asp.net core 2.2 and am in the process of converting various aspects of it over to Blazor (more on that soon). The first step is getting everything converted, in place, to asp.net core 3.0.

Microsoft provides a great migration walkthrough here. However, there’s a LOT of information in there as they try to throw the net as wide as possible. While somethings are pointed out, there isn’t a lot of why involved which, if you’re like me, you tend to skip over if there’s no apparent difference between the examples and your own code. :smile:

Here’s some of the lessons I had during the start-to-finish migration process.

No. 1: When removing references to Microsoft.AspNetCore.* in your {project}.csproj file, add in the new ones via NuGet rather than copy/pasting their examples.

The guide has you add in several references automatically, but they include things like EntityFramework which not all projects use and are not always necessary. I’ve had better experience by determining my missing references and adding them via NuGet for a cleaner upgrade.

No. 2: If you’re using .IsDevelopment() and other extensions, be sure to add a using statement to Microsoft.Extensions.Hosting; they’ve been moved there.

For now, Visual Studio doesn’t seem to auto-suggest the reference. Could be something fixed soon in a tooling update.

No. 3: If you’re using your environment name in Startup, you can DI the new IWebHostEnvironment in and continue using it.

In v2.2, you could use services.BuildServiceProvider() to call your enviornment, such as using the environment name in variables and startup routines; however, this has been obsoleced.

Using dependency injection, add IWebHostEnvironment to your Startup constructor and a readonly property to use throughout Startup.

public IConfiguration Configuration { get; }
private readonly IWebHostEnvironment Environment { get; }

public Startup(IConfiguration configuration, IWebHostEnvironment environment) {
  Configuration = configuration;
  Environment = environment;

and then you can use it as needed. For example, separating out development from production Redis instances:

services.AddDistributedRedisCache(options => {
  options.Configuration = ",syncTimeout=5000,writeBuffer=8192";
  options.InstanceName = $"server-key-{Environment.EnvironmentName}";

No. 4: The order of operations in Configure really matters now.

Previously, the order didn’t matter as much; however, the new middlewares require a specific order.

app.UseStaticFiles() must come before any calls for routing or endpoints.

app.UseRouting() must come before any calls for CORS, Authentication/Authorization, or EndPoints.

app.UseAuthentication() must come before app.UseAuthorization() or you’ll have authenticated users who can’t seem to access anything. This one caught me for a while. It’s in that order in the Microsoft documentation, but wasn’t called out like some of the others that it mattered… it does.

app.UseEndPoints(), which replaces app.UseMvc(), must be the final call.

Here’s an example of a simplified Configure method and the order of operations that worked best for me.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {

  if (env.IsDevelopment()) {
  } else {

  app.UseStaticFiles(new StaticFileOptions {
    OnPrepareResponse = ctx => {
      const int durationInSeconds = 60 * 60 * 24 * 30; // 30 days
      ctx.Context.Response.Headers[HeaderNames.CacheControl] =
          "public,max-age=" + durationInSeconds;


  app.UseEndpoints(endpoints => {

No. 5: If you’re using C# code in your razor views inside of certain tags, such as <option>, you’ll need to escape them with !.

You might also receive an error of “The tag helper ‘option’ must not have C# in the element’s attribute declaration area.”. This is caused by the new processor trying to force it into using a tag helper.

To fix this, use ! to escape the tag as pure HTML.

<!option value="1" @(isSelected ? "selected" : "")>Some Option</!option>

Another way around this is reworking the HTML to use “selected=true”, such as, which is still valid, but twitches me out a bit.

<!option value="1" selected="@isSelected">Some Option</!option>

That’s it for now. I’ll tack on any other gotchas I find as the come up.

What migration issues, if any, have you ran into? Leave a comment to share your experiences and, if you’re like me, a reminder for next time you migrate! :laughing:

comments powered by Disqus