JackcessDotNet 3.1.0

dotnet add package JackcessDotNet --version 3.1.0
                    
NuGet\Install-Package JackcessDotNet -Version 3.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="JackcessDotNet" Version="3.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="JackcessDotNet" Version="3.1.0" />
                    
Directory.Packages.props
<PackageReference Include="JackcessDotNet" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add JackcessDotNet --version 3.1.0
                    
#r "nuget: JackcessDotNet, 3.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package JackcessDotNet@3.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=JackcessDotNet&version=3.1.0
                    
Install as a Cake Addin
#tool nuget:?package=JackcessDotNet&version=3.1.0
                    
Install as a Cake Tool

JackcessDotNet

NuGet Downloads GitHub Repo License

Pure .NET 10 library for reading and writing Microsoft Access (.mdb / .accdb) files. No ODBC, no ACE drivers, no native dependencies, no NuGet dependencies — runs anywhere .NET 10 runs (Windows, Linux, macOS, containers).

2.0.0 targets net10.0. 1.2.x is the last net8.0 release; stay on it if you cannot move off .NET 8.

This is a C# port of the spannm/jackcess Java project (a maintained fork of the original Jackcess by James Ahlborn). The upstream Java project remains the source of truth for the Jet file format; this port tracks its behaviour and reuses its test corpus for verification.

Install

dotnet add package JackcessDotNet

Quick start

using JackcessDotNet;

// Open an existing .mdb / .accdb (auto-detects version)
using var db = Database.Open("Northwind.mdb");

foreach (var name in db.ListTables())
    Console.WriteLine(name);

var customers = db.GetTable("Customers");
foreach (var row in customers.NewCursor())
    Console.WriteLine($"{row["CustomerID"]}  {row["CompanyName"]}");

// Create a new database, define a schema, insert rows
using var fresh = Database.Create("MyData.mdb", JetVersion.Jet4);
var people = fresh.CreateTable("People", new[]
{
    new ColumnBuilder("Id",   DataType.Long).Build(),
    new ColumnBuilder("Name", DataType.Text).MaxLength(50).Build(),
}, primaryKey: "Id");
people.Insert(new Row { ["Id"] = 1, ["Name"] = "Alice" });

Opening password-protected files

// Jet RC4 (.mdb, Access 97 / 2000–2003)
using var db = Database.Open("Confidential.mdb", "passw0rd");

// .accdb encryption — Agile (Office 2010+), ECMA Standard (Office 2007),
// RC4 CryptoAPI (Office 2002–2003), or Non-Standard AES — all auto-detected
// from the EncryptionInfo header.
using var db = Database.Open("Confidential.accdb", "passw0rd");

// Inspect which scheme the codec picked (useful when triaging an unfamiliar file).
byte[] page0 = File.ReadAllBytes("Confidential.accdb").Take(4096).ToArray();
var codec = OfficeCryptCodecHandler.FromDbHeader(page0, "passw0rd");
Console.WriteLine(codec?.Scheme);   // → "Agile Encryption (Office 2010+)"

Wrong passwords throw UnauthorizedAccessException before any data page is parsed — no cryptic "page X corrupt" errors.

Composite (multi-column) primary keys

db.CreateTable("Orders", new[]
{
    new ColumnBuilder("CustomerId", DataType.Long).Build(),
    new ColumnBuilder("OrderId",    DataType.Long).Build(),
    new ColumnBuilder("Total",      DataType.Money).Build(),
}, primaryKeyColumns: new[] { "CustomerId", "OrderId" });

Up to 10 columns per composite key (Jet's hard limit). The single-column primaryKey: overload still works for the common case.

Foreign-key enforcement on insert (opt-in)

using var db = Database.Open("Northwind.mdb");
db.EnforceForeignKeys = true;                  // default: false

// MSysRelationships drives validation. Any Insert whose FK column doesn't
// match an existing parent row throws InvalidOperationException.
orders.Insert(new Row { ["CustomerID"] = "INVALID", ... });
//                                                    ^ throws "Foreign-key violation in
//                                                              relationship 'CustomersOrders'..."

Null FK values are allowed (SQL semantics). Restrict-only — no cascade.

Importing from DataTable, DataSet, or IEnumerable<T>

Skip the schema boilerplate — the importer infers columns from the source type:

using var db = DatabaseImporter.CreateFromDataTable("out.mdb", myDataTable);

// or add into an existing database
db.ImportTable(otherDataTable, tableName: "Extra", primaryKey: "Id");
db.ImportTables(myDataSet);

db.ImportTable<Customer>(customers, primaryKey: "Id");

// Append rows into a pre-existing table (instead of failing):
db.ImportTable(moreRows, options: new ImportOptions { AppendIfExists = true });

POCO mapping respects [Key], [Column("X")], [MaxLength], [NotMapped], and [DatabaseGenerated(Identity)]. Long strings (>255 chars) auto-promote to Memo; large byte arrays (>255 bytes) to OLE. Unmappable types can fall back to Memo via ImportOptions.FallbackUnmappableToString = true.

Exporting back

DataTable          dt    = db.ExportToDataTable("Customers");
DataSet            ds    = db.ExportToDataSet();
IEnumerable<Customer> cs = db.ExportToCollection<Customer>("Customers");

The IEnumerable<T> path is lazy via yield, so Take(n) stops early without materialising the whole table.

Status

Feature Status
Read Jet 3 (Access 97) / Jet 4 (Access 2000–2003)
Read ACE 12 / 14 / 16 / 17 (.accdb)
Create new .mdb files (Jet 4)
Create new .mdb files (Jet 3, Access 97) CreateTable throws — Jet 3 column headers are 18 bytes with a different layout
Create new .accdb files (ACE format) ⚠️ Produces a Jet 4 database, which Access opens⁹
Row CRUD + B-tree indexes (single-column PK)
Files and tables Microsoft Access can open ✅ Both paths verified against the ACE engine³
Appending into large existing files ✅ Inline usage-map window slides, then promotes to a reference map²
Maintaining every index of a table on insert ✅ Including leaf splits in trees Access wrote⁴
Keeping indexes correct on delete / update ✅ Entries moved or removed, counts adjusted
Creating secondary indexes Database.CreateIndex, single or composite, ascending or descending, backfilled⁵
Unique indexes enforced on insert ✅ Including primary keys; null keys exempt
Reclaiming space from deleted rows ✅ Whole emptied pages, and gaps within pages still in use
Table definitions spanning several pages ✅ Read and written
Reading complex columns (multi-value, attachments, memo history) Table.GetComplexValues
Writing complex columns ✅ Add, update and remove individual values
Writing rows into an existing .accdb ✅ ⁸
Writing a Memo / OLE value into an .accdb ⚠️ Completes and round-trips here; Access reads it empty⁸
Memo / OLE long values
PropertyMap & MSysRelationships
Password-protected .mdb (Jet RC4 codec)
Password-protected .accdb (Agile Encryption, Office 2010+) ✅ Read + write¹
Agile data-integrity hash (<dataIntegrity>) ⚠️ Implemented to spec, round-trip tested only⁷
Password-protected .accdb (ECMA Standard Encryption, Office 2007) ✅ Read + write
Password-protected .accdb (RC4 CryptoAPI, Office 2002–2003) ✅ Read + write
Password-protected .accdb (Non-Standard AES, compat mode 0) ✅ Read + write
Password-protected .accdb (Extensible Encryption) ❌ External CSP — non-portable
64-bit integers (BIGINT / "Large Number") ❌ Not a Jet 4 type at all; ACE's is unimplemented¹⁰
Multi-column primary keys
Foreign-key enforcement on insert (opt-in) ✅ Restrict-only
Queries (MSysQueries) ❌ Not yet

³ Two paths, both checked by reading the result back through Microsoft.ACE.OLEDB.12.0: creating a file and a table from scratch, and appending into a table an Access-authored file already contains. Before 2.2.0 the first was unreadable by Access and the second garbled Latin-1 text — see the 2.2.0 entry in CHANGELOG.md for the five on-disk details involved.

⁴ Access reads through indexes, so a row absent from one does not exist as far as Access is concerned even though a page scan still returns it. Every index of the table takes an entry, each index's entry count is kept current (Access answers COUNT(*) and MAX from an index), and a full leaf splits correctly — including in a tree Access wrote, which needs the new root recorded against the right index-data block rather than the right slot; the two are ordered independently. Pages Access prefix-compressed are expanded and re-emitted in full, and a full node splits under a new level, so trees grow past two levels — all three verified by querying the result through ACE. An insert is refused only when a key is too large for three to share a page, which is what splitting a node needs, and Jet's 255-byte key limit puts that out of reach.

db.CreateIndex("People", "ByName", "Name") — up to 10 columns, ascending by default or descending via IndexColumnSpec, spliced into the table's existing definition and then filled from the rows already stored, so it answers queries immediately. Access lists it (verified through ADOX) and uses it for seeks, ORDER BY, GROUP BY and MAX. Two caveats: the definition has to still fit on one page, and a definition that outgrows it continues on another. unique: true is enforced on insert.

² A table's data pages are tracked in a usage map whose inline bitmap addresses a fixed window — as little as 512 pages (~2 MB) in Access-authored files. The window slides to follow the table, and once the table outgrows one bitmap the map is promoted to reference-style (~2.2 GB of reach, past Access's own file limit). Before 2.0.0 an append past the window threw.

¹ Agile write doesn't recompute the DataIntegrity HMAC, so files modified through this library round-trip cleanly through Database.Open(path, password) but Office Access may flag a stale integrity hash. Matches the upstream jackcess-encrypt limitation.

Type round-trip cheat sheet

CLR type Jet column Notes
bool Boolean
byte Byte
short/ushort Int (16-bit) both come back as short
int/uint Long (32-bit) both come back as int
long/ulong Long (32-bit) truncated to 32 bits
float/double Float/Double
decimal Money 4-decimal precision; use Numeric for more
DateTime ShortDateTime Kind is dropped
DateTimeOffset ShortDateTime stored as UTC
Guid Guid
string ≤255 Text
string >255 Memo auto-promoted on MaxLength
byte[] ≤255 Binary
byte[] >255 Ole auto-promoted on MaxLength
Nullable<T> underlying T
enum underlying type round-trips back to the enum on read

Version history

See CHANGELOG.md — also shipped inside the .nupkg.

Credits

This project would not exist without the years of reverse-engineering work done by James Ahlborn (original Jackcess) and Markus Spann (maintained fork at spannm/jackcess). The Jet binary format is documented effectively only through their Java source.

License

Apache License 2.0 — same as the upstream Jackcess project.

AgileDataIntegrity computes and verifies the encryptedHmacKey / encryptedHmacValue pair of MS-OFFCRYPTO §2.3.4.14, and OfficeCryptCodecHandler exposes them for an Agile-encrypted file. It is checked against itself — tampered content, wrong key and swapped ciphertexts all fail, every Agile hash round-trips — but not against Microsoft's output: that needs an Agile file already carrying a <dataIntegrity> element to recompute, or real Access to open one written here, and neither the ACE nor the DAO engine substitutes. Which bytes the hash covers is the caller's to decide; the specification defines it over an OOXML package's encrypted stream, and an Access database has no such stream.

⁸ Ordinary rows and complex values write correctly into an existing .accdb, and the ACE engine reads both back — including a multi-value entry and an attachment appended by AddComplexValue. A Memo or OLE value is not there yet: the write completes and round-trips through this library, but ACE reads the value back as empty. The page accounting around it is now right — Access and this library agree on the table's row count, which they did not before the long-value map's row was honoured — so what remains is the chain's own content. Note that every other write path here is exercised against Jet 4 .mdb — that gap is how the encoder came to disagree with the reader about where a value lives.

Database.Create(path, JetVersion.Jet12…Jet17) writes a Jet 4 database — the header reads Standard Jet DB version 0x01 whatever the file is called. True ACE format, with its own header, system tables and page structures, is not written. It is still useful: Access and the ACE engine open a Jet 4 file regardless of extension, so a caller that creates one, fills it and hands it on gets a file that works. Reading a real .accdb is genuine ACE and unaffected.

¹⁰ Jet 4's integer types stop at Long (Int32). Its only 8-byte types are Money, Double and ShortDateTime, and none is a general 64-bit integer. BIGINT — Access's "Large Number", data type 0x13 — arrived with ACE 16 (Access 2016) and only when that option is enabled; this library does not implement it, so a column using it cannot be read either.

For an exact 64-bit value in a Jet 4 file, use DataType.Numeric (0x10): a 17-byte decimal with up to 28 digits of precision, which holds any Int64 exactly. The alternatives all lose something — Money is internally an Int64 of ten-thousandths, so it caps at ±922,337,203,685,477.5807 and carries currency semantics; Double is exact only to 2^53, and silently wrong above it.

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.
  • net10.0

    • No dependencies.

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
3.1.0 136 8/9/2026
3.0.2 100 8/8/2026
3.0.1 117 8/1/2026
3.0.0 103 8/1/2026
2.2.0 142 7/29/2026
2.0.0 111 7/28/2026
1.2.2 1,425 5/18/2026
1.2.0 126 5/18/2026
1.0.0 117 5/18/2026

3.1.0 — Three data-loss fixes that only Access could see, plus one new option.
     Pages appended to an existing database were never claimed in the database-wide
     free map at page 1, and Access treats everything past that map's window as free:
     a file this library grew kept offering its new pages up, and the next table
     Access created was written straight over live rows, which then read back as
     missing rows and duplicate keys. Rows Access had relocated (forwarding pointers)
     were skipped by every read path, so ordinary tables could vanish from ListTables
     and GetTable entirely — and an import aimed at one would create a duplicate.
     Appending to a table Access authored wrote every fixed-length value after the
     first Boolean column one byte too far right, because a Boolean lives in the null
     mask and takes no fixed data; the writer now honours each column's declared
     offset. New: ImportOptions.TreatNullAsBlank writes a null as its column's blank
     ("" for Text and Memo, 0 for numerics) instead of NULL, for readers that require
     a value in every declared column. See CHANGELOG.md.