Skip to content

[Blazor] CRUD scaffolder improvements #3177

@javiercn

Description

@javiercn

This is a tracking issue to collect the improvements to the CRUD razor scaffolder.

  • Support persistent component state on the scaffolder
    • On Edit.tt
  • Support adding NotFound
    • ReplaceNavigateTo("notfound") with NavigationManager.NotFound() Blazor CRUD.
    • Add NotFound.razor page to the project like:
      @page "/not-found"
      @layout MainLayout
      
      <h3>Not Found</h3>
      <p>Sorry, the content you are looking for does not exist.</p>
      
    • Add NotFoundPage parameter to the Router (typically on the Routes view.
      <Router ... NotFoundPage="typeof(Pages.NotFound)">
         ...
      </Router>
      
  • Add re-execution middleware with the route to the NotFound.razor page in Program.cs:
            app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
    
  • Fix the scaffolder pattern instances that uses initializers to use null coalescing assignment during OnInitialized.

The generator model lives in https://github.com/dotnet/Scaffolding/tree/main/src/Scaffolding/VS.Web.CG.Mvc/Blazor the templates live in https://github.com/dotnet/Scaffolding/tree/main/src/Scaffolding/VS.Web.CG.Mvc/Templates/Blazor

Also, take a look at https://github.com/dotnet/Scaffolding/tree/main/src/dotnet-scaffolding/dotnet-scaffold-aspnet/Templates/BlazorCrud and https://github.com/dotnet/Scaffolding/tree/main/src/dotnet-scaffolding/dotnet-scaffold-aspnet/Models for places to update.

Update the scaffolder to implement the changes mentioned above and update or create new tests for it. This testing guide should help you

# Testing Guide for Scaffolders in the dotnet/Scaffolding Repository

## Overview

The scaffolding tests in this repository focus on validating that the code generation templates produce the expected output. The tests are primarily template-based integration tests that verify the generated code matches expected patterns and functionality.

## Key Testing Components

### Main Test Classes

The primary test classes for scaffolders are located in the test projects:

1. **`BlazorCrudScaffolderTests`** - Tests for Blazor CRUD scaffolding functionality
2. **`MinimalApiScaffolderTests`** - Tests for Minimal API scaffolding
3. **`MvcScaffolderTests`** - Tests for MVC scaffolding
4. **`IdentityScaffolderTests`** - Tests for Identity scaffolding

### Testing Approach

The tests in this repository follow a pattern of:
1. Setting up a scaffolding model with specific configuration
2. Executing the scaffolder to generate files
3. Verifying the generated content matches expected output

## Example: Testing Changes to Blazor CRUD Scaffolder

Let's walk through testing a change to the Create.tt template where we want to move the model initialization from a property initializer to the `OnInitialized` method.

### Current Template Code
```csharp
[SupplyParameterFromForm]
private <#= modelName #> <#= modelName #> { get; set; } = new();

Proposed Change

[SupplyParameterFromForm]
private <#= modelName #> <#= modelName #> { get; set; }

protected override void OnInitialized()
{
    <#= modelName #> ??= new();
}

Test Examples

Example 1: Updating an Existing Test

When modifying the Create.tt template, you'll need to update the corresponding test in BlazorCrudScaffolderTests:

// In BlazorCrudScaffolderTests.cs
[Fact]
public async Task BlazorCrudScaffolder_GeneratesCreatePage_WithOnInitialized()
{
    // Arrange
    var model = new BlazorCrudModel
    {
        ModelInfo = new ModelInfo
        {
            ModelTypeName = "Product",
            ModelTypePluralName = "Products",
            PrimaryKeyName = "Id",
            ModelProperties = new List<PropertyInfo>
            {
                new PropertyInfo { Name = "Id", Type = "int" },
                new PropertyInfo { Name = "Name", Type = "string" }
            }
        },
        DbContextInfo = new DbContextInfo
        {
            DbContextClassName = "ApplicationDbContext",
            EntitySetVariableName = "Products"
        }
    };

    // Act
    var scaffolder = new BlazorCrudScaffolder();
    var generatedFiles = await scaffolder.GenerateAsync(model);
    var createPageContent = generatedFiles.First(f => f.Name.Contains("Create.razor")).Content;

    // Assert - Check for OnInitialized method presence
    Assert.Contains("protected override void OnInitialized()", createPageContent);
    Assert.Contains("Product ??= new()", createPageContent);
    
    // Assert - Property should not have initializer
    Assert.DoesNotContain("Product { get; set; } = new()", createPageContent);
}

Example 2: Creating a New Test for Null-Coalescing Behavior

Add a new test to verify the null-coalescing assignment behavior:

[Fact]
public async Task BlazorCrudScaffolder_Create_UsesNullCoalescingInOnInitialized()
{
    // Arrange
    var model = CreateTestBlazorCrudModel("Order", "Orders");
    
    // Act
    var scaffolder = new BlazorCrudScaffolder();
    var result = await scaffolder.GenerateAsync(model);
    var createPage = result.GetFileByName("Create.razor");
    
    // Assert multiple scenarios
    // 1. Verify OnInitialized method exists
    Assert.Matches(@"protected\s+override\s+void\s+OnInitialized\(\)", createPage.Content);
    
    // 2. Verify null-coalescing assignment pattern
    Assert.Contains("Order ??= new()", createPage.Content);
    
    // 3. Verify property declaration without initializer
    var propertyPattern = @"\[SupplyParameterFromForm\]\s+private\s+Order\s+Order\s+{\s+get;\s+set;\s+}";
    Assert.Matches(propertyPattern, createPage.Content);
}

Testing Template Changes

Step 1: Identify Affected Tests

For the Create.tt template change, look for tests in:

  • BlazorCrudScaffolderTests that verify Create page generation
  • Any integration tests that validate the full CRUD scaffold output

Step 2: Update Expected Output

The tests often compare against expected string patterns or template outputs. You'll need to:

  1. Update any hardcoded expected strings that contain the old pattern
  2. Modify regex patterns that validate the generated code structure
  3. Update any baseline files if the tests use them for comparison

Step 3: Add Coverage for New Behavior

Create tests that specifically validate your new functionality:

[Theory]
[InlineData("Product", "Products")]
[InlineData("Category", "Categories")]
[InlineData("OrderItem", "OrderItems")]
public async Task BlazorCrudScaffolder_Create_InitializesModelInOnInitialized(string modelName, string pluralName)
{
    // Arrange
    var model = CreateTestBlazorCrudModel(modelName, pluralName);
    
    // Act
    var scaffolder = new BlazorCrudScaffolder();
    var files = await scaffolder.GenerateAsync(model);
    var createFile = files.First(f => f.Path.EndsWith("Create.razor"));
    
    // Assert - Verify the OnInitialized method pattern
    var expectedPattern = $@"protected override void OnInitialized\(\)\s*{{\s*{modelName} \?\?= new\(\);\s*}}";
    Assert.Matches(expectedPattern, createFile.Content);
}

Test Organization Best Practices

  1. Group Related Tests: Keep tests for each template (Create, Edit, Delete, etc.) grouped together
  2. Use Descriptive Names: Test names should clearly indicate what template feature they're testing
  3. Test Edge Cases: Include tests for models with different property types, naming conventions, and namespaces
  4. Verify Template Logic: Test conditional template generation (e.g., required fields, different input types)

Running Tests

After making changes to templates:

  1. Run all BlazorCrudScaffolder tests to ensure no regressions
  2. Run integration tests that use the full scaffolding pipeline
  3. Verify generated code compiles by running build verification tests

Common Test Patterns

Verifying Generated Code Structure

// Test that the generated code has the expected structure
Assert.Contains("@page \"/products/create\"", generatedContent);
Assert.Contains("@inject IDbContextFactory<ApplicationDbContext> DbFactory", generatedContent);
Assert.Contains("<EditForm method=\"post\" Model=\"Product\"", generatedContent);

Testing Template Conditionals

// Test that required fields generate appropriate markup
var modelWithRequiredField = CreateModelWithRequiredProperty();
var result = await scaffolder.GenerateAsync(modelWithRequiredField);
Assert.Contains("aria-required=\"true\"", result.Content);
Assert.Contains("<span class=\"text-danger\">*</span>", result.Content);

Validating Namespace Handling

// Test that namespaces are correctly included
var modelWithNamespace = CreateModelWithCustomNamespace("MyApp.Models");
var result = await scaffolder.GenerateAsync(modelWithNamespace);
Assert.Contains("@using MyApp.Models", result.Content);

Helper Methods for Test Setup

Most test classes include helper methods to create test models:

private BlazorCrudModel CreateTestBlazorCrudModel(string modelName, string pluralName)
{
    return new BlazorCrudModel
    {
        ModelInfo = new ModelInfo
        {
            ModelTypeName = modelName,
            ModelTypePluralName = pluralName,
            PrimaryKeyName = "Id",
            ModelNamespace = "TestApp.Models",
            ModelProperties = new List<PropertyInfo>
            {
                new PropertyInfo { Name = "Id", Type = "int" },
                new PropertyInfo { Name = "Name", Type = "string", HasRequiredAttribute = true },
                new PropertyInfo { Name = "Description", Type = "string" },
                new PropertyInfo { Name = "Price", Type = "decimal" },
                new PropertyInfo { Name = "CreatedDate", Type = "DateTime" }
            }
        },
        DbContextInfo = new DbContextInfo
        {
            DbContextClassName = "ApplicationDbContext",
            DbContextNamespace = "TestApp.Data",
            EntitySetVariableName = pluralName
        }
    };
}

Testing Checklist

When making changes to scaffolding templates:

  • Identify all templates affected by your change
  • Review existing tests for those templates
  • Update tests to match new expected output
  • Add new tests for new functionality
  • Test edge cases (empty models, special characters in names, etc.)
  • Verify generated code compiles
  • Test with different model configurations
  • Ensure backward compatibility where appropriate
  • Run all related test suites
  • Update any documentation or comments in tests

Debugging Test Failures

When tests fail after template changes:

  1. Compare Generated vs Expected: Use test output to see the actual generated content
  2. Check Whitespace: Template changes often affect whitespace/formatting
  3. Verify Regex Patterns: Ensure regex patterns in tests account for variations in generated code
  4. Review Template Logic: Check if conditional logic in templates is being tested properly
  5. Use Debugger: Set breakpoints in test code to inspect generated content

Integration with CI/CD

The tests run automatically in the CI pipeline. Ensure:

  • All tests pass locally before pushing
  • New tests are included in the appropriate test projects
  • Test names follow the existing naming conventions
  • Tests are properly categorized (unit, integration, etc.)

This testing approach ensures that template changes are thoroughly validated and that the generated code meets the expected requirements for all scaffolding operations.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions