Directory Services

Example Code for Receiving Change Notifications

The following code example demonstrates how to use the LDAP change notification control to receive notifications of changes to an object in Active Directory. The example registers for notifications, reads the initial state of the object, and then uses a loop to wait for and process changes to the object.

First, the code example calls the ldap_search_ext function, which is an asynchronous search operation that returns after registering a notification request. After setting up the notification request, the example calls the ldap_search_s function, which is a synchronous search operation that reads the current state of the object. Finally, the example, uses a loop that calls ldap_result to wait for results of the asynchronous search operation. When ldap_result returns, the example processes the search results and repeats the loop.

Be aware that if the example read the current state and then set up the notification request, there would be a window of time during which changes could occur before the notification request was registered. By reading the object after setting up the notification request, the window works in reverse–you could receive notifications of changes that occurred before reading the initial state. To handle this, the example caches the object's uSNChanged value when it reads the object's initial state. Then, when ldap_result returns with a change notification, the example compares the cached uSNChanged value with the value reported by ldap_result. If the new uSNChanged value is less than or equal to the cached value, the example discards the results because they indicate a change that occurred prior to the initial read operation.

This example performs a base level search that monitors a single object. You could specify the LDAP_SCOPE_ONELEVEL scope to monitor all child object's of the specified object. You could also modify the code to register up to five notification requests and then use ldap_result to wait for notifications from any of the requests. Remember that change notification requests impact the performance of the server, so you should limit your use as described in Change Notifications in Active Directory.

#include <windows.h>
#include <winldap.h>
#include <ntldap.h>
#include <stdio.h>
#include <rpcdce.h>
 
// Forward declarations.
VOID BuildGUIDString(WCHAR *szGUID, LPBYTE pGUID);
BOOL ProcessResult(LDAP *ldapConnection, LDAPMessage *message, __int64 *piUSNChanged );
 
//********************************************************************
// GetChangeNotifications
// Binds to an LDAP server, registers for change notifications, 
// retrieves the current state, and then goes into a loop that 
// waits for and processes change notifications.
//********************************************************************
INT GetChangeNotifications(
	LPWSTR szSearchBaseDN)  // Distinguished name of object to monitor
{
INT err, n=0;
BOOL bSuccess;
ULONG version = LDAP_VERSION3;
LDAP *ldapConnection = NULL;
 
LDAPControl simpleControl;
PLDAPControl controlArray[2];
 
ULONG ulScope = LDAP_SCOPE_BASE;
LONG msgId;
LDAPMessage *results = NULL;
LDAPMessage *message = NULL;
 
// Attributes to retrieve.
LPWSTR szAttribs[]={
		{L"telephoneNumber"},
		{L"isDeleted"},
		{L"objectGUID"},
		{L"uSNChanged"},
		NULL
};
 
// Stores the latest USNChanged value for the object.
__int64 iUSNChanged = 0;
 
// Connect to the default LDAP server.
ldapConnection = ldap_open( NULL, 0 );
if ( ldapConnection == NULL ) {
	wprintf( L"ldap_open failed to connect. Error: 0x%x.\n", GetLastError() );
	goto FatalExit0;
}
wprintf( L"Connected to server.\n");
 
// Specify LDAP version 3.
ldapConnection->ld_lberoptions = 0;
ldap_set_option( ldapConnection, LDAP_OPT_VERSION, &version );
 
// Bind to the server using default credentials.
err = ldap_bind_s( ldapConnection, NULL, NULL, LDAP_AUTH_NEGOTIATE );
if (LDAP_SUCCESS != err) {
	wprintf(L"Bind failed: 0x%x\n", err);
	goto FatalExit0;
}
wprintf( L"Successful bind.\n");
 
// Set up the change notification control.
simpleControl.ldctl_oid = LDAP_SERVER_NOTIFICATION_OID_W;
simpleControl.ldctl_iscritical = TRUE;
simpleControl.ldctl_value.bv_len = 0;
simpleControl.ldctl_value.bv_val = NULL;
controlArray[0] = &simpleControl;
controlArray[1] = NULL;
 
// Start a persistent asynchronous search.
err   = ldap_search_ext( ldapConnection,
					 (PWCHAR) szSearchBaseDN,
					 ulScope,
					 L"ObjectClass=*",
					 szAttribs,  // Attributes to retrieve.
					 0, 		 // Retrieve attributes and values.
					 (PLDAPControl *) &controlArray,
					 NULL, 	// Client controls.
					 0, 		 // Timeout.
					 0, 		 // Sizelimit.
					 (PULONG)&msgId // Receives identifier for results.
					);
if (LDAP_SUCCESS != err) {
	wprintf( L" The asynch search failed. Error: 0x%x \n", err );
	goto FatalExit0;
}
wprintf( L"Registered for change notifications on %s.\n", szSearchBaseDN);
wprintf( L"Message identifier is %d.\n", msgId); 
 
// After starting the persistent search, perform a synchronous search 
// to retrieve the current state of the object monitored.
err = ldap_search_s( ldapConnection,
					 (PWCHAR) szSearchBaseDN,
					 ulScope,
					 L"ObjectClass=*",
					 szAttribs,  // List of attributes to retrieve.
					 0, 		 // Retrieve attributes and values.
					 &results);  // Receives the search results.
if (LDAP_SUCCESS != err) {
	wprintf(L"ldap_search_s error: 0x%x\n", err);
	goto FatalExit0;
}
wprintf( L"\nGot current state\n");
 
// Process the search results.
message = ldap_first_entry( ldapConnection, results );
while (message != NULL) 
{
	bSuccess = ProcessResult(ldapConnection, message, &iUSNChanged );
	message = ldap_next_entry( ldapConnection, message );
}
ldap_msgfree( results );
 
// Wait for a notification, process the results, 
// then loop back to wait for the next notification.
wprintf( L"Waiting for change notifications...\n" );
while (n<3) 
{
	// Wait for the results of the asynchronous search.
	results = NULL;
	err = ldap_result(
		ldapConnection, 
		LDAP_RES_ANY,   // Message identifier.
		LDAP_MSG_ONE,   // Retrieve one message at a time.
		NULL, 	 // No timeout.
		&results); // Receives the search results.
	if ((err == (ULONG) -1) || (results) == NULL) {
		wprintf(L"ldap_result error: 0x%x\n", ldapConnection->ld_errno);
		break;
}
	wprintf( L"\nGot a notification. Message ID: %d\n", results->lm_msgid);
 
	// Process the search results.
	message = ldap_first_entry( ldapConnection, results );
	while (message != NULL) 
	{
		bSuccess = ProcessResult(ldapConnection, message, &iUSNChanged );
		message = ldap_next_entry( ldapConnection, message );
}
	ldap_msgfree( results );
	n++;
}
 
FatalExit0:
if (ldapConnection)
	ldap_unbind( ldapConnection );
if (results)
	ldap_msgfree( results );
return 0;
}
 
//********************************************************************
// BuildGUIDString
// Routine that makes the GUID a string in directory service bind form.
//********************************************************************
VOID 
BuildGUIDString(WCHAR *szGUID, LPBYTE pGUID)
{
	DWORD i = 0;
	DWORD dwlen = sizeof(GUID);
	WCHAR buf[4];
 
	wcscpy(szGUID, L"");
 
	for (i;i<dwlen;i++) {
		wsprintf(buf, L"%02x", pGUID[i]);
		wcscat(szGUID, buf);
}
}
 
//********************************************************************
// ProcessResult
// Routine that processes the search results.
//********************************************************************
BOOL ProcessResult(LDAP *ldapConnection,   // Connection handle.
				 LDAPMessage *message,   // Result entry to process.
				 __int64 *piUSNChanged ) // Latest USNChanged value.
{
PWCHAR *value = NULL;
__int64 iNewUSNChanged;
 
PWCHAR dn = NULL, attribute = NULL;
BerElement *opaque = NULL;
berval **pbvGUID=NULL;
WCHAR szGUID[40]; // String version of the objectGUID attribute.
ULONG count, total;
 
// First, get the uSNChanged attribute to determine whether this 
// result is new data. If this uSNChanged value is less than 
// the previous one, the result contains out-of-date data, so 
// discard it.
value = ldap_get_values(ldapConnection, message, L"uSNChanged");
if (!value) {
	wprintf(L"ldap_get_values error\n");
	return FALSE;
}
iNewUSNChanged = _wtoi64(value[0]);  // Convert string to integer.
if (iNewUSNChanged <= *piUSNChanged)
{
	wprintf( L"Discarding outdated search results.\n");
	ldap_value_free( value );
	return TRUE;
} else
{
	*piUSNChanged = iNewUSNChanged; 
	ldap_value_free( value );
}
 
// The search results are newer than the previous state, so process 
// the results. First, print the distinguished name of the object.
dn = ldap_get_dn( ldapConnection, message );
if (!dn) {
	wprintf(L"ldap_get_dn error\n");
	return FALSE;
}
wprintf( L"	Distinguished Name is : %s\n", dn );
ldap_memfree(dn);
 
// Then loop through the attributes and display the new values.
attribute = ldap_first_attribute( ldapConnection, message, &opaque );
while (attribute != NULL) 
{
	// Handle objectGUID as a binary value.
	if (_wcsicmp(L"objectGUID", attribute)==0)
	{
		wprintf(L"	%s: ", attribute);
		pbvGUID = ldap_get_values_len (ldapConnection, message, attribute);
		if (pbvGUID) 
		{
			BuildGUIDString(szGUID, (LPBYTE) pbvGUID[0]->bv_val);
			wprintf(L"%s\n", szGUID);
	}
		ldap_value_free_len( pbvGUID );
} else 
	{
		// Handle other attributes as string values.
		value = ldap_get_values(ldapConnection, message, attribute);
		wprintf( L"	%s: ", attribute );
		if (total = ldap_count_values(value) > 1) {
			for (count = 0; count < total; count++ )
				wprintf( L"		%s\n", value[count] );
	} else 
			wprintf( L"%s\n", value[0] );
		ldap_value_free( value );
}
	ldap_memfree(attribute);
	attribute = ldap_next_attribute(ldapConnection, message, opaque);
} 
 
return TRUE;
}
 
//********************************************************************
// wmain
//********************************************************************
int wmain( int   cArgs, WCHAR  *pArgs[] )
{
PWCHAR szSearchBaseDN = NULL;
 
wprintf( L"\n" );
 
if (cArgs < 2)
	wprintf(L"Usage: getchanges <distinguished name of search base>\n");
 
szSearchBaseDN = (PWCHAR) pArgs[1];
 
return GetChangeNotifications(szSearchBaseDN);
 
}