Events
Power BI DataViz World Championships
Feb 14, 4 PM - Mar 31, 4 PM
With 4 chances to enter, you could win a conference package and make it to the LIVE Grand Finale in Las Vegas
Learn moreThis browser is no longer supported.
Upgrade to Microsoft Edge to take advantage of the latest features, security updates, and technical support.
Note
This isn't the latest version of this article. For the current release, see the .NET 9 version of this article.
Warning
This version of ASP.NET Core is no longer supported. For more information, see the .NET and .NET Core Support Policy. For the current release, see the .NET 9 version of this article.
Important
This information relates to a pre-release product that may be substantially modified before it's commercially released. Microsoft makes no warranties, express or implied, with respect to the information provided here.
For the current release, see the .NET 9 version of this article.
Apps deployed to Azure that experience intermittent high demand benefit from scalability to meet demand. Scalable apps can scale out to ensure capacity during workload peaks and then scale down automatically when the peak drops, which can lower costs. Horizontal scaling (scaling out) adds new instances of a resource, such as VMs or database replicas. This article demonstrates how to deploy a horizontally scalable ASP.NET Core app to Azure container apps by completing the following tasks:
This article uses Razor Pages, but most of it applies to other ASP.NET Core apps.
In some cases, basic ASP.NET Core apps are able to scale without special considerations. However, apps that utilize certain framework features or architectural patterns require extra configurations, including the following:
Secure form submissions: Razor Pages, MVC and Web API apps often rely on form submissions. By default these apps use cross site forgery tokens and internal data protection services to secure requests. When deployed to the cloud, these apps must be configured to manage data protection service concerns in a secure, centralized location.
SignalR circuits: Blazor Server apps require the use of a centralized Azure SignalR service in order to securely scale. These services also utilize the data protection services mentioned previously.
Centralized caching or state management services: Scalable apps may use Azure Cache for Redis to provide distributed caching. Azure storage may be needed to store state for frameworks such as Microsoft Orleans, which can help write apps that manage state across many different app instances.
The steps in this article demonstrate how to properly address the preceding concerns by deploying a scalable app to Azure Container Apps. Most of the concepts in this tutorial also apply when scaling Azure App Service instances.
Use the GitHub Explorer sample app to follow along with this tutorial. Clone the app from GitHub using the following command:
git clone "https://github.com/dotnet/AspNetCore.Docs.Samples.git"
Navigate to the /tutorials/scalable-razor-apps/start
folder and open the ScalableRazor.csproj
.
The sample app uses a search form to browse GitHub repositories by name. The form relies on the built-in ASP.NET Core data protection services to handle antiforgery concerns. By default, when the app scales horizontally on Container Apps, the data protection service throws an exception.
Use the search form to browse for GitHub repositories by name.
Visual Studio is used to deploy the app to Azure Container Apps. Container apps provide a managed service designed to simplify hosting containerized apps and microservices.
Note
Many of the resources created for the app require a location. For this app, location isn't important. A real app should select a location closest to the clients. You may want to select a location near you.
In Visual Studio solution explorer, right click on the top level project node and select Publish.
In the publishing dialog, select Azure as the deployment target, and then select Next.
For the specific target, select Azure Container Apps (Linux), and then select Next.
Create a new container app to deploy to. Select the green + icon to open a new dialog and enter the following values:
Once the resource is created, make sure it's selected in the list of container apps, and then select Next.
You'll need to create an Azure Container Registry to store the published image artifact for your app. Select the green + icon on the container registry screen.
Leave the default values, and then select Create.
After the container registry is created, make sure it's selected, and then select finish to close the dialog workflow and display a summary of the publishing profile.
If Visual Studio prompts you to enable the Admin user to access the published docker container, select Yes.
Select Publish in the upper right of the publishing profile summary to deploy the app to Azure.
When the deployment finishes, Visual Studio launches the browser to display the hosted app. Search for Microsoft
in the form field, and a list of repositories is displayed.
The app is currently working without any issues, but we'd like to scale the app across more instances in anticipation of high traffic volumes.
razorscaling-app-****
container app in the top level search bar and select it from the results.Navigate back to the app. When the page loads, at first it appears everything is working correctly. However, when a search term is entered and submitted, an error may occur. If an error is not displayed, submit the form several more times.
It's not immediately apparent why the search requests are failing. The browser tools indicate a 400 Bad Request response was sent back. However, you can use the logging features of container apps to diagnose errors occurring in your environment.
On the overview page of the container app, select Logs from the left navigation.
On the Logs page, close the pop-up that opens and navigate to the Tables tab.
Expand the Custom Logs item to reveal the ContainerAppConsoleLogs_CL node. This table holds various logs for the container app that can be queried to troubleshoot problems.
In the query editor, compose a basic query to search the ContainerAppConsoleLogs_CL Logs table for recent exceptions, such as the following script:
ContainerAppConsoleLogs_CL
| where Log_s contains "exception"
| sort by TimeGenerated desc
| limit 500
| project ContainerAppName_s, Log_s
The preceding query searches the ContainerAppConsoleLogs_CL table for any rows that contain the word exception. The results are ordered by the time generated, limited to 500 results, and only include the ContainerAppName_s and Log_s columns to make the results easier to read.
Select Run, a list of results is displayed. Read through the logs and note that most of them are related to antiforgery tokens and cryptography.
Important
The errors in the app are caused by the .NET data protection services. When multiple instances of the app are running, there is no guarantee that the HTTP POST request to submit the form is routed to the same container that initially loaded the page from the HTTP GET request. If the requests are handled by different instances, the antiforgery tokens aren't handled correctly and an exception occurs.
In the steps ahead, this issue is resolved by centralizing the data protection keys in an Azure storage service and protecting them with Key Vault.
To resolve the preceding errors, the following services are created and connected to the app:
Storage accounts
and select the matching result.Azure provisions the new storage account. When the task completes, choose Go to resource to view the new service.
Create a Container to store the app's data protection keys.
The new containers appear on the page list.
Create a key vault to hold the keys that protect the data in the blob storage container.
Key Vault
and select the matching result.Azure provisions the new key vault. When the task completes, select Go to resource to view the new service.
Create a secret key to protect the data in the blob storage account.
The Container App requires a secure connection to the storage account and the key vault services in order to resolve the data protection errors and scale correctly. The new services are connected together using the following steps:
Important
Security role assignments through Service Connector and other tools typically take a minute or two to propagate, and in some rare cases can take up to eight minutes.
The service connector enables a system-assigned managed identity on the container app. It also assigns a role of Storage Blob Data Contributor to the identity so it can perform data operations on the storage containers.
The service connector assigns a role to the identity so it can perform data operations on the key vault keys.
The necessary Azure resources have been created. In this section the app code is configured to use the new resources.
Install the following NuGet packages:
dotnet add package Azure.Identity
dotnet add package Microsoft.Extensions.Azure
dotnet add package Azure.Extensions.AspNetCore.DataProtection.Blobs
dotnet add package Azure.Extensions.AspNetCore.DataProtection.Keys
Update Program.cs
with the following highlighted code:
using Azure.Identity;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Azure;
var builder = WebApplication.CreateBuilder(args);
var BlobStorageUri = builder.Configuration["AzureURIs:BlobStorage"];
var KeyVaultURI = builder.Configuration["AzureURIs:KeyVault"];
builder.Services.AddRazorPages();
builder.Services.AddHttpClient();
builder.Services.AddServerSideBlazor();
builder.Services.AddAzureClientsCore();
builder.Services.AddDataProtection()
.PersistKeysToAzureBlobStorage(new Uri(BlobStorageUri),
new DefaultAzureCredential())
.ProtectKeysWithAzureKeyVault(new Uri(KeyVaultURI),
new DefaultAzureCredential());
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
The preceding changes allow the app to manage data protection using a centralized, scalable architecture. DefaultAzureCredential
discovers the managed identity configurations enabled earlier when the app is redeployed.
Update the placeholders in AzureURIs
section of the appsettings.json
file to include the following:
Replace the <storage-account-name>
placeholder with the name of the scalablerazorstorageXXXX
storage account.
Replace the <container-name>
placeholder with the name of the scalablerazorkeys
storage container.
Replace the <key-vault-name>
placeholder with the name of the scalablerazorvaultXXXX
key vault.
Replace the <key-name>
placeholder in the key vault URI with the razorkey
name created previously.
{
"GitHubURL": "https://api.github.com",
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"AzureURIs": {
"BlobStorage": "https://<storage-account-name>.blob.core.windows.net/<container-name>/keys.xml",
"KeyVault": "https://<key-vault-name>.vault.azure.net/keys/<key-name>/"
}
}
The app is now configured correctly to use the Azure services created previously. Redeploy the app for the code changes to be applied.
Visual Studio redeploys the app to the container apps environment created previously. When the processes finished, the browser launches to the app home page.
Test the app again by searching for Microsoft in the search field. The page should now reload with the correct results every time you submit.
The existing code and configuration of the app can also work while running locally during development. The DefaultAzureCredential
class configured previously is able to pick up local environment credentials to authenticate to Azure Services. You'll need to assign the same roles to your own account that were assigned to your app's managed identity in order for the authentication to work. This should be the same account you use to log into Visual Studio or the Azure CLI.
You'll need to be signed in to the Azure CLI, Visual Studio, or Azure PowerShell for your credentials to be picked up by DefaultAzureCredential
.
az login
scalablerazor****
storage account created previously.Storage blob data contributor
, select the matching result, and then select Next.As previously stated, role assignment permissions might take a minute or two to propagate, or in rare cases up to eight minutes.
Repeat the previous steps to assign a role to your account so that it can access the key vault service and secret.
razorscalingkeys
key vault created previously.Key Vault Crypto Service Encryption User
, select the matching result, and then select Next.You may need to wait again for this role assignment to propagate.
You can then return to Visual Studio and run the app locally. The code should continue to function as expected. DefaultAzureCredential
uses your existing credentials from Visual Studio or the Azure CLI.
For guidance on creating a reliable, secure, performant, testable, and scalable ASP.NET Core app, see Enterprise web app patterns. A complete production-quality sample web app that implements the patterns is available.
ASP.NET Core feedback
ASP.NET Core is an open source project. Select a link to provide feedback:
Events
Power BI DataViz World Championships
Feb 14, 4 PM - Mar 31, 4 PM
With 4 chances to enter, you could win a conference package and make it to the LIVE Grand Finale in Las Vegas
Learn moreTraining
Learning path
Solution Architect: Design Microsoft Power Platform solutions - Training
Learn how a solution architect designs solutions.
Certification
Microsoft Certified: Azure Developer Associate - Certifications
Build end-to-end solutions in Microsoft Azure to create Azure Functions, implement and manage web apps, develop solutions utilizing Azure storage, and more.