Proof-of-concept for the Excel-on-OneDrive ETL pipeline.
Extract : Graph download bytes -> byte[]
Transform : Proxy over interface -> typed ChangeRecord journal (cells + table ops)
Load actor : journal -> Graph workbook session -> PATCH cells / append+update+delete table rows
Verify : Graph download -> formulas recalculated, table rows landed
| Project | Purpose |
|---|---|
ExcelBridgeNext.Abstractions |
[Cell], [Table], [Column] attributes; polymorphic ChangeRecord (CellWrite/TableAppend/TableRowUpdate/TableDelete); ITableList<T>; Actions (JSON-serializable). |
ExcelBridgeNext.Core |
WorkbookFactory + DispatchProxy stack: WorkbookProxy, TableListProxy, AttachedRowProxy, PendingRowProxy. In-memory ClosedXML workbook + journal. |
ExcelBridgeNext.Graph |
MSAL auth, HttpClient wrapper for Graph workbook + table endpoints, GraphLoadActor with type dispatch. |
ExcelBridgeNext.Poc |
Console runner that exercises the round-trip end-to-end. |
public interface IHolding
{
[Column("Ticker")] string Ticker { get; set; }
[Column("BidPrice")] double BidPrice { get; set; }
[Column("Qty")] int Quantity { get; set; }
}
public interface ITestBook
{
[Cell] double Input { get; set; }
[Cell] double Double { get; } // formula =Input*2
[Cell("Sheet1!A5")] string? Label { get; set; }
[Table("Holdings")] ITableList<IHolding> Holdings { get; }
}
// usage
var actions = new Actions();
var wb = WorkbookFactory.Create<ITestBook>(bytes, actions);
wb.Input = 5; // -> CellWrite
wb.Holdings[0].BidPrice = 30.5; // -> TableRowUpdate (positional)
wb.Holdings.Add(h => // -> TableAppend
{
h.Ticker = "NVDA";
h.BidPrice = 1100;
h.Quantity = 10;
});
wb.Holdings[^1].Quantity = 11; // folds into the pending TableAppend
wb.Holdings.RemoveAt(2); // -> TableDelete (tables only)
await new GraphLoadActor(graph).ApplyAsync(item, actions);You need three things: an Azure AD app registration, a test workbook on OneDrive, and a populated appsettings.Local.json.
This is the work/school path. App-only auth has no user context; the app authenticates as itself with a client secret. Requires tenant admin to grant consent.
- https://entra.microsoft.com -> Applications -> App registrations -> New registration.
- Name it (e.g.
ExcelBridgeNext POC). Supported account types: "Accounts in this organizational directory only". - Skip the redirect URI (not used for client-credentials).
- Register, then note Application (client) ID and Directory (tenant) ID.
- API permissions -> Add a permission -> Microsoft Graph -> Application permissions:
Files.ReadWrite.All(Remove any previously-added Delegated permissions if you want; not required.)
- Click Grant admin consent for [tenant] and confirm. The Status column should show a green check. This requires a tenant admin — if you aren't one, ask an admin to do this step.
- Certificates & secrets -> Client secrets -> New client secret:
- Description: e.g.
POC - Expires: e.g. 6 months
- Click Add
- Immediately copy the
Valuecolumn (not the Secret ID). It is shown once and never again.
- Description: e.g.
Production note:
Files.ReadWrite.All(Application) grants access to every user's files in the tenant. For production switch toSites.Selected+ per-site grants. Fine for the POC.
Create ebn-test.xlsx in your OneDrive. Add:
Scalar named ranges:
- Pick a cell (say
B2), type0. Select it; in the Name Box (top-left) typeInput-> Enter. - Pick another cell (say
B3), type=Input*2. Name itDouble.
A Holdings Excel Table:
- Somewhere on
Sheet1(say starting atD1), enter the headers in a row:Ticker | BidPrice | Qty - Add at least one data row, e.g.:
AAPL | 175 | 5 - Select the whole block including header (
D1:F2), press Ctrl+T (Cmd+T on Mac), confirm My table has headers = Yes. - With the table selected, in the Table Design tab set Table Name =
Holdings. (Default would beTable1.) - Save.
The minimum data row ensures Holdings.DataRange is non-null on first load. The POC handles an empty table too, but having a row to update makes the verification meaningful.
App-only auth has no /me context, so you must supply both DriveId and ItemId explicitly.
Sign into Graph Explorer (https://developer.microsoft.com/graph/graph-explorer) as the owner of the workbook (your normal user, not the app). Then:
GET https://graph.microsoft.com/v1.0/me/drive
Copy the id field from the response -> that's your DriveId.
GET https://graph.microsoft.com/v1.0/me/drive/root:/ebn-test.xlsx
Copy the id field -> that's your ItemId. (If the file is in a subfolder, adjust the path, e.g. /me/drive/root:/Documents/ebn-test.xlsx.)
For SharePoint: replace /me/drive with /sites/{site-id}/drive to find that library's drive id.
cp src/ExcelBridgeNext.Poc/appsettings.Local.json.sample src/ExcelBridgeNext.Poc/appsettings.Local.jsonEdit it:
{
"ExcelBridge": {
"ClientId": "<client-id-from-step-1>",
"TenantId": "<tenant-guid-from-step-1>",
"ClientSecret": "<the-secret-Value-you-copied>",
"DriveId": "<drive-id-from-step-3>",
"ItemId": "<item-id-from-step-3>"
}
}ClientSecret: the Value column from Certificates & secrets, not the Secret ID.DriveIdis required for app-only auth (no/mecontext).
appsettings.Local.json is gitignored.
dotnet run --project src/ExcelBridgeNext.PocNo browser; the app authenticates itself via client credentials and acquires a token directly from Azure AD.
Expected end:
PASS - formula recalculated AND Holdings append (with pending-row update) landed correctly.
- Interface + attribute + DispatchProxy API shape (
wb.Input = 5,wb.Holdings.Add(...)). - Setter on a scalar property emits
CellWrite. Setter on an existing table row emitsTableRowUpdate.AddemitsTableAppend. - Pending-row folding: mutations on a just-appended row update the pending
TableAppend.Valuesin place, no orphanTableRowUpdatereferencing an unstable index. - Named-range convention +
[Cell]/[Table]/[Column]overrides. Actionsround-trips through STJ with$kindpolymorphic discriminator (cross-actor transport ready).- Excel Online recalculates formulas server-side when writes land via a workbook session with
persistChanges=true. - Graph's
tables/{name}/rows/addendpoint resolves server-side row positions (concurrent appends safe by design).
- Tables only. Range-backed lists throw at
EnsureTable. v2.1 will add a range fallback (single-writer-only). - No per-row PATCH coalescing. Multiple updates to the same row produce multiple Graph calls; coalescing to one range PATCH is a v2.1 optimization.
- No retry/backoff on Graph 429s.
- No eTag/conflict detection on the workbook itself (table appends are race-free server-side; cell writes and table row updates are not).
- Client secret in
appsettings.Local.json. Fine for dev; for prod move to a managed-identity-or-Key-Vault-resolved configuration, or switch to certificate-based auth. Files.ReadWrite.Allis broader than ideal. For production switch toSites.Selected.- No tests. A
dotnet testproject should land before layering further features.
- Range fallback for non-table lists (v2.1).
- Per-row PATCH coalescing for multi-column updates on the same row.
- Certificate-based auth instead of client secret; Key Vault / managed identity for prod.
- Retry + throttling handler (HttpMessageHandler chain).
- eTag on
GET /contentandIf-Matchon session close. - Integration into an ETL Transform/Load actor pipeline.