Azure Kusto with Cabazure
Querying Azure Data Explorer (Kusto) from .NET requires managing connections, embedding .kusto scripts, parametrizing queries, deserializing results, and handling pagination. It’s a lot of boilerplate—connection strings, credential chains, data readers, continuation tokens. Each project ends up with slightly different patterns. I found myself writing the same infrastructure code repeatedly.
That’s why I use Cabazure.Kusto—a .NET library written by my colleague @rickykaare that simplifies executing Kusto queries from .NET applications. Define your queries as records, embed .kusto scripts alongside your code, and let the framework handle connection management, parameter binding, result deserialization, and pagination. Your API endpoints become three lines of code.
Why Query Abstraction Matters
When you query Kusto directly, you manage low-level details: construct connection strings, authenticate with DefaultAzureCredential, open readers, map columns to objects, handle pagination tokens. This repeats across every endpoint. Each query becomes a Task that returns a list or a single object. Pagination requires passing session IDs and continuation tokens, then mapping response headers. It adds noise to your business logic.
Cabazure.Kusto removes this by providing a unified query interface. Define a query record, create a matching .kusto file, and inject IKustoProcessor. Call ExecuteAsync with your query and cancellation token. The framework handles authentication, script loading, parameter binding, result mapping, and pagination. Your controller focuses on business logic, not infrastructure.
Setting Up Cabazure.Kusto
Register the library in your dependency injection container:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCabazureKusto(o =>
{
o.HostAddress = new Uri("https://help.kusto.windows.net/");
o.DatabaseName = "ContosoSales";
o.Credential = new DefaultAzureCredential();
});
var app = builder.Build();
The options configure your Kusto cluster URL, target database, and authentication credential. DefaultAzureCredential supports managed identity, user sign-in, or environment variables—whatever your deployment requires.
Defining Query Records and .kusto Files
Queries are defined as C# records that inherit from KustoQuery<T>, where T is your result type. Each query lives alongside a .kusto file with the same name.
Create a query record:
namespace SampleApi.Queries;
using Cabazure.Kusto;
using SampleApi.Contracts;
public record CustomersQuery(
int? CustomerId = null)
: KustoQuery<Customer>;
The record properties become query parameters. Create a matching .kusto file in the same namespace directory:
declare query_parameters (
customerId:long = long(null)
);
Customers
| where isnull(customerId) or customerId == CustomerKey
| project
CustomerKey,
FirstName,
LastName,
CompanyName,
CityName,
StateProvinceName,
RegionCountryName,
ContinentName,
Gender,
MaritalStatus,
Education,
Occupation
The .kusto file declares parameters that match your record properties (note the camelCase convention in the script). The Kusto Query Language (KQL) builds your actual query. The result columns map directly to your result type record.
Define your result type as a record:
namespace SampleApi.Contracts;
public record Customer(
int CustomerKey,
string FirstName,
string LastName,
string? CompanyName,
string CityName,
string StateProvinceName,
string RegionCountryName,
string ContinentName,
string Gender,
string MaritalStatus,
string Education,
string Occupation);
The property names and types must match the Kusto query’s output columns.
Executing Simple Queries
Once your query and result types are defined, executing is straightforward. Inject IKustoProcessor and call ExecuteAsync:
app.MapGet(
"/customers/{customerId}",
async static (
int customerId,
IKustoProcessor processor,
CancellationToken cancellationToken)
=> await processor.ExecuteAsync(
new CustomersQuery(customerId),
cancellationToken) switch
{
[{ } customer] => Results.Ok(customer),
_ => Results.NotFound(),
})
.WithName("GetCustomer");
The ExecuteAsync method returns an array of your result type. Use pattern matching or LINQ to extract the single result or handle the collection as needed. Behind the scenes, the framework loads the .kusto file, binds your record properties as parameters, connects to Kusto, executes the query, and deserializes rows into Customer objects.
Queries That Return Collections
For endpoints that return multiple results, use the same pattern:
app.MapGet(
"/customers",
async static (
IKustoProcessor processor,
CancellationToken cancellationToken)
=> await processor.ExecuteAsync(
new CustomersQuery(),
cancellationToken))
.WithName("ListCustomers");
When ExecuteAsync returns without specifying pagination, it returns the full result array. Kusto imposes limits on result sizes, so for large datasets, implement pagination.
Pagination and Continuation Tokens
For endpoints serving paginated results, use the three-parameter overload of ExecuteAsync. This supports session-based continuation:
app.MapGet(
"/customers",
async static (
[FromHeader(Name = "x-client-session-id")] string? sessionId,
[FromHeader(Name = "x-max-item-count")] int? maxItemCount,
[FromHeader(Name = "x-continuation-token")] string? continuationToken,
IKustoProcessor processor,
CancellationToken cancellationToken)
=> await processor.ExecuteAsync(
new CustomersQuery(),
sessionId,
maxItemCount ?? 100,
continuationToken,
cancellationToken))
.WithName("ListCustomers");
The processor returns a PagedResult<T>:
public record PagedResult<T>(
IReadOnlyList<T> Items,
string? ContinuationToken);
Your response includes Items (the current page) and a ContinuationToken (for fetching the next page). The client passes the token in subsequent requests to continue pagination. The sessionId maintains state across multiple requests, and maxItemCount controls page size.
Complex Queries and Aggregations
Kusto excels at analytical queries. Create a query for customer sales aggregation:
namespace SampleApi.Queries;
using Cabazure.Kusto;
using SampleApi.Contracts;
public record CustomerSalesQuery
: KustoQuery<CustomerSales>
{
}
With the matching .kusto file:
Customers
| join kind=inner SalesFact on CustomerKey
| extend CustomerName = strcat(FirstName, ' ', LastName)
| summarize
SalesAmount = todecimal(round(sum(SalesAmount), 2)),
TotalCost = todecimal(round(sum(TotalCost), 2))
by CustomerKey, CustomerName
| take 100
And the result type:
namespace SampleApi.Contracts;
public record CustomerSales(
int CustomerKey,
string CustomerName,
decimal SalesAmount,
decimal TotalCost);
Execute it the same way:
app.MapGet("/customer-sales",
(IKustoProcessor processor, CancellationToken cancellationToken)
=> processor.ExecuteAsync(
new CustomerSalesQuery(),
cancellationToken))
.WithName("GetCustomerSales");
Kusto’s analytical operators (join, summarize, extend, take, top, etc.) let you build aggregations and pivots without multiple round-trips. The query runs server-side; only results are deserialized.
The Sample Application
The Cabazure.Kusto repository includes a samples/SampleApi project that demonstrates these patterns using the public Azure Data Explorer cluster with the ContosoSales database. It shows real endpoints: listing customers, fetching a single customer, and querying customer sales data.
To run the sample:
- Clone the repository
- Navigate to
samples/SampleApi - Run
dotnet run - Visit
http://localhost:5000/swaggerto explore endpoints
The sample uses DefaultAzureCredential, which automatically discovers credentials in your environment. For local testing, ensure you have the Azure CLI authenticated.
When to Use Cabazure.Kusto
Cabazure.Kusto shines when:
- You query Kusto frequently from a .NET application. Boilerplate reduction pays off immediately.
- You have many query types and want consistency. All queries follow the same record +
.kustofile pattern. - You need pagination. The built-in continuation token handling is cleaner than managing it manually.
- You want strong typing. Your queries are C# records with compile-time type safety; result deserialization is automatic and type-checked.
- You’re building an API with multiple Kusto endpoints. Minimal setup per endpoint means faster development.
It’s less useful if you have a single, massive query or if you’re building an interactive query tool. For those scenarios, use the Kusto SDK directly.
Under the Hood
The library provides a simple but powerful abstraction:
public interface IKustoProcessor
{
Task ExecuteAsync(
IKustoCommand command,
CancellationToken cancellationToken);
Task<T?> ExecuteAsync<T>(
IKustoQuery<T> query,
CancellationToken cancellationToken);
Task<PagedResult<T>?> ExecuteAsync<T>(
IKustoQuery<IReadOnlyList<T>> query,
string? sessionId,
int? maxItemCount,
string? continuationToken,
CancellationToken cancellationToken);
}
When you call ExecuteAsync, the processor:
- Locates the
.kustofile matching your query record’s namespace and name - Binds your record properties to Kusto query parameters
- Authenticates using the configured credential
- Executes the query against your Kusto cluster
- Deserializes results into your result type
- Returns the populated objects (or a paged result with a continuation token)
All of this happens transparently. Your code is clean and focused.
Getting Started
To use Cabazure.Kusto in your project:
- Install the NuGet package:
dotnet add package Cabazure.Kusto - Register in Program.cs with your cluster URL, database, and credential
- Create a query record for each query type
- Add a matching
.kustofile alongside your query record - Define a result type record matching your Kusto columns
- Inject
IKustoProcessorand callExecuteAsync
The README in the Cabazure.Kusto repository provides full documentation and additional examples.
Conclusion
Building analytics APIs shouldn’t require managing low-level Kusto infrastructure. Cabazure.Kusto removes the boilerplate so you can focus on query logic and API contracts. If you query Azure Data Explorer from .NET, give it a try. The source code is on GitHub at https://github.com/Cabazure/Cabazure.Kusto.