The Extensible Storage Engine (ESE) is a low-level database engine that is used internally by Windows for all sorts of things like Microsoft Exchange message store, Active Directory database, Microsoft Updates, Local Security Policy, etc. It is also called "Jet Blue"... not to be confused with "Jet Red", the engine that's used in Microsoft Access.

The Application Programming Interface (API) for ESE has recently been published, so now ordinary users like us can make use of ESE to poke around the internal Windows databases. However, the API documentation is horrible! It lacks critical information like the basic flow of accomplishing simple tasks. It's also full of omissions and errors.

One of the critical tools for dealing with an ESE database is the command-line tool called ESENTUTL.EXE (which is installed by default in Windows XP in the \Windows\System32 directory). You can use this tool to get the listing of tables in a database. An example command to dump the "meta data" for a database (that includes the table lists) is as follows:

esentutl /mm DataStore.edb

This article will describe the steps to use the low-level ESE APIs to read the contents of a table. The ESE engine itself does not support an SQL language interface. This might be a good time to look at a companion article In-memory SQL Engine.

API Declarations

Let's start with the API "boiler plate" stuff. This section is rather large, but that's OK considering that's what this article is all about... using the ESE API. I've also annotated where the API documentation is wrong or misleading.

'JET_ERR JET_API JetCreateInstance(
'  JET_INSTANCE* pinstance,
'  const tchar* szInstanceName
');
Declare Ansi Function JetCreateInstance Lib "esent.dll" ( _
    ByRef instance As IntPtr, _
    ByVal szInstanceName As String _
) As Integer

'JET_ERR JET_API JetInit(
'  JET_INSTANCE* pinstance
');
Declare Function JetInit Lib "esent.dll" ( _
    ByRef instance As IntPtr _
) As Integer

'JET_ERR JET_API JetBeginSession(
'  JET_INSTANCE instance,
'  JET_SESID* psesid,
'  JET_PCSTR szUserName,    ' Not used
'  JET_PCSTR szPassword     ' Not used
');
Declare Ansi Function JetBeginSession Lib "esent.dll" ( _
    ByVal instance As IntPtr, _
    ByRef psesid As IntPtr, _
    ByVal szUserName As String, _
    ByVal szPassword As String _
) As Integer

'JET_ERR JET_API JetAttachDatabase(
'  JET_SESID sesid,
'  const tchar* szFilename, ' ANSI only
'  JET_GRBIT grbit
');
Declare Ansi Function JetAttachDatabase Lib "esent.dll" ( _
    ByVal sesid As IntPtr, _
    ByVal szFilename As String, _
    ByVal grpit As Integer _
) As Integer

'JET_ERR JET_API JetOpenDatabase(
'  JET_SESID sesid,
'  const tchar* szFilename, ' ANSI Only
'  const tchar* szConnect,  ' Not Used
'  JET_DBID* pdbid,
'  JET_GRBIT grbit
');
Declare Ansi Function JetOpenDatabase Lib "esent.dll" ( _
    ByVal sesid As IntPtr, _
    ByVal szFilename As String, _
    ByVal szConnect As String, _
    ByRef pdbid As Integer, _
    ByVal grpit As Integer _
) As Integer

'JET_ERR JET_API JetOpenTable(
'  JET_SESID sesid,
'  JET_DBID dbid,
'  const tchar* szTableName,    ' ANSI Only
'  const void* pvParameters,
'  unsigned long cbParameters,
'  JET_GRBIT grbit,
'  JET_TABLEID* ptableid
');
Declare Ansi Function JetOpenTable Lib "esent.dll" ( _
    ByVal sesid As IntPtr, _
    ByVal dbid As Integer, _
    ByVal szTableName As String, _
    ByVal pvParameters As Integer, _
    ByVal cbParameters As Integer, _
    ByVal grbit As Integer, _
    ByRef ptableid As IntPtr _
) As Integer

'JET_ERR JET_API JetGetTableColumnInfo(
'  JET_SESID sesid,
'  JET_TABLEID tableid,
'  const tchar* szColumnName,   ' ANSI Only
'  void* pvResult,
'  unsigned long cbMax,
'  unsigned long InfoLevel
');
Declare Ansi Function JetGetTableColumnInfo Lib "esent.dll" ( _
    ByVal sesid As IntPtr, _
    ByVal tableid As IntPtr, _
    ByVal szColumnName As String, _
    ByVal pvResult As IntPtr, _
    ByVal cbMax As Integer, _
    ByVal InfoLevel As Integer _
) As Integer

'JET_ERR JET_API JetRetrieveColumn(
'  JET_SESID sesid,
'  JET_TABLEID tableid,
'  JET_COLUMNID columnid,
'  void* pvData,
'  unsigned long cbData,
'  unsigned long* pcbActual,
'  JET_GRBIT grbit,
'  JET_RETINFO* pretinfo
');
Declare Function JetRetrieveColumn Lib "esent.dll" ( _
    ByVal sesid As IntPtr, _
    ByVal tableid As IntPtr, _
    ByVal columnid As Integer, _
    ByVal pvData As IntPtr, _
    ByVal cbData As Integer, _
    ByRef pcbActual As Integer, _
    ByVal grbit As Integer, _
    ByRef pretinfo As JET_RETINFO _
) As Integer

'JET_ERR JET_API JetMove(
'  JET_SESID sesid,
'  JET_TABLEID tableid,
'  long cRow,
'  JET_GRBIT grbit
');
Declare Function JetMove Lib "esent.dll" ( _
    ByVal sesid As IntPtr, _
    ByVal tableid As IntPtr, _
    ByVal cRow As Integer, _
    ByVal grbit As Integer _
) As Integer

'JET_ERR JET_API JetCloseTable(
'  JET_SESID sesid,
'  JET_TABLEID tableid
');
Declare Function JetCloseTable Lib "esent.dll" ( _
    ByVal sesid As IntPtr, _
    ByVal tableid As IntPtr _
) As Integer

'JET_ERR JET_API JetCloseDatabase(
'  JET_SESID sesid,
'  JET_DBID dbid,
'  JET_GRBIT grbit
');
Declare Function JetCloseDatabase Lib "esent.dll" ( _
    ByVal sesid As IntPtr, _
    ByVal dbid As Integer, _
    ByVal grpit As Integer _
) As Integer

'JET_ERR JET_API JetDetachDatabase(
'  JET_SESID sesid,
'  const tchar* szFilename
');
Declare Ansi Function JetDetachDatabase Lib "esent.dll" ( _
    ByVal sesid As IntPtr, _
    ByVal szFilename As String _
) As Integer

'JET_ERR JET_API JetEndSession(
'  JET_SESID sesid,
'  JET_GRBIT grbit
');
Declare Function JetEndSession Lib "esent.dll" ( _
    ByVal sesid As IntPtr, _
    ByVal grbit As Integer _
) As Integer

'JET_ERR JET_API JetTerm(
'  JET_INSTANCE instance
');
Declare Function JetTerm Lib "esent.dll" ( _
    ByVal instance As IntPtr _
) As Integer

'JET_ERR JET_API JetGetSystemParameter(
'  JET_INSTANCE instance,
'  JET_SESID sesid,
'  unsigned long paramid,
'  JET_API_PTR* plParam,    ' really is Int32
'  JET_PSTR szParam,        ' ANSI Only
'  unsigned long cbMax
');
Declare Ansi Function JetGetSystemParameter Lib "esent.dll" ( _
    ByVal instance As IntPtr, _
    ByVal sesid As IntPtr, _
    ByVal paramid As Integer, _
    ByRef plParam As Integer, _
    ByVal szParam As String, _
    ByVal cbMax As Integer _
) As Integer

'JET_ERR JET_API JetSetSystemParameter(
'  JET_INSTANCE* pinstance,
'  JET_SESID sesid,
'  unsigned long paramid,
'  JET_API_PTR lParam,      ' really is Int32
'  JET_PCSTR szParam
');
Declare Ansi Function JetSetSystemParameter Lib "esent.dll" ( _
    ByRef pinstance As IntPtr, _
    ByVal sesid As IntPtr, _
    ByVal paramid As Integer, _
    ByVal lParam As Integer, _
    ByVal szParam As String _
) As Integer

'typedef struct {
'  unsigned long cbStruct;
'  JET_TABLEID tableid;
'  unsigned long cRecord;
'  JET_COLUMNID columnidPresentationOrder;
'  JET_COLUMNID columnidcolumnname;
'  JET_COLUMNID columnidcolumnid;
'  JET_COLUMNID columnidcoltyp;
'  JET_COLUMNID columnidCountry;
'  JET_COLUMNID columnidLangid;
'  JET_COLUMNID columnidCp;
'  JET_COLUMNID columnidCollate;
'  JET_COLUMNID columnidcbMax;
'  JET_COLUMNID columnidgrbit;
'  JET_COLUMNID columnidDefault;
'  JET_COLUMNID columnidBaseTableName;
'  JET_COLUMNID columnidBaseColumnName;
'  JET_COLUMNID columnidDefinitionName;
'} JET_COLUMNLIST;
' Note: is not packed (ie Pack:=4)
<StructLayout(LayoutKind.Sequential)> _
Structure JET_COLUMNLIST
    Dim cbStruct As Integer
    Dim tableid As IntPtr
    Dim cRecord As Integer
    Dim columnidPresentationOrder As Integer
    Dim columnidcolumnname As Integer
    Dim columnidcolumnid As Integer
    Dim columnidcoltyp As Integer
    Dim columnidCountry As Integer
    Dim columnidLangid As Integer
    Dim columnidCp As Integer
    Dim columnidCollate As Integer
    Dim columnidcbMax As Integer
    Dim columnidgrbit As Integer
    Dim columnidDefault As Integer
    Dim columnidBaseTableName As Integer
    Dim columnidBaseColumnName As Integer
    Dim columnidDefinitionName As Integer
End Structure

'typedef struct {
'  unsigned long cbStruct;
'  unsigned long ibLongValue;
'  unsigned long itagSequence;
'  JET_COLUMNID columnidNextTagged;
'} JET_RETINFO;
<StructLayout(LayoutKind.Sequential)> _
Structure JET_RETINFO
    Dim cbStruct As Integer
    Dim ibLongValue As Integer
    Dim itagSequence As Integer
    Dim columnidNextTagged As Integer
End Structure

' bunch of constants from essent.h
Const JET_bitDbReadOnly As Integer = 1
Const JET_bitTableReadOnly As Integer = 4
Const JET_ColInfoListSortColumnid As Integer = 7

Const JET_paramErrorToString As Integer = 70
Const JET_paramTempPath As Integer = 1
Const JET_paramLogFilePath As Integer = 2
Const JET_paramSystemPath As Integer = 0
Const JET_paramAccessDeniedRetryPeriod As Integer = 53

Const JET_cbNameMost As Integer = 64
Const JET_wrnColumnNull As Integer = 1004
Const JET_wrnBufferTruncated As Integer = 1006

Const JET_MoveFirst As Integer = &H80000000
Const JET_MoveNext As Integer = 1

Const JET_coltypNil As Integer = 0
Const JET_coltypBit As Integer = 1
Const JET_coltypUnsignedByte As Integer = 2
Const JET_coltypShort As Integer = 3
Const JET_coltypLong As Integer = 4
Const JET_coltypCurrency As Integer = 5
Const JET_coltypIEEESingle As Integer = 6
Const JET_coltypIEEEDouble As Integer = 7
Const JET_coltypDateTime As Integer = 8
Const JET_coltypBinary As Integer = 9
Const JET_coltypText As Integer = 10
Const JET_coltypLongBinary As Integer = 11
Const JET_coltypLongText As Integer = 12
Const JET_coltypSLV As Integer = 13
Const JET_coltypUnsignedLong As Integer = 14 ' New for Vista/2008
Const JET_coltypLongLong As Integer = 15
Const JET_coltypGUID As Integer = 16
Const JET_coltypUnsignedShort As Integer = 17
Const JET_coltypMax As Integer = 18

API Documentation Links

The Example Code

The typical "flow" of opening an ESE table is the followng:

This section shows in detail the flow required to open an ESE table.

Dim inst_id, sess_id, table_id As IntPtr
Dim ret, db_id As Integer

 ' create an instance
ret = JetCreateInstance(inst_id, "MyDemo")
If ret <> 0 Then
    Throw New ApplicationException(JetErrorMessage(ret))
End If

' initialize the instance
ret = JetInit(inst_id)
If ret <>0 Then
    Throw New ApplicationException(JetErrorMessage(ret))
End If

' start the session
ret = JetBeginSession(inst_id, sess_id, Nothing, Nothing)
If ret <> 0 Then
    Throw New ApplicationException(JetErrorMessage(ret))
End If

' attach the database file
ret = JetAttachDatabase(sess_id, DatabaseFile, JET_bitDbReadOnly)
If ret <> 0 Then
    Throw New ApplicationException(JetErrorMessage(ret))
End If

' open the database
ret = JetOpenDatabase(sess_id, DatabaseFile, Nothing, db_id, JET_bitDbReadOnly)
If ret <> 0 Then
    Throw New ApplicationException(JetErrorMessage(ret))
End If

' open the table
ret = JetOpenTable(sess_id, db_id, TableName, 0, 0, JET_bitTableReadOnly, table_id)
If ret <> 0 Then
    Throw New ApplicationException(JetErrorMessage(ret))
End If

' do something here!

 ' close the table
ret = JetCloseTable(sess_id, table_id)
If ret <> 0 Then
    Throw New ApplicationException(JetErrorMessage(ret))
End If

' close the database
ret = JetCloseDatabase(sess_id, db_id, JET_bitDbReadOnly)
If ret <> 0 Then
    Throw New ApplicationException(JetErrorMessage(ret))
End If

' detach the database file
ret = JetDetachDatabase(sess_id, DatabaseFile)
If ret <> 0 Then
    Throw New ApplicationException(JetErrorMessage(ret))
End If

' end the session
ret = JetEndSession(sess_id, 0)
If ret <> 0 Then
    Throw New ApplicationException(JetErrorMessage(ret))
End If

' terminate the instance
ret = JetTerm(inst_id)
If ret <> 0 Then
    Throw New ApplicationException(JetErrorMessage(ret))
End If

The JetErrorMessage is a "home made" function that translate the ESE error code into a text message:

' convert a JET_ERR code into a text message
Private Function JetErrorMessage(ByVal error_code As Integer) As String
    Dim msg As String
    Dim ret As Integer

    msg = Space(256)
    ret = JetGetSystemParameter(IntPtr.Zero, IntPtr.Zero, JET_paramErrorToString, _
     error_code, msg, 256)
    If ret <> 0 Then
        Throw New ApplicationException("Can't get error message for code " & error_code)
    End If
    Return msg
End Function

You read the contents of a Table in a manner similar to using the DataReader function in ADO.Net. However, that means you'll need to know the schema of the table. The technique to get the column names/data types from a table is kinda complicated. You use the JetGetTableColumnInfo function which returns a listing of column IDs that map to known parts of the schema. The actual schema itself is returned in a temporary table. So, now that you know the column IDs to the temp table, you can read the records of the temp table to get the schema of the real table. Whew!

Dim iPtr As IntPtr
Dim ret, req_size As Integer
Dim col_list As JET_COLUMNLIST
Dim ret_info As JET_RETINFO

col_list = Nothing
iPtr = Marshal.AllocHGlobal(Marshal.SizeOf(col_list))
ret = JetGetTableColumnInfo(sess_id, table_id, Nothing, iPtr, Marshal.SizeOf(col_list), _
 JET_ColInfoListSortColumnid)
If ret <> 0 Then
    Throw New ApplicationException(JetErrorMessage(ret))
End If

' do some pointer magic to populate the structure
col_list = CType(Marshal.PtrToStructure(iPtr, GetType(JET_COLUMNLIST)), _
 JET_COLUMNLIST)

' clean up
Marshal.FreeHGlobal(iPtr)

' Set the cursor at the beginning of the temp table. Probably not necessary,
' but what the heck...
ret = JetMove(sess_id, col_list.tableid, JET_MoveFirst, 0)
If ret <>0 Then
    Throw New ApplicationException(JetErrorMessage(ret))
End If

' allocate a buffer for the column names, et al
iPtr = Marshal.AllocHGlobal(JET_cbNameMost)
ret_info.cbStruct = Marshal.SizeOf(ret_info)

' If there are multiple values, we get only the first one
ret_info.itagSequence = 1

'
' Loop thru each record in the temp table get the column name, column ID, and data type
'
Do While ret = 0
    Dim col_name As String
    Dim col_id, col_type As Integer

    ' column name
    ret = JetRetrieveColumn(sess_id, col_list.tableid, col_list.columnidcolumnname, iPtr, _
     JET_cbNameMost, req_size, 0, ret_info)
    If ret <> 0 Then
        Throw New ApplicationException(JetErrorMessage(ret))
    End If
    col_name = Marshal.PtrToStringAnsi(iPtr, req_size)

    ' column data type
    ret = JetRetrieveColumn(sess_id, col_list.tableid, col_list.columnidcoltyp, iPtr, 4, req_size, _
     0, ret_info)
    If ret <> 0 Then
        Throw New ApplicationException(JetErrorMessage(ret))
    End If
    col_type = Marshal.ReadInt32(iPtr)

    ' column ID
    ret = JetRetrieveColumn(sess_id, col_list.tableid, col_list.columnidcolumnid, iPtr, 4, _
     req_size, 0, ret_info)
    If ret <> 0 Then
        Throw New ApplicationException(JetErrorMessage(ret))
    End If
    col_id = Marshal.ReadInt32(iPtr)

    ' do something here!

    ret = JetMove(sess_id, col_list.tableid, JET_MoveNext, 0)
Loop

' Free the memory we allocated and destroy the temporary table that got
' automatically created by the JetGetTableColumnInfo command.
Marshal.FreeHGlobal(iPtr)
JetCloseTable(sess_id, col_list.tableid

Now that you have the column ID and data types of the ESE table, you can use the JetRetrieveColumn function to get the data. For example to read an Integer from a known column ID, you'd use the following:

Dim ret, col_id, req_size, IntValue as Integer

ret = JetRetrieveColumn(sess_id, table_id, col_id, iPtr, 4, req_size, 0, ret_info)
If ret <> 0 And ret <> JET_wrnColumnNull Then
    Throw New ApplicationException(JetErrorMessage(ret))
End If

If ret <>JET_wrnColumnNull Then
    IntValue = Marshal.ReadInt32(iPtr)
End If

End Notes

This example doesn't go into too much detail and doesn't do anything useful. However, the sample source code below is for a full-blown application that will read the contents of the ESE database used by the Microsoft Update service and return the rows in a DataGridView as an ADO.Net DataTable.

Most folks will probably get as much or more out of poking around a working example than reading a bunch of articles. As far as I know, this will be the first source code example of using the ESE database APIs ever published on the net.

Download/Links

Download the complete VB.Net Source code example used in this article: ESE_Demo.zip