The Strangler Fig pattern is named after a real-world biological process: strangler fig trees grow around the trunk of a host tree, gradually replacing it until the original tree is completely enclosed. In software architecture, the pattern applies the same philosophy: incrementally replace parts of a legacy system while keeping it running, rather than attempting a complete rewrite all at once.
Instead of:
- Big bang rewrite — Stop old system, rewrite everything, hope it all works
- Extended downtime — Teams lock down the old app, everyone migrates, weeks of risk
The Strangler Fig approach:
- Keep both running — Legacy Web Forms app continues in production
- Route requests — Some traffic goes to new Blazor components, some stays in Web Forms
- Migrate incrementally — Move one page, control, or feature at a time
- Zero downtime — Users see no outage; changes are invisible
- Rollback easily — If something breaks, switch traffic back instantly
BWFC provides three layers of support designed specifically for incremental, side-by-side migration.
Add the BWFC NuGet package to your .NET 10 Blazor target project:
dotnet add package Fritz.BlazorWebFormsComponentsThis unlocks Roslyn analyzers that run at compile time in your Blazor project:
| Analyzer | What It Detects | Example |
|---|---|---|
| BWFC022 | Page.ClientScript access patterns |
Page.ClientScript.RegisterStartupScript(...) |
| BWFC023 | ViewState read/write access | ViewState["key"] assignments |
| BWFC024 | Server control event handler patterns | evt_Click(sender, e) method signatures |
Key fact: These analyzers are purely syntactic — they match patterns in the Roslyn syntax tree (e.g., IsClientScriptAccess() in the analyzer source) without requiring System.Web type resolution. This means they work in any .NET project, whether or not System.Web is available.
The analyzers highlight every Web Forms pattern that needs attention, guiding you toward migration opportunities one diagnostic at a time.
Move files from the legacy Web Forms project to the Blazor project one at a time.
The CLI tool (webforms-to-blazor) handles mechanical L1 transforms:
@Pagedirectives →@page<asp:tags →<Button>,<GridView>, etc.Page_Load→OnInitialized()IsPostBackguards → removed or rewrittenweb.config→appsettings.jsonentries
After L1 transforms, your code compiles and runs.
The BWFC analyzers then light up the remaining patterns:
Page.ClientScript.RegisterStartupScript()→ use ClientScriptShim or IJSRuntimeViewState["key"]→ use field/property or ViewStateDictionary- Event handler signatures → add
EventCallback<>return types
No guessing — every diagnostic is actionable.
Here's what makes BWFC unique: you don't have to rewrite your code to make it work. BWFC provides runtime shims that accept the exact same API calls as Web Forms, but run on Blazor's modern foundation.
// Web Forms code-behind (no changes needed)
protected void Page_Load(object sender, EventArgs e)
{
Page.ClientScript.RegisterStartupScript(
GetType(), "init",
"console.log('Page loaded');", true);
}// Blazor code-behind (inject shim, change lifecycle only)
@inject ClientScriptShim ClientScript
@code {
protected override void OnInitialized()
{
ClientScript.RegisterStartupScript(
GetType(), "init",
"console.log('Page loaded');", true); // ← Identical call
}
}How it works:
RegisterStartupScript()queues the script in memory- During
OnAfterRenderAsync, scripts execute viaIJSRuntime.InvokeVoidAsync("eval", script) - Queue clears after each cycle
- Deduplication works exactly like Web Forms (by Type + key)
The same philosophy applies to other patterns:
| Shim | Web Forms | Blazor | Migration Path |
|---|---|---|---|
| ClientScriptShim | Page.ClientScript.RegisterStartupScript() |
Same call via injected shim | Zero-rewrite |
| SessionShim | Session["key"] = value |
Same call, modern storage | Zero-rewrite |
| CacheShim | Cache.Insert(key, obj) |
Same call via IMemoryCache | Zero-rewrite |
| ServerShim | Server.UrlEncode() |
Same call via utility | Zero-rewrite |
All shims follow the same pattern: same API surface, modern implementation underneath.
Once your code is running in Blazor, you can optionally refactor shim calls to native Blazor patterns — but there's no deadline.
// Phase 1: Use shim for speed (zero-rewrite)
ClientScript.RegisterStartupScript(GetType(), "init", "alert('hi');", true);
// Phase 2: Optional refactor to native Blazor
@inject IJSRuntime JS
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
await JS.InvokeVoidAsync("eval", "alert('hi');");
}Or better yet, migrate to JS modules for production code:
@inject IJSRuntime JS
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var module = await JS.InvokeAsync<IJSObjectReference>(
"import", "./Components/AlertModule.js");
await module.InvokeVoidAsync("showAlert");
}
}The key: Shims are production-ready from day one. You modernize when it makes sense, not because you have to.
Here's what the migration journey looks like:
graph TD
subgraph "Phase 1: Legacy App"
WF1["🟥 Web Forms App<br/>100% Web Forms<br/>All traffic here"]
end
WF1 -->|"Add BWFC NuGet +<br/>Run CLI transforms"| Router
subgraph "Phase 2: Strangling (Side-by-Side)"
Router["Router / Load Balancer"]
Router --> WF2["🟥 Web Forms<br/>(Legacy)<br/>Home, Reports, Search"]
Router --> BZ1["🟦 Blazor App<br/>1-2 pages migrated<br/>ClientScriptShim<br/>SessionShim"]
end
BZ1 -->|"Traffic gradually shifts"| BZ2
subgraph "Phase 3: Blazor Dominant"
BZ2["🟦 Blazor App (Primary)<br/>30+ pages migrated<br/>Shims handle compatibility<br/>Old patterns still work"]
BZ2 -.->|"Fallback for<br/>unmigrated pages"| WF3["🟥 Web Forms<br/>(Remaining)"]
end
BZ2 -->|"Optional modernization"| BZ3
subgraph "Phase 4: Modernized (Optional)"
BZ3["🟩 Blazor App (Native)<br/>All pages migrated<br/>IJSRuntime · ISession · IMemoryCache<br/>Full Blazor patterns"]
end
style WF1 fill:#ff6b6b,color:#fff
style WF2 fill:#ff6b6b,color:#fff
style WF3 fill:#ff9999,color:#333
style BZ1 fill:#4dabf7,color:#fff
style BZ2 fill:#339af0,color:#fff
style BZ3 fill:#51cf66,color:#fff
style Router fill:#ffd43b,color:#333
Users never experience service interruption. Traffic routes smoothly between systems. If a migrated page has a bug, you instantly route traffic back to the legacy app.
Teams don't have to wait for everything to be ready. Front-end developers can migrate UI pages while back-end developers work on business logic. QA tests pieces incrementally.
Unlike "big bang" rewrites, strangler fig migration is reversible at every step. If the Blazor app isn't ready, you have a working fallback.
Week 1–2: Migrate the Product Search page to Blazor
- No SearchBox component yet? Use plain HTML inputs with
@bind - JavaScript search filtering? Use ClientScriptShim to keep
Page.ClientScript.RegisterStartupScript()calls - Runs in parallel with legacy Search page; router directs traffic
Week 3: Migrate the Shopping Cart page
- SessionShim keeps
Session["CartItems"]working - FormView component for edit/display modes
- Shims handle the Web Forms session storage
Week 4–5: Migrate Order History and Account Settings
- GridView becomes GridView (BWFC component)
- Master page becomes Layout
- Event handlers stay the same
By Week 6:
- Core pages in Blazor, legacy becomes fallback
- Team can now optionally refactor shims to native patterns
- No deadline — shims are production-ready
| Strategy | Timeline | Risk | Downtime | Reversibility |
|---|---|---|---|---|
| Big Bang Rewrite | 6–12 months | Very High | Days/Weeks | None (all-in) |
| Strangler Fig (BWFC) | Incremental (weeks) | Low | None | Full (every step) |
| Parallel Development | 3–6 months | High | Moderate | Risky (integration required) |
- Migration Readiness Assessment — Evaluate your app's readiness for incremental migration
- Strategies — High-level planning for your specific scenario
- Automated Migration Guide — Use the CLI to kick off L1 transforms
- Analyzers — Understand what BWFC022, BWFC023, BWFC024 are telling you
- ClientScript Migration Guide — Deep dive on JS patterns with ClientScriptShim
- Phase 2 Session Shim — Keep session state working without rewrites
- Phase 2 Lifecycle Transforms — Page lifecycle event patterns
The Strangler Fig pattern is the philosophical foundation of BWFC:
- Don't rewrite everything. Incrementally replace parts while keeping the system running.
- Use analyzers to guide. BWFC's Roslyn analyzers show you exactly what needs attention.
- Use shims to bridge. ClientScriptShim, SessionShim, and others let migrated code work unchanged.
- Modernize on your schedule. Refactor to native Blazor patterns when it makes sense, not because you have to.
- Zero downtime, zero risk. Both systems run in parallel. Every step is reversible.
BWFC was purpose-built for this pattern. The analyzers, CLI, and shims all exist to make incremental, side-by-side migration possible.