Table of Contents

Report expressions

RDL reports are mostly expressions: =Fields!Sales.Value, =Sum(Fields!Sales.Value, "Region"), =Globals!PageNumber & " of " & Globals!TotalPages. DocWright.Reporting evaluates them with an interpreter: no compiler, no reflection and no code generation, with identical results on every runtime.

Report processing evaluates expressions for you. Use the engine directly to evaluate an expression in a designer, a preview or a test.

Parse and evaluate

using System.Globalization;
using DocWright.Reporting.Expressions;

// Parse once (syntax, names and constants are checked here), evaluate many times.
ReportExpression expression = ReportExpression.Parse(
    "=Format(Parameters!Amount.Value * 1.1, \"C\") & \" for \" & Parameters!Region.Label");

var context = new ReportExpressionContext();
context.Parameters["Amount"] = new ReportParameterValue(1250);
context.Parameters["Region"] = new ReportParameterValue("W", "West");

Console.WriteLine(ReportExpressionEvaluator.Evaluate(expression, context));

// The culture is never the machine's: en-US unless you say otherwise.
context.Culture = CultureInfo.GetCultureInfo("de-DE");
Console.WriteLine(ReportExpressionEvaluator.Evaluate(expression, context));

// What the expression reads, with positions for a designer's rename.
foreach (ReportExpressionReference reference in expression.References)
{
    Console.WriteLine($"  {reference.Kind} {reference.Name} at {reference.Position}");
}

Output

$1,375.00 for West
1.375,00 € for West
  Parameter Amount at 8
  Parameter Region at 56
DXP9106: The expression reads the execution time, and none was supplied: set ReportExpressionContext.ExecutionTime. The clock is never read.
2026-10-01 09:00
  • Parse once, evaluate many times. Parse checks syntax, names, argument counts and constants. A parsed expression is immutable and safe to share across threads.
  • Text not starting with = is a literal and evaluates to itself.
  • Results are the .NET values Visual Basic produces: Integer is int, Date is DateTime, Nothing is null.
  • Names are case-sensitive, as in Reporting Services.
  • References, UsesAggregates, UsesPageGlobals, UsesCustomCode and UsesExecutionTime describe what an expression depends on.

Nothing comes from the machine

The culture defaults to en-US, which is what Reporting Services uses for a report with no <Language>, measured. Now(), Today() and Globals!ExecutionTime return the ExecutionTime you supply. Without one, they are refused rather than given the clock:

using DocWright.Reporting.Expressions;

// Now() is never the machine's clock: it is the ExecutionTime you supply.
try
{
    ReportExpressionEvaluator.Evaluate(ReportExpression.Parse("=Year(Now())"), new ReportExpressionContext());
}
catch (ReportExpressionException error)
{
    Console.WriteLine($"{error.DiagnosticCode}: {error.Message}");
}

var timed = new ReportExpressionContext { ExecutionTime = new DateTime(2026, 10, 1, 9, 0, 0) };
Console.WriteLine(ReportExpressionEvaluator.Evaluate(ReportExpression.Parse("=Format(Now(), \"yyyy-MM-dd HH:mm\")"), timed));

This differs from Reporting Services on purpose: binding every date function to one instant makes a report reproducible.

Custom code

A report's <Code> block is Visual Basic from the file, and running it in a server would mean running whatever an uploaded file contains. DocWright never executes it. Implement ReportCodeHost to provide the functions you trust. The SSRS tutorial shows one.

Limits

An expression is a program written by someone else, so evaluation is bounded, and these limits are on by default:

Limit Default
MaxReportExpressionDepth 256 levels
MaxReportExpressionSteps 10,000,000 per evaluation
MaxReportExpressionAllocation 16,000,000 characters and array elements per evaluation

=Space(2000000000) is refused rather than allocating four gigabytes.