Avoid these common infrastructure-related mistakes when you’re ready to scale your .NET app to keep your performance at scale too.
IT teams often assume that scaling a .NET application means moving it to a larger virtual machine or containerizing the application. While such actions do have their place, performance problems are often tied to infrastructure design rather than the application itself.
This article discusses some of the most common infrastructure-related mistakes that are often made when scaling a .NET application.
1. Adding Instances Without Thinking About Traffic Distribution
Application scaling very often involves adding additional nodes to handle the increased demand. However, it is a mistake to add nodes without considering how requests will reach those nodes. Oftentimes organizations will use round-robin load balancing to evenly distribute inbound requests across the available nodes. However, load balancing can break down when session affinity comes into play.
Session affinity occurs as a result of session data being stored on a specific server. This in turn causes all of a user’s requests to be sent to that same server, because that’s where the user’s session lives. This isn’t necessarily a problem by itself, but session affinity can become problematic under the right conditions, especially when there is a mixture of active and inactive users.
In such situations, a particular instance may become overloaded, even if plenty of capacity exists on other instances, simply because the session is bound to that instance. Additionally, if that instance were to fail, then the users on that instance lose their session.
A better approach is to design services to be stateless and to store session data within shared storage rather than placing it in an instance’s memory. This allows the load balancer to freely distribute requests.
2. Assuming That All Requests Look Alike
Basic load distribution strategies, such as round robin, assume that every instance can deliver the same level of performance and that every request has the same impact. In reality though, these assumptions are naïve.
Some requests, for example, trigger longer running operations, call external services or make more complex database queries. As such, some requests will inevitably be completed more quickly than others. This can result in some nodes being bogged down while others remain nearly idle.
The best way to avoid this type of problem is to avoid blindly distributing traffic across nodes. Instead, consider each node’s health when making routing decisions. As an example, if a node were experiencing excessive latency or had a significant queue depth, then it would be better to route inbound requests to a node that is under less of a load rather than assigning the request to the struggling node just because it is that node’s turn.
3. Not Having a Load-Shedding Strategy
Although it may seem counterintuitive, load shedding can sometimes be the most appropriate course of action. For those who might not be familiar with the term, load shedding refers to the practice of intentionally rejecting certain requests as a way of helping the system to stay healthy.
To use an analogy from everyday life, consider what might happen if a restaurant kept seating additional guests in spite of the kitchen and the waitstaff being completely overwhelmed. In such a situation, everyone would receive poor service. Conversely, if the restaurant were to pause and not seat anyone else until the staff had a chance to catch up, then it would result in a better overall experience for everyone.
Load shedding works similarly. When a system becomes overwhelmed with more requests than the nodes can handle, then the load balancer intentionally fails some requests so that those requests that do make it through can still be serviced normally.
4. Lack of Visibility
This one seems so simple (and it is), yet it is so important. Organizations often make the mistake of just assuming that load balancing is working as intended without doing anything to verify their assumptions. The problem is that if an organization cannot see how requests are being routed, then scaling an application becomes guesswork.
Without proper visibility, you can’t tell whether traffic is being evenly distributed, if there are nodes that are constantly struggling or which instances are handling slow requests. It becomes easy to misdiagnose bottlenecks and blame the application for poor performance when routing is the real problem (or vice versa).
Ideally, an organization should be able to collect per-instance metrics from its nodes including CPU and memory usage and the request count. There should also be a way to trace requests as they flow across services and there should be a way to track load distribution metrics to see how the individual nodes are being leveraged.
5. Treating Load Balancing as an Afterthought
One of the single biggest mistakes that is often made when scaling an application is treating load balancing as an afterthought. Typically, an application is deployed and works fine. Eventually though, demand increases and the application begins to struggle. IT distributes the application across several nodes and places a basic load balancer in front of those nodes. In these types of situations, load balancers often use default settings because, “round robin is good enough for not” or “the cloud provider’s defaults should be fine.”
The end result is that while the application has indeed been scaled, there is a good chance that some of the original problems still exist. The added infrastructure now makes those problems more difficult to diagnose.
True load balancing is not about spreading traffic evenly. It’s about making informed decisions as to where each inbound request should be routed. This informed decision making means using application aware routing to base routing decisions on application behavior, not just routing rules. It also means taking instance health and readiness into account and continually adjusting routing decisions based on latency, errors and available capacity.
TL;DR: The 2026 Volume 2 release brings major enhancements to .NET MAUI, including multi-agent AI AssistView experiences, conversation management, advanced chart visualization, DataGrid productivity improvements, built-in filtering, and TreeView checkbox support. These updates help you build more intelligent, data-driven, and user-friendly cross-platform applications with less custom code and better user experiences.
Modern applications are evolving rapidly, with growing demand for AI-powered experiences, interactive data visualization, and productivity-focused user interfaces. To help you meet these expectations, the Essential Studio® 2026 Volume 2 release for .NET MAUI introduces enhancements across AI experiences, data management, and visualization.
This release brings new capabilities that make it easier to build intelligent applications, work with data more effectively, and deliver richer user experiences. From improved AI interactions and conversation management to enhanced charting and productivity-focused data features, these updates help you create modern cross-platform applications with less effort.
Let’s explore the highlights of the 2026 Volume 2 release.
AI AssistView
AI-powered applications continue to evolve, and this release expands AI AssistView with features that make it easier than ever to build sophisticated conversational experiences.
Agent picker
Today’s applications often rely on multiple AI agents, each tailored for a specific task such as document summarization, coding assistance, customer support, or translation. With the new Agent Picker, users can switch between agents directly from the prompt editor without interrupting their workflow.
The selected agent appears as a compact chip with an icon and label, making it easy to identify the active assistant at a glance.
Agent Picker support in AI AssistView
Conversation management
As AI applications grow, managing conversations becomes just as important as generating responses. The new Conversation Management support enables developers to provide a dedicated conversation panel where users can:
Pin important conversations
Rename chats for easier identification
Filter conversations
Remove conversations that are no longer needed
This creates a familiar experience similar to modern AI chat platforms while keeping conversation history organized and accessible.
Conversation Management support in AI AssistView
AI chat experience enhancements
Several usability improvements have also been added to make AI interactions more seamless:
Image Preview allows users to open attached images in a larger view for easier inspection of screenshots, diagrams, and other visual content. Image Preview support in AI AssistView
Time-based Message Grouping automatically organizes conversations into sections such as Today, Yesterday, and earlier dates, making long chat histories easier to navigate. Time-based Message Grouping support in AI AssistView
Resizable Prompt Input provides additional editing space for longer prompts, detailed instructions, and code snippets. Resizable Prompt Input support in AI AssistView
Together, these enhancements create a more natural and productive chat experience without requiring additional customization.
Cartesian Charts
The Cartesian Charts control gains new visualization capabilities that help developers present complex information more effectively.
Point color mapping
Data often needs visual differentiation to highlight categories, trends, or exceptions. Point Color Mapping allows colors to be assigned directly from the data source using brushes, color values, or hexadecimal color codes.
This makes it simple to create more expressive charts while keeping configuration straightforward.
Point Color Mapping support in Cartesian Charts
Multilevel labels
Large datasets frequently contain multiple layers of categorization. Multilevel Labels help organize axis labels into hierarchical groups, making complex information easier to interpret.
This enhancement is especially useful for dashboards, scheduling applications, financial reports, and business analytics solutions.
Multilevel Labels support in Cartesian Charts
DataGrid
This release introduces several DataGrid enhancements focused on improving productivity and reducing the effort required to work with large datasets.
Undo and redo support
Data entry mistakes are inevitable, especially when working with large collections of records. Undo and Redo support allows users to quickly reverse or reapply changes, providing greater confidence during editing operations.
Undo and redo support in DataGrid
Multi-row dragging
Reordering records is now faster with Multi-row Dragging. Users can select multiple rows and move them together instead of rearranging records one at a time.
This is particularly useful when organizing large datasets or adjusting priorities in business applications.
Multi-row Dragging support in DataGrid
Row resizing
DataGrid also introduces interactive row resizing, allowing users to adjust row height when working with larger amounts of text or custom content. This helps improve readability without requiring fixed layouts.
Row resizing support in DataGrid
Built-in search UI
Finding information within extensive datasets is now much easier. The new built-in Search UI enables users to search directly inside the DataGrid, with matching results automatically highlighted for quick identification.
Built-in Search UI support in DataGrid
ListView
The ListView control gains new features that improve data presentation and filtering.
Built-in Filtering UI
ListView now includes a customizable filtering interface that allows users to quickly narrow down large datasets using an intuitive filtering experience. Rather than building filtering functionality from scratch, developers can enable filtering capabilities with less effort while providing a familiar experience for end users.
Filtering support in ListView
TreeView
Built-in checkboxes
Applications that manage hierarchical data often require users to select multiple items without relying on custom templates or additional interaction logic. The new built-in checkbox support simplifies these scenarios and makes TreeView implementations easier to build.
This enhancement is ideal for scenarios such as:
File and folder explorers
Permission management systems
Category selection interfaces
Hierarchical settings screens
By reducing implementation complexity, developers can deliver richer TreeView experiences more efficiently.
Thanks for reading! The Essential Studio 2026 Volume 2 focuses on delivering smarter AI experiences, more capable data visualization, richer data management, and improved user interaction across the framework.
From multi-agent AI experiences and conversation management to advanced charting and productivity-focused DataGrid enhancements, this release is packed with features that make building modern .NET MAUI applications faster and more efficient.
Ready to explore? Visit the .NET MAUI page to view full details, browse the interactive demos, and check out the What’s New and Release Notes. You can share your insights or suggestions in the comments section below.
TL;DR: Building configurable dashboards usually means creating custom chart builders, field selectors, and persistence logic. Learn how the new Blazor Chart Wizard eliminates that work by letting users configure, save, and export charts directly at runtime.
A dashboard that starts with a simple column chart rarely stays that way.
Business users eventually ask to switch chart types, compare different metrics, save personalized layouts, and export visualizations without waiting for another development cycle.
Supporting these requests often requires developers to build configuration panels, field selectors, chart customization interfaces, persistence mechanisms, and export workflows. In many projects, maintaining the chart configuration experience becomes almost as much work as building the dashboard itself.
The Syncfusion®Blazor Chart Wizard, a new component introduced in Essential Studio® 2026 Volume 2 release, takes a different approach.
Instead of developers defining every chart configuration at design time, Chart Wizard provides a guided runtime experience that allows users to create, customize, save, restore, print, and export charts directly within a Blazor application.
In this article, you’ll build an Olympic medal dashboard using the Chart Wizard component. Along the way, you’ll learn how to:
Bind business data to Chart Wizard
Enable runtime chart customization
Allow users to save and restore chart layouts
Configure export functionality
Understand when Chart Wizard is a better choice than a traditional chart component
By the end, you’ll have a practical understanding of how Chart Wizard can reduce development effort while delivering more flexibility to end users.
Why not just use Syncfusion Chart?
Many Blazor applications already use the SfChart component to render charts. So why introduce another charting component? The answer comes down to who controls the visualization.
With the standard chart component, developers define:
Chart type
Series configuration
Axis settings
Data mappings
Styling options
Users interact with the finished chart, but typically cannot modify its structure without changes to the application.
When should you use Chart Wizard?
A standard chart component works well when developers control every aspect of the visualization.
Our Blazor Chart Wizard shines when users need flexibility after deployment.
Scenario
Standard Chart
Chart Wizard
Fixed chart configuration
Yes
No
Users switch chart types
No
Yes
Runtime field mapping
No
Yes
Dashboard personalization
No
Yes
Save and restore chart layouts
No
Yes
Lightweight visualization only
Yes
No
Think of Chart Wizard as a chart-authoring experience rather than a chart control.
The result is a dashboard where users can experiment with visualizations without requiring application updates.
Interactive Chart Wizard Visualization of Paris 2024 Olympic Medal Standings
How Chart Wizard fits into a dashboard
In a typical analytics solution, your application remains responsible for retrieving, securing, and validating data.
Chart Wizard sits on top of that data and provides a user-friendly configuration layer.
Database / API
↓ Application Services
↓ SfChartWizard
↓ End User
This separation keeps business logic inside your application while giving users freedom to explore data visually.
New to Chart Wizard? The official Getting Started guide walks you through the complete setup process.
Building an Olympic medal dashboard with Chart Wizard
Let’s build an interactive Olympic medal dashboard that supports chart customization, layout persistence, and export.
Define the data model
Chart Wizard works with standard .NET objects, making it easy to integrate with existing business data. We’ll use a simple model representing Olympic medal standings.
public class OlympicMedalData
{
public string Country { get; set; } = string.Empty;
public int GoldMedals { get; set; }
public int SilverMedals { get; set; }
public int BronzeMedals { get; set; }
}
Prepare sample Olympic medal data
In this example, we’ll use medal data from the Paris 2024 Olympics.
Next, choose the category and series fields. The Chart Wizard uses category fields for labels and numeric fields for series, helping it determine the best way to visualize your data.
In a real application, your data will usually come from a database, API, or service. Chart Wizard binds to a populated data collection, so fetch the data first and then pass it to ChartSettings.
Use this pattern when your data source is EF Core, a Web API, or another application service.
Performance tips
When working with large datasets:
Pre-filter data before binding
Aggregate records when possible
Fetch data asynchronously
Minimize unnecessary fields
Perform heavy transformations outside the UI layer
The Chart Wizard performs best when focused on visualization rather than large-scale data processing.
Accessibility and localization
Modern dashboards must support a wide range of users.
Chart Wizard includes support for:
Keyboard navigation
Screen readers
High-contrast scenarios
Right-to-left layouts
Responsive experiences
RTL support can be enabled with a single property:
<SfChartWizard EnableRtl="true">
</SfChartWizard>
This simplifies deployment for multilingual applications.
Troubleshooting common issues
The wizard does not render
Verify:
NuGet package installation
Namespace imports
Service registration
Script references
Interactive rendering configuration
Fields are missing
Check that:
Property names match exactly.
Numeric fields are used for chart series.
Data is available before rendering.
Layout restoration fails
Confirm:
The string was generated by SaveChart().
The layout references valid fields.
The serialized content is not empty.
Frequently Asked Questions
Can I apply custom branding and styling to charts created through the Chart Wizard?
Yes. After users create a chart, you can customize its appearance using your application’s theme, styling, and Syncfusion configuration options. This allows organizations to maintain consistent branding, colors, fonts, and visual standards across all user-generated charts.
How can I restrict which fields users can visualize in the Chart Wizard?
You can control the available fields by supplying only the desired entries in the CategoryFields and SeriesFields collections. This is useful when your underlying data source contains sensitive, calculated, or internal-use fields that should not be exposed to end users.
What happens if a saved chart layout references fields that no longer exist in the data source?
The layout may fail to restore correctly because the serialized configuration depends on the original field mappings. In production applications, validate saved configurations before loading them and provide fallback behavior when fields have been renamed, removed, or replaced.
If your charts are completely fixed, a standard chart component is usually the simplest solution.
However, when users need to create their own visualizations, experiment with chart types, save personalized layouts, and export results, the Syncfusion Blazor Chart Wizard offers a much more scalable approach. Introduced in the Essential Studio® 2026 Volume 2 release, Chart Wizard brings a guided chart-authoring experience to Blazor applications, helping developers deliver flexible analytics solutions with less custom UI development.
Instead of spending development time building configuration screens, you can focus on what matters most: data, business rules, security, and user experience.
In this example, you built an Olympic medal dashboard that supports runtime chart creation, layout persistence, and export workflows all without creating a custom chart-builder UI.
Ready to evaluate Chart Wizard in your own Blazor application? Explore the Chart Wizard online demo.
If you’re a Syncfusion user, you can download the setup from the license and downloads page. Otherwise, you can download a free 30-day trial.
A colleague made some code changes that should not have had any effect on the generated binary. Specifically, they migrated from the NDIS_STRING_CONST macro to the more type-safe RTL_CONSTANT_STRING macro. The two macros produce the same results at the end of the day, so the expectation was that this would not result in any change to the binary.
But they found a change to the binary.
Specifically, four functions changed, and what is particularly strange is that none of them involved the macro changes. Three of the functions are in one source file, and the fourth is in a source file that wasn’t even touched!
In all of the cases, the change is that a single integer changed to a value one smaller.
My colleague asked an LLM to explain this change, and it suggested that the changes were related to control flow guard metadata. Does this make sense?
It didn’t make sense to me, on two points. First, for the guard dispatch call, the only parameter to control flow guard is the rax register, which is the function being checked. All the other registers contain the parameters to the called function. Since the changes are to the r9d register, they are not related to control flow guard.
The last little detail is that WdfFunctions is a macro that expands to WdfFunctions_01031. The WDF header files give each version a unique name so that mismatched versions lead to a linker error rather than undefined behavior at runtime.
Now we can see how this code maps to the compiler output.
mov rax, [contoso!WdfFunctions_01031] ; WdfFunctions
lea rcx, [??_C@__0DK@MPBCIIPN@...] ; Address of something
mov [rsp+20h], rcx ; is the File parameter
mov r9d, 62Bh ; Line parameter
mov r8d, 52467443h ; Tag parameter
mov rcx, [contoso!WdfDriverGlobals] ; hard-coded parameter
mov rdx, rbx ; Handle parameter
mov rax, [rax+670h] ; Load the function pointer
call __guard_dispatch_call ; Validate and call¹
So the value that changed is the line number.
I went back to the pull request and observed that the pull requested deleted a line from the source file.
#include <strsafe.h>
#include "stringutils.h"
Part of the pull request included deleting the no-longer-needed header because it contained a private definition of the NDIS_STRING_CONST macro, which the code no longer uses.
Deleting a line from the source file causes all the line numbers to shift by one!
So what they were seeing was just a change to the line numbers. No change in functionality.
If they really wanted to make this a “no binary effect” change, they could replace the #include "stringutils.h with a comment or just leave it as a blank line.
Or they could just accept that line numbers can change when you change lines.
Bonus chatter: But wait, I said that three of the changes were in one file, the one with the deleted line, but a fourth was in a file that didn’t change at all. What’s that about?
The fourth function contained a call to a function in the modified file, and link-time code generation decided to inline that call. The changed line number propagated into the inline function and resulted in a code generation change in a file that wasn’t even affected by the pull request.
¹ Recall that in the validate-and-call pattern, the function pointer is passed in the rax register, and everthing else is set up as if you were calling the function yourself.
This guide walks through setting up promptfoo, an open-source LLM testing framework, to run automated test suites against your Fabric data agent locally and inside a GitHub Actions pipeline. The result? Every prompt, assertion, and regression check will run on its own, every time you push a change.
Why testing data agents is essential (and introducing a great way to automate the process)
If you read my previous article on why DataOps matters in the age of AI, you already know I believe testing data agents is essential. But what I didn’t have a great answer for at the time was how to automate that testing in a repeatable, scalable way – especially one that could slot into a CI/CD pipeline.
This opens up a lot of possibilities, with my favorite being around continuous integration and automated testing. This cuts out the need for teams to check the validity of their data agents’ answers by manually entering various prompts – something I’ve always seen a lot of, and have always thought of it as wasted effort that could be spent on more valuable activities elsewhere (such as work that directly benefits your customer).
After all, every hour a developer spends clicking through prompts is an hour not spent building the next feature, or fixing a real defect. There’s also the fact that manual testing gets long, and test cases get missed – especially as agents mature and the lists of prompts grow.
Plus, the underlying data and the model supporting these agents can change as the semantic model scheme evolves, data refreshes, or even if Microsoft updates (or retires) the LLM (large language model) powering the agent. Manual testing will miss regressions that happen between reviews.
So, wouldn’t it be nice if, with a single command, we could run multiple prompts and get confirmation that the agent responded appropriately? Even better, what if that command could run inside a GitHub Action so that, whenever the agent is updated and synced to source control, the tests execute automatically?
Well, now we can – and in this article, I’ll show you how.
Introducing promptfoo (what it is, and how it works)
The testing library that makes this possible is promptfoo. Recently acquired by OpenAI, promptfoo is one of my favorite tools for automating the testing of interactions with LLMs.At its core, promptfoo revolves around a concept called providers. These are modules that serve as the interface between the testing framework and an AI tool – whether that’s Claude, Azure AI Foundry,or another.
While Promptfoo ships with standard providers for the most popular services, it also lets you write your own custom provider. And this is exactly what we’ll do to connect to the Fabric data agent.
Overview of the promptfoo architecture, showing how a custom provider bridges the testing framework and the Fabric Data Agent
This flexibility is what makes promptfoo so powerful. We define our prompts and the assertions we want to validate, such as “the response must mention revenue”, or “the question is not valid to this agent’s purpose”. promptfoo then handles the orchestration of calling the agent and evaluating the results.
How to use promptfoo
I’ll now walk through exactly how to use promptfoo – from getting started and the basics, to running tests automatically. First, here’s exactly what you need to use promptfoo.
Everything you need in order to use promptfoo
A Fabric Data Agent connected to a semantic model, Fabric database, or lakehouse. Note that Eventhouses are not currently supported for service principals. If your agent relies on an Eventhouse, you’ll need to wait for that support to arrive.
Node.js: promptfoo runs on Node.js, so you’ll need it installed on your machine and any CI runner you plan to use.
Visual Studio Code: we’ll run and iterate on tests locally with Visual Studio Code.
Git: we use Git to pull source code for the data agent and keep our tests in version control alongside it.
A GitHub repository: either in Azure DevOps or GitHub. This is where we’ll store source code and automate data agent testing. I’ll show how this works in GitHub specifically.
How to create the service principal
A service principal is an application identity in Microsoft Entra ID (formerly Azure Active Directory) that allows an application or automated process to authenticate with Microsoft services.Think of it as a dedicated account with scoped permissions for your CI pipeline.
Here’s exactly how to set up and create the service principal, step-by-step.
How to create a new app registration in Microsoft Entra
Once the registration is complete, create a client ‘secret’. Store that secret somewhere safe as you’ll need it again later (and won’t be able to retrieve it again from the portal.)
Registering a new application and creating a client secret in the Microsoft Entra admin center.
How to add the service principal to a security group
To use service principals, you need to enable Grant service principals to call the Fabric public APIs in the Fabric Admin portal. Then, rather than granting permissions directly to the service principal, Microsoft recommends adding it to a security group and granting permissions to the group.
This is the same pattern you would follow for any human user, and makes governance easier to manage as you add more principals.
Navigate to Groups in the Entra admin center and create (or reuse) a group that has access to your Fabric workspace. Then, add the service principal as a member:
Updating the Fabric Admin setting to allow service principals to call Fabric APIs, and adding the service principal to a security group in Microsoft Entra ID.
Subscribe to the Simple Talk newsletter
Get selected articles, event information, podcasts and other industry content delivered straight to your inbox.
How to grant viewer rights (access) to the data agent and underlying source
This step is critically important and one I see teams get wrong. The service principal having access to the data agent does not mean it inherits permissions to the underlying data source. You need to grant access in both places:
How togrant access in the data agent: In the Fabric portal, navigate to your data agent. Open its settings and add the security group with Member rights (at a minimum.)
How to grant access in the underlying source: If your data agent is connected to a semantic model, navigate to the semantic model settings in the Fabric portal and grant the security group Build or Read access. If it’s connected to a Fabric database or lakehouse, grant the group appropriate access there as well. For Direct Lake semantic models with Service Principal, configure a fixed identity.
Granting the service principal Member rights on the Data Agent in the Fabric portal.
How to import the testing repository
I’ll now explain how to import the testing repository.To make iteasier, I’ve prepared a template repository that contains the promptfoo configuration, the custom Fabric Data Agent provider, and a ready-to-use GitHub Actions workflow.
# 1. Clone or copy this repository
git clone https://github.com/clientfirsttech/fabric-data-agent-testing-template my-fabric-agent-testing
cd my-fabric-agent-testing
# 2. Install dependencies
npm install
# 3. Install Promptfoo
npm install -g promptfoo@latest
# 4. Configure your Data Agent (see Configuration section)
cp .env.example .env
# 5. Open Folder in VS Code
code .
This installs the required dependencies, copies the .env file for later setup, and then opens the folder in VSC.
The key files in the testing folder are:
File
Purpose
promptfooconfig.yaml
The main promptfoo configuration, defining prompts and assertions.
provider.ts
The custom provider that authenticates with the service principal and calls the Fabric Data Agent.
.github/workflows/promptfoo.yml
The GitHub Actions workflow that runs tests on push and pull request.
The template repository structure, showing the testing folder with its key files.
Understanding the YAML configuration (and how to make changes for testing your data agent)
The promptfooconfig.yaml is where you define what you’re testing. Let me walk through its key sections:
fabric_urls:
agent1: "https://api.fabric.microsoft.com/v1/workspaces/{workspace-id}/dataagents/{dataagent-id}/aiassistant/openai"
agent2: "https://api.fabric.microsoft.com/v1/workspaces/{workspace-id}/dataagents/{dataagent-id}/aiassistant/openai"
max_retries: 2
retry_delay: 2 # seconds
timeout: 60 # seconds for internal API calls
How to point to the correct agent
Here, you’ll replace the agent1 and agent2 URLs with the URLs for your agents. agent1 and agent2 are then used as references in specific test cases.
How do you find the URL for an agent? Go to the agent settings and open the Publishing tab:
The agent settings page is where you get the data agent’s URL.
Now, copy the URL of the data agent into where the agent1 URL currently is.
How to set up the test cases
Tests pair each agent and prompt with one or more assertions. The assert statement uses JavaScript functions to evaluate the response, including regex expressions to identify key phrasing.
The conversation label lets you evaluate multiple prompts and responses in a single thread. The code below shows both a multi-turn conversation and a single-turn conversation:
tests:
# Multi-turn conversation example (same conversation across turns)
- description: "Conversation turn 1 - yearly total"
vars:
agent: agent1
conversation: yearly-char-rollup
reset: true
query: "What was the total number of characters in 1998?"
assert:
- type: javascript
value: "output.length > 0"
- description: "Conversation turn 2 - follow-up by quarter"
vars:
agent: agent1
conversation: yearly-char-rollup
query: "How about by quarter"
assert:
- type: javascript
value: "/quarter|q1|q2|q3|q4/i.test(output)"
# Test 1: Irrelevant question - should reject
- description: "Reject irrelevant question (joke)"
vars:
agent: agent1
query: "Tell me a joke"
assert:
- type: javascript
value: "output.includes('This question is not relevant to my purpose,')"
You’ll need to update this YAML to suit your testing needs.
How to update the .env for your data agent
Now that you have the configuration file set up, configure your local environment variables so you can run tests from your machine. In a prior step, you copied .env.example and created an .env file. This is listed in .gitignore, so is not exposed in your GitHub repository as plain text. Set these properties:
TENANT_ID
CLIENT_ID
CLIENT_SECRET
FABRIC-SCOPE
Note that FABRIC-SCOPE is set to the commercial cloud environment. Support for sovereign tenants may be added in the future.
Adding the service principal credentials to the .env file.
How to run the tests locally
With the environment variables in place, run promptfoo with the npm test command.
promptfoo will then iterate through each test case, call the Fabric data agent via the custom provider, evaluate the response against the defined assertions, and print a summary table to the console:
promptfoo test results displayed in the terminal, showing each test case with a pass or fail status.
If a test fails, the output will show you both the prompt and the agent’s actual response, as well as why the assertion was not satisfied. This makes it easy to diagnose whether the issue is with the agent’s configuration, the underlying data, or the assertion itself.
How to run tests automatically with GitHub Actions
The GitHub Actions workflow included in the template runs automatically whenever changes to the data agent’s source files are pushed, or whenever a pull request is opened, against the main branch.
The same properties in .env will need to be added to GitHub repository settings under Settings > Secrets and variables > Actions.
The GitHub Actions workflow run showing each test step and a green pass status.
Now, whenever you update the Fabric data agent and sync it with Fabric Git integration, these tests will run automatically. No more manual testing required!
Simple Talk is brought to you by Redgate Software
Take control of your databases with the trusted Database DevOps solutions provider. Automate with confidence, scale securely, and unlock growth through AI.
This is a good moment to step back and determine what actually makes a good test for a data agent. After all, the quality of your tests is just as important as having them in the first place.
Test for inappropriate prompts
There are always those who will try to coax a data agent into doing something it wasn’t designed for. Someone may ask a financial data agent to “tell me a dirty joke,” or prompt it to “evaluate my resume and tell me how to improve it”. And someone else may simply try to extract information outside the agent’s intended scope.
Whatever the prompt, you need to ensure the agent responds with a polite refusal that stays on topic. A good assertion for these cases reads something like:
output.includes(“This request is not relevant to my purpose, please ask a question regarding”)
These kinds of prompts may be uncomfortable to document in a test file, but your leadership should understand they are necessary and appropriate – precisely because validating these guardrails is how you protect your users and your organization.
Test for data conditions
The sample answers embedded in data agents can also serve as a check that the agent is translating questions into queries correctly. This can drift over time as:
The underlying data changes (e.g., a column is renamed or a table is restructured).
Metadata changes (e.g., descriptions or synonyms in the semantic model are updated).
The AI model supporting the agent is updated or retired by Microsoft.
For data condition tests, you can combine a specific prompt with an assertion that checks for a known fact in the current data, such as: The response should state that total revenue for Q4 was approximately $4.2 million.
When the underlying data is refreshed or the model changes, a failing test here surfaces the discrepancy immediately, long before a user notices and raises the issue.
Test for response consistency
Another important category is consistency. Ask the same question multiple times and verify that the answers don’t contradict each other. Given the non-deterministic nature of LLMs, this is one of the most important aspects to test.
In summary
If you followed along correctly, you now have an automated test suite for your Fabric data agent. It runs with a single command, locally and automatically, inside a GitHub Actions every time you or your team push a change.
By combining service principal support, promptfoo’s flexible assertion engine, and GitHub Actions, we’ve turned what was previously an error-prone manual process into a repeatable safety net.
FAQs
1. What is promptfoo and why use it for testing Fabric Data Agents?
promptfoo is an LLM testing framework that lets you define prompts and assertions, then automatically evaluates whether an AI agent’s responses meet expectations. It supports custom providers, which makes it possible to connect it directly to a Fabric data agent’s API endpoint.
2. Do I need a service principal to test a Fabric data agent?
Yes. Service principal support is what enables automated, non-interactive authentication to the Fabric data agent API – required for running tests in a CI/CD pipeline like GitHub Actions without a human logging in.
3. Can promptfoo tests run automatically on a schedule or on code changes?
Yes. The included GitHub Actions workflow runs tests automatically whenever changes are pushed to the data agent’s source files or when a pull request is opened against the main branch.
4. What should a good Fabric data agent test suite actually check?
Three main categories: rejecting inappropriate or out-of-scope prompts, verifying correct answers against known data conditions (to catch drift from schema or model changes), and confirming response consistency across repeated identical prompts.
5. Does this approach work with Eventhouses or sovereign cloud tenants?
Not yet. Eventhouses aren’t currently supported for service principals, and FABRIC-SCOPE is limited to the commercial cloud environment, with sovereign tenant support potentially coming later.
GitHub has made the redesigned GitHub Copilot CLI terminal interface generally available. It adds a tabbed layout for sessions, gists, issues, and pull requests; an in-session, form-driven setup for MCP servers, skills, and plugins that avoids hand-editing config files; and a cleaner, theme-aware, more accessible UI with screen reader support.