Instructions Reference
Variables and Parameters
xsl:variable, xsl:param, static parameters, tunnel parameters, with-param
#Variables and Parameters
Variables and parameters in XSLT look similar to their C# counterparts, but they behave differently in one critical way: XSLT variables are immutable. Once a variable is set, its value cannot change. XSLT follows functional programming: you build new values instead of mutating existing ones.
For C# developers: an
xsl:variablewith a simpleselectvalue corresponds tovar x = 42;, and one with anasattribute corresponds to a typed local, such asint x = 42;. The content form, which builds a temporary tree, corresponds to constructing anXDocument. A template parameter with a defaultselectvalue corresponds to a method parameter with a default value, andrequired="yes"corresponds to a parameter with no default. A global stylesheet parameter works like dependency injection: the calling code supplies configuration values that the stylesheet then uses, similar toXsltArgumentList.AddParam(...)from C#. A static parameter withuse-whencorresponds to#if DEBUGor another preprocessor directive, evaluated at compile time rather than at run time. A tunnel parameter corresponds to an ambient context such asAsyncLocal<T>, to dependency injection reaching deep into a call chain, or to React's Context API. Variable scope follows the same block-scoping rules as C#. Finally, an XSLT variable is conceptuallyconst: you never reassign one, you create a new one.
#Contents
#xsl:variable
Declares a variable: a named, immutable value.
#Using select (Simple Values)
<xsl:variable name="tax-rate" select="0.08"/>
<xsl:variable name="product-name" select="name"/>
<xsl:variable name="total" select="price * (1 + $tax-rate)"/>
<xsl:variable name="all-products" select="//product"/>
The select attribute evaluates an XPath expression and binds the result to the variable name.
#Using Content (Constructing Nodes)
When you need to build a temporary tree (a document fragment), use the content form:
<xsl:variable name="product-card">
<div class="card">
<h3><xsl:value-of select="name"/></h3>
<p>$<xsl:value-of select="price"/></p>
</div>
</xsl:variable>
<!-- Use it later -->
<xsl:copy-of select="$product-card"/>
Important: when you use the content form without an as attribute, the variable holds a temporary tree (document node). You can read from it, but the nodes in it are detached from the original document. They have no parent, no siblings, and no relationship to the source tree.
#The as Attribute (Typing)
The as attribute declares the expected type. This attribute is optional, but recommended for clarity and early error detection:
<xsl:variable name="price" as="xs:decimal" select="price"/>
<xsl:variable name="product-names" as="xs:string*" select="//product/name"/>
<xsl:variable name="active" as="xs:boolean" select="@status = 'active'"/>
<xsl:variable name="items" as="element(product)*" select="//product[@category = 'electronics']"/>
If the actual value does not match the declared type, the processor raises a type error, similar to a compile-time error in C#.
C# parallel:
|
XSLT |
C# |
|---|---|
|
|
|
|
|
|
|
Content form (temporary tree) |
|
#select vs. Content: When to Use Which
|
Scenario |
Use |
|---|---|
|
Simple value (string, number, boolean) |
|
|
Node reference from source document |
|
|
Computed XPath expression |
|
|
Constructing new XML nodes |
Content form |
|
Multi-step construction with conditionals |
Content form |
#xsl:param
Declares a parameter: a variable whose value the caller can supply. Parameters appear in three places.
#Template Parameters
<xsl:template match="product">
<xsl:param name="show-price" as="xs:boolean" select="true()"/>
<xsl:param name="currency" as="xs:string" select="'USD'"/>
<div class="product">
<h3><xsl:value-of select="name"/></h3>
<xsl:if test="$show-price">
<p><xsl:value-of select="$currency"/>
<xsl:text> </xsl:text>
<xsl:value-of select="format-number(price, '#,##0.00')"/></p>
</xsl:if>
</div>
</xsl:template>
The select attribute provides a default value. If the caller does not pass the parameter, the template uses the default.
C# parallel: Method parameters with default values:
void RenderProduct(Product p, bool showPrice = true, string currency = "USD") { ... }
#Required Parameters
Use required="yes" to force the caller to supply a value:
<xsl:template name="format-currency">
<xsl:param name="amount" as="xs:decimal" required="yes"/>
<xsl:param name="currency" as="xs:string" required="yes"/>
<xsl:value-of select="concat($currency, ' ', format-number($amount, '#,##0.00'))"/>
</xsl:template>
If the caller omits a required parameter, the processor raises an error. This behavior is equivalent to a C# method parameter without a default value.
#Stylesheet Parameters (Global)
Parameters declared as children of xsl:stylesheet are global: the calling application can set them.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3.0">
<xsl:param name="output-format" as="xs:string" select="'html'"/>
<xsl:param name="debug" as="xs:boolean" select="false()"/>
<xsl:param name="base-url" as="xs:string" select="'https://example.com'"/>
<!-- These can be set from C#: -->
<!-- transformer.SetParameter("output-format", "pdf"); -->
<!-- transformer.SetParameter("debug", true); -->
</xsl:stylesheet>
C# parallel: Stylesheet parameters are like dependency injection — the calling code provides configuration values that the stylesheet uses.
// In C#, setting XSLT parameters:
var transformer = new XslCompiledTransform();
transformer.Load("catalog.xslt");
var args = new XsltArgumentList();
args.AddParam("output-format", "", "pdf");
args.AddParam("debug", "", true);
transformer.Transform(input, args, output);
#xsl:with-param
Passes a parameter value when calling a template or applying templates.
#With call-template
<xsl:call-template name="format-currency">
<xsl:with-param name="amount" select="price"/>
<xsl:with-param name="currency" select="'EUR'"/>
</xsl:call-template>
#With apply-templates
<xsl:apply-templates select="product">
<xsl:with-param name="show-price" select="false()"/>
<xsl:with-param name="currency" select="'GBP'"/>
</xsl:apply-templates>
#Content Form
Like xsl:variable, you can use content instead of select:
<xsl:apply-templates select="product">
<xsl:with-param name="header">
<h2>Featured Products</h2>
</xsl:with-param>
</xsl:apply-templates>
C# parallel: Named arguments in method calls:
RenderProduct(product, showPrice: false, currency: "GBP");
#Static Parameters
XSLT 3.0 introduces static parameters with static="yes". The processor evaluates these at compile time, which enables conditional compilation.
<xsl:param name="debug" as="xs:boolean" select="false()" static="yes"/>
<xsl:param name="edition" as="xs:string" select="'standard'" static="yes"/>
#Compile-Time Conditional: use-when
Static parameters are used primarily with the use-when attribute, which conditionally includes or excludes declarations at compile time:
<xsl:param name="debug" as="xs:boolean" select="false()" static="yes"/>
<!-- This template only exists when debug is true -->
<xsl:template match="product" use-when="$debug">
<xsl:message select="concat('Processing product: ', @id)"/>
<xsl:next-match/>
</xsl:template>
<!-- Include verbose error checking only in debug mode -->
<xsl:template match="product[not(@id)]" use-when="$debug">
<xsl:message terminate="yes">Product missing @id attribute!</xsl:message>
</xsl:template>
#Feature Toggles
<xsl:param name="enable-analytics" as="xs:boolean" select="false()" static="yes"/>
<xsl:param name="theme" as="xs:string" select="'light'" static="yes"/>
<xsl:template match="/">
<html>
<head>
<!-- Conditionally include analytics script -->
<script src="analytics.js" use-when="$enable-analytics"/>
</head>
<body class="{$theme}-theme">
<xsl:apply-templates/>
</body>
</html>
</xsl:template>
C# parallel: #if DEBUG / #if FEATURE_FLAG preprocessor directives, or feature flags evaluated at startup:
#if DEBUG
Console.WriteLine($"Processing product: {product.Id}");
#endif
Key difference: the processor resolves static parameters when it compiles the stylesheet, not when the stylesheet runs. Changing a static parameter requires recompiling the stylesheet.
#Tunnel Parameters
Every deep call chain creates the same problem: how do you pass a value through multiple layers of templates? Tunnel parameters solve this problem without adding the parameter to every intermediate template.
#The Problem
Without tunnel parameters, passing a value through a deep template chain requires every template in the chain to declare and forward the parameter:
<!-- Without tunneling: every template must explicitly forward the parameter -->
<xsl:template match="catalog">
<xsl:param name="currency"/>
<xsl:apply-templates select="category">
<xsl:with-param name="currency" select="$currency"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="category">
<xsl:param name="currency"/> <!-- must declare even though it doesn't use it -->
<xsl:apply-templates select="product">
<xsl:with-param name="currency" select="$currency"/> <!-- must forward -->
</xsl:apply-templates>
</xsl:template>
<xsl:template match="product">
<xsl:param name="currency"/> <!-- finally gets to use it -->
<span><xsl:value-of select="concat($currency, ' ', price)"/></span>
</xsl:template>
#The Solution
With tunnel="yes", intermediate templates do not need to know about the parameter:
<!-- With tunneling: only the sender and receiver mention it -->
<xsl:template match="catalog">
<xsl:apply-templates select="category">
<xsl:with-param name="currency" select="'EUR'" tunnel="yes"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="category">
<!-- No mention of currency here — it tunnels through automatically -->
<div class="category">
<h2><xsl:value-of select="name"/></h2>
<xsl:apply-templates select="product"/>
</div>
</xsl:template>
<xsl:template match="product">
<xsl:param name="currency" tunnel="yes"/> <!-- receives the tunneled value -->
<span><xsl:value-of select="concat($currency, ' ', price)"/></span>
</xsl:template>
The currency parameter passes invisibly through the category template.
#Rules
-
The sender must specify
tunnel="yes"onxsl:with-param. -
The receiver must specify
tunnel="yes"onxsl:param. -
Intermediate templates do not need to declare or forward the parameter.
-
Tunnel parameters and regular parameters are separate. You can have a tunnel parameter and a regular parameter with the same name.
-
Tunnel parameters propagate through
xsl:apply-templatesandxsl:call-template.
The following C# code shows the same ambient-context shape, using AsyncLocal<T>:
// C# ambient context (similar concept)
public static class CurrencyContext
{
private static readonly AsyncLocal<string> _currency = new();
public static string Currency
{
get => _currency.Value ?? "USD";
set => _currency.Value = value;
}
}
#Variable Scope and Shadowing
#Scope
A variable is visible from its declaration to the end of its enclosing block:
<xsl:template match="product">
<!-- $name is visible from here to the end of the template -->
<xsl:variable name="name" select="name"/>
<xsl:if test="@on-sale = 'true'">
<!-- $discount is only visible inside this xsl:if -->
<xsl:variable name="discount" select="@discount"/>
<span>Sale: <xsl:value-of select="$discount"/>% off</span>
</xsl:if>
<!-- $discount is NOT visible here — it is out of scope -->
<p><xsl:value-of select="$name"/></p>
</xsl:template>
#Shadowing
You can declare a variable with the same name as an outer variable. The inner declaration shadows the outer one:
<xsl:variable name="label" select="'default'"/>
<xsl:for-each select="product">
<!-- This shadows the outer $label -->
<xsl:variable name="label" select="name"/>
<span><xsl:value-of select="$label"/></span> <!-- uses the inner $label -->
</xsl:for-each>
<!-- Back to the outer $label -->
<p><xsl:value-of select="$label"/></p> <!-- "default" -->
Note: shadowing is legal, but it can confuse readers. Many XSLT processors emit warnings for shadowed variables.
#Top-Level vs. Local Variables
Variables declared as children of xsl:stylesheet are global:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3.0">
<!-- Global variable — visible in all templates -->
<xsl:variable name="site-title" select="'Product Catalog'"/>
<xsl:variable name="product-count" select="count(//product)"/>
<xsl:template match="/">
<h1><xsl:value-of select="$site-title"/></h1>
<p><xsl:value-of select="$product-count"/> products</p>
</xsl:template>
</xsl:stylesheet>
The processor evaluates global variables lazily: it computes each one the first time a template references it.
#Working with Immutable Variables
Coming from C#, the biggest adjustment is that you cannot reassign variables. There is no $counter = $counter + 1. The following patterns replace mutable state.
#Pattern 1: Compute Everything in One Expression
Instead of building a value incrementally, compute it in a single XPath expression:
<!-- C# thinking (won't work in XSLT):
var total = 0;
foreach (var p in products) total += p.Price;
-->
<!-- XSLT: compute in one expression -->
<xsl:variable name="total" select="sum(//product/price)"/>
<xsl:variable name="avg" select="avg(//product/price)"/>
<xsl:variable name="expensive" select="//product[price > 100]"/>
#Pattern 2: Chained Variables
When you need multi-step computation, use a chain of variables where each builds on the previous one:
<xsl:variable name="raw-price" select="price"/>
<xsl:variable name="discounted" select="$raw-price * (1 - @discount div 100)"/>
<xsl:variable name="with-tax" select="$discounted * 1.08"/>
<xsl:variable name="formatted" select="format-number($with-tax, '$#,##0.00')"/>
<span class="price"><xsl:value-of select="$formatted"/></span>
This pattern resembles a LINQ pipeline, or a series of let clauses:
var formatted = product.Price
.Apply(p => p * (1 - product.Discount / 100m))
.Apply(p => p * 1.08m)
.Apply(p => p.ToString("$#,##0.00"));
#Pattern 3: xsl:iterate for Running Totals
When you genuinely need a running accumulator, like a foreach with mutable state, use xsl:iterate:
<!-- Running total of prices -->
<xsl:iterate select="//product">
<xsl:param name="running-total" as="xs:decimal" select="0"/>
<tr>
<td><xsl:value-of select="name"/></td>
<td><xsl:value-of select="price"/></td>
<td><xsl:value-of select="$running-total + price"/></td>
</tr>
<xsl:next-iteration>
<xsl:with-param name="running-total" select="$running-total + price"/>
</xsl:next-iteration>
</xsl:iterate>
See Iteration and Sorting for full coverage of xsl:iterate.
#Pattern 4: Recursive Functions
For algorithms that need mutable state in C#, XSLT uses recursion:
<xsl:function name="my:factorial" as="xs:integer">
<xsl:param name="n" as="xs:integer"/>
<xsl:sequence select="if ($n le 1) then 1 else $n * my:factorial($n - 1)"/>
</xsl:function>