Blog Post

Databases Community Blog
8 MIN READ

From API to Action: A Disruption-Response Web App on Fabric's SQL Database

rajendraongole1's avatar
rajendraongole1
Icon for Super User rankSuper User
3 days ago

Part 3 of a Series: Putting the Data to Work

When a supplier location goes dark, the first question is always the same: who else is in that zone, and how much do we depend on them? The data to answer it already exists. The problem is reaching it fast enough, without opening a report or writing a query.

In Part 1 I built the foundation for a Supply Chain Disruption Response App inside Microsoft Fabric: a SQL database, supplier data from an external OData feed, layered analytical views, and a first Power BI report. In Part 2 I opened that foundation up to the team, with a T-SQL notebook for the analyst and Git integration with Azure DevOps for the engineer.

This final part is for the third person on that list: the developer. The goal is a small web app where a disruption-response team types in an affected location ID and instantly sees every supplier in that location and how many products each one provides.

It takes two pieces:

An API for GraphQL over one of the views from Part 1, so the app never needs a connection string.

An ASP.NET Core web app that calls that API with the user's own sign-in and renders the answer.

Same database, same workspace, still no new infrastructure to host for the data layer.

Finishing the API Part 2 Promised

Part 2 ended with "the API is ready." Here's what that took, because it's the quickest step in the whole series.

In the workspace I selected + New item > API for GraphQL and named it supplier_impact_gql. For the data source I chose single sign-on (SSO) authentication, picked supply_chain_analytics_database from the OneLake catalog, and loaded one object: the SupplyChain.vProductsbySuppliers view, which counts products per supplier and supplier location.

 

 

 

That was the whole build. Fabric generated the schema, the query types, and a filter for every column. Before writing any app code, I tested the exact question the app would ask in the built-in editor:

query {
  vProductsbySuppliers(filter: { SupplierLocationID: { eq: 7 } }) {
    items {
      CompanyName
      SupplierLocationID
      ProductCount
    }
  }
}

The JSON came back with every supplier at location 7 and its product count.

The SSO choice matters more than it looks. There's no shared key or service password baked into the API. Every caller is checked against the database with their own identity, so the API can never show someone more than the database would.

The last step on the Fabric side was Copy endpoint in the ribbon. That URL is the only thing the app needs to know about Fabric.

Scaffolding the App

The app is a plain ASP.NET Core project, built in Visual Studio Code with the .NET SDK (version 8 or later). After installing the SDK from dotnet.microsoft.com/download and checking it with dotnet --version, I created the project from the VS Code terminal:

 

 

dotnet new webapp -n GraphQLWebApp
cd GraphQLWebApp

 

The package install failed on my first try, because nuget.org wasn't registered as a source on that machine. If dotnet nuget list source doesn't show it, one command fixes it: dotnet nuget add source https://api.nuget.org/v3/index.json -n nuget.org

Then three packages, each with one job:

dotnet add package Azure.Identity
dotnet add package GraphQL.Client
dotnet add package GraphQL.Client.Serializer.Newtonsoft

Package

Role

Azure.Identity

Gets a Microsoft Entra token for whoever is signed in

GraphQL.Client

Sends the query to the Fabric endpoint

GraphQL.Client.Serializer.Newtonsoft

Turns the JSON response into objects

For sign-in during development, the app reuses the Azure CLI session. I installed the CLI (winget install -e --id Microsoft.AzureCLI on Windows; brew install azure-cli on macOS), restarted VS Code so the terminal could find the az command, and signed in with the same work account I use for Fabric:

 

One File, Two Pages

The whole app lives in Program.cs: a home page with a single input, and a results page that calls the API and renders a table. I replaced the generated file with the code below. The only line to change is the endpoint you copied from Fabric.

using System.Net;
using System.Net.Http.Headers;
using System.Text;
using Azure.Core;
using Azure.Identity;
using GraphQL;
using GraphQL.Client.Http;
using GraphQL.Client.Serializer.Newtonsoft;

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

// The endpoint you copied from the API for GraphQL item
const string graphQLEndpoint = "ReplaceWithYourGraphQLEndpointAddress";

// Sign-in: uses your Azure CLI login locally, a managed identity when hosted
var credential = new DefaultAzureCredential();
var tokenContext = new TokenRequestContext(
    new[] { "https://analysis.windows.net/powerbi/api/.default" });

const string pageHead = """
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Supplier Impact Finder</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
</head>
<body><div class="container mt-5">
<h3>Supplier Impact Finder</h3>
""";
const string pageFoot = "</div></body></html>";

// Home page: ask for the impacted location
app.MapGet("/", async context =>
{
    context.Response.ContentType = "text/html";
    await context.Response.WriteAsync(pageHead + """
<p>Enter the location ID facing an outage. You will see every supplier in
that impact zone and how many products each one supplies.</p>
<form method="get" action="/graphql" class="form-inline">
  <label for="locationId" class="mr-2">Location ID:</label>
  <input type="number" min="1" class="form-control mr-2" id="locationId" name="locationId" required>
  <button type="submit" class="btn btn-primary">Search</button>
</form>
""" + pageFoot);
});

// Results page: query the GraphQL API and render a table
app.MapGet("/graphql", async context =>
{
    context.Response.ContentType = "text/html";

    if (!int.TryParse(context.Request.Query["locationId"], out var locationId))
    {
        await context.Response.WriteAsync(pageHead +
            "<p>Please enter a numeric location ID.</p><a href='/'>Back</a>" + pageFoot);
        return;
    }

    // Get a fresh token per request; tokens expire after about an hour
    var token = await credential.GetTokenAsync(tokenContext);

    using var client = new GraphQLHttpClient(
        new GraphQLHttpClientOptions { EndPoint = new Uri(graphQLEndpoint) },
        new NewtonsoftJsonSerializer());
    client.HttpClient.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue("Bearer", token.Token);

    // Pass the location as a variable rather than building the query string
    var request = new GraphQLRequest
    {
        Query = """
            query ($locationId: Int!) {
              vProductsbySuppliers(filter: { SupplierLocationID: { eq: $locationId } }) {
                items { CompanyName SupplierLocationID ProductCount }
              }
            }
            """,
        Variables = new { locationId }
    };

    var response = await client.SendQueryAsync<dynamic>(request);

    if (response.Errors?.Length > 0)
    {
        await context.Response.WriteAsync(pageHead + "<p>Query failed: " +
            WebUtility.HtmlEncode(response.Errors[0].Message) +
            "</p><a href='/'>Back</a>" + pageFoot);
        return;
    }

    var rows = new StringBuilder();
    var count = 0;
    foreach (var item in response.Data.vProductsbySuppliers.items)
    {
        count++;
        rows.Append($"<tr><td>{WebUtility.HtmlEncode((string)item.CompanyName)}</td>" +
                    $"<td>{item.SupplierLocationID}</td><td>{item.ProductCount}</td></tr>");
    }

    var body = count == 0
        ? $"<p>No suppliers found at location {locationId}.</p>"
        : $"""
          <p>{count} supplier(s) affected at location {locationId}.</p>
          <table class="table table-striped">
            <thead><tr><th>Company</th><th>Location ID</th><th>Product count</th></tr></thead>
            <tbody>{rows}</tbody>
          </table>
          """;

    await context.Response.WriteAsync(pageHead + body +
        "<a href='/' class='btn btn-link'>New search</a>" + pageFoot);
});

app.Run();

Three decisions in that file are worth calling out, because they're the difference between a demo and something a team could keep running:

  • A fresh token on every request. Entra tokens expire after about an hour. Fetching one at startup works in a quick test and then fails quietly the next morning.
  • GraphQL variables, not string building. The location ID travels as $locationId, so the API checks that it's an integer and nothing typed into the box can change the shape of the query.
  • One credential for every environment. DefaultAzureCredential picks up the Azure CLI sign-in on my laptop and a managed identity once the app is hosted. The code doesn't change between the two.

     

  • Within a second the page listed every supplier operating at location 7, with the number of products each one provides to manufacturing.

  • That table is where a response starts: which suppliers to call first, which products are exposed, and where to look for alternatives. Because the API reads the view directly, the answer reflects the database as it stands right now, including any schema change that came through Git in Part 2.

    Why This Pattern Matters

    A custom API for this app would normally mean another project: a service layer to write, a host to run it on, connection secrets to store and rotate, and an authorization model to keep in step with the database. Here, the API was an item in the same workspace, the security came from the database itself, and the app is one file.

    For disruption response, that cuts the path from question to answer down to a text box. The person handling the crisis doesn't need Power BI open, a SQL login, or an analyst on call. They need a location ID.

    Looking Back Across the Series

    Three posts, one SQL database, one workspace:

    Part

    Built

    Fabric capability

    For

    1

    Database, external supplier data, layered views, Power BI report

    SQL database, Dataflow Gen2, SQL analytics endpoint, semantic model

    The business

    2

    Exploratory analysis and version-controlled schema

    T-SQL notebooks, Git integration with Azure DevOps

    The analyst and the engineer

    3

    Live API and a disruption-response web app

    API for GraphQL, Entra sign-in

    The developer and the response team

    The thread through all of it is a single definition of the data. The report, the notebook, the repository, and the app all read the same views. When a supplier table changes, every one of them sees the change, and nobody is reconciling copies in the middle of an incident.

    If you want to keep building, three natural next steps:

    • Host the app in Azure App Service with a managed identity. DefaultAzureCredential picks it up with no code change.
    • Add a GraphQL mutation so responders can log an incident against a location.
    • Expose vSalesByDate through the same API to show the revenue at risk next to each affected supplier.

    If you missed the earlier parts:

    Thanks for following the series. If you take this pattern into your own data, I'd like to hear what you build.

    Happy Reading and share your thoughts
Published 3 days ago
Version 1.0
No CommentsBe the first to comment