DataAnalysisLibrary.Connectors.Adx 0.1.2

There is a newer version of this package available.
See the version list below for details.
dotnet add package DataAnalysisLibrary.Connectors.Adx --version 0.1.2
                    
NuGet\Install-Package DataAnalysisLibrary.Connectors.Adx -Version 0.1.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DataAnalysisLibrary.Connectors.Adx" Version="0.1.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DataAnalysisLibrary.Connectors.Adx" Version="0.1.2" />
                    
Directory.Packages.props
<PackageReference Include="DataAnalysisLibrary.Connectors.Adx" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add DataAnalysisLibrary.Connectors.Adx --version 0.1.2
                    
#r "nuget: DataAnalysisLibrary.Connectors.Adx, 0.1.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package DataAnalysisLibrary.Connectors.Adx@0.1.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=DataAnalysisLibrary.Connectors.Adx&version=0.1.2
                    
Install as a Cake Addin
#tool nuget:?package=DataAnalysisLibrary.Connectors.Adx&version=0.1.2
                    
Install as a Cake Tool

DataAnalysisLibrary

CI NuGet License

Current Version: 0.1.2 · 繁體中文

A clean, loosely-coupled C# data analysis library that abstracts query-language generation (KQL / T-SQL / PostgreSQL SQL) away from your application code. Query multiple heterogeneous data sources through a single unified interface — no database-specific logic leaks into your business layer.

Live Demo

Try it out at https://jackyai.azurewebsites.net/data-analysis

The demo runs all four query types live against a real SQL Server table (dbo.Orders). Sign in to add your own Azure Data Explorer or SQL Server connectors.


Installation

Install Core (required) plus whichever connector(s) you need:

# Required
dotnet add package DataAnalysisLibrary.Core

# One or more connectors
dotnet add package DataAnalysisLibrary.Connectors.Adx        # Azure Data Explorer (KQL)
dotnet add package DataAnalysisLibrary.Connectors.MsSql       # Microsoft SQL Server
dotnet add package DataAnalysisLibrary.Connectors.PostgreSql  # PostgreSQL

Quick Start

Step 1 — Build the engine

Register your data sources with DataAnalysisEngineBuilder and call Build(). The resulting engine is thread-safe and suitable for singleton injection.

// ADX (Service Principal auth)
var adxConfig = new AdxConfig
{
    SourceKey    = "adx",
    ClusterUri   = "https://yourcluster.kusto.windows.net",
    Database     = "YourDatabase",
    ClientId     = "your-client-id",
    ClientSecret = "your-client-secret",
    TenantId     = "your-tenant-id"
};

// SQL Server
var mssqlConfig = new MsSqlConfig
{
    SourceKey        = "mssql",
    ConnectionString = "Server=myserver;Database=mydb;Integrated Security=true;"
};

// PostgreSQL
var pgConfig = new PostgreSqlConfig
{
    SourceKey        = "pg",
    ConnectionString = "Host=localhost;Database=mydb;Username=postgres;Password=secret"
};

var engine = new DataAnalysisEngineBuilder()
    .AddSource(new AdxDataSourceFactory(adxConfig))
    .AddSource(new MsSqlDataSourceFactory(mssqlConfig))
    .AddSource(new PostgreSqlDataSourceFactory(pgConfig))
    .Build();

ASP.NET Core DI:

builder.Services.AddSingleton(engine);

Step 2 — Run a query

Pick one of the four query methods and pass the appropriate SourceKey.


Query Types

Summarize — Aggregation

Equivalent to SQL GROUP BY / ADX summarize. Use it to compute aggregated metrics.

var result = await engine.SummarizeAsync(new SummarizeRequest
{
    SourceKey = "mssql",
    TableName = "Orders",
    Filters   = [new FilterDimension("Region", ["APAC", "EMEA"])],
    ViewBy    = [new GroupField("Region"), new GroupField("ProductCategory")],
    Measures  =
    [
        new MeasureField("Revenue",    AggregationType.Sum),
        new MeasureField("OrderId",    AggregationType.Count,         Alias: "OrderCount"),
        new MeasureField("CustomerId", AggregationType.DistinctCount, Alias: "UniqueCustomers"),
        new MeasureField("UnitPrice",  AggregationType.Average,       Alias: "AvgPrice"),
    ]
});

if (result.IsSuccess)
{
    foreach (var row in result.Value!.Rows)
    {
        var region  = row.Get<string>("Region");
        var revenue = row.Get<decimal>("Revenue");
        var orders  = row.Get<int>("OrderCount");
        Console.WriteLine($"{region}: {revenue:C} ({orders} orders)");
    }
}

Available aggregation types:

Value Description ADX MSSQL / PostgreSQL
Count Row count count() COUNT(*)
DistinctCount Distinct value count dcount() COUNT(DISTINCT ...)
Sum Total sum() SUM()
Average Mean avg() AVG()
Max Maximum max() MAX()
Min Minimum min() MIN()

ViewBy — Group Field Types

ViewBy accepts any mix of four GroupFieldBase subtypes. The library translates each one to the correct SQL / KQL expression per connector.

GroupField — plain column grouping
new GroupField("Region")               // GROUP BY [Region]
new GroupField("Region", Alias: "Rgn") // GROUP BY [Region] AS [Rgn]
DateTruncGroupField — date/time truncation

Groups a datetime column by a calendar granularity. No application-side processing needed — the truncation runs inside the query engine.

ViewBy =
[
    new DateTruncGroupField("OrderDate", DateGranularity.Month),         // "2024-03"
    new DateTruncGroupField("OrderDate", DateGranularity.Quarter, "Qtr") // "2024Q1"
]

Available granularities and generated expressions:

DateGranularity MSSQL PostgreSQL ADX (KQL)
Year YEAR(col) EXTRACT(YEAR FROM col)::text format_datetime(col,'yyyy')
Quarter CONCAT(YEAR(col),'Q',DATEPART(QUARTER,col)) EXTRACT(YEAR…)::text \|\| 'Q' \|\| EXTRACT(QUARTER…)::text strcat(tostring(getyear(col)),'Q',…)
Month FORMAT(col,'yyyy-MM') TO_CHAR(col,'YYYY-MM') format_datetime(col,'yyyy-MM')
Week CONCAT(YEAR(col),'-W',FORMAT(…,'D2')) TO_CHAR(DATE_TRUNC('week',col),'YYYY-MM-DD') format_datetime(startofweek(col),'yyyy-MM-dd')
Day CAST(col AS DATE) TO_CHAR(col,'YYYY-MM-DD') format_datetime(col,'yyyy-MM-dd')
Hour FORMAT(col,'yyyy-MM-dd HH:00') TO_CHAR(col,'YYYY-MM-DD HH24:00') format_datetime(col,'yyyy-MM-dd HH:00')
NumericBinGroupField — numeric range buckets

Splits a numeric column into labeled, left-closed right-open intervals [Min, Max). null on either end means open-ended.

ViewBy =
[
    new NumericBinGroupField("Age",
        Bins:
        [
            new NumericBin(null, 18,   "Minor"),   // Age < 18
            new NumericBin(18,   65,   "Adult"),   // 18 ≤ Age < 65
            new NumericBin(65,   null, "Senior"),  // Age ≥ 65
        ],
        Alias:    "AgeGroup",
        Fallback: "Unknown")   // rows matching no bin → "Unknown" (default: "Other")
]

Generated SQL (T-SQL / PostgreSQL):

CASE
  WHEN [Age] < 18        THEN 'Minor'
  WHEN [Age] >= 18 AND [Age] < 65 THEN 'Adult'
  WHEN [Age] >= 65       THEN 'Senior'
  ELSE 'Unknown'
END AS [AgeGroup]

Generated KQL:

['AgeGroup']=case(['Age'] < 18, 'Minor', ['Age'] >= 18 and ['Age'] < 65, 'Adult', ['Age'] >= 65, 'Senior', 'Unknown')
StringGroupingField — many-to-one string mapping

Consolidates raw string values into logical groups (e.g. roll up countries → regions). Values not matched by any group receive the Fallback label.

ViewBy =
[
    new StringGroupingField("Country",
        Groups:
        [
            new StringGroup("AMEA",    ["TW", "HK", "SG", "IN"]),
            new StringGroup("America", ["US", "CA", "BR"]),
        ],
        Alias:    "Region",
        Fallback: "Other")
]

Generated SQL (T-SQL / PostgreSQL):

CASE
  WHEN [Country] IN ('TW', 'HK', 'SG', 'IN') THEN 'AMEA'
  WHEN [Country] IN ('US', 'CA', 'BR')        THEN 'America'
  ELSE 'Other'
END AS [Region]

Generated KQL:

['Region']=case(['Country'] in ('TW', 'HK', 'SG', 'IN'), 'AMEA', ['Country'] in ('US', 'CA', 'BR'), 'America', 'Other')
Mixing GroupField types

All four types can be combined freely in one ViewBy list:

var result = await engine.SummarizeAsync(new SummarizeRequest
{
    SourceKey = "mssql",
    TableName = "Orders",
    ViewBy =
    [
        new StringGroupingField("Country", [
            new StringGroup("AMEA",    ["TW", "HK"]),
            new StringGroup("America", ["US", "CA"]),
        ], Alias: "Region"),
        new DateTruncGroupField("OrderDate", DateGranularity.Month),
        new NumericBinGroupField("Amount", [
            new NumericBin(null, 1000,  "Small"),
            new NumericBin(1000, 10000, "Medium"),
            new NumericBin(10000, null, "Large"),
        ], Alias: "DealSize"),
    ],
    Measures = [new MeasureField("OrderId", AggregationType.Count, Alias: "Orders")]
});

Details — Row-level data

Retrieves raw rows with server-side pagination — no in-memory buffering.

var result = await engine.DetailsAsync(new DetailsRequest
{
    SourceKey    = "adx",
    TableName    = "EventLogs",
    SelectFields = ["EventId", "Timestamp", "Level", "Message"],  // empty = all columns
    Filters      =
    [
        new FilterDimension("Level",  ["ERROR", "CRITICAL"]),
        new FilterDimension("Region", ["TW"], FilterOperator.NotIn),
    ],
    OrderBy = [new SortField("Timestamp", Descending: true)],
    Skip    = 0,
    Take    = 50,
});

if (result.IsSuccess)
{
    Console.WriteLine($"Total: {result.Value!.TotalCount} rows");
    foreach (var row in result.Value.Rows)
        Console.WriteLine($"[{row["Level"]}] {row["Message"]}");
}

FieldList — Schema inspection

Returns column definitions including the native type and a cross-source canonical type — useful for dynamic UI rendering.

var result = await engine.FieldListAsync(new FieldListRequest
{
    SourceKey = "pg",
    TableName = "customers"
});

if (result.IsSuccess)
{
    foreach (var field in result.Value!.Fields)
    {
        // DataType      = native type string, e.g. "character varying", "integer"
        // CanonicalType = unified enum across all connectors, e.g. String, Integer
        Console.WriteLine($"{field.Name,-20} {field.DataType,-25} → {field.CanonicalType}  nullable={field.IsNullable}");
    }
}

Example output (PostgreSQL customers table):

id                   integer                   → Integer        nullable=False
name                 character varying         → String         nullable=False
email                character varying         → String         nullable=True
created_at           timestamp with time zone  → DateTimeOffset nullable=False
is_active            boolean                   → Boolean        nullable=False
Mapping CanonicalType to UI components
foreach (var field in result.Value!.Fields)
{
    var component = field.CanonicalType switch
    {
        ColumnDataType.DateTime or
        ColumnDataType.DateTimeOffset or
        ColumnDataType.Date    => "DatePicker",

        ColumnDataType.Boolean => "Toggle",

        ColumnDataType.Integer or
        ColumnDataType.Float   or
        ColumnDataType.Decimal => "NumberInput",

        ColumnDataType.Json    => "JsonEditor",

        _                      => "TextInput"   // String, Unknown, etc.
    };

    Console.WriteLine($"{field.Name} → {component}");
}

Full ColumnDataType reference:

Value Native types it maps from
String nvarchar, varchar, text, string, character varying …
Integer int, bigint, long, smallint, integer …
Float float, real, double precision …
Decimal decimal, numeric, money …
Boolean bit, bool, boolean
DateTime datetime, datetime2, timestamp
DateTimeOffset datetimeoffset, timestamptz, timestamp with time zone
Date date
Time time
TimeSpan ADX timespan, PostgreSQL interval
Guid uniqueidentifier, uuid, guid
Binary varbinary, bytea, binary
Json ADX dynamic, jsonb, json
Xml xml
Unknown Fallback when no mapping is found

MaxDate — Date boundary

Returns MAX() of a date column. Commonly used to check data freshness or compute time ranges.

var result = await engine.MaxDateAsync(new MaxDateRequest
{
    SourceKey     = "adx",
    TableName     = "TelemetryEvents",
    DateFieldName = "Timestamp",
    Filters       = [new FilterDimension("DeviceType", ["Sensor"])]
});

if (result.IsSuccess)
{
    var bound = result.Value!.BoundDate;
    if (bound is null)
        Console.WriteLine("Table is empty");
    else
        Console.WriteLine($"Latest record: {bound:yyyy-MM-dd HH:mm:ss zzz}");
}

Filters (FilterDimension)

// IN (default) — most common
new FilterDimension("Status", ["Active", "Pending"])

// NOT IN
new FilterDimension("Region", ["TW"], FilterOperator.NotIn)

// Single-value comparisons
new FilterDimension("Score",    ["90"], FilterOperator.GreaterThan)
new FilterDimension("Priority", ["3"],  FilterOperator.LessThan)
new FilterDimension("Code",     ["US"], FilterOperator.Equals)

// Range (Between requires exactly two values: [min, max])
new FilterDimension("Amount", ["100", "500"], FilterOperator.Between)

// Substring match (translated to LIKE / ILIKE)
new FilterDimension("Name", ["John"], FilterOperator.Contains)

ADX limitation: ADX only supports In and NotIn. Other operators will throw NotSupportedException.


Result Pattern

All query methods return QueryResult<T> and never throw (except OperationCanceledException).

var result = await engine.SummarizeAsync(request);

if (result.IsSuccess)
{
    var data = result.Value!;  // guaranteed non-null
    // use data...
}
else
{
    logger.LogError("Query failed [{Code}]: {Message}", result.ErrorCode, result.ErrorMessage);
}

Built-in error codes:

ErrorCode When it occurs
SOURCE_NOT_FOUND The SourceKey was never registered with the builder
QUERY_FAILED A runtime exception occurred (network error, bad query syntax, etc.)

CancellationToken Support

All query methods accept a CancellationToken, making them suitable for ASP.NET Core request-abort scenarios:

public async Task<IActionResult> GetSummary(CancellationToken ct)
{
    var result = await engine.SummarizeAsync(request, ct);
    // If the client disconnects, OperationCanceledException is thrown;
    // ASP.NET Core handles it automatically.
    ...
}

Extending with a New Connector

Add new data sources without modifying any existing code (Open/Closed Principle):

var engine = new DataAnalysisEngineBuilder()
    .AddSource(new AdxDataSourceFactory(adxConfig))
    .AddSource(new MyCustomDataSourceFactory(myConfig))  // ← just add this
    .Build();

See the Developer Guide for implementation details.


Requirements

  • .NET 8.0+
  • C# 12+
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 196 6/9/2026
0.1.2 129 5/24/2026
0.1.1 103 5/24/2026
0.1.0 96 5/23/2026