AWS DevOps & Developer Productivity Blog
Modernizing and containerizing a legacy MVC .NET application with Entity Framework to .NET Core with Entity Framework Core: Part 1
Tens of thousands of .NET applications are running across the world, many of which are ASP.NET web applications. This number becomes interesting when you consider that the .NET framework, as we know it, will be changing significantly. The current release schedule for .NET 5.0 is November 2020, and going forward there will be just one .NET that you can use to target multiple platforms like Windows and Linux. This is important because those .NET applications running in version 4.8 and lower can’t automatically upgrade to this new version of .NET. This is because .NET 5.0 is based on .NET Core and thus has breaking changes when trying to upgrade from an older version of .NET.
This is an important step in the .NET Eco-sphere because it enables .NET applications to move beyond the Windows world. However, this also means that active applications need to go through a refactoring before they can take advantage of this new definition. One choice for this refactoring is to wait until the new version of .NET is released and start the refactoring process at that time. The second choice is to get an early start and start converting your applications to .NET Core v3.1 so that the migration to .NET 5.0 will be smoother. This post demonstrates an approach of migrating an ASP.NET MVC (Model View Controller) web application using Entity Framework 6 to and ASP.NET Core with Entity Framework Core.
This post shows steps to modernize a legacy enterprise MVC ASP.NET web application using .NET core along with converting Entity Framework to Entity Framework Core.
Overview of the solution
The first step that you take is to get an Asp.NET MVC application and its required database server up and working in your AWS environment. We take this approach so you can run the application locally to see how it works. You first set up the database, which is SQL Server running in Amazon Relational Database Service (Amazon RDS). Amazon RDS provides a managed SQL Server experience. After you define the database, you set up schema and data. If you already have your own SQL Server instance running, you can also load data there if desired; you simply need to ensure your connection string points to that server rather than the Amazon RDS server you set up in this walk-through.
Next you launch a legacy MVC ASP.NET web application that displays lists of bike categories and its subcategories. This legacy application uses Entity Framework 6 to fetch data from database.
Finally, you take a step-by-step approach to convert the same use case and create a new ASP.NET Core web application. Here you use Entity Framework Core to fetch data from the database. As a best practice, you also use AWS Secrets Manager to store database login information.
Prerequisites
For this walkthrough, you should have the following prerequisites:
- An AWS Account
- An AWS user with
AdministratorAccess
(see the instructions on the AWS Identity and Access Management (IAM) console) - Access to the following AWS services:
- Amazon RDS
- Amazon Simple Storage Service (Amazon S3)
- Secrets Manager
- .NET Core 3.1 SDK installed
- Microsoft Visual Studio 2017 or later (Visual Studio code can be an alternative)
- SQL Server Management Studio to connect to the SQL Server instance
- Asp.Net application development experience
Setting up the database server
For this walk-through, we have provided an AWS CloudFormation template inside the GitHub repository to create an instance of Microsoft SQL Server Express. Which can be downloaded from this link.
- On the AWS CloudFormation console, choose Create stack.
- For Prepare template, select Template is ready.
- For Template source, select Upload a template file.
- Upload
SqlServerRDSFixedUidPwd.yaml
and choose Next.
- For Stack name, enter
SQLRDSEXStack
and choose Next. - Keep the rest of the options at their default.
- Select I acknowledge that AWS CloudFormation might create IAM resources with custom names.
- Choose Create stack.
- When the status shows as
CREATE_COMPLETE
, choose the Outputs tab and record the value from theSQLDatabaseEndpoint
key. - Connect the database from the SQL Server Management Studio with the following credentials:
User id: DBUser
Password: DBU$er2020
Setting up the CYCLE_STORE database
To set up your database, complete the following steps:
- On the SQL Server Management console, connect to the DB instance using the ID and password you defined earlier.
- Under File, choose New.
- Choose Query with Current Connection.
Alternatively, choose New Query from the toolbar.
- Download cycle_store_schema_data.sql and run it.
This creates the CYCLE_STORE
database with all the tables and data you need.
Setting up and validating the legacy MVC application
- Download the source code from the GitHub repo.
- Open AdventureWorksMVC_2013.sln and modify the database connection string in the web.config file by replacing the Data Source property value with the server name from your Amazon RDS setup.
The ASP.NET application should load with bike categories and subcategories. The following screenshot shows the Unicorn Bike Rentals website after configuration.
Now that you have the legacy application running locally, you can look at what it would take to refactor it so that it’s a .NET Core 3.1 application. Two main approaches are available for this:
- Update in place – You make all the changes within a single code set
- Move code – You create a new .NET Core solution and move the code over piece by piece
For this post, we show the second approach because it means that you have to do less scaffolding.
Creating a new MVC Core application
To create your new MVC Core application, complete the following steps:
- Open Visual Studio.
- From the Get Started page, choose Create a New Project.
- Choose ASP.NET Core Web Application.
- For Project name, enter
AdventureWorksMVCCore.Web
. - Add a Location you prefer.
- For Solution name, enter
AdventureWorksMVCCore
. - Choose Web Application (Model-View-Controller).
- Choose Create. Make sure that the project is set to use .NET Core 3.1.
- Choose Build, Build Solution.
- Press CTRL + Shift + B to make sure the current solution is building correctly.
You should get a default ASP.NET Core startup page.
Aligning the projects
ASP.NET Core MVC is dependent upon the use of a well-known folder structure; a lot of the scaffolding depends upon view source code files being in the Views
folder, controller source code files being in the Controllers
folder, etc. Some of the non-.NET specific folders are also at the same level, such as css
and images
. In .NET Core, the expectations are that static content should be in a new construct, the wwwroot
folder. This includes Javascript, CSS, and image files. You also need to update the configuration file with the same database connection string that you used earlier.
Your first step is to move the static content.
- In the .NET Core solution, delete all the content created during solution creation.This includes
css, js
, and lib directories and thefavicon.ico
file. - Copy over the css, favicon, and Images folders from the legacy solution to the
wwwroot
folder of the new solution.When completed, your .NET Corewwwroot
directory should appear like the following screenshot.
- Open appsettings.Development.json and add a ConnectionStrings section (replace the server with the Amazon RDS endpoint that you have already been using). See the following code:.
{
"ConnectionStrings": {
"DefaultConnection": "Server=sqlrdsdb.xxxxx.us-east-1.rds.amazonaws.com; Database=CYCLE_STORE;User Id=DBUser;Password=DBU$er2020;"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
Setting up Entity Framework Core
One of the changes in .NET Core is around changes to Entity Framework. Entity Framework Core is a lightweight, extensible data access technology. It can act as an object-relational mapper (O/RM) that enables interactions with the database using .NET objects, thus abstracting out much of the database access code. To use Entity Framework Core, you first have to add the packages to your project.
- In the Solution Explorer window, choose the project (right-click) and choose Manage Nuget packages…
- On the Browse tab, search for the latest stable version of these two Nuget packages.You should see a screen similar to the following screenshot, containing:
- Microsoft.EntityFrameworkCore.SqlServer
- Microsoft.EntityFrameworkCore.Tools
After you add the packages, the next step is to generate database models. This is part of the O/RM functionality; these models map to the database tables and include information about the fields, constraints, and other information necessary to make sure that the generated models match the database. Fortunately, there is an easy way to generate those models. Complete the following steps:
- Open Package Manager Console from Visual Studio.
- Enter the following code (replace the server endpoint):
Scaffold-DbContext "Server= sqlrdsdb.xxxxxx.us-east-1.rds.amazonaws.com; Database=CYCLE_STORE;User Id= DBUser;Password= DBU`$er2020;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models
The ` in the password right before the $ is the escape character.
You should now have aContext
and severalModel
classes from the database stored within theModels
folder. See the following screenshot.
- Open the CYCLE_STOREContext.cs file under the Models folder and
comment
the following lines of code as shown in the following screenshot. You instead take advantage of the middleware to read the connection string inappsettings.Development.json
that you previously configured.if (!optionsBuilder.IsConfigured) { #warning To protect potentially sensitive information in your connection string, you should move it out of source code. See http://go.microsoft.com/fwlink/?LinkId=723263 for guidance on storing connection strings. optionsBuilder.UseSqlServer( "Server=sqlrdsdb.cph0bnedghnc.us-east-1.rds.amazonaws.com; " + "Database=CYCLE_STORE;User Id= DBUser;Password= DBU$er2020;"); }
- Open the
startup.cs
file and add the following lines of code in theConfigureServices
method. You need to add reference ofAdventureWorksMVCCore.Web.Models
andMicrosoft.EntityFrameworkCore
in theusing
statement. This reads the connection string from theappSettings
file and integrates with Entity Framework Core.services.AddDbContext<CYCLE_STOREContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
Setting up a service layer
Because you’re working with an MVC application, that application control logic belongs in the controller. In the previous step, you created the data access layer. You now create the service layer. This service layer is responsible for mediating communication between the controller and the data access layer. Generally, this is where you put considerations such as business logic and validation.
Setting up the interfaces for these services follows the dependency inversion and interface segregation principles.
-
- Create a folder named
Service
under the project directory. - Create two subfolders,
Interface
andImplementation
, under the newService
folder.
- Add a new interface, ICategoryService, to the Service\Interface folder.
- Add the following code to that interface
using AdventureWorksMVCCore.Web.Models; using System.Collections.Generic; namespace AdventureWorksMVCCore.Web.Service.Interface { public interface ICategoryService { List<ProductCategory> GetCategoriesWithSubCategory();” } }
- Add a new service file, CategoryService, to the Service/Implementation folder.
- Create a class file CategoryService.cs and implement the interface you just created with the following code:
using AdventureWorksMVCCore.Web.Models; using AdventureWorksMVCCore.Web.Service.Interface; using Microsoft.EntityFrameworkCore; using System.Collections.Generic; using System.Linq; namespace AdventureWorksMVCCore.Web.Service.Implementation { public class CategoryService : ICategoryService { private readonly CYCLE_STOREContext _context; public CategoryService(CYCLE_STOREContext context) { _context = context; } public List<ProductCategory> GetCategoriesWithSubCategory() { return _context.ProductCategory .Include(category => category.ProductSubcategory) .ToList(); } } }
Now that you have the interface and instantiation completed, the next step is to add the dependency resolver. This adds the interface to the application’s service collection and acts as a map on how to instantiate that class when it’s injected into a class constructor.
- To add this mapping, open the
startup.cs
file and add the following line of code below where you addedDbContext
:services.TryAddScoped<ICategoryService, CategoryService>();
You may also need to add the following references:
using AdventureWorksMVCCore.Web.Service.Implementation; using AdventureWorksMVCCore.Web.Service.Interface; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions;
- Create a folder named
Setting up view components
In this section, you move the UI to your new ASP.NET Core project. In ASP.NET Core, the default folder structure for managing views is different that it was in ASP.NET MVC. Also, the formatting of the Razor files is slightly different.
-
- Under
Views, Shared
, create a folder calledComponents
. - Create a sub folder called
Header
. - In the
Header
folder, create a new view calledDefault.cshtml
and enter the following code:<div method="post" asp-action="header" asp-controller="home"> <div id="hd"> <div id="doc2" class="yui-t3 wrapper"> <table> <tr> <td> <h2 class="banner"> <a href="@Url.Action("Default","Home")" id="LnkHome"> <img src="/Images/logo.png" style="width:125px;height:125px" alt="ComponentOne" /> </a> </h2> </td> <td class="hd-header"><h2 class="banner">Unicorn Bike Rentals</h2></td> </tr> </table> </div> </div> </div>
- Create a class within the Header folder called HeaderLayout.cs and enter the following code:
using Microsoft.AspNetCore.Mvc; namespace AdventureWorksMVCCore.Web { public class HeaderViewComponent : ViewComponent { public IViewComponentResult Invoke() { return View(); } } }
You can now create the content view component, which shows bike categories and subcategories.
- Under
Views
,Shared
,Components
, create a folder calledContent
- Create a class
ContentLayoutModel.cs
and enter the following code:using AdventureWorksMVCCore.Web.Models; using System.Collections.Generic; namespace AdventureWorksMVCCore.Web.Views.Components { public class ContentLayoutModel { public List<ProductCategory> ProductCategories { get; set; } } }
- In this folder, create a view
Default.cshtml
and enter the following code:@model AdventureWorksMVCCore.Web.Views.Components.ContentLayoutModel <div method="post" asp-action="footer" asp-controller="home"> <div class="content"> <div class="footerinner"> <div id="PnlExpFooter"> <div> @foreach (var category in Model.ProductCategories) { <div asp-for="@category.Name" class=@($"{category.Name}Menu")> <h1> <b>@category.Name</b> </h1> <ul class=@($"{category.Name}List")> @foreach (var subCategory in category.ProductSubcategory.ToList()) { <li>@subCategory.Name</li> } </ul> </div> } </div> </div> </div> </div> </div>
- Create a class
ContentLayout.cs
and enter the following code:using AdventureWorksMVCCore.Web.Models; using AdventureWorksMVCCore.Web.Service.Interface; using Microsoft.AspNetCore.Mvc; namespace AdventureWorksMVCCore.Web.Views.Components { public class ContentViewComponent : ViewComponent { private readonly CYCLE_STOREContext _context; private readonly ICategoryService _categoryService; public ContentViewComponent(CYCLE_STOREContext context, ICategoryService categoryService) { _context = context; _categoryService = categoryService; } public IViewComponentResult Invoke() { ContentLayoutModel content = new ContentLayoutModel(); content.ProductCategories = _categoryService.GetCategoriesWithSubCategory(); return View(content); } } }
The website layout is driven by
_Layout.cshtml
file. - To render the header and portal the way you want, modify
_Layout.cshtml
and replace the existing code with the following code:<!DOCTYPE html> <html lang="en"> <head> <meta name="viewport" content="width=device-width" /> <title>Core Cycles Store</title> <link rel="apple-touch-icon" sizes="180x180" href="favicon/apple-touch-icon.png" /> <link rel="icon" type="image/png" href="favicon/favicon-32x32.png" sizes="32x32" /> <link rel="icon" type="image/png" href="favicon/favicon-16x16.png" sizes="16x16" /> <link rel="manifest" href="favicon/manifest.json" /> <link rel="mask-icon" href="favicon/safari-pinned-tab.svg" color="#503b75" /> <link href="@Url.Content("~/css/StyleSheet.css")" rel="stylesheet" /> </head> <body class='@ViewBag.BodyClass' id="body1"> @await Component.InvokeAsync("Header"); <div id="doc2" class="yui-t3 wrapper"> <div id="bd"> <div id="yui-main"> <div class="content"> <div> @RenderBody() </div> </div> </div> </div> </div> </body> </html>
Upon completion your directory should look like the following screenshot.
- Under
Modifying the index file
In this final step, you modify the Home, Index.cshtml
file to hold this content ViewComponent
:
@{
ViewBag.Title = "Core Cycles";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<div id="homepage" class="">
<div class="content-mid">
@await Component.InvokeAsync("Content");
</div>
</div>
You can now build the solution. You should have an MVC .NET Core 3.1 application running with data from your database. The following screenshot shows the website view.
Securing the database user and password
The CloudFormation stack you launched also created a Secrets Manager entry to store the CYCLE_STORE
database user ID and password. As an optional step, you can use that to retrieve the database user ID and password instead of hard-coding it to ConnectionString
.
To do so, you can use the AWS Secrets Manager client-side caching library. The dependency package is also available through NuGet. For this post, I use NuGet to add the library to the project.
- On the NuGet Package Manager console, browse for AWSSDK.SecretsManager.Caching.
- Choose the library and install it.
- Follow these steps to also install Newtonsoft.Json.
- Add a new class
ServicesConfiguration
to this solution and enter the following code in the class. Make sure all the references are added to the class. This is an extension method so we made the class static:public static class ServicesConfiguration { public static async Task<Dictionary<string, string>> GetSqlCredential(this IServiceCollection services, string secretId) { var credential = new Dictionary<string, string>(); using (var secretsManager = new AmazonSecretsManagerClient(Amazon.RegionEndpoint.USEast1)) using (var cache = new SecretsManagerCache(secretsManager)) { var sec = await cache.GetSecretString(secretId); var jo = Newtonsoft.Json.Linq.JObject.Parse(sec); credential["username"] = jo["username"].ToObject<string>(); credential["password"] = jo["password"].ToObject<string>(); } return credential; } }
- In appsettings.Development.json, replace the DefaultConnection with the following code:
"Server=sqlrdsdb.cph0bnedghnc.us-east-1.rds.amazonaws.com; Database=CYCLE_STORE;User Id=<UserId>;Password=<Password>;"
- Add the following code in the
startup.cs
, which replaces the placeholder user ID and password with the value retrieved from Secrets Manager:Dictionary<string, string> secrets = services.GetSqlCredential("CycleStoreCredentials").Result; connectionString = connectionString.Replace("<UserId>", secrets["username"]); connectionString = connectionString.Replace("<Password>", secrets["password"]); services.AddDbContext<CYCLE_STOREContext>(options => options.UseSqlServer(connectionString));
Build the solution again.
Cleaning up
To avoid incurring future charges, on the AWS CloudFormation console, delete the SQLRDSEXStack stack.
Conclusion
This post showed the process to modernize a legacy enterprise MVC ASP.NET web application using .NET Core and convert Entity Framework to Entity Framework Core. In Part 2 of this post, we take this one step further to show you how to host this application in Linux containers.
About the Author
Saleha Haider is a Senior Partner Solution Architect with Amazon Web Services. |
|
Pratip Bagchi is a Partner Solutions Architect with Amazon Web Services. |