Table of Contents

Read, edit and save report definitions

DocWright.Reporting.Rdl reads and writes SSRS and Power BI paginated report definitions (.rdl, .rdlc) across the 2005, 2008, 2010 and 2016 schemas. A load-and-save changes nothing you didn't change, which makes it a sound base for tools that edit reports.

dotnet add package DocWright.Reporting.Rdl

Load

This loads Microsoft's Invoice sample report:

using DocWright.Reporting;
using DocWright.Reporting.Rdl;

ReportDefinition definition;
using (FileStream input = File.OpenRead("Invoice.rdl"))
{
    definition = new RdlReader().Read(input);
}

Console.WriteLine($"Schema:    {definition.SchemaVersion}");
Console.WriteLine($"Datasets:  {string.Join(", ", definition.DataSets.Select(d => d.AttributeValue("Name")))}");
Console.WriteLine($"Parameters: {string.Join(", ", definition.ReportParameters.Select(p => p.AttributeValue("Name")))}");

Output (this page's three steps)

Schema:    Rdl2016
Datasets:  SalesInvoiceDS, Company, Users, SalesInvoiceHeaderFooterDS, CompanyList
Parameters: Company
First textbox: Textbox5
Validation findings: 0

Edit and save

using DocWright.Reporting;
using DocWright.Reporting.Rdl;

// Typed properties for the common metadata; set null to remove an element.
definition.Description = "Sales invoice, revised for the 2026 brand";
definition.Author = "Finance systems";

// Anything else: find the element in the tree and change it.
RdlElement? firstTextbox = definition.Root.DescendantsAndSelf().FirstOrDefault(e => e.Name.Is("Textbox"));
Console.WriteLine($"First textbox: {firstTextbox?.AttributeValue("Name")}");

// Save. Everything DocWright doesn't model - designer data, extensions - is written back as it was.
using (FileStream output = File.Create("Invoice.revised.rdl"))
{
    new RdlDefinitionWriter().Write(definition, output);
}
  • The model is the element tree. Every element, attribute and namespace is kept, including Report Builder's private rd: data and third-party extensions. Unknown content survives because it is still an element, not because some class remembered to copy it.
  • Sparseness is kept. A report that writes <CanGrow>false</CanGrow> and one that omits it stay different, so saving never rewrites a customer's file.
  • Typed shortcuts read through to the tree: SchemaVersion, Description, Author, Language, DataSources, DataSets, ReportParameters and NamedElements().
  • New elements go where the schema says. definition.SetValue(element, "ZIndex", "3") and the typed setters insert children in the schema's declared order, not at the end. Some RDL types require an order, and appending works only until it doesn't.
  • The schema version never changes on save. It is the root element's namespace.

Validate

The RDL XML schema types sizes, colours and enumerations as plain strings, so passing it proves little. ReportValidator checks what matters:

using DocWright.Core.Diagnostics;
using DocWright.Reporting;

IReadOnlyList<ConversionDiagnostic> findings = ReportValidator.Validate(definition).ToList();
Console.WriteLine($"Validation findings: {findings.Count}");
foreach (ConversionDiagnostic finding in findings)
{
    Console.WriteLine($"{finding.Code} at {finding.Location}: {finding.Message}");
}

It reports duplicate report-item names (expressions refer to items by name, so a duplicate is ambiguous), duplicate sibling names, and references to datasets or data sources that don't exist. All seven Microsoft sample reports validate clean.

Sizes

RDL sizes are strings like 2.5in or 1cm. RdlMeasure.Parse converts them to Twips and reports anything that isn't a size, such as 2,5pt, which a comma-decimal culture produces, rather than guessing. RdlMeasure.ToPoints(twips) writes one back.

Untrusted input

An .rdl is untrusted: XML with unlimited nesting and an expression language inside. The reader refuses DTDs, never resolves external entities, and limits nesting depth (256) and element count (2 million) by default. Tighten them with new RdlReader(new RdlReadSettings { Limits = … }).