Pure-managed .NET library for reading Microsoft Access JET databases — no OleDB, ODBC, or ACE/Jet driver installation required.
v2.0 introduced typed DataTables and typed streaming by default. See CHANGELOG.md and the migration guide for breaking changes.
| ✅ No native dependencies | Pure C# — runs anywhere .NET runs |
| ✅ Jet3 & Jet4 / ACE | Access 97 through Access 2019 (.mdb / .accdb) |
| ✅ Typed by default | int, DateTime, decimal, Guid — not just strings |
| ✅ All column types | Text, Integer, Currency, Date/Time, GUID, MEMO, OLE Object, Decimal |
| ✅ Streaming API | Process millions of rows without loading the whole file |
| ✅ Async support | Full Task<T>-based async for all major operations |
| ✅ Page cache | 256-page LRU cache (~1 MB, configurable) |
| ✅ Fluent query | Query().Where().Take().Execute() — typed and string chains |
| ✅ Progress reporting | IProgress<int> callbacks on all long operations |
| ✅ Non-Western text | Code page auto-detected from the database header |
| ✅ OLE Objects | Detects embedded JPEG, PNG, PDF, ZIP, DOC, RTF |
dotnet add package JetDatabaseReaderInstall-Package JetDatabaseReaderJetDatabaseReader targets netstandard2.0, which is consumed by every current .NET surface:
| Consumer | Minimum version |
|---|---|
| .NET Framework | 4.6.1 |
| .NET Core | 2.0 |
| .NET | 5 / 6 / 7 / 8 / 9 |
| Mono / Xamarin | All |
| Unity | 2018.1+ |
| UWP | 10.0.16299+ |
using JetDatabaseReader;
using var reader = AccessReader.Open("database.mdb");
List<string> tables = reader.ListTables();
Console.WriteLine($"Found {tables.Count} tables: {string.Join(", ", tables)}");
DataTable dt = reader.ReadTable("Orders");
foreach (DataRow row in dt.Rows)
{
int id = (int)row["OrderID"];
var date = (DateTime)row["OrderDate"];
decimal amt = (decimal)row["Freight"];
Console.WriteLine($"#{id} {date:yyyy-MM-dd} {amt:C}");
}DataTable dt = reader.ReadTable("Products");
// dt.Columns["ProductID"].DataType == typeof(int)
// dt.Columns["UnitPrice"].DataType == typeof(decimal)
// dt.Columns["Discontinued"].DataType == typeof(bool)DataTable dt = reader.ReadTableAsStringDataTable("Products");
// every column is typeof(string)TablePreviewResult preview = reader.ReadTable("Products", maxRows: 20);
foreach (TablePreviewColumn col in preview.Schema)
Console.WriteLine($"{col.Name}: {col.TypeName} ({col.SizeDesc})");var progress = new Progress<int>(n => Console.Write($"\r{n:N0} rows"));
foreach (object[] row in reader.StreamRows("BigTable", progress))
{
int id = (int)row[0];
decimal val = row[2] == DBNull.Value ? 0m : (decimal)row[2];
}foreach (string[] row in reader.StreamRowsAsStrings("BigTable"))
Console.WriteLine(string.Join(", ", row));Null values in typed rows surface as DBNull.Value.
// Typed chain
object[] order = reader.Query("Orders")
.Where(row => row[2] is DateTime d && d.Year == 2024)
.Take(10)
.FirstOrDefault();
int count = reader.Query("OrderDetails")
.Where(row => row[3] is decimal p && p > 100m)
.Count();
// String chain
IEnumerable<string[]> recent = reader.Query("Orders")
.WhereAsStrings(row => row[2].StartsWith("2024"))
.Take(50)
.ExecuteAsStrings();List<string> tables = await reader.ListTablesAsync();
DataTable dt = await reader.ReadTableAsync("Orders");
DatabaseStatistics stats = await reader.GetStatisticsAsync();
Dictionary<string, DataTable> all = await reader.ReadAllTablesAsync();
Dictionary<string, DataTable> allStr = await reader.ReadAllTablesAsStringsAsync();// Typed columns
Dictionary<string, DataTable> all = reader.ReadAllTables(
new Progress<string>(t => Console.WriteLine($"Reading {t}...")));
// String columns (compatibility)
Dictionary<string, DataTable> allStr = reader.ReadAllTablesAsStrings();foreach (ColumnMetadata col in reader.GetColumnMetadata("Orders"))
Console.WriteLine($"{col.Ordinal}. {col.Name} — {col.TypeName} ({col.ClrType.Name})");
DatabaseStatistics s = reader.GetStatistics();
Console.WriteLine($"Version: {s.Version}");
Console.WriteLine($"Size: {s.DatabaseSizeBytes / 1024 / 1024} MB");
Console.WriteLine($"Tables: {s.TableCount} Rows: {s.TotalRows:N0}");
Console.WriteLine($"Cache hit: {s.PageCacheHitRate}%");var options = new AccessReaderOptions
{
PageCacheSize = 512, // pages in LRU cache (default: 256)
ParallelPageReadsEnabled = true, // parallel I/O (default: false)
DiagnosticsEnabled = false, // verbose logging (default: false)
ValidateOnOpen = true, // format check on open (default: true)
FileAccess = FileAccess.Read, // default
FileShare = FileShare.Read, // default: others may read, writes blocked
// FileShare = FileShare.ReadWrite // use when Access has the file open
};
using var reader = AccessReader.Open("database.mdb", options);try { var dt = await reader.ReadTableAsync("Orders"); }
catch (FileNotFoundException) { /* file missing */ }
catch (NotSupportedException) { /* encrypted / password-protected */ }
catch (InvalidDataException) { /* corrupt or non-JET file */ }
catch (JetLimitationException) { /* deleted-column gap, numeric overflow */ }
catch (ObjectDisposedException) { /* reader already disposed */ }| ❌ Encrypted databases | Remove password in Access (File › Info › Encrypt with Password) |
| ❌ Attachment fields (0x11) | Rare type added in Access 2007 |
| ❌ Linked tables | Only local tables are listed |
| ❌ Overflow rows | Rows spanning multiple pages are skipped |
| ❌ Write operations | Read-only library |
// Open
var r = new JetDatabaseReader("db.mdb"); // v1 ❌
var r = AccessReader.Open("db.mdb"); // v2 ✅
// Typed DataTable
var dt = r.ReadTableAsDataTable("Orders"); // v1 — string columns
var dt = r.ReadTable("Orders"); // v2 ✅ typed
var dt = r.ReadTableAsStringDataTable("Orders"); // v2 compat
// Preview
var t = r.ReadTable("T", maxRows: 10); // v1 ❌ — returned a tuple
TablePreviewResult t = r.ReadTable("T", 10); // v2 ✅ typed result
// Streaming
foreach (string[] row in r.StreamRows("T")) // v1
foreach (object[] row in r.StreamRows("T")) // v2 ✅ typed
foreach (string[] row in r.StreamRowsAsStrings("T")) // v2 compat
// Bulk
var all = r.ReadAllTables(); // v1 — string cols / v2 ✅ typed
var all = r.ReadAllTablesAsStrings(); // v2 compatFull details in CHANGELOG.md.
Based on the mdbtools format specification. The library parses JET pages directly:
- Page 0 — header: Jet3/Jet4 detection, code page, encryption flag
- Page 2 —
MSysObjectscatalog: table names → TDEF page numbers - TDEF pages — table definition chains: column descriptors + names
- Data pages — row slot arrays → null mask + fixed/variable fields
- LVAL pages — long-value chains for MEMO and OLE fields
Issues and pull requests welcome at github.com/diegoripera/JetDatabaseReader.
MIT — see LICENSE for details.