Skip to main content Skip to footer

How to Build Multi-Tenant Reports in ASP.NET Core Applications

  • 0 Comments
Quick Start Guide
What You Will Need

ActiveReports.NET (Installed and Licensed - Trial OK)

Visual Studio 2022 or 2019

Controls Referenced ActiveReports.NET JSViewer
Tutorial Concept Demonstrate how to dynamically switch a report’s data source at runtime based on the selected tenant in a multi-tenant ASP.NET Core Razor Pages application using ActiveReports.NET.

Multi-tenant applications serve multiple customers (tenants) from a single application instance. A common requirement is to isolate each tenant’s data so that each tenant only sees their own information. In reporting scenarios, this often means each tenant might have a separate database or schema. The challenge is to use a single report definition (layout) but dynamically switch the data source based on the current tenant.

ActiveReports.NET supports this scenario – for example, you can supply a dynamic connection string when each tenant uses a separate database. In this guide, we will build a sample Razor Pages application to demonstrate how to change a report’s data source at runtime for different tenants.

What we will build: A Razor Pages app with the ActiveReports JSViewer. The front-end will have a dropdown to select a tenant and another to select a report. When a report is viewed, the application will intercept the report-loading process on the server and inject a tenant-specific database connection string before rendering the report. This approach is for demonstration; we’ll pass the tenant ID via an unsecured dropdown for simplicity. In a real production app, tenant identification should be handled via authentication/authorization.

If you’d like to download the pre-built sample application instead of following along, you can get it here.

Ready to check it out? Download ActiveReports.NET Today!

Project Setup

Before diving into code, ensure you have ActiveReports.NET installed and licensed (full developer license or trial is fine) on your development machine. You can use the NuGet package MESCIUS.ActiveReports.Aspnetcore.Viewer to add ActiveReports JSViewer support to your ASP.NET Core project.

Start by creating a new ASP.NET Core Razor Pages project (using .NET 6 or later). Then follow these setup steps:

  1. Add Viewer NuGet to the project: Install the ActiveReports ASP.NET Core Viewer NuGet package (MESCIUS.ActiveReports.Aspnetcore.Viewer) via the NuGet Package Manager. This provides the server-side viewer engine and middleware.

  2. Add JS Viewer scripts/css to the project: Install the ActiveReports.NET JS Viewer NPM package to the project using the command npm i @mescius/activereportsnet-viewer.

    1. Copy the “jsViewer.min.js” and “jsViewer.min.css” files installed in the “\node_modules\@mescius\activereportsnet-viewer\dist” folder to the “wwwroot\js” and “wwwroot\css” folders in the application, respectively

      • Note: You may need to open the project directory in the file explorer to see these files, as they may not show up in the Solution Explorer

Add JSViewer Scripts to Project

  1. Create a Reports folder: Add a folder named Reports in your project, and include your report definition files (.rdlx or .rdlx-master for page reports/RDL reports, .rpx for section reports, etc.).

    1. Set each report file’s Build Action to Embedded Resource.

    2. In our sample, we’ll assume two page report files – e.g., Conditional Formatting.rdlx and Invoice.rdlx – embedded as resources under the Reports folder.

    3. If you would like to use our sample reports for testing, you can get those here.

MultiTenant JS Viewer

With the project references in place and reports added, we can proceed to build the front-end and back-end components of the multi-tenant reporting demo.

Frontend UI: Index.cshtml

The frontend UI (Razor Page) provides a simple interface for users to select a tenant and a report, and then displays the report using the ActiveReports JSViewer. The key parts of this page are:

  • Dropdowns for Tenant and Report Selection: A <select> for tenant ID (for example, Tenant 1 and Tenant 2) and another for available reports.

  • JSViewer Container: A <div> element that will host the ActiveReports JSViewer.

  • ActiveReports Viewer Scripts: References to the JSViewer’s CSS and JS files, which are typically placed in wwwroot.

  • Initialization Script: JavaScript to create and configure the JSViewer, and to handle the selection events to load the chosen report.

Below is the full code for Index.cshtml:

@page
@model IndexModel
@{
    ViewData["Title"] = "Multi-Tenant Reports";
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>MultiTenantJsViewer Demo</title>
    <!-- ActiveReports JSViewer style -->
    <link rel="stylesheet" href="css/jsViewer.min.css" />
</head>
<body>
    <h2>Multi-Tenant Report Viewer Demo</h2>
    <div>
        <!-- Tenant selection dropdown -->
        <label for="tenantSelect">Tenant:</label>
        <select id="tenantSelect" name="tenant">
            <option value="Tenant1">Tenant 1</option>
            <option value="Tenant2">Tenant 2</option>
        </select>

        <!-- Report selection dropdown -->
        <label for="reportSelect">Report:</label>
        <select id="reportSelect" name="report">
            <option value="Conditional Formatting.rdlx">Conditional Formatting</option>
            <option value="Invoice.rdlx">Invoice</option>
        </select>

        <button id="viewReportBtn" type="button">View Report</button>
    </div>

    <!-- Viewer container -->
    <div id="viewer-host" style="width:100%; height:600px; margin-top: 10px; border: 1px solid #ccc;">
        <!-- The ActiveReports JSViewer will be embedded here -->
    </div>

    <!-- ActiveReports JSViewer script -->
    <script src="js/jsViewer.min.js"></script>
    <script type="text/javascript">
        // Initialize the ActiveReports JSViewer, pointing it to the backend service
        var viewer = GrapeCity.ActiveReports.JSViewer.create({
            element: '#viewer-host',
            reportService: { url: 'api/reporting' }
            // Note: no initial reportID here, we will load based on user selection
        });

        // Button click event to open the selected report for the selected tenant
        document.getElementById('viewReportBtn').addEventListener('click', function () {
            var tenant = document.getElementById('tenantSelect').value;
            var report = document.getElementById('reportSelect').value;
            if (tenant && report) {
                // Combine report name and tenant into one identifier for the server
                // e.g. "Invoice_Tenant1.rdlx"
                var reportId = report.replace('.rdlx', '') + '_' + tenant + '.rdlx';
                viewer.openReport(reportId);
            }
        });
    </script>
</body>
</html>

In this setup, we include the JSViewer's CSS and JavaScript files in our Razor page and define dropdowns for selecting a tenant and report, as well as a button to explicitly trigger report loading. The viewer is instantiated within a designated <div> element and is configured to request reports from the backend at the default api/reporting endpoint. When the "View Report" button is clicked, JavaScript constructs a report identifier combining the selected report and tenant (e.g., Invoice_Tenant1.rdlx), prompting the viewer to request this specific report from the server.

Custom Report Provider

On the server side, ActiveReports allows us to plug in a custom report provider to intercept report loading. We’ll create a class MultiTenantReportProvider that implements ActiveReports’ IReportProvider interface. This provider’s job is to:

  1. Load the requested report definition (our .rdlx report) from the embedded resources (or wherever the reports are stored).

  2. Parse the tenant ID from the report name passed in (since we encoded the tenant in the reportId string).

  3. Modify the report’s data source connection string to the one appropriate for that tenant.

  4. Return the modified report definition to the ActiveReports engine for rendering.

Here’s the full code for MultiTenantReportProvider.cs:

namespace MultiTenantJsViewer
{
    using System;
    using System.IO;
    using System.Text;
    using System.Reflection;
    using GrapeCity.ActiveReports.Aspnetcore.Viewer;
    
    public class MultiTenantReportProvider : IReportProvider
    {
        // In a real app, use configuration or secure storage for connection strings:
        private readonly string _connStringTenant1 = "data source=localhost;initial catalog=Northwind;integrated security=true;TrustServerCertificate=True";
        private readonly string _connStringTenant2 = "data source=localhost;initial catalog=Northwind;integrated security=true;TrustServerCertificate=True";
    
        // IReportProvider method to retrieve the report definition as a Stream
        public Stream GetReport(string reportId)
        {
            // 1. Parse tenant ID from the reportId (format: ReportName_Tenant.rdlx)
            string baseReportId = Path.GetFileNameWithoutExtension(reportId);  // e.g. "Invoice_Tenant1"
            string tenantId = null;
            int sep = baseReportId.LastIndexOf('_');
            if (sep != -1)
            {
                tenantId = baseReportId.Substring(sep + 1);            // e.g. "Tenant1"
                baseReportId = baseReportId.Substring(0, sep);         // e.g. "Invoice"
            }
            else
            {
                tenantId = "Default";  // if not found, use a default or throw error
            }
    
            // 2. Load the report definition embedded resource (e.g. "MultiTenantJsViewer.Reports.InvoiceReport.rdlx")
            Assembly asm = Assembly.GetExecutingAssembly();
            string resourceName = $"MultiTenantJsViewer.Reports.{baseReportId}{Path.GetExtension(reportId)}";
            using Stream resStream = asm.GetManifestResourceStream(resourceName) 
                ?? throw new FileNotFoundException($"Report resource '{resourceName}' not found.");
            
            // Load the report definition from the stream
            using StreamReader reader = new StreamReader(resStream);
            var pageReport = new GrapeCity.ActiveReports.PageReport();  // PageReport is the report container
            pageReport.Load(reader);  // load the report definition from text
    
            // 3. Inject the tenant-specific connection string into the report's data source(s)
            string connectionString;
            switch (tenantId)
            {
                case "Tenant1":
                    connectionString = _connStringTenant1;
                    break;
                case "Tenant2":
                    connectionString = _connStringTenant2;
                    break;
                default:
                    connectionString = _connStringTenant1; // default to Tenant1 or handle as needed
                    break;
            }
            // Assume all data sources use the same tenant-specific DB; update each data source's connection string
            foreach (DataSource ds in pageReport.Report.DataSources)
            {
                if (ds.ConnectionProperties != null)
                {
                    ds.ConnectionProperties.ConnectString = connectionString;
                }
            }
    
            // 4. Return the modified report definition as a Stream
            string rdlContent = pageReport.ToRdlString();
            byte[] contentBytes = Encoding.UTF8.GetBytes(rdlContent);
            return new MemoryStream(contentBytes);
        }
    
        // IReportProvider method to provide report type information based on file extension
        public ReportDescriptor GetReportDescriptor(string reportId)
        {
            string ext = Path.GetExtension(reportId)?.ToLower();
            return ext switch
            {
                ".rdlx"        => new ReportDescriptor(ReportType.RdlXml),
                ".rdlx-master" => new ReportDescriptor(ReportType.RdlMasterXml),
                ".rdf"         => new ReportDescriptor(ReportType.Rdf),
                ".rpx"         => new ReportDescriptor(ReportType.RpxXml),
                _              => null
            };
        }
    }
}

The MultiTenantReportProvider implements ActiveReports' IReportProvider interface with two key methods. The GetReport method parses the incoming reportId to extract the tenant ID, retrieves the corresponding embedded report definition, dynamically assigns a tenant-specific database connection string to the report’s data source, and returns the modified report as an RDL XML stream.

The GetReportDescriptor method identifies the report type based on the file extension, ensuring the viewer correctly interprets report formats like .rdlx, .rdf, and .rpx. Finally, this provider is registered within the ASP.NET Core pipeline to serve reports dynamically according to the selected tenant.

Note: The project is currently configured to target an instance of the Northwind sample database on localhost. If you happen to have a local instance of Northwind, it might work for you out of the box, but otherwise, you’ll need to either get one or swap the reports and connection strings out with some of your own to run the sample properly.

Wiring It Up: Program.cs

To use our custom provider, we need to configure ActiveReports to use it when the app starts. ActiveReports provides a middleware extension method UseReportViewer for the JSViewer. We can call UseReportViewer and specify our provider via UseReportProvider. This setup is typically done in the Program.cs (or Startup.cs in older ASP.NET Core versions) during app configuration.

Open Program.cs and add the ActiveReports middleware configuration. For example:

using GrapeCity.ActiveReports.Aspnetcore.Viewer;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Hosting;
// ... other using statements ...

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
var app = builder.Build();

// ... (other middleware like error handling, HTTPS, etc.) ...
app.UseStaticFiles();

// Register ActiveReports JSViewer middleware with our custom report provider
app.UseReportViewer(cfg =>
{
    cfg.UseReportProvider(new MultiTenantReportProvider());
});

app.MapRazorPages();
app.Run();

In the provided setup, we configure ActiveReports' middleware by invoking UseReportViewer after the static file middleware and before routing, explicitly registering our MultiTenantReportProvider. This ensures that when the JSViewer requests reports through the built-in /api/reporting endpoint, ActiveReports automatically uses our custom provider's GetReport method to fetch and prepare reports dynamically. Notably, ActiveReports' middleware manages routing and HTTP handling internally, eliminating the need for a custom controller or explicit API setup.

At this stage, the application is fully wired. We have a front-end where the user can select a tenant and report, and a back-end that serves the report with the correct data source based on the tenant. You can run the application, and upon clicking "View Report," the JSViewer should render the report for the chosen tenant. For example, selecting Tenant 1 might show data from Tenant1’s database, and switching to Tenant 2 (with the same report) would show a different dataset (assuming your connection strings point to different databases with the same schema).

MultiTenant Report Viewer Demo

Security Note

In this demo, the tenant ID is provided via a dropdown on the UI purely for simplicity. This approach is not secure for a real application. In production, you would determine the tenant context from the authenticated user’s identity or subdomain/URL, not from user input. For instance, a user’s JWT or session might include a tenant ID claim, or you might use a multi-tenant library that resolves the tenant from the request hostname.

The server should then apply the appropriate connection string without trusting any client-side provided value. Always ensure that tenant switching is done on the server based on trusted context (and ideally isolate data per tenant at the database level). The dropdown method is only used here to easily demonstrate the concept of runtime data source switching.

Extending Further

We kept this example focused on embedded report definitions and basic string-based tenant IDs, but ActiveReports.NET is very flexible. A few ways you could extend or adapt this approach:

  • Different Report Storage: Instead of embedding reports as resources, you could load them from external files or even a database. ActiveReports provides helpers like UseFileStore to load reports from a folder, or you can use UseReportProvider (as we did) to fetch reports from any custom location (for example, a report server or blob storage). In fact, our MultiTenantReportProvider could be extended to pull report definitions from a database or cache for each tenant, rather than from embedded resources.

  • Dynamic Connection Strings via Parameters: ActiveReports supports using report parameters to dynamically set connection strings at runtime. We manually set the ConnectString in code for clarity, but you could design your reports to accept a connection string or tenant identifier as a parameter and use that in the data source connection string expression. The ActiveReports documentation covers techniques for dynamic data sources using parameters and the UserContext for multi-tenant scenarios.

  • Additional Tenant Context: In a complex scenario, you might want to not only switch the connection string but also filter data inside the report (for example, if tenants share a database but have tenant-specific fields). In this case, you could pass a tenant ID as a report parameter to filter queries.

For more information, refer to the official ActiveReports.NET documentation and samples. The ActiveReports docs provide guidance on JSViewer integration and custom report providers, as well as other storage options and security considerations. With these tools, you can confidently implement multi-tenant reporting, ensuring each tenant sees the right data, all while reusing common report layouts across your application.

Ready to check it out? Download ActiveReports.NET Today!

Tags: