Moving a legacy ASP.NET 4 web app to Azure

Vidar Kongsli
Bredvid
Published in
9 min readDec 5, 2022

--

I had been getting used to more-or-less cloud-ready apps with newer versions of .NET. Suddenly having to work with a 10+ years old .NET Framework 4.6 application was troublesome, but somewhat enlightening. (And I learned to appreciate modern .NET even more.) In this article, I give a step-by-step guide of the steps needed to move to Azure App Service.

Photo by Markus Spiske on Unsplash

Prelude: upgrading to .NET Framework 4.8

When moving your application to Azure, you would almost certainly like to take advantage of the libraries available for Azure that have become available in the latest versions of the .NET framework 4. For instance, if your legacy app is on 4.6.2, the first thing you should do, is to upgrade to 4.8 and then check that everything still works.

As the time of writing, 4.8.1 is the newest version of the .NET Framework. Move over to project properties in Visual Studio, and change the target framework:

Check web.config and confirm that both the compilation element as well as the httpRuntime element is targeting 4.8.1:

Upgrade packages

The next step preparing for Azure is to upgrade the Nuget packages to the latest versions. I may want to check the Auto-genetate binding redirects option in the project settings before starting upgrading the packages. Alas, you should still prepare yourself for having to create or edit binding redirects manually. I appreciate that newer versions of .NET have gotten rid of binding redirects more than I can express 🙌.

Do away with XML-based configuration✊

Back in the day, I liked the Web Transforms feature in the .NET toolchain. (I even blogged about it 10 years ago.) But then again, once upon a time I liked XML as well. I entered the industry when XML was at its peak. In this day and age, XML are used less, also in the .NET ecosystem.

Web Transforms are quaint in a modern CI/CD. In most CI/CD setups, you build an application bundle which you deploy to serveral environments and running web transforms for each environment is often not an option or not practical. So, here’s how I would work with web.config and Web Transforms:

  • Web.Config should be configured for the local development environment (Visual Studio)
  • Web.Release.Config should contain Web Transforms to make a Web.Config suitable for all other environments. That is, all environments in Azure.
  • Variations between Azure environments that you may have (development, test, production, etc.) would need to be reflected in Azure Web App Configurations and Azure Key Vault contents such as secrets. Read on.

Azure App Service Configuration

So, when having one common web.config for all your Azure environments, you need to place environment-specific configuration somewhere else. In Azure App Service, its Configuration feature is the natural place for it.

In a default ASP.NET 4 application, ConfigurationManager AppSettings and ConfigurationStrings properties read from web.config will be overridden by App Service Configuration Settings.

Any App Service Configuration setting will also be available as an environment variable for the application to read.

If you use Bicep (or ARM Templates proper, or Terraform), which you should, you can specify your App Service Configuration settings when provisioning the App Service. For instance:

resource web 'config' = {
name: 'web'
properties: {
appSettings: [
{
name: 'ApiKey'
value: 'value from azure web'
}
{
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
value: ai.properties.ConnectionString
}
{
name: 'SecretApiKey'
value: '@Microsoft.KeyVault(VaultName=${keyVaultName};SecretName=SecretApiKey)'
}
]
connectionStrings: [
{
name: 'default'
connectionString: 'Server=${sqlServer.properties.fullyQualifiedDomainName}; Authentication=Active Directory Managed Identity; Encrypt=True; Database=${sqlDatabaseName}'
}
]
}
}

Fixing App Insights configuration

Moving to the cloud, you need to accommodate for logging and monitoring. In Azure, Application Insights is the go-to solution for that.

First, install the required Nuget package:

Install-Package Microsoft.ApplicationInsights.Web

Then, add the class AiHandlErrorAttribute :

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method,
Inherited = true, AllowMultiple = true)]
public class AiHandleErrorAttribute : HandleErrorAttribute
{
public override void OnException(ExceptionContext filterContext)
{
if (filterContext?.HttpContext != null
&& filterContext.Exception != null)
{
if (filterContext.HttpContext.IsCustomErrorEnabled)
{
var ai = new TelemetryClient();
ai.TrackException(filterContext.Exception);
}
}
base.OnException(filterContext);
}
}

Add the AiHandleErrorAttribute as a global filter:

public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new AiHandleErrorAttribute()); // <-- Add this
filters.Add(new HandleErrorAttribute());
}
}

We are now at a place where we are faced with an XML-based configuration. Application Insights keeps its configuration in a file named ApplicationInsights.config . To make it so that we can have different configuration in our different cloud environments, we need a workaround. For instance, you might want to have your logs in different Application Insights instances in your different environments. (Which you should!).

We need a workaround to make the Application Insights connection string different. We can assume that we have set the configuration string for Application Insights in the App Service Configuration. (See above.) Then, we need to add the following to the Application_Start event in the application:

var telemetryConnectionString
= ConfigurationManager.AppSettings["APPINSIGHTS_CONNECTION_STRING"];
if (!string.IsNullOrWhiteSpace(telemetryConnectionString))
{
TelemetryConfiguration.Active.ConnectionString
= telemetryConnectionString;
}

You should then see your application logs appearing in Application Insights.

Load secrets from Azure Key Vault

Azure Key Vault is a PaaS solution for handling secrets, keys and certificates. To have more control of your application secrets, this is a better solution than App Service Configuration. To have your application load secrets from Azure Key Vault, you should use the following approach:

  1. Enable Managed identities Azure resources for your App Service
  2. Grant the App Service instance access to your key vault secrets
  3. Use Key Vault references for secrets to be loaded from the key vault

Let’s go:

Enable Managed identities for Azure resources

Managed identities for Azure resources, formerly known as Managed Service Identity (MSI), provides identification for your Azure resources when communicating with other resources.

Essentially, when you enable it, a service principal for your service is created in Azure AD behind the scenes. Your application can then be given access rights to other resources. The App Service provides an endpoint on the local host that allows your application to retrieve an access token to be used as a bearer token when calling other Azure services. In this context, for talking to Azure Key Vault, the token handling is automatically taken care of by the infrastructure.

You can enable the managed identity for your service in the Azure Portal, using the Azure CLI, or using Bicep:

resource webApp 'Microsoft.Web/sites@2022-03-01' = {
name: webAppName
kind: 'app'
location: location
identity: {
type: 'SystemAssigned' // <-- This!
}
}

Setting the identity property to 'SystemAssigned' means that the identity (service principal in AAD) will be handled automatically.

Grant the App Service instance access to your key vault

Once the App Service has a managed identity, you can refer to it in Bicep using .identity.principalId on the web application object.

Here, we create an access policy that allows the app service to list and retrieve secrets:

resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = {
name: keyVaultName
properties: {
accessPolicies: [
{
tenantId: subscription().tenantId
objectId: webApp.identity.principalId
permissions: {
keys: []
secrets: [
'Get'
'List'
]
certificates: []
}
}
]
}
}

(Note that the excerpt above is not a complete key vault configuration. Some parts are skipped for brevity.)

Use Key Vault references for secrets

Once the managed identity has access to the key vault, the app service can automatically load secrets from the key vault into the app service configuration using key vault references.

In order to load a secret, say SecretApiKey , from the key vault, the app service configuration value can be set to @Microsoft.KeyVault(VaultName=<vault name>;SecretName=SecretApiKey) . Notice that we here can map to another secret name. There do not need to be a correspondence between the setting name the application sees and the secret name in the key vault.

Troubleshooting Key Vault references

Resolving the key vault references can fail for several reasons. A symptom of this happening would be that the setting value that the application sees contains the key vault reference itself:

In case the reference is not resolved, you may want to go to the Azure Portal, navigate to the app service and navigate to the configuration section. Chances are that you will find the explanation there:

In the case above, there were no secret named “SecretApiKey” in the key vault.

Other reasons why the key vault reference is not resolved include:

  • There is something wrong with the access policy set on the key vault, or if using RBAC instead of access policies, the application does not have the necessary rights to access the secret
  • The network infrastructure does not allow the app service access to the key vault. For instance, if the app service is on a virtual network, the key vault is not publicly available on the Internet, and there is no service endpoint and private endpoint to the key vault configured

Migrating data from SQL Server to Azure SQL

For a typical web application, most workloads that you run on an on-premises SQL Server would be easy to migrate to Azure SQL. There are exceptions, worthy of blogposts in their own right. For most apps, using SqlPackage or Microsoft SQL Server Management Studio will help you a long way. Export the database as a bacpac-file and import it to an Azure SQL server.

Authenticating using Azure AD for Azure SQL connections

When moving to Azure, you should use the security features of the platform to the fullest. Earlier in this post, we placed secrets, passwords, etc. in Azure Key Vault. The App Service accessed the key vault using Managed identities. We can also use Managed identities to connect to the Azure SQL database, so that we can do away with handling passwords in database connection strings. Make sure your App Service has a system managed identity.

Connect to the database and create a database user for the application. No need to create an SQL Login, just reference the managed identity of the App Service:

CREATE USER [<app service name>] FROM EXTERNAL PROVIDER;
ALTER ROLE db_datareader ADD MEMBER [<app service name>];
ALTER ROLE db_datawriter ADD MEMBER [<app service name>];

(Take notice that when running the T-SQL above, you need to have authenticated to the Azure SQL Server using Azure AD when establishing the session.)

In order for your application to authenticate using Managed identities, you need to one of the following:

  1. Replace usage of System.Data.SqlClient with Microsoft.Data.SqlClient . See more information about this step here.
  2. Create custom code to acquire and use an access token with the database connections. For example, use this approach.

I would recommend option 1 over option 2 if you can. It involves less changes to your application and using Microsoft.Data.SqlClient has several advantages apart from the possibility to authenticate using Managed identitites.

Lastly, create a database connection string without a user id and without a password. If you are using Microsoft.Data.SqlClient to handle the authentication, make sure to set the authentication mode in the connection string correctly. For instance:

Server=server1.database.windows.net; Authentication=Active Directory Managed Identity;Database=default

Handling network and database unavailability

The Fallacies of Distributed computing states that one of the fallacies is that the network is reliable. In the case of SQL in the cloud, you need your app to gracefully handle transient errors. If you wish to use the serverless tier of Azure SQL, you might experience that the database is paused, and needs a few seconds to come back online. In these situations, the app would need to wait and try the operation again.

The solution to this problem, like in the case with using Managed identities above, is to use the Microsoft.Data.SqlClient package instead of System.Data.SqlClient . This has support for handling transient errors.

You then need to control the creation of new SqlConnection object, and enable transient error handling:

var conn = new SqlConnection("<connection string>")
{
RetryLogicProvider = SqlConfigurableRetryFactory
.CreateExponentialRetryProvider(new SqlRetryLogicOption
{
NumberOfTries = 5,
MaxTimeInterval = TimeSpan.FromSeconds(20),
DeltaTime = TimeSpan.FromSeconds(1)
})
};

More on the transient error handling here.

Summary

We have now gone through some of the steps you most likely will need to do in order to move an old, on-premises hosted .NET Framework web application to Azure. It’s not always smooth sailing, and the devil is often in the details. But it is definitely worth the effort in the end. You can find sample code used in this post here. Good luck!

--

--

Software professional living in Oslo, Norway. I work as a consultant, system architect and developer at Bredvid AS.