Modernizing a legacy .NET Framework estate is one of the few engineering problems where the technical work is often the easy part and the organizational discipline is what actually determines success or failure. Interviewers give this topic to lead engineers specifically because it tests judgment under constraints that a greenfield project never has: no test coverage to lean on, a business that cannot tolerate downtime, and a codebase full of decisions nobody currently at the company remembers making. Expect questions about sequencing a migration so the business keeps running throughout, choosing the right target for a WCF or Web Forms dependency that has no drop-in replacement, and proving the effort was worth it in terms someone outside engineering will accept. This page works through the strangler fig pattern, incremental migration mechanics, and the risk-management discipline that separates a modernization program that ships from one that stalls for years.

Q1 Explain the strangler fig pattern and how you would apply it to a large .NET Framework monolith.#

Short answer: The strangler fig pattern routes traffic for a legacy system through a facade that can redirect individual capabilities to a new implementation one at a time, so the new system gradually takes over real production traffic while the old system keeps running underneath, until eventually nothing routes to the legacy code anymore and it can be decommissioned — named for the fig vine that grows around a host tree and eventually replaces it entirely.

Applying it to a .NET Framework monolith starts with placing a routing layer — often a reverse proxy such as YARP, or an API gateway — in front of the application, so URLs can be redirected on a per-route basis without changing how any existing caller reaches the system. You then pick the first capability to migrate based on two criteria that matter more than technical elegance: it should be relatively self-contained (low fan-out to the rest of the monolith), and valuable enough that proving the new stack works on it justifies the investment, but not so critical that a mistake is catastrophic. The new implementation is built on modern .NET, often sharing the legacy database initially (an explicitly temporary compromise) while ownership is untangled in parallel, and traffic cuts over once the new path is validated against real request patterns, not just staging. The pattern's real value for a lead engineer to articulate is that it keeps the system shippable and revenue-generating throughout a migration that might otherwise take years, because at every point there is a working, deployed system — never a long-lived branch or a big-bang cutover that must work perfectly on day one.

What interviewers look for: an understanding that the pattern is about incremental, reversible traffic migration behind a stable facade, not simply "rewrite it piece by piece," and the judgment to explain how you would choose the first capability to strangle.

Common mistakes: treating the strangler fig as just "microservices extraction" without the routing-facade mechanism that makes the migration incremental and safely reversible at each step.

Q2 What is your incremental migration strategy for moving a .NET Framework 4.8 application to modern .NET?#

Short answer: Start from the bottom of the dependency graph, not the top: port shared class libraries first by multi-targeting them against both net48 and the current .NET LTS, fix the compilation and behavioral differences that surface while both targets still build, then move up to the entry-point projects (web app, Windows service) one at a time once their dependencies already run clean on modern .NET.

Multi-targeting a library is the technique that makes this safe rather than a leap of faith: the same .csproj builds for both frameworks, giving immediate, continuous feedback on which APIs are missing or behave differently on modern .NET while the application keeps shipping on .NET Framework in production. Only once every shared library builds and passes its tests on both targets do you tackle an entry point, and that migration should be its own incremental project — porting a large ASP.NET MVC application usually means standing up a new ASP.NET Core host behind the strangler-fig routing facade, moving controllers over in batches, and running both hosts side by side rather than one atomic cutover. Tooling has shifted from manual, checklist-driven upgrades toward AI-assisted analysis: Microsoft's upgrade-agent project (successor to the earlier dotnet/upgrade-assistant and modernize-dotnet tooling) analyzes a codebase and proposes a concrete upgrade plan, though it accelerates the analysis, it does not remove the need for a human to sequence and verify the migration.

XML
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>net48;net10.0</TargetFrameworks>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup Condition="'$(TargetFramework)' == 'net48'">
    <PackageReference Include="System.Text.Json" Version="8.0.5" />
  </ItemGroup>
</Project>

What interviewers look for: a bottom-up sequencing strategy driven by the dependency graph, and awareness of multi-targeting as the specific mechanic that keeps the app shippable throughout, rather than a vague "we'd migrate it gradually."

Common mistakes: starting the migration with the most visible or most requested feature rather than the lowest, most-depended-upon layer, which produces a partially ported dependency graph that cannot compile cleanly on either framework.

Q3 How do you write characterization tests for legacy code that has no tests and no clear specification?#

Short answer: A characterization test captures what the code actually does today, correct or not, by calling it with representative inputs and asserting on the outputs it currently produces, giving you a safety net that detects any behavior change during refactoring — the goal is not to validate correctness, which nobody can specify without the original requirements, it is to make the current behavior visible and protected.

The practical starting point is instrumenting the method or class you are about to touch with the widest input coverage you can gather cheaply — production log samples, existing manual test scripts, or exercising every code path you can find by reading the implementation — and asserting on the actual observed output rather than what you assume it should be; if the legacy code has a bug, the test captures the bug too, because removing it becomes a deliberate, reviewed decision rather than an accidental side effect. This is the technique Michael Feathers popularized for legacy code specifically because it does not require understanding intent, only behavior — exactly the situation a lead engineer inherits when the original author left years ago and the only specification is the running system itself. Once characterization tests exist, refactoring becomes safe in the ordinary sense — small, verifiable steps, tests staying green — and a failing test after a change becomes a deliberate checkpoint: either the change was wrong, or it intentionally altered behavior and the test needs updating with sign-off, not silent adjustment.

C#
[Theory]
[InlineData(100.00, "CA", 8.25)]
[InlineData(50.00, "OR", 0.00)]
[InlineData(0.00, "NY", 0.00)]
public void CalculateTax_MatchesObservedLegacyBehavior(decimal amount, string state, decimal expectedTax)
{
    // Captures current output, not a validated spec — the legacy method predates this test.
    var actual = LegacyTaxCalculator.Calculate(amount, state);
    Assert.Equal(expectedTax, actual);
}

What interviewers look for: the distinction between characterizing and validating — that the test's purpose is regression protection during refactoring, not proof of correctness — and a concrete technique for gathering representative inputs without a specification to work from.

Follow-up questions:

  • What do you do when a characterization test captures behavior that is clearly a bug?
  • How do you characterize a method with hidden dependencies, like a static call to DateTime.Now or a database?

Q4 A critical WCF service needs to move off .NET Framework. What are your options, and how do you choose?#

Short answer: The three realistic paths are porting the service to CoreWCF (a community-maintained port of the WCF service side that runs on modern .NET and supports the common bindings), rewriting the service surface as gRPC for internal service-to-service calls where you control both ends, or rewriting it as a REST/JSON API when the consumers are external or heterogeneous — the choice depends primarily on how much control you have over the clients and how much of the existing contract you can afford to keep.

CoreWCF is the lowest-risk, lowest-effort option when clients cannot change: it targets enabling existing WCF services to move to modern .NET with minimal changes to the service code and supports the common bindings, including HTTP, NetTcp and NetNamedPipe, through separate packages, so a service using straightforward SOAP or basic bindings can often move with configuration changes rather than a rewrite. gRPC is the better target when you control both service and client and communication is internal, because it gives a modern, strongly typed, high-performance contract with first-class .NET support, but it is a genuine rewrite of the contract, not a port, so it only works when you can also update every caller. A REST rewrite makes sense when consumers are external or numerous, or you want to shed WCF's contract-first model for a broadly interoperable HTTP API — it is the most work up front, since both the contract and every integration change. In practice, a large legacy estate often uses all three: CoreWCF for rigid external clients, gRPC internally, REST at any edge facing third parties.

C#
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddServiceModelServices();
builder.Services.AddSingleton<IPricingService, PricingService>();

var app = builder.Build();
app.UseServiceModel(serviceBuilder =>
{
    serviceBuilder.AddService<PricingService>();
    serviceBuilder.AddServiceEndpoint<PricingService, IPricingService>(
        new BasicHttpBinding(), "/PricingService.svc");
});
app.Run();

What interviewers look for: naming CoreWCF specifically as the low-disruption path for clients you cannot change, and framing the decision around client control rather than treating "rewrite as REST" as the automatic default.

Common mistakes: assuming WCF has no supported path on modern .NET at all and defaulting straight to a full rewrite, which is often far more expensive than the constraint actually requires.

Q5 The business wants a Web Forms application modernized without a multi-year rewrite. What do you recommend?#

Short answer: Recommend an incremental, page-by-page migration to Blazor behind a routing facade rather than a full rewrite: Microsoft's own guidance for this migration is strategic rather than a mechanical converter, because Web Forms and Blazor have different execution and state models, so each page is effectively re-implemented, but pages can move one at a time while the rest of the application keeps running on Web Forms underneath.

There is no automated, drop-in conversion tool for Web Forms, because its server-side control tree and view-state model has no direct equivalent in Blazor's component model, so any migration is a genuine re-implementation of each page rather than a mechanical translation — worth stating plainly to a stakeholder expecting a quick converter, since setting that expectation early prevents the project from being scoped far shorter than it actually is. The incremental path treats each Web Forms page as its own strangler-fig target: place a routing layer in front of the site that can direct specific URLs to a new ASP.NET Core host running Blazor pages while everything else resolves to the existing application, then migrate pages in order of business value and simplicity, starting with pages least entangled with Session state and ViewState, since neither has a direct Blazor equivalent. Shared concerns — authentication, a common layout, cross-cutting validation — are worth extracting once, early, so every later page migration reuses that foundation. Framing the project as a sequence of independently shippable page migrations, rather than one large rewrite, is what makes it possible to show incremental business value instead of a multi-year ask with nothing to show until the end.

What interviewers look for: honesty that no automated Web Forms-to-Blazor converter exists, paired with a concrete incremental strategy (routing facade, page-by-page, shared concerns first) rather than either "just rewrite it" or an overpromised automated migration.

Q6 How do you manage risk during a modernization project that cannot afford downtime or a feature freeze?#

Short answer: Run the migration and feature delivery on separate, interleaved tracks rather than sequentially — never ask the business to accept a freeze — by keeping every migration step small enough to ship independently behind the routing facade, validating each step against production-like traffic before full cutover, and maintaining an explicit, tested rollback path for every single increment, not just the migration as a whole.

The biggest risk-management mistake is treating the migration as one large project with a single go-live date, because that concentrates all the risk into one event and forces a choice between a feature freeze the business usually cannot accept, or shipping migration and feature work on colliding branches, which produces merge and regression risk. The alternative is decomposing the migration into the smallest increments that can each be deployed and verified independently — one route, one page, one service at a time — so a problem discovered after any increment is cheap to roll back and does not block increments that already shipped. Canary or shadow traffic is the specific technique that catches problems before they reach every user: route a small percentage of real traffic, or a mirrored copy, to the new path first, compare behavior and error rate against the old path, and widen the rollout only once that holds under real load. Equally important is treating rollback as a first-class, tested capability, not a theoretical fallback — a facade that can only move traffic forward turns every cutover into a one-way door under pressure, exactly what a risk-managed migration is meant to avoid.

What interviewers look for: decomposition into small, independently reversible increments and canary-style validation, rather than a plan built around a single freeze-and-cutover event.

Common mistakes: agreeing to a feature freeze as the default way to "make room" for a migration, which usually erodes business sponsorship for the project the longer it drags on.

Q7 How do you decide what to rewrite, what to lift-and-shift, and what to leave alone in a legacy estate?#

Short answer: Score each application or component on two axes — how business-critical or differentiating it is, and how well it currently meets its quality attribute needs (stability, performance, security) — and let that placement drive the decision: rewrite what is both critical and currently failing, lift-and-shift what is critical but currently working fine, and leave alone what is low-value and low-risk, since modernizing it would spend effort the business will not see a return on.

This assessment has to be done at the portfolio level, not application by application in isolation, because the point is prioritizing a limited budget across many candidates, and a component worth rewriting on its own merits might still rank behind three others compared side by side on the same two axes. A component that is business-critical and actively causing incidents is the clearest rewrite candidate, because the cost of leaving it alone compounds every quarter. One that is critical but stable and rarely touched is usually a lift-and-shift candidate — move it onto a supported runtime to remove security and compliance risk without spending rewrite-level effort on something that is not currently a problem. Low-value, low-risk components — an internal tool used by three people, an untouched reporting job — are frequently best left alone entirely, or retired if a review shows nobody depends on them, which is common enough that a dependency and usage audit should be an explicit early step, not an afterthought.

What interviewers look for: a portfolio-level, two-axis triage framework rather than a component-by-component gut call, and willingness to recommend retiring or leaving something alone rather than defaulting to modernizing everything.

Follow-up questions:

  • How would you handle a component that scores as business-critical but nobody can explain what it actually does?
  • What evidence would justify retiring a component instead of migrating it?

Q8 What breaking changes and behavior differences most often catch teams off guard moving from .NET Framework to modern .NET?#

Short answer: The most disruptive ones are BinaryFormatter being fully removed starting in .NET 9 (it now throws PlatformNotSupportedException rather than just being discouraged), the absence of multiple AppDomains as an isolation mechanism, the move from web.config/XML configuration to the options pattern and JSON configuration, and subtler runtime behavior differences around culture-sensitive string comparisons and reflection-based serialization that do not fail to compile but silently change behavior at runtime.

BinaryFormatter deserves calling out specifically because it is not merely deprecated: starting with .NET 9 the runtime no longer includes an implementation at all, so any code path that still depends on it — a surprisingly common hidden dependency in older caching or session-persistence code — throws immediately, and the supported path forward is a safer serializer such as System.Text.Json, DataContractSerializer or MessagePack, chosen before the migration reaches production rather than discovered as a runtime exception during it. The loss of multiple AppDomains matters for any app that used them for plugin isolation, since modern .NET supports only one, and the replacements are AssemblyLoadContext for unloadable plugins or, for stronger isolation, separate processes — neither is a drop-in replacement. Configuration is a near-universal rewrite: web.config's appSettings and custom sections become appsettings.json bound through the options pattern, touching every place the old app read configuration. The silent-behavior category is the most dangerous, since it does not throw or fail to compile: default string comparison changed with globalization-invariant mode and ICU-based culture data, so legacy culture-comparison quirks can produce different sort orders without a single compiler warning — exactly why characterization tests matter as much for the migration as for ordinary refactoring.

C#
// Pre-.NET 9 this silently worked; on .NET 9+ it throws PlatformNotSupportedException
// the instant this code path runs, so it must be found and replaced before cutover.
var formatter = new BinaryFormatter();
using var stream = File.OpenRead(cachePath);
var cached = (SessionState)formatter.Deserialize(stream); // now throws — replace with:
var cached = JsonSerializer.Deserialize<SessionState>(File.ReadAllBytes(cachePath));

What interviewers look for: BinaryFormatter's removal named as a hard failure rather than a soft deprecation, and awareness of the silent-behavior-change category specifically, since that is the class of bug that characterization tests and careful QA are meant to catch before a cutover, not after.

Q9 How do you measure whether a modernization effort is actually succeeding, beyond "we finished migrating X services"?#

Short answer: Track delivery and reliability metrics before and after each increment — deployment frequency, lead time for a change to reach production, change failure rate, and mean time to restore, the four metrics popularized by DevOps Research and Assessment — alongside a small set of business-facing signals like incident volume and infrastructure cost for the migrated component, so "success" is defined as measurable improvement in how the system behaves, not merely as a completed migration checklist.

Migration-completion percentage is a status metric, not a success metric, because a team can finish moving every line of code and still leave the organization no better off if lead time for a change is unchanged, or the new system is just as fragile — modernizing was rarely "run on a newer runtime" in isolation, it was to fix a specific pain, so the measurement plan has to trace back to that pain. Capturing a baseline before touching anything is what makes this credible later: deployment frequency and lead time for the legacy component, its incident count over the prior two or three quarters, and its infrastructure cost, measured the same way after each increment ships, turns "modernization" into a comparison a skeptical stakeholder can audit themselves. It is also worth tracking a leading indicator during the migration: the ratio of time spent on new feature work versus migration work on the affected component, since a healthy migration should show that ratio recovering as each piece stabilizes, while a stalled one shows the team permanently absorbed with no daylight. Reporting these numbers regularly, even when not yet where you want them, protects a program's funding far better than status updates that only ever say "on track."

What interviewers look for: naming concrete, before-and-after delivery and reliability metrics rather than a completion percentage, and tying those metrics back to the original business pain the modernization was meant to fix.

Q10 Describe how you would sequence a modernization program end to end for a large legacy estate with many interdependent applications.#

Short answer: Start with a dependency and risk inventory across the whole portfolio before migrating anything, use that inventory to build a dependency-ordered sequence — migrating the applications and shared services other things depend on before the things that depend on them — and run the program as a series of independently valuable increments with their own success criteria, rather than one multi-year initiative with a single finish line.

The inventory phase is the step most programs under-invest in, and it is where a lead engineer's judgment matters most: cataloging every application, its owning team, its dependencies, current stability and compliance exposure, and a rough size estimate gives you the two-axis triage from the earlier question applied at scale, plus a dependency graph that shows the actual required order — a shared authentication library or core data service has to move before the applications depending on it can meaningfully modernize, regardless of how appealing a downstream application looks as a starting point. Sequencing then balances that technical ordering against a second, equally real constraint: early wins that build organizational confidence and funding for the rest of the program. The practical resolution is usually migrating one full, moderately complex, dependency-light application completely first, end to end, to prove out the tooling and the team's new working rhythm on something real, before tackling the deeply depended-upon components that unlock everything else. Throughout, each phase should ship independently useful value and be measurable on its own terms, using the before-and-after metrics from the previous question, so the program never depends on a distant final milestone to demonstrate it is working.

What interviewers look for: a two-phase structure — portfolio-wide dependency and risk inventory, then a sequence balancing technical dependency order against early-win confidence-building — rather than either a purely technical ordering or a purely political one.

Quick-Fire Round#

QuestionAnswer
What does the strangler fig pattern route traffic through to enable incremental migration?A facade or routing layer (reverse proxy or API gateway) in front of the legacy system.
What technique lets a shared library be validated on both .NET Framework and modern .NET at once?Multi-targeting the project (for example net48;net10.0).
What does a characterization test verify?The code's current actual behavior, not whether that behavior is correct.
What is the lowest-disruption path for a WCF service whose clients cannot change?Porting it to CoreWCF, which runs the WCF service side on modern .NET.
Since which .NET version does BinaryFormatter throw instead of running?.NET 9, where the implementation was removed from the runtime.
What replaces multiple AppDomains for plugin isolation on modern .NET?AssemblyLoadContext, or separate processes for stronger isolation.
Is there an automated converter from Web Forms to Blazor?No — each page is effectively re-implemented, migrated incrementally.
What validation technique catches a migration problem before it reaches all users?Canary or shadow traffic comparison against the old path.
What four metrics commonly measure delivery and reliability before/after a migration?Deployment frequency, lead time for changes, change failure rate, time to restore.
What should a migration program sequence around, beyond technical dependency order?Early, independently shippable wins that build organizational confidence.

How to Prepare#

  • Be able to describe the strangler fig pattern end to end, including how you would pick the first capability to migrate and how you would decommission the old path.
  • Practice explaining multi-targeting as the specific mechanic that keeps a bottom-up migration verifiable at every step, not just "we ported it gradually."
  • Have one clear explanation of characterization tests and how they differ from ordinary unit tests written against a known specification.
  • Know CoreWCF by name as the low-disruption path for WCF clients you cannot change, and be ready to contrast it with a gRPC or REST rewrite.
  • Rehearse the two-axis triage framework (business criticality versus current quality) for deciding what to rewrite, lift-and-shift, or leave alone.
  • Prepare a generic, numbers-free story about measuring a modernization effort's success with before-and-after delivery metrics, not a completion percentage.