| name | cli-transform-authoring |
| description | Create and register new CLI migration transforms for the webforms-to-blazor pipeline. Covers IMarkupTransform and ICodeBehindTransform interfaces, transform ordering, dual-registration in Program.cs and TestHelpers.cs, testing patterns, and when to use a transform vs. a semantic pattern. Use when adding a new markup or code-behind transform, debugging transform output, or understanding transform execution order. |
CLI Transform Authoring
This skill covers creating new transforms for the webforms-to-blazor CLI migration pipeline.
Architecture Overview
The migration CLI uses two transform interfaces that run in sequence during file conversion:
- Markup transforms (
IMarkupTransform) — Convert .aspx/.ascx/.master markup to .razor syntax
- Code-behind transforms (
ICodeBehindTransform) — Convert .aspx.cs code-behind to .razor.cs
Both interfaces share the same contract:
public interface IMarkupTransform
{
string Name { get; }
int Order { get; }
string Apply(string content, FileMetadata metadata);
}
Name — Human-readable identifier (used in logs and diagnostics)
Order — Execution sequence (ascending). Lower numbers run first.
Apply — Takes current content + file metadata, returns transformed content
Pipeline Execution Order
MigrationPipeline runs transforms in this order:
- Markup transforms (sorted by
Order ascending) — all run on .razor content
- Code-behind transforms (sorted by
Order ascending) — all run on .razor.cs content
- Semantic patterns (
SemanticPatternCatalog) — page-level rewrites after all transforms
FileMetadata
Every transform receives FileMetadata which carries context about the file being processed:
OriginalPath — Source .aspx path
OutputPath — Target .razor path
PageDirectives — Parsed directive info (CodeBehind, Inherits, MasterPageFile)
MarkupContent — Can be set by code-behind transforms to modify the markup file
CodeBehindContent — The code-behind source
IsUserControl / IsMasterPage — File type flags
Creating a New Transform
Step 1: Create the Transform Class
src/BlazorWebFormsComponents.Cli/Transforms/Markup/MyNewTransform.cs
— or —
src/BlazorWebFormsComponents.Cli/Transforms/CodeBehind/MyNewTransform.cs
using BlazorWebFormsComponents.Cli.Pipeline;
namespace BlazorWebFormsComponents.Cli.Transforms.Markup;
public class MyNewTransform : IMarkupTransform
{
public string Name => "MyNewTransform";
public int Order => 500;
public string Apply(string content, FileMetadata metadata)
{
if (!content.Contains("pattern-to-match"))
return content;
return content.Replace("old-pattern", "new-pattern");
}
}
Step 2: Register in BOTH Locations
⚠️ CRITICAL: Every transform must be registered in TWO places.
src/BlazorWebFormsComponents.Cli/Program.cs (runtime DI):
services.AddSingleton<IMarkupTransform, MyNewTransform>();
services.AddSingleton<ICodeBehindTransform, MyNewTransform>();
tests/BlazorWebFormsComponents.Cli.Tests/TestHelpers.cs (test pipeline):
var markupTransforms = new List<IMarkupTransform>
{
new MyNewTransform(),
};
var codeBehindTransforms = new List<ICodeBehindTransform>
{
new MyNewTransform(),
};
Step 3: Write Tests
Create a test file at tests/BlazorWebFormsComponents.Cli.Tests/MyNewTransformTests.cs:
using BlazorWebFormsComponents.Cli.Pipeline;
using BlazorWebFormsComponents.Cli.Transforms.Markup;
using Xunit;
namespace BlazorWebFormsComponents.Cli.Tests;
public class MyNewTransformTests
{
private readonly MyNewTransform _transform = new();
[Fact]
public void Apply_WithMatchingPattern_TransformsCorrectly()
{
var input = "<asp:CustomControl runat=\"server\" />";
var metadata = new FileMetadata { OriginalPath = "Test.aspx" };
var result = _transform.Apply(input, metadata);
Assert.Contains("expected-output", result);
Assert.DoesNotContain("asp:", result);
}
[Fact]
public void Apply_WithoutMatchingPattern_ReturnsUnchanged()
{
var input = "<div>No Web Forms here</div>";
var metadata = new FileMetadata { OriginalPath = "Test.aspx" };
var result = _transform.Apply(input, metadata);
Assert.Equal(input, result);
}
}
Step 4: Run Tests
dotnet test tests\BlazorWebFormsComponents.Cli.Tests --nologo
Transform Ordering Guidelines
| Order Range | Category | Examples |
|---|
| 0–99 | Directive stripping | PageDirectiveTransform, RegisterDirectiveTransform |
| 100–199 | Structural transforms | MasterPageTransform, ContentWrapperTransform |
| 200–299 | Prefix/attribute removal | AspPrefixTransform, AttributeStripTransform |
| 300–399 | Expression/binding rewrites | ExpressionTransform, DataBindingAttributeTransform |
| 400–499 | Template/column processing | TemplateFieldChildComponentsTransform, GridViewColumnItemTypeTransform |
| 500–599 | Event/form wiring | EventWiringTransform, FormWrapperTransform |
| 600–699 | URL/reference cleanup | UrlReferenceTransform, ScriptManagerStripTransform |
| 700+ | Post-processing | FormAntiforgeryPostProcessor, ValidatorGenericTypeTransform |
Key ordering dependencies:
AspPrefixTransform must run before AttributeStripTransform (strip asp: before removing runat)
TemplateContextTransform must run before TemplateFieldChildComponentsTransform
SelectMethodTransform must run before EventWiringTransform
Transform vs. Semantic Pattern
| Use a Transform when... | Use a Semantic Pattern when... |
|---|
| Converting a specific Web Forms tag/attribute | Rewriting page-level structure |
| Change is mechanical (regex/string) | Change requires understanding page intent |
| Applies to ALL files universally | Applies only to specific page types (query, detail, action) |
| No context about surrounding markup needed | Needs to understand the full page layout |
Example: stripping runat="server" | Example: converting a search form + GridView into a query page |
Current Transform Inventory
- 24 markup transforms in
src/BlazorWebFormsComponents.Cli/Transforms/Markup/
- 5 directive transforms in
src/BlazorWebFormsComponents.Cli/Transforms/Directives/
- 27 code-behind transforms in
src/BlazorWebFormsComponents.Cli/Transforms/CodeBehind/
Checklist