Trust is earned, not given

A different perspective

2026-04-22 · Technology

Modernizing Legacy Architectures

Modernizing Legacy Architectures: Architectural Migration from .NET Framework Web Site to .NET 10 MVC Utilizing Claude Code and Playwright Test Automation

1. Introduction and Architectural Paradigm Shift

For over two decades, the Microsoft enterprise software ecosystem relied heavily on the classic .NET Framework as its primary foundation for web application development. Within this paradigm, ASP.NET Web Sites and ASP.NET Web Forms emerged as the dominant paradigms for building enterprise software. These architectures allowed developers to rapidly assemble web interfaces using server-side controls, complex event-driven lifecycle pipelines, and heavy abstractions such as ViewState to simulate desktop-like statefulness over a stateless HTTP protocol. However, as web standards evolved toward stateless RESTful APIs, modern single-page applications, cross-platform containerization, and cloud-native deployments, the limitations of ASP.NET Web Sites became increasingly pronounced. Legacy .NET Framework Web Site projects—often characterized by dynamic compilation models, tight coupling between UI markup (.aspx) and code-behind files (.aspx.cs), direct database access patterns, and heavy reliance on Windows-specific IIS server infrastructure—now represent significant technical debt for modern organizations.

With the official release of .NET 10, Microsoft has solidified a modern, high-performance, unified, cross-platform development ecosystem. Operating on a modern runtime optimized for cloud environments, Linux containers, web assembly, and scalable microservices, .NET 10 offers superior throughput, memory efficiency, and developer productivity compared to legacy .NET Framework 4.x. Consequently, enterprise software engineering teams face an urgent strategic imperative: modernizing legacy web assets to current architectural standards. However, migrating an ASP.NET Web Site to a .NET 10 Model-View-Controller (MVC) architecture presents profound technical challenges. Primary among these challenges is the realization that no direct, automated, line-for-line, or tool-assisted conversion path exists between ASP.NET Web Sites and .NET 10 MVC. The architectural disparity between the event-driven page lifecycle of classic ASP.NET and the clean request-response, action-driven, dependency-injected design of modern ASP.NET Core MVC is simply too vast for traditional automated conversion wizards or static transformation tools.

In response to this architectural divide, modern engineering teams must adopt an agentic AI-driven rewrite strategy. Rather than attempting brittle syntax translation, software architects must leverage advanced, autonomous command-line agentic coding systems—specifically Claude Code—to systematically comprehend, refactor, and rewrite legacy codebases into idiomatic .NET 10 MVC structures. Furthermore, to ensure functional parity, prevent regressions, and validate complex enterprise business logic throughout this transformative process, continuous automated end-to-end testing must be integrated directly into the migration feedback loop. By combining cross-platform browser automation via Playwright scripts with Claude Code’s autonomous goal-driven iterative refinement execution engine (invoked via the `/goal` command), development teams can construct an automated, self-correcting modernization pipeline. This paper presents an exhaustive analysis of the architectural, programmatic, and operational methodologies required to orchestrate a successful AI-assisted enterprise migration from legacy .NET Framework Web Sites to modern .NET 10 MVC architectures.

2. Architectural Divergence: The Imperative for a Complete Architectural Rewrite

Understanding why direct automated conversion tools fail requires an in-depth examination of the fundamental architectural divergence between ASP.NET Web Sites running on .NET Framework and ASP.NET Core MVC running on .NET 10. Legacy ASP.NET Web Sites operate on an event-driven lifecycle model introduced in the early 2000s to ease the transition of desktop developers (specifically Visual Basic and WinForms engineers) into web development. Central to this model is the `System.Web.UI.Page` class, which processes incoming requests through a sequence of deterministic lifecycle events, including `Init`, `Load`, `ControlEvents`, `PreRender`, and `Unload`. Within this abstraction, control state and form data are persisted across HTTP POST requests using `ViewState`—a heavily encoded, potentially massive string injected into hidden HTML form fields. Furthermore, Web Site projects lack a formal project file (`.csproj`), relying instead on dynamic, runtime compilation where files in directory trees are parsed on demand by the IIS web server.

In stark contrast, ASP.NET 10 MVC is built upon a modular, stateless, middleware-driven HTTP pipeline. It enforces strict separation of concerns through the Model-View-Controller design pattern. Incoming HTTP requests are intercepted by routing engines, mapped to controller actions, processed asynchronously, and rendered via dynamic Razor templates (`.cshtml`) or returned as serialized JSON payloads. In .NET 10 MVC, concepts such as server controls (`<asp:TextBox>`, `<asp:GridView>`), postbacks (`IsPostBack`), controls tree state, and `ViewState` do not exist. There is no direct equivalent of `System.Web.HttpContext.Current` accessible static global state; instead, request contexts, configuration, logging, and application dependencies are managed cleanly through dependency injection (DI) containers managed by `IServiceCollection` during application bootstrapping in `Program.cs`.

Previous migration tools, such as the Microsoft .NET Upgrade Assistant, are effective at updating project file schemas, upgrading target framework monikers (TFMs), and modifying references for modern class libraries or ASP.NET Core Web APIs that already utilize modern patterns. However, when confronted with an ASP.NET Web Site or Web Forms application, automated upgrade assistants hit an insurmountable wall. They cannot automatically transform event-driven code handlers (`protected void Button1_Click(object sender, EventArgs e)`) into RESTful or REST-like Controller action methods (`public async Task<IActionResult> SubmitForm(SubmitModel model)`). Automated tools cannot reconcile server-side databinding controls (`DataBind()`, `Eval()`) with strongly typed ViewModel binding and Razor view helpers. Consequently, attempts to perform direct migration invariably produce uncompilable code bases littered with broken static calls to `System.Web`.

Therefore, modern software engineering methodologies acknowledge that migrating an ASP.NET Web Site to .NET 10 MVC is fundamentally an architectural rewrite, not an in-place refactoring. The legacy application must be treated as a behavioral specification. Developers must extract business logic, domain models, validation rules, and UI behaviors from the legacy source code and reimplement them within a newly architected .NET 10 solution structure. Because manual rewrites of large-scale enterprise systems are notoriously time-consuming, prone to human error, and expensive, integrating agentic artificial intelligence into the rewrite workflow becomes the primary mechanism for accelerating modernizations while maintaining complete precision.

3. Claude Code as the Agentic Engine for Modernization

Claude Code represents a paradigm shift in AI-assisted software engineering. Unlike traditional IDE-integrated inline autocompletion plugins that operate within isolated context windows or single-file scopes, Claude Code is an autonomous command-line interface (CLI) agentic system. It operates directly within the developer's local terminal environment, maintaining direct access to the file system, shell environment, git repository, compiler tools, and language servers. This agentic architecture enables Claude Code to comprehend entire repository structures, analyze complex multi-file dependency graphs, execute shell commands, run test suites, interpret error logs, and continuously modify code iteratively until specific objectives are achieved.

In the context of migrating a legacy .NET Web Site to .NET 10 MVC, Claude Code serves as an intelligent architectural translator. Because legacy Web Sites lack structured dependency graphs, business logic is frequently buried inside code-behind files (`.aspx.cs`), master pages (`.master.cs`), global configuration files (`Global.asax`), and embedded SQL or inline data access utilities. Claude Code excels at performing global deep static analysis of these unstructured legacy files, disentangling presentation logic from core domain rules, and synthesizing clean, idiomatic C# 14 / .NET 10 enterprise structures.

To maximize Claude Code's performance during a project rewrite, software engineers must establish a structured configuration and operational context within the repository. Central to this preparation is the initialization of a specialized `CLAUDE.md` file in the project root. The `CLAUDE.md` file serves as the agent's persistent memory and architectural directive, defining explicit coding standards, framework conventions, target folder structures, command shortcuts, and migration rules. A representative `CLAUDE.md` file for a .NET 10 MVC migration specifies targeted rules such as enforcing async/await patterns across all repository calls, mandating constructor dependency injection over static initializations, enforcing strict nullable reference types, and standardizing Razor view naming conventions.

Furthermore, Claude Code utilizes specialized memory mechanisms, local context files, and directory summaries to navigate massive codebases without exceeding context limits. When pointed at a legacy Web Site directory, Claude Code builds an internal mental model of legacy database schemas, user authorization policies, session management patterns, and client-side scripts. By issuing high-level structural prompts, software architects can direct Claude Code to scaffold a clean .NET 10 solution, establish Clean Architecture or Onion Architecture layers (Domain, Infrastructure, Application, and Web Presentation), configure dependency injection within `Program.cs`, and incrementally transcribe legacy business rules into dedicated domain service classes.

4. Step-by-Step Technical Execution of the Migration

Executing a migration from an ASP.NET Web Site to .NET 10 MVC using Claude Code requires a rigorous, multi-phased engineering methodology. The process begins with Preparation and Architectural Analysis. Before writing new code, the legacy ASP.NET Web Site must be compiled, cataloged, and audited. Developers execute Claude Code to inspect the legacy directory structure, analyze all `.aspx`, `.ascx`, `.master`, `.cs`, and `web.config` files, and map existing routing structures, database connections, and third-party dependencies. During this phase, Claude Code generates a comprehensive dependency mapping report, identifying legacy dependencies—such as `System.Web.Security.Membership`, legacy ADO.NET `SqlDataReader` patterns, or `ConfigurationManager` static calls—that lack direct cross-platform equivalents in .NET 10 and must be replaced with modern alternatives like ASP.NET Core Identity, Entity Framework Core 10, and `IConfiguration` options patterns.

The second phase entails Environment Setup and Greenfield Solution Scaffolding. Utilizing modern CLI commands via the terminal, the engineer instructs Claude Code to create a new .NET 10 MVC solution. Claude Code issues standard CLI commands (`dotnet new sln`, `dotnet new mvc`, `dotnet new classlib`) to construct a modular project structure comprising core domain libraries, EF Core data contexts, and the ASP.NET Core Web MVC host application. During scaffolding, `Program.cs` is configured with necessary middleware pipelines, including static file hosting, routing, authentication, authorization, and modern logging frameworks. The database access layer is modernized by having Claude Code analyze legacy SQL queries or EDMX files and generate EF Core 10 DbContext implementations and strongly typed entity models.

Phase three focuses on Component-by-Component Modernization. The migration proceeds incrementally, module by module or feature by feature, rather than attempting a high-risk 'big bang' cutover. For each legacy web page (e.g., `AccountDetails.aspx`), Claude Code is instructed to perform a multi-step transcription process:

First, Claude Code extracts all data contracts from input fields, grid layouts, and postback state, creating modern C# ViewModels with data annotations (`[Required]`, `[StringLength]`) for client and server-side validation. Second, Claude Code extracts raw business operations from `AccountDetails.aspx.cs` page lifecycle methods (`Page_Load`, `btnSave_Click`) and refactors them into asynchronous domain service methods in the application layer. Third, Claude Code constructs a modern ASP.NET 10 Controller (`AccountController.cs`) with clean action methods (`[HttpGet] Index()`, `[HttpPost] Save(AccountViewModel model)`) utilizing explicit model binding and returning `IActionResult` responses. Finally, Claude Code transcribes the markup from `AccountDetails.aspx` into a clean Razor view (`Views/Account/Index.cshtml`), replacing legacy ASP.NET server controls (`<asp:TextBox id="txtName" runat="server" />`) with modern HTML5 controls and Razor Tag Helpers (`<input asp-for="Name" class="form-control" />`).

Phase four addresses the replacement of legacy ASP.NET Session State, Authentication, and Global Handlers. ASP.NET Web Sites rely heavily on in-memory `Session["Key"]` storage and Forms Authentication modules configured via `web.config`. Under Claude Code's orchestration, these legacy mechanics are refactored. Session usages are either migrated to distributed Redis caching abstractions (`IDistributedCache`) or redesigned to utilize stateless JWT tokens and modern Cookie Authentication schemes configured through `builder.Services.AddAuthentication()`. Global lifecycle handlers previously residing in `Global.asax.cs` (`Application_Start`, `Application_Error`) are converted into standard ASP.NET Core middleware classes or custom error handling filters registered in the modern request pipeline.

5. Verification and Quality Assurance via Playwright Automated Testing

A primary risk in rewriting enterprise applications is functional drift—the unintentional alteration of business rules, UI validation behavior, edge-case calculations, or user workflows during structural refactoring. Because legacy ASP.NET Web Sites often suffer from low unit-test coverage due to tight coupling with `System.Web`, traditional unit testing cannot adequately validate migration accuracy. The most effective mechanism for guaranteeing behavioral parity is end-to-end (E2E) automated browser testing implemented via Microsoft Playwright.

Microsoft Playwright is a modern, fast, cross-browser automation framework that provides reliable, resilient end-to-end testing across Chromium, WebKit, and Firefox engines. Playwright's auto-waiting capabilities, shadow-DOM inspection, network interception, and headless execution make it the ideal test framework for enterprise web applications. In a modernizing workflow, Playwright scripts serve as an immutable behavioral benchmark. Before rewriting a legacy web module, developers configure Playwright scripts (written in Node.js/TypeScript or C#) to execute full user workflows against the running legacy ASP.NET Web Site. These scripts capture baseline behavioral snapshots: form submission responses, DOM mutations, dynamic CSS class shifts, AJAX payload structures, and database persistence states.

Once baseline Playwright scripts are established and validated against the legacy application, they are pointed at the newly scaffolded .NET 10 MVC application. When executing against the modernized site, the Playwright tests act as strict validation gates. Any discrepancy in element selectors, route behavior, form post locations, validation messages, or state persistence triggers test assertions and generates detailed failure logs, visual comparison diffs, and execution traces. This immediate testing feedback provides the objective ground truth required to verify that the newly rewritten .NET 10 application accurately reproduces legacy behavior while executing on a modern architecture.

6. Continuous Autonomous Remediation: The `/goal` Command Loop

While Playwright provides robust test failure reporting, manually inspecting test logs, locating bugs in newly rewritten .NET 10 controllers, and writing fixes remains a tedious manual task. Claude Code bridges this gap through its autonomous, goal-oriented execution mode, driven by the `/goal` command. The `/goal` command allows software engineers to assign high-level declarative objectives to Claude Code, shifting the developer's role from manual operator to high-level system reviewer.

When executing an autonomous iteration, a developer issues a comprehensive directive such as: `/goal "Execute the Playwright end-to-end test suite against the modern .NET 10 MVC application. If any test fails, inspect the Playwright failure trace and error logs, identify the root cause in the .NET 10 C# controllers or Razor views, fix the source code, rebuild the solution using dotnet build, re-run the failed Playwright test, and repeat this cycle until all end-to-end tests pass cleanly."

Upon receiving this goal, Claude Code initiates a closed-loop, autonomous engineering control loop:

First, Claude Code issues terminal commands to launch the target .NET 10 application and execute the Playwright test runner (`npx playwright test` or `dotnet test`). Second, if Playwright reports a failure (for example, a missing validation summary element or an incorrect routing target), Claude Code automatically intercepts the stdout/stderr stream, locates the Playwright trace file, and parses the exact DOM selector failure or HTTP error code. Third, Claude Code uses its static analysis engine to trace the bug back to specific modern source files—such as a missing `[ValidateAntiForgeryToken]` attribute, an unhandled null model property, or an incorrect ASP.NET Core Tag Helper path (`asp-action`). Fourth, Claude Code autonomously edits the source code files in place, adjusting implementation logic to match expected behavior. Fifth, Claude Code invokes the compiler (`dotnet build`) to verify that no compilation errors were introduced. Finally, Claude Code re-runs the specific Playwright test. If the test passes, it continues to the next failing test; if it fails, it re-evaluates its approach and applies alternative code adjustments.

This autonomous loop operates continuously until every Playwright script executes successfully. By combining Playwright's objective validation assertions with Claude Code's autonomous code-repair engine via `/goal`, development teams achieve a self-correcting rewrite pipeline. Technical debt is eliminated, runtime bugs are caught instantly, and human developers are freed from repetitive debugging cycles, allowing them to focus on architectural oversight and system design.

7. Applied Architectural Modernization: A Concrete Code Transformation Case Study

To clearly illustrate the transformation process, consider a real-world enterprise scenario involving a user registration module within a legacy ASP.NET Web Site. In the legacy environment, the module consists of `UserRegistration.aspx` and its associated code-behind `UserRegistration.aspx.cs`. Below is an examination of the structural changes required during the AI-driven migration.

In the legacy `UserRegistration.aspx.cs` file, code is directly coupled to page controls, event handlers, manual database connection strings, and legacy membership static utilities:

// Legacy ASP.NET Web Site: UserRegistration.aspx.cs
public partial class UserRegistration : System.Web.UI.Page {
    protected void Page_Load(object sender, EventArgs e) {
        if (!IsPostBack) {
            lblMessage.Text = "Please enter details.";
        }
    }
    protected void btnSubmit_Click(object sender, EventArgs e) {
        string connStr = ConfigurationManager.ConnectionStrings["DbConn"].ConnectionString;
        using (SqlConnection conn = new SqlConnection(connStr)) {
            string sql = "INSERT INTO Users (Username, Email) VALUES (@u, @e)";
            SqlCommand cmd = new SqlCommand(sql, conn);
            cmd.Parameters.AddWithValue("@u", txtUsername.Text);
            cmd.Parameters.AddWithValue("@e", txtEmail.Text);
            conn.Open();
            cmd.ExecuteNonQuery();
        }
        Session["User"] = txtUsername.Text;
        Response.Redirect("Welcome.aspx");
    }
}

When Claude Code analyzes this legacy code-behind file during the migration workflow, it identifies several modern architectural violations: synchronous I/O, hardcoded ADO.NET SQL calls, raw session mutations, tight UI control coupling, and direct page redirection. Claude Code refactors this component into modern, layered .NET 10 MVC structures.

First, Claude Code generates a strongly typed ViewModel (`RegisterViewModel.cs`) with validation annotations:

// Modern .NET 10 MVC: RegisterViewModel.cs
namespace ModernApp.Models;

using System.ComponentModel.DataAnnotations;

public class RegisterViewModel {
    [Required(ErrorMessage = "Username is required.")]
    [StringLength(50)]
    public string Username { get; set; } = string.Empty;

    [Required]
    [EmailAddress]
    public string Email { get; set; } = string.Empty;
}

Next, Claude Code constructs a modern ASP.NET Core MVC Controller (`AccountController.cs`) that utilizes asynchronous database operations via EF Core 10 and leverages constructor dependency injection for service dependencies:

// Modern .NET 10 MVC: AccountController.cs
namespace ModernApp.Controllers;

using Microsoft.AspNetCore.Mvc;
using ModernApp.Models;
using ModernApp.Services;

public class AccountController : Controller {
    private readonly IUserService _userService;

    public AccountController(IUserService userService) {
        _userService = userService;
    }

    [HttpGet]
    public IActionResult Register() => View(new RegisterViewModel());

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Register(RegisterViewModel model) {
        if (!ModelState.IsValid) {
            return View(model);
        }
        await _userService.RegisterUserAsync(model.Username, model.Email);
        TempData["User"] = model.Username;
        return RedirectToAction("Index", "Home");
    }
}

Finally, Claude Code transcribes the legacy markup into a responsive Razor View (`Register.cshtml`) utilizing Tag Helpers and ASP.NET Core anti-forgery validation:

@* Modern .NET 10 MVC: Register.cshtml *@
@model ModernApp.Models.RegisterViewModel

<h2>User Registration</h2>
<form asp-action="Register" method="post">
    <div class="form-group">
        <label asp-for="Username"></label>
        <input asp-for="Username" class="form-control" />
        <span asp-validation-for="Username" class="text-danger"></span>
    </div>
    <div class="form-group">
        <label asp-for="Email"></label>
        <input asp-for="Email" class="form-control" />
        <span asp-validation-for="Email" class="text-danger"></span>
    </div>
    <button type="submit" class="btn btn-primary">Submit</button>
</form>

To validate this modern implementation, a Playwright test script written in C# executes end-to-end verification across the user journey:

// Playwright E2E Validation Script: RegistrationTests.cs
using Microsoft.Playwright;
using Xunit;

public class RegistrationTests {
    [Fact]
    public async Task ValidUserRegistration_ShouldSucceedAndRedirect() {
        using var playwright = await Playwright.CreateAsync();
        await using var browser = await playwright.Chromium.LaunchAsync(new() { Headless = true });
        var page = await browser.NewPageAsync();
        await page.GotoAsync("https://localhost:7001/Account/Register");

        await page.FillAsync("input[name='Username']", "testuser");
        await page.FillAsync("input[name='Email']", "[email protected]");
        await page.ClickAsync("button[type='submit']");

        await page.WaitForURLAsync("**/Home");
        Assert.Contains("Home", page.Url);
    }
}

If the Playwright script fails due to an incorrect selector or route mismatch during automated validation, Claude Code intercepts the test runner output via the `/goal` execution loop, corrects the underlying Razor view or Controller route, and re-executes the test until zero assertions fail. This practical transformation highlights the immense power and efficiency of AI-driven architectural modernizations.

8. Strategic Implications and Engineering Best Practices

The integration of agentic AI CLI tools like Claude Code and robust E2E test automation frameworks like Playwright fundamentally alters the economics and risk profile of enterprise software modernization. Historically, legacy rewrites were viewed as high-risk, multi-year initiatives that frequently exceeded budgets or produced unstable software due to manual transcription errors. By adopting the AI-driven agentic methodology described in this paper, engineering organizations can dramatically accelerate modernization timelines while increasing software quality and operational reliability.

To achieve maximum success when migrating legacy enterprise assets to .NET 10 MVC, software engineering teams should adhere to several critical best practices:

1. Adopt an Incremental, Strangler-Fig Migration Pattern: Avoid attempting to rewrite vast legacy monoliths in a single unified cutover. Instead, utilize the Strangler-Fig pattern, deploying a modern ASP.NET Core reverse proxy (such as YARP—Yet Another Reverse Proxy) in front of the legacy environment. Route incoming traffic incrementally to modernized .NET 10 MVC endpoints as individual modules pass Playwright validation suites.

2. Treat AI as a Co-Architect, Not an Unsupervised Decision Maker: While Claude Code excels at structural refactoring and rapid transcription, senior human software architects must maintain rigorous code review over architectural boundaries, security configurations, data access patterns, and authentication mechanisms.

3. Invest Early in Comprehensive Playwright Test Coverage: The speed and reliability of the `/goal` continuous remediation loop are directly bounded by the thoroughness of the Playwright test suite. Writing robust, edge-case-heavy Playwright scripts against the legacy application before initiating code transcription guarantees that subtle business logic is never lost during migration.

4. Enforce Strict Architectural Boundaries in CLAUDE.md: Use the `CLAUDE.md` repository configuration file to strictly enforce clean architecture principles, asynchronous standards,EF Core optimization guidelines, and secure coding practices. Clear directives prevent the AI from reproducing anti-patterns from legacy code.

9. Conclusion

Migrating legacy .NET Framework Web Sites to .NET 10 MVC represents a necessary evolution for enterprise software systems seeking cross-platform performance, modern security standards, and cloud-native scalability. Because direct automated conversion tools are incapable of bridging the fundamental architectural gap between event-driven Web Sites and stateless MVC architectures, modern enterprise engineering requires a complete architectural rewrite. By deploying Claude Code as an autonomous, agentic modernization engine, development teams can systematically analyze legacy code bases, decouple business logic, and transcribe legacy structures into idiomatic .NET 10 MVC components.

Furthermore, combining cross-browser automated testing via Playwright with Claude Code’s autonomous `/goal` continuous remediation loop creates a self-correcting development pipeline. This methodology eliminates functional drift, guarantees exact behavioral parity with legacy systems, and dramatically reduces manual debugging overhead. As software architectures continue to evolve, the integration of autonomous agentic AI tools with rigorous automated test suites represents the modern benchmark for rapid, secure, and precise enterprise software modernization.

Works Cited

Anthropic. "Claude Code: Developer Documentation and CLI Architecture." Anthropic Documentation, 2026, docs.anthropic.com/en/docs/agents-and-tools/claude-code. Accessed 27 Sept. 2026.

Fowler, Martin. "StranglerFigApplication." MartinFowler.com, 2024, martinfowler.com/bliki/StranglerFigApplication.html. Accessed 20 Sept. 2026.

Microsoft Documentation Team. "Overview of ASP.NET Core MVC and Modern Web Development." Microsoft Learn, Microsoft, 2026, learn.microsoft.com/en-us/aspnet/core/mvc/overview. Accessed 25 Sept. 2026.

Microsoft Documentation Team. "Migrating from ASP.NET to ASP.NET Core." Microsoft Learn, Microsoft, 2026, learn.microsoft.com/en-us/aspnet/core/migration/50-to-60. Accessed 22 Sept. 2026.

Microsoft Documentation Team. "Playwright for .NET and Node.js Documentation." Microsoft Learn, Microsoft, 2026, playwright.dev/docs/intro. Accessed 26 Sept. 2026.

Skeet, Jon. C# in Depth. 4th ed., Manning Publications, 2019.