Skip to content

RDLC reports in ASP.NET Core

ReportViewer’s web control is a Web Forms control, and LocalReport, which renders RDLC without it, belongs to .NET Framework: neither runs in ASP.NET Core. This page shows how to serve the same reports from an ASP.NET Core application on .NET 8, 9 or 10, as PDF or Excel, with the data your application already loads. For the options besides this one, see ReportViewer and RDLC alternatives.

  1. Install the tagua command, and convert the folder that holds your .rdlc files. Each report becomes a .report.yaml template next to it, and tagua-import-summary.md lists what, if anything, needs a look:

    Terminal window
    dotnet tool install --global Tagua.Cli
    tagua import Reports
  2. Add the package to your ASP.NET Core project, and declare the license your company uses (see Licensing):

    Terminal window
    dotnet add package Tagua
  3. Copy the templates, and their images, to the output with the application:

    <ItemGroup>
    <Content Include="Reports/**"
    CopyToOutputDirectory="PreserveNewest" />
    </ItemGroup>

Compile each template once, at startup: a compiled report is immutable and serves any number of requests at once. Each request passes the rows and parameters, and gets a PDF back.

using System.Data.Common;
using Tagua;
using Tagua.Fill;
using Tagua.Pipeline;
using Tagua.Serialization;
TaguaSettings.License = LicenseType.Community;
var builder = WebApplication.CreateBuilder(args);
var template = TemplateLoader.LoadFile("Reports/Invoice.report.yaml");
builder.Services.AddSingleton(CompiledReport.Compile(template));
var app = builder.Build();
app.MapGet("/invoices/{id:int}/pdf", async (
int id, CompiledReport invoice, DbDataSource db, CancellationToken ct) =>
{
const string sql = """
select Description, Quantity, UnitPrice
from InvoiceLines where InvoiceId = @id
""";
var inputs = new ReportInputs
{
Parameters = new Dictionary<string, object?> { ["InvoiceId"] = id },
Data = new Dictionary<string, IDataSet>
{
// The dataset's name in the report, as ReportDataSource named it.
["Lines"] = DataSources.FromDataReader(async token =>
{
var command = db.CreateCommand(sql);
var parameter = command.CreateParameter();
(parameter.ParameterName, parameter.Value) = ("@id", id);
command.Parameters.Add(parameter);
return await command.ExecuteReaderAsync(token);
}),
},
Resources = new DirectoryResourceResolver("Reports"),
};
var pdf = new MemoryStream();
await Reports.RenderPdfAsync(invoice, inputs, pdf, cancellationToken: ct);
pdf.Position = 0;
return Results.File(pdf, "application/pdf", $"invoice-{id}.pdf");
});
app.Run();

The rows come straight from the reader, with no DataTable in between. Rows you already have in memory go in with DataSources.FromRows, and a DataTable with DataSources.FromDataReader(_ => Task.FromResult<DbDataReader>(table.CreateDataReader())).

What you did with LocalReport has a direct counterpart:

ReportViewer (LocalReport) Tagua
report.ReportPath = "Invoice.rdlc" CompiledReport.Compile(TemplateLoader.LoadFile("Invoice.report.yaml")), once
report.DataSources.Add(new ReportDataSource("Lines", table)) Data = { ["Lines"] = DataSources.FromDataReader(...) }
report.SetParameters(new ReportParameter("InvoiceId", "42")) Parameters = { ["InvoiceId"] = 42 }, typed
report.Render("PDF") Reports.RenderPdfAsync(report, inputs, stream)
report.Render("EXCELOPENXML") ExcelReports.RenderExcelAsync(report, inputs, stream), in Tagua.Excel
SubreportProcessing event The subreport’s datasets, passed by name with the others

It runs the same on Windows, Linux and macOS, in a container, and with native AOT: see RDLC reports on Linux and in Docker. The migration guide covers what converts and what needs a person, and two worked examples show reports converted and rendered.