Migration from .NET Framework to ASP.NET Core

aspnetcoremigration

Migration aspect is a major decision and not always it follows a green field path. There can be numerous reasons for taking up the decision to migrate an application codebase from its legacy form to future state or performing database migration or even infrastructure migration from on-premise to cloud. All migration aspects turns out to be a major disaster if proper strategies are not defined and lack efficient guidelines and resource management.

Anyway, this post emphasize on carrying out the migration aspect of our sample ASP.NET Web API solution that we had build earlier to ASP.NET Core. However, as the approach will follow a migration aspect with few basic core changes, I will not emphasize it as one of the green field project. Every migration aspect differ with its own set of issues and options to handle them.

Why migrating to .NET Core

Definitely this one is a good question that we developers bear in our mind knowing the fact that the existing codebase suffice our business needs and is in much stable state. It’s like asking someone who is looking forward to upgrade his four wheeler from a sedan to suv. Well, the relevant answer you might get is to gain more flexibility, power and adaptable features. Same kind of thoughts goes here too when you would like to migrate an application build on .NET framework to .NET core.

Few of the parameters that come to my mind for this migration are like better testability of application, ability to run on any platform and not restricted to windows only, easily available for cloud deployment and leveraging configuration management, ability to host anywhere and lightweight with high performance. Having said that, let us look into the migration aspect of the application.

Is there any migration tool

Honestly, as far as I know we don’t have any specific migration tool that can help you to migrate the whole application to .NET core. This has to follow step-by-step strategic approach. However, my initial approach would be to use .NET Portability Analyzer, that can be used to identify the compatibility of the migration approach. You can either download the tool or add it from visual studio extensions. Let’s look into that first and see what the analyzer gives us.

We will first search for .NET Portability Analyzer from Visual Studio extensions and download it.

image

Once installed, right-click the solution and select Portability Analyzer Settings, which will give you the configuration management tool for the Analyzer. In this tool you can define the output directory where the analyzed report will get generated, output format of the report (XLS, HTML or JSON) and the Target Platforms to where you want the codebase to get migrated which for us would be .NET Core 2.1 and .NET Standard.

Now, as best practice one thing that I am sure you know is that all class libraries should be converted to .NET Standard instead of .NET Core. The only reason for doing this is because, many external or 3rd party libraries like Newtonsoft.JSON support .NET Standard rather than .NET core. And moreover .NET Standard is lower than .NET core hence it is compatible to .NET Core version.

image

Since I have only one API project in my solution and I want to migrate it to ASP.NET Core, hence I will select on ASP.NET Core as my target framework for portability analysis

image

Once the settings are done, go ahead and right click the API project and select Analyze Project Portability, to start the analysis.

image

Once the analysis is complete, you can review the output generated in HTML or Excel format like shown here.

image

Based on the report, most highlighting factors or rather assemblies that .NET Core is not supporting are –

System.Configuration.ConfigurationManager

System.Net.Http.Formatting.MediaTypeFormatter

System.Net.Http

System.Web.Http

System.Web.Mvc

System.Web.Routing

Since this was the report generated for Web API, a few non-compatible assemblies are listed. If you have a complex project with various class libraries and 3rd party libraries, you might see a different result altogether. At the end of this post, I will try to provide some of the incompatible libraries that I had encountered in a real time project conversion and the alternate usage in .NET Core.

Starting the migration aspect

The portable analysis report will provide only few details to get started. However, it might not cover everything which you might encounter during compilation or execution phase of the converted application.

Since we don’t have any migration utility available yet, our first step would be to create a fresh new solution having same number of projects as in .NET framework application but targeting to ASP.NET Core 2.1 and .NET Standard (higher version).

What if I have complex structure having multiple class libraries along with API project

Well in this case you need to start step-by-step conversion process by creating related class libraries and dependencies targeting to .NET Standard (higher version). Copy the codebase from original solution to your libraries. Fix all the compatibility issues for .NET Standard. Once this phase is complete, you can add references of these dependencies to your new ASP.NET Core 2.1 project.

Structural differences between the API project

There is a significant structural differences between the API project developed with .NET Framework and .NET Core

image                      image

Let us identify some of the significant differences between these two applications.

Startup.cs – This class defines the request handling pipeline and services that need to be configured

Typically in ASP.NET MVC, we have the startup class in App_Start, which get triggered when the application is launched and initialize the pipeline. This is only required if you are handling any Katana/OWIN functionality for the MVC or WebAPI app and hence it is optional. But in case of ASP.NET Core, this class is must to have and is generated by default.

If you look into this class, there are three major components –

  1. Configure method to create application request processing pipeline where IApplicationBuilder used for configuring the request pipeline and IHostingEnvironment used to provide web hosting environment information is injected. If you have Swagger implemented, then this is the place where you are going to configure the Swagger endpoint.
  2. ConfigureServices method used to configure the application services by injecting IServiceCollection which specifies the contracts for service descriptors. IServiceCollection is under the namespace Microsoft.Extensions.DependencyInjection which helps services to resolve using inbuild dependency injection.
  3. IConfiguration which is followed by constructor injection of the Startup class, used to read the configuration properties represented by key/value pair

 

[csharp]
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.AddSingleton<IConfigurationProvider<Employee>, EmployeeProvider>();
services.AddSingleton<IConfigurationProvider<Project>, ProjectProvider>();
services.AddSingleton<IConfigurationProvider<Department>, DepartmentProvider>();
services.AddSingleton<IConfigurationProvider<Client>, ClientProvider>();
services.AddSingleton<IConfigurationProvider<Skills>, SkillProvider>();

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Swashbuckle.AspNetCore.Swagger.Info { Title = "EmployeeManagementApi", Version = "v1" });
c.IncludeXmlComments(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "EmployeeManagement.Api.xml"));
c.ResolveConflictingActions(apidescription => apidescription.First());
c.DescribeAllEnumsAsStrings();
});
services.AddMediatR(typeof(Startup));
services.AddScoped<IMediator, Mediator>();
services.AddMediatorHandlers(typeof(Startup).GetTypeInfo().Assembly);
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseSwagger();
app.UseStaticFiles();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Employee Managament Api");
c.RoutePrefix = string.Empty;
});

app.UseMvc();
}

}
[/csharp]

Configuration files for ASP.NET Core

In ASP.NET MVC, we provide all our application configuration settings in web.config file. However, in case of ASP.NET Core we provide these settings in JSON format in appsettings.json file which is placed to the root of the Api project.

[csharp]
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*",
"AppSettings": {
"endpoint": "https://localhost:8081/&quot;,
"authKey": "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
"database": "empmanagement",
"collection": "employee"
}
}
[/csharp]

For me, the json file has only few keys like endpoint to Cosmos DB, authKey for Cosmos DB, database and collection key/value pairs.

Another file which is important to look into is launchSettings.json file, which contains the information of how and where we are going to run the application. We can configure settings for various environments like Development, Staging and Production along with the information of how to run the application in local IIS express or from IIS Server.  This file is also in JSON format.

 

[csharp]
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:1789&quot;,
"sslPort": 0
}
},
"$schema": "http://json.schemastore.org/launchsettings.json&quot;,
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"EmployeeManagement.Api": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "api/values",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:5000&quot;
},
"Docker": {
"commandName": "Docker",
"launchBrowser": true,
"launchUrl": "{Scheme}://{ServiceHost}"
}
}
}
[/csharp]

Implement Exception Error Handling in ASP.NET Core

System.Web.Mvc.HandleErrorAttribute which is responsible to handle any exception thrown by an action method in asp.net mvc is not support in asp.net core.

In this case we can use create a custom exception filter that is derived from ExceptionFilterAttribute class of Microsoft.AspNetCore.Mvc.Filters namespace which runs asynchronously when an exception is thrown. We can have an ApplicationLogging class which will have an instance of ILoggerFactory from Microsoft.Extensions.Logging namespace, that support API logging using third party logging providers like NLog or Log4Net. This logging class use it in the custom exception filter which tells the appl ication where to log.

[csharp]
public static class ApplicationLogging
{
public static ILoggerFactory LoggerFactory { get; } = new LoggerFactory();
public static ILogger CreateLogger<T>() => LoggerFactory.CreateLogger<T>();
}
[/csharp]

[csharp]
public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
{
ILogger Logger { get; } = ApplicationLogging.CreateLogger<CustomExceptionFilterAttribute>();// to tell where we log

public override void OnException(ExceptionContext context)
{
using (Logger.BeginScope($"=>{ nameof(OnException) }")) // to tell which method we log
{
Logger.LogInformation("Log Message"); // to tell what exception we log
}
}

}
[/csharp]

You then use the custom exception filter attribute created for your controllers.

[csharp]
[Produces("application/json")]
[Route("api/[controller]/[action]")]
[ApiController]
[CustomExceptionFilter]
public class SkillController : ControllerBase
{
private readonly IConfigurationProvider<Skills> _provider;
private readonly IMediator _mediator;
}
[/csharp]

You can also use Microsoft.IdentityModel.Logging for implementing logging capabilities by installing the nuget package and overriding the OnException() method in custom exception filter class.

[powershell]
Install-Package Microsoft.IdentityModel.Logging -Version 5.3.0
[/powershell]

 

[csharp]
public override void OnException(ExceptionContext context)
{
Microsoft.IdentityModel.Logging.LogHelper.LogExceptionMessage(context.Exception);
}
[/csharp]

I am not covering much over here on exception handling as this should be a different post. Just an insight of the issues that we can encounter and the alternatives to fix them.

Using Configuration Manager in ASP.NET Core

Mostly when we try to retrieve values from configuration files like Web.config or App.config, we generally use ConfigurationManager, to get the values from appSettings. However, since System.Configuration.ConfigurationManager is not supported in .NET Core, we cannot use it. The workaround here is to install the package

 

[powershell]
Install-Package System.Configuration.ConfigurationManager -Version 4.5.0
[/powershell]

Now, in case of .NET Core we are supposed to read the values from appSettings.json file instead of any web.config or app.config file. In order to do that, we can create a static helper class like ConfigurationResolver.

 

[csharp]
public static class ConfigurationResolver
{

public static IConfiguration Configuration()
{
string basePath = AppContext.BaseDirectory;
var configuration = new ConfigurationBuilder()
.SetBasePath(basePath)
.AddJsonFile("AppSettings.json", optional: true, reloadOnChange: true)
.AddJsonFile("AppSettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true)
.Build();
return configuration;
}
}
[/csharp]

And then you can use the helper class like this

 

[csharp]
private static IConfiguration Configuration;
public static void Main()
{
Configuration = ConfigurationResolver.Configuration();
DatabaseId = Configuration.GetSection("AppSettings").GetSection("database").Value;
}
[/csharp]

 

Unavailability of System.Web.Http in ASP.NET Core

This limitation of not having System.Web.Http in .NET Core gives us a lot of issues where most of the code base had been using libraries and references belonging to this namespace.

For example, ApiParameterDescription or ApiDescription which belongs to System.Web.Http.Description and which gives metadata description of an input to API.

In order to implement this, we need to install the package ApiExplorer

 

[powershell]
Install-Package Microsoft.AspNetCore.Mvc.ApiExplorer -Version 2.1.2
[/powershell]

Once the package has been installed successfully, we can use most of the functionalities

 

Defining routes in ASP.NET Core

Configuring routes using MapHttpRoute is not supported in .NET Core. You can define the default routes in Startup.cs file

 

[csharp]
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseMvc(routes =>
{
//New Route
routes.MapRoute(
name: "about-route",
template: "about",
defaults: new { controller = "Home", action = "About" }
);

routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
[/csharp]

 

System.Web.Mvc in ASP.NET Core

System.Web.Mvc.Controller is not supported in .NET Core. However, you can install the package Microsoft.AspNetCore.Mvc as an alternative to serve your purpose.

 

[powershell]
Install-Package Microsoft.AspNetCore.Mvc -Version 2.1.2
[/powershell]

 

Enabling Swagger capabilities in ASP.NET Core

Swagger is an elegant way to provide API documentation. For that you need to install the package Swashbuckle.AspNetCore. Once installed, you need to update your Startup.cs file to provide the swagger endpoint and add it to the service collection.

 

[csharp]
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info { Title = "SampleApi", Version = "v1" });
c.IncludeXmlComments(GetXmlCommentsPath());
c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());
c.DescribeAllEnumsAsStrings();
});
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseSwagger();
app.UseStaticFiles();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Sample API V1");
c.RoutePrefix = string.Empty;
});

app.UseMvc();
}

private string GetXmlCommentsPath()
{
return System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Sample.Api.xml");
}

}
[/csharp]

Change your appSettings.json file to provide the launchUrl to Index.html which will open the endpoint to Swagger

 

[csharp]
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "index.html",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
[/csharp]

In your controller and action methods, you can add the following attributes.

 

[csharp]
[Produces("application/json")]
[Route("api/[controller]/[action]")]
public class ClientController : Controller
{
/// Your block of code
}

[HttpPost()]
[ActionName("GetClient")]
[ProducesResponseType(typeof(ClientRequest), 200)]
[ProducesResponseType(typeof(void), 400)]
[ProducesResponseType(typeof(void), 404)]
public async Task<IActionResult> GetClient(ClientRequest clientRequest)
{
/// Your block of code
}
[/csharp]

 

Dependency Injection using StructureMap in ASP.NET Core

As dependency injection is in-build in .NET Core, you don’t need StructureMap here. If you old code has referred to StructureMapDependencyResolver and StructureMapScope, these has been deprecated and cannot be used since .NET Core doesn’t support System.Web.Http and System.Web.Http.Dependencies.

You can use IServiceCollection to add all the dependencies required in StartUp.cs file

 

[csharp]
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IClientProvider, ClientProvider>();
services.AddSingleton<IProjectProvider, ProjectProvider>();

var container = new Container();
container.Configure(config =>
{
config.Populate(services);
});
}
[/csharp]

 

RestSharp library in .NET Core

If you are using RestSharp library, for .NET Core you need to install the nuget package RestSharp.NetCore and then create an extension to RestClient

 

[csharp]
public static class RestClientExtensions
{
public static async Task<RestResponse> ExecuteAsync(this RestClient client, RestRequest request)
{
TaskCompletionSource<IRestResponse> taskCompletion = new TaskCompletionSource<IRestResponse>();
RestRequestAsyncHandle handle = client.ExecuteAsync(request, r => taskCompletion.SetResult(r));
return (RestResponse)(await taskCompletion.Task);

}
}
[/csharp]

Change the implementation of RestSharp in your helper class or wherever you are using it.

 

[csharp]
public static async Task<IRestResponse> ExecuteAsync(string apiUrl, string request)
{
try
{
var client = new RestClient(apiUrl);
var apiRequest = new RestRequest(Method.POST);
apiRequest.AddHeader("Content-Type", "application/json");
apiRequest.AddHeader("Accept", "application/json");
apiRequest.RequestFormat = DataFormat.Json;
client.Timeout = 120000;
apiRequest.AddParameter("application/json", request, ParameterType.RequestBody);
return await client.ExecuteAsync(apiRequest);
}
catch (Exception ex)
{
throw;
}
}
[/csharp]

 

If you are using StatusCode and Content of Response object, change response.StatusCode == HttpStatusCode.OK to response.Result.StatusCode == HttpStatusCode.OK and response.Content to response.Result.Content

 

DataAnnotations in .NET Core

System.ComponentModel.DataAnnotations has been replaced by System.ComponentModel.Annotations. Add this from the nuget package.

 

Few more unsupported libraries and best practices in .NET Core

Install the nuget package Microsoft.AspNetCore.Http.Abstractions for using StatusCodes in your action methods

 

[csharp]
[HttpGet]
[ActionName("GetAllClients")]
public async Task<IActionResult> GetAllClients()
{
try
{
var response = await _mediator.Send(new GetAllClientsQuery());
return StatusCode(response.ResponseStatusCode, response.Value);
}
catch (Exception ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex);
}
}
[/csharp]

 

Remove JavaScriptSerializer() since System.Web.Script.Serialization under System.Web.Extensions is no longer supported in .NET Core

Adding WCF services built with previous versions of .NET Framework are not supported. You need to modify the services to .NET Core.

Also, if you are trying to add the service into a .NET Core 2.1 application, you might encounter issues which states “An unknown error occurred while invoking the service metadata component. Failed to generate service reference.” Modify the application to .NET Core 2.0 version from 2.1 and it will work.

 

Well pretty much, I have covered only few of the issues and workaround during .NET Core migration. Obviously, this is not covering everything but hopefully, these pointers might help you at some stage.

I do have a plan to post various methodologies and in-depth programming strategies with .NET Core in my future posts, hence stay tuned.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.