Skip to main content

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:

ExtensionUse Case
.bimlBiml XML fragments
.sqlSQL scripts (edit in SSMS with IntelliSense)
.csC# code blocks
.vbVB code blocks
.txtAny 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

  1. Define properties in the callee file using <#@ property ... #> directives
  2. Call the file using CallBimlScript() in an expression block
  3. Pass parameters in the same order as properties are defined
  4. 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

  1. Declare a dynamic variable in the caller
  2. Pass it with out as the second parameter to CallBimlScriptWithOutput
  3. In the callee, assign properties to the built-in CustomOutput object
  4. 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>

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:

MethodTypeDescription
GetBiml()AstNodeReturns Biml XML representation
GetDatabaseSchema()ConnectionImports tables from database
GetDropAndCreateDdl()AstTableNodeGenerates DDL script
GetTag()AstNodeRetrieves 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

ScenarioRecommended MethodWhy
Same code fragment everywhereIncludeSimple, no parameters needed
SQL script maintained in SSMSInclude (.sql file)Edit with SQL IntelliSense
Template with different inputsCallBimlScriptPass table, connection, flags
Need metadata back from templateCallBimlScriptWithOutputReturn column counts, validation
Complex logic in one fileInline class feature blockQuick, no external files
Shared logic across filesCode fileReusable, IDE support
Cleaner calling syntaxExtension methodtable.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

MethodDirectiveCall SyntaxReturns
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