RDLL.excelcannon 0.5.2

dotnet tool install --global RDLL.excelcannon --version 0.5.2
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local RDLL.excelcannon --version 0.5.2
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=RDLL.excelcannon&version=0.5.2
                    
nuke :add-package RDLL.excelcannon --version 0.5.2
                    

ExcelCannon

Fire an Excel workbook in — get a canonical, deterministic, LLM-ready model out. Then diff, score, and lint filled templates against it.

ExcelCannon reads .xlsx/.xlsm and produces one canonical form: JSON for machines (diffable, byte-identical on every run) and a compact text render for LLM inference (~7× fewer tokens than the JSON). It captures everything that makes the workbook function — formulas, styles, merged headers, tables, dropdowns/validations, conditional formatting, named ranges, frozen panes, VBA macro source — plus derived semantics: detected headers, cell roles, a first-class input surface (every fillable field with its label and constraint), and dependency summaries. Three modes decide how much of the value layer comes with it: template (structure and literals), data (everything) and schema (structure and column schemas, with every data-region value suppressed).

Website: https://excelcannon.wee-app.com · Release notes: releases.html

Install

dotnet tool install -g RDLL.excelcannon        # the CLI
dotnet add package RDLL.excelcannon.sdk        # the same engine, in-process from .NET

Ships as a .NET tool wrapping a native Rust binary, and as a library package that P/Invokes the same engine. Both carry the engine for win-x64 today; linux-x64 and macOS natives are on the roadmap — see Supported platforms.

Commands

Command What it does
extract <SRC> Workbook → canonical JSON model (--mode template\|data\|schema, --data = --mode data; --header "Sheet!A7", --distinct-values[=MAX], --out, --text)
render <SRC> Extracted JSON → compact text (--mode/--data) — byte-identical to extract --text. A --mode that disagrees with the model's own kind is stated on stderr; what was stated still decides, so --mode template over a data model withholds the cached results it holds, and --mode data over a model that holds none cannot put them back
diff <LEFT> <RIGHT> Score two workbooks/models: category percentages + input-surface completion (--json, --epsilon, --weights, --top-n, --sheet, --exclude-sheet, --region)
lint <SRC> Check a workbook/model against the lint rules (--json, --strict, --top-n, --rule, --exclude-rule, --list-rules, --sheet, --exclude-sheet, --region)

diff and lint accept either raw workbooks (.xlsx/.xlsm) or previously extracted .json models on any argument — extension-sniffed.

Scoping. --sheet NAME (repeatable) compares or checks only the named sheets; --exclude-sheet NAME skips them instead; --region "SHEET!RANGE" narrows a sheet to one block. A scoped percentage is always labelled (scoped) and the report states what it covered, so it can never be read as a whole-workbook number.

A sheet named in a lint scope that the workbook does not have is reported as a scope-missing-sheet error — the run fails and the rest of the report still arrives, because a sheet the generator was supposed to produce and did not is the defect worth hunting. In a diff a sheet present on one side is likewise a difference; a sheet present on neither is refused before anything is scored (error[16] InvalidScope, exit 2, no report and no --json file), because there is nothing to compare and a 0.00% would read as a measurement rather than a rejection.

Choosing the rules. --rule ID (repeatable) runs only those rules; --exclude-rule ID skips them; --list-rules prints the ids, what each one needs to fire, and what it looks for. An id that is not a rule is refused rather than ignored, so a typo in a gate cannot pass as a clean workbook. A rule-filtered report is labelled (rule-filtered) and states rules in its JSON, exactly as a scoped one states scope.

Every <SRC> is a path or an https:// URL — see scenario 8. Global flags for the URL case: --timeout <SECS> (60), --max-download <MB> (200), --allow-insecure-http.

Exit codes: 0 clean/identical · 1 differences or lint errors found · 2 tool failure. CI-friendly.

Scenarios

1. Feed a workbook to an LLM

excelcannon extract report.xlsx --data --text report.txt

report.txt is the token-efficient render: per-sheet grids, detected headers, input fields with labels and constraints, formula fills stated once, dependency summaries. Prompt with the text; keep the JSON (--out model.json) as the exact machine record.

A real render, in full — this is the whole of one of the test fixtures:

# excelcannon v5 | kind=data | sheets=2
= INPUT SURFACE 5 fields | 2 blank | 3 filled
-- name Regions = Lists!$A$1:$A$4
== SHEET Form | A1:D6 | frozen r1
-- grid A1:D6
r1: Region | Q1 | Q2 | Total
r2: North | 100 | 120 | 220
r3: South | 90 | 80 | 170
r4: East | 70 | 60 | 130
r5: West | 50 | 40 | 90
r6: ~ | =SUBTOTAL(109,[Q1]) | =SUBTOTAL(109,[Q2]) | =SUBTOTAL(109,[Total])
-- fills
D2:D5 = SUM(RC[-2]:RC[-1])
-- table tblSales A1:D6 hdr totals [Region,Q1:sum,Q2:sum,Total:sum] style=TableStyleMedium2
-- validation F2:F5 list("Yes,No") blank-ok dropdown prompt="Approved?: Pick one" error="Not a choice: Choose Yes or No"
-- validation G2:G5 whole between(1,100) blank-ok
-- condfmt D2:D5 cellIs > 100 -> color#9C0006,fill#FFC7CE
-- headers
A1:D1 [table,type,frozen] A=Region B=Q1 C=Q2 D=Total
-- inputs
Region: A2:A5 filled
Q1: B2:B5 filled
Q2: C2:C5 filled
Approved?: F2:F5 [list("Yes,No")] blank
G2:G5 [whole between(1,100)] blank
-- deps
D2:D5 <- B2:C5
terminals: D2:D5 B6 C6 D6
== SHEET Lists | A1:A4 | hidden
-- cells
A1: North
A2: South
A3: East
A4: West

2. Understand a blank template before filling it

excelcannon extract template.xlsx --text template.txt

Template mode (no --data) keeps labels and formulas but drops cached formula results. The = INPUT SURFACE section lists every fillable field — address, nearest label, validation constraint, number format — which is exactly what a filler (human or model) needs to complete the form.

3. Benchmark how well a template was filled

excelcannon diff expected.xlsx produced.xlsx --json report.json

Markdown summary to stdout, machine report to report.json:

**Overall match: 96.40%**

> weights: value 0.40, formula 0.30, format 0.10, structure 0.20

| category | match | matched | union |
| --- | --- | --- | --- |
| value | 90.91% | 30 | 33 |

**Input surface: 32 fields | A 100.00% | B 90.63% | both 90.63%**

| field | range | A | B |
| --- | --- | --- | --- |
| Revaluation basis | Sales!D45:D60 | filled | blank |

### Sales - value (1)

- `D47` [Revaluation basis D45:D60]: value differs (`S52a CPI` -> `Statutory S148`) - not a permitted value

Category percentages localize what kind of difference; the input-surface score tells you how much of the form was actually completed, and the table under it names the fields somebody left incomplete. Findings carry the field they fall in — field_label / field_range in the JSON — and a value the cell's list validation does not offer is flagged. Exit 1 on any difference makes it gate-able in CI or a benchmark loop.

Scope it when only part of the workbook was under test:

excelcannon diff expected.xlsx produced.xlsx \
  --sheet "2b. REV AND ESC" --sheet "3a. DEFERRED BENEFITS" \
  --region "4. NOTES!B2:Q120"
**Overall match (scoped): 97.50%**

> scope: 2 sheets ("2b. REV AND ESC", "3a. DEFERRED BENEFITS") + 1 region ("4. NOTES!B2:Q120"); 9 of 12 sheets not scored

> weights: value 0.40, formula 0.30, format 0.10, structure 0.20

Weighting the headline. overall_pct is a weighted mean of the four category percentages, renormalized over the categories the two workbooks actually exercised. The defaults are value=0.4, formula=0.3, structure=0.2, format=0.1, and --weights changes them:

excelcannon diff expected.xlsx produced.xlsx --weights "value=0.5,structure=0.4,formula=0.1,format=0"

A category the flag does not name keeps its default, so --weights format=0 is the whole of "stop letting cell styling drag the number down" — two consultants filling the same template produce different fills and borders and neither is wrong. The weights are ratios, so 1,1,1,1 and 0.25,0.25,0.25,0.25 are the same request; a name that is not one of the four, a negative weight, and four zeros are all refused before either file is read. Every report states the weighting it was scored with — summary.weights in the JSON, the > weights: line in the markdown — so two differently-weighted runs cannot be compared by accident. The SDK sets the same four numbers as DiffOptions.Weights.

4. Catch template-fill defects

excelcannon lint filled.xlsx --strict

Rules: broken references (#REF!), values violating list validations, formulas overwritten with literals, cached error values (#DIV/0!…), input fields left blank. --strict fails on warnings too.

A run of one defect down a column is one finding over the range — P29:P50 hold a cached error value: #N/A (22 cells) — not one per cell, and the report opens with a per-rule count table. --top-n N caps how many findings each rule lists in the human summary, so one flooding rule cannot push the others off the end; the JSON always carries them all.

Gating a generated workbook. Fail the build on the two rules that mean the fill is wrong, and tolerate the rest:

excelcannon lint filled.xlsx --rule broken-refs --rule validation-violations --strict

Half the rules need data the extraction mode may not carry — a schema-mode model has no cell values at all. Those rules are now named rather than silently silent: the report raises one rules-not-evaluated notice and lists the ids under skipped_rules, so a strict gate fails instead of passing a model nothing could be checked against. excelcannon lint --list-rules says which rules need what.

5. Compare against a stored baseline

excelcannon extract golden.xlsx --data --out golden.json   # once, commit the JSON
excelcannon diff golden.json produced.xlsx                  # later runs, no re-extract

Extraction is deterministic — the same workbook always yields byte-identical JSON — so committed models are stable baselines.

6. Share the structure without sharing the data

excelcannon extract personnel.xlsx --mode schema --text schema.txt

Schema mode keeps the headers and describes the rows instead of reproducing them. It publishes text only for an affirmative reason — a column name of a region it actually described, or a caption that passes a set of conservative checks — so a cell it cannot account for is withheld rather than printed. Each column comes back as a type, a format, a constraint, the R1C1 it computes itself with, and how many rows were filled:

The whole of a six-row personnel workbook, schema mode, verbatim — twenty-two planted names, addresses, phone numbers and salaries, none of which appear:

# excelcannon v5 | kind=schema | sheets=2
= INPUT SURFACE 5 fields | 5 filled
-- name Approvers = Directory!$A$1:$A$3
== SHEET Roster | A1:F6 | frozen r1
-- cells
F2: =E2*0.1 {fmt:#,##0.00}
-- fills
F2:F6 = RC[-1]*0.1 {fmt:#,##0.00}
-- validation D2:D6 list("Alpha,Beta") blank-ok dropdown
-- headers
A1:F1 [style,frozen] A=Name B=Email C=Phone D=Team E=Salary F=Bonus
-- schema A1:F1 -> A2:F6
A=Name (string) 5/5
B=Email (string) 5/5
C=Phone (string) 5/5
D=Team (string) [list("Alpha,Beta")] 5/5
E=Salary (number) {#,##0.00} 5/5
F=Bonus (number) {#,##0.00} =RC[-1]*0.1 5/5
-- inputs
Name: A2:A6 filled
Email: B2:B6 filled
Phone: C2:C6 filled
Team: D2:D6 [list("Alpha,Beta")] filled
Salary: E2:E6 {#,##0.00} filled
-- deps
F2:F6 <- E2:E6
terminals: F2:F6
== SHEET Directory | A1:A3 | hidden
-- suppressed 3 cells
A1:A3 hidden 3 string

Use it to hand a workbook's shape to an LLM, a schema reviewer or a ticket when the contents are personal or confidential. A units/notes row under the header is kept as each column's description; a title alone at the top of a sheet is kept as a caption. Hidden sheets are withheld whole, and anything no schema accounts for is counted rather than dropped in silence — every populated cell is described, published, or stubbed here, with no fourth outcome:

-- suppressed 9854 cells
C5:C705 undescribed 701 string
A1:D12572 hidden 50288 string

On the corpus that is the difference between a schema render of eiopa-solvency2-dpm-annotated-templates at 6.35 MB and one at 562 KB.

A header row does not have to be the first row of the sheet. A frozen pane or an autofilter is the author saying where their table starts, so both are read as anchors — from the sheet itself, never from the stale copies a saved custom view keeps — and the rows just above the boundary are searched — which is what finds the header under a title block, under a band of summary figures, or across a blank row from its own data. An anchor alone is never enough: a style or a type break has to agree, or the rows stay withheld and the sheet says so (diagnostic 1017). Where the sheet gives nothing to go on, say it yourself:

excelcannon extract deferred.xlsx --mode schema --header "4a. DEFERRED DATA!A7"

A dropdown that draws its answers from a hidden lookup sheet states them here as well, so a reader of the schema knows what the column actually allows without being handed the lookup sheet. A source the engine will not read out — dynamic, two-dimensional, or full of formulas — publishes nothing and says so:

-- validation B2:B10 list(REV_BASES_Valid = None,S52a CPI,S52a RPI,…(+8 more)) blank-ok dropdown
-- validation F2:F10 list(OFFSET(DROPDOWNS!$E$3,0,0,4,1)) blank-ok dropdown

Schema mode also takes back a source it could read where reading it would restate what the same model just suppressed: a range landing inside a described region's body is that region's rows rather than a vocabulary, and a hidden sheet does not publish a dropdown of its own contents. Data and template modes suppress nothing and keep both.

Schema mode publishes counts, not values. The one exception is opt-in, and off unless you ask for it by name — the vocabulary of a low-cardinality column:

excelcannon extract members.xlsx --mode schema --distinct-values --text schema.txt
B=Plan section (string) values(Bayer|Sterling|Ex-DuPont|Non-Bayer) 24/24
C=Gender (string) values(M|F) 24/24
E=Email (string) values(withheld:identifier_shaped) 24/24

With no number the threshold is 12 distinct values a column; =30 raises it. Clearing the threshold is not enough: a set is withheld anyway when the column holds numbers, dates or formulas, when its values repeat too little to read as a vocabulary rather than as records, when any single one of them was written only once — a category label is written many times, a name is written once — or when any one of them looks like an email, a postcode, a national-insurance number or another identifier. It is withheld whole rather than in part. Without the flag the model is byte-identical to one from before the option existed.

It reduces exposure; it is not a certified anonymizer. A header somebody wrote a person's name into is still a header. What changed in BL-010 is the direction of failure: a region the detector cannot read is now withheld rather than published, so a miss costs information — stated in the -- suppressed block — instead of costing privacy. See the limitation section in docs/semantics.md.

7. Call it from .NET, in-process

dotnet add package RDLL.excelcannon.sdk

Same four verbs, no child process — as a static class or as an injected IExcelCannon. The package carries the native engine for win-x64 and P/Invokes it; on any other platform the first call throws PlatformNotSupportedException naming the gap. Linux and macOS natives are planned — the packaging already accepts a staged multi-RID tree, so the missing piece is building and smoke-testing the other binaries, not shipping them. See Supported platforms. The canonical JSON stays the contract, so nothing here duplicates the model as C# types.

using RDLL.ExcelCannon.Sdk;

// Workbook -> canonical model. ModelResult.Json is the same document the CLI writes.
var schema = ExcelCannon.Extract("personnel.xlsx", ExtractMode.Schema);
Console.WriteLine($"{schema.Kind} v{schema.SchemaVersion}: {string.Join(", ", schema.SheetNames)}");

// The token-efficient rendering, for a prompt.
Console.WriteLine(ExcelCannon.Render(schema.Json, ExtractMode.Schema));

// Score a filled template against the gold copy.
var expected = ExcelCannon.Extract("golden.xlsx", ExtractMode.Data);
var produced = ExcelCannon.Extract("filled.xlsx", ExtractMode.Data);
var diff = ExcelCannon.Diff(expected.Json, produced.Json, new DiffOptions { Epsilon = 1e-9 });
Console.WriteLine($"{diff.OverallPct:F1}% match, {diff.FindingCount} differences (exit {diff.ExitCode})");

// Lint what was produced.
var lint = ExcelCannon.Lint(produced.Json);
Console.WriteLine($"{lint.FindingCount} findings, {lint.ErrorCount} errors");

ExtractOptions carries everything extract has on the command line — a stated header row, schema mode's distinct-values threshold, and the three limits that apply to an https:// source. Header rows are spelled exactly as --header spells them, quoted sheet names included, because both surfaces parse them with the same engine code:

var model = ExcelCannon.Extract("insurer-return.xlsx", new ExtractOptions
{
    Mode = ExtractMode.Schema,
    Headers = new[] { "'4a. DEFERRED DATA'!A7" },   // same spelling as --header
    DistinctValues = 12,                           // Schema mode only; null or 0 is off
    FetchTimeout = TimeSpan.FromSeconds(120),      // URL sources only
    MaxDownloadMegabytes = 500,
});

An option the engine cannot honour — a header that is not SHEET!CELL, or DistinctValues outside schema mode — is refused as ExcelCannonErrorCategory.InvalidExtractOptions (code 17) rather than ignored; nothing is read. An override that is well-formed but names a sheet the workbook does not have is not a failure: the extraction succeeds and says so as diagnostic 1018.

In a host with a container, register it once and inject IExcelCannon — the same four verbs behind a seam a test can replace:

using Microsoft.Extensions.DependencyInjection;   // AddExcelCannon lives here
using RDLL.ExcelCannon.Sdk;

builder.Services.AddExcelCannon(options =>
{
    options.ExtractMode = ExtractMode.Schema;   // what Extract/Render use when no mode is passed
    options.DiffEpsilon = 1e-9;
    options.LintStrict = true;                  // what LintExitCode fails on
});

public sealed class Auditor(IExcelCannon excel)
{
    public string Summarize(string path) => excel.Render(excel.Extract(path).Json);
}

AddExcelCannon is idempotent and TryAdd-based — your own registration of IExcelCannon wins — and registers a singleton, because the engine is stateless and its error channel is per-thread. Configuration is validated at registration, not at the first call. new ExcelCannonClient() gives the same instance surface without a container, and an unconfigured one behaves exactly like the static class.

netstandard2.0 and net8.0; failures come back as ExcelCannonException, which carries a typed Category, a stable ErrorCode, the Source/Part/ Sheet/Cell the engine determined, and the IsRetryable/IsUserFixable flags — so an app can retry a locked file, tell the user to remove a workbook's password, or report a bug, without parsing English:

catch (ExcelCannonException error) when (error.Category == ExcelCannonErrorCategory.EncryptedWorkbook)
{
    Console.Error.WriteLine($"{error.Source} is password-protected; remove the password and re-save.");
}

The code table, the append-only stability covenant and the reaction guidance are in docs/errors.md, which also documents the C ABI's excelcannon_last_error_json() and the CLI's --error-format json. Panics are caught at the boundary rather than unwinding into the CLR.

Extraction that succeeds can still have guessed at things — a workbook with no styles part, rows placed by document order because a producer omitted r, a chart recorded as present without being read. Those arrive as diagnostics: workbook.diagnostics in the model, model.Diagnostics and model.HasWarnings on ModelResult, and one summary line on stderr from excelcannon extract (exit code unchanged, stdout still pure JSON). They are not errors, they have their own append-only code range starting at 1000, and diff ignores them — same document, second half:

ModelResult model = ExcelCannon.Extract(path, ExtractMode.Data);
if (model.HasWarnings)
{
    // fidelity was reduced somewhere; model.Diagnostics says where
}
``` The only package
dependency on `net8.0` is
`Microsoft.Extensions.DependencyInjection.Abstractions` (the `AddExcelCannon`
seam); `netstandard2.0` additionally brings `System.Text.Json`. Packaging
details: [`docs/packaging.md`](docs/packaging.md).

#### Supported platforms

| Package | Native engine shipped for |
|---|---|
| `RDLL.excelcannon` (CLI tool) | `win-x64` |
| `RDLL.excelcannon.sdk` (library) | `win-x64` |

**Restore is not the same as run.** The SDK assembly targets `netstandard2.0`
and `net8.0`, so it restores and compiles on any platform your .NET SDK
supports. What is platform-specific is the native engine underneath it: on a
platform this package carries no binary for, the *first call* throws

PlatformNotSupportedException: RDLL.excelcannon.sdk ships its native engine for win-x64 only in this version; this process is <OS>/<arch>. See the package README, 'Supported platforms'.


— named and immediate, rather than a bare "Unable to load shared library". It is
deliberately not an `ExcelCannonException`: no engine ran, so no engine error
category would be true of it.

`linux-x64` is the next one planned; the packaging path for it already exists
(`package-sdk.ps1 -NativeBinaryRoot <staged tree> -RuntimeIdentifiers …`) and
needs no script changes. What is missing is a build host that can produce the
`.so` *and* run a consumer against it, because this repo's standard is that
packaging proves the packed bits by executing them — shipping a native nobody
has run is worse than shipping none. macOS needs an Apple build host and is
roadmap only. See [`docs/backlog/BL-015-linux-x64-native.md`](docs/backlog/BL-015-linux-x64-native.md).

Every release states the RID list it shipped, in its notes.

### 8. Read straight from a presigned blob or S3 URL

The workbooks are in Azure Blob or S3 and what you hold is a presigned link.
Hand it over as-is — the query string is what authenticates it, so a plain GET
is all that is needed and **no cloud SDK is involved**:

excelcannon extract "https://acct.blob.core.windows.net/reports/q3.xlsx?sv=2024-11-04&sig=..." --mode data --out q3.json excelcannon diff "https://acct.blob.core.windows.net/gold/template.xlsx?sig=..." q3.json --json report.json excelcannon lint "https://bucket.s3.amazonaws.com/filled/q3.xlsx?X-Amz-Signature=..." --strict


```csharp
var model = ExcelCannon.Extract(presignedUrl, ExtractMode.Data);

The signature is treated as a credential. It is never printed, logged, or written into anything: not the model, not an error message, not a diff or lint report. A URL is named as scheme://host/path everywhere and nothing else, and source.filename records the last path segment — q3.xlsx — exactly as it records a file name for a path. A user:password@ authority is dropped the same way. The model is byte-identical to one extracted from the same workbook on disk apart from source.origin, which reads "url"; the SHA-256 covers the bytes as delivered, so baseline diffs keep working across the two.

Limits and behaviour, all of them deliberate:

Default Change it with
Transport https:// only — an unencrypted request would hand the signature to the network --allow-insecure-http
Timeout, whole fetch 60 s --timeout <SECS>
Size cap 200 MB, enforced while reading so a lying Content-Length cannot balloon memory --max-download <MB>
Redirects followed, bounded at 10
Retries one, on 5xx / timeout / failed connection only

Failures map onto the existing error taxonomy with no new codes: 404/410NotFound, 401/403AccessDenied (retry by re-issuing the URL), size cap → LimitExceeded, everything the network can do → Io, refused http://UnsupportedFeature. The full table is in docs/errors.md.

Not in scope, by design: Azure/AWS/GCS SDK auth flows (account keys, IAM, OAuth), writing back to a URL, and caching downloads on disk.

Notes

  • Determinism: fixed ordering everywhere, no timestamps; render reproduces extract --text byte-for-byte from the JSON.
  • Schema: canonical JSON carries schema_version (currently 5); breaking model changes bump it and the minor version. diagnostics arrived without a bump — absent unless extraction had something to report, so no existing field changed meaning. source.origin arrived the same way: absent for a file, "url" for a download.
  • URL sources: any source argument may be an https:// URL, presigned or public. The HTTP client is ureq + rustls with bundled roots — no tokio, no cloud SDK — and it costs ~3.4 MB of binary; --no-default-features builds it out.
  • Fidelity: diagnostics says what extraction guessed at or did not model. 12 of the 89 corpus workbooks report anything at all; one reports a warning.
  • Battle-tested against an 89-workbook corpus (1631 sheets, 7.0M cells) including EIOPA Solvency II QRT sets (574 sheets), EBA COREP/Pillar 3 layouts, and real filled bank/insurer disclosures. 767 tests in the workspace, plus 4 --ignored corpus tests run per release.

Development

Rust workspace: excelcannon-core (library), excelcannon-cli, excelcannon-ffi (C ABI cdylib behind the .NET SDK), excelcannon-stress (corpus harness). Managed side: pack/ (the tool's shim) and sdk/RDLL.ExcelCannon.Sdk/ (the library package). See docs/architecture.md, docs/errors.md (the error-code contract), docs/packaging.md, docs/releases.md (release process), docs/backlog/ (work tracking). Build: cargo test --workspace (on the dev box: $env:RUSTUP_TOOLCHAIN='stable-x86_64-pc-windows-gnu').

MIT licensed.

Product Compatible and additional computed target framework versions.
.NET 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

Version Downloads Last Updated
0.5.2 0 8/10/2026
0.5.1 0 8/10/2026
0.5.0 0 8/10/2026
0.4.0 42 8/9/2026
0.3.0 38 8/9/2026
0.2.0 49 8/9/2026
0.1.0 44 8/8/2026
0.0.1 42 8/8/2026