diff --git a/.squad/decisions/inbox/cyclops-shim-first.md b/.squad/decisions/inbox/cyclops-shim-first.md new file mode 100644 index 000000000..3ff677752 --- /dev/null +++ b/.squad/decisions/inbox/cyclops-shim-first.md @@ -0,0 +1,30 @@ +# Decision: CLI Transforms Adopt Shim-First Strategy + +**Date:** 2026-07-30 +**Author:** Cyclops +**Status:** Implemented + +## Context + +WebFormsPageBase already provides protected shim properties (Request, Response, Server, Session, Cache, ClientScript) that migrated pages inherit. CLI transforms were redundantly injecting `[Inject]` properties or rewriting calls that already compile against these shims. + +## Decision + +Refactored 5 CLI transforms to be shim-first: + +1. **ResponseRedirectTransform** — No longer rewrites `Response.Redirect()` → `NavigationManager.NavigateTo()`. Calls compile against ResponseShim which handles ~/prefix and .aspx stripping. Only strips `Page.`/`this.` prefix. No `[Inject] NavigationManager`. +2. **SessionDetectTransform** — Removed `[Inject] SessionShim Session` and `[Inject] CacheShim Cache` injection. WebFormsPageBase already provides these. +3. **ClientScriptTransform** — Guidance now says "works via WebFormsPageBase — no injection needed" instead of suggesting `@inject ClientScriptShim`. +4. **RequestFormTransform** — Guidance leads with "Request.Form calls work automatically via RequestShim on WebFormsPageBase." +5. **ServerShimTransform** — Guidance leads with "Server.* calls work automatically via ServerShim on WebFormsPageBase." + +## Rationale + +- Eliminates redundant `[Inject]` properties that shadow base class members +- `Response.Redirect()` calls now preserved as-is — ResponseShim handles URL normalization +- Guidance comments accurately reflect the runtime architecture +- Non-page classes still directed to inject shims via DI + +## Build Verification + +0 errors across net8.0/net9.0/net10.0. diff --git a/.squad/decisions/inbox/forge-shim-review.md b/.squad/decisions/inbox/forge-shim-review.md new file mode 100644 index 000000000..2f30a141e --- /dev/null +++ b/.squad/decisions/inbox/forge-shim-review.md @@ -0,0 +1,28 @@ +# Shim-First Transform Review — Forge's Verdict + +**Date:** 2026-07-30 +**By:** Forge (Lead / Web Forms Reviewer) +**Status:** Advisory +**Requested by:** Jeffrey T. Fritz + +## Decision + +The shim-first refactoring of 5 CLI transforms (ResponseRedirect, SessionDetect, ClientScript, RequestForm, ServerShim) is a **NET IMPROVEMENT** for page-class migrations. Recommend merging with two follow-up items tracked. + +## Rationale + +1. **Eliminating unnecessary [Inject] lines** — WebFormsPageBase already provides Session, Cache, Response, Request, Server, ClientScript as protected properties. The old transforms injected redundant `[Inject] SessionShim` / `[Inject] CacheShim` that would conflict or shadow. +2. **Preserving familiar API surface** — Developers see `Response.Redirect()` and `Session["key"]` in migrated code, matching their mental model and the original source. +3. **All 5 transforms correctly warn about non-page classes** — each emits "For non-page classes, inject via DI" guidance. + +## Gaps Identified (for follow-up) + +1. **Server.Transfer / Server.GetLastError / Server.ClearError** — ServerShim only covers MapPath + Encode/Decode. WingtipToys uses all three missing methods. ServerShimTransform should detect and emit specific TODO guidance for these. +2. **IdentityHelper.RedirectToReturnUrl(string, HttpResponse)** — Takes `System.Web.HttpResponse`, not `ResponseShim`. Type mismatch requires manual rewrite regardless of transform approach. Neither old nor new transforms handle this. +3. **HttpContext.Current.Session in non-page classes** — ShoppingCartActions uses `HttpContext.Current.Session[key]` (5 occurrences). SessionDetectTransform won't match this pattern because it only looks for bare `Session[`. + +## Impact + +- **Bishop**: Consider adding `Server.Transfer` / `Server.GetLastError` / `Server.ClearError` detection to ServerShimTransform with TODO guidance. +- **Bishop**: Consider adding `HttpContext.Current.Session[` pattern detection to SessionDetectTransform. +- **All agents**: When reviewing migrated non-page classes, verify shim DI is manually wired. diff --git a/.squad/decisions/inbox/forge-toolkit-improvements.md b/.squad/decisions/inbox/forge-toolkit-improvements.md new file mode 100644 index 000000000..c62f942f1 --- /dev/null +++ b/.squad/decisions/inbox/forge-toolkit-improvements.md @@ -0,0 +1,283 @@ +# Shim-First Toolkit Improvements — Forge's Proposal + +**Date:** 2026-07-30 +**By:** Forge (Lead / Web Forms Reviewer) +**Status:** Proposed +**Requested by:** Jeffrey T. Fritz +**Builds on:** forge-shim-review.md (same date) + +--- + +## Executive Summary + +The shim-first approach is proven (~95% compile-as-is for page classes), but three detection gaps and one silent risk leave non-trivial migration failures at compile time and runtime. This proposal adds **4 CLI transform enhancements**, **2 skill documentation patches**, and **1 new non-page class detection strategy** — all surgical changes that extend the existing architecture without introducing new base classes or breaking changes. + +--- + +## 1. Proposed Changes — CLI Transforms + +### 1.1 ServerShimTransform: Detect Transfer/GetLastError/ClearError + +**File:** `src/BlazorWebFormsComponents.Cli/Transforms/CodeBehind/ServerShimTransform.cs` +**Gap:** Server.Transfer(), Server.GetLastError(), Server.ClearError() are undetected; WingtipToys ErrorPage.aspx.cs, Default.aspx.cs, Global.asax.cs won't compile +**Priority:** P0 (must-have — compile failure) +**Complexity:** Small + +**What to change:** + +Add three new regex patterns after line 28: + +```csharp +// Server.Transfer("page.aspx") — no Blazor equivalent; must become NavigationManager.NavigateTo +private static readonly Regex ServerTransferRegex = new( + @"\bServer\.Transfer\s*\(", + RegexOptions.Compiled); + +// Server.GetLastError() — no direct equivalent; use IExceptionHandlerFeature or middleware +private static readonly Regex ServerGetLastErrorRegex = new( + @"\bServer\.GetLastError\s*\(\s*\)", + RegexOptions.Compiled); + +// Server.ClearError() — no direct equivalent; errors flow through middleware pipeline +private static readonly Regex ServerClearErrorRegex = new( + @"\bServer\.ClearError\s*\(\s*\)", + RegexOptions.Compiled); +``` + +Update `Apply()` to detect these and emit specific TODO guidance: + +``` +// TODO(bwfc-server): Server.Transfer() has NO shim — replace with Response.Redirect() or NavigationManager.NavigateTo(). +// Server.Transfer preserves URL; for same effect use forceLoad:true on NavigateTo. +// TODO(bwfc-server): Server.GetLastError() has NO shim — use IExceptionHandlerFeature in error-handling middleware. +// TODO(bwfc-server): Server.ClearError() has NO shim — error handling uses middleware pipeline, remove this call. +``` + +**Key detail:** These are NOT shimmable. The guidance must clearly say "manual rewrite required" — not "works via shim." + +--- + +### 1.2 SessionDetectTransform: Detect HttpContext.Current.Session + +**File:** `src/BlazorWebFormsComponents.Cli/Transforms/CodeBehind/SessionDetectTransform.cs` +**Gap:** `HttpContext.Current.Session["key"]` pattern (5 occurrences in ShoppingCartActions.cs) not caught by existing `\bSession\s*\[` regex +**Priority:** P0 (must-have — compile failure) +**Complexity:** Small + +**What to change:** + +Add new regex after line 22: + +```csharp +// HttpContext.Current.Session["key"] — common in non-page helper classes +private static readonly Regex HttpContextSessionRegex = new( + @"\bHttpContext\.Current\.Session\s*\[", + RegexOptions.Compiled); +``` + +Update `Apply()`: +1. Check `HttpContextSessionRegex.IsMatch(content)` alongside existing `hasSession` +2. When matched, **replace** `HttpContext.Current.Session[` → `Session[` (safe textual transform) +3. Emit specific TODO: `// TODO(bwfc-session-state): HttpContext.Current.Session replaced with Session. This class needs [Inject] SessionShim or constructor injection — see non-page class guidance below.` + +Also detect `HttpContext.Current.` broadly for awareness: + +```csharp +private static readonly Regex HttpContextCurrentRegex = new( + @"\bHttpContext\.Current\b", + RegexOptions.Compiled); +``` + +--- + +### 1.3 ResponseRedirectTransform: Detect ThreadAbortException dead code + +**File:** `src/BlazorWebFormsComponents.Cli/Transforms/CodeBehind/ResponseRedirectTransform.cs` +**Gap:** `Response.Redirect(url, true)` silently ignores `endResponse` param; `catch (ThreadAbortException)` blocks become unreachable dead code +**Priority:** P1 (should-have — runtime risk, not compile failure) +**Complexity:** Small + +**What to change:** + +Add regex after line 28: + +```csharp +// catch (ThreadAbortException) — dead code in Blazor; Web Forms threw this on Redirect(url, true) +private static readonly Regex ThreadAbortCatchRegex = new( + @"catch\s*\(\s*ThreadAbortException\b", + RegexOptions.Compiled); + +// Response.Redirect(url, true) — endResponse parameter silently ignored +private static readonly Regex RedirectEndResponseRegex = new( + @"Response\.Redirect\s*\([^,]+,\s*true\s*\)", + RegexOptions.Compiled); +``` + +Emit warnings: +``` +// TODO(bwfc-navigation): Response.Redirect(url, true) — the endResponse parameter is silently ignored. +// In Web Forms, true caused ThreadAbortException to end page processing. +// In Blazor, code after Redirect() always continues. Review control flow. +// TODO(bwfc-navigation): catch (ThreadAbortException) is dead code — Blazor never throws this. Remove the catch block. +``` + +--- + +### 1.4 New: NonPageClassDetectTransform + +**File:** `src/BlazorWebFormsComponents.Cli/Transforms/CodeBehind/NonPageClassDetectTransform.cs` (NEW) +**Gap:** Classes like ShoppingCartActions.cs that use Session/Response/Server but don't inherit WebFormsPageBase get shim guidance that says "works via WebFormsPageBase" — which is wrong for them +**Priority:** P1 (should-have) +**Complexity:** Medium + +**What to change:** + +Create a new transform (Order: 50, runs early) that detects whether a class inherits from a page/component base: + +```csharp +// Detect if class inherits WebFormsPageBase, ComponentBase, LayoutComponentBase, etc. +private static readonly Regex PageBaseClassRegex = new( + @"class\s+\w+\s*:\s*(?:.*?)(?:WebFormsPageBase|ComponentBase|LayoutComponentBase|Page)\b", + RegexOptions.Compiled); +``` + +When a file uses shim-relevant APIs (`Session[`, `Response.Redirect`, `Server.MapPath`, `Cache[`) but does NOT inherit a page base class, inject a **different** guidance block: + +``` +// --- Non-Page Class: Manual DI Required --- +// TODO(bwfc-non-page): This class uses Web Forms APIs but does NOT inherit WebFormsPageBase. +// Shims are NOT automatically available. You must inject them: +// +// Option A: Constructor injection (recommended for services) +// public class ShoppingCartActions +// { +// private readonly SessionShim _session; +// public ShoppingCartActions(SessionShim session) => _session = session; +// } +// +// Option B: [Inject] attribute (for Blazor components only) +// [Inject] SessionShim Session { get; set; } +// +// Register in Program.cs: builder.Services.AddBlazorWebFormsComponents(); +``` + +Store the `IsPageClass` result in `FileMetadata` (add a bool property) so downstream transforms can adjust their guidance between "works via WebFormsPageBase" vs. "needs manual DI." + +--- + +## 2. Proposed Changes — Migration Skills + +### 2.1 bwfc-migration SKILL.md: Add gap patterns + +**File:** `migration-toolkit/skills/bwfc-migration/SKILL.md` +**Priority:** P1 +**Complexity:** Small + +Add a new section after the shim table (after line 57): + +```markdown +### ⚠️ Server Methods NOT Covered by ServerShim + +These `Server.*` methods have NO shim equivalent and require manual rewrite: + +| Pattern | Replacement | +|---------|------------| +| `Server.Transfer("page.aspx")` | `Response.Redirect("/page")` or `NavigationManager.NavigateTo("/page", forceLoad: true)` | +| `Server.GetLastError()` | Error-handling middleware with `IExceptionHandlerFeature` | +| `Server.ClearError()` | Remove — error handling is middleware-based | +| `Server.Execute("page.aspx")` | Not applicable in Blazor — decompose into shared components | + +### ⚠️ Non-Page Classes Need Manual DI Wiring + +Classes that are not Blazor components (services, helpers, utilities) do NOT inherit WebFormsPageBase. They cannot use `Session["key"]` or `Response.Redirect()` directly. + +**Pattern to detect:** Any `.cs` file that uses `HttpContext.Current.Session`, `HttpContext.Current.Response`, or `HttpContext.Current.Server` — these must be refactored to accept shims via constructor injection. + +**Example (ShoppingCartActions.cs):** +```csharp +// Before: HttpContext.Current.Session["CartId"] +// After: +public class ShoppingCartActions +{ + private readonly SessionShim _session; + public ShoppingCartActions(SessionShim session) => _session = session; + + public string GetCartId() => _session["CartId"]?.ToString(); +} +``` +``` + +### 2.2 bwfc-data-migration SKILL.md: Add ThreadAbortException warning + +**File:** `migration-toolkit/skills/bwfc-data-migration/SKILL.md` +**Priority:** P2 +**Complexity:** Small + +In the "Static Helpers with HttpContext" section (line 492), add: + +```markdown +### ThreadAbortException Is Dead Code + +In Web Forms, `Response.Redirect(url, true)` threw `ThreadAbortException` to halt page processing. Any `catch (ThreadAbortException)` blocks are dead code in Blazor — remove them. Review control flow after `Response.Redirect()` calls, as code after the redirect WILL execute in Blazor (it doesn't halt like Web Forms). +``` + +--- + +## 3. Proposed Changes — Copilot Instructions Template + +### 3.1 copilot-instructions-template.md: Add gap warnings + +**File:** `migration-toolkit/copilot-instructions-template.md` +**Priority:** P1 +**Complexity:** Small + +Add to the "Common Gotchas" section (after item 10, line 216): + +```markdown +11. **Server.Transfer has no shim** — Replace with `Response.Redirect()` or `NavigationManager.NavigateTo()`. Server.Transfer preserved the URL; use `forceLoad: true` for similar behavior. +12. **Non-page classes need DI** — Service/helper classes that used `HttpContext.Current.Session` cannot use shims directly. Inject `SessionShim`, `ResponseShim`, etc. via constructor. +13. **ThreadAbortException is dead code** — `catch (ThreadAbortException)` blocks after `Response.Redirect(url, true)` never execute in Blazor. Remove them and review control flow. +14. **IdentityHelper.RedirectToReturnUrl** — This helper takes `System.Web.HttpResponse`, not `ResponseShim`. Rewrite to accept `NavigationManager` or `ResponseShim` parameter. +``` + +--- + +## 4. What NOT to Do + +1. **❌ Do NOT create a new base class for non-page classes** (e.g., `WebFormsServiceBase`). Constructor DI is the correct pattern for services. A base class would create artificial coupling and fight against ASP.NET Core's DI model. + +2. **❌ Do NOT add Server.Transfer to ServerShim as a method.** Transfer implies server-side URL rewriting without browser redirect — a fundamentally incompatible concept in Blazor's component model. A `Transfer()` method on ServerShim would give false confidence. + +3. **❌ Do NOT auto-remove ThreadAbortException catch blocks.** The CLI should emit TODO guidance only — auto-removal could delete meaningful error-handling logic that the developer wrapped inside the catch block. + +4. **❌ Do NOT auto-inject `[Inject] SessionShim` into non-page classes.** The CLI cannot determine the correct DI pattern (constructor vs. property injection, scoping, lifetime). Emit guidance and let the developer or L2 Copilot decide. + +5. **❌ Do NOT add `HttpContext.Current` as a shim.** `HttpContext.Current` is a static accessor pattern that fundamentally doesn't work in async/Blazor contexts. The correct fix is to replace it with DI, not to shim it. + +--- + +## 5. Implementation Order + +| Order | Item | Priority | Depends On | Est. Effort | +|-------|------|----------|------------|-------------| +| 1 | SessionDetectTransform: `HttpContext.Current.Session` regex | P0 | None | 1 hour | +| 2 | ServerShimTransform: Transfer/GetLastError/ClearError detection | P0 | None | 1 hour | +| 3 | ResponseRedirectTransform: ThreadAbortException detection | P1 | None | 1 hour | +| 4 | NonPageClassDetectTransform: new transform + FileMetadata.IsPageClass | P1 | #1-3 for full benefit | 3 hours | +| 5 | bwfc-migration SKILL.md: gap patterns section | P1 | None | 30 min | +| 6 | copilot-instructions-template.md: gotcha items | P1 | None | 15 min | +| 7 | bwfc-data-migration SKILL.md: ThreadAbort warning | P2 | None | 15 min | + +Items 1-3 are independent and can be done in parallel. Item 4 benefits from 1-3 being done first (so the downstream transforms can check `IsPageClass`). Items 5-7 are documentation-only and can ship independently. + +--- + +## 6. Verification Plan + +- **Unit tests:** Add test cases to existing CLI transform test suite for each new regex pattern: + - `Server.Transfer("ErrorPage.aspx")` → detected, TODO emitted + - `HttpContext.Current.Session["CartId"]` → detected, rewritten, TODO emitted + - `catch (ThreadAbortException)` → detected, TODO emitted + - Non-page class with `Session[` → different guidance than page class +- **Integration test:** Run CLI against WingtipToys source, verify all 3 gaps produce TODO comments +- **Regression:** Existing 373 tests must continue passing diff --git a/migration-toolkit/copilot-instructions-template.md b/migration-toolkit/copilot-instructions-template.md index 64490391a..fc8a0b834 100644 --- a/migration-toolkit/copilot-instructions-template.md +++ b/migration-toolkit/copilot-instructions-template.md @@ -214,6 +214,10 @@ When Copilot encounters these attributes during migration, remove them without c 8. **Visibility** — `Visible="false"` works in BWFC, but prefer `@if (condition)` for dynamic visibility. 9. **ClientScript migration** — `Page.ClientScript.RegisterStartupScript()` works via `ClientScriptShim`. For new code, prefer `IJSRuntime`. 10. **Form POST data** — Use `` to enable `Request.Form["key"]` in interactive mode. +11. **Server.Transfer has NO shim** — There is no BWFC shim for `Server.Transfer()`. Use `NavigationManager.NavigateTo()` instead. Server.Transfer does server-side URL rewriting which doesn't exist in Blazor. +12. **Server.GetLastError/ClearError have NO shim** — Use `ILogger` and middleware-based error handling (`app.UseExceptionHandler`). +13. **ThreadAbortException is dead code** — Web Forms throws `ThreadAbortException` on `Response.Redirect(url, true)`. Blazor does not. Any `catch (ThreadAbortException)` blocks are dead code after migration — review and remove. +14. **HttpContext.Current doesn't work in Blazor** — Static `HttpContext.Current.Session["key"]` must be replaced with `Session["key"]` (on pages) or constructor-injected `SessionShim` (in non-page classes). The CLI tool handles this replacement automatically. --- diff --git a/migration-toolkit/skills/bwfc-data-migration/SKILL.md b/migration-toolkit/skills/bwfc-data-migration/SKILL.md index 9f984ad00..593f2b5bb 100644 --- a/migration-toolkit/skills/bwfc-data-migration/SKILL.md +++ b/migration-toolkit/skills/bwfc-data-migration/SKILL.md @@ -492,6 +492,9 @@ Web Forms `SelectMethod` returns `IQueryable` synchronously. Blazor services sho ### Static Helpers with HttpContext Web Forms often has static helper classes that access `HttpContext.Current`. These must be refactored to accept dependencies via constructor injection. +### ThreadAbortException Dead Code Warning +Web Forms throws `ThreadAbortException` when `Response.Redirect(url, true)` is called with `endResponse=true`. Blazor does **not** throw this exception — `ResponseShim.Redirect()` silently ignores the `endResponse` parameter. Any `catch (ThreadAbortException)` blocks become dead code after migration. Review and remove them. Code that runs AFTER `Response.Redirect(url, true)` **will execute** in Blazor (unlike Web Forms where execution stopped). + --- ## ❌ Common Anti-Patterns to Avoid diff --git a/migration-toolkit/skills/bwfc-migration/SKILL.md b/migration-toolkit/skills/bwfc-migration/SKILL.md index 296176677..7bf214377 100644 --- a/migration-toolkit/skills/bwfc-migration/SKILL.md +++ b/migration-toolkit/skills/bwfc-migration/SKILL.md @@ -56,6 +56,35 @@ The CLI tool emits structured `// TODO(bwfc-*)` comments and a JSON migration re | `ClientScript.RegisterStartupScript()` | ClientScriptShim | ✅ Yes | Injects JavaScript via JSRuntime | | `ViewState["key"]` | WebFormsPageBase | ✅ Yes | In-memory dictionary per component instance | +### ⚠️ Server Methods WITHOUT Shims + +These Server.* methods have **no BWFC shim** and require manual rewriting: + +| Web Forms Pattern | Shim? | Migration Action | +|-------------------|-------|-----------------| +| `Server.Transfer("page.aspx")` | ❌ None | Replace with `NavigationManager.NavigateTo()`. Server.Transfer does server-side URL rewriting which doesn't exist in Blazor. | +| `Server.GetLastError()` | ❌ None | Use `ILogger` and middleware-based error handling (`app.UseExceptionHandler`). | +| `Server.ClearError()` | ❌ None | Error clearing is handled by middleware in ASP.NET Core. | +| `HttpContext.Current.Session["key"]` | ❌ None | Replace with `Session["key"]` (on pages) or inject `SessionShim` via constructor DI (non-page classes). The CLI tool handles this automatically. | + +### ⚠️ Non-Page Classes + +Classes that use `Session["key"]`, `Response.Redirect()`, etc. but do **NOT** inherit from `WebFormsPageBase` must receive shims via **constructor DI**, not the base class: + +```csharp +// Non-page class — inject shims via DI +public class CartHelper +{ + private readonly SessionShim _session; + public CartHelper(SessionShim session) => _session = session; + public string GetCartId() => _session["CartId"]?.ToString(); +} +``` + +### ⚠️ ThreadAbortException Dead Code + +Web Forms throws `ThreadAbortException` when `Response.Redirect(url, true)` is called with `endResponse=true`. Blazor does **not** throw this exception. Any `catch (ThreadAbortException)` blocks become **dead code** after migration — review and remove them. + ### Key Benefits of Shims 1. **Minimal Code Changes** — Original Web Forms code works with ZERO changes in most cases diff --git a/src/BlazorWebFormsComponents.Cli/Transforms/CodeBehind/ResponseRedirectTransform.cs b/src/BlazorWebFormsComponents.Cli/Transforms/CodeBehind/ResponseRedirectTransform.cs index f5bc31997..87e97e347 100644 --- a/src/BlazorWebFormsComponents.Cli/Transforms/CodeBehind/ResponseRedirectTransform.cs +++ b/src/BlazorWebFormsComponents.Cli/Transforms/CodeBehind/ResponseRedirectTransform.cs @@ -42,7 +42,19 @@ public class ResponseRedirectTransform : ICodeBehindTransform @"((?:public|internal|private)\s+(?:partial\s+)?class\s+\w+[^{]*\{)", RegexOptions.Compiled); + // catch (ThreadAbortException) — dead code after migration (Blazor doesn't throw) + private static readonly Regex ThreadAbortCatchRegex = new( + @"catch\s*\(\s*ThreadAbortException\b", + RegexOptions.Compiled); + + // Response.Redirect(url, true) — endResponse=true silently ignored by ResponseShim + private static readonly Regex RedirectEndResponseTrueRegex = new( + @"Response\.Redirect\s*\([^,)]+,\s*true\s*\)", + RegexOptions.Compiled); + private const string GuidanceMarker = "// --- Response.Redirect Migration ---"; + private const string ThreadAbortMarker = "// TODO(bwfc-navigation): DEAD CODE"; + private const string EndResponseMarker = "// TODO(bwfc-navigation): endResponse=true"; public string Apply(string content, FileMetadata metadata) { @@ -54,6 +66,9 @@ public string Apply(string content, FileMetadata metadata) if (!hasRedirect) return content; + // Pre-detect endResponse=true BEFORE any other processing + var hasEndResponseTrue = RedirectEndResponseTrueRegex.IsMatch(content); + // Strip Page.Response.Redirect → Response.Redirect and this.Response.Redirect → Response.Redirect if (PageOrThisPrefixRegex.IsMatch(content)) { @@ -70,6 +85,25 @@ public string Apply(string content, FileMetadata metadata) content = ClassOpenRegex.Replace(content, "$1" + guidanceBlock, 1); } + // Detect ThreadAbortException catch blocks — dead code after migration + if (ThreadAbortCatchRegex.IsMatch(content) && !content.Contains(ThreadAbortMarker)) + { + content = ThreadAbortCatchRegex.Replace(content, m => + m.Value + $"\n {ThreadAbortMarker} — Blazor does not throw ThreadAbortException on redirect. " + + "This catch block is dead code after migration. Review and remove if safe."); + } + + // Warn about Response.Redirect(url, true) endResponse behavior + if (hasEndResponseTrue && !content.Contains(EndResponseMarker)) + { + if (ClassOpenRegex.IsMatch(content)) + { + var warning = $"\n {EndResponseMarker} is silently ignored by ResponseShim. " + + "Code after redirect calls WILL execute (unlike Web Forms where it threw ThreadAbortException).\n"; + content = ClassOpenRegex.Replace(content, "$1" + warning, 1); + } + } + return content; } } diff --git a/src/BlazorWebFormsComponents.Cli/Transforms/CodeBehind/ServerShimTransform.cs b/src/BlazorWebFormsComponents.Cli/Transforms/CodeBehind/ServerShimTransform.cs index 5dbd38396..359de1a12 100644 --- a/src/BlazorWebFormsComponents.Cli/Transforms/CodeBehind/ServerShimTransform.cs +++ b/src/BlazorWebFormsComponents.Cli/Transforms/CodeBehind/ServerShimTransform.cs @@ -29,6 +29,21 @@ public class ServerShimTransform : ICodeBehindTransform @"\bHttpServerUtility\b", RegexOptions.Compiled); + // Server.Transfer("page.aspx") — no shim, architecturally incompatible with Blazor + private static readonly Regex ServerTransferRegex = new( + @"\bServer\.Transfer\s*\(", + RegexOptions.Compiled); + + // Server.GetLastError() — no shim, use middleware-based error handling + private static readonly Regex ServerGetLastErrorRegex = new( + @"\bServer\.GetLastError\s*\(", + RegexOptions.Compiled); + + // Server.ClearError() — no shim, use middleware-based error handling + private static readonly Regex ServerClearErrorRegex = new( + @"\bServer\.ClearError\s*\(", + RegexOptions.Compiled); + private static readonly Regex ClassOpenRegex = new( @"((?:public|internal|private)\s+(?:partial\s+)?class\s+\w+[^{]*\{)", RegexOptions.Compiled); @@ -40,8 +55,11 @@ public string Apply(string content, FileMetadata metadata) var hasMapPath = ServerMapPathRegex.IsMatch(content); var hasEncode = ServerEncodeRegex.IsMatch(content); var hasUtility = HttpServerUtilityRegex.IsMatch(content); + var hasTransfer = ServerTransferRegex.IsMatch(content); + var hasGetLastError = ServerGetLastErrorRegex.IsMatch(content); + var hasClearError = ServerClearErrorRegex.IsMatch(content); - if (!hasMapPath && !hasEncode && !hasUtility) return content; + if (!hasMapPath && !hasEncode && !hasUtility && !hasTransfer && !hasGetLastError && !hasClearError) return content; // Build guidance if (!content.Contains(GuidanceMarker) && ClassOpenRegex.IsMatch(content)) @@ -59,13 +77,35 @@ public string Apply(string content, FileMetadata metadata) var guidanceBlock = "\n " + GuidanceMarker + "\n" + " // TODO(bwfc-server): Server.* calls work automatically via ServerShim on WebFormsPageBase.\n" - + $" // Methods found: {string.Join(", ", methods)}\n" + + (methods.Count > 0 ? $" // Methods found: {string.Join(", ", methods)}\n" : "") + " // For non-page classes, inject ServerShim via DI.\n" + (hasMapPath ? " // MapPath(\"~/path\") maps to IWebHostEnvironment.WebRootPath.\n" : ""); content = ClassOpenRegex.Replace(content, "$1" + guidanceBlock, 1); } + // Emit manual-rewrite TODOs for unsupported Server methods + if (hasTransfer && !content.Contains("TODO(bwfc-server): Server.Transfer")) + { + content = ServerTransferRegex.Replace(content, m => + "Server.Transfer( /* TODO(bwfc-server): Server.Transfer has NO SHIM — architecturally incompatible with Blazor. " + + "Use NavigationManager.NavigateTo() instead. Server.Transfer does server-side URL rewriting which doesn't exist in Blazor. */ "); + } + + if (hasGetLastError && !content.Contains("TODO(bwfc-server): Server.GetLastError")) + { + content = ServerGetLastErrorRegex.Replace(content, m => + "Server.GetLastError( /* TODO(bwfc-server): Server.GetLastError has NO SHIM. " + + "Use ILogger and middleware-based error handling (app.UseExceptionHandler). */ "); + } + + if (hasClearError && !content.Contains("TODO(bwfc-server): Server.ClearError")) + { + content = ServerClearErrorRegex.Replace(content, m => + "Server.ClearError( /* TODO(bwfc-server): Server.ClearError has NO SHIM. " + + "Error clearing is handled by middleware in ASP.NET Core. */ "); + } + return content; } } diff --git a/src/BlazorWebFormsComponents.Cli/Transforms/CodeBehind/SessionDetectTransform.cs b/src/BlazorWebFormsComponents.Cli/Transforms/CodeBehind/SessionDetectTransform.cs index 786eece4f..805866843 100644 --- a/src/BlazorWebFormsComponents.Cli/Transforms/CodeBehind/SessionDetectTransform.cs +++ b/src/BlazorWebFormsComponents.Cli/Transforms/CodeBehind/SessionDetectTransform.cs @@ -22,6 +22,11 @@ public class SessionDetectTransform : ICodeBehindTransform @"\bSession\s*\[", RegexOptions.Compiled); + // HttpContext.Current.Session[ — static accessor doesn't work in Blazor + private static readonly Regex HttpContextCurrentSessionRegex = new( + @"HttpContext\.Current\.Session\s*\[", + RegexOptions.Compiled); + // Literal string key: Cache["ProductList"] private static readonly Regex CacheKeyRegex = new( @"\bCache\[""([^""]*)""\]", @@ -42,6 +47,23 @@ public class SessionDetectTransform : ICodeBehindTransform public string Apply(string content, FileMetadata metadata) { + // Replace HttpContext.Current.Session[ with Session[ BEFORE detection + if (HttpContextCurrentSessionRegex.IsMatch(content)) + { + content = HttpContextCurrentSessionRegex.Replace(content, "Session["); + // Add guidance about static accessor removal + if (!content.Contains("TODO(bwfc-session-state): HttpContext.Current.Session")) + { + var classMatch = ClassOpenRegex.Match(content); + if (classMatch.Success) + { + var httpContextNote = "\n // TODO(bwfc-session-state): HttpContext.Current.Session was replaced with Session[].\n" + + " // For non-page classes, inject SessionShim via constructor DI instead of using HttpContext.Current.\n"; + content = ClassOpenRegex.Replace(content, "$1" + httpContextNote, 1); + } + } + } + // Detect patterns before any modifications var sessionLiteralMatches = SessionKeyRegex.Matches(content); var hasSession = SessionAccessRegex.IsMatch(content); diff --git a/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC13-ResponseRedirect.razor.cs b/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC13-ResponseRedirect.razor.cs index 1793d562b..8798646e1 100644 --- a/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC13-ResponseRedirect.razor.cs +++ b/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC13-ResponseRedirect.razor.cs @@ -24,15 +24,17 @@ namespace MyApp { public partial class TC13_ResponseRedirect { - // TODO(bwfc-general): ClientScript calls preserved — uses ClientScriptShim + ScriptManagerShim. Inject @inject ClientScriptShim ClientScript and @inject ScriptManagerShim ScriptManager if not using BaseWebFormsComponent. + // TODO(bwfc-general): ClientScript calls preserved — works via WebFormsPageBase (no injection needed). ScriptManagerShim may need @inject ScriptManagerShim ScriptManager for non-page classes. // --- Request.Form Migration --- - // TODO(bwfc-form): Request.Form calls work via FormShim on WebFormsPageBase. + // TODO(bwfc-form): Request.Form calls work automatically via RequestShim on WebFormsPageBase. // For interactive mode, wrap your form in . // Form keys found: key // For non-page classes, inject RequestShim via DI. - [Inject] private NavigationManager NavigationManager { get; set; } // TODO(bwfc-navigation): Add @using Microsoft.AspNetCore.Components to _Imports.razor if needed + // --- Response.Redirect Migration --- + // TODO(bwfc-navigation): Response.Redirect() works via ResponseShim on WebFormsPageBase. Handles ~/ and .aspx automatically. + // For non-page classes, inject ResponseShim via DI. // --- ConfigurationManager Migration --- // TODO(bwfc-config): ConfigurationManager calls work via BWFC shim. @@ -43,9 +45,9 @@ protected override async Task OnInitializedAsync() // TODO(bwfc-lifecycle): Review lifecycle conversion — verify async behavior await base.OnInitializedAsync(); - NavigationManager.NavigateTo("/Products.aspx"); - NavigationManager.NavigateTo("/Cart.aspx"); - NavigationManager.NavigateTo(GetUrl() /* TODO(bwfc-navigation): Verify navigation target */); + Response.Redirect("/Products"); + Response.Redirect("/Cart.aspx", false); + Response.Redirect(GetUrl()); } } } diff --git a/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC14-SessionDetect.razor.cs b/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC14-SessionDetect.razor.cs index d813382a0..5a5828f7a 100644 --- a/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC14-SessionDetect.razor.cs +++ b/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC14-SessionDetect.razor.cs @@ -20,7 +20,7 @@ // ============================================================================= // --- Session State Migration --- -// TODO(bwfc-session-state): SessionShim auto-wired via [Inject] — Session["key"] calls compile against the shim's indexer. +// TODO(bwfc-session-state): Session["key"] calls work automatically via SessionShim on WebFormsPageBase. // Session keys found: CartId, UserName // Options for long-term replacement: // (1) ProtectedSessionStorage (Blazor Server) — persists across circuits @@ -34,12 +34,10 @@ namespace MyApp { public partial class TC14_SessionDetect { - // TODO(bwfc-general): ClientScript calls preserved — uses ClientScriptShim + ScriptManagerShim. Inject @inject ClientScriptShim ClientScript and @inject ScriptManagerShim ScriptManager if not using BaseWebFormsComponent. - - [Inject] private SessionShim Session { get; set; } + // TODO(bwfc-general): ClientScript calls preserved — works via WebFormsPageBase (no injection needed). ScriptManagerShim may need @inject ScriptManagerShim ScriptManager for non-page classes. // --- Request.Form Migration --- - // TODO(bwfc-form): Request.Form calls work via FormShim on WebFormsPageBase. + // TODO(bwfc-form): Request.Form calls work automatically via RequestShim on WebFormsPageBase. // For interactive mode, wrap your form in . // Form keys found: key // For non-page classes, inject RequestShim via DI. diff --git a/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC15-ViewState.razor.cs b/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC15-ViewState.razor.cs index 23f128fed..160c8b27d 100644 --- a/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC15-ViewState.razor.cs +++ b/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC15-ViewState.razor.cs @@ -32,10 +32,10 @@ namespace MyApp { public partial class TC15_ViewState { - // TODO(bwfc-general): ClientScript calls preserved — uses ClientScriptShim + ScriptManagerShim. Inject @inject ClientScriptShim ClientScript and @inject ScriptManagerShim ScriptManager if not using BaseWebFormsComponent. + // TODO(bwfc-general): ClientScript calls preserved — works via WebFormsPageBase (no injection needed). ScriptManagerShim may need @inject ScriptManagerShim ScriptManager for non-page classes. // --- Request.Form Migration --- - // TODO(bwfc-form): Request.Form calls work via FormShim on WebFormsPageBase. + // TODO(bwfc-form): Request.Form calls work automatically via RequestShim on WebFormsPageBase. // For interactive mode, wrap your form in . // Form keys found: key // For non-page classes, inject RequestShim via DI. diff --git a/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC16-IsPostBackGuard.razor.cs b/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC16-IsPostBackGuard.razor.cs index 241b623fa..24becba8d 100644 --- a/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC16-IsPostBackGuard.razor.cs +++ b/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC16-IsPostBackGuard.razor.cs @@ -24,10 +24,10 @@ namespace MyApp { public partial class TC16_IsPostBackGuard { - // TODO(bwfc-general): ClientScript calls preserved — uses ClientScriptShim + ScriptManagerShim. Inject @inject ClientScriptShim ClientScript and @inject ScriptManagerShim ScriptManager if not using BaseWebFormsComponent. + // TODO(bwfc-general): ClientScript calls preserved — works via WebFormsPageBase (no injection needed). ScriptManagerShim may need @inject ScriptManagerShim ScriptManager for non-page classes. // --- Request.Form Migration --- - // TODO(bwfc-form): Request.Form calls work via FormShim on WebFormsPageBase. + // TODO(bwfc-form): Request.Form calls work automatically via RequestShim on WebFormsPageBase. // For interactive mode, wrap your form in . // Form keys found: key // For non-page classes, inject RequestShim via DI. diff --git a/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC18-UrlCleanup.razor.cs b/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC18-UrlCleanup.razor.cs index ff909015b..6b4da9afe 100644 --- a/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC18-UrlCleanup.razor.cs +++ b/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC18-UrlCleanup.razor.cs @@ -24,15 +24,17 @@ namespace MyApp { public partial class TC18_UrlCleanup { - // TODO(bwfc-general): ClientScript calls preserved — uses ClientScriptShim + ScriptManagerShim. Inject @inject ClientScriptShim ClientScript and @inject ScriptManagerShim ScriptManager if not using BaseWebFormsComponent. + // TODO(bwfc-general): ClientScript calls preserved — works via WebFormsPageBase (no injection needed). ScriptManagerShim may need @inject ScriptManagerShim ScriptManager for non-page classes. // --- Request.Form Migration --- - // TODO(bwfc-form): Request.Form calls work via FormShim on WebFormsPageBase. + // TODO(bwfc-form): Request.Form calls work automatically via RequestShim on WebFormsPageBase. // For interactive mode, wrap your form in . // Form keys found: key // For non-page classes, inject RequestShim via DI. - [Inject] private NavigationManager NavigationManager { get; set; } // TODO(bwfc-navigation): Add @using Microsoft.AspNetCore.Components to _Imports.razor if needed + // --- Response.Redirect Migration --- + // TODO(bwfc-navigation): Response.Redirect() works via ResponseShim on WebFormsPageBase. Handles ~/ and .aspx automatically. + // For non-page classes, inject ResponseShim via DI. // --- ConfigurationManager Migration --- // TODO(bwfc-config): ConfigurationManager calls work via BWFC shim. @@ -43,15 +45,15 @@ protected override async Task OnInitializedAsync() // TODO(bwfc-lifecycle): Review lifecycle conversion — verify async behavior await base.OnInitializedAsync(); - NavigationManager.NavigateTo("/Products.aspx"); - NavigationManager.NavigateTo("/Admin/Dashboard.aspx?id=1"); + Response.Redirect("/Products"); + Response.Redirect("/Admin/Dashboard?id=1"); string url = "~/Help.aspx#section"; var path = "~/Images/logo.png"; } protected void Button_Click() { - NavigationManager.NavigateTo("/Checkout.aspx"); + Response.Redirect("/Checkout", false); } } } diff --git a/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC19-PageLifecycle.razor.cs b/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC19-PageLifecycle.razor.cs index a88aed4b8..d957a8a30 100644 --- a/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC19-PageLifecycle.razor.cs +++ b/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC19-PageLifecycle.razor.cs @@ -24,10 +24,10 @@ namespace MyApp { public partial class TC19_PageLifecycle { - // TODO(bwfc-general): ClientScript calls preserved — uses ClientScriptShim + ScriptManagerShim. Inject @inject ClientScriptShim ClientScript and @inject ScriptManagerShim ScriptManager if not using BaseWebFormsComponent. + // TODO(bwfc-general): ClientScript calls preserved — works via WebFormsPageBase (no injection needed). ScriptManagerShim may need @inject ScriptManagerShim ScriptManager for non-page classes. // --- Request.Form Migration --- - // TODO(bwfc-form): Request.Form calls work via FormShim on WebFormsPageBase. + // TODO(bwfc-form): Request.Form calls work automatically via RequestShim on WebFormsPageBase. // For interactive mode, wrap your form in . // Form keys found: key // For non-page classes, inject RequestShim via DI. diff --git a/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC20-EventHandlerStandard.razor.cs b/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC20-EventHandlerStandard.razor.cs index 6638208b6..ddd2cd28d 100644 --- a/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC20-EventHandlerStandard.razor.cs +++ b/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC20-EventHandlerStandard.razor.cs @@ -24,10 +24,10 @@ namespace MyApp { public partial class TC20_EventHandlerStandard { - // TODO(bwfc-general): ClientScript calls preserved — uses ClientScriptShim + ScriptManagerShim. Inject @inject ClientScriptShim ClientScript and @inject ScriptManagerShim ScriptManager if not using BaseWebFormsComponent. + // TODO(bwfc-general): ClientScript calls preserved — works via WebFormsPageBase (no injection needed). ScriptManagerShim may need @inject ScriptManagerShim ScriptManager for non-page classes. // --- Request.Form Migration --- - // TODO(bwfc-form): Request.Form calls work via FormShim on WebFormsPageBase. + // TODO(bwfc-form): Request.Form calls work automatically via RequestShim on WebFormsPageBase. // For interactive mode, wrap your form in . // Form keys found: key // For non-page classes, inject RequestShim via DI. diff --git a/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC21-EventHandlerSpecialized.razor.cs b/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC21-EventHandlerSpecialized.razor.cs index 2bbe22677..9f9a1f594 100644 --- a/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC21-EventHandlerSpecialized.razor.cs +++ b/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC21-EventHandlerSpecialized.razor.cs @@ -23,10 +23,10 @@ namespace MyApp { public partial class TC21_EventHandlerSpecialized { - // TODO(bwfc-general): ClientScript calls preserved — uses ClientScriptShim + ScriptManagerShim. Inject @inject ClientScriptShim ClientScript and @inject ScriptManagerShim ScriptManager if not using BaseWebFormsComponent. + // TODO(bwfc-general): ClientScript calls preserved — works via WebFormsPageBase (no injection needed). ScriptManagerShim may need @inject ScriptManagerShim ScriptManager for non-page classes. // --- Request.Form Migration --- - // TODO(bwfc-form): Request.Form calls work via FormShim on WebFormsPageBase. + // TODO(bwfc-form): Request.Form calls work automatically via RequestShim on WebFormsPageBase. // For interactive mode, wrap your form in . // Form keys found: key // For non-page classes, inject RequestShim via DI. diff --git a/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC26-GetRouteUrl.razor.cs b/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC26-GetRouteUrl.razor.cs index ae0120ed5..064704012 100644 --- a/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC26-GetRouteUrl.razor.cs +++ b/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC26-GetRouteUrl.razor.cs @@ -24,10 +24,10 @@ namespace MyApp { public partial class TC26_GetRouteUrl { - // TODO(bwfc-general): ClientScript calls preserved — uses ClientScriptShim + ScriptManagerShim. Inject @inject ClientScriptShim ClientScript and @inject ScriptManagerShim ScriptManager if not using BaseWebFormsComponent. + // TODO(bwfc-general): ClientScript calls preserved — works via WebFormsPageBase (no injection needed). ScriptManagerShim may need @inject ScriptManagerShim ScriptManager for non-page classes. // --- Request.Form Migration --- - // TODO(bwfc-form): Request.Form calls work via FormShim on WebFormsPageBase. + // TODO(bwfc-form): Request.Form calls work automatically via RequestShim on WebFormsPageBase. // For interactive mode, wrap your form in . // Form keys found: key // For non-page classes, inject RequestShim via DI. diff --git a/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC27-IsPostBackElse.razor.cs b/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC27-IsPostBackElse.razor.cs index 3b97cefbf..c54df110d 100644 --- a/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC27-IsPostBackElse.razor.cs +++ b/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC27-IsPostBackElse.razor.cs @@ -24,10 +24,10 @@ namespace MyApp { public partial class TC27_IsPostBackElse { - // TODO(bwfc-general): ClientScript calls preserved — uses ClientScriptShim + ScriptManagerShim. Inject @inject ClientScriptShim ClientScript and @inject ScriptManagerShim ScriptManager if not using BaseWebFormsComponent. + // TODO(bwfc-general): ClientScript calls preserved — works via WebFormsPageBase (no injection needed). ScriptManagerShim may need @inject ScriptManagerShim ScriptManager for non-page classes. // --- Request.Form Migration --- - // TODO(bwfc-form): Request.Form calls work via FormShim on WebFormsPageBase. + // TODO(bwfc-form): Request.Form calls work automatically via RequestShim on WebFormsPageBase. // For interactive mode, wrap your form in . // Form keys found: key // For non-page classes, inject RequestShim via DI. diff --git a/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC29-Combined.razor.cs b/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC29-Combined.razor.cs index 7435e91d5..8f229d2e5 100644 --- a/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC29-Combined.razor.cs +++ b/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC29-Combined.razor.cs @@ -20,7 +20,7 @@ // ============================================================================= // --- Session State Migration --- -// TODO(bwfc-session-state): SessionShim auto-wired via [Inject] — Session["key"] calls compile against the shim's indexer. +// TODO(bwfc-session-state): Session["key"] calls work automatically via SessionShim on WebFormsPageBase. // Session keys found: LastVisit // Options for long-term replacement: // (1) ProtectedSessionStorage (Blazor Server) — persists across circuits @@ -33,17 +33,17 @@ namespace MyApp { public partial class TC29_Combined { - // TODO(bwfc-general): ClientScript calls preserved — uses ClientScriptShim + ScriptManagerShim. Inject @inject ClientScriptShim ClientScript and @inject ScriptManagerShim ScriptManager if not using BaseWebFormsComponent. - - [Inject] private SessionShim Session { get; set; } + // TODO(bwfc-general): ClientScript calls preserved — works via WebFormsPageBase (no injection needed). ScriptManagerShim may need @inject ScriptManagerShim ScriptManager for non-page classes. // --- Request.Form Migration --- - // TODO(bwfc-form): Request.Form calls work via FormShim on WebFormsPageBase. + // TODO(bwfc-form): Request.Form calls work automatically via RequestShim on WebFormsPageBase. // For interactive mode, wrap your form in . // Form keys found: key // For non-page classes, inject RequestShim via DI. - [Inject] private NavigationManager NavigationManager { get; set; } // TODO(bwfc-navigation): Add @using Microsoft.AspNetCore.Components to _Imports.razor if needed + // --- Response.Redirect Migration --- + // TODO(bwfc-navigation): Response.Redirect() works via ResponseShim on WebFormsPageBase. Handles ~/ and .aspx automatically. + // For non-page classes, inject ResponseShim via DI. // --- ConfigurationManager Migration --- // TODO(bwfc-config): ConfigurationManager calls work via BWFC shim. @@ -62,7 +62,7 @@ protected override async Task OnInitializedAsync() protected void SaveButton_Click() { // Save logic - NavigationManager.NavigateTo("/Confirmation.aspx"); + Response.Redirect("/Confirmation"); } private void BindGrid() diff --git a/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC31-ComplexCodeBehind.razor.cs b/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC31-ComplexCodeBehind.razor.cs index 45d89a953..d62590b99 100644 --- a/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC31-ComplexCodeBehind.razor.cs +++ b/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC31-ComplexCodeBehind.razor.cs @@ -28,7 +28,7 @@ // Note: BaseWebFormsComponent.ViewState exists as an [Obsolete] compatibility shim. // --- Session State Migration --- -// TODO(bwfc-session-state): SessionShim auto-wired via [Inject] — Session["key"] calls compile against the shim's indexer. +// TODO(bwfc-session-state): Session["key"] calls work automatically via SessionShim on WebFormsPageBase. // Session keys found: LastVisitDate // Options for long-term replacement: // (1) ProtectedSessionStorage (Blazor Server) — persists across circuits @@ -42,17 +42,17 @@ namespace ContosoAdmin { public partial class Dashboard { - // TODO(bwfc-general): ClientScript calls preserved — uses ClientScriptShim + ScriptManagerShim. Inject @inject ClientScriptShim ClientScript and @inject ScriptManagerShim ScriptManager if not using BaseWebFormsComponent. - - [Inject] private SessionShim Session { get; set; } + // TODO(bwfc-general): ClientScript calls preserved — works via WebFormsPageBase (no injection needed). ScriptManagerShim may need @inject ScriptManagerShim ScriptManager for non-page classes. // --- Request.Form Migration --- - // TODO(bwfc-form): Request.Form calls work via FormShim on WebFormsPageBase. + // TODO(bwfc-form): Request.Form calls work automatically via RequestShim on WebFormsPageBase. // For interactive mode, wrap your form in . // Form keys found: key // For non-page classes, inject RequestShim via DI. - [Inject] private NavigationManager NavigationManager { get; set; } // TODO(bwfc-navigation): Add @using Microsoft.AspNetCore.Components to _Imports.razor if needed + // --- Response.Redirect Migration --- + // TODO(bwfc-navigation): Response.Redirect() works via ResponseShim on WebFormsPageBase. Handles ~/ and .aspx automatically. + // For non-page classes, inject ResponseShim via DI. // --- ConfigurationManager Migration --- // TODO(bwfc-config): ConfigurationManager calls work via BWFC shim. @@ -88,19 +88,19 @@ protected override async Task OnAfterRenderAsync(bool firstRender) protected void Export_Click() { var reportPath = GetRouteUrl( /* TODO(bwfc-route-url): converted from Page route lookup – ensure page inherits WebFormsPageBase */ "ReportRoute", new { format = "csv" }); - NavigationManager.NavigateTo(reportPath) /* TODO(bwfc-navigation): Verify navigation target */; + Response.Redirect(reportPath); } protected void Refresh_Click() { - NavigationManager.NavigateTo("/Admin/Dashboard.aspx"); + Response.Redirect("/Admin/Dashboard"); } protected void Users_RowCommand(GridViewCommandEventArgs e) { if (e.CommandName == "Select") { - NavigationManager.NavigateTo("/Admin/UserDetail?id=" + e.CommandArgument) /* TODO(bwfc-navigation): Verify navigation target */; + Response.Redirect("/Admin/UserDetail?id=" + e.CommandArgument); } } diff --git a/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC33-ClientScript.razor.cs b/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC33-ClientScript.razor.cs index 1aedb093f..cf689ad8b 100644 --- a/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC33-ClientScript.razor.cs +++ b/tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC33-ClientScript.razor.cs @@ -24,10 +24,10 @@ namespace MyApp { public partial class TC33_ClientScript { - // TODO(bwfc-general): ClientScript calls preserved — uses ClientScriptShim + ScriptManagerShim. Inject @inject ClientScriptShim ClientScript and @inject ScriptManagerShim ScriptManager if not using BaseWebFormsComponent. + // TODO(bwfc-general): ClientScript calls preserved — works via WebFormsPageBase (no injection needed). ScriptManagerShim may need @inject ScriptManagerShim ScriptManager for non-page classes. // --- Request.Form Migration --- - // TODO(bwfc-form): Request.Form calls work via FormShim on WebFormsPageBase. + // TODO(bwfc-form): Request.Form calls work automatically via RequestShim on WebFormsPageBase. // For interactive mode, wrap your form in . // Form keys found: key // For non-page classes, inject RequestShim via DI. diff --git a/tests/BlazorWebFormsComponents.Cli.Tests/TransformUnit/ClientScriptTransformTests.cs b/tests/BlazorWebFormsComponents.Cli.Tests/TransformUnit/ClientScriptTransformTests.cs index cb3ac1375..0dfabb48f 100644 --- a/tests/BlazorWebFormsComponents.Cli.Tests/TransformUnit/ClientScriptTransformTests.cs +++ b/tests/BlazorWebFormsComponents.Cli.Tests/TransformUnit/ClientScriptTransformTests.cs @@ -60,7 +60,7 @@ void Page_Load() var result = _transform.Apply(input, TestMetadata(input)); Assert.Contains("TODO(bwfc-general)", result); - Assert.Contains("ClientScriptShim", result); + Assert.Contains("WebFormsPageBase", result); } [Fact] @@ -79,7 +79,7 @@ void Page_Load() var result = _transform.Apply(input, TestMetadata(input)); Assert.DoesNotContain("[Inject] private IJSRuntime JS", result); - Assert.Contains("ClientScriptShim", result); + Assert.Contains("WebFormsPageBase", result); } [Fact] @@ -99,7 +99,7 @@ void Page_Load() Assert.Contains("ClientScript.RegisterStartupScript", result); Assert.DoesNotContain("JS.InvokeVoidAsync", result); - Assert.Contains("ClientScriptShim", result); + Assert.Contains("WebFormsPageBase", result); } [Fact] @@ -165,7 +165,7 @@ void Page_Load() Assert.Contains("ClientScript.RegisterClientScriptInclude", result); Assert.DoesNotContain("Page.ClientScript", result); - Assert.Contains("ClientScriptShim", result); + Assert.Contains("WebFormsPageBase", result); } [Fact] @@ -205,7 +205,7 @@ void Page_Load() Assert.Contains("ClientScript.RegisterClientScriptBlock", result); Assert.DoesNotContain("Page.ClientScript", result); - Assert.Contains("ClientScriptShim", result); + Assert.Contains("WebFormsPageBase", result); } [Fact] @@ -288,7 +288,7 @@ void DoWork() var result = _transform.Apply(input, TestMetadata(input)); Assert.Contains("ClientScript.GetPostBackEventReference(btnSubmit", result); - Assert.Contains("ClientScriptShim", result); + Assert.Contains("WebFormsPageBase", result); } [Fact] @@ -366,7 +366,7 @@ void Page_Load() var result = _transform.Apply(input, TestMetadata(input)); Assert.Contains("ScriptManagerShim", result); - Assert.Contains("ClientScriptShim", result); + Assert.Contains("WebFormsPageBase", result); } [Fact] @@ -429,8 +429,8 @@ void Page_Load() // ScriptManager.GetCurrent → Page replaced with this Assert.Contains("ScriptManager.GetCurrent(this)", result); Assert.DoesNotContain("ScriptManager.GetCurrent(Page)", result); - // ClientScriptShim comment injected (not IJSRuntime) - Assert.Contains("ClientScriptShim", result); + // ClientScript preserved comment injected (not IJSRuntime) + Assert.Contains("ClientScript calls preserved", result); Assert.DoesNotContain("[Inject] private IJSRuntime JS", result); } @@ -464,7 +464,7 @@ void DoWork() }"; var result = _transform.Apply(input, TestMetadata(input)); - Assert.Contains("ClientScriptShim", result); + Assert.Contains("ClientScript calls preserved", result); } [Fact] diff --git a/tests/BlazorWebFormsComponents.Cli.Tests/TransformUnit/RequestFormTransformTests.cs b/tests/BlazorWebFormsComponents.Cli.Tests/TransformUnit/RequestFormTransformTests.cs index 84437064a..f95e6ef6e 100644 --- a/tests/BlazorWebFormsComponents.Cli.Tests/TransformUnit/RequestFormTransformTests.cs +++ b/tests/BlazorWebFormsComponents.Cli.Tests/TransformUnit/RequestFormTransformTests.cs @@ -35,7 +35,7 @@ void Process() var result = _transform.Apply(input, TestMetadata(input)); Assert.Contains("TODO(bwfc-form)", result); - Assert.Contains("FormShim", result); + Assert.Contains("RequestShim", result); Assert.Contains("WebFormsForm", result); Assert.Contains("txtName", result); } diff --git a/tests/BlazorWebFormsComponents.Cli.Tests/TransformUnit/ResponseRedirectTransformTests.cs b/tests/BlazorWebFormsComponents.Cli.Tests/TransformUnit/ResponseRedirectTransformTests.cs new file mode 100644 index 000000000..8423d13b5 --- /dev/null +++ b/tests/BlazorWebFormsComponents.Cli.Tests/TransformUnit/ResponseRedirectTransformTests.cs @@ -0,0 +1,135 @@ +using BlazorWebFormsComponents.Cli.Pipeline; +using BlazorWebFormsComponents.Cli.Transforms.CodeBehind; + +namespace BlazorWebFormsComponents.Cli.Tests.TransformUnit; + +/// +/// Unit tests for ResponseRedirectTransform — ThreadAbortException dead code +/// detection and endResponse=true warning. +/// +public class ResponseRedirectTransformTests +{ + private readonly ResponseRedirectTransform _transform = new(); + + private static FileMetadata TestMetadata(string content) => new() + { + SourceFilePath = "Default.aspx.cs", + OutputFilePath = "Default.razor.cs", + FileType = FileType.Page, + OriginalContent = content + }; + + [Fact] + public void DetectsThreadAbortCatch_EmitsDeadCodeWarning() + { + var input = @"namespace MyApp +{ + public partial class MyPage + { + void Process() + { + try + { + Response.Redirect(""~/Login.aspx""); + } + catch (ThreadAbortException) + { + // Web Forms pattern — absorb redirect exception + } + } + } +}"; + var result = _transform.Apply(input, TestMetadata(input)); + + Assert.Contains("TODO(bwfc-navigation): DEAD CODE", result); + Assert.Contains("dead code after migration", result); + } + + [Fact] + public void DetectsEndResponseTrue_EmitsWarning() + { + var input = @"namespace MyApp +{ + public partial class MyPage + { + void Process() + { + Response.Redirect(""~/Login.aspx"", true); + // This code would NOT execute in Web Forms + DoSomething(); + } + } +}"; + var result = _transform.Apply(input, TestMetadata(input)); + + Assert.Contains("TODO(bwfc-navigation): endResponse=true", result); + Assert.Contains("silently ignored by ResponseShim", result); + } + + [Fact] + public void NoRedirect_NoChanges() + { + var input = @"namespace MyApp +{ + public partial class MyPage + { + void Process() { var x = 42; } + } +}"; + var result = _transform.Apply(input, TestMetadata(input)); + + Assert.Equal(input, result); + } + + [Fact] + public void ThreadAbortCatch_Idempotent() + { + var input = @"namespace MyApp +{ + public partial class MyPage + { + void Process() + { + try + { + Response.Redirect(""~/Login.aspx""); + } + catch (ThreadAbortException) + { + } + } + } +}"; + var result = _transform.Apply(input, TestMetadata(input)); + result = _transform.Apply(result, TestMetadata(result)); + + var count = result.Split("DEAD CODE").Length - 1; + Assert.Equal(1, count); + } + + [Fact] + public void EndResponseTrue_Idempotent() + { + var input = @"namespace MyApp +{ + public partial class MyPage + { + void Process() + { + Response.Redirect(""~/Login.aspx"", true); + } + } +}"; + var result = _transform.Apply(input, TestMetadata(input)); + result = _transform.Apply(result, TestMetadata(result)); + + var count = result.Split("endResponse=true").Length - 1; + Assert.Equal(1, count); + } + + [Fact] + public void OrderIs300() + { + Assert.Equal(300, _transform.Order); + } +} diff --git a/tests/BlazorWebFormsComponents.Cli.Tests/TransformUnit/ServerShimTransformTests.cs b/tests/BlazorWebFormsComponents.Cli.Tests/TransformUnit/ServerShimTransformTests.cs index 15ee6bcf6..a57501cba 100644 --- a/tests/BlazorWebFormsComponents.Cli.Tests/TransformUnit/ServerShimTransformTests.cs +++ b/tests/BlazorWebFormsComponents.Cli.Tests/TransformUnit/ServerShimTransformTests.cs @@ -134,6 +134,104 @@ void Process() Assert.Equal(1, count); } + [Fact] + public void DetectsServerTransfer_AddsNoShimTodo() + { + var input = @"namespace MyApp +{ + public partial class MyPage + { + void Process() + { + Server.Transfer(""~/OtherPage.aspx""); + } + } +}"; + var result = _transform.Apply(input, TestMetadata(input)); + + Assert.Contains("TODO(bwfc-server): Server.Transfer has NO SHIM", result); + Assert.Contains("NavigationManager.NavigateTo()", result); + } + + [Fact] + public void DetectsServerGetLastError_AddsNoShimTodo() + { + var input = @"namespace MyApp +{ + public partial class MyPage + { + void Process() + { + var ex = Server.GetLastError(); + } + } +}"; + var result = _transform.Apply(input, TestMetadata(input)); + + Assert.Contains("TODO(bwfc-server): Server.GetLastError has NO SHIM", result); + Assert.Contains("ILogger", result); + } + + [Fact] + public void DetectsServerClearError_AddsNoShimTodo() + { + var input = @"namespace MyApp +{ + public partial class MyPage + { + void Process() + { + Server.ClearError(); + } + } +}"; + var result = _transform.Apply(input, TestMetadata(input)); + + Assert.Contains("TODO(bwfc-server): Server.ClearError has NO SHIM", result); + Assert.Contains("middleware", result); + } + + [Fact] + public void DetectsTransferAndMapPath_BothHandled() + { + var input = @"namespace MyApp +{ + public partial class MyPage + { + void Process() + { + var path = Server.MapPath(""~/uploads""); + Server.Transfer(""~/Other.aspx""); + } + } +}"; + var result = _transform.Apply(input, TestMetadata(input)); + + Assert.Contains("MapPath", result); + Assert.Contains("TODO(bwfc-server): Server.Transfer has NO SHIM", result); + Assert.Contains("TODO(bwfc-server): Server.* calls work automatically via ServerShim on WebFormsPageBase", result); + } + + [Fact] + public void ServerTransfer_Idempotent() + { + var input = @"namespace MyApp +{ + public partial class MyPage + { + void Process() + { + Server.Transfer(""~/Other.aspx""); + } + } +}"; + var result = _transform.Apply(input, TestMetadata(input)); + result = _transform.Apply(result, TestMetadata(result)); + + var count = result.Split("Server.Transfer has NO SHIM").Length - 1; + Assert.Equal(1, count); + } + [Fact] public void OrderIs330() { diff --git a/tests/BlazorWebFormsComponents.Cli.Tests/TransformUnit/SessionDetectTransformTests.cs b/tests/BlazorWebFormsComponents.Cli.Tests/TransformUnit/SessionDetectTransformTests.cs index 887099826..f146bffc3 100644 --- a/tests/BlazorWebFormsComponents.Cli.Tests/TransformUnit/SessionDetectTransformTests.cs +++ b/tests/BlazorWebFormsComponents.Cli.Tests/TransformUnit/SessionDetectTransformTests.cs @@ -5,8 +5,8 @@ namespace BlazorWebFormsComponents.Cli.Tests.TransformUnit; /// /// Unit tests for SessionDetectTransform — detects Session["key"] and Cache["key"] -/// patterns, injects [Inject] SessionShim / CacheShim properties, and generates -/// migration guidance. +/// patterns and generates migration guidance. Session/Cache are provided by +/// WebFormsPageBase — no [Inject] needed. /// public class SessionDetectTransformTests { @@ -21,9 +21,10 @@ public class SessionDetectTransformTests }; [Fact] - public void InjectsSessionShimProperty() + public void DetectsSessionAccess_AddsGuidance() { - var input = @"namespace MyApp + var input = @"// ============================================================================= +namespace MyApp { public partial class MyPage { @@ -32,13 +33,16 @@ public partial class MyPage }"; var result = _transform.Apply(input, TestMetadata(input)); - Assert.Contains("[Inject] private SessionShim Session { get; set; }", result); + Assert.Contains("// --- Session State Migration ---", result); + Assert.Contains("SessionShim on WebFormsPageBase", result); + Assert.DoesNotContain("[Inject]", result); } [Fact] - public void InjectsCacheShimProperty() + public void DetectsCacheAccess_AddsGuidance() { - var input = @"namespace MyApp + var input = @"// ============================================================================= +namespace MyApp { public partial class MyPage { @@ -47,13 +51,16 @@ public partial class MyPage }"; var result = _transform.Apply(input, TestMetadata(input)); - Assert.Contains("[Inject] private CacheShim Cache { get; set; }", result); + Assert.Contains("// --- Cache Migration ---", result); + Assert.Contains("CacheShim on WebFormsPageBase", result); + Assert.DoesNotContain("[Inject]", result); } [Fact] - public void InjectsBothSessionAndCacheShims() + public void DetectsBothSessionAndCache_AddsBothGuidanceBlocks() { - var input = @"namespace MyApp + var input = @"// ============================================================================= +namespace MyApp { public partial class MyPage { @@ -66,8 +73,9 @@ void Load() }"; var result = _transform.Apply(input, TestMetadata(input)); - Assert.Contains("[Inject] private SessionShim Session { get; set; }", result); - Assert.Contains("[Inject] private CacheShim Cache { get; set; }", result); + Assert.Contains("// --- Session State Migration ---", result); + Assert.Contains("// --- Cache Migration ---", result); + Assert.DoesNotContain("[Inject]", result); } [Fact] @@ -84,7 +92,7 @@ public partial class MyPage var result = _transform.Apply(input, TestMetadata(input)); Assert.Contains("// --- Session State Migration ---", result); - Assert.Contains("SessionShim auto-wired via [Inject]", result); + Assert.Contains("calls work automatically via SessionShim on WebFormsPageBase", result); Assert.Contains("Session keys found: CartId", result); Assert.Contains("Options for long-term replacement:", result); } @@ -103,7 +111,7 @@ public partial class MyPage var result = _transform.Apply(input, TestMetadata(input)); Assert.Contains("// --- Cache Migration ---", result); - Assert.Contains("CacheShim auto-wired via [Inject]", result); + Assert.Contains("calls work automatically via CacheShim on WebFormsPageBase", result); Assert.Contains("Cache keys found: Products", result); Assert.Contains("CacheShim wraps IMemoryCache", result); } @@ -133,9 +141,10 @@ void Load() } [Fact] - public void IdempotentDoesNotDuplicateInjectProperty() + public void IdempotentDoesNotDuplicateGuidance() { - var input = @"namespace MyApp + var input = @"// ============================================================================= +namespace MyApp { public partial class MyPage { @@ -145,13 +154,12 @@ public partial class MyPage var firstPass = _transform.Apply(input, TestMetadata(input)); var secondPass = _transform.Apply(firstPass, TestMetadata(firstPass)); - // Count occurrences of the inject line - var injectCount = secondPass.Split("[Inject] private SessionShim Session").Length - 1; - Assert.Equal(1, injectCount); + var markerCount = secondPass.Split("// --- Session State Migration ---").Length - 1; + Assert.Equal(1, markerCount); } [Fact] - public void IdempotentDoesNotDuplicateGuidanceBlock() + public void IdempotentDoesNotDuplicateGuidanceBlock_WithTodoHeader() { var input = @"// ============================================================================= namespace MyApp @@ -186,7 +194,8 @@ public partial class MyPage [Fact] public void DetectsVariableKeySessionAccess() { - var input = @"namespace MyApp + var input = @"// ============================================================================= +namespace MyApp { public partial class MyPage { @@ -195,7 +204,8 @@ public partial class MyPage }"; var result = _transform.Apply(input, TestMetadata(input)); - Assert.Contains("[Inject] private SessionShim Session { get; set; }", result); + Assert.Contains("// --- Session State Migration ---", result); + Assert.Contains("SessionShim on WebFormsPageBase", result); } [Fact] @@ -229,9 +239,10 @@ public partial class MyPage } [Fact] - public void InjectPropertyAppearsAfterClassBrace() + public void GuidanceAppearsInOutput() { - var input = @"namespace MyApp + var input = @"// ============================================================================= +namespace MyApp { public partial class MyPage { @@ -240,10 +251,9 @@ public partial class MyPage }"; var result = _transform.Apply(input, TestMetadata(input)); - var classIdx = result.IndexOf("public partial class MyPage"); - var braceIdx = result.IndexOf('{', classIdx); - var injectIdx = result.IndexOf("[Inject] private SessionShim Session", braceIdx); - Assert.True(injectIdx > braceIdx, "[Inject] should appear after the class opening brace"); + Assert.Contains("// --- Session State Migration ---", result); + Assert.Contains("SessionShim on WebFormsPageBase", result); + Assert.DoesNotContain("[Inject]", result); } [Fact] @@ -266,6 +276,74 @@ void Load() Assert.Contains(@"Session[""UserName""] = ""test""", result); } + [Fact] + public void DetectsHttpContextCurrentSession_ReplacesWithSession() + { + var input = @"namespace MyApp +{ + public partial class MyPage + { + void Load() { var x = HttpContext.Current.Session[""CartId""]; } + } +}"; + var result = _transform.Apply(input, TestMetadata(input)); + + // The actual code line should have HttpContext.Current.Session replaced + Assert.DoesNotContain(@"var x = HttpContext.Current.Session[""CartId""]", result); + Assert.Contains(@"var x = Session[""CartId""]", result); + Assert.Contains("TODO(bwfc-session-state): HttpContext.Current.Session was replaced", result); + } + + [Fact] + public void HttpContextCurrentSession_StillAddsSessionGuidance() + { + var input = @"// ============================================================================= +namespace MyApp +{ + public partial class MyPage + { + void Load() { var x = HttpContext.Current.Session[""CartId""]; } + } +}"; + var result = _transform.Apply(input, TestMetadata(input)); + + // After replacement, Session[ is detected → guidance is added + Assert.Contains("SessionShim on WebFormsPageBase", result); + Assert.DoesNotContain("[Inject]", result); + } + + [Fact] + public void HttpContextCurrentSession_EmitsDiGuidanceForNonPage() + { + var input = @"namespace MyApp +{ + public partial class MyPage + { + void Load() { var x = HttpContext.Current.Session[""CartId""]; } + } +}"; + var result = _transform.Apply(input, TestMetadata(input)); + + Assert.Contains("non-page classes, inject SessionShim via constructor DI", result); + } + + [Fact] + public void HttpContextCurrentSession_Idempotent() + { + var input = @"namespace MyApp +{ + public partial class MyPage + { + void Load() { var x = HttpContext.Current.Session[""CartId""]; } + } +}"; + var result = _transform.Apply(input, TestMetadata(input)); + result = _transform.Apply(result, TestMetadata(result)); + + var count = result.Split("HttpContext.Current.Session was replaced").Length - 1; + Assert.Equal(1, count); + } + [Fact] public void OrderIs400() {