Biml Utility Methods
When your Biml solution grows beyond a few files, you'll notice code repetition. The same logging task appears in multiple packages. The same variable definitions show up everywhere. The same pattern gets copied across projects.
The Don't Repeat Yourself (DRY) principle applies to Biml: if you copy-paste code more than twice, refactor it for reuse. Biml provides four mechanisms for code reuse:
- Include files: Copy code fragments at compile time
- CallBimlScript: Pass parameters to reusable templates
- CallBimlScriptWithOutput: Return data from templates back to callers
- Helper classes and extension methods: Create reusable C#/VB functions
This guide shows you how to use each method with practical examples.
Include Files
Include files work like automated copy-paste. The compiler replaces the include directive with the contents of the referenced file.
Syntax
<#@ include file="path/to/file.biml" #>
How It Works
The included file must contain valid code for its insertion point. If you include a file inside <Variables>, it must contain only <Variable> elements.
Package.biml (main file):
<Biml xmlns="http://schemas.varigence.com/biml.xsd">
<Packages>
<Package Name="MyPackage">
<Variables>
<#@ include file="StandardVariables.biml" #>
</Variables>
<Tasks>
<!-- Package tasks -->
</Tasks>
</Package>
</Packages>
</Biml>
StandardVariables.biml (included fragment):
<Variable Name="RowCount" DataType="Int32">0</Variable>
<Variable Name="ErrorCount" DataType="Int32">0</Variable>
<Variable Name="LoadDateTime" DataType="DateTime" EvaluateAsExpression="true">GETDATE()</Variable>
Compiled result:
<Biml xmlns="http://schemas.varigence.com/biml.xsd">
<Packages>
<Package Name="MyPackage">
<Variables>
<Variable Name="RowCount" DataType="Int32">0</Variable>
<Variable Name="ErrorCount" DataType="Int32">0</Variable>
<Variable Name="LoadDateTime" DataType="DateTime" EvaluateAsExpression="true">GETDATE()</Variable>
</Variables>
<Tasks>
<!-- Package tasks -->
</Tasks>
</Package>
</Packages>
</Biml>
Supported File Types
Include files aren't limited to .biml:
| Extension | Use Case |
|---|---|
.biml | Biml XML fragments |
.sql | SQL scripts (edit in SSMS with IntelliSense) |
.cs | C# code blocks |
.vb | VB code blocks |
.txt | Any text content |
Example: Reusable Logging Task
Create a logging task once, include it in every package:
LogRows.biml:
<ExecuteSQL Name="LogRowCount" ConnectionName="Admin">
<DirectInput>
INSERT INTO dbo.ETLLog (PackageName, RowCount, LoadTime)
VALUES (?, ?, GETDATE())
</DirectInput>
<Parameters>
<Parameter Name="0" VariableName="System.PackageName" DataType="AnsiString" Length="-1" />
<Parameter Name="1" VariableName="User.RowCount" DataType="Int32" Length="-1" />
</Parameters>
</ExecuteSQL>
LoadPackage.biml:
<#@ template tier="20" #>
<Biml xmlns="http://schemas.varigence.com/biml.xsd">
<Packages>
<# foreach (var table in RootNode.Tables) { #>
<Package Name="Load_<#= table.Name #>" ConstraintMode="Linear">
<Variables>
<Variable Name="RowCount" DataType="Int32">0</Variable>
</Variables>
<Tasks>
<Dataflow Name="LoadData">
<Transformations>
<OleDbSource Name="Source" ConnectionName="Source">
<DirectInput>SELECT * FROM [<#= table.Schema.Name #>].[<#= table.Name #>]</DirectInput>
</OleDbSource>
<RowCount Name="CountRows" VariableName="User.RowCount" />
<OleDbDestination Name="Target" ConnectionName="Target">
<ExternalTableOutput Table="[stg].[<#= table.Name #>]" />
</OleDbDestination>
</Transformations>
</Dataflow>
<#@ include file="LogRows.biml" #>
</Tasks>
</Package>
<# } #>
</Packages>
</Biml>
Benefits:
- Change logging logic once, all packages update on rebuild
- Consistent logging across all packages
- Reduces copy-paste errors
When to Use Include Files
- Static code fragments that don't need parameters
- SQL scripts maintained separately
- Standard variables or tasks used across packages
CallBimlScript
CallBimlScript passes parameters to reusable template files, similar to calling a stored procedure. The callee file can use parameters to control logic and return different code based on input.
Syntax
Caller:
<#= CallBimlScript("Template.biml", param1, param2) #>
Callee (Template.biml):
<#@ property name="param1" type="String" required="true" #>
<#@ property name="param2" type="Boolean" #>
How It Works
- Define properties in the callee file using
<#@ property ... #>directives - Call the file using
CallBimlScript()in an expression block - Pass parameters in the same order as properties are defined
- The callee returns Biml XML that replaces the
CallBimlScript()call
Example: Parameterized Variable Template
VariableTemplate.biml (callee):
<#@ property name="usePrefix" type="Boolean" required="true" #>
<#@ property name="prefix" type="String" #>
<Variable Name="<#= usePrefix ? prefix : "" #>RowCount" DataType="Int32">0</Variable>
<Variable Name="<#= usePrefix ? prefix : "" #>ErrorCount" DataType="Int32">0</Variable>
Caller.biml:
<Biml xmlns="http://schemas.varigence.com/biml.xsd">
<Packages>
<Package Name="WithPrefix">
<Variables>
<#= CallBimlScript("VariableTemplate.biml", true, "Stg_") #>
</Variables>
</Package>
<Package Name="WithoutPrefix">
<Variables>
<#= CallBimlScript("VariableTemplate.biml", false) #>
</Variables>
</Package>
</Packages>
</Biml>
Compiled result:
<Biml xmlns="http://schemas.varigence.com/biml.xsd">
<Packages>
<Package Name="WithPrefix">
<Variables>
<Variable Name="Stg_RowCount" DataType="Int32">0</Variable>
<Variable Name="Stg_ErrorCount" DataType="Int32">0</Variable>
</Variables>
</Package>
<Package Name="WithoutPrefix">
<Variables>
<Variable Name="RowCount" DataType="Int32">0</Variable>
<Variable Name="ErrorCount" DataType="Int32">0</Variable>
</Variables>
</Package>
</Packages>
</Biml>
Passing Complex Objects
You can pass any object type, including Biml AST nodes:
PackageTemplate.biml (callee):
<#@ property name="table" type="AstTableNode" #>
<#@ property name="sourceConnection" type="String" #>
<#@ property name="targetConnection" type="String" #>
<#@ property name="includeLogging" type="Boolean" #>
<Package Name="Load_<#= table.Name #>" ConstraintMode="Linear">
<Variables>
<Variable Name="RowCount" DataType="Int32">0</Variable>
</Variables>
<Tasks>
<ExecuteSQL Name="Truncate" ConnectionName="<#= targetConnection #>">
<DirectInput>TRUNCATE TABLE [stg].[<#= table.Name #>]</DirectInput>
</ExecuteSQL>
<Dataflow Name="LoadData">
<Transformations>
<OleDbSource Name="Source" ConnectionName="<#= sourceConnection #>">
<DirectInput>SELECT * FROM [<#= table.Schema.Name #>].[<#= table.Name #>]</DirectInput>
</OleDbSource>
<RowCount Name="CountRows" VariableName="User.RowCount" />
<OleDbDestination Name="Target" ConnectionName="<#= targetConnection #>">
<ExternalTableOutput Table="[stg].[<#= table.Name #>]" />
</OleDbDestination>
</Transformations>
</Dataflow>
<# if (includeLogging) { #>
<ExecuteSQL Name="LogLoad" ConnectionName="Admin">
<DirectInput>INSERT INTO dbo.LoadLog (TableName, RowCount) VALUES ('<#= table.Name #>', ?)</DirectInput>
<Parameters>
<Parameter Name="0" VariableName="User.RowCount" DataType="Int32" />
</Parameters>
</ExecuteSQL>
<# } #>
</Tasks>
</Package>
Caller.biml:
<#@ template tier="30" #>
<Biml xmlns="http://schemas.varigence.com/biml.xsd">
<Packages>
<# foreach (var table in RootNode.Tables) { #>
<#= CallBimlScript(
"PackageTemplate.biml",
table, /* AstTableNode */
"Source", /* sourceConnection */
"Target", /* targetConnection */
true /* includeLogging */
) #>
<# } #>
</Packages>
</Biml>
When to Use CallBimlScript
- Reusable patterns that need different parameters per call
- Package templates applied to multiple tables
- Conditional code generation based on input
CallBimlScriptWithOutput
CallBimlScriptWithOutput extends CallBimlScript by returning a dynamic object from the callee. Use it when the callee needs to communicate data back to the caller beyond the generated Biml XML.
Syntax
Caller:
<# dynamic outputObject; #>
<#= CallBimlScriptWithOutput("Template.biml", out outputObject, param1) #>
<!-- Now use outputObject.PropertyName -->
Callee:
<#@ property name="param1" type="AstTableNode" #>
<!-- Biml XML output -->
<#
CustomOutput.ColumnCount = param1.Columns.Count;
CustomOutput.HasIdentity = param1.Columns.Any(c => c.IsIdentity);
#>
How It Works
- Declare a
dynamicvariable in the caller - Pass it with
outas the second parameter toCallBimlScriptWithOutput - In the callee, assign properties to the built-in
CustomOutputobject - After the call, access properties on your output variable
Example: Return Metadata from Callee
AnalyzeTable.biml (callee):
<#@ property name="table" type="AstTableNode" #>
<!-- Return a comment with table info -->
<!-- Table: <#= table.Name #> -->
<#
// Set properties on CustomOutput to return to caller
CustomOutput.ColumnCount = table.Columns.Count;
CustomOutput.TableDescription = $"{table.Name} has {table.Columns.Count} columns";
CustomOutput.HasAuditColumns = table.Columns.Any(c => c.Name == "LoadDateTime" || c.Name == "ModifiedDate");
#>
Caller.biml:
<#@ template tier="20" #>
<Biml xmlns="http://schemas.varigence.com/biml.xsd">
<# foreach (var table in RootNode.Tables) {
dynamic tableInfo;
#>
<#= CallBimlScriptWithOutput("AnalyzeTable.biml", out tableInfo, table) #>
<# if (tableInfo.ColumnCount > 50) { #>
<!-- Warning: <#= tableInfo.TableDescription #> - consider partitioning -->
<# } #>
<# if (!tableInfo.HasAuditColumns) { #>
<!-- Note: <#= table.Name #> missing audit columns -->
<# } #>
<# } #>
</Biml>
Use Cases
- Return validation results from a template
- Pass computed metadata between compilation stages
- Determine control flow paths based on callee analysis
Helper Classes and Methods
For complex logic that doesn't fit in code nuggets, create helper classes with reusable methods. There are three approaches:
Approach 1: Inline Class Feature Control Blocks
Define methods directly in your Biml file using <#+ ... #>:
<Biml xmlns="http://schemas.varigence.com/biml.xsd">
<# foreach (var table in RootNode.Tables) { #>
<# if (HasStagingAnnotation(table)) { #>
<!-- Generate staging package for <#= table.Name #> -->
<# } #>
<# } #>
</Biml>
<#+
public static bool HasStagingAnnotation(AstTableNode table)
{
return table.GetTag("LoadType") == "Staging";
}
#>
Limitation: Methods only available in that one file.
Approach 2: Included Files with Class Feature Blocks
Move the class feature block to a separate file and include it:
Helpers.biml:
<#+
public static class BimlHelpers
{
public static bool TagExists(AstNode node, string tagName)
{
return node.GetTag(tagName) != "" || node.ObjectTag.ContainsKey(tagName);
}
public static string SafeSchemaName(AstTableNode table)
{
return table.Schema?.Name ?? "dbo";
}
}
#>
MainFile.biml:
<#@ include file="Helpers.biml" #>
<Biml xmlns="http://schemas.varigence.com/biml.xsd">
<# foreach (var table in RootNode.Tables) { #>
<# if (BimlHelpers.TagExists(table, "IncludeInStaging")) { #>
<!-- Process table from schema: <#= BimlHelpers.SafeSchemaName(table) #> -->
<# } #>
<# } #>
</Biml>
Approach 3: External Code Files (Recommended)
Move helper code to .cs or .vb files for better maintainability:
BimlHelpers.cs:
public static class BimlHelpers
{
public static bool TagExists(AstNode node, string tagName)
{
return node.GetTag(tagName) != "" || node.ObjectTag.ContainsKey(tagName);
}
public static string GetSourceQuery(AstTableNode table)
{
var columns = string.Join(", ", table.Columns.Select(c => $"[{c.Name}]"));
return $"SELECT {columns} FROM [{table.Schema.Name}].[{table.Name}]";
}
}
MainFile.biml:
<#@ code file="BimlHelpers.cs" #>
<Biml xmlns="http://schemas.varigence.com/biml.xsd">
<Packages>
<# foreach (var table in RootNode.Tables.Where(t => BimlHelpers.TagExists(t, "LoadType"))) { #>
<Package Name="Load_<#= table.Name #>">
<Tasks>
<Dataflow Name="LoadData">
<Transformations>
<OleDbSource Name="Source" ConnectionName="Source">
<DirectInput><#= BimlHelpers.GetSourceQuery(table) #></DirectInput>
</OleDbSource>
</Transformations>
</Dataflow>
</Tasks>
</Package>
<# } #>
</Packages>
</Biml>
Benefits of code files:
- Full IDE support (IntelliSense, debugging)
- Version control friendly
- Reusable across multiple Biml projects
- Unit testable
Extension Methods
Extension methods make helper methods appear as if they belong to the object itself. Instead of BimlHelpers.TagExists(table, "LoadType"), you write table.TagExists("LoadType").
Creating an Extension Method
Add the this keyword before the first parameter:
BimlExtensions.cs:
public static class BimlExtensions
{
// Extension method - note 'this' before first parameter
public static bool TagExists(this AstNode node, string tagName)
{
return node.GetTag(tagName) != "" || node.ObjectTag.ContainsKey(tagName);
}
public static string GetColumnList(this AstTableNode table)
{
return string.Join(", ", table.Columns.Select(c => $"[{c.Name}]"));
}
public static bool HasColumn(this AstTableNode table, string columnName)
{
return table.Columns.Any(c => c.Name.Equals(columnName, StringComparison.OrdinalIgnoreCase));
}
}
Usage in Biml:
<#@ code file="BimlExtensions.cs" #>
<Biml xmlns="http://schemas.varigence.com/biml.xsd">
<Packages>
<# foreach (var table in RootNode.Tables.Where(t => t.TagExists("LoadType"))) { #>
<Package Name="Load_<#= table.Name #>">
<Tasks>
<Dataflow Name="LoadData">
<Transformations>
<OleDbSource Name="Source" ConnectionName="Source">
<DirectInput>SELECT <#= table.GetColumnList() #> FROM [<#= table.Schema.Name #>].[<#= table.Name #>]</DirectInput>
</OleDbSource>
<# if (table.HasColumn("ModifiedDate")) { #>
<ConditionalSplit Name="SplitByDate">
<OutputPaths>
<OutputPath Name="Recent">
<Expression>ModifiedDate > DATEADD("d", -7, GETDATE())</Expression>
</OutputPath>
</OutputPaths>
</ConditionalSplit>
<# } #>
</Transformations>
</Dataflow>
</Tasks>
</Package>
<# } #>
</Packages>
</Biml>
Built-in Extension Methods
Biml includes several extension methods you've likely already used:
| Method | Type | Description |
|---|---|---|
GetBiml() | AstNode | Returns Biml XML representation |
GetDatabaseSchema() | Connection | Imports tables from database |
GetDropAndCreateDdl() | AstTableNode | Generates DDL script |
GetTag() | AstNode | Retrieves annotation value |
VB Extension Methods
For VB, use the <Extension()> attribute:
BimlExtensions.vb:
Imports System.Runtime.CompilerServices
Imports Varigence.Languages.Biml.Table
Module BimlExtensions
<Extension()>
Public Function HasAuditColumns(table As AstTableNode) As Boolean
Return table.Columns.Any(Function(c) c.Name = "LoadDateTime" OrElse c.Name = "CreatedDate")
End Function
End Module
Decision Guide: Which Method to Use
| Scenario | Recommended Method | Why |
|---|---|---|
| Same code fragment everywhere | Include | Simple, no parameters needed |
| SQL script maintained in SSMS | Include (.sql file) | Edit with SQL IntelliSense |
| Template with different inputs | CallBimlScript | Pass table, connection, flags |
| Need metadata back from template | CallBimlScriptWithOutput | Return column counts, validation |
| Complex logic in one file | Inline class feature block | Quick, no external files |
| Shared logic across files | Code file | Reusable, IDE support |
| Cleaner calling syntax | Extension method | table.Method() vs Helper.Method(table) |
Combining Methods
Use multiple methods together:
<#@ code file="BimlExtensions.cs" #>
<#@ include file="StandardConnections.biml" #>
<Biml xmlns="http://schemas.varigence.com/biml.xsd">
<Packages>
<# foreach (var table in RootNode.Tables.Where(t => t.TagExists("Staging"))) { #>
<#= CallBimlScript("LoadPackageTemplate.biml", table, "Source", "Target") #>
<# } #>
</Packages>
</Biml>
Platform Considerations
SSIS (BimlExpress/BimlStudio)
All utility methods are fully supported:
- Include files compile at build time
- Code files reference .NET assemblies
- Extension methods work with all Biml AST types
BimlFlex (ADF, Databricks, Snowflake, Fabric)
BimlFlex handles most code reuse through metadata and extension points:
- Extension Points: Use utility methods within custom code blocks
- Metadata-driven: Table/column attributes replace many CallBimlScript scenarios
- Custom components: Helper classes useful for complex transformations
CI/CD Integration
External code files (.cs, .vb) integrate well with CI/CD:
- Version control alongside Biml files
- Code review standard development practices
- Unit testable outside of Biml compilation
Common Mistakes and Fixes
Include File Context Errors
Problem: Included content doesn't match insertion point.
<!-- ERROR: Including full package inside <Tasks> -->
<Tasks>
<#@ include file="FullPackage.biml" #> <!-- Wrong! -->
</Tasks>
Fix: Ensure included content matches the context. Include only <Task> elements inside <Tasks>.
CallBimlScript Parameter Order
Problem: Parameters passed in wrong order.
<!-- Callee expects: table, connectionName, includeLogging -->
<#= CallBimlScript("Template.biml", "Source", table, true) #> <!-- Wrong order! -->
Fix: Match parameter order to property declaration order in callee.
Extension Method Missing 'this'
Problem: Method doesn't appear on objects.
// Missing 'this' - not an extension method
public static bool TagExists(AstNode node, string tag) { ... }
Fix: Add this keyword:
public static bool TagExists(this AstNode node, string tag) { ... }
Code File Not Found
Problem: Compiler can't locate external code file.
Fix: Use relative path from the Biml file location:
<#@ code file="Helpers/BimlExtensions.cs" #>
Quick Reference
| Method | Directive | Call Syntax | Returns |
|---|---|---|---|
| Include | <#@ include file="x.biml" #> | N/A (inline) | File contents |
| CallBimlScript | <#@ property ... #> | <#= CallBimlScript("x.biml", p1, p2) #> | Biml XML |
| CallBimlScriptWithOutput | <#@ property ... #> | <#= CallBimlScriptWithOutput("x.biml", out obj, p1) #> | Biml XML + object |
| Code File | <#@ code file="x.cs" #> | Helper.Method() or obj.Method() | Any type |
Next Steps
- Introduction to BimlScript - Code nuggets and directives
- Common Patterns - Ready-to-use staging and loading patterns
- C# Primer - C# fundamentals for BimlScript
- Troubleshooting Guide - Debug compilation errors