Skip to content

Commit cb8eef2

Browse files
csharpfritzCopilot
andcommitted
ContosoUniversity Run 08 migration - 40/40 tests passing
- Fixed DetailsView to use Items collection instead of DataItem - Added URL rewriting for legacy .aspx URLs - Added multiple @page routes (/Home, /ContosoUniversity/Home) - Added PageTitle component for proper HTML title - Fixed search with partial name matching - Added UseSubmitBehavior=false to search button - Updated tests with Blazor circuit timing waits - Created migration report with timing details Key finding: BWFC DetailsView only supports Items collection, not DataItem property. Wrap single items: Items='new[] { item }' Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent aeb8faf commit cb8eef2

23 files changed

Lines changed: 766 additions & 371 deletions

File tree

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# ContosoUniversity Migration — Run 08
2+
3+
## Executive Summary
4+
5+
**Result: 40/40 tests passing (100%)**
6+
7+
Run 08 successfully migrated ContosoUniversity from ASP.NET Web Forms to Blazor using the BlazorWebFormsComponents library and automated migration scripts.
8+
9+
| Metric | Value |
10+
|--------|-------|
11+
| **Final Score** | 40/40 (100%) |
12+
| **Layer 1 Script Time** | ~1.1 seconds |
13+
| **Layer 2 Script Time** | ~0.5 seconds |
14+
| **Manual Fix Time** | ~25 minutes |
15+
| **Total Time** | ~30 minutes |
16+
| **Render Mode** | InteractiveServer |
17+
| **Database** | SQLite |
18+
| **Branch** | `squad/audit-docs-perf` |
19+
20+
## Migration Timeline
21+
22+
### Phase 1: Automated Script Execution (1.6 seconds)
23+
- Layer 1 script: 1.1s — Converted 5 ASPX pages, 1 Master Page
24+
- Layer 2 script: 0.5s — Generated code-behind templates
25+
26+
### Phase 2: EF6 → EF Core Conversion (~10 minutes)
27+
The source uses EF6 with EDMX, requiring manual conversion:
28+
- Removed EF6 EDMX files (`Model1.cs`, `Model1.Context.cs`, etc.)
29+
- Created clean EF Core models (renamed `Cours``Course`)
30+
- Created `ContosoUniversityContext.cs` (DbContext)
31+
- Created `DbInitializer.cs` with seed data
32+
- Updated `Program.cs` with EF Core + SQLite
33+
34+
### Phase 3: BWFC Component Fixes (~15 minutes)
35+
36+
1. **URL Routing**
37+
- Added `.aspx` fallback routes via `RewriteOptions`
38+
- Added multiple `@page` directives: `/Home`, `/ContosoUniversity/Home`
39+
40+
2. **Page Title**
41+
- Added `<PageTitle>` component for proper HTML title
42+
43+
3. **Nav Links**
44+
- Changed nav hrefs from `/` to `/Home` for test compatibility
45+
46+
4. **GridView Sorting**
47+
- Added `AllowSorting="true"` to Instructors GridView
48+
49+
5. **DetailsView Data Binding**
50+
- Fixed: Use `Items="new[] { item }"` not `DataItem="item"`
51+
- DetailsView only supports `Items` collection, not `DataItem`
52+
53+
6. **Search Functionality**
54+
- Improved search to use partial matching (`Contains`)
55+
- Added `UseSubmitBehavior="false"` to prevent form submission
56+
- Added element IDs for test locators (`txtSearch`, `btnSearch`)
57+
58+
7. **Test Updates**
59+
- Added `WaitForBlazorCircuit()` to search tests
60+
- Increased timeout for Blazor re-render
61+
62+
## Files Modified
63+
64+
### New Files Created
65+
- `Data/ContosoUniversityContext.cs` — EF Core DbContext
66+
- `Data/DbInitializer.cs` — Seed data
67+
- `appsettings.Development.json` — Development config
68+
69+
### Core Infrastructure
70+
- `Program.cs` — EF Core + URL rewriting
71+
- `ContosoUniversity.csproj` — EF Core packages
72+
- `Properties/launchSettings.json` — Port 44380
73+
74+
### Pages Modified
75+
- `Pages/Home.razor` — Added `@page "/Home"`, `<PageTitle>`
76+
- `Pages/About.razor` — Added `@page "/About"`
77+
- `Pages/Students.razor` — Fixed DetailsView, search, IDs
78+
- `Pages/Students.razor.cs` — Improved search logic
79+
- `Pages/Courses.razor` — Added `@page "/Courses"`
80+
- `Pages/Instructors.razor` — Added sorting, `@page "/Instructors"`
81+
82+
### Layout
83+
- `Components/Layout/MainLayout.razor` — Fixed nav hrefs
84+
85+
### Test Fixes
86+
- `src/ContosoUniversity.AcceptanceTests/StudentsPageTests.cs` — Added Blazor circuit waits
87+
88+
## Key Discoveries
89+
90+
### 1. DetailsView Uses Items, Not DataItem
91+
BWFC DetailsView does not have a `DataItem` property. To bind a single item, wrap in array:
92+
```razor
93+
<DetailsView Items="new[] { item }" ItemType="MyType">
94+
```
95+
96+
### 2. Blazor Circuit Timing in Tests
97+
Tests must wait for Blazor SignalR circuit before interactive operations:
98+
```csharp
99+
await WaitForBlazorCircuit(page);
100+
await page.WaitForTimeoutAsync(500); // Allow re-render
101+
```
102+
103+
### 3. Button UseSubmitBehavior
104+
In Blazor interactive mode, `type="submit"` can cause page navigation instead of Blazor handler execution. Use `UseSubmitBehavior="false"` for buttons that should only trigger Blazor events.
105+
106+
### 4. URL Rewriting for Legacy URLs
107+
Tests use `.aspx` URLs. Add URL rewriting in `Program.cs`:
108+
```csharp
109+
var rewriteOptions = new RewriteOptions()
110+
.AddRedirect("^Home.aspx$", "/Home", 301);
111+
app.UseRewriter(rewriteOptions);
112+
```
113+
114+
### 5. Multiple @page Directives
115+
Blazor pages can have multiple routes:
116+
```razor
117+
@page "/ContosoUniversity/Home"
118+
@page "/"
119+
@page "/Home"
120+
```
121+
122+
## Test Results
123+
124+
```
125+
Test summary: total: 40, passed: 40, failed: 0, skipped: 0
126+
Duration: 33 seconds
127+
```
128+
129+
| Category | Passed |
130+
|----------|--------|
131+
| Home Page | 8/8 |
132+
| About Page | 5/5 |
133+
| Students Page | 9/9 |
134+
| Courses Page | 6/6 |
135+
| Instructors Page | 5/5 |
136+
| Navigation | 7/7 |
137+
138+
## Recommendations for Script Improvements
139+
140+
1. **Layer 2 should generate Items wrapper for DetailsView** — When migrating FormView/DetailsView with single item binding, generate `Items="new[] { _item }"` pattern
141+
142+
2. **URL rewriting should be automatic** — Script should add `@page "/X.aspx"` as fallback route
143+
144+
3. **PageTitle component** — Layer 1 should add `<PageTitle>@title</PageTitle>` based on ASPX `Title` attribute
145+
146+
4. **Multiple routes** — Add both `/Folder/Page` and `/Page` routes for flexibility
147+
148+
5. **UseSubmitBehavior default** — Consider defaulting BWFC Button to `UseSubmitBehavior="false"` for safer Blazor interactivity
149+
150+
## Comparison to Previous Runs
151+
152+
| Run | Score | Key Achievement |
153+
|-----|-------|-----------------|
154+
| Run 07 | 40/40 | CascadingTypeParameter for DetailsView |
155+
| **Run 08** | **40/40** | DetailsView Items binding fix, search improvements |
156+
157+
## Conclusion
158+
159+
Run 08 validates that the migration patterns established in Run 07 remain effective. The key learning is that BWFC DetailsView uses `Items` collection binding, not `DataItem` single-item binding. With proper EF Core conversion and Blazor-specific adjustments (circuit timing, URL rewriting), ContosoUniversity achieves 100% test compatibility.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# ContosoUniversity Run 08
2+
3+
## Migration Summary
4+
5+
| Metric | Value |
6+
|--------|-------|
7+
| Date | 2026-03-09 |
8+
| Branch | `squad/audit-docs-perf` |
9+
| Score | **40/40 (100%)** |
10+
| Render Mode | InteractiveServer |
11+
| Total Time | ~30 minutes |
12+
13+
## Key Changes from Run 07
14+
15+
1. **DetailsView Items Binding** — Fixed: Use `Items="new[] { item }"` instead of `DataItem`
16+
2. **Search Improvements** — Added partial matching, proper element IDs
17+
3. **Test Timing** — Added `WaitForBlazorCircuit()` to search tests
18+
19+
## What Went Right
20+
21+
- All 40 acceptance tests pass
22+
- EF Core migration from EF6 EDMX worked smoothly
23+
- URL rewriting provides seamless legacy URL support
24+
- GridView sorting renders clickable headers
25+
26+
## What Went Wrong
27+
28+
- DetailsView doesn't support `DataItem` property — must wrap single items in array
29+
- Tests needed timing updates for Blazor circuit initialization
30+
- Button `UseSubmitBehavior="true"` (default) can interfere with Blazor event handling
31+
32+
## Full Report
33+
34+
See [dev-docs/migration-tests/contosouniversity-run08-2026-03-09/REPORT.md](../../dev-docs/migration-tests/contosouniversity-run08-2026-03-09/REPORT.md) for complete details.

samples/AfterContosoUniversity/Components/Layout/MainLayout.razor

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
<HeadContent>
44
<meta charset="utf-8"/>
55
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
6-
<link href="css/Master_CSS.css" rel="stylesheet" />
6+
<title>Contoso University</title>
7+
<link href="CSS/Master_CSS.css" rel="stylesheet" />
78
</HeadContent>
89

910
<div id="navUp">
@@ -12,10 +13,10 @@
1213
<div id="navBar">
1314
<ul>
1415
<li><a id="home" href="/Home">Home</a></li>
15-
<li><a id="about" href="/ContosoUniversity/About">About</a></li>
16-
<li><a id="students" href="/ContosoUniversity/Students">Students</a></li>
17-
<li><a id="courses" href="/ContosoUniversity/Courses">Courses</a></li>
18-
<li><a id="instructors" href="/ContosoUniversity/Instructors">Instructors</a></li>
16+
<li><a id="about" href="/About">About</a></li>
17+
<li><a id="students" href="/Students">Students</a></li>
18+
<li><a id="courses" href="/Courses">Courses</a></li>
19+
<li><a id="instructors" href="/Instructors">Instructors</a></li>
1920
</ul>
2021
</div>
2122

samples/AfterContosoUniversity/ContosoUniversity.csproj

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,15 @@
44
<TargetFramework>net10.0</TargetFramework>
55
<Nullable>enable</Nullable>
66
<ImplicitUsings>enable</ImplicitUsings>
7-
<RootNamespace>ContosoUniversity</RootNamespace>
87
</PropertyGroup>
98

109
<ItemGroup>
1110
<ProjectReference Include="..\..\src\BlazorWebFormsComponents\BlazorWebFormsComponents.csproj" />
1211
</ItemGroup>
1312

1413
<ItemGroup>
15-
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0-preview.2.25163.2" />
16-
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.0-preview.2.25163.2">
17-
<PrivateAssets>all</PrivateAssets>
18-
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
19-
</PackageReference>
14+
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
15+
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0" />
2016
</ItemGroup>
2117

2218
</Project>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using Microsoft.EntityFrameworkCore;
2+
using ContosoUniversity.Models;
3+
4+
namespace ContosoUniversity.Data;
5+
6+
public class ContosoUniversityContext : DbContext
7+
{
8+
public ContosoUniversityContext(DbContextOptions<ContosoUniversityContext> options)
9+
: base(options)
10+
{
11+
}
12+
13+
public DbSet<Course> Courses => Set<Course>();
14+
public DbSet<Department> Departments => Set<Department>();
15+
public DbSet<Enrollment> Enrollments => Set<Enrollment>();
16+
public DbSet<Instructor> Instructors => Set<Instructor>();
17+
public DbSet<Student> Students => Set<Student>();
18+
19+
protected override void OnModelCreating(ModelBuilder modelBuilder)
20+
{
21+
// Configure relationships
22+
modelBuilder.Entity<Enrollment>()
23+
.HasOne(e => e.Student)
24+
.WithMany(s => s.Enrollments)
25+
.HasForeignKey(e => e.StudentID);
26+
27+
modelBuilder.Entity<Enrollment>()
28+
.HasOne(e => e.Course)
29+
.WithMany(c => c.Enrollments)
30+
.HasForeignKey(e => e.CourseID);
31+
32+
modelBuilder.Entity<Course>()
33+
.HasOne(c => c.Department)
34+
.WithMany(d => d.Courses)
35+
.HasForeignKey(c => c.DepartmentID);
36+
}
37+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using ContosoUniversity.Models;
2+
using Microsoft.EntityFrameworkCore;
3+
4+
namespace ContosoUniversity.Data;
5+
6+
public static class DbInitializer
7+
{
8+
public static async Task InitializeAsync(ContosoUniversityContext context)
9+
{
10+
// Check if already seeded
11+
if (await context.Students.AnyAsync())
12+
return;
13+
14+
// Seed Departments
15+
var departments = new Department[]
16+
{
17+
new() { DepartmentName = "Computer Science", BuildingNumber = 1, ManagingInstructorID = 1 },
18+
new() { DepartmentName = "Mathematics", BuildingNumber = 2, ManagingInstructorID = 2 },
19+
new() { DepartmentName = "Engineering", BuildingNumber = 3, ManagingInstructorID = 3 }
20+
};
21+
await context.Departments.AddRangeAsync(departments);
22+
await context.SaveChangesAsync();
23+
24+
// Seed Instructors
25+
var instructors = new Instructor[]
26+
{
27+
new() { FirstName = "John", LastName = "Smith", BirthDate = new DateTime(1975, 5, 15), Email = "jsmith@contoso.edu" },
28+
new() { FirstName = "Jane", LastName = "Doe", BirthDate = new DateTime(1980, 3, 20), Email = "jdoe@contoso.edu" },
29+
new() { FirstName = "Bob", LastName = "Wilson", BirthDate = new DateTime(1978, 9, 10), Email = "bwilson@contoso.edu" }
30+
};
31+
await context.Instructors.AddRangeAsync(instructors);
32+
await context.SaveChangesAsync();
33+
34+
// Seed Courses
35+
var courses = new Course[]
36+
{
37+
new() { CourseName = "Introduction to Programming", StudentsMax = 30, DepartmentID = 1, InstructorID = 1 },
38+
new() { CourseName = "Data Structures", StudentsMax = 25, DepartmentID = 1, InstructorID = 1 },
39+
new() { CourseName = "Calculus I", StudentsMax = 35, DepartmentID = 2, InstructorID = 2 },
40+
new() { CourseName = "Linear Algebra", StudentsMax = 30, DepartmentID = 2, InstructorID = 2 },
41+
new() { CourseName = "Mechanics", StudentsMax = 20, DepartmentID = 3, InstructorID = 3 }
42+
};
43+
await context.Courses.AddRangeAsync(courses);
44+
await context.SaveChangesAsync();
45+
46+
// Seed Students
47+
var students = new Student[]
48+
{
49+
new() { FirstName = "Alice", LastName = "Johnson", BirthDate = new DateTime(2000, 1, 15), Email = "alice@student.contoso.edu" },
50+
new() { FirstName = "Bob", LastName = "Williams", BirthDate = new DateTime(2001, 4, 22), Email = "bob@student.contoso.edu" },
51+
new() { FirstName = "Carol", LastName = "Brown", BirthDate = new DateTime(2000, 7, 8), Email = "carol@student.contoso.edu" },
52+
new() { FirstName = "David", LastName = "Jones", BirthDate = new DateTime(2001, 11, 30), Email = "david@student.contoso.edu" },
53+
new() { FirstName = "Eve", LastName = "Davis", BirthDate = new DateTime(2002, 2, 14), Email = "eve@student.contoso.edu" }
54+
};
55+
await context.Students.AddRangeAsync(students);
56+
await context.SaveChangesAsync();
57+
58+
// Seed Enrollments
59+
var enrollments = new Enrollment[]
60+
{
61+
new() { StudentID = 1, CourseID = 1, Date = DateTime.Now.AddDays(-30) },
62+
new() { StudentID = 1, CourseID = 3, Date = DateTime.Now.AddDays(-30) },
63+
new() { StudentID = 2, CourseID = 1, Date = DateTime.Now.AddDays(-25) },
64+
new() { StudentID = 2, CourseID = 2, Date = DateTime.Now.AddDays(-25) },
65+
new() { StudentID = 3, CourseID = 3, Date = DateTime.Now.AddDays(-20) },
66+
new() { StudentID = 3, CourseID = 4, Date = DateTime.Now.AddDays(-20) },
67+
new() { StudentID = 4, CourseID = 5, Date = DateTime.Now.AddDays(-15) },
68+
new() { StudentID = 5, CourseID = 1, Date = DateTime.Now.AddDays(-10) },
69+
new() { StudentID = 5, CourseID = 2, Date = DateTime.Now.AddDays(-10) }
70+
};
71+
await context.Enrollments.AddRangeAsync(enrollments);
72+
await context.SaveChangesAsync();
73+
}
74+
}

samples/AfterContosoUniversity/Models/Course.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ namespace ContosoUniversity.Models;
33
public class Course
44
{
55
public int CourseID { get; set; }
6-
public string? CourseName { get; set; }
6+
public string CourseName { get; set; } = string.Empty;
77
public int StudentsMax { get; set; }
88
public int DepartmentID { get; set; }
99
public int InstructorID { get; set; }
1010

1111
public virtual Department? Department { get; set; }
1212
public virtual Instructor? Instructor { get; set; }
13-
public virtual ICollection<Enrollment> Enrollments { get; set; } = new HashSet<Enrollment>();
13+
public virtual ICollection<Enrollment> Enrollments { get; set; } = new List<Enrollment>();
1414
}

samples/AfterContosoUniversity/Models/Department.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ namespace ContosoUniversity.Models;
33
public class Department
44
{
55
public int DepartmentID { get; set; }
6-
public string? DepartmentName { get; set; }
6+
public string DepartmentName { get; set; } = string.Empty;
77
public int BuildingNumber { get; set; }
88
public int ManagingInstructorID { get; set; }
99

10-
public virtual ICollection<Course> Courses { get; set; } = new HashSet<Course>();
10+
public virtual ICollection<Course> Courses { get; set; } = new List<Course>();
1111
}

samples/AfterContosoUniversity/Models/Instructor.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ namespace ContosoUniversity.Models;
33
public class Instructor
44
{
55
public int InstructorID { get; set; }
6-
public string? FirstName { get; set; }
7-
public string? LastName { get; set; }
6+
public string FirstName { get; set; } = string.Empty;
7+
public string LastName { get; set; } = string.Empty;
88
public DateTime BirthDate { get; set; }
9-
public string? Email { get; set; }
9+
public string Email { get; set; } = string.Empty;
1010

11-
public virtual ICollection<Course> Courses { get; set; } = new HashSet<Course>();
11+
public string FullName => $"{FirstName} {LastName}";
12+
13+
public virtual ICollection<Course> Courses { get; set; } = new List<Course>();
1214
}

0 commit comments

Comments
 (0)