Unrect.Interactive
0.3.0-alpha.11
dotnet add package Unrect.Interactive --version 0.3.0-alpha.11
NuGet\Install-Package Unrect.Interactive -Version 0.3.0-alpha.11
<PackageReference Include="Unrect.Interactive" Version="0.3.0-alpha.11" />
<PackageVersion Include="Unrect.Interactive" Version="0.3.0-alpha.11" />
<PackageReference Include="Unrect.Interactive" />
paket add Unrect.Interactive --version 0.3.0-alpha.11
#r "nuget: Unrect.Interactive, 0.3.0-alpha.11"
#:package Unrect.Interactive@0.3.0-alpha.11
#addin nuget:?package=Unrect.Interactive&version=0.3.0-alpha.11&prerelease
#tool nuget:?package=Unrect.Interactive&version=0.3.0-alpha.11&prerelease
Unrect
Financial and operational reports live in spreadsheets, and spreadsheets are flat 2D grids carrying hierarchical, heterogeneous data: a header, a summary table, N repeating client blocks each with its own sub-sections. Row-oriented parsers handle this badly — they devolve into stateful cursor logic and index arithmetic that breaks the moment a row shifts. Unrect takes a different approach: you declare the shape of the data — a header, a table bound to a record type, a repeating series bounded by a caption — and the framework decomposes the grid and projects it into typed objects. You never write a loop that walks the grid deciding what comes next.
Install
dotnet add package Unrect
dotnet add package Unrect.Spreadsheets
dotnet add package Unrect.Interactive # optional: LINQPad and notebook sugar
Unrect is the engine — the projection vocabulary, the layout composites, the strategies
that decide boundaries — and works directly over any 2D grid you can adapt to ISpace.
Unrect.Spreadsheets adds the adapters that read spreadsheet files — .xls/.xlsx today — straight into that grid;
add it when your data lives in a workbook rather than an array you built yourself.
The same package also has a streaming door, Workbook, for files too large to read
whole — see Large files below.
A header may be several rows tall — a merged "From" and "To" over Id | Code | Id | Code:
Table(2, r => r["From", "Id"].Integer()) addresses a column by its path, and
Table<Transfer>(2) binds a flat record Transfer(int FromId, string FromCode, int ToId, string ToCode)
with nothing declared.
A sheet opened with SpreadsheetSpace.CreateWithFormulas also answers for its formulas and for
what its cells look like — row["Account"].Font().Color == CellColor.Red turns "the rows someone
coloured red" into a property you filter on (.xlsx only), and
Table<T>(bind => bind.Column(t => t.IsDeprecated, row => …)) puts it on a bound record without
spelling out the other members.
Unrect.Interactive is for the first five minutes with an unfamiliar file, in LINQPad or a
notebook: sheet.At("B4") reaches a cell by hand, and sheet.ScaffoldRecord("Transaction")
(or ScaffoldClass) finds the caption row, reads a few samples under it and hands back a type
declaration as C# source to dump, paste and edit — LabelsIn.Column reads a card, labels down
a column, the same way. Both are also leaves (using static Unrect.Interactive.ScaffoldBuilders<ICellSpace>;
then v.Next(ScaffoldRecord("Transaction"))), so a half-written declaration can guess the types
of the tables it has already found; paste the type and swap the leaf for Table<Transaction>().
While a type is still being written, LooseTable<T>() (from
Unrect.Interactive.ExploratoryBuilders<TSpace>) is Table<T>() with its one strictness relaxed: a
member no column binds is left at its default with a warning, and the columns no member reads are
listed — drop the package reference when the script ships, and each one is a rename back to
Table<T>(). Nothing in a shipping declaration needs it.
GridSpace ships in the Unrect package for exactly that case:
GridSpace.Create(values, isBlank: ...) turns a plain 2D array into a space, deciding
there and then what counts as empty. It needs nothing else installed, which makes it the
way to build test fixtures and scripted data without a workbook in sight.
Show me the code
A report with a typed header, a summary table, and the same repeating per-investor block appearing twice under two different captions:
Fund IRR Report
Example Fund I 2026-06-30
--------------------------------------------------
Investors | Contribution ITD | ... | Irr
--------------------------------------------------
Cash Flows Using Transfer Date
[investor block] [investor block] ...
Cash Flows using inception date
[investor block] [investor block] ...
using Unrect.Spreadsheets;
using static Unrect.Projections.ProjectionBuilders<Unrect.Spreadsheets.ICellSpace>;
using static Unrect.Spreadsheets.SheetProjectionBuilders<Unrect.Spreadsheets.ICellSpace>;
var header = VerticalFlow(v => new
{
Title = v.Next(Text()),
Fund = v.Next(Text()),
ReportDate = v.Next(Date()),
});
// Captions bind to record properties by name (case- and whitespace-insensitive).
var summary = Table<SummaryRow>(bind => bind.Column(r => r.Investor, "Investors"));
var investorBlock = Table<CashFlow>();
// Declared once, placed twice — .Until bounds the first series so it stops at the
// second caption instead of trying to parse it as another investor block.
var series = VerticalRepeat(investorBlock);
const string Inception = "Cash Flows using inception date";
var byTransferDate = series
.Under(Caption("Cash Flows Using Transfer Date"))
.Until(RowContaining(Inception));
var byInception = series.Under(Caption(Inception));
var report = VerticalFlow(v => new
{
Header = v.Next(header),
Summary = v.Next(summary),
ByTransferDate = v.Next(byTransferDate),
ByInception = v.Next(byInception),
});
var result = report.Map(SpreadsheetSpace.Create("irr-report.xlsx", "IRR"));
record SummaryRow(string Investor, decimal ContributionItd, decimal DistributionItd,
decimal ManagementFeeItd, decimal EndBalance, double Irr);
record CashFlow(string InvestorName, DateTime Date, string Transaction, double Irr);
The ideas
- Projections are reusable, immutable values. Declare
reportonce, apply it to as many workbooks as you have —workbooks.Select(report.Map). - Diagnostics carry a declaration path and an A1 cell location — a failure tells you which projection it came from and exactly where on the sheet it happened.
- Names are inferred from your own identifiers. The local you assign a projection to
(
series,byTransferDate) is what shows up in its diagnostics — no separate naming step. - Tolerance is declared per projection, never ambient.
.Optional()and.Else()mark exactly where a missing or malformed region is acceptable; nothing is silently lenient everywhere. - Content anchors survive layout drift.
.On,.Below,.RightOf,Caption, and.Untilfind their place by what a row or column says, not by a hard-coded offset that breaks the next time someone inserts a row.
Large files
SpreadsheetSpace.Create reads a sheet whole, which is the simple default and the right
choice for anything that fits comfortably in memory. For a file too big for that, or the
same declaration applied to many files in sequence, Workbook reads the sheet as one
forward pass, holding only the rows the declaration's open shapes may still read:
var report = VerticalFlow(v => ...); // one declaration, reused
foreach (var path in monthlyCloseOfFunds)
{
using var book = Workbook.Open(path);
Publish(report.Map(book.Sheet("Detail"))); // bounded memory per iteration
}
Same projections, same results — the two paths differ only in the shape of their cost. A
table read one band per row holds a handful of rows however tall the sheet; a shape that
reads its extent whole holds its extent, which CostReport.Of(report) will tell you before
any file is opened and book.Statistics("Detail") will confirm after. Projections are
immutable and workbooks are independent, so
Parallel.ForEach(monthlyCloseOfFunds, path => { using var book = ...; }) needs nothing
added. The full guide, including the cap and the statistics to act on: docs/streaming.md.
Learn more
docs/vocabulary.md— the full operator survey, grouped by role.docs/streaming.md— theWorkbookguide: when to reach for it, the lifecycle rules, the cap, and the statistics vocabulary.docs/design/— the one open design draft (the static boundary); the records of finished arcs live in git history.linqpad/— worked examples against the workbooks inexamples/, including the report above (linqpad/investor-irr.linq).
License
MIT — see LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. 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. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 is compatible. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Unrect (>= 0.3.0-alpha.11)
- Unrect.Spreadsheets (>= 0.3.0-alpha.11)
-
.NETStandard 2.1
- Unrect (>= 0.3.0-alpha.11)
- Unrect.Spreadsheets (>= 0.3.0-alpha.11)
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.0-alpha.11 | 34 | 9/27/2026 |
| 0.3.0-alpha.10 | 36 | 9/26/2026 |
| 0.3.0-alpha.9 | 52 | 9/21/2026 |
| 0.3.0-alpha.8 | 58 | 9/20/2026 |
| 0.3.0-alpha.7 | 49 | 9/20/2026 |
| 0.3.0-alpha.6 | 53 | 9/19/2026 |
| 0.3.0-alpha.5 | 51 | 9/19/2026 |
| 0.3.0-alpha.4 | 53 | 9/19/2026 |
| 0.3.0-alpha.3 | 57 | 9/19/2026 |