.NET 5 Migration: Real World Lessons

.NET 5 has been out for a few months now, and after migrating several enterprise-grade monoliths from .NET Framework 4.8 and .NET Core 3.1, the dust has settled. The promise of “One .NET” is largely fulfilled, but the path is not without its perilous cliffs. In this deep dive, I will share the architectural strategies, performance benchmarks, and common pitfalls encountered during real-world migrations of high-throughput financial systems.

The Business Case for Migration

Before writing a single line of code, you must justify the migration to stakeholders. “It’s newer” is not a valid business reason. However, the following are:

  • Performance Per Dollar: .NET 5 boasts a 30-50% improvement in socket throughput and JSON serialization speed compared to .NET Core 3.1. This directly translates to needing fewer Azure App Service Instances or Kubernetes Nodes. In one case, we reduced our cluster size by 40%, saving $12,000/month.
  • Long Term Support? No.: Be transparent. .NET 5 is Current, not LTS. Support ends 3 months after .NET 6 launches. This migration is a stepping stone to .NET 6 LTS. If your organization requires strict LTS adherence, wait. But for Agile teams, the performance gains are worth the interim upgrade.
  • Cross-Platform Capabilities: Moving off Windows-only .NET Framework allows you to run on Linux containers, which are significantly cheaper and faster to spin up than Windows containers.

Architectural Pattern: The Strangler Fig

Attempting a “Big Bang” rewrite of a 10-year-old monolith is a career-limiting move. The risk of regression is too high. Instead, we employ the Strangler Fig pattern using a reverse proxy.

flowchart TB
    Client["Web/Mobile Client"] -- HTTPS --> YARP["YARP Reverse Proxy"]
    
    subgraph Legacy ["Legacy Environment (Windows)"]
        YARP -- "/api/legacy" --> Old["IIS Monolith (.NET 4.8)"]
        Old --> SQL["SQL Server"]
    end
    
    subgraph Modern ["Modern Environment (Linux K8s)"]
        YARP -- "/api/orders" --> NewOrders["Order Service (.NET 5)"]
        YARP -- "/api/users" --> NewUsers["User Service (.NET 5)"]
        NewOrders --> Redis["Redis Cache"]
        NewOrders --> SQL
    end
    
    style YARP fill:#FFF3E0,stroke:#E65100
    style Old fill:#FFEBEE,stroke:#C62828
    style NewOrders fill:#E8F5E9,stroke:#2E7D32

We use YARP (Yet Another Reverse Proxy), a Microsoft-supported proxy written in .NET. It allows us to route traffic based on complex logic (headers, cookies, paths) that would be difficult in Nginx.

Common Breaking Changes

1. WCF is Dead (Mostly)

There is no server-side WCF in .NET 5. If your monolith relies on exposing `BasicHttpBinding` endpoints, you have two choices:

  • CoreWCF: A community-led port of WCF to .NET Core. It works for basic scenarios but lacks full feature parity (like WS-* protocols).
  • gRPC: The preferred modernization path. We rewrote our internal WCF communication to gRPC. The contract-first approach (`.proto` files) is similar to WSDL but much more efficient.

2. BinaryFormatter is Obsolete

Security researchers have proven that `BinaryFormatter` is fundamentally insecure (deserialization attacks). .NET 5 explicitly blocks it in ASP.NET Core apps. If you are using it to store session state in Redis, you must migrate to `System.Text.Json` or `MessagePack`. This is a hard blocker.

Implementation: Updating the Project File

The `.csproj` format has been simplified. Gone are the days of listing every single file. Here is what a clean .NET 5 Web API project looks like:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <Nullable>enable</Nullable> <!-- Critical for C# 9 -->
    <ImplicitUsings>enable</ImplicitUsings>
    <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.0" />
    <PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
  </ItemGroup>

</Project>

Performance Benchmarks

We ran a load test comparison using k6. The scenario was a simple JSON serialization of a list of 100 customer objects.

FrameworkRequests/SecP99 LatencyMemory Usage
.NET Framework 4.812,500120ms450 MB
.NET Core 3.145,00045ms210 MB
.NET 5.058,00032ms180 MB

Conclusion

Migrating to .NET 5 is not just an upgrade; it is a transformation. It enables modern architectural patterns (Containerization, Microservices) that were painful or impossible on Framework. Start small, verify benchmarks, and leverage the new C# 9 features to clean up your codebase.


Discover more from C4: Container, Code, Cloud & Context

Subscribe to get the latest posts sent to your email.

Leave a comment

Your email address will not be published. Required fields are marked *

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