#Control Flow
XSLT provides conditional instructions that determine what output to produce based on runtime conditions. If you come from C#, you will find familiar concepts here — if, switch, and some patterns that have no direct C# equivalent but solve real problems elegantly.
#Contents
#xsl:if
Simple conditional — if the test expression is true, execute the body. There is no else branch.
<xsl:if test="price > 100">
<span class="premium-badge">Premium</span>
</xsl:if>
C# parallel: if (price > 100) { ... } — except there is no else. For if/else, use xsl:choose.
#Effective Boolean Value
The test attribute evaluates its XPath expression and converts the result to a boolean using the "effective boolean value" (EBV) rules. Understanding EBV is essential for writing correct conditions:
|
Expression Result |
Boolean Value |
Example |
|---|---|---|
|
Empty sequence |
|
|
|
Non-empty sequence |
|
|
|
Empty string |
|
|
|
Non-empty string |
|
|
|
Number |
|
|
|
Any other number |
|
|
|
Boolean |
|
Explicit false |
|
Boolean |
|
Explicit true |
This means you can write very concise existence checks:
<!-- Check if element exists (has children named "description") -->
<xsl:if test="description">
<p><xsl:value-of select="description"/></p>
</xsl:if>
<!-- Check if attribute exists and is non-empty -->
<xsl:if test="@discount">
<span class="discount"><xsl:value-of select="@discount"/>% off</span>
</xsl:if>
<!-- Check if a string value is present -->
<xsl:if test="normalize-space(notes)">
<div class="notes"><xsl:value-of select="notes"/></div>
</xsl:if>
C# parallel: This is similar to C#'s truthiness in pattern matching, but more pervasive. In XSLT, the node-existence test test="description" is the equivalent of C#'s null check if (description != null).
#Common Conditions
<!-- Equality -->
<xsl:if test="@status = 'active'">...</xsl:if>
<!-- Numeric comparison -->
<xsl:if test="price >= 50 and price < 100">...</xsl:if>
<!-- Note: < must be escaped as < in XML attributes -->
<!-- Alternative using not(): -->
<xsl:if test="not(price >= 100) and price >= 50">...</xsl:if>
<!-- Existence check -->
<xsl:if test="@id">...</xsl:if>
<!-- Position-based (inside for-each) -->
<xsl:if test="position() != last()">
<xsl:text>, </xsl:text>
</xsl:if>
<!-- Multiple conditions -->
<xsl:if test="@category = 'electronics' and price > 50">...</xsl:if>
<xsl:if test="@status = 'sale' or @status = 'clearance'">...</xsl:if>
Gotcha: You cannot write <xsl:if test="price < 100"> because < is not valid inside an XML attribute. Use < or reverse the comparison: test="not(price >= 100)" or test="100 > price".
#xsl:choose
Multi-branch conditional — the XSLT equivalent of if / else if / else or switch. This is your go-to when you need more than a simple xsl:if.
<xsl:choose>
<xsl:when test="price > 100">
<span class="tier">Premium</span>
</xsl:when>
<xsl:when test="price > 50">
<span class="tier">Standard</span>
</xsl:when>
<xsl:when test="price > 0">
<span class="tier">Budget</span>
</xsl:when>
<xsl:otherwise>
<span class="tier">Free</span>
</xsl:otherwise>
</xsl:choose>C# parallel:
var tier = price switch
{
> 100 => "Premium",
> 50 => "Standard",
> 0 => "Budget",
_ => "Free"
};#Structure Rules
-
xsl:choosemust contain one or morexsl:whenelements -
xsl:otherwiseis optional and must be last -
The first
xsl:whenwhosetestis true wins — subsequent branches are skipped (just likeelse if) -
If no
xsl:whenmatches and there is noxsl:otherwise, nothing is produced
#Pattern: Value Mapping
<!-- Map status codes to display labels -->
<xsl:choose>
<xsl:when test="@status = 'A'">Active</xsl:when>
<xsl:when test="@status = 'D'">Discontinued</xsl:when>
<xsl:when test="@status = 'P'">Pending Review</xsl:when>
<xsl:otherwise>Unknown (<xsl:value-of select="@status"/>)</xsl:otherwise>
</xsl:choose>#Pattern: Type-Based Processing
<!-- Render different content types -->
<xsl:choose>
<xsl:when test="self::image">
<img src="{@src}" alt="{@alt}"/>
</xsl:when>
<xsl:when test="self::video">
<video src="{@src}" controls="controls"/>
</xsl:when>
<xsl:when test="self::text">
<p><xsl:value-of select="."/></p>
</xsl:when>
</xsl:choose>Note: For type-based processing, template matching (xsl:template match="image", etc.) is usually a better fit. Reserve xsl:choose for conditions that depend on values rather than element names.
#xsl:switch (XSLT 4.0)
XSLT 4.0 introduces xsl:switch — a cleaner syntax for value-based branching. It evaluates a single select expression and matches the result against multiple cases.
<xsl:switch select="@category">
<xsl:when test="'electronics'">
<span class="icon">💻</span>
</xsl:when>
<xsl:when test="'clothing'">
<span class="icon">👕</span>
</xsl:when>
<xsl:when test="'books'">
<span class="icon">📚</span>
</xsl:when>
<xsl:otherwise>
<span class="icon">📦</span>
</xsl:otherwise>
</xsl:switch>The key difference from xsl:choose:
|
|
|
|
|---|---|---|
|
Evaluated |
Each |
|
|
Comparison |
Each test is a full boolean expression |
Each test is a value compared with |
|
Best for |
Complex conditions, range checks |
Simple value matching |
C# parallel: xsl:switch maps directly to C#'s switch statement or switch expression:
var icon = category switch
{
"electronics" => "\U0001F4BB",
"clothing" => "\U0001F455",
"books" => "\U0001F4DA",
_ => "\U0001F4E6"
};#Multiple Values per Case
Each xsl:when can match multiple values if the test expression returns a sequence:
<xsl:switch select="@status">
<xsl:when test="('active', 'enabled')">
<span class="badge-green">Available</span>
</xsl:when>
<xsl:when test="('discontinued', 'disabled')">
<span class="badge-red">Unavailable</span>
</xsl:when>
<xsl:otherwise>
<span class="badge-gray">Unknown</span>
</xsl:otherwise>
</xsl:switch>This is equivalent to C#'s multi-case pattern:
switch (status)
{
case "active":
case "enabled":
// ...
break;
}#xsl:where-populated
Conditionally produces its content only if the content is non-empty. This solves the "conditional wrapper" problem — you want to output a container element only if there is something to put in it.
#The Problem
Consider this common situation: you want to output a <ul> of tags, but only if there are tags:
<!-- Without where-populated: you have to check twice -->
<xsl:if test="tag">
<ul class="tags">
<xsl:for-each select="tag">
<li><xsl:value-of select="."/></li>
</xsl:for-each>
</ul>
</xsl:if>The condition in xsl:if duplicates the logic of the xsl:for-each. If the selection logic is complex, this duplication becomes a maintenance problem.
#The Solution
<xsl:where-populated>
<ul class="tags">
<xsl:for-each select="tag">
<li><xsl:value-of select="."/></li>
</xsl:for-each>
</ul>
</xsl:where-populated>
xsl:where-populated evaluates its content. If the content produces any output (nodes or text), it is emitted. If the content produces nothing, the entire block — including the <ul> wrapper — is suppressed.
#Practical Example
<xsl:template match="product">
<div class="product">
<h3><xsl:value-of select="name"/></h3>
<!-- Only show specs table if there are specs -->
<xsl:where-populated>
<table class="specs">
<xsl:for-each select="spec">
<tr>
<td><xsl:value-of select="@name"/></td>
<td><xsl:value-of select="."/></td>
</tr>
</xsl:for-each>
</table>
</xsl:where-populated>
<!-- Only show related products section if any exist -->
<xsl:where-populated>
<section class="related">
<h4>Related Products</h4>
<xsl:apply-templates select="//product[@id = current()/related/@ref]"/>
</section>
</xsl:where-populated>
</div>
</xsl:template>C# parallel: There is no direct C# equivalent. The closest pattern is a Razor helper that checks for content:
@if (items.Any())
{
<ul>
@foreach (var item in items) { <li>@item</li> }
</ul>
}
xsl:where-populated avoids the need to write the check separately.
#xsl:on-empty and xsl:on-non-empty
These instructions provide fallback or conditional content based on whether the preceding sibling instructions produced any output.
#xsl:on-empty
Produces output only if nothing else in the same sequence constructor produced output. Think of it as a "default" or "fallback."
<xsl:template match="product">
<p class="description">
<xsl:value-of select="description"/>
<xsl:on-empty>No description available.</xsl:on-empty>
</p>
</xsl:template>If the description element is empty or missing, the output is:
<p class="description">No description available.</p>#xsl:on-non-empty
The inverse — produces output only if preceding instructions did produce output. Useful for adding decorative wrappers or separators:
<xsl:template match="product">
<div class="product">
<h3><xsl:value-of select="name"/></h3>
<xsl:apply-templates select="tag"/>
<xsl:on-non-empty>
<hr/>
</xsl:on-non-empty>
<xsl:apply-templates select="review"/>
</div>
</xsl:template>Here, the <hr/> only appears if there were tags. If the product has no tags, no <hr/> is produced.
#Combined Example
<xsl:template match="product">
<div class="product-card">
<h3><xsl:value-of select="name"/></h3>
<div class="tags">
<xsl:for-each select="tag">
<span class="tag"><xsl:value-of select="."/></span>
</xsl:for-each>
<xsl:on-empty>
<span class="no-tags">Uncategorized</span>
</xsl:on-empty>
</div>
</div>
</xsl:template>
C# parallel: The null-coalescing operator ?? or the DefaultIfEmpty() LINQ method:
var displayTags = product.Tags.Any()
? product.Tags
: new[] { "Uncategorized" };#Placement Rules
-
xsl:on-emptyandxsl:on-non-emptymust be the last instructions in their parent sequence constructor -
They evaluate whether everything before them in the same parent produced output
-
They can appear inside
xsl:where-populatedfor layered conditional logic
#Common Patterns
#Combining Conditions with and / or
<!-- Product is on sale AND in stock -->
<xsl:if test="@on-sale = 'true' and stock > 0">
<span class="available-sale">On Sale — In Stock!</span>
</xsl:if>
<!-- Product is either new or featured -->
<xsl:if test="@new = 'true' or @featured = 'true'">
<span class="highlight">Recommended</span>
</xsl:if>#Nested choose
When you need multi-dimensional branching:
<xsl:choose>
<xsl:when test="@category = 'electronics'">
<xsl:choose>
<xsl:when test="price > 500">High-end Electronics</xsl:when>
<xsl:when test="price > 100">Mid-range Electronics</xsl:when>
<xsl:otherwise>Budget Electronics</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:when test="@category = 'clothing'">
<!-- ... -->
</xsl:when>
</xsl:choose>
Tip: Deeply nested xsl:choose is a code smell. Consider extracting inner conditions into separate templates or functions.
#Null-Checking with xsl:if
A common pattern is checking whether a value exists before using it:
<xsl:template match="product">
<div class="product">
<h3><xsl:value-of select="name"/></h3>
<!-- Only show price if it exists -->
<xsl:if test="price">
<p class="price">$<xsl:value-of select="price"/></p>
</xsl:if>
<!-- Only show image if src attribute is present and non-empty -->
<xsl:if test="normalize-space(image/@src)">
<img src="{image/@src}" alt="{name}"/>
</xsl:if>
<!-- Only show discount if it's a positive number -->
<xsl:if test="number(@discount) > 0">
<span class="discount"><xsl:value-of select="@discount"/>% off</span>
</xsl:if>
</div>
</xsl:template>
C# parallel: Null-conditional operators ?. and null-coalescing ??:
// C# null checks
if (product.Price != null)
html += $"<p class='price'>${product.Price}</p>";
if (!string.IsNullOrWhiteSpace(product.Image?.Src))
html += $"<img src='{product.Image.Src}'/>";#Separator Pattern (Comma-Separated Lists)
<xsl:for-each select="tag">
<xsl:if test="position() > 1">
<xsl:text>, </xsl:text>
</xsl:if>
<xsl:value-of select="."/>
</xsl:for-each>
<!-- Output: electronics, sale, new -->Or more simply, use xsl:value-of with separator:
<xsl:value-of select="tag" separator=", "/>