Narity Reporterpreview

Examples

Render Crystal Reports to PDF in C#

Start with a complete .NET 10 application. Render .rpt or JSON reports, then connect your own data.

Render your first PDF

You need the .NET 10 SDK and your Reporter NuGet package. Start a 30-day evaluation or download your package. This example uses saved data, so no database or SAP runtime is needed.

1. Create an application

Terminal
dotnet new console -n FirstPdf -f net10.0
cd FirstPdf
mkdir packages

2. Add your SDK download

Put Narity.Reporter.0.16.0.nupkg in FirstPdf/packages. Use the version in your filename below.

Releases up to 0.13 name the package Reporter. For those, use Reporter in the commands below and in nuget.config.

The account package includes your licence. Keep it private. The local package cache keeps it separate from other projects.

3. Restore your package from your folder only

Create FirstPdf/nuget.config with this content.

XML — FirstPdf/nuget.config
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <clear />
    <add key="local" value="packages" />
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
  </packageSources>
  <packageSourceMapping>
    <packageSource key="local">
      <package pattern="Narity.Reporter" />
    </packageSource>
    <packageSource key="nuget.org">
      <package pattern="*" />
    </packageSource>
  </packageSourceMapping>
</configuration>

The source mapping restores Narity.Reporter only from your packages folder. A package with the same name on nuget.org cannot replace it. Other packages, such as PDFsharp, come from nuget.org.

4. Install the package

Terminal
dotnet add package Narity.Reporter --version 0.16.0 --no-restore
dotnet restore --packages ./packages-cache

5. Add the sample and code

Download the sample report definition into FirstPdf. It contains three fictional orders.

Replace Program.cs with this complete file.

C# — Program.cs
using Reporter.Engine.Data;
using Reporter.Engine.Format;
using Reporter.Engine.Formula;
using Reporter.Engine.Layout;
using Reporter.Engine.Render;
using Reporter.Model;
using Reporter.Rpt;

string input = args.Length > 0 ? args[0] : "order-summary.json";
ReportModel model = Path.GetExtension(input).Equals(".rpt", StringComparison.OrdinalIgnoreCase)
    ? ReportModelBuilder.FromRpt(input)
    : ReportModel.FromJson(File.ReadAllText(input));

IReadOnlyDictionary<string, FormulaValue> parameters = ParameterBinding.Resolve(
    model, new Dictionary<string, string>(), useSavedValues: true);
model = ReportDataBinder.Bind(model, new SavedDataRowSource(), parameters, input);

PaginatedDocument document = Paginator.Paginate(model, new PdfTextMeasurer(), Locale.EnGb, parameters);
PdfRenderer.RenderToFile(document, model, "out.pdf");
Console.WriteLine($"Created {Path.GetFullPath("out.pdf")} ({document.Pages.Count} pages).");

6. Run and open the PDF

Terminal
dotnet run --no-restore

Open FirstPdf/out.pdf. Expect one page titled “Order summary”, with orders ORD-1001, ORD-1002, and ORD-1003. View the expected layout.

A valid evaluation package produces clean pages. UNLICENSED COPY L1 means no licence was found. Download the package from your account, or load reporter.lic.

Then try your own report

Put an .rpt saved with data in FirstPdf, then pass its filename:

Terminal
dotnet run --no-restore -- report.rpt

Compare the PDF with your Crystal output. A report without saved data needs a database provider or supplied rows.

Keep the using directives above for the integration examples below. Replace the rest of Program.cs with the example you need.

Render from the rows saved in the file

An .rpt saved with data carries its rows and the parameter values of the run that saved it, so this needs no database. The two middle lines matter: without them, every parameter the report prints comes out empty.

C# — saved data
ReportModel model = ReportModelBuilder.FromRpt("report.rpt");

// A saved report prints with the parameter values it was saved with.
IReadOnlyDictionary<string, FormulaValue> parameters =
    ParameterBinding.Resolve(model, new Dictionary<string, string>(), useSavedValues: true);
model = ReportDataBinder.Bind(model, new SavedDataRowSource(), parameters, "report.rpt");

PaginatedDocument doc = Paginator.Paginate(model, new PdfTextMeasurer(), Locale.EnGb, parameters);
PdfRenderer.RenderToFile(doc, model, "out.pdf");

Convert .rpt to JSON and render PDF

Convert an .rpt into a JSON definition you can read, edit, and version. JSON preserves decoded sections, objects, positions, fonts, formulas, groups, and record selection.

Use the quickstart package and namespace imports. Run this example with a report saved with data.

C#
ReportModel model = ReportModelBuilder.FromRpt("report.rpt");
File.WriteAllText("report.json", ReportModel.ToJson(model));

// ...and back: the engine renders the definition, not the .rpt.
ReportModel definition = ReportModel.FromJson(File.ReadAllText("report.json"));

IReadOnlyDictionary<string, FormulaValue> parameters =
    ParameterBinding.Resolve(definition, new Dictionary<string, string>(), useSavedValues: true);
definition = ReportDataBinder.Bind(definition, new SavedDataRowSource(), parameters, "report.json");

PaginatedDocument doc = Paginator.Paginate(definition, new PdfTextMeasurer(), Locale.EnGb, parameters);
PdfRenderer.RenderToFile(doc, definition, "out.pdf");

Open out.pdf to inspect the result. Reuse report.json for later renders.

Saving and reloading JSON preserves the decoded model. Compare the PDF with Crystal to check your report’s feature support.

JSON — trimmed; the real file carries every section and object
{
  "RecordSelection": "{ORDERS.CUSTOMER_ID} = {?CUSTOMER_ID}",
  "Formulas": [
    { "Name": "IsOpenOrder", "ResultType": "Boolean", "Text": "{ORDERS.STATUS} <> 'CANCELLED'" }
  ],
  "Groups": [
    { "ConditionField": "ORDERS.ORDER_ID", "Direction": "NoSortOrder" }
  ],
  "Areas": [
    {
      "Kind": "Detail",
      "Sections": [
        {
          "Height": 240,
          "Objects": [
            {
              "Name": "LineDescription",
              "Kind": "Field",
              "Bounds": { "Left": 598, "Top": 0, "Width": 9737, "Height": 180 },
              "FontName": "Arial",
              "FontSizePt": 9,
              "FontBold": false,
              "CanGrow": true,
              "DataSource": "{ORDER_LINES.DESCRIPTION}"
            }
          ]
        }
      ]
    }
  ]
}

Choose a database provider

The SDK supports .NET 10 and exports PDF. It depends on PDFsharp and needs no SAP runtime.

For live data, download the libraries ZIP from your account. Extract it into providers beside your project.

Add the assembly you need to your project file. This example selects PostgreSQL:

XML — inside FirstPdf.csproj
<ItemGroup>
  <Reference Include="Reporter.Db.Postgres">
    <HintPath>providers/Reporter.Db.Postgres.dll</HintPath>
  </Reference>
</ItemGroup>

Add its driver from NuGet, then restore. The nuget.config file from the quickstart supplies the sources.

  • PostgreSQL: Reporter.Db.Postgres.dll, using Reporter.Db.Postgres;, driver Npgsql.
  • Oracle: Reporter.Db.Oracle.dll, using Reporter.Db.Oracle;, driver Oracle.ManagedDataAccess.Core.
  • SQLite: Reporter.Db.Sqlite.dll, using Reporter.Db.Sqlite;, driver Microsoft.Data.Sqlite.
Terminal — PostgreSQL
dotnet add package Npgsql --no-restore
dotnet restore --packages ./packages-cache

Table-based reports use the providers below. SQL Command reports need supplied rows; Reporter does not execute their stored SQL.

Run it against PostgreSQL

The same report file against live rows. Reporter builds the report’s own query from its tables and links, pushes the record selection down to the server where SQL can express it, and binds every parameter instead of splicing it into the SQL. A live run starts from the values you pass; the saved ones belong to an old run.

C# — PostgreSQL: reference Reporter.Db.Postgres and the Npgsql package
IDbProvider provider = new PostgresProvider();
string connection = Environment.GetEnvironmentVariable("REPORTS_DB")!;

ReportModel model = ReportModelBuilder.FromRpt("orders.rpt");

// Values arrive as text and are coerced to each parameter's declared type.
IReadOnlyDictionary<string, FormulaValue> parameters = ParameterBinding.Resolve(
    model,
    new Dictionary<string, string> { ["CUSTOMER_ID"] = "100234" });

// The row source builds the report's own query: links, join order, pruning, and the
// record selection pushed down to the server. Parameters bind, never interpolate.
var rows = new DbRowSource(
    () => provider.Open(connection),
    provider.Dialect,
    provider.PrepareCommand,
    provider.ReadValue);

model = ReportDataBinder.Bind(model, rows, parameters, "orders.rpt");
PaginatedDocument doc = Paginator.Paginate(model, new PdfTextMeasurer(), Locale.EnGb, parameters);
PdfRenderer.RenderToFile(doc, model, "orders.pdf");

Run it against Oracle

Swap the provider, and nothing else changes. It runs on Oracle’s fully managed driver, so there is no Instant Client to install. A service name, a tnsnames.ora alias, and a wallet for TCPS all go in the connection string.

C# — Oracle: reference Reporter.Db.Oracle and the Oracle.ManagedDataAccess.Core package
IDbProvider provider = new OracleProvider();

// Any connection string the managed driver accepts, for example:
//   User Id=reports;Password=...;Data Source=db.internal:1521/ORCLPDB1
//   User Id=reports;Password=...;Data Source=REPORTS;TNS_ADMIN=/etc/oracle
//   User Id=reports;Password=...;Data Source=REPORTS_TCPS;TNS_ADMIN=/etc/oracle;WALLET_LOCATION=/etc/oracle/wallet
string connection = Environment.GetEnvironmentVariable("REPORTS_DB")!;

ReportModel model = ReportModelBuilder.FromRpt("statement.rpt");
IReadOnlyDictionary<string, FormulaValue> parameters = ParameterBinding.Resolve(
    model,
    new Dictionary<string, string> { ["ACCOUNT_ID"] = "40018" });

var rows = new DbRowSource(
    () => provider.Open(connection),
    provider.Dialect,
    provider.PrepareCommand,
    provider.ReadValue);

model = ReportDataBinder.Bind(model, rows, parameters, "statement.rpt");
PaginatedDocument doc = Paginator.Paginate(model, new PdfTextMeasurer(), Locale.EnGb, parameters);
PdfRenderer.RenderToFile(doc, model, "statement.pdf");

Run it against SQLite

No server: the database is one file, which suits tests, demos, and an offline copy of a report’s data. SQLite has no date type, so a date stored as ISO-8601 text reads as a date wherever the report declares one.

C# — SQLite: reference Reporter.Db.Sqlite and the Microsoft.Data.Sqlite package
IDbProvider provider = new SqliteProvider();

// The database is one file. Read-only, so a mistyped path fails rather than creating an empty one.
string connection = "Data Source=/var/data/sales.db;Mode=ReadOnly";

ReportModel model = ReportModelBuilder.FromRpt("sales.rpt");
IReadOnlyDictionary<string, FormulaValue> parameters = ParameterBinding.Resolve(
    model,
    new Dictionary<string, string> { ["REGION"] = "North" });

var rows = new DbRowSource(
    () => provider.Open(connection),
    provider.Dialect,
    provider.PrepareCommand,
    provider.ReadValue);

model = ReportDataBinder.Bind(model, rows, parameters, "sales.rpt");
PaginatedDocument doc = Paginator.Paginate(model, new PdfTextMeasurer(), Locale.EnGb, parameters);
PdfRenderer.RenderToFile(doc, model, "sales.pdf");

Supply rows for a SQL Command report

Your application executes the query and supplies its results through IRowSource. Reporter does not execute SQL stored in Command tables.

Use the quickstart imports. Adapt the column names, types, rows, and parameter values to your report.

C# — supplied rows
ReportModel model = ReportModelBuilder.FromRpt("command-report.rpt");
IReadOnlyDictionary<string, FormulaValue> parameters = ParameterBinding.Resolve(
    model, new Dictionary<string, string>());

// Replace these fictional rows with results from your own parameterized query.
// Column names must match the report's ALIAS.FIELD names, without braces.
var data = new SavedRows(
    [new SavedColumn("Command.ORDER_ID", FieldValueType.String),
     new SavedColumn("Command.AMOUNT", FieldValueType.Number)],
    [["ORD-1001", "125.00"], ["ORD-1002", "79.50"]]);
model = ReportDataBinder.Bind(model, new CommandRows(data), parameters, "command-report.rpt");
PaginatedDocument document = Paginator.Paginate(model, new PdfTextMeasurer(), Locale.EnGb, parameters);
PdfRenderer.RenderToFile(document, model, "out.pdf");

sealed class CommandRows(SavedRows rows) : IRowSource
{
    public RowFetch Fetch(ReportModel report, IReadOnlyDictionary<string, FormulaValue> parameters) =>
        new(rows, SelectionApplied: false);
}

Cells use invariant strings: decimal points for numbers, ISO dates for dates, and null for database nulls. Keep each row in column order.

SelectionApplied: false lets Reporter apply the report’s record selection. Use bound parameters in your query. For subreports, return the matching rows for each report passed to Fetch.

Serve a PDF from a web endpoint

For this example, create an ASP.NET Core project with dotnet new web -n FirstPdf -f net10.0. Follow the package and PostgreSQL setup above, including the namespace imports.

Parse the report once at startup and render one PDF per request. A value that does not fit the parameter’s declared type is a 400. A database failure is logged with the connection string’s secrets removed, because a driver can quote them back in its error message.

C# — ASP.NET Core minimal API, Program.cs
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();

IDbProvider provider = new PostgresProvider();
string connection = builder.Configuration.GetConnectionString("Reports")!;

// Parse the .rpt once. Binding returns a new model, so the parsed one is never changed.
ReportModel invoice = ReportModelBuilder.FromRpt("Reports/invoice.rpt");

app.MapGet("/invoices/{id}.pdf", (string id) =>
{
    IReadOnlyDictionary<string, FormulaValue> parameters;
    try
    {
        parameters = ParameterBinding.Resolve(invoice, new Dictionary<string, string> { ["INVOICE_ID"] = id });
    }
    catch (FormatException)
    {
        return Results.BadRequest();
    }

    var rows = new DbRowSource(
        () => provider.Open(connection),
        provider.Dialect,
        provider.PrepareCommand,
        provider.ReadValue);
    try
    {
        ReportModel bound = ReportDataBinder.Bind(invoice, rows, parameters, "invoice.rpt");
        PaginatedDocument doc = Paginator.Paginate(bound, new PdfTextMeasurer(), Locale.EnGb, parameters);

        var pdf = new MemoryStream();
        PdfRenderer.Render(doc, bound, pdf);
        pdf.Position = 0;
        return Results.File(pdf, "application/pdf", $"invoice-{id}.pdf");
    }
    catch (ReportDataException ex)
    {
        // A driver can quote part of the connection string back in its message.
        app.Logger.LogError("invoice {Id}: {Error}", id, ConnectionSecrets.Redact(ex.Message, connection));
        return Results.StatusCode(StatusCodes.Status502BadGateway);
    }
});

app.Run();

Check a data source before the first run

Point a report at a new database and find out what is missing before anyone waits for a PDF. DataSourceVerifier opens the connection, then builds the report’s query and each subreport’s and asks the server to describe it. Every table and column a query names is resolved, and no rows are read. model, provider, connection, and parameters are the ones from the examples above.

C# — one line per check: the connection, the report, each subreport
VerificationReport report = DataSourceVerifier.Verify(
    model,
    () => provider.Open(connection),
    provider.Dialect,
    parameters,
    provider.PrepareCommand);

foreach (VerificationCheck check in report.Checks)
{
    Console.WriteLine($"{check.Outcome,-8} {check.Name}: {ConnectionSecrets.Redact(check.Detail, connection)}");
}
return report.Passed ? 0 : 1;

Check or load a licence

The account’s NuGet package already carries your licence. For loose assemblies or a renewal, add using Reporter.Engine.Licensing; and load the downloaded file before rendering.

Without a licence, every page carries a diagonal UNLICENSED COPY band and a code that says why. L1 means no licence was found. The demo on this site draws the same band. A licensed run draws none.

C# — at startup
// The engine reads REPORTER_LICENSE, then reporter.lic beside the application.
// To keep the file somewhere else, point the engine at it once, at startup:
LicenseInfo licence = ReporterLicense.SetFile("/run/secrets/reporter.lic");
if (!licence.IsLicensed)
{
    Console.Error.WriteLine($"Reporter is unlicensed ({licence.Code}): {licence.Reason}");
}
The database providers ship separately. The Narity.Reporter package carries the parser, the model, and the engine, and depends only on PDFsharp, so nobody gets a database driver they don’t need. DbRowSource takes any IDbProvider. The PostgreSQL, Oracle, and SQLite providers are separate assemblies in Reporter-libraries-<version>.zip. Reference the one you use and add its driver package from NuGet. The zip and the package are not a public download. Licensed clients sign in to download them. To try them first, create an account and start a 30-day evaluation.