CubeSharp is a .NET library for constructing and querying in-memory multidimensional data cubes, offering hierarchical navigation, flexible aggregations, and async LINQ-based querying.
$ dotnet add package CubeSharpHigh-performance .NET library for constructing and querying in-memory multidimensional data cubes (a.k.a. OLAP style aggregates) using strongly-typed, testable C# code.
NuGet:
Targets: .NET 8, .NET 9
Detailed walkthroughs and additional examples are provided in the notebooks:
- Tutorial (concepts, end‑to‑end): 01-Tutorial.ipynb
- Cookbook / patterns: 02-Examples.ipynb
Building multi‑factor tabular reports (dimensions, totals, hierarchies) directly with ad‑hoc LINQ or SQL quickly becomes fragile, repetitive, and hard to unit test. CubeSharp lets you:
Everything runs purely in memory over existing collections / async streams.
dotnet add package CubeSharp
(Temporary package id retains historical name; library namespace / docs use CubeSharp.)
var orders = new[] {
new { OrderDate = new DateTime(2007,08,02), Product = "X", EmployeeId = 3, CustomerId = "A", Quantity = 10m },
new { OrderDate = new DateTime(2007,12,24), Product = "Y", EmployeeId = 4, CustomerId = "B", Quantity = 12m },
// ...
};
// 1. Aggregation (measure)
var qty = AggregationDefinition.CreateForCollection(
orders, o => o.Quantity, (a,b) => a + b, 0m);
// 2. Dimensions (explicit index ordering + trailing total)
var customers = DimensionDefinition.CreateForCollection(
orders, o => o.CustomerId, title: "Customers",
IndexDefinition.Create("A","Customer A"),
IndexDefinition.Create("B","Customer B"))
.WithTrailingDefaultIndex("Total");
var years = DimensionDefinition.CreateForCollection(
orders, o => o.OrderDate.Year.ToString(), title: "Years",
IndexDefinition.Create("2007","2007 Year"),
IndexDefinition.Create("2008","2008 Year"))
.WithTrailingDefaultIndex("Total");
// 3. Build cube
var cube = orders.BuildCube(qty, customers, years);
// 4. Query some cells
var a2007 = cube.GetValue("A", "2007");
var all2007 = cube.GetValue(default, "2007"); // total over customers for 2007
var aAllYears = cube.GetValue("A"); // trailing default omitted
var grandTotal = cube.GetValue(); // all defaults omitted
// 5. Create a table (rows: customers, columns: years)
var table = cube
.BreakdownByDimensions(..^1) // all but last dimension => rows
.Select(row => row
.GetBoundDimensionsAndIndexes()
.Select(di => KeyValuePair.Create(di.dimension.Title!, (object?)di.dimension[di.index].Title))
.Concat(row.BreakdownByDimensions(^1)
.Select(col => KeyValuePair.Create(col.GetBoundIndexDefinition(^1).Title!, (object?)col.GetValue())))
.ToDictionary(k => k.Key, v => v.Value));
For a fuller narrative see doc/01-Tutorial.ipynb.
Because slices retain (dimension, index) metadata you can:
See reusable helpers & variations in doc/02-Examples.ipynb (e.g. ToTable extension) and narrative in doc/01-Tutorial.ipynb (Generic Cube Operations section).
| Aspect | Notes |
|---|---|
| Build | Single pass over source; each item routed to indexes of each dimension (multi-selection may fan out). |
| Memory | Stores aggregated values per Cartesian product of explicitly declared indexes (controlled cardinality). |
| Query | O(1) per cell lookup (array indexing). |
| Breakdown | Enumerates pre-computed slices; no re-aggregation. |
Design encourages declaring only required indexes (no auto-discovery unless you choose to). This makes cube size predictable & testable.
Q: How do I get a grand total? A: cube.GetValue(). Q: How do I add a total row/column? A: Add a default index (WithLeading/TrailingDefaultIndex). Q: Missing index? A: Returns seedValue (no exception). Q: Different dimension key types? A: Normalize to a single TIndex (e.g. string) during definition.
More Q&A patterns in doc/02-Examples.ipynb.
dotnet build && dotnet testIssues / ideas: please include concise reproduction plus expected vs actual.
MIT (see LICENSE). Packaged README references this file.