Platform SDK: Active Directory, ADSI, and Directory Services

Example Code to Retrieve Changes Using USNChanged

The following sample code uses the uSNChanged attribute of Active Directory objects to retrieve changes that have occurred since a previous query. The code can perform either a full synchronization or an incremental update. For a full synchronization, the sample application connects to the rootDSE of a domain controller and reads the following parameters that it stores to be used in the next incremental synchronization.

The example uses the IDirectorySearch interface, specifying the distinguished name of the base of the search, a search scope, and a filter. There are no restrictions on the search base or scope. In addition to specifying the objects of interest, the filter must also specify a uSNChanged comparison, such as (uSNChanged>=lowerBoundUSN). For a full synchronization, lowerBoundUSN is zero. For an incremental synchronization, it is the 1 plus the highestCommittedUSN value from the previous search.

Note that this sample program is intended only to show how to use uSNChanged to retrieve changes from Active Directory. It simple prints out the changes and does not actually synchronize the data in a secondary storage. Consequently, it does not show how to deal with issues like moved objects or "no parent" conditions. It does show how to retrieve deleted objects, but it doesn't show how an application uses the objectGUID of the deleted objects to determine the corresponding object to delete in the storage.

Also, the sample simply caches the DC name, invocation ID, and higestCommittedUSN in the registry. In a real synchronization application, you must store the parameters in the same storage that you are keeping consistent with Active Directory. This ensures that the parameters and object data remain in sync if your database is ever restored from a backup.

#include <windows.h>
#include <stdio.h>
#include <activeds.h>
#include <ntdsapi.h>

typedef struct {
    WCHAR objectGUID[40];
    WCHAR distinguishedName[MAX_PATH];
    WCHAR phoneNumber[32];
} MyUserData;

// forward declaration
VOID BuildGUIDString(WCHAR *szGUID, LPBYTE pGUID);
VOID WriteObjectDataToStorage(MyUserData *userdata, BOOL bUpdate);
VOID DeleteObjectDataFromStorage(MyUserData *userdata);

//********************************************************************
// DoUSNSyncSearch
//********************************************************************
HRESULT DoUSNSyncSearch(
      LPWSTR szSearchBaseDN,      // Distinguished name of search base
      ULONG ulScope,              // Scope of the search
      LPWSTR *pAttributeNames,    // Attributes to retrieve
      DWORD dwAttributes,         // Number of attributes
      LPWSTR szPrevInvocationID,  // GUID string for DC's invocationID
      LPWSTR szPrevHighUSN,       // Highest USN from previous sync
      LPWSTR szDC)                // Name of DC to bind to
{
LPOLESTR szDSPath = new OLECHAR[MAX_PATH];
LPOLESTR szServerPath = new OLECHAR[MAX_PATH];
 
IADs *pRootDSE = NULL;
IADs *pDCService = NULL;
IADs *pDeletedObj = NULL;

IDirectorySearch *pSearch = NULL;
ADS_SEARCH_HANDLE hSearch = NULL;
ADS_SEARCHPREF_INFO arSearchPrefs[3];
WCHAR szSearchFilter[256];    // Search filter
ADS_SEARCH_COLUMN col;

MyUserData userdata;
void HUGEP *pArray;
WCHAR szGUID[40];
INT64 iLowerBoundUSN;
HRESULT hr;
DWORD dwCount = 0;
VARIANT var;
BOOL bUpdate = TRUE;

// Validate input parameters.
if (!szPrevInvocationID || !szPrevHighUSN || !szDC) {
    wprintf(L"Invalid parameter.\n");
    return E_FAIL;
}

// If we have a DC name from the previous USN sync, 
// include it in the binding string.
if (szDC[0]) {
    wcscpy(szServerPath, L"LDAP://");
    wcscat(szServerPath, szDC);
    wcscat(szServerPath, L"/");
} else
    wcscpy(szServerPath, L"LDAP://");

// Bind to root DSE.
wcscpy(szDSPath, szServerPath);
wcscat(szDSPath, L"rootDSE");
hr = ADsOpenObject(szDSPath,
                 NULL,
                 NULL,
                 ADS_SECURE_AUTHENTICATION, 
                 IID_IADs,
                 (void**)&pRootDSE);
if (FAILED(hr)) {
    wprintf(L"failed to bind to root: 0x%x\n", hr);
    goto cleanup;
}

// Get the name of the DC we connected to.
hr = pRootDSE->Get(L"DnsHostName", &var);
if (FAILED(hr)) {
    wprintf(L"failed to get DnsHostName: 0x%x\n", hr);
    goto cleanup;
}
// Compare it to the DC name from the previous USN sync operation. 
// If they aren't the same, do a full sync.
if (_wcsicmp(szDC, var.bstrVal)!=0)
{
    bUpdate = FALSE;

    // Save the DC name for next time.
    wcscpy(szDC, var.bstrVal);

    // Use the DC name in the bind string prefix.
    wcscpy(szServerPath, L"LDAP://");  
    wcscat(szServerPath, szDC);
    wcscat(szServerPath, L"/");
}

// Bind to the DC's service object to get the invocationID.
// The dsServiceName property of root DSE contains the distinguished 
// name of this DC's service object.
hr = pRootDSE->Get(L"dsServiceName", &var);
wcscpy(szDSPath, szServerPath);
wcscat(szDSPath, var.bstrVal);
VariantClear(&var);
hr = ADsOpenObject(szDSPath,
                 NULL,
                 NULL,
                 ADS_SECURE_AUTHENTICATION, 
                 IID_IADs,
                 (void**)&pDCService);
if (FAILED(hr)) {
    wprintf(L"failed to bind to the DC's service object: 0x%x\n", hr);
    goto cleanup;
}

// Get the invocationID GUID from the service object.
hr = pDCService->Get(L"invocationID",&var);
hr = SafeArrayAccessData((SAFEARRAY*)(var.pparray), (void HUGEP* FAR*)&pArray);
if (FAILED(hr)) {
    wprintf(L"failed to get hugep: 0x%x\n", hr);
    goto cleanup;
}
BuildGUIDString(szGUID, (LPBYTE) pArray);
VariantClear(&var);

// Compare the invocationID GUID to the GUID string from the previous 
// sync. If they are not the same, this is a different DC or the DC 
// was restored from backup, so do a full sync.
if (_wcsicmp(szGUID, szPrevInvocationID)!=0)
{
    bUpdate = FALSE;
    wcscpy(szPrevInvocationID, szGUID);  // Save the invocationID GUID.
}

// If previous high USN is an empty string, treat this as a full sync.
if (szPrevHighUSN[0] == '\0')
    bUpdate = FALSE;

// Set the lower bound USN to zero if this is a full sync. 
// Otherwise, set it to the previous high USN plus one.
if (bUpdate == FALSE)
    iLowerBoundUSN = 0;
else
    iLowerBoundUSN = _wtoi64(szPrevHighUSN) + 1; // Convert string to integer.

// Get and save the current high USN.
hr = pRootDSE->Get(L"highestCommittedUSN", &var);
wcscpy(szPrevHighUSN, var.bstrVal);
wprintf(L"current highestCommittedUSN: %s\n", szPrevHighUSN);
VariantClear(&var);

// Get an IDirectorySearch pointer to the base of the search.
wcscpy(szDSPath, szServerPath);
wcscat(szDSPath, szSearchBaseDN);
hr = ADsOpenObject(szDSPath,
                 NULL,
                 NULL,
                 ADS_SECURE_AUTHENTICATION, 
                 IID_IDirectorySearch,
                 (void**)&pSearch);
if (FAILED(hr)) {
    wprintf(L"failed to get IDirectorySearch: 0x%x\n", hr);
    goto cleanup;
}

// Set up the scope and page size search preferences.
arSearchPrefs [0].dwSearchPref = ADS_SEARCHPREF_SEARCH_SCOPE; 
arSearchPrefs [0].vValue.dwType = ADSTYPE_INTEGER; 
arSearchPrefs [0].vValue.Integer = ulScope; 

arSearchPrefs [1].dwSearchPref = ADS_SEARCHPREF_PAGESIZE;
arSearchPrefs [1].vValue.dwType = ADSTYPE_INTEGER;
arSearchPrefs [1].vValue.Integer = 100;

hr = pSearch->SetSearchPreference(arSearchPrefs, 2);
if (FAILED(hr)) {
    wprintf(L"failed to set search prefs: 0x%x\n", hr);
    goto cleanup;
}

// The search filter specifies the objects to monitor  
// and the USNChanged value to exceed.
swprintf(szSearchFilter, 
         L"(&(objectClass=user)(objectCategory=person)(uSNChanged>=%I64d))", 
         iLowerBoundUSN );

// Search for the objects indicated by the search filter.
hr = pSearch->ExecuteSearch(szSearchFilter,
                    pAttributeNames, dwAttributes, &hSearch );
if (FAILED(hr)) {
    wprintf(L"failed to set execute search: 0x%x\n", hr);
    goto cleanup;
}

// Loop through the rows of the search result. Each row is an object 
// with USNChanged greater than or equal to the specified value.
hr = pSearch->GetNextRow( hSearch);
while ( SUCCEEDED(hr) && hr != S_ADS_NOMORE_ROWS )
{
    ZeroMemory(&userdata, sizeof(MyUserData) );

    // Get the distinguishedName.
    hr = pSearch->GetColumn( hSearch, L"distinguishedName", &col );
    if ( SUCCEEDED(hr) ) {
        wcscpy(userdata.distinguishedName, col.pADsValues->CaseIgnoreString);
        pSearch->FreeColumn( &col );
    }

    // Get the telephone number.
    hr = pSearch->GetColumn( hSearch, L"telephoneNumber", &col );
    if ( SUCCEEDED(hr) ) {
        wcscpy(userdata.phoneNumber, col.pADsValues->CaseIgnoreString);
        pSearch->FreeColumn( &col );
    }

    // Get the objectGUID.
    hr = pSearch->GetColumn( hSearch, L"objectGUID", &col );
    if ( SUCCEEDED(hr) ) {
        if (col.pADsValues->OctetString.lpValue) {
            BuildGUIDString(szGUID, (LPBYTE) col.pADsValues->OctetString.lpValue);
            wcscpy(userdata.objectGUID, szGUID);
        }
        pSearch->FreeColumn( &col );
    }

    // Write the data from Active Directory to the secondary storage.
    WriteObjectDataToStorage(&userdata, bUpdate);
    dwCount++;
    hr = pSearch->GetNextRow( hSearch);
}
wprintf(L"dwCount: %d\n", dwCount);

// If this is a full sync, we're done.
if (!bUpdate) 
    goto cleanup;

// If it's an update, we need to look for deleted objects.

// Release the search handle and pointer so we can reuse them.
wprintf(L"Searching for deleted objects\n");
if (hSearch) {
    pSearch->CloseSearchHandle(hSearch);
    hSearch = NULL;
}
if (pSearch) {
    pSearch->Release();
    pSearch = NULL;
}

// Bind to the Deleted Objects container.
hr = pRootDSE->Get(L"defaultNamingContext",&var);
swprintf(szDSPath, 
         L"%s<WKGUID=%s,%s>", 
         szServerPath, GUID_DELETED_OBJECTS_CONTAINER_W, var.bstrVal);
VariantClear(&var);
hr = ADsOpenObject(szDSPath,
                 NULL,
                 NULL,
                 ADS_SECURE_AUTHENTICATION | ADS_FAST_BIND, 
                 IID_IDirectorySearch,
                 (void**)&pSearch);
if (FAILED(hr)) {
    wprintf(L"failed to get IDirectorySearch: 0x%x\n", hr);
    goto cleanup;
}

// Specify the scope, pagesize, and tombstone search preferences.
arSearchPrefs [0].dwSearchPref = ADS_SEARCHPREF_SEARCH_SCOPE; 
arSearchPrefs [0].vValue.dwType = ADSTYPE_INTEGER; 
arSearchPrefs [0].vValue.Integer = ADS_SCOPE_SUBTREE; 

arSearchPrefs [1].dwSearchPref = ADS_SEARCHPREF_PAGESIZE;
arSearchPrefs [1].vValue.dwType = ADSTYPE_INTEGER;
arSearchPrefs [1].vValue.Integer = 100;

arSearchPrefs [2].dwSearchPref = ADS_SEARCHPREF_TOMBSTONE;
arSearchPrefs [2].vValue.dwType = ADSTYPE_BOOLEAN;
arSearchPrefs [2].vValue.Boolean = TRUE;

hr = pSearch->SetSearchPreference(arSearchPrefs, 3);
if (FAILED(hr)) {
    wprintf(L"failed to set search prefs: 0x%x\n", hr);
    goto cleanup;
}

// Set up the search filter.
swprintf(szSearchFilter, 
         L"(&(isDeleted=TRUE)(uSNChanged>=%I64d))", 
         iLowerBoundUSN );

// Execute the search.
hr = pSearch->ExecuteSearch(szSearchFilter,
                    pAttributeNames, dwAttributes, &hSearch );
if (FAILED(hr)) {
    wprintf(L"failed to set execute search: 0x%x\n", hr);
    goto cleanup;
}
wprintf(L"Started search for deleted objects.\n");

// Loop through the rows of the search result.
// Each row is an object that was deleted since the previous call.
dwCount = 0;
hr = pSearch->GetNextRow( hSearch);
while ( SUCCEEDED(hr) && hr != S_ADS_NOMORE_ROWS )
{
    ZeroMemory(&userdata, sizeof(MyUserData) );

    // Get the distinguishedName.
    hr = pSearch->GetColumn( hSearch, L"distinguishedName", &col );
    if ( SUCCEEDED(hr) ) {
        wcscpy(userdata.distinguishedName, col.pADsValues->CaseIgnoreString);
        pSearch->FreeColumn( &col );
    }

    // Get the objectGUID number.
    hr = pSearch->GetColumn( hSearch, L"objectGUID", &col );
    if ( SUCCEEDED(hr) ) {
        if (col.pADsValues->OctetString.lpValue) {
            BuildGUIDString(szGUID, (LPBYTE) col.pADsValues->OctetString.lpValue);
            wcscpy(userdata.objectGUID, szGUID);
        }
        pSearch->FreeColumn( &col );
    }

    // If the objectGUID of a deleted object matches an objectGUID in 
    // our secondary storage, delete the object from our storage.
    DeleteObjectDataFromStorage(&userdata);
    dwCount++;
    hr = pSearch->GetNextRow( hSearch);
}
wprintf(L"deleted dwCount: %d\n", dwCount);

cleanup:

if (pRootDSE)
    pRootDSE->Release();
if (pDCService)
    pDCService->Release();
if (pDeletedObj)
    pDeletedObj->Release();
if (pSearch)
    pSearch->Release();
if (hSearch)
    pSearch->CloseSearchHandle(hSearch);
VariantClear(&var);

return hr;

}

//********************************************************************
// DeleteObjectDataFromStorage routine
//********************************************************************
VOID DeleteObjectDataFromStorage(MyUserData *userdata)
{
wprintf(L"DELETED OBJECT:\n");
wprintf(L"   objectGUID: %s\n", userdata->objectGUID);
wprintf(L"   distinguishedName: %s\n", userdata->distinguishedName);
wprintf(L"---------------------------------------------\n");
return;
}

//********************************************************************
// WriteObjectDataToStorage routine
//********************************************************************
VOID WriteObjectDataToStorage(MyUserData *userdata, BOOL bUpdate)
{
if (bUpdate)
    wprintf(L"UPDATE:\n");
else
    wprintf(L"INITIAL DATA:\n");
wprintf(L"   objectGUID: %s\n", userdata->objectGUID);
wprintf(L"   distinguishedName: %s\n", userdata->distinguishedName);
wprintf(L"   phoneNumber: %s\n", userdata->phoneNumber);
wprintf(L"---------------------------------------------\n");
return;
}

//********************************************************************
// WriteSyncParamsToStorage routine
// This example caches the parameters in the registry. In a real 
// synchronization application, you must store the parameters in the
// same storage that you are keeping consistent with Active Directory.
// This ensures that the parameters and object data remain in sync if 
// the storage is ever restored from a backup.
//********************************************************************
DWORD WriteSyncParamsToStorage(
            LPWSTR szPrevInvocationID, // Receives invocation ID
            LPWSTR szPrevHighUSN,      // Receives previous high USN
            LPWSTR pszDCName)          // Receives name of DC to bind to
{
HKEY hReg = NULL;
DWORD dwStat = NO_ERROR;

// Create a registry key under 
//     HKEY_CURRENT_USER\SOFTWARE\Vendor\Product.
dwStat = RegCreateKeyExW(HKEY_CURRENT_USER,
            L"Software\\Microsoft\\Windows 2000 AD-Synchro-USN",
            0,
            NULL,
            REG_OPTION_NON_VOLATILE,
            KEY_ALL_ACCESS,
            NULL,
            &hReg,
            NULL);
if (dwStat != NO_ERROR) {
    wprintf(L"RegCreateKeyEx failed: 0x%x\n", dwStat);
    return dwStat;
}

// Cache the invocationID as a value under the registry key.
dwStat = RegSetValueExW(hReg, L"InvocationID", 0, REG_SZ,
                           (const BYTE *)szPrevInvocationID, 
                           2*(wcslen(szPrevInvocationID)));
if (dwStat != NO_ERROR)
    wprintf(L"RegSetValueEx for invocationID failed: 0x%x\n", dwStat);

// Cache the previous high USN as a value under the registry key.
dwStat = RegSetValueExW(hReg, L"PreviousHighUSN", 0, REG_QWORD,
                           (const BYTE *)szPrevHighUSN, 
                           2*(wcslen(szPrevHighUSN)) );//sizeof(INT64) );
if (dwStat != NO_ERROR)
    wprintf(L"RegSetValueEx for PreviousHighUSN failed: 0x%x\n", dwStat);

// Cache the DC name as a value under the registry key.
dwStat = RegSetValueExW(hReg, L"DC name", 0, REG_SZ,
                           (const BYTE *)pszDCName, 2*(wcslen(pszDCName)) );
if (dwStat != NO_ERROR)
    wprintf(L"RegSetValueEx for DC name failed: 0x%x\n", dwStat);

RegCloseKey(hReg);
return dwStat;
}

//********************************************************************
// GetSyncParamsFromStorage routine
// This example reads the parameters from the registry. In a real 
// synchronization application, you must store the parameters in the
// same storage that you are keeping consistent with Active Directory.
//********************************************************************
DWORD GetSyncParamsFromStorage(
            LPWSTR szPrevInvocationID, // Receives invocation ID
            LPWSTR szPreviousHighUSN,  // Receives previous high USN
            LPWSTR pszDCName)          // Receives name of DC to bind to
{
HKEY hReg = NULL;
DWORD dwStat;
DWORD dwLen;

// Open the registry key.
dwStat = RegOpenKeyExW(
            HKEY_CURRENT_USER,
            L"Software\\Microsoft\\Windows 2000 AD-Synchro-USN",
            0,
            KEY_QUERY_VALUE,
            &hReg);
if (dwStat != NO_ERROR) {
    wprintf(L"RegOpenKeyEx failed: 0x%x\n", dwStat);
    return dwStat;
}

// Get the previous invocationID from the registry.
dwLen = 40*2; // size of buffer
dwStat = RegQueryValueExW(hReg, L"InvocationID", NULL, NULL, 
                          (LPBYTE)szPrevInvocationID, &dwLen );
if (dwStat != NO_ERROR) {
    wprintf(L"RegQueryValueEx failed to get invocationID: 0x%x\n", dwStat);
    goto cleanup;
}

// Now get the previous high USN from the registry.
dwLen = 40*2;
dwStat = RegQueryValueExW(hReg, L"PreviousHighUSN", NULL, NULL, 
                         (LPBYTE)szPreviousHighUSN, &dwLen );
if (dwStat != NO_ERROR) {
    wprintf(L"RegQueryValueEx failed to get previous high USN: 0x%x\n", dwStat);
    goto cleanup;
}

// Get the DC name from the registry.
dwLen = MAX_PATH*2;
dwStat = RegQueryValueExW(hReg, L"DC name", NULL, NULL, 
                         (LPBYTE)pszDCName, &dwLen );
if (dwStat != NO_ERROR) {
    wprintf(L"RegQueryValueEx failed to get DC name: 0x%x\n", dwStat);
    goto cleanup;
}

cleanup:

RegCloseKey(hReg);
return dwStat;
}

//********************************************************************
// BuildGUIDString
// Routine that makes the GUID a string in directory service bind form.
//********************************************************************
VOID 
BuildGUIDString(WCHAR *szGUID, LPBYTE pGUID)
{
    DWORD i = 0x0;
    DWORD dwlen = sizeof(GUID);
    WCHAR buf[4];

    wcscpy(szGUID, L"");
    for (i;i<dwlen;i++) {
        wsprintf(buf, L"%02x", pGUID[i]);
        wcscat(szGUID, buf);
    }
}

//********************************************************************
// main
//********************************************************************
int main(int argc, char* argv[])
{
DWORD dwStat;
HRESULT hr;

// attributes to retrieve
LPWSTR szAttribs[] = { 
    {L"telephoneNumber"},
    {L"distinguishedName"},
    {L"uSNChanged"},
    {L"objectGUID"},
    {L"isDeleted"}
};
LPWSTR *pszAttribs=szAttribs;
DWORD dwAttribs = sizeof(szAttribs)/sizeof(LPWSTR);

// DC properties to cache for next synchronization run.
WCHAR szPrevInvocationID[40];
WCHAR szPrevHighUSN[40];
WCHAR szDCName[MAX_PATH];

CoInitialize(NULL);

if (argc>1) 
{
    // Perform a full synchronization.
    // Initialize synchronization parameters to empty strings.
    wprintf(L"Performing a full read.\n");
    szPrevInvocationID[0] = '\0';
    szPrevHighUSN[0] = '\0';
    szDCName[0] = '\0';
} else
{
    // Perform a synchronization update.
    // Initialize synchronization parameters from storage.
    wprintf(L"Retrieving changes only.\n");
    dwStat = GetSyncParamsFromStorage(szPrevInvocationID, 
                                      szPrevHighUSN, 
                                      szDCName);
    if (dwStat != NO_ERROR) {
        wprintf(L"Could not get synchronization parameters: %u\n", dwStat);
        goto cleanup;
    }
}

// Perform the search and update the synchronization parameters.
hr = DoUSNSyncSearch(L"CN=Users,DC=twokay,DC=local", 
                     ADS_SCOPE_ONELEVEL,
                     pszAttribs, dwAttribs, 
                     szPrevInvocationID, szPrevHighUSN, szDCName);
if (FAILED(hr)) {
    wprintf(L"DoUSNSyncSearch failed: 0x%x\n", hr);
    goto cleanup;
}

// Cache the synchronization parameters in storage for next time.
wprintf(L"Caching the synchronization parameters.\n");
dwStat = WriteSyncParamsToStorage(
            szPrevInvocationID, szPrevHighUSN, szDCName);
if (dwStat != NO_ERROR) {
    wprintf(L"Error caching the synchronization params: %u\n", dwStat);
    goto cleanup;
}

cleanup:
CoUninitialize();
return 1;
}