Skip to content

Commit b5b813e

Browse files
csharpfritzCopilot
andcommitted
fix: sync library, CLI, tests, and docs from main to resolve PR FritzAndFriends#552 CI failures
The -X ours merge strategy kept dev's older versions of source files while other files referenced features only in main (from PR FritzAndFriends#547 squash). This created inconsistent state causing CS0506 (FindControl not virtual) and CS0117 (missing CascadingParameterName) errors. Restored main's versions of: - Core library: BaseWebFormsComponent, WebFormsPageBase, GridView, CheckBox, etc. - CLI project: all transforms, pipeline, scaffolding files - Tests: all CLI test files and test data - Docs: mkdocs.yml, CLI docs, migration guides Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 4523f99 commit b5b813e

103 files changed

Lines changed: 3482 additions & 662 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/Migration/FindControl-Migration.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,31 @@ public class BaseWebFormsComponent : ComponentBase
344344

345345
**What it does:** Searches the current component's child controls and all descendants recursively for one with the matching ID. This mirrors the deep-search behavior that migrated Web Forms code typically expects.
346346

347+
### GridViewRow.FindControl — Data Row Controls
348+
349+
For data controls like `GridView`, `FindControl` also works on individual rows. `GridViewRow<T>` and the non-generic `GridViewRow` shim both support `FindControl(string id)`:
350+
351+
```csharp
352+
// This Web Forms pattern compiles and works unchanged in BWFC:
353+
GridViewRow row = CartList.Rows[i]; // implicit operator from GridViewRow<T>
354+
TextBox qty = (TextBox)row.FindControl("PurchaseQuantity");
355+
CheckBox remove = (CheckBox)row.FindControl("chkRemove");
356+
```
357+
358+
In SSR mode, `FindControl` returns proxy `TextBox` and `CheckBox` instances whose `Text` and `Checked` properties are populated from the form POST data. This means migrated code that reads control values inside a postback handler works without modification.
359+
360+
**Key compatibility features:**
361+
362+
| Feature | Description |
363+
|---------|-------------|
364+
| `Rows[i]` typed indexer | Returns `GridViewRow<T>`, not `IRow<T>` |
365+
| Implicit conversion | `GridViewRow<T>` converts to non-generic `GridViewRow` automatically |
366+
| `FindControl` on rows | Returns proxy controls populated from form POST data |
367+
| `Cells` collection | `DataControlFieldCellCollection` with `ContainingField.ExtractValuesFromCell()` |
368+
| `RowState` | `DataControlRowState` flags enum (Normal, Alternate, Edit, Selected) |
369+
370+
See [GridView — GridViewRow Compatibility](../DataControls/GridView.md#gridviewrow-compatibility) for full details and code examples.
371+
347372
---
348373

349374
## Complete DepartmentPortal Migration Examples

docs/cli/index.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ This tool **reduces manual migration effort** by:
2121
- Extracting code patterns and flagging them with TODO comments for Copilot L2 automation
2222
- Quarantining risky legacy bootstrap/source artifacts out of the generated SSR compile surface
2323
- Scaffolding a new .NET 10 Blazor SSR project structure with shims, services, and relaxed code-style build enforcement for copied legacy files
24-
- Detecting common runtime needs from the source app (DbContext classes, session usage, Account pages, and Global.asax startup hooks) and wiring matching `Program.cs` services/middleware automatically, including static-file serving and antiforgery middleware for SSR form posts
24+
- Detecting common runtime needs from the source app (DbContext classes, session usage, Account pages, and Global.asax startup hooks) and wiring matching `Program.cs` services/middleware automatically, including static-file serving, antiforgery middleware for SSR form posts, and generated account login/register/logout endpoints when Identity is detected
25+
- Modernizing legacy `AttachDbFilename=|DataDirectory|\*.mdf` connection strings to use `Initial Catalog=...` so migrated apps do not depend on missing local MDF files at runtime
2526

2627
The tool processes `.aspx`, `.ascx`, and `.master` files in a fixed sequence, then applies a bounded semantic pattern catalog so each higher-level rewrite builds on a normalized page shape.
2728

@@ -93,7 +94,7 @@ webforms-to-blazor migrate \
9394
**Output:**
9495
- Converted `.razor` files
9596
- Quarantined manual code-behind and risky legacy source artifacts under `migration-artifacts\`, including a `quarantine-manifest.json` inventory for deferred page migration work
96-
- Generated `Program.cs` with shim registration for static SSR on .NET 10 plus detected runtime wiring for EF Core, session state, identity, and legacy `Application_Start` review notes
97+
- Generated `Program.cs` with shim registration for static SSR on .NET 10 plus detected runtime wiring for EF Core, session state, identity, generated account auth endpoints, and legacy `Application_Start` review notes
9798
- Migration report (`migration-report.json`)
9899

99100
### `convert` — File-Level Transformation
@@ -117,8 +118,8 @@ webforms-to-blazor convert \
117118
The tool applies an ordered transform pipeline and then a semantic pattern catalog:
118119

119120
1. **Directives** (5) — Page, Master, Control, Register, Import directives
120-
2. **Markup** (21) — Controls, expressions, master-page script cleanup, display-expression cleanup, templates, validator typing, typed GridView columns, data binding
121-
3. **Code-Behind** (27) — Using statements, cart session-key stabilization, HttpUtility/EF modernization, base classes, lifecycle, event handlers, compile-surface stubs, markup-driven safety stubs
121+
2. **Markup** (21) — Controls, expressions, master-page script cleanup, display-expression cleanup, templates, validator typing, typed GridView columns (including `CommandField`), and CRUD model-binding attributes
122+
3. **Code-Behind** (29) — Using statements, cart session-key stabilization, HttpUtility/EF modernization, IQueryable SelectMethod materialization, WebMethod TODO annotation, base classes, lifecycle, event handlers, compile-surface stubs, markup-driven safety stubs
122123

123124
See **[Transform Reference](transforms.md)** for the flat transform list and **[Semantic Pattern Catalog](semantic-pattern-catalog.md)** for the bounded semantic pass that runs afterward.
124125

docs/cli/transforms.md

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,15 @@ This page documents the flat markup and code-behind transforms applied by the `w
2020
| 500 | ExpressionTransform | Markup | Markup | Convert `<%: %>`, `<%= %>`, and inline data expressions to Razor |
2121
| 510 | ServerCodeBlockTransform | Markup | Markup | Convert `<% ... %>` statement blocks to Razor control flow |
2222
| 510 | LoginViewTransform | Markup | Markup | Convert `<asp:LoginView>``<AuthorizeView>` |
23-
| 520 | SelectMethodTransform | Markup | Markup | Flag SelectMethod/InsertMethod/etc. |
23+
| 520 | SelectMethodTransform | Markup | Markup | Preserve SelectMethod / InsertMethod / UpdateMethod / DeleteMethod model-binding attributes |
24+
| 550 | WebMethodAnnotationTransform | Code-Behind | Code-Behind | Flag legacy static WebMethod endpoints and normalize `Page_PreRenderComplete` for lifecycle conversion |
2425
| 600 | AjaxToolkitPrefixTransform | Markup | Markup | Remove `ajaxToolkit:` prefixes |
2526
| 610 | AspPrefixTransform | Markup | Markup | Remove `asp:` prefixes from controls |
2627
| 615 | DataBindingAttributeTransform | Markup | Markup | Convert `<%# ... %>` and `<%= ... %>` attribute values to `@(...)` |
2728
| 615 | ValidatorGenericTypeTransform | Markup | Markup | Add explicit `Type="string"` / `InputType="string"` defaults for generic BWFC validators |
2829
| 620 | TemplateFieldChildComponentsTransform | Markup | Markup | Wrap TemplateField style children in `<ChildComponents>` |
2930
| 700 | AttributeStripTransform | Markup | Markup | Remove `runat="server"`, preserve BWFC `ItemType`, add generic fallbacks |
30-
| 705 | GridViewColumnItemTypeTransform | Markup | Markup | Propagate typed `GridView ItemType` values to `BoundField` / `TemplateField` child columns |
31+
| 705 | GridViewColumnItemTypeTransform | Markup | Markup | Propagate typed `GridView ItemType` values to `BoundField` / `TemplateField` / `ButtonField` / `CommandField` child columns |
3132
| 710 | EventWiringTransform | Markup | Markup | Convert `OnClick="X"``OnClick="@X"` |
3233
| 720 | UrlReferenceTransform | Markup | Markup | Convert `~/` paths to `/` |
3334
| 750 | ComponentRefMarkupTransform | Markup | Markup | Convert control IDs to `@ref`-compatible component references |
@@ -50,6 +51,8 @@ This page documents the flat markup and code-behind transforms applied by the `w
5051
| 50 | UrlCleanupTransform | Code-Behind | Code-Behind | Clean URL literals in code |
5152
| 104 | HttpUtilityRewriteTransform | Code-Behind | Code-Behind | Rewrite `HttpUtility.*` calls to `WebUtility.*` and add `using System.Net;` |
5253
| 106 | EfContextConstructorTransform | Code-Behind | Code-Behind | Rewrite EF6 `base("name")` DbContext constructors to EF Core `DbContextOptions<TContext>` constructors |
54+
| 107 | DbContextInstantiationTransform | Code-Behind | Code-Behind | Replace inline `new XxxContext()` usage with injected DbContext references |
55+
| 108 | SelectMethodMaterializeTransform | Code-Behind | Code-Behind | Materialize IQueryable SelectMethod results before methods exit after `CreateDbContext()` usage |
5356
| 850 | CompileSurfaceStubTransform | Code-Behind | Code-Behind | Quarantine identity/payment/mobile/admin/compile-blocked pages with build-safe stubs while preserving transformed originals in `migration-artifacts\codebehind\` and tracking them in `migration-artifacts\quarantine-manifest.json` |
5457
| 900 | MarkupReferencedMemberStubTransform | Code-Behind | Code-Behind | Add fallback fields, render-method stubs, and event handlers for markup references still missing from emitted partial classes |
5558

@@ -1021,7 +1024,41 @@ string imageUrl = "/images/logo.png";
10211024

10221025
---
10231026

1024-
### 32. CompileSurfaceStubTransform (Order: 850)
1027+
### 32. SelectMethodMaterializeTransform (Order: 108)
1028+
1029+
**Materializes `IQueryable` SelectMethod results before the scoped DbContext goes out of scope.**
1030+
1031+
**Before:**
1032+
```csharp
1033+
public IQueryable<Product> GetProducts(int maxRows, int startRowIndex, string sortByExpression, out int totalRowCount)
1034+
{
1035+
using var db = DbFactory.CreateDbContext();
1036+
var query = db.Products.OrderBy(p => p.ProductName);
1037+
totalRowCount = query.Count();
1038+
return query;
1039+
}
1040+
```
1041+
1042+
**After:**
1043+
```csharp
1044+
public IQueryable<Product> GetProducts(int maxRows, int startRowIndex, string sortByExpression, out int totalRowCount)
1045+
{
1046+
var db = DbFactory.CreateDbContext();
1047+
var query = db.Products.OrderBy(p => p.ProductName);
1048+
totalRowCount = query.Count();
1049+
return query.ToList().AsQueryable();
1050+
}
1051+
```
1052+
1053+
**Details:**
1054+
- Only applies to methods whose signature returns `IQueryable<...>`
1055+
- Removes `using` from `using var db = ...CreateDbContext()` so the query can be materialized before the method exits
1056+
- Rewrites simple `return query;` patterns to `return query.ToList().AsQueryable();`
1057+
- Leaves non-`IQueryable` methods untouched
1058+
1059+
---
1060+
1061+
### 33. CompileSurfaceStubTransform (Order: 850)
10251062

10261063
**Quarantines non-migratable pages behind build-safe placeholders.**
10271064

@@ -1035,7 +1072,7 @@ string imageUrl = "/images/logo.png";
10351072

10361073
---
10371074

1038-
### 33. MarkupReferencedMemberStubTransform (Order: 900)
1075+
### 34. MarkupReferencedMemberStubTransform (Order: 900)
10391076

10401077
**Adds compile-safe fallback members for identifiers that remain referenced from markup.**
10411078

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ nav:
215215
- Migration Strategies: Migration/Strategies.md
216216
- .NET Standard to the Rescue: Migration/NET-Standard.md
217217
- Automated Migration Guide: Migration/AutomatedMigration.md
218+
- Automated Migration with Copilot: Migration/AutomatedMigrationWithCopilot.md
218219
- Roslyn Analyzers: Migration/Analyzers.md
219220
- Guides:
220221
- ClientScript Migration: Migration/ClientScriptMigrationGuide.md

src/BlazorWebFormsComponents.Cli/BlazorWebFormsComponents.Cli.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@
2525
<RepositoryType>GitHub</RepositoryType>
2626
</PropertyGroup>
2727

28+
<ItemGroup>
29+
<InternalsVisibleTo Include="BlazorWebFormsComponents.Cli.Tests" />
30+
</ItemGroup>
31+
2832
<ItemGroup>
2933
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
3034
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />

src/BlazorWebFormsComponents.Cli/Config/WebConfigTransformer.cs

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Text.Json;
2+
using System.Text.RegularExpressions;
23
using System.Xml.Linq;
34
using BlazorWebFormsComponents.Cli.Io;
45

@@ -68,7 +69,7 @@ public class WebConfigTransformer
6869
{
6970
if (!BuiltInConnectionNames.Contains(name))
7071
{
71-
connectionStrings[name] = connStr;
72+
connectionStrings[name] = NormalizeConnectionString(connStr);
7273
}
7374
}
7475
}
@@ -123,6 +124,109 @@ public class WebConfigTransformer
123124
};
124125
}
125126

127+
private static string NormalizeConnectionString(string connStr)
128+
{
129+
// EF6 metadata connection strings wrap the real SQL connection string inside
130+
// a provider connection string parameter. Unwrap it first.
131+
connStr = UnwrapEf6ConnectionString(connStr);
132+
133+
if (!connStr.Contains("AttachDbFilename=|DataDirectory|", StringComparison.OrdinalIgnoreCase))
134+
return connStr;
135+
136+
var segments = connStr.Split(';', StringSplitOptions.RemoveEmptyEntries)
137+
.Select(segment => segment.Trim())
138+
.ToList();
139+
140+
string? databaseName = null;
141+
var normalizedSegments = new List<string>();
142+
143+
foreach (var segment in segments)
144+
{
145+
var separatorIndex = segment.IndexOf('=');
146+
if (separatorIndex < 0)
147+
{
148+
normalizedSegments.Add(segment);
149+
continue;
150+
}
151+
152+
var key = segment[..separatorIndex].Trim();
153+
var value = segment[(separatorIndex + 1)..].Trim();
154+
155+
if (key.Equals("AttachDbFilename", StringComparison.OrdinalIgnoreCase) &&
156+
value.StartsWith("|DataDirectory|", StringComparison.OrdinalIgnoreCase))
157+
{
158+
var fileName = Path.GetFileNameWithoutExtension(value.Replace("|DataDirectory|\\", string.Empty, StringComparison.OrdinalIgnoreCase)
159+
.Replace("|DataDirectory|/", string.Empty, StringComparison.OrdinalIgnoreCase));
160+
161+
if (!string.IsNullOrWhiteSpace(fileName))
162+
databaseName ??= NormalizeDatabaseName(fileName);
163+
164+
continue;
165+
}
166+
167+
normalizedSegments.Add(segment);
168+
}
169+
170+
if (string.IsNullOrWhiteSpace(databaseName))
171+
return connStr;
172+
173+
if (!normalizedSegments.Any(segment => segment.StartsWith("Initial Catalog=", StringComparison.OrdinalIgnoreCase)))
174+
{
175+
var insertIndex = normalizedSegments.FindIndex(segment => segment.StartsWith("Data Source=", StringComparison.OrdinalIgnoreCase));
176+
var catalogSegment = $"Initial Catalog={databaseName}";
177+
if (insertIndex >= 0)
178+
normalizedSegments.Insert(insertIndex + 1, catalogSegment);
179+
else
180+
normalizedSegments.Add(catalogSegment);
181+
}
182+
183+
return string.Join(';', normalizedSegments);
184+
}
185+
186+
private static string NormalizeDatabaseName(string fileName)
187+
{
188+
return fileName.ToLowerInvariant() switch
189+
{
190+
"wingtiptoys" => "WingtipToys",
191+
"contosouniversity" => "ContosoUniversity",
192+
"departmentportal" => "DepartmentPortal",
193+
_ => string.Concat(fileName
194+
.Split(['-', '_', ' '], StringSplitOptions.RemoveEmptyEntries)
195+
.Select(segment => char.ToUpperInvariant(segment[0]) + segment[1..].ToLowerInvariant()))
196+
};
197+
}
198+
199+
// Matches EF6 metadata connection strings:
200+
// metadata=res://*/...;provider=System.Data.SqlClient;provider connection string="actual conn string"
201+
private static readonly Regex Ef6MetadataRegex = new(
202+
@"^\s*metadata\s*=\s*res://",
203+
RegexOptions.Compiled | RegexOptions.IgnoreCase);
204+
205+
private static readonly Regex Ef6ProviderConnectionStringRegex = new(
206+
@"provider\s+connection\s+string\s*=\s*""(?<inner>[^""]*)""",
207+
RegexOptions.Compiled | RegexOptions.IgnoreCase);
208+
209+
/// <summary>
210+
/// Unwraps EF6 metadata connection strings to extract the inner SQL connection string.
211+
/// EF6 format: metadata=res://*/...;provider=System.Data.SqlClient;provider connection string="Data Source=...;Initial Catalog=..."
212+
/// Returns the inner provider connection string, or the original if not EF6 format.
213+
/// </summary>
214+
private static string UnwrapEf6ConnectionString(string connStr)
215+
{
216+
if (!Ef6MetadataRegex.IsMatch(connStr))
217+
return connStr;
218+
219+
var match = Ef6ProviderConnectionStringRegex.Match(connStr);
220+
if (match.Success)
221+
{
222+
var inner = match.Groups["inner"].Value;
223+
// The inner string may use &quot; for quotes (from XML) — unescape
224+
return inner.Replace("&quot;", "\"");
225+
}
226+
227+
return connStr;
228+
}
229+
126230
private static string? FindWebConfig(string sourcePath)
127231
{
128232
var path1 = Path.Combine(sourcePath, "Web.config");

0 commit comments

Comments
 (0)