EFCore.Kusto
0.3.3
dotnet add package EFCore.Kusto --version 0.3.3
NuGet\Install-Package EFCore.Kusto -Version 0.3.3
<PackageReference Include="EFCore.Kusto" Version="0.3.3" />
<PackageVersion Include="EFCore.Kusto" Version="0.3.3" />
<PackageReference Include="EFCore.Kusto" />
paket add EFCore.Kusto --version 0.3.3
#r "nuget: EFCore.Kusto, 0.3.3"
#:package EFCore.Kusto@0.3.3
#addin nuget:?package=EFCore.Kusto&version=0.3.3
#tool nuget:?package=EFCore.Kusto&version=0.3.3
EFCore.Kusto
A lightweight, extensible Entity Framework Core provider for translating LINQ queries into Kusto Query Language (KQL) AKA Azure Data Explorer (ADX).
While I primarily built this to integrate with ASP.NET Core OData (v8+) for analytical workloads, it can be used standalone for any LINQ-to-KQL translation needs.
Table of Contents
- 1. Installation
- 2. Getting Started
- 3. Project Goals
- 4. Read Capabilities
- 5. Write Capabilities
- 6. Migrations (Experimental)
- 7. Changelog
- 8. Contributing
- 9. License
- 10. Disclaimer
1. Installation
Install the package from NuGet:
dotnet add package EFCore.Kusto
Or via csproj:
<ProjectReference Include="src/EFCore.Kusto/EFCore.Kusto.csproj" />
2. Getting Started
Register your DbContext with the Kusto provider and pick an authentication option:
builder.Services.AddDbContext<PropertyContext>((sp, options) => { options.UseKusto( clusterUrl: "https://<cluster>.kusto.windows.net", database: "<database>", kusto => kusto.UseManagedIdentity()); });- Use
UseManagedIdentity(clientId)for a user-assigned identity, or omit the client id for system-assigned identities. - Use
UseApplicationAuthentication(tenantId, clientId, clientSecret)for app registrations. - Use
UseTokenCredential(credential)to supply anyTokenCredential(e.g., one registered viaAddKustoManagedIdentityCredentialorAddKustoApplicationRegistration). - If no authentication option is configured, the provider falls back to
DefaultAzureCredentialwhen executing queries.
- Use
Optional: register shared credentials so they can be reused when building
DbContextoptions:builder.Services.AddKustoManagedIdentityCredential(clientId: "<client-id>"); // or builder.Services.AddKustoApplicationRegistration( tenantId: "<tenant-id>", clientId: "<client-id>", clientSecret: "<client-secret>");These helpers register a singleton
TokenCredentialyou can inject when callingUseTokenCredentialinsideAddDbContext.
3. Project Goals
- Provide a reliable LINQ-to-KQL translation layer.
- Integrate cleanly with ASP.NET Core OData (v8+).
- Offer predictable, debuggable SQL generation.
- Ensure correctness and performance for high‑volume analytical datasets.
- Remain lightweight with minimal runtime overhead.
4. Read Capabilities
Query Translation
This provider currently supports:
WherefiltersSelectprojections- Ordering (
OrderBy,ThenBy) - Pagination (
Skip,Take) GroupBywith aggregates (Sum,Min,Max,Average,Count,LongCount,Count(predicate)→countif,Distinct().Count()→dcount)- Conditional expressions (
?:) →iif/case - Basic join translation used by OData
$expand - Counts as used by OData
$count
OData Compatibility
EFCore.Kusto works well with:
$filter$select$orderby$count$skip,$top$expand(entity relationships)
If specific OData query shapes cause issues, they can be addressed case‑by‑case. Community reports are welcome.
5. Write Capabilities
EFCore.Kusto supports data modification using Kusto-native control commands.
Supported Operations
- Insert (
Add,AddRange) via.ingest - Update (
Update) via.update table - Delete (
Remove,RemoveRange) via.delete table
Batching Semantics
- Commands are batched per entity type and target table
- Read and write operations are never mixed in the same batch
- Each batch is executed as a single Kusto command
Note: Transactional guarantees and concurrency semantics are constrained by Kusto’s execution model.
6. Migrations (Experimental)
EF Core migrations are supported, translating schema operations into KQL control commands
(.create-merge table, .alter-merge table, .drop, .rename, etc.). Standard tooling works:
dotnet ef migrations add, dotnet ef database update, dotnet ef migrations script. Applied
migrations are tracked in an EFMigrationsHistory table.
Migrations run against the database selected by UseKusto(cluster, database) — Kusto's database
is the schema equivalent and is fixed at the connection, so EF schema operations are no-ops.
This feature is experimental. Limitations:
- Indexes, foreign keys, primary keys, unique/check constraints, and sequences are no-ops (Kusto has none).
- Migrations are non-transactional — no rollback; execution stops at the first failing command.
.alter column type=clears existing data in that column (Kusto semantics).
7. Changelog
See CHANGELOG.md for a detailed version history.
8. Contributing
Contributions are welcome.
If you encounter a translation issue, please include:
- The LINQ expression (or OData URL if applicable).
- The expected KQL.
- The generated KQL (if available).
This helps isolate translation gaps quickly.
9. License
MIT License – simple, permissive, widely accepted.
EFCore.Kusto is free for commercial and open‑source use.
10. Disclaimer
While this provider is functional and under active development, it is not yet battle-tested in production environments.
If you encounter unexpected behavior, open an issue — the goal is full reliability for production workloads.
| Product | Versions 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 is compatible. 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 is compatible. 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. |
-
net10.0
- Azure.Identity (>= 1.13.0)
- Microsoft.Azure.Kusto.Data (>= 14.0.2)
- Microsoft.EntityFrameworkCore (>= 10.0.9)
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.9)
-
net8.0
- Azure.Identity (>= 1.13.0)
- Microsoft.Azure.Kusto.Data (>= 14.0.2)
- Microsoft.Bcl.Memory (>= 9.0.14)
- Microsoft.EntityFrameworkCore (>= 8.0.8)
- Microsoft.EntityFrameworkCore.Relational (>= 8.0.8)
- Microsoft.Extensions.Caching.Memory (>= 8.0.1)
-
net9.0
- Azure.Identity (>= 1.13.0)
- Microsoft.Azure.Kusto.Data (>= 14.0.2)
- Microsoft.EntityFrameworkCore (>= 9.0.9)
- Microsoft.EntityFrameworkCore.Relational (>= 9.0.9)
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 |
|---|---|---|
| 0.3.3 | 40 | 10/28/2026 |
| 0.3.2 | 72 | 10/27/2026 |
| 0.3.1 | 112 | 10/20/2026 |
| 0.3.0 | 114 | 10/2/2026 |
| 0.2.11 | 139 | 8/29/2026 |
| 0.2.10 | 181 | 8/27/2026 |
| 0.2.9 | 124 | 8/21/2026 |
| 0.2.8 | 118 | 8/21/2026 |
| 0.2.7 | 132 | 8/13/2026 |
| 0.2.6 | 134 | 8/10/2026 |
| 0.2.5 | 275 | 6/17/2026 |
| 0.2.4 | 161 | 6/1/2026 |
| 0.2.3 | 250 | 5/25/2026 |
| 0.2.2 | 152 | 5/8/2026 |
| 0.2.1 | 312 | 3/11/2026 |
| 0.2.0 | 189 | 1/30/2026 |
| 0.1.9 | 151 | 1/28/2026 |
| 0.1.8 | 163 | 1/23/2026 |
| 0.1.7 | 161 | 1/18/2026 |
| 0.1.6 | 168 | 1/9/2026 |
Version 0.3.3:
- Fixed batched updates failing when a column was set to null: the changes bag contained typed nulls such as guid(null), datetime(null) or bool(null), which Kusto's dynamic literals don't accept. Those are now sent as null; long(null), real(null) and dynamic(null) are kept.
Version 0.3.2:
- Fixed batched updates failing when an int or decimal column changed: the changes bag contained int(...) and decimal(...) literals, which Kusto's dynamic literals don't accept. Those values are now sent as bare numbers.
- Fixed reading an int property throwing when Kusto returned the column as another numeric type; the value is now converted to int.
Version 0.3.1:
- Fixed OData $apply aggregates generating invalid KQL: aggregate, groupby + aggregate, several aggregates together and $count failed with "Duplicate column name(s): Name, Value" or "Syntax error: Expected: ,". Constants no longer appear in the summarize aggregate list and every result column now gets a unique name.
- Fixed type conversions inside queries being emitted as SQL CAST(x AS type), which Kusto rejects. They now emit todecimal, toreal, tolong, toint, tostring, tobool, todatetime and toguid, and a conversion between identical types is dropped. A DateOnly conversion maps to datetime.
- Fixed GroupBy(...).OrderBy(g => g.Count()) (and LongCount()) placed before Select emitting "order by count()", which is invalid KQL, instead of the aggregate's column name.
- Fixed decimal results wider than .NET's decimal (for example avg over a decimal column, up to 34 digits) throwing "Conversion overflows."; they are now rounded to 28 digits.
- Batched updates now send each changed value as a typed KQL literal (datetime(...), decimal(...), ...) instead of a JSON-serialized value, so dates and decimals keep their Kusto type.
Version 0.3.0:
- BREAKING: Guid properties now map to Kusto's native guid type instead of string, and new migrations create guid columns. Tables created by earlier versions with a string column for a Guid property must be altered or recreated: reading such a column into a Guid property fails, because the reader cannot cast a string column to Guid.
- Added translation of date/time components (Year, Month, Day, DayOfYear, Hour, Minute, Second, Millisecond, Date, DayOfWeek) on DateTime, DateOnly and DateTimeOffset. Predicates using them previously threw NotSupportedException, notably OData $filter date comparisons.
- Batched updates are now sent as an inline datatable joined on the key, instead of one union leg per row: a 1000-row batch against a 443-column table went from ~35 s and ~30 CPU-seconds to ~2 s and ~1 CPU-second. Each row carries only the columns it changed, so rows changing different columns no longer overwrite each other.
- Batched updates are split into .update commands that each stay within a maximum length (UseMaxUpdateCommandLength, default 2,095,674), instead of one command that Kusto rejected with SYN0009 past 2,097,152 characters. The commands are not transactional: if one fails, the ones before it stay applied.
- Fixed Any(predicate)/All(predicate) over a shadow array-column property throwing NotSupportedException for predicates mixing == and != leaves, e.g. Any(a => a == x || a != y) and All(a => a != x && a == y).
- Fixed byte[] values being written into KQL literals as a JSON array instead of base64.
- net8.0: pinned the transitive Microsoft.Extensions.Caching.Memory (8.0.1) and Microsoft.Bcl.Memory (9.0.14) to versions without known high-severity advisories.
Version 0.2.11:
- Fixed Any(predicate)/All(predicate) over a shadow array-column property silently discarding the predicate for anything beyond a single equality/inequality against one constant (it collapsed to "array is non-empty" regardless of the predicate). Compound predicates (Any(a => a == x || a == y), All(a => a != x && a != y)) now translate correctly; unsupported shapes now throw instead of silently returning the wrong answer.
- Fixed Queryable.Contains over the same shadow array-column property mis-typing a parameterized non-string value (e.g. a captured int), binding it with DbType = String instead of its correct type.
Version 0.2.10:
- Fixed string.Contains/StartsWith/EndsWith (the plain single-string-argument overloads) throwing NotSupportedException; they now translate to Kusto's native contains_cs/startswith_cs/endswith_cs operators. This notably broke OData's $filter=contains(...)/startswith(...)/endswith(...) query functions.
Version 0.2.9:
- Fixed batched deletes (e.g. RemoveRange) generating malformed KQL for 2+ rows due to an unbalanced parenthesis in AppendDeleteOperation.
Version 0.2.8:
- Added string.IsNullOrEmpty/IsNullOrWhiteSpace translation to isempty()/isempty(trim(...)); previously unsupported and silently missed actual-null rows via EF Core's default expansion.
- Added opt-in UseIsEmptyForStringIsNull(): when enabled, x.Field == null / != null on a string operand generates isempty()/isnotempty() instead of isnull()/isnotnull(), since a Kusto string column can never hold a database null. Disabled by default.
- Fixed null literals reached through a constant or CASE branch (e.g. a ternary's null arm) rendering as the invalid bare text "null" instead of a typed null (int(null), datetime(null), guid(null), ...) or, for strings, "".
Version 0.2.7:
- Added support for inner and right joins (previously only left join was translated); unrecognized join expression types now throw instead of silently falling back to a left join.
- Fixed DbType.Decimal being mapped to Kusto's "real" (binary floating-point) type instead of its own "decimal" type, which could silently lose precision on decimal parameters.
- Control-command routing (.show/.drop/etc. vs. query) is now decided once from the provider's own pristine command text before any DbCommandInterceptor can mutate it, closing a spoofing/tampering gap in the previous execution-time text sniff. Commands created outside the EF Core pipeline (e.g. via DbConnection.CreateCommand() directly) fall back to the original sniffing behavior unchanged.
Version 0.2.6:
- Fixed regression with count translation introduced in 0.2.3.
Version 0.2.5:
- Multi-targeted net8.0, net9.0 and net10.0, building against EF Core 8, 9 and 10 respectively. Adapted to the EF Core 9 migrations API (HistoryRepository database lock, IMigrationCommandExecutor overloads) and the EF Core 10 RelationalCommand logCommandText. Query parameter naming now works across versions (EF Core 10 dropped the "__" prefix).
Version 0.2.4:
- Added experimental EF Core migrations support (schema ops -> KQL control commands, EFMigrationsHistory tracking). Non-transactional; alter-column-type clears data; relational-only constructs are no-ops.
Version 0.2.3:
- Added GroupBy -> KQL summarize translation (Sum/Min/Max/Avg/Count/LongCount, countif, dcount).
- Added conditional (?:) translation to iif / case.
- Fixed parameter substitution to emit typed KQL literals (was using raw ToString).
Version 0.2.2:
- Fixed duplicate parameter issue.
Version 0.2.1:
- Convert from Hex string for byte arrays.
Version 0.2.0:
- Added support for `Any()` translation.
Version 0.1.9:
- Added support for OUTER APPLY and CROSS APPLY.
Version 0.1.8:
- Fixed inequality comparisons on strings.
Version 0.1.7:
- Fixed `not` operator translation
- Fixed duplicate column issue in joins
- Implemented `Contains`
Version 0.1.6:
- Fixed NULL handling in PATCH requests.
- Better string escaping in PATCH requests.
Version 0.1.5:
- Optimized `.update` command to use less nesting and support larger batches.
Version 0.1.4:
- Fixed `COUNT(*)` regression resulting from `KustoQuerySqlGenerator.WriteProjection` refactor.
Version 0.1.3:
- Added support for `DateOnly` type translation.
Version 0.1.2:
- Added write command batching per entity/table.
Version 0.1.1:
- Added update support via Kusto `.update table` commands.
Version 0.1.0:
- Initial release (read-only query support).