Stairway to Biml Level 5: Language Elements
Why Understand the Language Elements
Earlier articles in the Stairway to Biml series produced everything from a single Biml file. Splitting Biml across multiple files allows code reuse and a more modular project layout, which makes development, maintenance, and troubleshooting easier as the surface area grows. Understanding the language elements is the prerequisite for that split, because the rules that govern compilation order, namespace scope, and helper code reuse all live inside those elements.
A Quick Primer on XML
Biml is built on XML, so a few terminology items are worth pinning down:
- An XML tag is a markup construct that begins with the less than character and ends with the greater than character. There are start tags such as 'Package', end tags such as the closing 'Package', and empty element tags such as 'Package' followed immediately by a slash and the closing character. An empty element is the abbreviated form of a start tag and an end tag with no content between them.
- An XML element is the named item inside a tag. In a tag named 'Package', the element name is 'Package'.
- An XML attribute is a name and value pair inside a start tag or empty element tag, used to describe the element. Attributes such as 'Name' and 'ConstraintMode' commonly appear on a 'Package' element.
The Two Halves of the Biml Language
A Biml file decomposes into two parts:
- Biml proper is the XML that describes the Microsoft Business Intelligence objects. It currently supports Integration Services, Analysis Services, and database objects.
- BimlScript is the programming layer embedded in the same file. It splits further into directives and control blocks.
The XML schema bundled with the editor provides IntelliSense for the Biml elements and attributes that are valid at the current scope.
Text Blocks
A text block is the section of a Biml file that gets passed to the compiler as is. No preprocessing happens to the contents of a text block. A minimal Biml file with a single text block describes one Integration Services package and produces a .dtsx with that name when the file is compiled.
Directives
Directives instruct the compiler on how to prepare the file. They typically appear at the top of a Biml file. The general syntax wraps the directive name in a control nugget that begins with an at sign:
<#@ DirectiveName AttributeName="AttributeValue" #>
The supported directives include 'annotation', 'assembly', 'dependency', 'import', 'include', 'output', 'property', 'target', and 'template'. Four of those merit close attention: 'assembly', 'import', 'include', and 'template'.
Assembly
The 'assembly' directive references another .NET assembly, similar to adding a reference in Visual Studio. Several common assemblies are referenced automatically by the engine:
- System.dll
- System.Core.dll
- System.Data.dll
- System.Xml.dll
- System.Xml.Linq.dll
- WindowsBase.dll
- BimlEngine.dll
Because most common functionality is already available, explicit 'assembly' directives are rare in BimlExpress projects and more common in BimlStudio projects. The 'System.IO' namespace lives inside WindowsBase.dll, which is referenced by default, so it can be imported without an additional 'assembly' directive:
<#@ assembly name="[assembly strong name or assembly file name]" #>
Import
The 'import' directive brings a namespace into scope so types and methods can be referenced by short name instead of fully qualified name. It is the Biml equivalent of the C# 'using' keyword or the Visual Basic 'imports' keyword. Only namespaces from referenced assemblies can be imported:
<#@ import namespace="namespace" #>
A common example imports the schema management namespace from BimlEngine.dll so the 'SchemaManager' type can be used by short name:
<#@ import namespace="Varigence.Hadron.CoreLowerer.SchemaManagement" #>
With that import in place a connection node can be created with a short call:
<# var conn = SchemaManager.CreateConnectionNode("ProviderName", "Data Source=..."); #>
Without the import the same call requires the fully qualified name 'Varigence.Hadron.CoreLowerer.SchemaManagement.SchemaManager.CreateConnectionNode'.
Include
The 'include' directive substitutes the contents of another Biml file into the current file at the point where the directive appears. Included files may themselves contain 'include' directives:
<#@ include file="filepath" #>
A typical pattern is to keep shared variables in a small parameters file and pull them in from a main file with an 'include' directive. The compiler emits the combined text to the engine as if the included content had been written inline.
Template
The 'template' directive controls how the file is processed. Two attributes carry the most weight: 'language' and 'tier'. The 'language' attribute selects the BimlScript language for the file. The default is C# and the attribute can be omitted when C# is the language in use:
<#@ template language="C#" tier="0" #>
The 'tier' attribute sets the explicit compilation order of the file. Tiers are zero based. Files in a lower tier compile before files in a higher tier, so a file that depends on objects defined elsewhere must sit in a higher tier than the file that defines those objects. Files within the same tier compile in unpredictable order, which means files in the same tier cannot reference each other.
Without the 'tier' attribute, the compiler applies implicit rules:
- Files without BimlScript compile in tier 0; files with BimlScript compile in tier 1.
- Within a tier, files compile in the order they were selected.
File selection order is brittle. Setting the tier explicitly with a 'template' directive is the predictable approach. Note that included files do not need to appear in the selected group, because their contents are inlined into the file that includes them.
Control Blocks
A control block holds the C# or Visual Basic code that drives generation. Three forms exist: standard, expression, and class feature.
Standard Control Blocks
A standard control block contains code that runs during compilation but does not write its result back to the output. Loops, variable declarations, and conditionals all sit in standard control blocks:
<# [C# or Visual Basic Code] #>
A typical example assigns the result of a method call to a local variable for use later in the file:
<# var tables = connection.GenerateTableNodes(); #>
Standard control blocks become a pattern engine when they wrap Biml markup with a loop. Iterating from one through four around a 'Package' element produces four packages from a single template.
Expression Control Blocks
An expression control block evaluates a code fragment and writes the result into the output as a string. The marker is a control nugget that begins with an equals sign, and unlike a standard block it does not end with a semicolon:
<#= [C# or Visual Basic Code] #>
The expression below evaluates to twelve and writes that value into the surrounding markup:
<#= 2 + 10 #>
Class Feature Control Blocks
A class feature control block defines helper methods, properties, or fields that the rest of the file can call. The marker begins with a plus sign:
<#+ [C# or Visual Basic Code] #>
Class feature blocks must appear at the end of the file when written inline. The cleaner pattern is to keep them in a dedicated file and pull them in with an 'include' directive, in which case the position constraint does not apply. A trivial example might define an 'UpperCase' helper that other parts of the script call to normalize a name. Anything called from multiple places in the script is a candidate for a class feature block.
RootNode
The 'RootNode' is the in memory representation of the objects assembled from the selected Biml files at compile time. It is the entry point used to reach previously compiled objects, and it builds up tier by tier. Files compiled in a lower tier expose their objects through 'RootNode' to files compiled in a higher tier.
In a two file project where 'EnvironmentDefinitions.biml' is at tier 0 and 'PackageGroupOne.biml' is at tier 1, the package file can reach an OLE DB connection defined in the environment file through 'RootNode':
<# var src = RootNode.OleDbConnections["WarehouseSource"]; #>
Two important constraints follow from this:
- During compilation of tier 0, 'RootNode' is empty. A tier 0 file cannot reference objects through 'RootNode' because nothing has been added yet.
- 'RootNode' only contains files that were selected for the current generate operation. Files that exist in the project but were not selected do not contribute objects.
The 'RootNode' object is large. The most commonly used collections are 'Connections', 'FileFormats', 'Packages', 'Tables', and 'Schemas'. The full list is documented in the Biml API reference.
Conclusion
The text blocks, directives, control blocks, and 'RootNode' described above are the building blocks for splitting a Biml project into composable files. Understanding how compilation tiers and 'RootNode' interact is the prerequisite for any pattern that spans more than a single file.