I've been looking for a few days on a way to do this in C# as well.
SQLite defines as part of it's C-Interface a method to do just the thing:
https://www.sqlite.org/c3ref/serialize.html
unsigned char *sqlite3_serialize(
sqlite3 *db, /* The database connection */
const char *zSchema, /* Which DB to serialize. ex: "main", "temp", ... */
sqlite3_int64 *piSize, /* Write size of the DB here, if not NULL */
unsigned int mFlags /* Zero or more SQLITE_SERIALIZE_* flags */
);
The sqlite3_serialize(D,S,P,F) interface returns a pointer to memory that is a serialization of the S database on database connection D
For an ordinary on-disk database file, the serialization is just a copy of the disk file. For an in-memory database or a "TEMP" database, the serialization is the same sequence of bytes which would be written to disk if that database where backed up to disk.
However, the C# library I'm using (sqlite-net-base with SQLitePCLRaw.provider.winsqlite3 as the provider) does not DllImport this method as part of it's provider implementation. The method is still available though as the SQLite specification includes it.
So, what I've done is import the method myself along side the library. This has worked in my case (using winsqlite3). You should be able to do the same by changing the DLL name to the one you are using.
[DllImport("winsqlite3", CallingConvention = CallingConvention.StdCall, ExactSpelling = true)]
public static extern IntPtr sqlite3_serialize(sqlite3 db, [MarshalAs(UnmanagedType.LPStr)] string a, IntPtr piSize, uint mFlags);
In the docs there is an important note that
The caller is responsible for freeing the returned value to avoid a memory leak
As this is calling C code it's important to remember that C# won't be garbage collecting this for us until we get any memory it gives us back into C# managed memory.
So, a method to wrap this call and make it more C# friendly can be used:
private byte[] SeralizeDb()
{
var lengthInput = Marshal.AllocHGlobal(sizeof(long));
var unmanagedResult = sqlite3_serialize(this.sqliteDbConnection.Handle, "main", lengthInput, 0); // https://www.sqlite.org/c3ref/serialize.html main is mentioned as an example and is what works
// Handle null result or a bad length.
var lengthResult = (int)Marshal.ReadInt64(lengthInput); // Cast as int for the Marshel.Copy. Db is only a cache of a few Mb ever in our case
var managedResult = new byte[lengthResult];
Marshal.Copy(unmanagedResult, managedResult, 0, lengthResult);
// Free unmanaged memory allocations
Marshal.FreeHGlobal(lengthInput);
Marshal.FreeHGlobal(unmanagedResult);
return managedResult;
}