Skip to content

Building Modern Applications on Microsoft Fabric

Photo by Pixabay: https://www.pexels.com/photo/purple-foot-bridge-220769/

Modern applications demand seamless data integration, real-time responsiveness, and scalability (+ of course cool looking UI). Microsoft Fabric unifies data movement, processing and storage into a cohesive environment. As of now the end game for Microsoft Fabric has been the Power BI report. When that is done we are fine, but think about all the work, calculation power and storage usage that we are having just to provide a report. With data platforms we don’t need to stop at the reporting stage. We can use all that same data that we have been gathering and refining also outside of Fabric and build our own custom applications on top of it.

In this post I am going to show few ways to integrate Fabric data into your own app. You can use functionality like SQL Analytics Endpoints or ADLS GEN 2 SDK to access the data, but I have find out that GraphQL is the simplest way to query data from your own app and User Data Functions can be leveraged on mutations and validation.

GraphQL API for Data Access

Fabric’s GraphQL API exposes your Lakehouses and Warehouses as strongly typed, discoverable endpoints. It supports both queries and mutations, allowing client applications to fetch and push data with a single interface. Whole tables or just specific columns can be exposed to API users, depending on your needs. For more advanced scenarios, it’s possible to implement complex logic and perform mutations by exposing Warehouse Stored Procedures through the GraphQL API. Think of GraphQL as a ready-made REST API that simplifies access to your data.

GraphQL act as a single resource in Microsoft Fabric and you can of course have multiple copies of GraphQL resources in your workspace. Each resource has own endpoint and you can partition endpoints by using multiple GraphQL resources. If we combine GraphQL with Azure API management we can have better control over API partitioning in APIM, so doing partitioning in Fabric might not be viable strategy.

GraphQL API resource supports exporting GraphQL schema as *.gql file and Azure APIM supports importing that file format to build strongly typed API without any extra work.

On pricing wise the GraphQL uses same compute units as all the other Fabric resources. In my experience the API does not add too much of overhead on top of lakehouse/warehouse queries, so it is relatively cheap to use too.

If you want to learn more about Microsoft Fabric GraphQL check out my previous post about it.

GraphQL is a query language for APIs. It is little bit like REST, but build especially for queries.

User Data Functions for Business Logic

User Data Functions (UDFs) offer an “Azure Functions”-style compute layer within Fabric. Authors can write Python functions, leverage PyPI libraries, and encapsulate business logic as reusable, parameterized code blocks. UDFs can be invoked via REST endpoints or embed the usage into Fabrics own actions like Data Pipelines. User Data Functions can do basically all the things that Notebooks are able to do. You can read lakehouses and warehouses, change data and even work with Fabric SQL databases. On problematic side we have not so great developer experience and bad monitoring. Of course we can improve monitoring with APIM, but out of the box solution does not offer much for logging and metrics.

As User Data Functions offers HTTP endpoint, we can connect that into our Azure APIM solution and publish versioned API’s through it. APIM also offers all the monitoring tools that are lacking in Fabric at this point.

User Data Functions is still in public preview, so you need to enable the feature flag in Fabric admin portal to create UDF type of resources.

Azure API Management for Security & Governance

Even though that Fabric exposes GraphQL and UDF’s as HTTP endpoints which we can use outside of Fabric, it does not have very good tools to monitor or manage API usage. That’s why we want to have extra layer of protection and monitoring on top of Fabric endpoints.

Integrating Fabric services with Azure API Management (APIM) provides extra security, rate limiting, and other userful API features. APIM allows us to publish Fabric HTTP endpoints as API products. We can leverage the APIM features to wrap our endpoints into consumable packages that are served through nice developer portal. Fabric’s native service-principal authentication secures machine-to-machine calls, APIM enables OAuth 2.0, API keys, IP filtering, and caching policies to suit diverse user scenarios.

graph TB
  subgraph "Internal Fabric"
    GW[GraphQL API]
    UDFs[User Data Functions]
    DW[Warehouse]
    Lakehouse
  end
  APIM --> GW
  APIM --> UDFs
  Client --> APIM
  GW --> DW
  GW --> Lakehouse
  UDFs --> DW
  UDFs --> Lakehouse

Figure 3: API Management as the unified gateway for Fabric services.


Application

Now that we have all the pieces for our puzzle, lets see how we can actually build the app. For this sample I have created simple Warehouse that holds customers that we are going to view and edit from our app.

Our application has customers with same generated data to act on.

Then we of course need the GraphQL API resource against the warehouse + some User Data Functions. GraphQL API is quite straight forward to create, so I am not going to spend more time on that.

User Data Functions

In this sample I am going to use User Data Functions to create, modify and delete data from my warehouse. We could use GraphQL mutations for this also, but as I like to have more control over the code + maybe add some validation logic, I am going to to use UDF’s.

User Data Functions are quite easy to implement. You need to add other Fabric resources as “Connections” into your UDF resource to use them. The documentation side of UDF’s is a little bit thin currently, but I think it will get better over time. As of now just check the code samples from top ribbon for more information on how to use lakehouses and warehouses.

For this application I created one User Data Function resource and put all three methods in it. All UDF methods are using connection called “WHSample” which I have added into Connection settings. Behind the scene Fabric will inject the warehouse connection into variable called “myWarehouse” (as I have named it in this sample). Connection autocommit is set to false, so you need to remember to call the commit() method at the end to store the changes into warehouse.

The code itself is quite self explanatory. For inserts I am receiving collection of keyvalue pairs and building the insert statements from them directly. This can be prone to SQL injection so for external usage please use named parameters (add parameters as method arguments).

As Fabric does not have identity fields I am using MAX query to fetch the biggest identity from table. If you have large dataset you can optimize this by using GUID as identity, or having separate identity table that contains largest identity.

Update is implemented in a same way and delete just accepts the identity of object to delete. Nothing crazy here.

import datetime
import fabric.functions as fn
import logging
import datetime

udf = fn.UserDataFunctions()

@udf.connection(argName="myWarehouse", alias="WHSample")
@udf.function()
def insert_customer(myWarehouse: fn.FabricSqlConnection, data: dict) -> list:
    try:
        whSqlConnection = myWarehouse.connect()
        cursor = whSqlConnection.cursor()        
        
        # Fetch max id for new customer
        cursor.execute("SELECT MAX(Id) FROM Customer;")
        max_id_row = cursor.fetchone()
        max_id = max_id_row[0] or 0  # Handle None case
        max_id = max_id + 1
        data["Id"] = max_id      # Increment if needed
        logging.info(f"Inserting customer with id {max_id}")

        # Build the query dynamically from the dict
        columns = ", ".join(data.keys())
        placeholders = ", ".join(["?" for _ in data])
        insert_query = f"INSERT INTO Customer ({columns}) VALUES ({placeholders});"

        # Provide values in the correct order
        cursor.execute(insert_query, tuple(data.values()))
        whSqlConnection.commit()
        
        cursor.close()
        whSqlConnection.close()

        logging.info(f"Customer created successfully.")
        return f"Customer created with id {max_id}"
    except Exception as e:
        logging.error(f"Failed to process payload: {e}")
        return "Error processing the payload"

@udf.connection(argName="myWarehouse", alias="WHSample")
@udf.function()
def update_customer(myWarehouse: fn.FabricSqlConnection, data: dict, id: int) -> list:
    try:
        whSqlConnection = myWarehouse.connect()
        cursor = whSqlConnection.cursor()

        logging.info(f"Updating customer with id {id}")
        # Build the SET clause dynamically from the dict
        set_clause = ", ".join([f"{k} = ?" for k in data.keys()])
        update_query = f"UPDATE Customer SET {set_clause} WHERE Id = ?"
        # Add the id to the end of values list for the WHERE clause
        values = list(data.values()) + [id]

        cursor.execute(update_query, values)
        whSqlConnection.commit()

        cursor.close()
        whSqlConnection.close()

        logging.info(f"Customer with id {id} updated successfully.")
        return f"Customer with id {id} updated"
    except Exception as e:
        logging.error(f"Failed to process payload: {e}")
        return "Error processing the payload"

@udf.connection(argName="myWarehouse", alias="WHSample")
@udf.function()
def delete_customer(myWarehouse: fn.FabricSqlConnection, id: int) -> list:
    try:
        whSqlConnection = myWarehouse.connect()
        cursor = whSqlConnection.cursor()

        logging.info(f"Deleting customer with id {id}")

        delete_query = "DELETE FROM Customer WHERE Id = ?"
        cursor.execute(delete_query, (id,))
        whSqlConnection.commit()

        cursor.close()
        whSqlConnection.close()

        logging.info(f"Customer with id {id} deleted successfully.")
        return f"Customer with id {id} deleted"
    except Exception as e:
        logging.error(f"Failed to process payload: {e}")
        return "Error processing the payload" 

Application Code

I decided to build this sample application by using a dotnet backend and React frontend with some Tailwind styling. I am not going to go through the whole app logic as it is quite long and simple, but I will show some pieces on how to use GraphQL API and UDF’s from dotnet app.

Calling GraphQL

Dotnet has multiple libraries for using GraphQL API’s, but you can quite easily use it with pure HTTP client. The idea in GraphQL is to provide a query structure as parameter, which defines what we are going to query and how. On top of that we need to also provide variables that contains values for the query. In sample below I have C# method that returns paginated collection of customers from Fabric. As of today Fabric always adds pagination for GraphQL queries automatically.

public async Task<ActionResult<IEnumerable<Customer>>> GetCustomers(int? first = 100, string after = null)
 {
     // GraphQL query to get all customers
     var query = @"
         query($first: Int, $after: String) {
             customers(first: $first, after: $after) {
                 items {
                     Id
                     Name
                     Address
                     Email
                     IsBillable
                     IsOnlineCustomer
                     City
                     Phone
                 }
                 endCursor
                 hasNextPage
             }
         }";

     var variables = new
     {
         first,
         after
     };

     var graphQLRequest = new
     {
         query,
         variables
     };

     var content = new StringContent(JsonSerializer.Serialize(graphQLRequest), Encoding.UTF8, "application/json");
     var response = await _httpClient.PostAsync(_graphqlEndpoint, content);

     if (!response.IsSuccessStatusCode)
     {
         return StatusCode((int)response.StatusCode, "Failed to retrieve customers");
     }

     var responseBody = await response.Content.ReadAsStringAsync();
     
     // Log the raw GraphQL response for debugging
     Console.WriteLine($"GraphQL Response: {responseBody}");
     
     var graphQLResponse = JsonSerializer.Deserialize<GraphQLResponse<CustomersData>>(responseBody, new JsonSerializerOptions 
     { 
         PropertyNameCaseInsensitive = true 
     });

     if (graphQLResponse.Errors != null && graphQLResponse.Errors.Count > 0)
     {
         return BadRequest(graphQLResponse.Errors[0].Message);
     }

     // Extract the items from the CustomerConnection and return as a plain list
     var customerItems = graphQLResponse.Data?.Customers?.Items ?? new List<Customer>();
     
     // Check what we're actually returning
     Console.WriteLine($"Returning {customerItems.Count} customers");
     
     return Ok(customerItems);
 }

Calling User Data Functions

User Data Functions are simple HTTP endpoints, which we can invoke by sending POST message. Our UDF sample used Python dictionary parameter type so we can send serialized C# object as paylod and it will be handled correctly by our function. The code to call the User Data Function is super simple and there is no extra hazzle required. For authentication I have used some default headers which are set when the _httpClient is instantiated.

 public async Task<IActionResult> UpdateCustomer(int id, Customer customer)
 {
     string endpoint = $"{_baseAddress}/{WorkspaceId}/userDataFunctions/{UserDataFunctionId}/functions/update_customer/invoke";
     customer.Id = id;
     var payload = new { data = customer, id };
     var content = new StringContent(
         JsonSerializer.Serialize(payload),
         Encoding.UTF8,
         "application/json"
     );

     var response = await _httpClient.PostAsync(endpoint, content);

     if (response.IsSuccessStatusCode)
     {
         return Ok();
     }
     
     return StatusCode((int)response.StatusCode, "Failed to update customer");
 }

And finally this is what our application looks like:

I vibe coded this UI in like 30 minutes with Cline. The UI shows customers as tiles and has features to create new customer, update existing ones and delete them. Styling is done with Tailwind. Remember that this application is showing data directly from Microsoft Fabric Warehouse.

As I have mentioned in my earlier posts, Fabric has some delay when first request is made after 5 minutes of idle time. I don’t what causes this, but usually the first request takes like 10 seconds to complete. After that the performance is in good level. The initial delay with Lakehouse and Warehouse datastores is one reason why I don’t build customer facing apps on top of those.

Summary

By bridging software engineering practices with data engineering capabilities, Microsoft Fabric enables you to build scalable, maintainable, and secure applications on a unified data platform. GraphQL APIs provide a flexible data interface; User Data Functions can encapsulate business logic; and Azure API Management provides API governance. Combined with Fabric’s core services—OneLake, Data Pipelines, Notebooks, Warehouses, and Real-Time Intelligence—you can transform raw data into insights and deliver modern applications that can evolve with your organization’s needs.