Thursday, 05 June 2008

Paged Asynchronous LDAP Searches Revisited

A member in the book's forum mentioned some code I had originally posted here in the blog for asynchronous, paged searches in System.DirectoryServices.Protocols (SDS.P).  He questioned whether or not it was thread safe.  I honestly don't know - it might not be as I didn't test it extensively.

Regardless, I had actually moved on from that code and started using anonymous delegates for callbacks instead of events.  I liked this pattern a bit better because it also got rid of the shared resources.

After reading Stephen Toub's article on asynchronous stream processing, I learned about the AsyncOperationManager which was something I was missing in my implementation.  I have been doing a lot lately with .NET 3.5, LINQ, and lambda expressions, so I also decided to rewrite the anonymous delegates to lambda expressions.  That is not as big a change, but it is more concise.

I actively investigated using async iterators, but ultimately I decided closures seemed to be more intuitive for me.  I might revisit this at some time and change my mind.  Here is my outcome:

public class AsyncSearcher
{
    LdapConnection _connect;

    public AsyncSearcher(LdapConnection connection)
    {
        this._connect = connection;
        this._connect.AutoBind = true; //will bind on first search
    }

    public void BeginPagedSearch(
            string baseDN,
            string filter,
            string[] attribs,
            int pageSize,
            Action<SearchResponse> page,
            Action<Exception> completed                
            )
    {
        if (page == null)
            throw new ArgumentNullException("page");

        AsyncOperation asyncOp = AsyncOperationManager.CreateOperation(null);

        Action<Exception> done = e =>
            {
                if (completed != null) asyncOp.Post(delegate
                {
                    completed(e);
                }, null);
            };

        SearchRequest request = new SearchRequest(
            baseDN,
            filter,
            System.DirectoryServices.Protocols.SearchScope.Subtree,
            attribs
            );

        PageResultRequestControl prc = new PageResultRequestControl(pageSize);

        //add the paging control
        request.Controls.Add(prc);

        AsyncCallback rc = null;

        rc = readResult =>
            {
                try
                {
                    var response = (SearchResponse)_connect.EndSendRequest(readResult);
                    
                    //let current thread handle results
                    asyncOp.Post(delegate
                    {
                        page(response);
                    }, null);

                    var cookie = response.Controls
                        .Where(c => c is PageResultResponseControl)
                        .Select(s => ((PageResultResponseControl)s).Cookie)
                        .Single();

                    if (cookie != null && cookie.Length != 0)
                    {
                        prc.Cookie = cookie;
                        _connect.BeginSendRequest(
                            request,
                            PartialResultProcessing.NoPartialResultSupport,
                            rc,
                            null
                            );
                    }
                    else done(null); //signal complete
                }
                catch (Exception ex) { done(ex); }
            };


        //kick off async
        try
        {
            _connect.BeginSendRequest(
                request,
                PartialResultProcessing.NoPartialResultSupport,
                rc,
                null
                );
        }
        catch (Exception ex) { done(ex); }
    }

}

It can be consumed very easily using something like this:

class Program
{
    static ManualResetEvent _resetEvent = new ManualResetEvent(false);
    
     static void Main(string[] args)
    {
        //set these to your environment
        string servername = "server.yourdomain.com";
        string baseDN = "dc=yourdomain,dc=com";

        using (LdapConnection connection = CreateConnection(servername))
        {
            AsyncSearcher searcher = new AsyncSearcher(connection);

            searcher.BeginPagedSearch(
                baseDN,
                "(sn=Dunn)",
                null,
                100,
                f => //runs per page
                {
                    foreach (var item in f.Entries)
                    {
                        var entry = item as SearchResultEntry;

                        if (entry != null)
                        {
                            Console.WriteLine(entry.DistinguishedName);
                        }
                    }

                },
                c => //runs on error or when done
                {
                    if (c != null) Console.WriteLine(c.ToString());
                    Console.WriteLine("Done");
                    _resetEvent.Set();
                }
            );

            _resetEvent.WaitOne();
            
        }

        Console.WriteLine();
        Console.WriteLine("Finished.... Press Enter to Continue.");
        Console.ReadLine();
    }

    static LdapConnection CreateConnection(string server)
    {
        LdapConnection connect = new LdapConnection(
            new LdapDirectoryIdentifier(server),
            null,
            AuthType.Negotiate
            );

        connect.SessionOptions.ProtocolVersion = 3;
        connect.SessionOptions.ReferralChasing = ReferralChasingOptions.None;

        connect.SessionOptions.Sealing = true;
        connect.SessionOptions.Signing = true;

        return connect;
    }
}

 

The important thing to note is that because everything is running asynchronously, it is totally possible for the end delegate to be invoked before the paging delegate has a chance to finish processing results (depending on how complicated your code is).  You would need to compensate for this yourself.

This client is a console application, so I am using a ManualResetEvent just to prevent it from closing before finishing.  You wouldn't need to do this in a WinForms or WPF app.

I am sure there are other optimizations you could make to pass in parameters or even other directory controls.  However, the general pattern should apply.

Friday, 01 June 2007

Working with Large Amounts of Data in Directory Services

I almost missed this one from Tomek, but he has a good analysis of what happens when you have many results to return from the directory and a nice comparison of how the different stacks (System.DirectoryServices vs. System.DirectoryServices.Protocols) handle it.  The moral of the story:  if you have a ton of results coming back, it might be in your interest to pursue using the Protocols stack.

Monday, 30 October 2006

Asynchronous LDAP Searching with System.DirectoryServices.Protocols

I have been hanging on to this post for some time now and never getting around to polishing it up and putting it out.  Eric Fleischman’s recent posting on SDS.P has inspired me to get some more content out there, however.  For whatever reason, this is a poorly documented topic that probably deserves a series of postings on it.  We neglected this topic mostly in the book because we had concerns about exceeding a certain # of pages on an already pretty dense topic.  I wonder if there would be any demand for a book on this subject?

Previously, I demonstrated how you need to perform paging in SDS.P using the cookie and the paging control.  There is a lot more to take care of here than how it is done with ADSI and System.DirectoryServices (SDS).  Now, what if we really wanted to perform this type of search asynchronously?  We actually wrote a sample in the book to show how asynchronous searching is done in SDS.P, but to keep it simple I left out the whole problem of paging.  Let’s be honest, any useful searching is probably going to need to handle paging as well.

SDS.P supports asynchronous searching directly, unlike SDS.  It is entirely possible to emulate this behavior in SDS by leveraging a general asynchronous pattern supported in .NET (e.g. by way of delegates, background thread workers, or the thread pool), but there is nothing in SDS to support asynchronously searching directly.  Hold on you say!  What about .NET 2.0 and the fancy new property on the DirectorySearcher called Asynchronous?  That is what we call a very misleading property.  While it actually sets the underlying ADS_SEARCHPREF_ASYNCHRONOUS option on the IDirectorySearch interface, the SDS model consumes the results synchronously, netting you exactly nothing.  I honestly wish they (the SDS .NET team) would just not have exposed that one if you couldn’t really use it.

Let’s look at the most basic pattern for how this can be accomplished:

  1. Create a connection to the LDAP directory (LdapConnection).
  2. Create a searching request operation (SearchRequest)
  3. Add a paging control to the SearchRequest to control paging
  4. Invoke the method asynchronously using LdapConnection.BeginSendRequest
  5. Provide a callback method to handle the results.

Simple, no?  It actually looks harder than it is.

When I set out creating this sample, I had a particular use in mind.  Specifically, I wanted to create an easy searching object – something where I wouldn’t have to worry about paging and that could also handle performing multiple searches on the same instance.  That last point, as you will see makes for additional complication.  I started with my usage model or how I visualized that I wanted to use it:

using (LdapConnection connection = CreateConnection(servername))
{
    AsyncSearcher searcher = CreateSearcher(connection);
 
    //this call is asynch, so we need to keep this main
    //thread alive in order to see anything
    //we can use the same searcher for multiple requests - we just have to track which one
    //is which, so we can interpret the results later in our events.
    _firstSearch = searcher.BeginPagedSearch(baseDN, "(sn=d*)", null, 500);
    _secondSearch = searcher.BeginPagedSearch(baseDN, "(sn=f*)", null, 500);
 
    //we will use a reset event to signal when we are done (using Sleep() on
    //current thread would work too...)
    _resetEvent.WaitOne(); //wait for signal;
}

I also knew that I would want to be notified of a couple key events that would occur with my object.  Specifically, whenever a page was returned and when the final search was complete:

static AsyncSearcher CreateSearcher(LdapConnection connection)
{
    AsyncSearcher searcher = new AsyncSearcher(connection);
 
    //assign some handlers for our events
    searcher.PageCompleted += new EventHandler<AsyncEventArgs>(searcher_PageCompleted);
    searcher.SearchCompleted += new EventHandler<AsyncEventArgs>(searcher_SearchCompleted);
 
    return searcher;
}

This would allow me to hook up a couple handlers that would be invoked each time a page came back or my search was finished.

static void searcher_SearchCompleted(object sender, AsyncEventArgs e)
{
    //this is volatile, so we need check it first or another thread
    //could change this from under us
    bool lastSearch = (((AsyncSearcher)sender).PendingSearches == 0);
 
    Console.WriteLine(
        "{0} Search Complete on thread {1}",
        e.RequestID.Equals(_firstSearch) ? "First" : "Second",
        Thread.CurrentThread.ManagedThreadId
        );
 
    if (lastSearch)
        _resetEvent.Set();
}
 
static void searcher_PageCompleted(object sender, AsyncEventArgs e)
{
    //or do something with e.Results here...
    Console.WriteLine(
        "Found {0} results on thread {1} for {2} search",
        e.Results.Count,
        Thread.CurrentThread.ManagedThreadId,
        e.RequestID.Equals(_firstSearch) ? "first" : "second"
        );
}

The complication with this model has to do with the fact that I wanted to be able to use the same object to search multiple times.  I could have limited my AsyncSearcher class to disallow more than one search at a time, but I thought that would be lame if I had to spin up a new instance and set of handlers for each search I wanted to perform.  Since I could only register for one paging and one completion event and yet multiple searches could be firing it, I needed a way to distinguish one search from another.  I decided to return a unique Guid for each search that could be matched up later to determine which search was activating the event.  With more time, I suppose I could have wrapped the Guid in something a little prettier or easier to use, but it suffices for this sample.

I also decided that I wanted to return results by pages as I got them as well as the huge block of them on completion of the search.  To do this, I created a simple class to hold the results called AsyncEventArgs:

/// <summary>
/// Just a simple class to hold some results
/// </summary>
public class AsyncEventArgs : EventArgs
{
    Guid _id;
    List<SearchResultEntry> _entries;
 
    public AsyncEventArgs(List<SearchResultEntry> entries, string requestID)
    {
        _entries = entries;
        _id = new Guid(requestID);
    }
 
    public List<SearchResultEntry> Results
    {
        get
        {
            return _entries;
        }
    }
 
    public Guid RequestID
    {
        get
        {
            return _id;
        }
    }
}

I could have created a different EventArg class depending on the type of event being fired, but I didn’t think it was worth it as this point.  Now that I had how I wanted to use the object nailed down a bit, I created the actual implementation.  The logic goes something like this in:

  1. Setup initial search and kick it off (steps 1–5 above), register a callback and pass my SearchRequest as state.
  2. Callback now picks up request and unwraps the SearchRequest (which also tells me which unique search I am after here) and calls LdapConnection.EndSendRequest()
  3. Pull the results from the resulting SearchResponse and fire the PageCompleted event – passing them as EventArgs
  4. Next, determine if the SearchResponse has a cookie and if it does, call BeginSendRequest again with updated paging cookie and point it back to my original callback (#2).  This means there is a lot of 2 through 4 going on here as each page gets processed and the PageCompleted event fires.
  5. If the cookie in #4 is empty, then I am done and I fire the SearchCompleted event and pass as EventArgs all the results I have been collecting internally in a hashtable keyed to the original request Guid.

Here is what that class looks like:

public class AsyncSearcher
{
    LdapConnection _connect;
    Hashtable _results = new Hashtable();
 
    public event EventHandler<AsyncEventArgs> SearchCompleted;
    public event EventHandler<AsyncEventArgs> PageCompleted;
 
    public AsyncSearcher(LdapConnection connection)
    {
        this._connect = connection;
        this._connect.AutoBind = true; //will bind on first search
    }
 
    /// <summary>
    /// Volatile count of outstanding searches in process
    /// </summary>
    public int PendingSearches
    {
        get
        {
            return _results.Count;
        }
    }
 
    private void InternalCallback(IAsyncResult result)
    {
        SearchResponse response = this._connect.EndSendRequest(result) as SearchResponse;
 
        ProcessResponse(response, ((SearchRequest)result.AsyncState).RequestId);
 
        //find the returned page response control
        foreach (DirectoryControl control in response.Controls)
        {
            if (control is PageResultResponseControl)
            {
                //call paged search again
                NextPage((SearchRequest)result.AsyncState, ((PageResultResponseControl)control).Cookie);
                break;
            }
        }
    }
 
    private void ProcessResponse(SearchResponse response, string guid)
    {
        //only 1 thread at a time gets here...
        List<SearchResultEntry> entries = new List<SearchResultEntry>();
 
        foreach (SearchResultEntry entry in response.Entries)
        {
            entries.Add(entry);
        }
 
        //signal our caller that we have a page
        EventHandler<AsyncEventArgs> OnPage = PageCompleted;
        if (OnPage != null)
        {
            OnPage(
                this,
                new AsyncEventArgs(entries, guid)
                );
        }
 
        //add to the main collection
        ((List<SearchResultEntry>)_results[guid]).AddRange(entries);
    }
 
    public Guid BeginPagedSearch(
        string baseDN,
        string filter,
        string[] attribs,
        int pageSize
        )
    {
        Guid guid = Guid.NewGuid();
 
        SearchRequest request = new SearchRequest(
            baseDN,
            filter,
            System.DirectoryServices.Protocols.SearchScope.Subtree,
            attribs
            );
 
        PageResultRequestControl prc = new PageResultRequestControl(pageSize);
 
        //add the paging control
        request.Controls.Add(prc);
 
        //we will use this to distinguish multiple searches.
        request.RequestId = guid.ToString();
 
        //create a temporary placeholder for the results
        _results.Add(request.RequestId, new List<SearchResultEntry>());
 
        //kick off async
        IAsyncResult result = this._connect.BeginSendRequest(
            request,
            PartialResultProcessing.NoPartialResultSupport,
            new AsyncCallback(InternalCallback),
            request
            );
 
        return guid;
    }
 
    private void NextPage(SearchRequest request, byte[] cookie)
    {
        //our last page is when the cookie is empty
        if (cookie != null && cookie.Length != 0)
        {
            //update the cookie and preserve page size
            foreach (DirectoryControl control in request.Controls)
            {
                if (control is PageResultRequestControl)
                {
                    ((PageResultRequestControl)control).Cookie = cookie;
                    break;
                }
            }
 
            //call it again to get next page
            IAsyncResult result = this._connect.BeginSendRequest(
                request,
                PartialResultProcessing.NoPartialResultSupport,
                new AsyncCallback(InternalCallback),
                request
                );
        }
        else
        {
            List<SearchResultEntry> results = (List<SearchResultEntry>)_results[request.RequestId];
 
            //decrement our collection when we are done
            _results.Remove(request.RequestId);
 
            //we have finished, signal the caller
            EventHandler<AsyncEventArgs> OnComplete = SearchCompleted;
            if (OnComplete != null)
            {
                OnComplete(
                    this,
                    new AsyncEventArgs(results, request.RequestId)
                    );
            }
        }
    }
}

I originally toyed with the idea of having the AsyncSearcher manage the connection to the directory, but ultimately decided that it was a bad idea for two main reasons.  First, not everyone will construct the connection the same.  Some people will use SSPI, others SSL, perhaps different ports, even certificates or otherwise.  The other reason is that the lifetime of the connection is harder to control from inside another class.  Wrapping it in a “using” statement won’t work because the connection must be open the whole time during the callbacks.  Cleaning it up in a Dispose() method is sloppy as well because it leads to the client needing to know that they should keep this object around until all their events fire.  By pulling the LdapConnection out, I am implicitly telling clients they need to manage the connection themselves.

You can download this class along with a sample client as an attachment to this post.  Next time, perhaps we will delve into retrieving partial results.

 File Attachment: AsynchClient.zip (5 KB).

Friday, 04 August 2006

Fast Concurrent Binding in SDS.P

So, this is really a lesson learned about putting together a book and code samples.  Namely, refactoring your code just before the final cut is generally not a good idea.  Or perhaps I should say, refactoring your code and not thoroughly testing it is not a good idea.

In Chapter 12 of the book, we had a number of examples for how to perform authentication.  One of them was using System.DirectoryServices.Protocols (SDS.P).  The sample tried a number of techniques – first a secure SSL bind using Fast Concurrent Binding (FCB), then it tried either a secure SPNEGO bind or a Digest bind (if ADAM).  Well, initially these were all different samples.  I thought it might be nice to tie them all together a bit more comprehensively – hence the refactoring.  I figured that a bigger sample that did more in a practical manner was more useful than a few line snippets that showed each one.

Anyhow, what ended up happening is that I broke the FCB authentication during the refactoring.  Because of unforseen testing environment meltdown a week earlier I did not have the proper Win2k3 clients to test again (it used to work, really!).  So… I borked it because the FCB code never got tested again.

One of my Avanade co-workers was actually implementing something like this and asked why it was not working.  At first I chalked it up to an environment thing, but after a closer inspection I noticed what the issue was.  Namely, in my attempt to bring all the samples together I had attempted to reuse the same connection for authentication as the bootstrapping.  Well, you can’t do that with FCB – you have to enable it before you bind and cannot turn it off until you close the connection.

The good news is that it is a fairly simple fix and I have already refactored (yet again) to support it.  I will be posting that code in another week or so when I get back from vacation.  Then poor Joe gets to convert it yet again to VB.NET.  Mea Culpa…

Friday, 17 March 2006

Paging in System.DirectoryServices.Protocols

One of the new features of .NET 2.0 is the System.DirectoryServices.Protocols (SDS.P) namespace that gives us access to the native LDAP api without relying on ADSI.  We can accomplish pretty much anything now in LDAP using managed code and we get around some of the wonkiness of ADSI.

Performing a simple search in SDS.P is pretty straightforward actually.  We just create a connection to the directory using the LdapConnection class, bind to the connection and then make request/response pairs on the connection.  One of the request/response pairs is SearchRequest and SearchResponse.  Without going into too many details, using these two classes in a synchronous manner is pretty easy.  What might be surprising however is that developers will often run into the following error:

System.DirectoryServices.Protocols.DirectoryOperationException : The size limit was exceeded

This departs from System.DirectoryServices (SDS) and ADSI where we do not get errors for simple searches even if we have not enabled paging.  In SDS, a similar query with DirectorySearcher would just return the MaxPageSize for the directory (usually 1000).  We know from SDS that to get more than the MaxPageSize, we need to use paging.  That is as simple as setting DirectorySearcher.PageSize > 0.

So, what about SDS.P.  It turns out that one of the really nice things that ADSI did for us was make paging so seamless.  If we wanted to use it, we simply asked for a specific page size and it just worked.  Now, since we are at a lower level, we need to take care of these details ourselves (native LDAP programmers already know this of course).

How do we do this in SDS.P?  Here is some sample code that implements paging and returns a collection of SearchResultEntry objects.

private List<SearchResultEntry> PerformPagedSearch(

    LdapConnection connection,

    string baseDN,

    string filter,

    string[] attribs)

{

    List<SearchResultEntry> results = new List<SearchResultEntry>();

 

        SearchRequest request = new SearchRequest(

            baseDN,

            filter,

            System.DirectoryServices.Protocols.SearchScope.Subtree,

            attribs

            );

 

    PageResultRequestControl prc = new PageResultRequestControl(500);

 

    //add the paging control

    request.Controls.Add(prc);

 

    while (true)

    {

        SearchResponse response = connection.SendRequest(request) as SearchResponse;

 

        //find the returned page response control

        foreach (DirectoryControl control in response.Controls)

        {

            if (control is PageResultResponseControl)

            {

                //update the cookie for next set

                prc.Cookie = ((PageResultResponseControl)control).Cookie;

                break;

            }

        }

 

        //add them to our collection

        foreach (SearchResultEntry sre in response.Entries)

        {

            results.Add(sre);

        }

 

        //our exit condition is when our cookie is empty

        if (prc.Cookie.Length == 0)

            break;

    }

 

    return results;

}

Thursday, 22 December 2005

Resurrecting Tombstones in Active Directory or ADAM

I did not get to put all the material I wanted to into the book, so I figure some of the stuff we have left out will make good material for a blog entry here and there.

Active Directory and ADAM both have a special container called “CN=Deleted Objects” that can only be viewed by administrators by default.  When we delete objects in either of these directories, the entries are typically moved there and most of their attributes are stripped off.  This is done such that other replication partners know which items were deleted.  These objects are now referred to as ‘tombstones’.  The system will hold these objects for a set period of time (60 days or so IIRC) before purging them completely.

Now, occasionally we run into situations where we want to resurrect one of the tombstones.  This is typically when we need an object to have the identical GUID or SID it had while alive as simply recreating the object would generate new values for these attributes.  There is an example on MSDN on how to resurrect these objects, but it is in C++.  I thought it would be fun to show how to do this in System.DirectoryServices.Protocols as it has the capabilities to do this while System.DirectoryServices (ADSI) does not.  We have to use the LDAP replace method in this case which is supported in the new Protocols namespace.

Instead of just posting a link to my code, I thought I would post it in whole here.  Notice that my Lazarus (resurrected, get it?) class is using Well-known GUID binding.  You should have your RootDSE return a defaultNamingContext for ADAM if you have not already done so.

class Lazarus : IDisposable
{
    const string WK_TOMBSTONE = "18e2ea80684f11d2b9aa00c04f79f805";
    string _defaultNC;
    string _server;
 
    DirectoryEntry _entry;
 
    public Lazarus()
        : this(null)
    {
    }
 
    public Lazarus(string server)
    {
        _server = server;
 
        DirectoryEntry root = new DirectoryEntry(
            String.Format(
                "LDAP://{0}RootDSE",
                _server != null ? _server + "/" : String.Empty
                )
            );
 
        using (root)
        {
            _defaultNC = root.Properties["defaultNamingContext"][0].ToString();
        }
 
        _entry = new DirectoryEntry(
            String.Format(
                "LDAP://{0}<WKGUID={1},{2}>",
                _server != null ? _server + "/" : String.Empty,
                WK_TOMBSTONE,
                _defaultNC
                ),
            null,
            null,
            AuthenticationTypes.Secure
            | AuthenticationTypes.FastBind
            );
    }
 
    public void ShowAllTombStones()
    {
 
        DirectorySearcher ds = new DirectorySearcher(
            _entry,
            "(isDeleted=TRUE)",
            null,
            System.DirectoryServices.SearchScope.OneLevel
            );
 
        ds.PageSize = 1000;
        ds.Tombstone = true;
 
        using (SearchResultCollection src = ds.FindAll())
        {
            foreach (SearchResult sr in src)
            {
                foreach (string key in sr.Properties.PropertyNames)
                {
                    foreach (object o in sr.Properties[key])
                    {
                        Console.WriteLine("{0}: {1}", key, o);
                    }
                }
                Console.WriteLine("====================");
            }
        }
 
    }
 
    private SearchResult GetTombstone(string name)
    {
        DirectorySearcher ds = new DirectorySearcher(
            _entry,
            String.Format("(&(isDeleted=TRUE)(name={0}*))", name),
            new string[] { "cn", "lastKnownParent", "distinguishedName" },
            System.DirectoryServices.SearchScope.OneLevel
            );
 
        ds.Tombstone = true;
 
        return ds.FindOne();
    }
 
    public void RaiseFromTheDead(string name)
    {
        SearchResult deadGuy = GetTombstone(name);
 
        if (deadGuy == null)
            throw new ArgumentException("No object exists");
 
        LdapConnection conn = new LdapConnection(
            new LdapDirectoryIdentifier(_server),
            System.Net.CredentialCache.DefaultNetworkCredentials,
            AuthType.Negotiate
            );
 
        using (conn)
        {
            conn.Bind();
            conn.SessionOptions.ProtocolVersion = 3;
 
            //we have to remove the isDELETED attribute
            DirectoryAttributeModification dam = new DirectoryAttributeModification();
            dam.Name = "isDELETED";
            dam.Operation = DirectoryAttributeOperation.Delete;
 
            //and set a new dn string - a bit of a copout because
            //I am always assuming a CN prefix.
            string newDN = String.Format(
                "CN={0},{1}",
                deadGuy.Properties["cn"][0].ToString().Split(new char[]{'\n'})[0],
                deadGuy.Properties["lastKnownParent"][0]
                );
 
            //word of warning... 'lastKnownParent' is only guaranteed good
            //on Windows Server 2003 and ADAM.
 
            DirectoryAttributeModification dam2 = new DirectoryAttributeModification();
            dam2.Name = "distinguishedName";
            dam2.Operation = DirectoryAttributeOperation.Replace;
            dam2.Add(newDN);
 
            //kinda bizzare that a collection is not used
            ModifyRequest mr = new ModifyRequest(
                deadGuy.Properties["distinguishedName"][0].ToString(),
                new DirectoryAttributeModification[] { dam, dam2 }
                );
 
            //we need to have the ShowDeletedControl to find this DN
            mr.Controls.Add(new ShowDeletedControl());
 
            //optionally, we must put any required attributes on the object
            //as well.  For user/computer, this might be 'sAMAccountName'
 
            ModifyResponse resp = (ModifyResponse)conn.SendRequest(mr);
 
            if (resp.ResultCode != ResultCode.Success)
            {
                //we should also check to see if it was already
                //existing here (same DN).
                Console.WriteLine(resp.ErrorMessage);
            }
        }
    }
 
    #region IDisposable Members
 
    public void Dispose()
    {
        if (_entry != null)
            _entry.Dispose();
    }
 
    #endregion
}

This is definitely sample code, but it should give a working idea of how to accomplish this.  I think that as I used the .Protocols namespace, there were some usability issues that I question, however it is definitely a neat addition.  I decided to mix using System.DirectoryServices with using .Protocols because I wanted to show how to do a Tombstone search using SDS as well.

Finally, this was tested on ADAM, but should work fine on AD.  The usual warnings apply:  This is not production code, don’t expect it to be and don’t cry to me if it breaks your machine horribly or your dog dies.

Enjoy, your comments are always welcome.