Registering change notification with Active Directory using C#

Sam picture Sam · Jan 4, 2010 · Viewed 13.3k times · Source

This link http://msdn.microsoft.com/en-us/library/aa772153(VS.85).aspx says:

You can register up to five notification requests on a single LDAP connection. You must have a dedicated thread that waits for the notifications and processes them quickly. When you call the ldap_search_ext function to register a notification request, the function returns a message identifier that identifies that request. You then use the ldap_result function to wait for change notifications. When a change occurs, the server sends you an LDAP message that contains the message identifier for the notification request that generated the notification. This causes the ldap_result function to return with search results that identify the object that changed.

I cannot find a similar behavior looking through the .NET documentation. If anyone knows how to do this in C# I'd be very grateful to know. I'm looking to see when attributes change on all the users in the system so I can perform custom actions depending on what changed.

I've looked through stackoverflow and other sources with no luck.

Thanks.

Answer

stuartd picture stuartd · Jan 4, 2010

I'm not sure it does what you need, but have a look at http://dunnry.com/blog/ImplementingChangeNotificationsInNET.aspx

Edit: Added text and code from the article:



There are three ways of figuring out things that have changed in Active Directory (or ADAM).  These have been documented for some time over at MSDN in the aptly titled "Overview of Change Tracking Techniques".  In summary:

  1. Polling for Changes using uSNChanged. This technique checks the 'highestCommittedUSN' value to start and then performs searches for 'uSNChanged' values that are higher subsequently.  The 'uSNChanged' attribute is not replicated between domain controllers, so you must go back to the same domain controller each time for consistency.  Essentially, you perform a search looking for the highest 'uSNChanged' value + 1 and then read in the results tracking them in any way you wish.
    • Benefits
      • This is the most compatible way.  All languages and all versions of .NET support this way since it is a simple search.
    • Disadvantages
      • There is a lot here for the developer to take care of.  You get the entire object back, and you must determine what has changed on the object (and if you care about that change).
      • Dealing with deleted objects is a pain.
      • This is a polling technique, so it is only as real-time as how often you query.  This can be a good thing depending on the application. Note, intermediate values are not tracked here either.
  2. Polling for Changes Using the DirSync Control.  This technique uses the ADS_SEARCHPREF_DIRSYNC option in ADSI and the LDAP_SERVER_DIRSYNC_OID control under the covers.  Simply make an initial search, store the cookie, and then later search again and send the cookie.  It will return only the objects that have changed.
    • Benefits
      • This is an easy model to follow.  Both System.DirectoryServices and System.DirectoryServices.Protocols support this option.
      • Filtering can reduce what you need to bother with.  As an example, if my initial search is for all users "(objectClass=user)", I can subsequently filter on polling with "(sn=dunn)" and only get back the combination of both filters, instead of having to deal with everything from the intial filter.
      • Windows 2003+ option removes the administrative limitation for using this option (object security).
      • Windows 2003+ option will also give you the ability to return only the incremental values that have changed in large multi-valued attributes.  This is a really nice feature.
      • Deals well with deleted objects.
    • Disadvantages
      • This is .NET 2.0+ or later only option.  Users of .NET 1.1 will need to use uSNChanged Tracking.  Scripting languages cannot use this method.
      • You can only scope the search to a partition.  If you want to track only a particular OU or object, you must sort out those results yourself later.
      • Using this with non-Windows 2003 mode domains comes with the restriction that you must have replication get changes permissions (default only admin) to use.
      • This is a polling technique.  It does not track intermediate values either.  So, if an object you want to track changes between the searches multiple times, you will only get the last change.  This can be an advantage depending on the application.
  3. Change Notifications in Active Directory.  This technique registers a search on a separate thread that will receive notifications when any object changes that matches the filter.  You can register up to 5 notifications per async connection.
    • Benefits
      • Instant notification.  The other techniques require polling.
      • Because this is a notification, you will get all changes, even the intermediate ones that would have been lost in the other two techniques.
    • Disadvantages
      • Relatively resource intensive.  You don't want to do a whole ton of these as it could cause scalability issues with your controller.
      • This only tells you if the object has changed, but it does not tell you what the change was.  You need to figure out if the attribute you care about has changed or not.  That being said, it is pretty easy to tell if the object has been deleted (easier than uSNChanged polling at least).
      • You can only do this in unmanaged code or with System.DirectoryServices.Protocols.

For the most part, I have found that DirSync has fit the bill for me in virtually every situation.  I never bothered to try any of the other techniques.  However, a reader asked if there was a way to do the change notifications in .NET.  I figured it was possible using SDS.P, but had never tried it.  Turns out, it is possible and actually not too hard to do.

My first thought on writing this was to use the sample code found on MSDN (and referenced from option #3) and simply convert this to System.DirectoryServices.Protocols.  This turned out to be a dead end.  The way you do it in SDS.P and the way the sample code works are different enough that it is of no help.  Here is the solution I came up with:

public class ChangeNotifier : IDisposable
{
    LdapConnection _connection;
    HashSet<IAsyncResult> _results = new HashSet<IAsyncResult>();

    public ChangeNotifier(LdapConnection connection)
    {
        _connection = connection;
        _connection.AutoBind = true;
    }

    public void Register(string dn, SearchScope scope)
    {
        SearchRequest request = new SearchRequest(
            dn, //root the search here
            "(objectClass=*)", //very inclusive
            scope, //any scope works
            null //we are interested in all attributes
            );

        //register our search
        request.Controls.Add(new DirectoryNotificationControl());

        //we will send this async and register our callback
        //note how we would like to have partial results

        IAsyncResult result = _connection.BeginSendRequest(
            request,
            TimeSpan.FromDays(1), //set timeout to a day...
            PartialResultProcessing.ReturnPartialResultsAndNotifyCallback,
            Notify,
            request);

        //store the hash for disposal later

        _results.Add(result);
    }

    private void Notify(IAsyncResult result)
    {
        //since our search is long running, we don't want to use EndSendRequest
        PartialResultsCollection prc = _connection.GetPartialResults(result);

        foreach (SearchResultEntry entry in prc)
        {
            OnObjectChanged(new ObjectChangedEventArgs(entry));
        }
    }

    private void OnObjectChanged(ObjectChangedEventArgs args)
    {
        if (ObjectChanged != null)
        {
            ObjectChanged(this, args);
        }
    }

    public event EventHandler<ObjectChangedEventArgs> ObjectChanged;

    #region IDisposable Members

    public void Dispose()
    {
        foreach (var result in _results)
        {
            //end each async search
            _connection.Abort(result);

       }
    }

    #endregion
}


public class ObjectChangedEventArgs : EventArgs
{
    public ObjectChangedEventArgs(SearchResultEntry entry)
    {
        Result = entry;
    }

    public SearchResultEntry Result { get; set;}
}

It is a relatively simple class that you can use to register searches. The trick is using the GetPartialResults method in the callback method to get only the change that has just occurred. I have also included the very simplified EventArgs class I am using to pass results back. Note, I am not doing anything about threading here and I don't have any error handling (this is just a sample). You can consume this class like so:

static void Main(string[] args)
{
    using (LdapConnection connect = CreateConnection("localhost"))
    {
        using (ChangeNotifier notifier = new ChangeNotifier(connect))
        {
            //register some objects for notifications (limit 5)
            notifier.Register("dc=dunnry,dc=net", SearchScope.OneLevel);
            notifier.Register("cn=testuser1,ou=users,dc=dunnry,dc=net", SearchScope.Base);

            notifier.ObjectChanged += new EventHandler<ObjectChangedEventArgs>(notifier_ObjectChanged);

            Console.WriteLine("Waiting for changes...");
            Console.WriteLine();
            Console.ReadLine();
        }
    }
}


static void notifier_ObjectChanged(object sender, ObjectChangedEventArgs e)
{
    Console.WriteLine(e.Result.DistinguishedName);

    foreach (string attrib in e.Result.Attributes.AttributeNames)
    {
        foreach (var item in e.Result.Attributes[attrib].GetValues(typeof(string)))
        {
            Console.WriteLine("\t{0}: {1}", attrib, item);
        }
    }
    Console.WriteLine();
    Console.WriteLine("====================");
    Console.WriteLine();
}