Migrate ASP.NET application to .NET Core

Coding Languages

More and more people are talking about .NET Core. True, .NET Core is the future, but the .NET Framework is still supported because a large number of applications cannot be migrated in a short time.

The .NET Core and .NET Framework are like electric cars and petrol-powered cars. Gasoline cars are mature, you can drive it without any problems, but electric cars have their advantages and are replacing gasoline cars. So don’t get me wrong, you should start moving to .NET Core today.
I have migrated several traditional ASP.NET/MVC projects running on the full .NET Framework and IIS to ASP.NET Core 2.x, which can run in IIS or non-IIS environments.

My blog is one of them. This is a 10-year-old blogging system originally written by ASP.NET 2.0 Web Form and Visual Basic. Since 2008, I have been updating the code base for the latest .NET technology. The .NET Core version of the blog system will arrive at the end of this year. I am writing this article to document the roadblocks I have encountered and how to solve them.

This article is aimed at developers who are new to .NET Core but have experience with the .NET Framework to help them transition their existing applications to .NET Core more smoothly.

1 Migrate or rewrite

Sometimes, I prefer to use “rewrite” instead of “migration” because in some cases, .NET Core and the .NET Framework are two completely different things.

In my experience, most of the front-end code can be ported directly to .NET Core with only minor modifications, because their nature is server-independent and inherently cross-platform. As for back-end code, the cost of migration depends on how well they couple to Windows and IIS. I understand that some applications take full advantage of the features of Windows and IIS, so developers can avoid having to work hard to implement some features. These include scheduled tasks, the registry, Active Directory, or Windows services. These are not directly portable because .NET Core is cross-platform. For these parts, you might want to consider redesigning your business logic and think of a way to do the same thing, but not rely on Windows or IIS components.

For historical legacy code that cannot be migrated, you may want to consider redesigning the entire application’s architecture and expose these features as a REST API, which can be implemented using the ASP.NET Web API on the .NET Framework. In this way, your ASP.NET Core application can continue to use these APIs and continue to do business functions.

If your app uses WCF services, even older ASMX services, this may not work. Because .NET Core does not currently support calling WCF. Unless you can update your WCF service to expose the REST protocol. But REST and WCF are not completely functional, such as duplex communication. In some cases, you need to redesign your API for REST before the application layer migrates to .NET Core.

2 NuGet package management

Make sure you need to use the NuGet package to support .NET Core or .NET Standard. If not, then you need to investigate whether there is a NuGet package that can be replaced, or if you can write your own code to achieve the same functionality.

.NET Standard means that this package can be used in both .NET Framework 4.6.1+ and .NET Core, which replaces the old Portable Class Library (PCL) technology. So, if you see a package with .NET Standard in its dependencies, this means you can install it into your .NET Core project.

Some packages, such as NLog, have a special .NET Core version, such as NLog.Web.AspNetCore, you should choose to use such a version.

You can still reference a .NET Framework package in a .NET Core project, but this will allow your application to run on Windows only. This is not recommended.

I’ve listed some popular NuGet packages that already support .NET Core:

NLog.Web.AspNetCore
Newtonsoft.Json
HtmlAgilityPack
RestSharp
NUnit
Dapper
AutoMapper
Moq

For client packages, such as jQuery, don’t use NuGet to install them into a .NET Core project, see the “Client Package Management” section of this article.

If you use Visual Studio Code for .NET Core development, please note that the command to install the NuGet package is not Install-Package, which is used by Visual Studio’s PowerShell host. In VSCode, you need to use the dotnet CLI tool, such as:

dotnet add package Newtonsoft.Json
See https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-add-package

3 client package management

ASP.NET Core used Bower to manage client packages. But in the latest ASP.NET Core 2.1, Bower has been removed because the author is not doing it. Therefore, Microsoft uses its own package manager “Library Manager” or “libman” to manage front-end packages by default. It can be used in Visual Studio and Visual Studio Code, and even in the CLI using the command line.

libman.json can be edited directly or in the UI, with IntelliSense support. My advice is to use libman to manage front-end packages if your application is not a heavy client, as other technologies such as NPM are too heavyweight. You’ll want to install and configure NodeJS and everything else on your build server just to pull a jQuery library.

See the official documentation for more details https://docs.microsoft.com/en-us/aspnet/core/client-side/libman/?view=aspnetcore-2.1

4 Html / JavaScript / CSS

You can copy these files directly into the .NET Core project. But make sure you have modified the file path correctly, such as the image file path in CSS. Because traditional ASP.NET / MVC templates use the “/Content/” directory by default, and .NET Core templates use “/css/”, “/js/”, “/lib/”, etc., this is not mandatory, just Conventional conventions.

If you want to bundle and compress CSS and JS files, there are many tools you can do. I personally like to use a plugin for VS called “Bundler & Minifier”, which you can get from https://github.com/madskristensen/BundlerMinifier.

This plugin can generate bundled and compressed files at development time, but not compiled or run.

5 App_Data folder

In traditional ASP.NET/MVC applications, you can save data files to a special folder called “App_Data”, but this stuff no longer exists in .NET Core. In order to achieve similar functionality, you need to create a folder called “App_Data” yourself, but outside the “wwwroot” directory.

Then use like this

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // set
    string baseDir = env.ContentRootPath;
    AppDomain.CurrentDomain.SetData("DataDirectory", Path.Combine(baseDir, "App_Data"));
    // use
    var feedDirectoryPath = $"{AppDomain.CurrentDomain.GetData("DataDirectory")}\\feed";
}

6 Custom Http Headers

In traditional ASP.NET, you can configure a custom HTTP header for each response in Web.Config like this:

<httpProtocol>
  <customHeaders>
    <add name="X-Content-Type-Options" value="nosniff" />
  </customHeaders>
</httpProtocol>

In .NET Core, if you want to deploy your application from Windows, you can’t use the Web.config file. Therefore, you need a three-way NuGet package to do this: NetEscapades.AspNetCore.SecurityHeaders.

app.UseSecurityHeaders(new HeaderPolicyCollection()
    .AddCustomHeader("X-UA-Compatible", "IE=edge")
    .AddCustomHeader("X-Developed-By", "Edi Wang")
);
See https://github.com/andrewlock/NetEscapades.AspNetCore.SecurityHeaders for details.

7 Get the client IP address and HttpContext

In traditional ASP.NET, we can get the client IP address through Request.UserHostAddress. But this property does not exist in ASP.NET Core 2.x. We need another way to get HTTP request information.

  • Define a private variable in your MVC controller
private IHttpContextAccessor _accessor;
  • Initialize it using constructor injection
public SomeController(IHttpContextAccessor accessor)
{
    _accessor = accessor;
}
  • Get the client IP address
_accessor.HttpContext.Connection.RemoteIpAddress.ToString()

It’s that simple.
If your ASP.NET Core project was created with the MVC default template, registration for HttpContextAcccessor dependency injection should be done in Startup.cs:

services.AddHttpContextAccessor();
services.TryAddSingleton<IActionContextAccessor, ActionContextAccessor>();

The type of RemoteIpAddress is IPAddress and not string. It contains IPv4, IPv6 and other information. This is not the same as traditional ASP.NET and is more useful to us.

If you want to use it in the Razor view (cshtml), just inject it into the view with the @inject directive:

@inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContextAccessor

Use it as:

Client IP: @HttpContextAccessor.HttpContext.Connection.RemoteIpAddress.ToString()

8 JsonResult

By default, ASP.NET Core uses camelCase to serialize JsonResult , whereas traditional ASP.NET MVC uses PascalCase, which causes JavaScript code that relies on Json results to burst.

For example the following code:

public IActionResult JsonTest()
{
    return Json(new { Foo = 1, Goo = true, Koo = "Test" });
}

It will return the Json of camelCase to the client:

If you have a lot of JavaScript code and can’t change to use camelCase in time, you can still configure ASP.NET Core to output PascalCase’s Json to the client.

public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddMvc()
        .AddJsonOptions(options => options.SerializerSettings.ContractResolver = new DefaultContractResolver());
}

Now, the previous code will return the result of PascalCase:

10 HttpModules and HttpHandlers

Both are replaced in the ASP.NET Core for Middleware. But before migrating, you can consider using other methods to implement these functions in a normal ASP.NET Core Controller.

For example, my old blog system has an HttpHandler called “opml.axd” that outputs an XML document to the client. This can be done entirely with the Controller:

public async Task<IActionResult> Index()
{
    var opmlDataFile = $"{AppDomain.CurrentDomain.GetData(Constants.DataDirectory)}\\opml.xml";
    if (!System.IO.File.Exists(opmlDataFile))
    {
        Logger.LogInformation($"OPML file not found, writing new file on {opmlDataFile}");
        await WriteOpmlFileAsync(HttpContext);
        if (!System.IO.File.Exists(opmlDataFile))
        {
            Logger.LogInformation($"OPML file still not found, something just went very very wrong...");
            return NotFound();
        }
    }
    string opmlContent = await Utils.ReadTextAsync(opmlDataFile, Encoding.UTF8);

    if (opmlContent.Length > 0)
    {
        return Content(opmlContent, "text/xml");
    }
    return NotFound();
}

I have also used HttpHandler to complete Open Search, RSS/Atom and other functions, and they can also be rewritten as Controller.

For other components that cannot be rewritten as MVC Controller, such as requests to handle special extensions. See: Https://docs.microsoft.com/en-us/aspnet/core/migration/http-modules?view=aspnetcore-2.1

11 IIS URL Rewrite

You can still use the exact same configuration files as in the old application, regardless of whether your .NET Core application is deployed on IIS.

For example, create a file called “UrlRewrite.xml” under the application root directory, as follows:

<rewrite>
  <rules>
    <rule name="Redirect Misc Homepage URLs to canonical homepage URL" stopProcessing="false">
      <match url="(index|default).(aspx?|htm|s?html|php|pl|jsp|cfm)"/>
      <conditions logicalGrouping="MatchAll" trackAllCaptures="false">
        <add input="{REQUEST_METHOD}" pattern="GET"/>
      </conditions>
      <action type="Redirect" url="/"/>
    </rule>
  </rules>
</rewrite>

Note: You must set this file to always copy to the output directory, otherwise it will be invalid!

<ItemGroup>
  <None Update="UrlRewrite.xml">
    <CopyToOutputDirectory>Always</CopyToOutputDirectory>
  </None>
</ItemGroup>

Open Startup.cs and add the following code to the Configure method:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    ...
    using (var urlRewriteStreamReader = File.OpenText("UrlRewrite.xml"))
    {
        var options = new RewriteOptions().AddIISUrlRewrite(urlRewriteStreamReader);
        app.UseRewriter(options);
    }
    ...
}

12 Web.config

The Web.config file is not completely dead. In In .NET Core, a web.config file is still used to deploy websites in an IIS environment. In this scenario, the configuration in Web.config only works on IIS, and has nothing to do with your application code. See https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/iis/index?view=aspnetcore-2.1#configuration-of-iis-with-webconfig

The web.config file for deploying an ASP.NET Core application under a typical IIS is as follows:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <location path="." inheritInChildApplications="false">
    <system.webServer>
      <handlers>
        <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" />
      </handlers>
      <aspNetCore processPath="dotnet" arguments=".\Moonglade.Web.dll" stdoutLogEnabled="false" stdoutLogFile="\\?\%home%\LogFiles\stdout" />
    </system.webServer>
  </location>
</configuration>

The previous AppSettings node can be migrated to appsettings.json, which is explained in this article: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-2.1

13 Session and Cookie

ASP.NET Core does not have Session support enabled by default. You must manually add Session support.

services.AddDistributedMemoryCache();
services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromMinutes(20);
    options.Cookie.HttpOnly = true;
});

as well as

app.UseSession();

Set and get the Session value:

HttpContext.Session.SetString("CaptchaCode", result.CaptchaCode);
HttpContext.Session.GetString("CaptchaCode");

Remove value:

context.Session.Remove("CaptchaCode");

14 Html.Action

We used Html.Action to call an Action, return a Partial View, and then display it in the main View, such as the layout page. This is very widely used in Layout pages, such as displaying widgets such as categorical lists in a blog system.

@Html.Action("GetOrderList", "Category")

In ASP.NET Core, it is replaced with ViewComponents, see https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components

One thing to note is that the Invoke method can only be async signed:

async Task<IViewComponentResult> InvokeAsync();

But if your code isn’t born asynchronous, you can add this line of code in order to prevent the compiler from alerting:

await Task.CompletedTask;

15 Check running environment is Debug or Release

In my old system, I used HttpContext.Current.IsDebuggingEnabled to check if the current running environment is Debug and display the word “(Debug)” on the title bar.

@if (HttpContext.Current.IsDebuggingEnabled)
{
    <text>(Debug)</text>
}

In ASP.NET Core, we can use the new razor tag helper to do this.

<environment include="Development">
    (Debug)
</environment>

16 new Razor Tag Helpers

Tag helper can help you talk about the old HTML helper simplified to more HTML-readable code, such as a form, we used to write:

The result of converting to Tag Helpers is this:

My personal favorite feature is to automatically add a version string to a JS or CSS file: <script src=”~/js/app/ediblog.app.min.js” asp-append-version=”true”></script>

Its result is: <script src=”/js/app/ediblog.app.min.js?v=lvNJVuWBoD_RVZwyBT15T_i3_ZuEIaV_w0t7zI_UYxY”></script>

The new razor syntax is compatible with previous HTML helpers, which means that you can still use old HTML helpers without problems in ASP.NET Core. If your application migration time is tight, you can use the old code first and then gradually switch to Tag Helpers.

For a complete list of introductions and grammars, see https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro?view=aspnetcore-2.2

17 Anti-Forgery Token

There are some improvements to the Anti-forgery token. First, you can customize the name of the cookie and the field.

services.AddAntiforgery(options =>
{
    options.Cookie.Name = "X-CSRF-TOKEN-MOONGLADE";
    options.FormFieldName = "CSRF-TOKEN-MOONGLADE-FORM";
});

Second, you no longer need to manually add this line of code to each form:

@Html.AntiForgeryToken();

If you use the new form tag helper, the anti-forgery field is automatically added when it is output to the client.

But you still need to add the [ValidateAntiForgeryToken] attribute to the corresponding Action in the background.

However, there is another way to automatically verify the anti-forgery token for each POST request.

services.AddMvc(options =>
options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()));

Or you can add this property to a Controller separately.

[Authorize]
[AutoValidateAntiforgeryToken]
public class ManageController : Controller

18 Using dependency injection for non-Controller

ASP.NET Core has its own DI framework that can be used on the Controller. We can modify the constructor of a Controller to inject the services it depends on.

public class HomeController : Controller
{
    private readonly IDateTime _dateTime;

    public HomeController(IDateTime dateTime)
    {
        _dateTime = dateTime;
    }
}

But this does not mean that the built-in DI framework can only be used on the Controller. For other classes, you can use the exact same DI, for example, my custom class, or use the constructor injection:

public class CommentService : MoongladeService
{
    private readonly EmailService _emailService;

    public CommentService(MoongladeDbContext context,
        ILogger<CommentService> logger,
        IOptions<AppSettings> settings,
        EmailService emailService) : base(context, logger, settings)
    {
        _emailService = emailService;
    }
       // ....
}

The way is, as long as you register the custom class in the DI container in Startup.cs.

services.AddTransient<CommentService>();

For more information on how to use ASP.NET Core dependency injection, see https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/dependency-injection?view=aspnetcore-2.2

19 API behavior is inconsistent

Some code from traditional ASP.NET can be compiled without errors, but this does not guarantee success at runtime. For example, this code from ASP.NET (.NET Framework) throws an exception in ASP.NET Core:

var buffer = new byte[context.Request.Body.Length];
context.Request.Body.Read(buffer, 0, buffer.Length);
var xml = Encoding.Default.GetString(buffer);

Its result is:

System.NotSupportedException: Specified method is not supported.
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpRequestStream.get_Length()

In .NET Core, we need to implement it in a different way:

var xml = await new StreamReader(context.Request.Body, Encoding.Default).ReadToEndAsync();

20 Be careful the problems caused by GDPR

ASP.NET Core 2.2 adds support for GDPR by default, but it also brings us some problems. See https://docs.microsoft.com/en-us/aspnet/core/security/gdpr?view=aspnetcore-2.2 for GDPR

The main problem is that cookies do not work until the user accepts the GDPR protocol. You need to check which cookies are necessary for your application to run. Instant users do not accept the GDPR protocol and mark them as IsEssential

This is an example of my blog system:

private void SetPostTrackingCookie(CookieNames cookieName, string id)
{
    var options = new CookieOptions
    {
        Expires = DateTime.UtcNow.AddDays(1),
        SameSite = SameSiteMode.Strict,
        Secure = Request.IsHttps,
        // Mark as essential to pass GDPR
        // https://docs.microsoft.com/en-us/aspnet/core/security/gdpr?view=aspnetcore-2.2
        IsEssential = true
    };
    Response.Cookies.Append(cookieName.ToString(), id, options);
}

Another problem is that if you want to use a Session, the user must accept the GDPR policy, otherwise the Session will not work. Because the Session needs to rely on cookies to save the SessionID on the client.

21 Hot Updates Views

In traditional ASP.NET MVC, the Views folder is not compiled into DLL files by default, so we can update the razor page without compiling the entire application. This is very useful in scenarios where only text or some layout modifications are needed without updating the C# code. I sometimes use this feature to post some modified pages directly to the production environment.

However, ASP.NET Core 2.1 compiles our Views into DLLs by default to improve performance. Therefore, you cannot modify a view directly on the server because there is no Views at all in the folder, only one *.Views.dll:

If you still want to hot update Views in ASP.NET Core, you need to manually modify the csproj file:

<PropertyGroup>
  <TargetFramework>netcoreapp2.1</TargetFramework>
  <RazorCompileOnBuild>false</RazorCompileOnBuild>
  <RazorCompileOnPublish>false</RazorCompileOnPublish>
</PropertyGroup>

22 compiled version number grows

In traditional .NET applications, we can modify “AssemblyInfo.cs” to automatically increase the version number each time we compile. This is very common in the build server.

[assembly: AssemblyVersion(“9.0.*”)]

The result is this:
9.0.6836.29475

Unfortunately, .NET Core does not currently have a built-in method to do this. Only one tripartite solution might be useful: https://github.com/BalassaMarton/MSBump

End

ASP.NET Core has a lot of differences compared to traditional ASP.NET, and there are certain limitations. This article only covers the problems I have encountered, and there must be a lot of situations that I have not encountered.

Leave a Reply