Migrating from ASP.NET MVC 5.x to ASP.NET Core

Migrating from ASP.NET MVC 5.x to ASP.NET Core

Introduction

We have recently moved our ASP.NET ZERO solution from ASP.NET MVC 5.2.3 to ASP.NET Core 1.0. In this post, I will share my experiences and explain mechanics of this migration in brief.

Notice that: I didn’t convert the solution to .NET Core, I just moved to ASP.NET Core on top of full .NET Framework 4.6.1.

Solution Structure

I decided to use new .xproj/project.json format instead of old .csproj format for the solution. Although Microsoft announced that .xproj/project.json format will also be changed and returning back to the .csproj format, actually new .csproj format will be different than the old one. So, I decided to use the latest format (as Microfost also does for ASP.NET Core platform) and migrate to the new .csproj format when the time comes.

There are pros and cons of project.json format, but I will not go to all details. The most big problem is it’s not well documented yet. I found some partial documents/articles and most of them was out-dated. So, “try and see” approach was my best friend in some cases.

Old solution structure was like that:

Old solution format

New solution structure is shown below:

New solution

.WebApi project has gone and solution is seperated to src & test folders (default convention for new solution structure).

Obviously, I created a new solution and empty projects inside it. Then copied all files into this solution manually. All layers (including test project) except the Web layer are successfully compiled in new project format without any change.

Nuget Packages

After moving code to new solution folder, I also added needed nuget package references to project.json. For example, my project.json file for new .Core project is like that:

{
  "version": "1.0.0.0-*",

  "dependencies": {
    "Abp": "0.11.2",
    "Abp.AutoMapper": "0.11.2",
    "Abp.Zero": "0.11.2",
    "Abp.Zero.Ldap": "0.11.2",
    "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0",
    "Microsoft.Extensions.Configuration.Json": "1.0.0",
    "Microsoft.Extensions.Configuration.UserSecrets": "1.0.0"
  },

  "frameworks": {
    "net461": {
    }
  },

  "buildOptions": {
    "embed": {
      "include": [
        "Localization/PhoneBook/*.xml",
        "Emailing/EmailTemplates/default.html"
      ]
    }
  }
}

Embedded Resources

Previously, we were just changing Build Action to Embedded Resource to embed file into an assembly. With the new xproj format, we should do it in project.json as shown above. I added all xml files in Localization/AbpZeroTemplate and default.html as embedded resource. Consuming an embedded resource hasn’t changed.

ASP.NET MVC

There was not code change for class library projects (except the project structure and dependencies). But when it comes to ASP.NET Core, we have much things to talk about.

Including Styles & Scripts To Views

This was the most time consuming and tedious part of the migration. Since we are using many client side libraries and we have many razor views, I manually changed all views to include related styles and scripts. An example css include approach in ASP.NET Core:

<environment names="Development">
    <link rel="stylesheet" href="~/view-resources/Areas/AppAreaName/Views/Users/Index.css" asp-append-version="true" />
</environment>

<environment names="Staging,Production">
    <link rel="stylesheet" href="~/view-resources/Areas/AppAreaName/Views/Users/Index.min.css" asp-append-version="true" />
</environment>

We are including un-minified version in development, and including minified version in production. Also, notice to the asp-append-version=”true” attribute, which saves us from caching styles/scripts by browsers, even it development time.

Script/Style Bundling & Minifiying

In the previous ASP.NET MVC, we were using Microsoft ASP.NET Web Optimization library to accomplish this.

I used new bunlder & minifier Visual Studio extension for minifying css/js files and bundling them when needed. Using this tool is much easy but another time consuming work. I created some bundled css/js files for my layout pages to reduce included css/js file count. Also, used minifying for all css/js files in individual razor views.

We are also using less for writing CSS. I used Web Compiler Visual Studio extension to compile less to CSS (and minify them automatically).

Bye Bye @helper

We were using @helper to write some simple functions rendering HTML in a razor view in ASP.NET 5.x. In new ASP.NET Core MVC, there is no @helper. So, I converted @helper blocks to partial views with converting @html parameters to view models.

Bye Bye Child Actions

ASP.NET Core introduced view components, which can be used instead of child actions. So, we converted all of child actions to components.

Switch View Base to RazorPage

Previously, we were creating view base classes deriving from WebViewPage. Now, we should derive it from RazorPage class. So, we changed our base view class. Also, we can now inject dependencies to razor views. So, I also changed static calls to injections.

Bye Bye Web API

ASP.NET MVC and Web API frameworks are unified in new ASP.NET Core framework and there is only ASP.NET MVC Controller anymore. So, I changed my Web API Controllers to MVC Controllers by following new rules. Fortunately, I hadn’t much Web API Controllers, thanks to ABP’s dynamic Web API layer, which is also implemented for ASP.NET Core. So, I just changed a few configuration lines to move all my application services from Web API Controllers to ASP.NET Core Controllers.

Entity Framework Migration Problems

We were using Entity Framework 6.x in our previous solution and we have decided to use EF 6.x in our new solution instead of migrating to EF Core 1.0. Some reasons of that decision were:

  • EF Core still has some missing features (like lazy loading, seed data and others) which we (and our customers) are using.
  • EF Core has some missing interception/extend points. Therefore, we can not use a consistent automatic data filtering tool (like EF.DynamicFilters) for multi tenancy and soft delete.

So, we decided to move our existing EF 6.x layer (and migrations) into new solution format, but it just didn’t work (before of embedded resource changes and other reasons). We found a tool, Migrator.EF, which allows us to add/apply migrations just like EF Core. We don’t use Package Manager Console (PMC) commands like “Update-Database”, but we are using Windows Command Prompt commands like “dotnet ef database update”.

Migrator.EF does good job. But what about our existing Migration classes? We manually changed their format to properly work in new tool.

Assume that we have a migration like that:

public partial class Added_ConnString_To_Tenant_Entity : DbMigration
{
    public override void Up()
    {
        AddColumn("dbo.AbpTenants", "ConnectionString", c => c.String(maxLength: 1024));
    }
    
    public override void Down()
    {
        DropColumn("dbo.AbpTenants", "ConnectionString");
    }
}

This class has no need to be changed. But.. notice that it’s a partail class. It’s other part (.Designer.cs) is like that:

[GeneratedCode("EntityFramework.Migrations", "6.1.3-40302")]
public sealed partial class Added_ConnString_To_Tenant_Entity : IMigrationMetadata
{
    private readonly ResourceManager Resources = new ResourceManager(typeof(Added_ConnString_To_Tenant_Entity));
    
    string IMigrationMetadata.Id
    {
        get { return "201604201100303_Added_ConnString_To_Tenant_Entity"; }
    }
    
    string IMigrationMetadata.Source
    {
        get { return null; }
    }
    
    string IMigrationMetadata.Target
    {
        get { return Resources.GetString("Target"); }
    }
}

As you see, it uses resource manager to get some metadata (model snapshot) from a resx file. We removed resource manager usage, and changed the Designer partial class like that:

[GeneratedCode("EntityFramework.Migrations", "6.1.3-40302")]
public sealed partial class Added_ConnString_To_Tenant_Entity : IMigrationMetadata
{
    string IMigrationMetadata.Id
    {
        get { return "201604201100303_Added_ConnString_To_Tenant_Entity"; }
    }

    string IMigrationMetadata.Source
    {
        get { return null; }
    }

    string IMigrationMetadata.Target
    {
        get { return "H4s..........AQA="; }
    }
}

Now, Target property returns the hash value instead of getting it from resource manager. To do that, you should open the .resx file and copy hash to here.

Then we could able to run migrator tool.

Model Binding Changes

ASP.NET Core has similar model binding approach with the previous version but also has some differences. For example, see the action below:

public JsonResult SwitchToLinkedAccount(SwitchToLinkedAccountModel model)
{
    //....
}

This model binding will not work if your client requested with a JSON in HTTP body. We should explicitly add [FromBody] attribute to the parameter:

public JsonResult SwitchToLinkedAccount([FromBody] SwitchToLinkedAccountModel model)
{
    //....
}

Read the model binding document to fully understand new model binding approach.

wwwroot Folder

Now, we have a wwwroot folder which is actually the root folder for our static files (like js, css and image files for our web application). I moved all such files into this new folder.

Client Side Dependencies

We were mostly used nuget for js/css libraries in previous ASP.NET, and we couldn’t find all libraries and their latest versions in the nuget. Nuget has also other problems with client libraries containing static files. Fortunately, now bower is fully integrated to Visual Studio 2015. So, I added all libraries with bower.

One more good thing; Nuget does not add package files into the solution anymore and uses from a central directory in the local computer, which is much more performant and does not add unnecessary files into our solution folder.

Startup File

ASP.NET Core is initialized from the Startup class in the application. We configure all libraries (including ABP) in this class. So, I removed old global.asax and configured fundamental libraries in Startup class.

Social Logins

Since authentication infrastructure is completely changed, we should not use OWIN based social login middlewares anymore. Instead, I moved my code to new social auth packages based on this documentation.

SignalR & OWIN Integration

As you probably know, there is no SignalR version for ASP.NET Core yet (see road map). Current SignalR (2.x) works on OWIN. While ASP.NET Core has OWIN support, it was not easily to properly integrate SignalR to ASP.NET Core pipeline. Fortunately, I did it.

First step is to make OWIN pipeline properly integrated to ASP.NET Core middleware pipeline. I created an extension method based on some code I found:

public static class BuilderExtensions
{
    public static IApplicationBuilder UseAppBuilder(
        this IApplicationBuilder app,
        Action<IAppBuilder> configure)
    {
        app.UseOwin(addToPipeline =>
        {
            addToPipeline(next =>
            {
                var appBuilder = new AppBuilder();
                appBuilder.Properties["builder.DefaultApp"] = next;

                configure(appBuilder);

                return appBuilder.Build<Func<IDictionary<string, object>, Task>>();
            });
        });

        return app;
    }
}

Then I can use MapSignalR method to add SignalR to the pipeline as shown below:

app.UseAppBuilder(appBuilder =>
{
    appBuilder.Properties["host.AppName"] = "MyProjectName";
    appBuilder.MapSignalR();
});

User Secrets

ASP.NET Core has a nice tool to store sensitive configuration values out of the solution folder. Thus, our secret or custom configuration values (like passwords) remain in our local computer, instead of source control system. I used it to store Social Login API keys for example.

Token Based Authentication

In the previous ASP.NET, it was relatively easy to add a token based auth mechanism to our applications. It was including infrastructure to generate and validate tokens. In ASP.NET Core, there is no mechanism to generate tokens (but there is a package to validate it). So, I investigated much, tried different OpenId Connect Servers (ASOS, Identity Server 4…) and finally decided to build my own, custom and simple token generator middleware based on this article.

Swagger & Swagger UI

We previously were using Swashbuckle to add Swagger API document generator for our application. Fortunately, they created a version compatiple to ASP.NET Core and we easily switched to that new version (which is currently in beta).

6 Comments
  • Jonathas Morais
    Reply
    Posted at 16:14, October 18, 2016

    Nice post.
    When do you think we will be able to use ABP in .NET Core (and multi-platform)?

    How can the community be of some help on this (porting to DotNet Core)?

  • Stevo
    Reply
    Posted at 19:40, February 21, 2017

    Share some experience how did you solve nugget packages not compatible with .net core

  • ShiningRush
    Reply
    Posted at 07:35, March 20, 2017

    Hi,
    I Wonder what benefits achieved from migrating to asp.net core, but still be dependent on .net framework.

    I Consider that the structure is unable to cross platform since the dependency of .net framework.

Post a Comment

Comment
Name
Email
Website