Skip to content
Legacy Modernisation

Migrating from .NET Framework to .NET 10: An AI-Assisted Approach with Claude Code

13 min read Matt Hammond

Migrating from .NET Framework 4.x to .NET 10 is the most common modernisation path for enterprise .NET applications. This guide walks through the process step by step, showing where Claude Code and the .NET Upgrade Assistant fit in and where human judgement is still essential.

  • Start with the .NET Upgrade Assistant for project file conversion and initial analysis.
  • Use Claude Code with a project-specific CLAUDE.md for the deeper code migration work.
  • Migrate project by project, starting with shared libraries and working outward to web/API projects.
  • Test continuously. Every migrated project should compile and pass tests before moving to the next.
  • Budget 30-50% of your time for human review, architectural decisions, and edge cases.

You have a .NET Framework 4.x solution with 15 projects. Where do you start?

The worst approach is opening every .csproj file and starting to change target frameworks. That path leads to a broken solution, hundreds of compiler errors, and no way to tell migration issues from pre-existing bugs.

The right approach is systematic: analyse first, migrate in dependency order, test at each step, and use AI tooling for the mechanical work while reserving human attention for the decisions that matter.

This guide assumes a typical enterprise .NET Framework 4.x application: ASP.NET MVC or Web API, Entity Framework 6, some class libraries, a test project or two, and possibly WCF services. If your application uses Web Forms, the additional complexity is addressed separately in our .NET modernisation playbook.

Phase 1: Analysis and preparation

Run the .NET Upgrade Assistant

Install and run the .NET Upgrade Assistant to get a baseline analysis:

dotnet tool install -g upgrade-assistant
upgrade-assistant analyze <path-to-solution.sln>

The analysis output tells you:

  • Which projects can migrate directly and which have blocking dependencies
  • Which NuGet packages have .NET 10 equivalents and which need replacement
  • Which APIs are unavailable on .NET 10

Do not use the Upgrade Assistant to perform the actual migration yet. Its automated migration is useful for project file conversion but tends to produce code that compiles but does not follow modern .NET 10 patterns. We use it for analysis and project file conversion, then use Claude Code for the code-level migration.

Set up your CLAUDE.md

Before using Claude Code for migration work, create a CLAUDE.md file in the repository root. This is the single most impactful step for migration quality.

# Project: OrderManagement Migration

## Target
- Source: .NET Framework 4.8
- Target: .NET 10 (LTS)
- Hosting: Azure Container Apps
- CI: GitHub Actions

## Conventions
- File-scoped namespaces
- Primary constructors for services with injected dependencies
- ILogger<T> (replace log4net calls)
- IOptions<T> pattern for configuration sections
- Nullable reference types: enabled
- Implicit usings: enabled

## API migration rules
- HttpWebRequest / WebClient -> HttpClient via IHttpClientFactory
- ConfigurationManager.AppSettings -> IConfiguration
- ConfigurationManager.ConnectionStrings -> IConfiguration.GetConnectionString()
- Thread.Sleep -> await Task.Delay
- Global.asax -> Program.cs with WebApplicationBuilder
- web.config appSettings -> appsettings.json
- web.config transforms -> appsettings.{Environment}.json
- System.Web.HttpContext.Current -> inject IHttpContextAccessor
- FormsAuthentication -> ASP.NET Core Identity or JWT

## Data access
- EF6 DbContext -> EF Core DbContext
- Keep all table names and column names identical
- Fluent API configuration only (no data annotations)
- Lazy loading: OFF (use explicit .Include())

## Testing
- MSTest -> xUnit
- Moq -> NSubstitute
- FluentAssertions for all assertions
- Test class naming: {ClassUnderTest}Tests
- Test method naming: {Method}_{Scenario}_{ExpectedResult}

## Do not
- Change any business logic during migration
- Rename public API methods or parameters
- Remove or modify XML doc comments
- Add async to synchronous methods unless required for API compatibility
- Change database schema

Map the dependency graph

Before migrating any code, understand the dependency order. In a typical .NET Framework solution:

Shared libraries (no project references)

Domain / business logic projects

Data access projects (EF6 context, repositories)

Service layer projects

Web / API projects (ASP.NET MVC, Web API)

Test projects

Migrate bottom-up: shared libraries first, web projects last. Each project should compile and pass its tests before you move to the next. This prevents cascading errors and makes it clear which failures are caused by migration and which are pre-existing.

Phase 2: Project-by-project migration

Step 1: Project file conversion

Use the .NET Upgrade Assistant to convert packages.config and old-format .csproj files to the SDK-style format:

upgrade-assistant upgrade <path-to-project.csproj> --target-tfm net10.0

This handles:

  • Converting to SDK-style .csproj (the biggest structural change)
  • Updating the target framework moniker to net10.0
  • Migrating packages.config to <PackageReference> elements
  • Removing references to System.Web and other framework assemblies

Review the converted .csproj carefully. The Upgrade Assistant sometimes adds unnecessary package references or misses transitive dependencies.

Step 2: Code migration with Claude Code

With the project file converted, use Claude Code for the code-level migration. For each project, the workflow is:

Analyse the project:

> Read the project at src/OrderManagement.Domain/ and identify all .NET Framework APIs
> that need migration. List each API, its location, and the .NET 10 equivalent from CLAUDE.md.

Claude Code produces a structured migration list. Review it before proceeding.

Execute the migration:

> Migrate src/OrderManagement.Domain/ to .NET 10 following the rules in CLAUDE.md.
> Migrate one file at a time. After each file, confirm the change before moving on.

For larger projects, you can batch files:

> Migrate all files in src/OrderManagement.Domain/Services/ to .NET 10.
> Apply the API migration rules from CLAUDE.md. Use file-scoped namespaces.

Common migrations Claude Code handles well:

// BEFORE: .NET Framework configuration access
var connectionString = ConfigurationManager
    .ConnectionStrings["OrderDb"].ConnectionString;
var maxRetries = int.Parse(
    ConfigurationManager.AppSettings["MaxRetries"]);

// AFTER: .NET 10 configuration (generated by Claude Code)
public class OrderService(
    IConfiguration configuration,
    IOptions<RetryOptions> retryOptions)
{
    private readonly string _connectionString =
        configuration.GetConnectionString("OrderDb")
        ?? throw new InvalidOperationException(
            "OrderDb connection string not configured");
    private readonly int _maxRetries = retryOptions.Value.MaxRetries;
}
// BEFORE: .NET Framework HTTP client
var request = (HttpWebRequest)WebRequest.Create(url);
request.Method = "GET";
request.ContentType = "application/json";
using var response = (HttpWebResponse)request.GetResponse();
using var reader = new StreamReader(response.GetResponseStream());
var json = reader.ReadToEnd();

// AFTER: .NET 10 HttpClient via IHttpClientFactory
public class ExternalApiClient(IHttpClientFactory httpClientFactory)
{
    public async Task<string> GetAsync(string url)
    {
        using var client = httpClientFactory.CreateClient("ExternalApi");
        var response = await client.GetAsync(url);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync();
    }
}

Step 3: Dependency injection registration

.NET Framework applications often use third-party DI containers (Autofac, Ninject, Unity) or manual service location. .NET 10 has built-in DI. The migration pattern:

// BEFORE: Autofac registration in Global.asax
var builder = new ContainerBuilder();
builder.RegisterType<OrderRepository>().As<IOrderRepository>();
builder.RegisterType<OrderService>().As<IOrderService>();
var container = builder.Build();
DependencyResolver.SetResolver(
    new AutofacDependencyResolver(container));

// AFTER: .NET 10 built-in DI in Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<IOrderService, OrderService>();

Claude Code handles this translation reliably when the CLAUDE.md specifies the DI convention. The main decision for humans: whether to stick with the third-party container (Autofac has a .NET 10 integration) or migrate to built-in DI. For most applications, built-in DI is sufficient and reduces a dependency.

Step 4: Configuration migration

// web.config
<appSettings>
  <add key="MaxRetries" value="3" />
  <add key="ApiBaseUrl" value="https://api.example.com" />
</appSettings>

// appsettings.json (generated by Claude Code)
{
  "RetryOptions": {
    "MaxRetries": 3
  },
  "ExternalApi": {
    "BaseUrl": "https://api.example.com"
  }
}

// Strongly-typed options class
public class RetryOptions
{
    public int MaxRetries { get; init; } = 3;
}

// Registration in Program.cs
builder.Services.Configure<RetryOptions>(
    builder.Configuration.GetSection("RetryOptions"));

Phase 3: Testing and verification

Migrate the test projects

Test project migration follows the same pattern. If migrating from MSTest to xUnit:

// BEFORE: MSTest
[TestClass]
public class OrderServiceTests
{
    [TestMethod]
    public void CalculateTotal_WithDiscount_AppliesDiscount()
    {
        var service = new OrderService();
        var result = service.CalculateTotal(100m, 0.1m);
        Assert.AreEqual(90m, result);
    }
}

// AFTER: xUnit with FluentAssertions (generated by Claude Code)
public class OrderServiceTests
{
    [Fact]
    public void CalculateTotal_WithDiscount_AppliesDiscount()
    {
        var service = new OrderService();
        var result = service.CalculateTotal(100m, 0.1m);
        result.Should().Be(90m);
    }
}

Claude Code handles test framework migration effectively because the patterns are well-defined. The main gotcha: MSTest [TestInitialize] and [TestCleanup] become constructor and IDisposable.Dispose() in xUnit, which changes the lifecycle model. Claude Code knows this mapping, but review the converted tests to ensure setup/teardown order is preserved.

Run the full suite

After migrating all projects, run the complete test suite. Categorise failures:

  • Migration errors: API mismatches, missing using statements, type conversion issues. Claude Code can fix these.
  • Behavioural differences: Logic that behaves differently on .NET 10 (e.g., EF Core lazy loading changes, string comparison differences). These need human investigation.
  • Pre-existing failures: Tests that were already failing before migration. Do not fix these during migration. Track them separately.

Phase 4: Program.cs and startup

The final migration step is replacing Global.asax, Startup.cs, and web.config with the .NET 10 Program.cs entry point.

var builder = WebApplication.CreateBuilder(args);

// Configuration
builder.Services.Configure<RetryOptions>(
    builder.Configuration.GetSection("RetryOptions"));

// Data access
builder.Services.AddDbContext<OrderDbContext>(options =>
    options.UseSqlServer(
        builder.Configuration.GetConnectionString("OrderDb")));

// Services
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<IOrderService, OrderService>();

// HTTP clients
builder.Services.AddHttpClient("ExternalApi", client =>
{
    client.BaseAddress = new Uri(
        builder.Configuration["ExternalApi:BaseUrl"]
        ?? throw new InvalidOperationException(
            "ExternalApi:BaseUrl not configured"));
});

// MVC
builder.Services.AddControllersWithViews();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");

app.Run();

Claude Code generates this from your existing Global.asax and Startup.cs, but review it carefully. The middleware order matters (authentication before authorisation, CORS before routing in API projects), and getting it wrong causes subtle bugs.

What to watch for

String comparison behaviour. .NET 10 defaults to ordinal string comparison in some contexts where .NET Framework used culture-sensitive comparison. This can cause subtle sorting and equality bugs. If your application depends on culture-sensitive string behaviour, add explicit StringComparison parameters.

DateTime handling. DateTime.Now behaves identically, but if your application serialises dates to JSON, the default serialiser in .NET 10 (System.Text.Json) handles dates differently from Newtonsoft.Json. Either configure System.Text.Json to match your existing format or keep Newtonsoft.Json during migration.

Assembly loading. .NET Framework’s AppDomain and assembly loading model is different from .NET 10’s. If your application loads plugins or assemblies dynamically, this requires manual redesign.

Windows-specific APIs. Some .NET Framework APIs (registry access, Windows services, COM interop) require the Microsoft.Windows.Compatibility NuGet package on .NET 10. Claude Code adds this when it encounters Windows-specific APIs, but verify the package is present.

What we have seen in practice

[CLIENT EXAMPLE: Professional services firm, 150k LOC .NET Framework 4.7.2 MVC application with EF6 and Autofac. Migrated to .NET 10 in 7 weeks using this approach. Claude Code handled approximately 65% of code changes. The biggest challenge was EF6 to EF Core: the application used lazy loading extensively, and the behavioural differences caused 23 test failures that required manual investigation. Total test pass rate after migration: 98.5% (the remaining 1.5% were pre-existing failures).]

[CLIENT EXAMPLE: Logistics company, 85k LOC .NET Framework 4.8 Web API with heavy HttpWebRequest usage for third-party integrations. Migrated to .NET 10 in 4 weeks. The CLAUDE.md-driven approach was particularly effective here: the API migration rules were consistent across the codebase, so Claude Code applied them with high accuracy. The HttpClient migration (IHttpClientFactory pattern) was the single largest category of changes and was handled almost entirely by Claude Code.]

[CLIENT EXAMPLE: Energy sector, 220k LOC across 18 projects, mix of .NET Framework 4.6 and 4.7.2. Required a phased approach: shared libraries and domain projects first (weeks 1-3), data access layer (weeks 4-5), service layer and API projects (weeks 6-9), test migration and verification (weeks 10-11). Claude Code’s /compact command was essential for maintaining context across the large solution. Total AI-authored code: ~70%.]

Next steps

If you are planning a .NET Framework to .NET 10 migration, start with an assessment. Our Legacy .NET Assessment Framework provides a scoring model to evaluate your application’s migration readiness before committing resources.

For the broader strategic context, including when modernisation is not the right answer, see our .NET modernisation playbook.

For IP and licensing considerations around AI-generated code in enterprise deliverables, see Who Owns AI-Written Code?.

For guidance on setting up Claude Code for .NET development beyond migration work, see our Claude Code for .NET developers guide.

Ready to plan your migration? Book a free Legacy .NET Assessment consultation.

Frequently asked questions

Can Claude Code migrate an entire .NET Framework solution automatically?
No. Claude Code handles the mechanical translation work (project file conversion, namespace changes, API replacements) effectively, but it cannot make architectural decisions, resolve ambiguous business logic, or guarantee behavioural equivalence. Expect 50-70% of changes to be AI-generated, with human review on every change.
Should I use the .NET Upgrade Assistant or Claude Code?
Use both. The .NET Upgrade Assistant handles project file conversion and produces an initial analysis. Claude Code handles the deeper code-level changes: API replacements, pattern migrations, and test updates. They complement each other rather than competing.
What about ASP.NET Web Forms migration?
Web Forms to Blazor is the most complex migration pattern in the .NET ecosystem. The page lifecycle, ViewState, and server control models have no direct equivalents. Claude Code can convert individual controls, but most Web Forms pages need rethinking rather than translating. Consider whether Blazor, React, or another UI framework is the right target.
How do I handle EF6 to EF Core migration?
Claude Code handles the mechanical changes well: DbContext configuration, fluent API syntax, and LINQ query updates. Watch for lazy loading behaviour changes (EF Core defaults to no lazy loading), ObjectContext usage (EF Core only supports DbContext), and inheritance mapping differences.
What if my application uses WCF?
WCF is not available on modern .NET. Options include gRPC (best for internal services), REST minimal APIs (best for external APIs), or CoreWCF (community compatibility layer for simpler cases). See our WCF to gRPC migration guide for the detailed comparison.
How do I set up CLAUDE.md for a .NET migration project?
Create a CLAUDE.md file in the repository root. Document the target framework, coding conventions, migration rules (which APIs map to which replacements), testing framework, and any project-specific patterns. This gives Claude Code the context to generate code that matches your project's standards.

Ready to transform your software?

Let's talk about your project. Contact us for a free consultation and see how we can deliver a business-critical solution at startup speed.