<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dotnet Report</title>
    <description>The latest articles on DEV Community by Dotnet Report (@dotnetreport).</description>
    <link>https://dev.to/dotnetreport</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3854625%2F95752761-4882-4cdf-9d93-1345bd3c679b.png</url>
      <title>DEV Community: Dotnet Report</title>
      <link>https://dev.to/dotnetreport</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dotnetreport"/>
    <language>en</language>
    <item>
      <title>Build vs Buy: The Real Cost of Adding Reports to Your .NET SaaS Product</title>
      <dc:creator>Dotnet Report</dc:creator>
      <pubDate>Sat, 04 Apr 2026 04:33:46 +0000</pubDate>
      <link>https://dev.to/dotnetreport/build-vs-buy-the-real-cost-of-adding-reports-to-your-net-saas-product-4b5h</link>
      <guid>https://dev.to/dotnetreport/build-vs-buy-the-real-cost-of-adding-reports-to-your-net-saas-product-4b5h</guid>
      <description>&lt;p&gt;Every .NET SaaS team hits this moment: customers start asking for reports and dashboards. Do you build it yourself or use an embedded reporting solution?&lt;/p&gt;

&lt;p&gt;The "build it" answer feels right at first. You know SQL, you know your data model, you have a charting library picked out. But there's a pattern I've seen repeatedly: teams underestimate the ongoing maintenance cost and end up with a reporting system that consumes 20–30% of a developer's time indefinitely.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You're Actually Building
&lt;/h2&gt;

&lt;p&gt;A production reporting system for a multi-tenant SaaS requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Query engine&lt;/strong&gt; — filter, sort, group, aggregate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-tenant data isolation&lt;/strong&gt; — tenant-scoped at the query layer (not just UI)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Chart rendering + configuration UI&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashboard layout engine&lt;/strong&gt; (if you want multi-widget dashboards)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Saved report persistence&lt;/strong&gt; + permissions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PDF/Excel export&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scheduled email delivery&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Year 1 estimate for a custom build: &lt;strong&gt;460–780 developer hours&lt;/strong&gt; (~$57,500–$97,500 at market rates). That's before self-service report creation — if customers want to define their own reports, add 200–400 more hours.&lt;/p&gt;

&lt;p&gt;Year 2+: ~20% of a senior developer's time for maintenance, new reports, schema migrations. That's $40,000–$60,000/year ongoing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Opportunity Cost
&lt;/h2&gt;

&lt;p&gt;Every sprint spent on reporting infrastructure is a sprint not spent on your core product. Reporting is table stakes — customers expect it, but it's not what differentiates your SaaS.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Buy
&lt;/h2&gt;

&lt;p&gt;For most .NET SaaS products: buying an embedded reporting solution is the right call. &lt;a href="https://dotnetreport.com" rel="noopener noreferrer"&gt;Dotnet Report&lt;/a&gt; ships as a NuGet package with the full stack included — report builder, dashboards, charts, scheduling, export, multi-tenant isolation. Integration takes 1–2 weeks. Ongoing maintenance is minimal.&lt;/p&gt;

&lt;p&gt;The math: custom build saves you the license cost but costs you 460+ hours in Year 1 and $40K+ every year after. The break-even is usually never — especially when you factor in opportunity cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Build
&lt;/h2&gt;

&lt;p&gt;Building makes sense if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reporting IS your core product (you're a BI/analytics SaaS)&lt;/li&gt;
&lt;li&gt;You have highly specific domain requirements no commercial solution fits&lt;/li&gt;
&lt;li&gt;You have dedicated reporting engineers with specialized domain expertise&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For everyone else: the buy case is strong.&lt;/p&gt;




&lt;p&gt;Full cost breakdown with tables: &lt;a href="https://dotnetreport.com/blogs/build-vs-buy-reporting-net-saas/" rel="noopener noreferrer"&gt;dotnetreport.com/blogs/build-vs-buy-reporting-net-saas/&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Dotnet Report vs. Telerik Reporting: Which Is Right for Your .NET SaaS?</title>
      <dc:creator>Dotnet Report</dc:creator>
      <pubDate>Sat, 04 Apr 2026 03:13:09 +0000</pubDate>
      <link>https://dev.to/dotnetreport/dotnet-report-vs-telerik-reporting-which-is-right-for-your-net-saas-40oe</link>
      <guid>https://dev.to/dotnetreport/dotnet-report-vs-telerik-reporting-which-is-right-for-your-net-saas-40oe</guid>
      <description>&lt;p&gt;If you're evaluating embedded reporting for a .NET SaaS application, Telerik Reporting and Dotnet Report will both appear on your list. They're both mature, .NET-native products — but they're designed around fundamentally different workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Difference
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Telerik Reporting&lt;/strong&gt; is designed for developer-authored report delivery. A developer or report author uses the Telerik Visual Studio designer to create report definitions. Those reports are then delivered to end users who can view and filter them. Users can't build reports from scratch without designer access.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dotnet Report&lt;/strong&gt; is designed for user self-service. You expose your data model to Dotnet Report's engine, and your customers use a drag-and-drop builder to create their own reports — choosing columns, setting filters, picking chart types. Developer involvement per report drops to near zero after initial setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-Tenant Isolation: The Key Difference for SaaS
&lt;/h2&gt;

&lt;p&gt;Telerik doesn't have built-in multi-tenant isolation. If you serve multiple clients from a shared database, you implement tenant scoping yourself in every report definition. That works but creates maintenance risk — one missed filter can expose one client's data to another.&lt;/p&gt;

&lt;p&gt;Dotnet Report enforces isolation at the query layer via a &lt;code&gt;GetCurrentUser&lt;/code&gt; endpoint you implement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"getCurrentUser"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Authorize&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;GetCurrentUser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;clientId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindFirst&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tenant_id"&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;dataFilters&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"TenantId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindFirst&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tenant_id"&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;allowedTables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"Orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Customers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Products"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These filters are applied server-side before any SQL executes. Users cannot override them in the report builder — they're enforced at the query layer, not the UI layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing Model Comparison
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Telerik Reporting:&lt;/strong&gt; Per developer per year (~$1,500-$2,000+ per seat as part of DevCraft Complete). Cost rises with team size.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dotnet Report:&lt;/strong&gt; Fixed subscription regardless of the number of developers, end users, or tenants. Predictable and flat as you scale.&lt;/p&gt;

&lt;p&gt;For SaaS economics, the distinction matters: as you add more customers and grow your team, Telerik costs scale up with headcount. Dotnet Report stays flat.&lt;/p&gt;

&lt;h2&gt;
  
  
  User Self-Service
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Telerik:&lt;/strong&gt; Provides a Web Report Designer for power users and report authors to create/edit reports in a browser. For non-technical end users who need to build reports from scratch, the UX requires training.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dotnet Report:&lt;/strong&gt; The drag-and-drop report builder is designed for non-technical business users. Finance managers, ops leads, customer success teams can build their own reports without SQL knowledge or training. Comparable in difficulty to building a pivot table in Excel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full Feature Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Dotnet Report&lt;/th&gt;
&lt;th&gt;Telerik Reporting&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;User self-service builder&lt;/td&gt;
&lt;td&gt;Full drag-and-drop&lt;/td&gt;
&lt;td&gt;Limited (power user designer)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-tenant isolation&lt;/td&gt;
&lt;td&gt;Built-in at query layer&lt;/td&gt;
&lt;td&gt;DIY implementation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pricing model&lt;/td&gt;
&lt;td&gt;Fixed subscription&lt;/td&gt;
&lt;td&gt;Per developer per year&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Open-source front-end&lt;/td&gt;
&lt;td&gt;Yes (MIT license)&lt;/td&gt;
&lt;td&gt;No (proprietary viewers)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;White-label branding&lt;/td&gt;
&lt;td&gt;Full control&lt;/td&gt;
&lt;td&gt;CSS theming only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Report scheduling&lt;/td&gt;
&lt;td&gt;Built-in, user-configurable&lt;/td&gt;
&lt;td&gt;Requires Reporting Server add-on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dashboard builder&lt;/td&gt;
&lt;td&gt;Included&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixel-perfect print reports&lt;/td&gt;
&lt;td&gt;Operational focus&lt;/td&gt;
&lt;td&gt;Strong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Non-relational data sources&lt;/td&gt;
&lt;td&gt;Relational only&lt;/td&gt;
&lt;td&gt;JSON, CSV, web service, etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  When to Choose Telerik
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You need pixel-perfect, print-layout reports (invoices, regulatory documents)&lt;/li&gt;
&lt;li&gt;A dedicated report author creates and maintains all report definitions&lt;/li&gt;
&lt;li&gt;You already have Telerik DevCraft licenses&lt;/li&gt;
&lt;li&gt;Reporting consumers are internal users, not SaaS customers&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When to Choose Dotnet Report
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Your customers need self-service (they build their own reports without developer involvement)&lt;/li&gt;
&lt;li&gt;Multi-tenant data isolation needs to be bulletproof and enforced at the query layer&lt;/li&gt;
&lt;li&gt;You want flat pricing that doesn't scale with developer headcount or tenant count&lt;/li&gt;
&lt;li&gt;You need full white-label control (the Angular front-end is open-source)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full comparison: &lt;a href="https://dotnetreport.com/blogs/dotnet-report-vs-telerik-reporting/" rel="noopener noreferrer"&gt;dotnetreport.com/blogs/dotnet-report-vs-telerik-reporting/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dotnetreport.com" rel="noopener noreferrer"&gt;Start a free 30-day trial of Dotnet Report&lt;/a&gt; — no credit card required.&lt;/p&gt;

</description>
      <category>analytics</category>
      <category>dotnet</category>
      <category>saas</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Angular Reporting with a .NET Backend: What Actually Works in 2026</title>
      <dc:creator>Dotnet Report</dc:creator>
      <pubDate>Sat, 04 Apr 2026 03:04:57 +0000</pubDate>
      <link>https://dev.to/dotnetreport/angular-reporting-with-a-net-backend-what-actually-works-in-2026-1744</link>
      <guid>https://dev.to/dotnetreport/angular-reporting-with-a-net-backend-what-actually-works-in-2026-1744</guid>
      <description>&lt;p&gt;Adding reporting to an Angular + ASP.NET Core app sounds straightforward. You have SQL, Angular, and Chart.js. How hard can it be?&lt;/p&gt;

&lt;p&gt;Harder than it looks, especially for multi-tenant SaaS.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Challenges
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;CORS and auth passthrough.&lt;/strong&gt; Your Angular app needs to call your ASP.NET Core API to fetch report data. That API needs to know who the current user is (for tenant isolation) and what they're allowed to see. JWT tokens, CORS headers, and security policies all add complexity before you write a single line of report logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-tenant data isolation.&lt;/strong&gt; This is where teams most often introduce bugs. The rule: tenant filtering must happen server-side in your ASP.NET Core API, never in the Angular component. A user could modify Angular component state. They can't modify your server-side WHERE clause.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chart rendering across frameworks.&lt;/strong&gt; Angular has great charting libraries (ng2-charts, Highcharts, ECharts). But integrating them with dynamic, user-defined report configurations — where the chart type, X axis, and grouping are all user-controlled — requires significant glue code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture That Works
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Angular service to fetch report data&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;providedIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ReportService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="nf"&gt;runReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ReportConfig&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Observable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ReportResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ReportResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/reports/run&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ASP.NET Core: always enforce tenant isolation server-side&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"run"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;RunReport&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;FromBody&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;ReportConfig&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tenantId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_userContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetTenantId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataFilters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Filter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"TenantId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_reportEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Evaluation Criteria for Angular Reporting Solutions
&lt;/h2&gt;

&lt;p&gt;When evaluating reporting tools for an Angular + .NET stack, prioritize:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Native Angular component&lt;/strong&gt; — not an iframe or server-rendered widget&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-side tenant enforcement&lt;/strong&gt; — the tool must enforce data isolation at the query layer, not just the UI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic report configuration&lt;/strong&gt; — users should be able to define columns, filters, and chart type without developer involvement&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JWT / auth integration&lt;/strong&gt; — passes through your existing auth context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export to PDF/Excel&lt;/strong&gt; — table stakes for business users&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduling&lt;/strong&gt; — automated report delivery via email&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Top Options for Angular + .NET Reporting in 2026
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Dotnet Report — Best for Multi-Tenant SaaS
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://dotnetreport.com" rel="noopener noreferrer"&gt;Dotnet Report&lt;/a&gt; ships an open-source Angular component (MIT license) that integrates with your ASP.NET Core API. You implement a GetCurrentUser endpoint that returns tenant context — the reporting engine applies it as a mandatory WHERE filter on every query.&lt;/p&gt;

&lt;p&gt;What you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Drag-and-drop report builder in Angular (open-source, customizable)&lt;/li&gt;
&lt;li&gt;SQL Server, MySQL, PostgreSQL support&lt;/li&gt;
&lt;li&gt;Multi-tenant enforcement via GetCurrentUser endpoint&lt;/li&gt;
&lt;li&gt;PDF, Excel, CSV export&lt;/li&gt;
&lt;li&gt;Report scheduling with email delivery&lt;/li&gt;
&lt;li&gt;Dashboard builder with user-defined KPI widgets&lt;/li&gt;
&lt;li&gt;Fixed subscription pricing (no per-user fees)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Integration: 1-2 weeks for a typical ASP.NET Core app.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Telerik Reporting
&lt;/h3&gt;

&lt;p&gt;Telerik's Angular Report Viewer component wraps server-side generated reports. The reports themselves must be authored in Telerik's designer — end users can view and filter, but can't build new reports. Good for developer-designed report delivery; not for self-service.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. DevExpress Web Report Viewer
&lt;/h3&gt;

&lt;p&gt;Similar to Telerik: Angular wrapper for server-rendered reports. Strong designer tooling, developer-oriented workflow. Per-developer licensing model that scales poorly for SaaS products serving many tenants.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. AG Grid + Custom Build
&lt;/h3&gt;

&lt;p&gt;AG Grid handles tabular display very well. Combined with a custom query builder and charting library, you can assemble a reporting solution — but expect 460-780 hours to build the full stack (query engine, multi-tenant enforcement, export, scheduling, saved reports). Only makes sense if reporting is your core product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-Tenant Enforcement Pattern (Critical)
&lt;/h2&gt;

&lt;p&gt;This is the pattern most teams get wrong. Here is the correct implementation:&lt;/p&gt;

&lt;p&gt;The key: dataFilters are applied server-side before any SQL executes. Users can add additional filters in the Angular report builder, but they cannot remove or override the tenant filter. This is what makes the isolation production-safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Dotnet Report&lt;/th&gt;
&lt;th&gt;Telerik&lt;/th&gt;
&lt;th&gt;DevExpress&lt;/th&gt;
&lt;th&gt;Custom Build&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Angular component&lt;/td&gt;
&lt;td&gt;Open-source&lt;/td&gt;
&lt;td&gt;Viewer only&lt;/td&gt;
&lt;td&gt;Viewer only&lt;/td&gt;
&lt;td&gt;Build it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User self-service&lt;/td&gt;
&lt;td&gt;Full builder&lt;/td&gt;
&lt;td&gt;View only&lt;/td&gt;
&lt;td&gt;View only&lt;/td&gt;
&lt;td&gt;Build it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-tenant enforcement&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;td&gt;DIY&lt;/td&gt;
&lt;td&gt;DIY&lt;/td&gt;
&lt;td&gt;Build it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PDF / Excel export&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Build it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scheduling&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Build it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pricing model&lt;/td&gt;
&lt;td&gt;Fixed subscription&lt;/td&gt;
&lt;td&gt;Per developer&lt;/td&gt;
&lt;td&gt;Per developer&lt;/td&gt;
&lt;td&gt;Developer time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time to integrate&lt;/td&gt;
&lt;td&gt;1-2 weeks&lt;/td&gt;
&lt;td&gt;2-4 weeks&lt;/td&gt;
&lt;td&gt;2-4 weeks&lt;/td&gt;
&lt;td&gt;460-780 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Reporting in an Angular + .NET app is solvable, but the multi-tenant data isolation piece is where most teams introduce risk. Get that wrong and you have a security incident, not a reporting feature.&lt;/p&gt;

&lt;p&gt;If you are building a SaaS product where reporting is a customer-facing feature, the build-vs-buy math strongly favors using an embedded reporting package. The 1-2 week integration cost vs. 460-780 hours to build the full stack is a significant difference in calendar time and engineering resources.&lt;/p&gt;

&lt;p&gt;Full guide with integration walkthrough: &lt;a href="https://dotnetreport.com/blogs/angular-reporting-dotnet-backend-2026/" rel="noopener noreferrer"&gt;https://dotnetreport.com/blogs/angular-reporting-dotnet-backend-2026/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Start a free 30-day trial: &lt;a href="https://dotnetreport.com" rel="noopener noreferrer"&gt;https://dotnetreport.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>angular</category>
      <category>architecture</category>
      <category>dotnet</category>
      <category>saas</category>
    </item>
    <item>
      <title>Build vs Buy: The Real Cost of Adding Reports to Your .NET SaaS Product</title>
      <dc:creator>Dotnet Report</dc:creator>
      <pubDate>Fri, 03 Apr 2026 03:06:50 +0000</pubDate>
      <link>https://dev.to/dotnetreport/build-vs-buy-the-real-cost-of-adding-reports-to-your-net-saas-product-3gbf</link>
      <guid>https://dev.to/dotnetreport/build-vs-buy-the-real-cost-of-adding-reports-to-your-net-saas-product-3gbf</guid>
      <description>&lt;p&gt;Every .NET SaaS team hits this moment: customers start asking for reports and dashboards. Do you build it yourself or use an embedded reporting solution?&lt;/p&gt;

&lt;p&gt;The "build it" answer feels right at first. You know SQL, you know your data model, you have a charting library picked out. But there's a pattern I've seen repeatedly: teams underestimate the ongoing maintenance cost and end up with a reporting system that consumes 20–30% of a developer's time indefinitely.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You're Actually Building
&lt;/h2&gt;

&lt;p&gt;A production reporting system for a multi-tenant SaaS requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Query engine&lt;/strong&gt; — filter, sort, group, aggregate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-tenant data isolation&lt;/strong&gt; — tenant-scoped at the query layer (not just UI)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Chart rendering + configuration UI&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashboard layout engine&lt;/strong&gt; (if you want multi-widget dashboards)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Saved report persistence&lt;/strong&gt; + permissions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PDF/Excel export&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scheduled email delivery&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Year 1 estimate for a custom build: &lt;strong&gt;460–780 developer hours&lt;/strong&gt; (~$57,500–$97,500 at market rates).&lt;/p&gt;

&lt;p&gt;Year 2+: ~20% of a senior developer's time for maintenance. That's $40,000–$60,000/year ongoing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Opportunity Cost
&lt;/h2&gt;

&lt;p&gt;Every sprint spent on reporting infrastructure is a sprint not spent on your core product.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Buy
&lt;/h2&gt;

&lt;p&gt;For most .NET SaaS products: buying an embedded reporting solution is the right call. &lt;a href="https://dotnetreport.com" rel="noopener noreferrer"&gt;Dotnet Report&lt;/a&gt; ships as a NuGet package with the full stack included — report builder, dashboards, charts, scheduling, export, multi-tenant isolation. Integration takes 1–2 weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Build
&lt;/h2&gt;

&lt;p&gt;Building makes sense if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reporting IS your core product&lt;/li&gt;
&lt;li&gt;You have highly specific domain requirements no commercial solution fits&lt;/li&gt;
&lt;li&gt;You have dedicated reporting engineers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For everyone else: the buy case is strong.&lt;/p&gt;




&lt;p&gt;Full cost breakdown: &lt;a href="https://dotnetreport.com/blogs/build-vs-buy-reporting-net-saas/" rel="noopener noreferrer"&gt;dotnetreport.com/blogs/build-vs-buy-reporting-net-saas/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>saas</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Row-Level Security in Embedded Reporting: The Patterns That Actually Work for .NET SaaS</title>
      <dc:creator>Dotnet Report</dc:creator>
      <pubDate>Wed, 01 Apr 2026 05:27:21 +0000</pubDate>
      <link>https://dev.to/dotnetreport/row-level-security-in-embedded-reporting-the-patterns-that-actually-work-for-net-saas-4enn</link>
      <guid>https://dev.to/dotnetreport/row-level-security-in-embedded-reporting-the-patterns-that-actually-work-for-net-saas-4enn</guid>
      <description>&lt;p&gt;When you add embedded reporting to a multi-tenant SaaS product, row-level security isn't optional — it's the whole ballgame. One misconfigured RLS policy and you're serving Tenant A's data to Tenant B. In a B2B context, that's a trust violation that ends customer relationships.&lt;/p&gt;

&lt;p&gt;Here's a practical breakdown of RLS patterns for embedded reporting in ASP.NET Core, including the vulnerability that catches most teams.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Critical Rule
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tenant ID must come from the server-side authenticated session. Always.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This pattern is wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// DON'T DO THIS&lt;/span&gt;
&lt;span class="nx"&gt;reportTool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tenantId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A malicious user changes that value. You have a cross-tenant data breach.&lt;/p&gt;

&lt;p&gt;This pattern is correct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Server-side — user cannot tamper with this&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DotNetReportUser&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetCurrentUser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;DotNetReportUser&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ClientId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HttpContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindFirstValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"TenantId"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;IsAdmin&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsInRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ReportAdmin"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Two Layers of RLS
&lt;/h2&gt;

&lt;p&gt;Effective RLS in reporting requires:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1: Tenant isolation&lt;/strong&gt; — Company A never sees Company B's data, even a single row, even in aggregates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2: User-level scoping&lt;/strong&gt; — Within Tenant A, a regional manager only sees their region's records.&lt;/p&gt;

&lt;p&gt;Most implementations get Layer 1 right but skip Layer 2 for "later." Later often doesn't come until a customer notices a regional manager can see another region's data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defense in Depth with SQL Server RLS
&lt;/h2&gt;

&lt;p&gt;Application-layer RLS is necessary but not sufficient. Add SQL Server's native Row-Level Security as a backstop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fn_tenantPredicate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;TenantId&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;SCHEMABINDING&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;result&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;TenantId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SESSION_CONTEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'TenantId'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;SECURITY&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TenantPolicy&lt;/span&gt;
    &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="n"&gt;PREDICATE&lt;/span&gt; &lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fn_tenantPredicate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TenantId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;STATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set the session context in an EF Core interceptor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TenantRlsInterceptor&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DbCommandInterceptor&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="n"&gt;InterceptionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DbDataReader&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ReaderExecuting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;DbCommand&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CommandEventData&lt;/span&gt; &lt;span class="n"&gt;eventData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;InterceptionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DbDataReader&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;SetTenantContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Connection&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReaderExecuting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eventData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SetTenantContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DbConnection&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateCommand&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CommandText&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"EXEC sp_set_session_context @key=N'TenantId', @value=@t"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Parameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SqlParameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"@t"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_tenantService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetCurrentTenantId&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
        &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteNonQuery&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Test Your RLS
&lt;/h2&gt;

&lt;p&gt;Write dedicated cross-tenant isolation tests — not just happy-path functional tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Report_ShouldNotReturnCrossTenantData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;SeedData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tenant-a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recordCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;SeedData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tenant-b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recordCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;CreateTestUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"tenant-a"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_reportService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RunReportAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;testReportId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Must only return tenant-a's records&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;All&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tenant-a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"TenantId"&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Full Writeup
&lt;/h2&gt;

&lt;p&gt;This is a condensed version — the full article covers user-level scoping patterns, the common RLS vulnerabilities (cached results without tenant keys, inconsistent table coverage, aggregate leakage), and a complete comparison table of approaches:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://dotnetreport.com/blogs/row-level-security-embedded-reporting-net/" rel="noopener noreferrer"&gt;https://dotnetreport.com/blogs/row-level-security-embedded-reporting-net/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Building embedded reporting into a .NET SaaS product? Happy to answer questions in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with &lt;a href="https://dotnetreport.com" rel="noopener noreferrer"&gt;Dotnet Report&lt;/a&gt; — embedded self-service reporting for multi-tenant ASP.NET Core applications.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>security</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
