Razor Code Management
This article explains how to control Code Management / Merging behaviour for .razor
files when using the Intent.Code.Weaving.Razor
module.
Overview of how it works
The Razor Merger parses .razor
files into an abstract syntax tree and applies code management logic on a node-by-node basis. An individual node on the abstract syntax tree is referred to as a syntax node. Syntax nodes may have one or more children which are also syntax nodes.
The Razor Merger compares the generated content from the template with the existing file (if there is one) on a node-by-node basis. Instructions are used by the Razor Merger for it to determine for a particular syntax node what content it should ignore, replace with content generated by the template or perhaps remove entirely.
Code management instructions
Instructing the Razor Merger on how to treat particular syntax nodes is done using code management instructions in your source code, within Razor syntax, these are instructions like @Intent.Ignore
above elements.
Within @codeblock
directives, the Razor Merger is delegating to the RoslynWeaver, please refer to its article for information on controlling C# Code management behaviour.
Management modes
@Intent.Fully(["<path>"])
- Intent has full control over the particular syntax node, any deviations in the existing file's syntax node are overwritten with the content generated by the template. Descendant syntax nodes can be opted-out of being fully managed having an@Intent.<mode>
instruction applied to them.@Intent.Merge(["<path>"])
- Intent will add and remove Intent generated code for the syntax node but will never remove code which was manually added.@Intent.Ignore(["<path>"])
- Intent must ignore this syntax node and not remove or overwrite it with content generated by the template. Code management instructions on descendant syntax nodes are likewise ignored, i.e. it is not possible to opt-out of being ignored as a descendant.
Each of the above can be suffixed with the following:
Body
- Override only the body mode behaviour of the syntax node, generally body refers to inner syntax nodes or the content of a syntax node.Signature
- Override only the signature mode behaviour of the syntax node, generally this refers to aspects like HTML element / Directive attributes of a syntax node.
Each of the above can optionally have path argument specified which like the MoveHere
instruction can be used to move the element's location from where it was in the generated content.
The following instructions can be used to instruct the code weaver how to manage specific particular attributes on a markup element:
@Intent.FullyAttributes("attribute1", ["attribute2", ...])
- Fully manages the specified attributes.@Intent.MergeAttributes("attribute1", ["attribute2", ...])
- Separates the value of the attribute by space and will merge them. By defaultclass
attributes are in merge mode.@Intent.IgnoreAttributes("attribute1", ["attribute2", ...])
- Ignores the specified attributes.
Management mode examples
In the below, the class
attribute in the <div>
will be ignored, but not the content:
@Intent.Fully()
@Intent.IgnoreAttributes("class")
<div class="content-block">
content
</div>
The next example shows how management modes and their suffixes can be combined. The <div>
will be "Fully" controlled, while its body mode is overridden to be ignored, i.e. the Razor Merger should always update the attributes to match that generated by the template, but it should never update its body ("content" in this case).
@Intent.Fully()
@Intent.IgnoreBody()
<div class="content-block">
content
</div>
The following example shows how with some Razor Components, placing Razor expressions within them will cause a compilation error. In such cases the instructions can be placed in Razor comments:
@* @Intent.Fully() *@
@* @Intent.IgnoreBody() *@
<div class="content-block">
content
</div>
The @
prefix is optional for instructions inside razor comments so the following is equivalent:
@* Intent.Fully() *@
@* Intent.IgnoreBody() *@
<div class="content-block">
content
</div>
InitialGen
instructions
These instructions are useful for having syntax which can be initially generated by a template, and then changed or even removed by users without the Software Factory trying to update or put the it back into the file.
When an InitialGen
instruction is on a syntax node in a template, it is generated during the initial generation (creation) of a file and is then essentially treated as "Ignored" on subsequent generations. Furthermore, if the syntax node is deleted then it won't be re-generated on subsequent updates to the file.
During initial generation of a file, the instruction is removed from syntax node so this instruction will never be visible except to template authors.
The following instructions are available and are applied the same way as normal management instructions:
@Intent.InitialGen()
- All aspects of the syntax are ignored after initial generation.@Intent.InitialGenAttributes()
- Attributes of the syntax are ignored after initial generation.@Intent.InitialGenBody()
- The body of the syntax is ignored after initial generation.@Intent.InitialGenSignature()
- The signature of the syntax are ignored after initial generation.
Intent.Skip("<path1>", ["<path2>", "<path3>", ...])
Will skip over insertion of template content at the specified paths.
For example, you have deleted generated content and the code weaver keeps trying to bring it back:
<PageTitle>My Page</PageTitle>
+ <h1>Page Heading</h1>
+
+ <div id="generated">
+ <p>Generated content</p>
+ </div>
<div id="manually-added">
<p>Manually added content.</p>
</div>
You can add @attribute [Intent.Skip("h1", "/div[@id='generated']")]
to the top of the file and the code weaver will then know to "skip over" insertion of items generated at those paths:
@attribute [Intent.Skip("h1", "/div[@id='generated']")]
<PageTitle>My Page</PageTitle>
<div id="manually-added">
<p>Manually added content.</p>
</div>
Intent.MoveHere("<path>")
Adding an Intent.MoveHere("<path>")
instruction allows moving an element from a completely structurally different location in the generated output at the specified path.
For example, you're wanting to move the <h1>
element in the example below to the outer scope, but the code weaver keeps trying to add it back to where it was:
@page "/page"
<h1>Heading 1</h1>
<div id="main">
+ <h1>Heading 1</h1>
<p>Generated content</p>
</div>
You can add @Intent.MoveHere("/div[@id='main']/h1")
above the h1
element to instruct the code weaver that the element has been moved from elsewhere in the generated output:
@page "/page"
@Intent.MoveHere("/div[@id='main']/h1")
<h1>Heading 1</h1>
<div id="main">
<p>Generated content</p>
</div>
Path syntax
This section discusses the syntax for the path arguments used by management mode, Skip and MoveHere instructions.
The path supports a URL like path separated by forward-slashes (/
), for example, /div/div/h1
would match the <h1>
in the following:
<div>
<div>
<h1>Heading 1</h1>
</div>
</div>
The path syntax supports the following very small subset of XPath:
attribute
Axis
This will match elements with attribute names prefixed with an @
with the specified value, for example the string /div/div[@id='additional']
will match the 2nd nested <div>
in the following:
<div>
<div id="main">
Main content.
</div>
<div id="additional">
Additional content.
</div>
<div id="footer">
Footer content.
</div>
</div>
You can also refer to the MDN documentation on the attribute
Axis for more information.
position
Function
This will match the element at the specified 1-based index position, for example the string /div/div[position()=3]
will match the 3rd div in the following:
<div>
<div>
1st
</div>
<div>
2nd
</div>
<div>
3rd
</div>
</div>
You can also refer to the MDN documentation on the position
Function for more information.
Default code management behaviour
By default, templates are in Merge
mode. This default can be changed in the settings for your application:
Syntax node matching
The Razor Merger applies a heuristic to try "match" syntax nodes in your "existing" file with a corresponding syntax node generated by the template it is merging with.
Matching by identity
There are cases where syntax nodes can't be easily differentiated and the Merger's default match may not be correct, this is particularly common with HTML Elements and Directives which appear numerous times within the same parent syntax node or root of the document. In such cases, an "identity" can be assigned to the syntax node which will force the Razor Merger to correlate the Syntax Node only with syntax nodes with a matching identity.
An "identity" can be assigned to a syntax node using any of the following ways:
- An
id="<identity>"
HTML element attribute - As this is a default attribute for HTML element it can be a "natural" identifier to use on elements if it's present. - An
intent-id="<identity>"
HTML element attribute - This takes precedence over theid
HTML element attribute and is useful for scenarios an HTML element'sid
is not "stable", i.e. if it's expected that the template output'sid
may change based on other factors. - An
@Intent.Id("<identity>")
or@* @Intent.Id("<identity>") *@
or@* Intent.Id("<identity>") *@
instruction above the syntax node - This takes precedence over both of the HTML element attributes and was created with Directives in mind as trying to apply unknown attributes to them causes a compilation error.
Matching by other attribute types
If a syntax node doesn't have an identifier, a match is performed in order by the following attributes:
@bind-Value
@bind
Value
Component specific attribute matching configuration
Module authors can configure matching behaviour for particular element/component tag names by using the ConfigureRazorTagMatchingFor
extension method on any instance of ISoftwareFactoryExecutionContext
.
Tip
The IApplication
interface and the ExecutionContext
property on template base types are two common places which are ISoftwareFactoryExecutionContext
and this extension method can be used.
The extension method takes an Action<IRazorTagMatchingConfiguration>
argument which allows fluent style configuration to occur. The following methods are available:
AllowMatchByDescendant
The tag may be matched by a descendant at the specified path.
Consider you're trying matching MudGrid in the following existing file:
@if (Model is not null)
{
<MudGrid>
<MudItem>
<MudImage id="SomeId" />
</MudItem>
</MudGrid>
}
With the one in the following generated file:
<MudGrid>
<MudItem>
<MudImage id="SomeId" />
</MudItem>
</MudGrid>
We can specify the following in a factory extension in our module:
protected override void OnAfterTemplateRegistrations(IApplication application)
{
application.ConfigureRazorTagMatchingFor("MudGrid", c => c.AllowMatchByDescendant(["MudItem"]));
}
The Razor Weaver will then know that for a MudGrid
that it can consider it matched with a MudGrid
when both have at least one direct descendant under their MudItem
with is considered a match.
AllowMatchByNameOnly
The tag may be matched by its name alone rather than requiring content also be matched.
protected override void OnAfterTemplateRegistrations(IApplication application)
{
application.ConfigureRazorTagMatchingFor("MudDialogProvider", c => c.AllowMatchByNameOnly());
}
AllowMatchByAttributes
The tag may be match by the one or more specified attribute names.
protected override void OnAfterTemplateRegistrations(IApplication application)
{
application.ConfigureRazorTagMatchingFor("MudDialogProvider", c => c.AddTagNameAttributeMatch("@bind-Date", "otherAttribute", ...));
}