Open source

Reading and writing dBASE and FoxPro files in modern .NET

Thunder.Dbf is a free, MIT-licensed C# library for reading and writing dBASE, FoxPro, Visual FoxPro and Clipper .dbf files on .NET 10. It runs on Windows, Linux and macOS with no ODBC, OLE DB, Jet or 32-bit process, and it keeps the legacy app's indexes and locks intact, so the old program can keep running on the same files.

dotnet add package Thunder.Dbf

Published: October 31, 2026 · Calvin Luttrell

A surprising amount of business still runs on xBase files. GoldMine CRM keeps its contacts in dBASE tables. So do older accounting and inventory systems, Siemens STEP 7 projects and every GIS shapefile. The programs that write them are often decades old, and they still work, which is exactly why nobody has replaced them.

The trouble starts when a modern system needs the same data. Microsoft's Visual FoxPro OLE DB and Jet dBASE drivers are 32-bit only and no longer developed, so every .NET app that talks to these files ends up pinned to an x86 build and a driver nobody maintains. Reading the files is the easy part. The hard part is writing to them while the old program is still running, without breaking its indexes or its locks.

That is the problem Thunder.Dbf solves. It's a free, MIT-licensed C# library for .NET 10 that reads and writes dBASE, FoxPro, Visual FoxPro and Clipper tables, memos and indexes in plain managed code, on Windows, Linux and macOS.

Sharing files with a program from 1995

Old xBase programs don't use a database server. Each copy of the app opens the same files on a shared drive and coordinates through byte-range locks and the index files themselves. Anything else that writes to those files has to play by the same rules, or the old app finds a corrupt index the next morning.

Thunder.Dbf takes the same record and header locks dBASE takes, updates every index tag on every write, and leaves fields you didn't change byte for byte as they were. It opens read-only unless you ask otherwise, so pointing it at live data to look around is safe:

dotnet add package Thunder.Dbf
using Thunder.Dbf;

using var db = DbfDatabase.Open(@"\\server\goldmine\data");
Console.Write(db.Check());   // what's readable, writable, locked or damaged

foreach (var record in db["CONTACT1"].ReadAll())
{
    if (!record.IsDeleted)
        Console.WriteLine($"{record["COMPANY"]}, {record["CITY"]}");
}

// Find a record through the app's own .mdx index, like dBASE's SEEK
var contacts = db["CONTACT1"];
using var index = contacts.OpenProductionIndex()!;
var acme = contacts.Seek(index["CONTCOMP"], "ACME");

Values come back as ordinary .NET types (string, decimal, DateOnly, bool). To write, open with new DbfDatabaseOptions { ReadOnly = false }.

Proving it against the real programs

Unit tests prove a library agrees with itself. The question that matters is whether the old program still trusts the files afterward. So we spent a day running Thunder.Dbf against the software it actually shares files with: Visual FoxPro 9 SP2, the VFP OLE DB 9.0 driver, the Jet 4.0 and ACE 16 Access drivers, and Harbour 3.0, the open-source Clipper. After each write, Visual FoxPro itself walked every index tag and looked up every key. The results below are from the build going into the next release.

First, the two programs took turns. Over twenty rounds, Thunder.Dbf and Visual FoxPro alternated on the same tables with random appends, updates, deletes and recalls, then wrote at the same time. That came to 616,640 writes and 39.4 million index entries and records checked, and every one agreed.

Many programs on one table

Next, a simulated office: four Visual FoxPro programs, ten OLE DB connections and Thunder.Dbf (three programs and three threads) writing the same table for 60 seconds at a time, adding records, changing keys, deleting and recalling. We ran it on four kinds of table, on a local disk and over a network share. Every time, every record added was counted, Visual FoxPro found every key, and every index matched a fresh rebuild. The same held with Clipper programs (Harbour) on their own index format.

Appends in each 60-second office run, by pass or failEvery run passedcalls.dbf, local, calls.dbf, local: 650,136 appends, pass650,136kinds.dbf, local, kinds.dbf, local: 51,314 appends, pass51,314TEST10K.DBF, local, TEST10K.DBF, local: 49,874 appends, pass49,874calls.dbf, SMB, calls.dbf, SMB: 195,726 appends, pass195,726kinds.dbf, SMB, kinds.dbf, SMB: 96,688 appends, pass96,688TEST10K.DBF, SMB, TEST10K.DBF, SMB: 102,522 appends, pass102,522Clipper office: people.dbf .ntx, HarbourClipper office, people.dbf .ntx, Harbour: 273,098 appends, pass273,098.cdx created by Thunder.Dbf: kinds.dbf, local.cdx created by Thunder.Dbf, kinds.dbf, local: 12,309 appends, pass12,309
Records added in each 60-second office run. The last run used .cdx tags created by Thunder.Dbf itself.

Other languages

FoxPro tables in other languages use other code pages, and an index built in the wrong one is useless to the old program. So we had Visual FoxPro, set to each code page, build tables of random text using every character the code page has: Western (1252, 850 and 437), Central European (852 and 1250), Cyrillic (1251 and 866), Greek (1253), Turkish (1254), Baltic (1257) and Japanese (932).

In all eleven, Thunder.Dbf's index keys were byte-for-byte identical to VFP's, whether it rebuilt VFP's indexes or created its own. That includes case-insensitive UPPER() indexes, down to the handful of letters where VFP's capitals differ from the standard ones. After Thunder.Dbf added and changed 800 records, Visual FoxPro found every record under every key, and its own REINDEX agreed.

How the other drivers compare

Thunder.Dbf matches Visual FoxPro on everything both were tested on. The other drivers got the same treatment, and they didn't all hold up: Jet corrupts its own index when two programs write at once, Harbour lets FoxPro's candidate-key tags go stale, and ACE fails under load. Those limits are in Thunder.Dbf's documentation.

BehaviourThunder.DbfVFP 9Harbour 3.0Jet / ACEEvidence
Keeps each tag's root on its page while others have the table openYesYesYesNot testedChecked with VFP sessions holding the table open
Writes the same keys, in the same order, as VFP's own REINDEXYesYesNot testedNot tested527,000 entries on one table and 130,000 on another equal after 20 rounds
Index keys match VFP's in other code pages, including UPPER() indexesYesYesNot testedNot testedByte for byte in eleven code pages, from Western and Central European to Cyrillic, Greek, Turkish, Baltic and Japanese
Keeps VFP CANDIDATE tags up to dateYesYesNoNot testedHarbour left a tag at 16 entries while the table grew to 100,130 records
Several of its own writers on one table stay consistentYesYesYesNoJet: two processes left 4,228 keys for 4,227 records
Writes safely while VFP has the same table openYesYesPartlyNoHarbour keeps roots but not CANDIDATE tags
Keeps FOR-filtered tagsYesYesNot testedNoJet left a FOR tag at 1,518 entries against 4,684 in a rebuild
After a writer is killed mid-change, nothing is silently wrongYesNoNot testedNot testedThunder.Dbf 0 in 120 kills; Visual FoxPro left a wrong index with no warning 47 times in 120
PACK, REINDEX and INDEX ON wait for the table to themselvesYesYesYesNot testedThunder.Dbf refuses while VFP has the table open (8 of 8 cases)
Holds up under heavy load without driver errorsYesYesYesPartlyACE 16 failed a few thousand single-row inserts in

Creating FoxPro indexes

Thunder.Dbf also creates FoxPro .cdx tags, including FOR, UNIQUE, CANDIDATE and DESCENDING ones. All 16 tag kinds tested came out byte-identical to the tags Visual FoxPro builds itself, and they held up in a full office run alongside VFP.

Crashes, power cuts and damaged files

The harshest test for a shared file is a writer dying halfway through a change, the way a program does when someone closes a laptop lid or the power drops. What matters is not whether damage happens, since no file format can promise that, but whether anyone finds out before the old program starts trusting a broken index.

A writer killed mid-write

We killed each writer at a random moment, 60 times, 30 on each of two tables. Then Visual FoxPro judged the table with its own tools: it opened it, looked up every record by every key, and compared each index with its own REINDEX.

Visual FoxPro and its OLE DB driver left a wrong index with no warning 51 times in 180. That means a record's key was missing from the index or still had its old value, so VFP's own SEEK couldn't find the record, and nothing would tell anyone until someone ran REINDEX. Thunder.Dbf never did. Every time something was wrong it said so, and refused to write on until it was fixed.

Killed mid-write: index left wrong with no warning. Visual FoxPro 26 of 60 and 21 of 60, VFP OLE DB 4 of 60, Thunder.Dbf 0 of 120Visual FoxPro 9, killed mid-INSERTVisual FoxPro 9, killed mid-INSERT: 26 of 6026 of 60Visual FoxPro 9, killed mid-REPLACEVisual FoxPro 9, killed mid-REPLACE: 21 of 6021 of 60VFP OLE DB driverVFP OLE DB driver: 4 of 604 of 60Thunder.Dbf0 of 120
Killed mid-write: index left wrong, with no warning. Judged by Visual FoxPro's own SEEK and REINDEX. Thunder.Dbf's 120 kills cover its default settings and a locked bulk load.
Writer, 60 kills eachCleanDamage reported (refused until fixed)Index left wrong, no warningWon't open (VFP error 2091)
Visual FoxPro 9, killed mid-INSERT320262
Visual FoxPro 9, killed mid-REPLACE390210
VFP OLE DB driver55041
Thunder.Dbf, default settings392100
Thunder.Dbf, locked bulk load456, every one recovered by a rebuild00

A power cut

A killed program still leaves the operating system to finish its writes. A power cut doesn't. We switched off a virtual machine, hard, at a random moment while Thunder.Dbf was writing, 50 times in each mode. On its default settings it never lost a write it had confirmed, and in no mode did it leave damage it didn't report.

Setting, 50 power cuts eachConfirmed writes lostAfter the restartLeft wrong without a word
Default0 in 50 (the last confirmed write was there all 50 times)35 clean; 15 damage found and reported0
Flushes off (full speed)The last few seconds of writes, as documented48 clean; 2 damage found and reported0
Locked bulk loadOnly the batch in progress; every finished batch kept, 50 of 5047 damage found and reported; 3 clean (the cut fell between batches)0

“Confirmed” means every write Thunder.Dbf had reported as done before the power went, and after each cut we looked up the last of those writes in the file itself. After a power cut, the default leaves at most an extra index entry, never a missing one, and Thunder.Dbf finds it on open. Visual FoxPro doesn't: looking at the same files after the default-mode cuts, it called 15 of them clean when its own REINDEX disagreed, and couldn't open 3.

Damaged files

Files on old shared drives get damaged in ways no test plans for. We fed Thunder.Dbf 200,000 deliberately corrupted tables and indexes. In the final 100,000, every damaged file got a plain error that explains what's wrong, not a raw .NET exception.

How we tested: on 31 October 2026, on one Windows 11 PC, with Visual FoxPro 9 SP2, the VFP OLE DB 9.0 driver and Thunder.Dbf main: the kill tests on 8310b1c (the bulk-load row on a836679), the power cuts on 8ef7cd8 (the flushes-off row on 8310b1c) and the code pages on b848387. Each writer was killed 0.1 to 2.5 seconds into writing. “Damage reported” means Thunder.Dbf named the problem and refused to write until a rebuild; it doesn't count as silent. Power cuts were Hyper-V's hard Turn Off of an Ubuntu 24.04 virtual machine with a standard ext4 disk, 0.3 to 3 seconds into writing. Turn Off loses everything the machine hadn't yet written to its disk; a physical disk's own write cache isn't part of the test, and on default settings Thunder.Dbf flushes through it. Bulk loads ran in batches of 200 changes. The office runs on a local disk used main; the network-share runs (SMB, on the same PC) used an earlier build of the same write path. The damaged-file tests ran on a Mac.

How fast it is

Most teams that come to Thunder.Dbf are leaving the Visual FoxPro OLE DB driver, and many still have Visual FoxPro itself writing the same files. So we timed all three on one Windows 11 PC, on the same tables, doing the same work.

Reading

Reading is where the gap is widest. Thunder.Dbf found records by key 49 times faster than the OLE DB driver, about 76,900 lookups a second against 1,580, and read a whole 10,000-row table 2.8 times faster.

Reading speed: Thunder.Dbf against the VFP OLE DB driverReadingLook up a record by key, per secondThunder.DbfLook up a record by key, per second, Thunder.Dbf: 76,92376,923VFP OLE DB driverLook up a record by key, per second, VFP OLE DB driver: 1,5801,580Read a whole 10,000-row table, rows per secondThunder.DbfRead a whole 10,000-row table, rows per second, Thunder.Dbf: 666,667666,667VFP OLE DB driverRead a whole 10,000-row table, rows per second, VFP OLE DB driver: 238,095238,095
Each pair has its own scale, so the bars show the ratio between the two.

Writing

Writing is closer, and it depends on how many index tags each write has to keep current. With nothing forced to disk, which is how Visual FoxPro and its driver always write, Thunder.Dbf added records 45% faster than the OLE DB driver on a table with 2 index tags, and 12% faster than Visual FoxPro itself. On a table with 7 tags it ran about 8% behind the driver, and Visual FoxPro was faster still, at 21,053 records a second to Thunder.Dbf's 15,267.

Bulk loads and migrations usually lock the table first. Locked, Thunder.Dbf beat Visual FoxPro's FLOCK() by 21% on the 2-tag table and came within about 5% of it on the 7-tag one.

Records added per second: Thunder.Dbf, the VFP OLE DB driver and Visual FoxPro 9Nothing forced to disk (how VFP and its driver always write)Table with 2 index tagsThunder.DbfTable with 2 index tags, Thunder.Dbf: 21,05321,053VFP OLE DB driverTable with 2 index tags, VFP OLE DB driver: 14,49314,493Visual FoxPro 9Table with 2 index tags, Visual FoxPro 9: 18,86818,868Table with 7 index tagsThunder.DbfTable with 7 index tags, Thunder.Dbf: 15,26715,267VFP OLE DB driverTable with 7 index tags, VFP OLE DB driver: 16,66716,667Visual FoxPro 9Table with 7 index tags, Visual FoxPro 9: 21,05321,053Table locked for a bulk loadTable with 2 index tagsThunder.DbfTable with 2 index tags, Thunder.Dbf: 24,39024,390Visual FoxPro 9, FLOCK()Table with 2 index tags, Visual FoxPro 9, FLOCK(): 20,20220,202Table with 7 index tagsThunder.DbfTable with 7 index tags, Thunder.Dbf: 21,27721,277Visual FoxPro 9, FLOCK()Table with 7 index tags, Visual FoxPro 9, FLOCK(): 22,47222,472
Records added per second, like for like: unforced writes against unforced writes, and a locked table against Visual FoxPro's FLOCK().

Why the default is slower

Out of the box, Thunder.Dbf forces every write to disk, in the order that makes the crash results above possible. Visual FoxPro and its driver don't, which is why the comparisons above run Thunder.Dbf with flushes off. Forced to disk, Thunder.Dbf added 1,096 records a second on the 2-tag table and 1,352 on the 7-tag one, against 1,479 and 2,307 for Visual FoxPro with FLUSH FORCE.

The developer picks per table, and the default is the safe end. Full speed is one setting away (FlushWritesToDisk = false).

SettingIf a write is cut offRecords a second (2 tags / 7 tags)
DefaultDamage from a killed process or a power cut is always found and reported. No confirmed write was lost in 50 power cuts.1,096 / 1,352
Default plus the write journalA cut-off write is finished exactly by the next one.550 / 607
Flushes offDamage is still always reported, but a power cut can lose the last few seconds of writes, as it can with Visual FoxPro.21,053 / 15,267
Locked bulk loadOnly the batch in progress can be lost; it's found and refused until a rebuild. Other users wait for the batch to finish.24,390 / 21,277

All the numbers

What the app doesThunder.DbfVFP OLE DB driverVisual FoxPro 9
Add records, nothing forced to disk, 2-tag table21,05314,49318,868
Same, 7-tag table15,26716,66721,053
Add records with the table locked (bulk loads, migrations), 2-tag table24,390–20,202 (FLOCK)
Same, 7-tag table21,277–22,472 (FLOCK)
Add records, every one forced to disk (Thunder.Dbf's default), 2-tag table1,096–1,479 (FLUSH FORCE)
Same, 7-tag table1,352–2,307 (FLUSH FORCE)
Look up a record by key, per second76,9231,580–
Read every record of a 10,000-row table, rows per second666,667238,095–

Records added per second unless noted; the fastest in each row is in bold.

Thunder.Dbf also runs on macOS, where Visual FoxPro and its driver don't. On a MacBook Pro with an Apple M4 Pro, it added up to 27,027 records a second on the 2-tag table with nothing forced to disk, and 19,801 a second in a locked bulk load on the 7-tag table. It found about 150,000 records a second by key and read about 800,000 rows a second.

How we measured: on 31 October 2026, on one Windows 11 PC with a local SSD. Thunder.Dbf main (8310b1c) on .NET 10, 64-bit; the VFP OLE DB 9.0 driver through System.Data.OleDb, 32-bit; Visual FoxPro 9 SP2. Both .NET programs ran fully compiled (DOTNET_TieredCompilation=0), as a long-running app does; Visual FoxPro is native. The tables were calls.dbf (2 index tags, with memos) and TEST10K.DBF (7 index tags, 10,000 rows), as fresh copies every run. Each run did an untimed warm-up of 200 appends, then 2,000 timed appends, and each figure is the median of 3 runs. “Nothing forced to disk” means Thunder.Dbf's FlushWritesToDisk = false, the way Visual FoxPro and its driver always write. A Defender exclusion made no difference (within 4%).

From reading the data to moving off it

Most teams don't stop at reading. The same package covers the usual next steps, from typed queries to a full migration where the old app and the new database stay in step until cut-over.

You want toUse
Query with C# classestable.AsQueryable<Contact>(), a LINQ provider that seeks through .mdx tags when it gives the same answer as a scan; query.Explain() shows which
Keep existing ADO.NET codeDbfConnection, DbfCommand and DbfDataReader, with parameterized SQL, in place of the 32-bit VFP OLE DB or Jet driver
Move to SQL Server, PostgreSQL or SQLiteDbfMigrator.Migrate(...) in Thunder.Dbf.Sql copies a whole folder
Migrate graduallyDbfBridge.Sync() keeps a live DBF table and its SQL twin in step, both ways, until cut-over
Shape a CSV into a DBF, or exportDbfImport.FromCsv and DbfExport.ToCsv
Maintain tablesCreateIndexTag (dBASE's INDEX ON) and Pack() (PACK)
Script itthunder-dbf check, info, import, export, migrate
Put a web app on topThunder.Dbf.Server: a JSON API with hashed API keys or OpenID Connect, per-table and per-field permissions, a write journal, backups and an OpenAPI schema per table

Install the tool with dotnet tool install -g thunder-dbf, and the SQL package with dotnet add package Thunder.Dbf.Sql.

Why we built it

Thunder.Dbf came out of the work we do for clients: keeping an old system running while the business moves to something new. That kind of move only works if the old and new systems can share data safely in the meantime, and that is a data-layer problem before it is anything else. If you're facing one, see our legacy .NET modernization piece or get in touch.

Reference

At a glance

NameThunder.Dbf
Made byProjectThunder
LicenseMIT, free for commercial use
Current version0.1.0, released October 31, 2026
Runtime.NET 10, pure C#, no native dependencies
PlatformsWindows, Linux, macOS (64-bit)
NuGet packagesThunder.Dbf (engine), Thunder.Dbf.Sql (SQL migration and sync), thunder-dbf (command-line tool)
Web serviceThunder.Dbf.Server, a self-contained download on each GitHub release
Sourcegithub.com/pthunder/Thunder.Dbf

Supported formats

FileReadWrite
dBASE III and IV tables, .dbt memosYesYes
FoxPro 2 and Visual FoxPro tables, .fpt memosYesYes
Visual FoxPro integer, currency, double, datetime, null and autoincrement fieldsYesYes
dBASE IV .mdx indexesYesYes, every tag kept exact
dBASE III .ndx indexesYesYes
Clipper .ntx indexesYesYes, under Clipper's own index lock
FoxPro and Visual FoxPro .cdx and .idx indexesYesPreview, opt in with MaintainFoxProIndexes
Code pagesOver 60, including DOS 437 and 850 and Windows 125xSame

dBASE II and dBASE 7 tables are refused with a clear error rather than misread. The test suite checks every index against files written by dBASE III Plus, dBASE IV, FoxPro 2, Visual FoxPro, X# and a real Siemens STEP 7 project, on Windows, Linux and macOS.

Frequently asked questions

How do I read a DBF file in C#?

Install Thunder.Dbf from NuGet, call DbfDatabase.Open(folder), and loop over db["TABLE"].ReadAll(). It works on .NET 10 on Windows, Linux and macOS, with no drivers to install.

How do I replace the Visual FoxPro OLE DB driver or the Jet dBASE driver?

Use Thunder.Dbf's ADO.NET provider (DbfConnection, DbfCommand, DbfDataReader). It runs in a normal 64-bit process, so the app no longer has to be built for x86.

Can my legacy dBASE or FoxPro app keep running while .NET writes to its files?

Yes, on Windows and Linux. Thunder.Dbf takes the same byte-range locks dBASE takes and keeps .mdx, .ndx and .ntx indexes exact. macOS can't enforce those locks, so there it's for read-only or single-user use.

Can I read GoldMine CRM data in .NET?

Yes. GoldMine stores contacts in dBASE tables such as CONTACT1.DBF with .mdx indexes, which Thunder.Dbf reads, seeks and writes. Its tests have been run against real GoldMine data.

How do I convert DBF files to SQL Server, PostgreSQL or SQLite?

Use DbfMigrator.Migrate from the Thunder.Dbf.Sql package, or run thunder-dbf migrate from the command line. To keep both sides live during a move, use DbfBridge.Sync().

Can Visual FoxPro, OLE DB and Thunder.Dbf users write the same table at once?

Yes. In 60-second runs with 4 Visual FoxPro processes, 10 VFP OLE DB connections and 6 Thunder.Dbf writers on one table, locally and over an SMB share, every append was counted and Visual FoxPro found every index key. One run took 650,136 appends with nothing lost.

Is Thunder.Dbf faster than the Visual FoxPro OLE DB driver?

For reading, by a wide margin: on one Windows 11 PC it found records by key 49 times faster and read a whole table 2.8 times faster. For writing, with nothing forced to disk as the driver always runs, it was 45% faster on a table with 2 index tags and about 8% slower on one with 7. By default Thunder.Dbf forces every write to disk, which is slower and survives a crash; that is a per-table setting.

What happens if a program crashes or the power fails while Thunder.Dbf is writing?

On its default settings, Thunder.Dbf never lost a write it had confirmed in 50 hard power cuts, and in 120 killed writes it never left an index wrong without reporting it. When it finds damage it says so and refuses to write until the index is rebuilt. In the same kill tests, Visual FoxPro and its OLE DB driver left a wrong index with no warning 51 times in 180.

Does Thunder.Dbf work with Central European, Cyrillic, Greek or Japanese FoxPro tables?

Yes. In eleven code pages (Western 1252, 850 and 437, Central European 852 and 1250, Cyrillic 1251 and 866, Greek 1253, Turkish 1254, Baltic 1257 and Japanese 932) its index keys are byte-for-byte identical to Visual FoxPro's, including case-insensitive UPPER() indexes.

Is it free?

Yes. It's MIT-licensed, including for commercial use. ProjectThunder offers help with modernizing or migrating legacy systems.

Sources