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.
72
90%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
This skill covers creating new transforms for the webforms-to-blazor CLI migration pipeline.
The migration CLI uses two transform interfaces that run in sequence during file conversion:
IMarkupTransform) — Convert .aspx/.ascx/.master markup to .razor syntaxICodeBehindTransform) — Convert .aspx.cs code-behind to .razor.csBoth interfaces share the same contract:
public interface IMarkupTransform // (or ICodeBehindTransform)
{
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 contentMigrationPipeline runs transforms in this order:
Order ascending) — all run on .razor contentOrder ascending) — all run on .razor.cs contentSemanticPatternCatalog) — page-level rewrites after all transformsEvery transform receives FileMetadata which carries context about the file being processed:
OriginalPath — Source .aspx pathOutputPath — Target .razor pathPageDirectives — Parsed directive info (CodeBehind, Inherits, MasterPageFile)MarkupContent — Can be set by code-behind transforms to modify the markup fileCodeBehindContent — The code-behind sourceIsUserControl / IsMasterPage — File type flagssrc/BlazorWebFormsComponents.Cli/Transforms/Markup/MyNewTransform.cs
— or —
src/BlazorWebFormsComponents.Cli/Transforms/CodeBehind/MyNewTransform.csusing BlazorWebFormsComponents.Cli.Pipeline;
namespace BlazorWebFormsComponents.Cli.Transforms.Markup; // or .CodeBehind
public class MyNewTransform : IMarkupTransform // or ICodeBehindTransform
{
public string Name => "MyNewTransform";
public int Order => 500; // Choose based on dependencies
public string Apply(string content, FileMetadata metadata)
{
// Return content unchanged if this transform doesn't apply
if (!content.Contains("pattern-to-match"))
return content;
// Apply transformation
return content.Replace("old-pattern", "new-pattern");
}
}⚠️ CRITICAL: Every transform must be registered in TWO places.
src/BlazorWebFormsComponents.Cli/Program.cs (runtime DI):
services.AddSingleton<IMarkupTransform, MyNewTransform>();
// — or —
services.AddSingleton<ICodeBehindTransform, MyNewTransform>();tests/BlazorWebFormsComponents.Cli.Tests/TestHelpers.cs (test pipeline):
var markupTransforms = new List<IMarkupTransform>
{
// ... existing transforms ...
new MyNewTransform(),
};
// — or for code-behind —
var codeBehindTransforms = new List<ICodeBehindTransform>
{
// ... existing transforms ...
new MyNewTransform(),
};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);
}
}dotnet test tests\BlazorWebFormsComponents.Cli.Tests --nologo| 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 TemplateFieldChildComponentsTransformSelectMethodTransform must run before EventWiringTransform| 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 |
src/BlazorWebFormsComponents.Cli/Transforms/Markup/src/BlazorWebFormsComponents.Cli/Transforms/Directives/src/BlazorWebFormsComponents.Cli/Transforms/CodeBehind/IMarkupTransform or ICodeBehindTransformOrder is set appropriately (check ordering dependencies)Program.cs DI containerTestHelpers.cs test pipelinedotnet test tests\BlazorWebFormsComponents.Cli.Tests passesFritzAndFriends/BlazorWebFormsComponents
3154758
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.