AI Analytics Dashboards in Blazor: Natural Language to Charts

Every business has SQL Server full of data nobody is reading. The CEO asks "what was last quarter's revenue by region trending into this one" and someone opens a BI tool, drags fields onto a canvas, gets a chart, and emails it. That cycle takes 30 minutes — and only people who know the data model and the BI tool can do it. The 2026 fix is natural-language analytics: the user types a question in plain English, an LLM turns it into a parameterized SQL query against your schema, the result renders as a chart, and ML.NET layers in forecasts and anomaly flags. Built into a Blazor dashboard, this becomes "ask the database" — usable by anyone.

This guide walks through the four pieces of a production analytics dashboard in .NET 10: natural-language-to-SQL, schema-constrained query generation, Blazor charting that reacts to the query result, and ML.NET layered on top for forecasting and anomaly detection.

NL→SQLSchema-grounded generation

.NET 10LTS runtime

ML.NETForecasting + anomaly

The analytics stack in .NET 10

Microsoft.Extensions.AI

IChatClient + structured outputs turn "show me revenue by region last quarter" into a SQL query and a chart-type recommendation.

✅ Data layer

SQL Server 2022 + Dapper

Generated queries execute against a read-only replica or read-only connection. Dapper handles parameterized execution and result mapping.

✅ Charting

Blazor + ChartJs.Blazor / ApexCharts

Reactive charts that re-render when data changes. The LLM picks chart type from query shape; Blazor renders it.

✅ Forecasting

ML.NET time series

SSA forecasting + SR-CNN anomaly detection. Native .NET, no Python in the path, runs inside the same app pool.

🟡 Mandatory

SQL safety net

Read-only DB user, query allowlist, timeout, row-count cap. The LLM is not allowed to write or run unbounded scans.

🟡 Optional

Vector-indexed schema

For large schemas, retrieve the relevant tables/columns via embeddings before passing context to the LLM. Saves tokens and improves accuracy.

Quick reference: the four-stage pipeline

  • Natural language to SQL: the core trick

Modern GPT-4-class models are remarkably good at SQL when given the schema as context. The bottleneck used to be schema understanding; now it's getting the right slice of schema into the prompt. For schemas under 30 tables, send everything; for larger, embed the tables and retrieve the relevant subset per query.

The prompt structure: tell the model the schema, tell it the dialect (SQL Server T-SQL), tell it the safety rules, give it the question, get back a structured response with the query and a chart suggestion.

public record QueryPlan(

string Sql,

string ChartType, // "line" | "bar" | "pie" | "table" | "single-value"

string XAxis,

string YAxis,

string Title,

string Explanation);

public async Task<QueryPlan> PlanQueryAsync(string userQuestion, string schemaText)

{

var systemPrompt = $@"You generate SQL Server queries (T-SQL syntax) for a business analytics dashboard.

SCHEMA (read-only):

{schemaText}

RULES:

  • Output a single SELECT statement. No INSERT, UPDATE, DELETE, MERGE, EXEC, or DDL.
  • Always include explicit column names. No SELECT *.
  • Always include WHERE date filters when the question mentions time ('last quarter', 'this year', etc.).
  • Use TOP 1000 or a reasonable LIMIT for safety.
  • Pick the chart type that best fits the result: line for time-series, bar for category comparison, pie for proportions, single-value for KPIs, table when the result has too many dimensions to chart.

Output JSON: {{ ""sql"": ""..."", ""chartType"": ""..."", ""xAxis"": ""..."", ""yAxis"": ""..."", ""title"": ""..."", ""explanation"": ""..."" }}";

var response = await _chatClient.GetResponseAsync<QueryPlan>(

new List<ChatMessage>

{

new(ChatRole.System, systemPrompt),

new(ChatRole.User, userQuestion)

},

new ChatOptions { Temperature = 0.1 });

return response.Result;

}

Schema retrieval for large databases

If your schema has 100+ tables, sending all of it on every query is expensive and dilutes the model's attention. Pre-embed each table's metadata (name, columns, key business meaning) once, then retrieve the top-K relevant tables per question. Same RAG mechanic as our RAG guide but applied to schema instead of documents.

public async Task<string> GetRelevantSchemaAsync(string question)

{

var qVec = await _embedder.GenerateAsync(new[] { question });

var topTables = await _vectorStore.SearchAsync(qVec[0].Vector.ToArray(), limit: 8);

return string.Join("\n\n", topTables.Select(t => t.SchemaText));

}

  • Safe execution: the guardrails layer

An LLM that emits SQL will eventually emit destructive SQL, no matter how good your prompt. Defense in depth: a read-only SQL Server user, a query parser that rejects non-SELECT statements before execution, a timeout that kills runaway queries, and a row-count cap that prevents accidental millions-of-rows scans.

public class SafeQueryRunner(string readOnlyConnectionString)

{

private static readonly Regex AllowedPattern = new(

@"^\sWITH\s|^\sSELECT\s",

RegexOptions.IgnoreCase | RegexOptions.Compiled);

private static readonly Regex ForbiddenKeywords = new(

@"\b(INSERT|UPDATE|DELETE|MERGE|TRUNCATE|DROP|ALTER|CREATE|EXEC|EXECUTE|sp_)\b",

RegexOptions.IgnoreCase | RegexOptions.Compiled);

public async Task<DataResult> ExecuteAsync(string sql, TimeSpan timeout, int maxRows = 5000)

{

if (!AllowedPattern.IsMatch(sql))

throw new InvalidOperationException("Only SELECT statements are allowed.");

if (ForbiddenKeywords.IsMatch(sql))

throw new InvalidOperationException("Query contains forbidden keywords.");

using var conn = new SqlConnection(readOnlyConnectionString);

conn.Open();

using var cmd = new SqlCommand(sql, conn)

{

CommandTimeout = (int)timeout.TotalSeconds

};

var rows = new List<Dictionary<string, object>>();

using var reader = await cmd.ExecuteReaderAsync();

while (await reader.ReadAsync() && rows.Count < maxRows)

{

var row = new Dictionary<string, object>();

for (int i = 0; i < reader.FieldCount; i++)

row[reader.GetName(i)] = reader.GetValue(i);

rows.Add(row);

}

return new DataResult(rows, reader.FieldCount, conn.Database);

}

}

Pair this with a SQL Server login that has SELECT permission only on the views your dashboard exposes — not on the underlying tables. A LLM-induced destructive query then fails twice: once at the application validator, once at the database permission layer.

  • Blazor rendering: charts from query plans

The query plan from stage 1 tells you what kind of chart to render. A switch over ChartType picks the right component. The user sees both the chart and the underlying SQL — transparency builds trust.

@page "/analytics"

@inject IAnalyticsService Analytics

<input @bind="_question" @bind:event="onchange" placeholder="Ask a question about your data..." />

<button @onclick="RunQuery" disabled="@_busy">Ask</button>

@if (_plan is { } plan && _data is { } data)

{

<h2>@plan.Title</h2>

<p class="text-muted-foreground">@plan.Explanation</p>

@switch (plan.ChartType)

{

case "line":

<LineChart Data="data" XField="@plan.XAxis" YField="@plan.YAxis" />

break;

case "bar":

<BarChart Data="data" XField="@plan.XAxis" YField="@plan.YAxis" />

break;

case "pie":

<PieChart Data="data" LabelField="@plan.XAxis" ValueField="@plan.YAxis" />

break;

case "single-value":

<KpiCard Value="@data[0][plan.YAxis]" Label="@plan.Title" />

break;

default:

<DataTable Rows="data" />

break;

}

<details>

<summary>View SQL</summary>

<pre><code>@plan.Sql</code></pre>

</details>

}

@code {

string _question = "";

QueryPlan? _plan;

List<Dictionary<string, object>>? _data;

bool _busy;

async Task RunQuery()

{

_busy = true;

try

{

_plan = await Analytics.PlanAsync(_question);

_data = (await Analytics.ExecuteAsync(_plan.Sql)).Rows;

}

finally { _busy = false; }

}

}

The conversational refinement loop

The first answer is often close but not quite right. Users want to follow up: "now break that down by product", "show only the top 5", "compare to last year". Pass the prior conversation into the next planning call:

async Task FollowUpAsync(string followUp)

{

var messages = new List<ChatMessage>

{

new(ChatRole.System, systemPromptWithSchema),

new(ChatRole.User, _previousQuestion),

new(ChatRole.Assistant, $"Generated SQL: {_plan!.Sql}\nExplanation: {_plan.Explanation}"),

new(ChatRole.User, followUp)

};

var response = await _chatClient.GetResponseAsync<QueryPlan>(messages,

new ChatOptions { Temperature = 0.1 });

_plan = response.Result;

_data = (await _runner.ExecuteAsync(_plan.Sql, ...)).Rows;

}

  • ML.NET overlays: forecasts + anomaly flags

LLMs don't do time-series forecasting well. They produce plausible-looking numbers that aren't grounded in trend/seasonality decomposition. ML.NET's SsaForecastingEstimator and SrCnnAnomalyEstimator are classic statistical methods that have been production-ready for years. Use them for what they're good at; use the LLM for what it's good at.

When the query plan returns time-series data, layer a forecast and anomaly detection on top of the chart:

public class TimeSeriesAnalysis

{

private readonly MLContext _ml = new(seed: 42);

public List<ForecastPoint> Forecast(List<DataPoint> series, int horizonDays = 30)

{

var data = _ml.Data.LoadFromEnumerable(series);

var pipeline = _ml.Forecasting.ForecastBySsa(

outputColumnName: nameof(ForecastOutput.ForecastedValue),

inputColumnName: nameof(DataPoint.Value),

windowSize: 14,

seriesLength: series.Count,

trainSize: series.Count,

horizon: horizonDays,

confidenceLevel: 0.95f,

confidenceLowerBoundColumn: nameof(ForecastOutput.LowerBound),

confidenceUpperBoundColumn: nameof(ForecastOutput.UpperBound));

var model = pipeline.Fit(data);

var forecaster = model.CreateTimeSeriesEngine<DataPoint, ForecastOutput>(_ml);

var forecast = forecaster.Predict();

return forecast.ForecastedValue

.Select((v, i) => new ForecastPoint(

series.Last().Date.AddDays(i + 1), v,

forecast.LowerBound[i], forecast.UpperBound[i]))

.ToList();

}

public List<AnomalyPoint> DetectAnomalies(List<DataPoint> series)

{

var data = _ml.Data.LoadFromEnumerable(series);

var pipeline = _ml.Transforms.DetectAnomalyBySrCnn(

outputColumnName: nameof(SrCnnOutput.Prediction),

inputColumnName: nameof(DataPoint.Value),

windowSize: 14,

backAddWindowSize: 5,

lookaheadWindowSize: 5,

averagingWindowSize: 3,

judgementWindowSize: 14,

threshold: 0.3);

var transformed = pipeline.Fit(data).Transform(data);

var preds = _ml.Data.CreateEnumerable<SrCnnOutput>(transformed, false);

return preds.Zip(series, (p, s) => new { p, s })

.Where(x => x.p.Prediction[0] == 1)

.Select(x => new AnomalyPoint(x.s.Date, x.s.Value, x.p.Prediction[2]))

.ToList();

}

}

The Blazor chart component renders the historical line, the forecasted line in a distinct color with the confidence band shaded, and anomaly points as red dots. The dashboard tells a story: "here's what happened, here's where you went off-trend, here's where you're heading."

  • Putting it all together

A complete analytics page does this in sequence on every question:

User types a question, presses Enter.

Front-end calls the planner endpoint with the question + conversation history.

Planner retrieves the relevant schema slice, calls the LLM, gets back a query plan.

Runner validates the SQL, executes against read-only replica, caps rows, applies timeout.

Server detects time-series shape, runs ML.NET forecast + anomaly detection if applicable.

Blazor renders the appropriate chart, the SQL details, the explanation, and the forecast/anomaly overlay.

User refines: "show that as monthly", "include last year", "exclude returns". Each refinement adds to the conversation context and re-runs the loop.

Use cases that pay back fast

Executive dashboards. CEOs and managers ask questions in their own words; the dashboard returns charts. No more "can someone pull X for me by Friday."

Customer-facing analytics. Embed inside your product so customers can query their own data. SaaS differentiator.

Operational monitoring. "Show me orders that haven't shipped within 48 hours grouped by warehouse." Refresh on a schedule.

Sales analytics. Pipeline questions, win-rate analysis, rep performance — all answerable in natural language.

Financial reporting. Revenue, expenses, margins, forecasts — combined with ML.NET's seasonality decomposition.

Production patterns

Query caching

Common questions repeat. Cache (question_hash → query_plan) with a 1-hour TTL. Cache (query_sql → results) with a shorter TTL. Saves cost and improves latency.

Permission-aware queries

Multi-tenant analytics needs row-level security. Inject a tenant_id filter into every generated query before execution — either via a query rewriter or by routing each tenant to a separate schema. Don't trust the LLM to add the filter.

Read-replica routing

Analytics queries can run long. Route them to a read replica, not your operational primary. SQL Server's Always On readable secondaries are the standard pattern.

Quality monitoring

Log every question, the generated SQL, the row count, and whether the user took further action (refinement, export, share). Questions with zero follow-up + high row count usually indicate the model didn't understand the question. Sample these for prompt tuning.

Cost

Per-question cost runs ~$0.005-0.02 depending on schema size and follow-up depth. For 1,000 questions per day, that's $5-20/day in LLM cost. The forecast and anomaly detection run entirely on your own infrastructure — no per-call cost.

Hosting recommendations

ASP.NET Business — $17.49/mo

Customer-facing analytics in SaaS, multi-user dashboards with concurrent questions, ML.NET forecasting in-process. 2 GB headroom.

View Business plan →

ASP.NET Professional — $27.49/mo

Multi-tenant analytics platforms, white-label BI products, agency-built customer dashboards. 4 GB per pool, highest priority scheduling.

View Professional plan →

FAQs

How accurate is natural-language-to-SQL?

For schemas the model has good context on, 85-95% syntactically correct queries on first attempt. The remaining gap is semantic — the SQL ran, but answered a slightly different question. Showing the user the SQL + explanation lets them catch these immediately.

Can the LLM see my actual data?

Only if you let it. The planning stage sees schema (table names, column names, types, comments) — not data values. The execution happens server-side; results stream back to Blazor. To completely prevent any data exposure to a hosted model, use a self-hosted LLM (see our self-hosted inference guide) for the planning step.

What about non-relational data?

The pattern generalizes: replace SQL with whatever query language fits — KQL for Application Insights, MongoDB query objects, REST API calls. The LLM is good at writing query DSLs given the schema or API spec as context.

Can I let end customers ask questions about their own data?

Yes — this is the killer SaaS feature. Make absolutely sure your row-level-security and tenant isolation are bulletproof, generated queries go through your filter injector, and the LLM never sees data from one tenant when planning a query for another.

What if the question is ambiguous?

Have the model ask back. Allow the planner to return {"clarification": "Did you mean revenue by sales region or shipping region?"} instead of a SQL plan. The Blazor UI surfaces the clarification, the user answers, planning continues. This is the conversational refinement loop applied at the very first step.

How do I prevent SQL injection?

LLM-generated SQL isn't user-supplied SQL — there's no string interpolation of user input into the query. The whole query comes from the model, executes via Dapper with no string concatenation, and the read-only DB user can't modify data even if a bad query slipped through. The risk profile is different from classic injection.

Ship it

Natural-language analytics is the highest-leverage AI feature for any business with substantial data. It removes the BI-tool middleman, brings analytics to people who don't write SQL, and combines LLM language understanding with ML.NET's solid time-series math.

Adaptive Web Hosting's ASP.NET hosting plans run all of this on real Windows + IIS with SQL Server 2022 included on every plan (both your operational database and a read replica for analytics queries), dedicated app pools that handle ML.NET model loading cleanly, and free SSL out of the box.

Back to Blog