15 Effective Methods for Enhancing Performance in Our ASP.NET Core Application

Overview

In the ever-changing world of software development, achieving optimal efficiency is crucial, especially in the realm of ASP.NET Core applications. This article takes a deep dive into the realm of performance optimisation, offering a comprehensive exploration of 15 essential strategies. These strategies serve as the foundation for improving the performance of our .NET applications, creating an environment where smoothness and efficiency are paramount.

A Perfect Blend of Performance and Robustness

As ASP.NET Core applications become more complex and robust, the need for performance optimisation becomes increasingly important. Within this article, we embark on an enlightening journey through 15 carefully selected best practices, each with the potential to transform the landscape of our ASP.NET applications. By fully embracing these practices, we empower our applications to overcome operational limitations and operate with the fluidity and efficiency we desire.

The Intersection of Efficiency and Progress

In the ever-evolving world of ASP.NET Core application development, the focus on performance optimisation is more crucial than ever. This article serves as a guiding light, shedding light on 15 fundamental best practices that have the potential to revolutionise the performance of our ASP.NET applications. With these practices at our disposal, we pave the way for a harmonious combination of streamlined operations and impeccable efficiency.

Driving Progress through Thoughtful Practices

In an era where ASP.NET Core applications are expected to achieve unprecedented levels of robustness and efficiency, the art of performance optimisation takes on unparalleled significance. Within this enlightening article, we embark on an exploration of 15 foundational best practices, meticulously chosen to invigorate the performance of our ASP.NET applications. By incorporating these practices into our development approach, we not only ensure the smooth functioning of our applications but also set a course for unexplored levels of efficiency and success.

Boost Efficiency with Strategic Caching Techniques Caching frequently accessed data can significantly reduce the load on our application. Utilise caching mechanisms like MemoryCache or distributed caching providers to swiftly store and retrieve data, minimising database queries.

By caching frequently accessed data, it is possible to significantly decrease the load on our application, including reducing the number of database queries and optimising resource utilisation. Employ caching mechanisms such as MemoryCache or distributed caching providers to efficiently store and retrieve data.

One of the pivotal techniques for optimising our application's performance involves the judicious implementation of caching mechanisms. Caching empowers we to proactively address performance bottlenecks by efficiently managing frequently accessed data. By doing so, we alleviate the strain on our application's resources and notably curtail the need for frequent interactions with the underlying database.

Maximising Efficiency via Caching

When we systematically cache data that is accessed with high frequency, we institute a formidable defence against unnecessary resource utilisation. This translates to reduced overhead, minimised strain on our application's infrastructure and an overall smoother user experience. Importantly, it curtails the often-expensive interactions with databases, resulting in a noticeable acceleration in response times.

Leveraging Caching Mechanisms

To execute these optimisations, leverage advanced caching mechanisms such as the potent MemoryCache or employ distributed caching providers. MemoryCache empowers we to store frequently accessed data directly in memory, ensuring lightning-fast retrieval times. On the other hand, distributed caching providers enable we to extend caching capabilities across a network, benefiting from shared resources for even greater efficiency gains.

Seamless Data Provision

By employing these caching methodologies, we pave the way for swift and seamless data provisioning. Whether it's a frequently requested database query result, dynamically generated content, or resource-intensive computations, the cached data stands ready to be delivered promptly, without taxing our application's core components.

In essence, harnessing caching mechanisms not only optimises our application's performance but also orchestrates a harmonious interaction between responsiveness and resource conservation. It's a strategic move that reflects the profound synergy between technology and user satisfaction.

The below examples illustrate how to use both in-memory caching with MemoryCache and distributed caching with a basic Redis setup. Remember to adjust the caching configurations, error handling, and the actual data fetching according to our application's needs.

Using MemoryCache for In-Memory Caching

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using System;

namespace ZRCodeExample.Controllers
{
    public class HomeController : Controller
    {
        private readonly IMemoryCache _memoryCache;

        public HomeController(IMemoryCache memoryCache)
        {
            _memoryCache = memoryCache;
        }

        public IActionResult Index()
        {
            if (!_memoryCache.TryGetValue("cachedData", out string cachedValue))
            {
                // Data not in cache, fetch and cache it
                cachedValue = GetDataFromSource();
                _memoryCache.Set("cachedData", cachedValue, TimeSpan.FromMinutes(10));
            }

            return View("Index", cachedValue);
        }

        private string GetDataFromSource()
        {
            // Simulating data retrieval
            return "Data from source";
        }
    }
}

Using Distributed Caching with Redis

First, we need to add the required NuGet packages

dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis

Then, configure Redis in our Program.cs

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();

// Inside ConfigureServices method
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "localhost"; // Redis server address
    options.InstanceName = "SampleInstance"; // Unique identifier for the cache instance
});


var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

Now, we can use distributed caching in our controller.

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Distributed;
using System.Diagnostics;
using System.Text;
using ZR.CodeExampleCachingWithRedis.Models;

namespace ZR.CodeExampleCachingWithRedis.Controllers
{
    public class HomeController : Controller
    {
        private readonly ILogger<HomeController> _logger;
        private readonly IDistributedCache _distributedCache;
        public HomeController(ILogger<HomeController> logger, IDistributedCache distributedCache)
        {
            _logger = logger;
            _distributedCache = distributedCache;
        }

        public IActionResult Index()
        {
            byte[] cachedBytes = _distributedCache.Get("cachedData");
            if (cachedBytes == null)
            {
                // Data not in cache, fetch and cache it
                string data = GetDataFromSource();
                cachedBytes = Encoding.UTF8.GetBytes(data);
                DistributedCacheEntryOptions cacheOptions = new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
                };
                _distributedCache.Set("cachedData", cachedBytes, cacheOptions);
            }

            string cachedValue = Encoding.UTF8.GetString(cachedBytes);
            return View("Index", cachedValue);

        }

        public IActionResult Privacy()
        {
            return View();
        }

        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResult Error()
        {
            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
        }

        private string GetDataFromSource()
        {
            // Simulating data retrieval
            return "Data from source";
        }

    }
}

Optimise Hot Code Paths

Identify and enhance frequently accessed code paths, commonly referred to as hot code paths, to achieve improved performance. Utilise profiling tools to pinpoint bottlenecks and effectively resolve them. Leverage profiling tools to identify performance bottlenecks and devise strategies for addressing them by optimising frequently accessed code paths, also known as hot code paths.

Profiling

ASP.NET Core provides tools for profiling, including built-in diagnostics and third-party profilers. One popular profiler is the "dotMemory" profiler from JetBrains.

Identify Hot Code Paths

Use the profiler to gather data on which parts of our application are consuming the most time and resources. Look for methods or areas that are frequently called and contribute significantly to the overall execution time.

Optimise Hot Code Paths

Once we've identified the hot code paths, we can consider various optimisation strategies:

  • Algorithmic Improvements: Review our code to see if there are more efficient algorithms or data structures that can be used to accomplish the same tasks.
  • Caching: Implement caching mechanisms to store frequently accessed data or computation results.
  • Parallelisation: Utilise parallel processing to distribute workloads across multiple threads or tasks.
  • Async Programming: Use asynchronous programming to avoid blocking threads and improve responsiveness.
  • Database Optimisation: Optimise database queries and access patterns.
  • Reducing I/O Operations: Minimise disk I/O and network requests where possible.

Measure Performance

After making optimisations, measure the performance again using the profiler to ensure that the changes have a positive impact on the hot code paths.

In this simple example of using custom middleware to profile and optimise a hot code path in an ASP.NET Core application. This example focuses on the concept and doesn't include actual profiling tools or sophisticated optimisation techniques.

After making optimisations, measure the performance again using the profiler to ensure that the changes have a positive impact on the hot code paths.

In this simple example of using custom middleware to profile and optimise a hot code path in an ASP.NET Core application. This example focuses on the concept and doesn't include actual profiling tools or sophisticated optimisation techniques.

using System.Diagnostics;

namespace ZR.CodeExampleMeasurePerformance.Middleware
{
    public class ProfilingMiddleware
    {
        private readonly RequestDelegate _next;

        public ProfilingMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task Invoke(HttpContext context)
        {
            var stopwatch = Stopwatch.StartNew();

            await _next(context);

            stopwatch.Stop();
            if (stopwatch.ElapsedMilliseconds > 100) // Example threshold for a "hot" code path
            {
                // Log or report the slow code path
            }
        }

    }
}
using ZR.CodeExampleMeasurePerformance.Middleware;

namespace ZR.CodeExampleMeasurePerformance.Extensions
{
    // Extension method used to add the middleware to the HTTP request pipeline.
    public static class ProfilingMiddlewareExtensions
    {
        public static IApplicationBuilder UseProfilingMiddleware(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<ProfilingMiddleware>();
        }
    }
}

 

Harness the Power of Asynchronous APIs

In the intricate realm of software development, the mastery of concurrency emerges as a pivotal pursuit. In this exploration, we dive deep into the art of asynchronous programming, wielding its power to handle multiple requests with finesse. By embracing this paradigm, we break free from the constraints of traditional synchronous execution and set our sights on an era of enhanced scalability and responsiveness.

Elevating Scalability through Asynchronous Ingenuity

As we navigate the landscape of modern applications, the demand for scalability looms larger than ever. The utilisation of asynchronous programming serves as a game-changing strategy to address this very need. By enabling the concurrent handling of multiple requests, we dismantle the shackles of thread pool starvation, paving the way for an application that gracefully juggles a multitude of tasks, all while ensuring optimal performance and resource utilisation.

The Convergence of Asynchronous Brilliance

At the core of this paradigm lies the elegance of asynchronous operations, which unfurl their potential across various domains. Particularly, when it comes to data access, I/O operations, and tasks of extended durations, the asynchronous approach takes centre stage. Through meticulous implementation, we not only relinquish the traditional shackles of blocking operations but also usher in an era where responsiveness thrives, user experiences elevate, and resource efficiency takes precedence.

Embarking on an Asynchronous Odyssey: Code Exemplified

Let's embark on a journey of code, where the potential of asynchronous programming comes to life.

namespace ZR.CodeExampleEmbarkingAsynchronousOdyssey.Helpers
{
    public class AsyncExample
    {
        private readonly HttpClient _httpClient;

        public AsyncExample()
        {
            // Initialize HttpClient in the constructor to reuse it efficiently.
            _httpClient = new HttpClient();
        }

        public async Task<int> FetchDataAsync()
        {
            try
            {
                var response = await _httpClient.GetAsync("https://exampleUrlAddress.com/data");

                if (response.IsSuccessStatusCode)
                {
                    // Perform processing on response asynchronously
                    var data = await response.Content.ReadAsStringAsync();
                    return data.Length;
                }
                else
                {
                    // Handle unsuccessful response (e.g., log an error, return a specific value)
                    return -1;
                }
            }
            catch (HttpRequestException)
            {
                // Handle exceptions related to the HTTP request (e.g., network issues)
                return -1;
            }
        }
    }
}

Elevating Excellence: Embrace Asynchronous Programming

With the fervent embrace of asynchronous programming, we transcend the limitations of synchronous execution. We usher in a new era where applications flourish in the face of concurrency, scaling gracefully to meet the demands of modern computing. By adorning our codebase with asynchronous prowess, we align ourselves with a future where performance knows no bounds, responsiveness reigns supreme, and the user experience stands elevated to unparalleled heights.

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using ZR.CodeExampleEmbarkingAsynchronousOdyssey.Helpers;

namespace ZR.CodeExampleEmbarkingAsynchronousOdyssey.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class AsyncApiExample : ControllerBase
    {
        private readonly AsyncExample _asyncExample;

        public AsyncApiExample()
        {
            _asyncExample = new AsyncExample();
        }

        public async Task<IActionResult> ProcessAsync()
        {
            var data = await _asyncExample.FetchDataAsync();
            // Process data asynchronously

            // Return data as JSON
            return new JsonResult(data);
        }
    }
}

Make Hot Code Paths Asynchronous

Within the complex fabric of software optimisation, the notion of hot code paths serves as a central focus. This article reveals a revolutionary approach by incorporating the power of asynchronous programming into these critical routes. By embarking on this journey, we tap into the potential to create a resilient and highly reactive application, even when confronted with formidable workloads.Unveiling the Wonders of Asynchronous Programming

The prowess of asynchronous programming extends far beyond its initial allure. While its advantages are well-established, its potential impact on hot code paths is truly groundbreaking. As we navigate through sections of code that experience significant demand and frequent execution, embracing asynchronous constructs ensures that our application remains agile and responsive, unburdened by the weight of intensive workloads.

The Synchronisation of Resilience and Responsiveness

Hot code paths, often the lifeblood of application performance, can determine the distinction between an application that flourishes and one that falters. By introducing asynchronous programming into these vital channels, we unlock a multitude of benefits. Our application gains the capability to multitask with elegance, adeptly managing multiple tasks without succumbing to performance bottlenecks. The outcome? A responsive and seamless user experience, even during the most challenging computational endeavors.Embracing Asynchronicity: Code in Motion

To embark on this transformative journey, let's explore how to make a hot code path asynchronous. In this illustrative snippet, the ProcessHotPathAsync method simulates an intensive operation using Task.Delay, representing a hot code path. By making this operation asynchronous, we enable the application to remain responsive during the delay. The subsequent CalculateResult method demonstrates that even within these hot code paths, intricate calculations can be efficiently executed, ensuring that performance remains uncompromised.

namespace ZR.CodeExampleEmbarkingAsynchronousOdyssey.Helpers
{
    public class HotCodePathExample
    {
        public async Task<int> ProcessHotPathAsync()
        {
            // Simulate intensive work
            await Task.Delay(TimeSpan.FromSeconds(2));

            return CalculateResult();
        }

        private int CalculateResult()
        {
            // Perform calculations
            return 42;
        }
    }

}

In this illustrative snippet, the ProcessHotPathAsync method simulates an intensive operation using Task.Delay, representing a hot code path. By making this operation asynchronous, we allow the application to remain responsive during the delay. The subsequent CalculateResult method showcases that even within these hot code paths, intricate calculations can be efficiently carried out, ensuring that performance remains uncompromised.

Elevating Excellence: Combining Asynchrony and Hot Code Paths

By intertwining the virtues of asynchronous programming with the crucial hot code paths of our application, we orchestrate a harmonious blend of performance and responsiveness. The transformative impact is evident as our application effortlessly navigates through intensive workloads while retaining its ability to provide users with an exceptional experience. The combination of asynchrony and hot code paths heralds an era where efficiency reigns supreme, and our codebase becomes a symbol of technological excellence.

Efficiently Retrieve Large Collections with Pagination

When dealing with large data sets, implement pagination to avoid overloading resources. Return only the necessary subset of data to improve response times. As a result of dealing with large data sets, implement pagination in order to make sure resources do not get overloaded. Return only the necessary subset of data in order to improve response times.

using ZR.CodeExampleEmbarkingAsynchronousOdyssey.Models;

namespace ZR.CodeExampleEmbarkingAsynchronousOdyssey.Services
{
    public class AsyncDataServiceExample
    {
        public AsyncDataServiceExample()
        {
            // Constructor logic, if needed
        }

        public async Task<IEnumerable<Person>> GetPaginatedDataAsync(int page, int pageSize)
        {
            // Implement your data retrieval logic here
            // For example, fetch data from a database or other data source
            // Return a paginated subset of the data
            // Replace Person with your actual data model class
            return await AsyncDatabaseServiceExample.GetPaginatedDataFromDatabaseAsync(page, pageSize);
        }
    }
}
using ZR.CodeExampleEmbarkingAsynchronousOdyssey.Models;

namespace ZR.CodeExampleEmbarkingAsynchronousOdyssey.Services
{
    public class AsyncDatabaseServiceExample
    {
        public static async Task<IEnumerable<Person>> GetPaginatedDataFromDatabaseAsync(int page, int pageSize)
        {
            // Implement database query logic here
            // Return a paginated result
            // This is just a placeholder; replace it with actual database access code
            var data = new List<Person>
            {
                new Person { Id = 1, Name = "Item 1" },
                new Person { Id = 2, Name = "Item 2" },
                // Add more data as needed
            };

            // Calculate the start index and take a subset of data for pagination
            int startIndex = (page - 1) * pageSize;
            var paginatedData = data.Skip(startIndex).Take(pageSize);

            await Task.Delay(TimeSpan.FromSeconds(1)); // Simulate asynchronous delay

            return paginatedData;
        }
    }
}
namespace ZR.CodeExampleEmbarkingAsynchronousOdyssey.Models
{
    public class Person
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
}

IAsyncEnumerable for Asynchronous Enumeration

Use IAsyncEnumerable<T> when asynchronously enumerating collections. This choice prevents synchronous blocking and enhances performance, especially in scenarios involving large datasets.

public async IAsyncEnumerable<Person> GetItemsAsync()
{
    // Fetch items asynchronously from a data source (e.g., database)
    var items = await this.dbContext.Person.ToListAsync(); // Use .Persons instead of .Person

    foreach (var item in items)
    {
        yield return item;
    }
}
using Microsoft.EntityFrameworkCore;
using ZR.CodeExampleEmbarkingAsynchronousOdyssey.Models;

namespace ZR.CodeExampleEmbarkingAsynchronousOdyssey.Data
{
    public class ExampleDatabaseContext : DbContext
    {
        public ExampleDatabaseContext(DbContextOptions<ExampleDatabaseContext> options)
            : base(options)
        {
        }

        // Define your DbSet for the Person entity (replace Person with your actual entity class)
        public DbSet<Person> Persons { get; set; }

        // Configure your entities and relationships here
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // Configure your entity mappings and relationships here if needed

            base.OnModelCreating(modelBuilder);
        }
    }
}

Cache Frequently Used Large Objects

Cache large objects that are frequently used to reduce memory consumption and improve retrieval times. Utilise ArrayPool<T> to manage large arrays efficiently.

using Microsoft.Extensions.Caching.Memory;
using System.Buffers;

            // Create an instance of MemoryCache
            var memoryCache = new MemoryCache(new MemoryCacheOptions());

            // Create a large object (for demonstration purposes)
            var largeObject = new byte[1000000]; // A large byte array

            // Caching large objects
            memoryCache.Set("largeObject", largeObject, TimeSpan.FromMinutes(10));

            // Using ArrayPool<T> to rent and return arrays
            var largeArray = ArrayPool<byte>.Shared.Rent(10000); // Rent an array of 10,000 bytes
            try
            {
                // Use the rented array
                for (int i = 0; i < largeArray.Length; i++)
                {
                    largeArray[i] = (byte)(i % 256); // Fill the array with sample data
                }

                // Process the array or perform your operations here
                // ...

                // When done, return the rented array to the pool
                ArrayPool<byte>.Shared.Return(largeArray);
            }
            finally
            {
                // Ensure that the array is returned to the pool even in case of exceptions
                ArrayPool<byte>.Shared.Return(largeArray);
            }

Optimise Data Access and I/O

Utilise caching strategies to minimise database and remote service calls. Retrieve only the required data, reducing unnecessary overhead and boosting performance.

using Microsoft.EntityFrameworkCore;
static async Task Main(string[] args)
{
    // Initialize your DbContext (replace MyDbContext with your actual DbContext)
    using var dbContext = new MyDbContext();

    // Check if the data is cached
    var cachedUser = await GetCachedUserAsync();
    if (cachedUser != null)
    {
        Console.WriteLine("User data retrieved from cache:");
        Console.WriteLine($"Id: {cachedUser.Id}, Name: {cachedUser.Name}");
    }
    else
    {
        // Fetch the user data from the database
        var user = await dbContext.Users
            .Select(u => new UserData { Id = u.Id, Name = u.Name })
            .FirstOrDefaultAsync();

        if (user != null)
        {
            Console.WriteLine("User data retrieved from the database:");
            Console.WriteLine($"Id: {user.Id}, Name: {user.Name}");

            // Cache the retrieved user data
            await CacheUserDataAsync(user);
        }
        else
        {
            Console.WriteLine("User not found in the database.");
        }
    }
}

// Simulate caching - replace with your caching logic
static async Task<UserData> GetCachedUserAsync()
{
    // Implement your caching logic here
    // Return the cached user data or null if not found in cache
    return null;
}

// Simulate caching - replace with your caching logic
static async Task CacheUserDataAsync(UserData user)
{
    // Implement your caching logic here
    // Cache the user data
}


// Define your DbContext class (replace MyDbContext with your actual DbContext)
public class MyDbContext : DbContext
{
    public DbSet<User> Users { get; set; }

    // Add your DbContext configuration here
}

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class UserData
{
    public int Id { get; set; }
    public string Name { get; set; }
}
using Microsoft.EntityFrameworkCore;

namespace ZR.CodeExampleOptimiseDataAccess
{
    public class ExampleDatabaseContext : DbContext
    {
        public ExampleDatabaseContext(DbContextOptions<ExampleDatabaseContext> options)
            : base(options)
        {
        }

        // Define your DbSet for the User entity (replace User with your actual entity class)
        public DbSet<User> Users { get; set; }

        // Add your DbSet for other entities if needed

        // Configure your entities and relationships here
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // Configure your entity mappings here if needed

            base.OnModelCreating(modelBuilder);
        }
    }
}

Use HttpClientFactory for Efficient HTTP Connections

Manage and pool HTTP connections using HttpClientFactory. This approach prevents excessive creation and disposal of HttpClient instances, enhancing connection reuse.

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddHttpClient("apiClient", client =>
{
    client.BaseAddress = new Uri("https://api.example.com");
});

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();


var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace ZR.CodeExampleHttpClientFactory.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ExampleController : ControllerBase
    {
        private readonly HttpClient apiClient;

        public ExampleController(IHttpClientFactory httpClientFactory)
        {
            this.apiClient = httpClientFactory.CreateClient("apiClient");
        }

        [HttpGet(Name = "GetExample")]
        public async Task<IActionResult> Get()
        {
            try
            {
                // Use the _apiClient to make HTTP requests to the configured base address
                HttpResponseMessage response = await this.apiClient.GetAsync("/api/someendpoint");

                if (response.IsSuccessStatusCode)
                {
                    // Handle the successful response
                    var content = await response.Content.ReadAsStringAsync();
                    return Ok(content); // Use Ok() to return a 200 OK response
                }
                else
                {
                    // Handle the error response
                    return StatusCode((int)response.StatusCode, "Error"); // Use StatusCode to return the appropriate status code
                }
            }
            catch (HttpRequestException)
            {
                // Handle exceptions related to the HTTP request
                return StatusCode(500, "Internal Server Error");
            }
        }

    }
}

Profile and Optimise Middleware

Performance-tune frequently-called code paths, especially middleware components. Leverage performance profiling tools to identify bottlenecks and ensure smooth request handling.

using ZR.CodeExamplePerformanceMiddleware.Middleware;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();


var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
app.UseMiddleware<PerformanceMiddleware>();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
using System.Diagnostics;

namespace ZR.CodeExamplePerformanceMiddleware.Middleware
{
    public class PerformanceMiddleware
    {
        private readonly RequestDelegate _next;

        public PerformanceMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task InvokeAsync(HttpContext context)
        {
            // Start the stopwatch to measure the execution time
            var stopwatch = Stopwatch.StartNew();

            // Call the next middleware component in the pipeline
            await _next(context);

            // Stop the stopwatch
            stopwatch.Stop();

            // Log the execution time (you can use a logging framework like Serilog)
            var elapsedMilliseconds = stopwatch.ElapsedMilliseconds;
            Console.WriteLine($"Middleware execution time: {elapsedMilliseconds} ms");
        }
    }
}

Delegate Long-Running Tasks

Employ background services or out-of-process approaches for handling long-running tasks. This prevents these tasks from blocking the main thread, ensuring uninterrupted application responsiveness.

using ZR.CodeExampleDelegateLongRunning.Services;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddHostedService<ExampleBackgroundService>();
var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
using Microsoft.Extensions.Hosting;

namespace ZR.CodeExampleDelegateLongRunning.Services
{
    public class ExampleBackgroundService: BackgroundService
    {
        private readonly ILogger<ExampleBackgroundService> logger;

        public ExampleBackgroundService(ILogger<ExampleBackgroundService> logger)
        {
            this.logger = logger;
        }
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                // Replace this with your actual long-running task logic
                this.logger.LogInformation("Background service is running at: {time}", DateTimeOffset.Now);

                // Simulate a task that runs every 10 minutes
                await Task.Delay(TimeSpan.FromMinutes(10), stoppingToken);
            }
        }
    }
}

Minimise Payload Size with Compression

Reduce the payload size of responses by enabling response compression. Compressed responses transmit faster over the network, enhancing user experience.

builder.Services.AddResponseCompression(options =>
{
    options.EnableForHttps = true;
});

Stay Up-to-Date with the Latest ASP.NET Core Release

Keep our ASP.NET Core application updated to the latest version. Updates often include performance enhancements and bug fixes, ensuring our app benefits from the latest optimisations.

Step 1. Update the .NET SDK

Ensure you have the latest .NET SDK installed by using the following command.

dotnet --list-sdks # List installed SDKs to check the current version
dotnet --list-runtimes # List installed runtimes (optional)
dotnet --version # Display the currently installed .NET SDK version
# To update the .NET SDK to the latest version (replace X.Y.Z with the latest version)
dotnet sdk install X.Y.Z

Step 2. Update the ASP.NET Core Application

To update your ASP.NET Core application, navigate to your project directory and run the following commands.

# Check the current version of the project (optional)
dotnet --version
# Update the ASP.NET Core project to the latest version
dotnet build --no-restore # Build the project without restoring dependencies
dotnet restore # Restore project dependencies
dotnet publish -c Release # Publish the project (optional)
# Run your application
dotnet run

The following commands will update your ASP.NET Core application to the latest version by restoring the project dependencies, building it, and running it.

Step 3. Test and Validate

Test your application thoroughly after updating it to ensure that it behaves as expected and that it is compatible with the latest ASP.NET Core version. Verify that any optimizations introduced by the update benefit your application's functionality and performance.

Step 4. Review Release Notes

ASP.NET Core release notes are also essential for understanding the changes, enhancements, and potentially breaking changes introduced in the update. By doing so, you will be able to take full advantage of the latest optimizations and adapt your application if necessary.

Avoid Accessing HttpContext from Multiple Threads

Keep in mind that HttpContext is not thread-safe. Avoid simultaneous access from multiple threads to prevent unexpected behavior and potential performance issues.

using Microsoft.AspNetCore.Mvc;

namespace ZR.CodeExampleAvoidAccessingHttpContext.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class ExampleController : ControllerBase
    {
        private readonly ILogger<ExampleController> logger;

        public ExampleController(ILogger<ExampleController> logger)
        {
            this.logger = logger;
        }

        [HttpGet("safe")]
        public async Task<IActionResult> SafeAccessAsync()
        {
            try
            {
                // Access HttpContext within the asynchronous action method
                var context = HttpContext;

                // Perform your operations using HttpContext
                var userId = context.User.Identity.Name;

                // Simulate asynchronous work (e.g., database access)
                await Task.Delay(TimeSpan.FromSeconds(2));

                // Access HttpContext again
                var requestId = context.TraceIdentifier;

                // Return a response
                return Ok(new
                {
                    UserId = userId,
                    RequestId = requestId
                });
            }
            catch (Exception ex)
            {
                this.logger.LogError(ex, "An error occurred.");
                return StatusCode(500, "Internal Server Error");
            }
        }
    }
}

Handle Unknown Request Body Length

Be prepared for scenarios where the HttpRequest.ContentLength is not known. Design our application to handle these situations gracefully without causing disruptions.

using Microsoft.AspNetCore.Mvc;
using System.Text;

namespace ZR.CodeExampleHandleUnknownRequest.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ExampleController : ControllerBase
    {
        private readonly ILogger<ExampleController> logger;
        public ExampleController(ILogger<ExampleController> logger)
        {
            this.logger = logger;
        }

        [HttpPost("handle")]
        public async Task<IActionResult> HandleUnknownRequestBodyLengthAsync()
        {
            try
            {
                // Check if the content length is known and greater than zero
                if (HttpContext.Request.ContentLength == null || HttpContext.Request.ContentLength.Value <= 0)
                {
                    // Handle the scenario where content length is not known or invalid
                    return BadRequest("Invalid or missing request content length");
                }

                // Read the request body
                using (var reader = new StreamReader(Request.Body, Encoding.UTF8))
                {
                    var requestBody = await reader.ReadToEndAsync();

                    // Process the request body
                    this.logger.LogInformation($"Received request body: {requestBody}");

                    // Add your logic here to process the request body

                    return Ok("Request body processed successfully");
                }
            }
            catch (Exception ex)
            {
                this.logger.LogError(ex, "An error occurred while handling the request.");
                return StatusCode(500, "Internal Server Error");
            }
        }
    }
}

Summary

In summary, optimising the performance of our ASP.NET Core application requires a combination of well-structured code, asynchronous programming, caching strategies, and utilising the latest tools and features. By following these 15 best practices, we can create a high-performing application that delivers an exceptional user experience and efficiently handles various workloads.

The Code Examples are available on my GitHub Repository: https://github.com/ziggyrafiq/ASP.NET-Core-Performance-Enhancements    

Please do not forget to follow me on LinkedIn https://www.linkedin.com/in/ziggyrafiq/ and click the like button if you have found this article useful/helpful.

Ziggy Rafiq has won the recent C# Cornet MVP Award, C# Corner Member of the Month, C# Corner VIP Award, C# Corner Speak, C# Corner Chapter Lead, Microsoft West Midlands Top 10 Best Developer Award 2008 and Shell Step Award 2002