Tuesday, October 30, 2007
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:
- 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.
- 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.
- 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();
}
And there you have it... change notifications in .NET. You can also download my project file for Visual Studio 2008.
Monday, October 29, 2007
This is just a reminder that you should not use server-side sorting with your queries in Active Directory or ADAM. This situation was reinforced when a reader asked me why a particular ASQ (attribute scope query) was failing with an error when querying a rather large group (more than 20,000 members). The customer was getting a fairly nondescript error, "The server does not support the requested critical extension" about halfway through his results.
After checking the network trace, and handing the DSID error back to my buddy Eric - we (more he, than me) determined it was failing in the code path for sorting. It turns out that sorting this much data on the server requires temp table space. If you run out of space before the sorting is complete you get this type of error. This is not particular to ASQ by any means, but all sorting.
The moral of story is don't sort on the server. This is a very real example of why this is the case. You can always easily sort once you have results on the client.
Wednesday, August 01, 2007
I have previously covered pretty extensively the options for getting a user's group membership in Active Directory or ADAM (soon to be Active Directory LDS (Lightweight Directory Services)) here on the blog, in the forum, and in the book. However, there is a new option for users of .NET 3.5 that should be of interest.
The Directory Services group at Microsoft has released in beta form a new API for dealing with a lot of the common things we need to do with users, groups, and computers in Active Directory, ADAM, and the local machine. This API is called System.DirectoryServices.AccountManagement (or SDS.AM). Here is a simple example of how to get a users groups (including nested, and primary):
static void Main(string[] args)
{
PrincipalContext ctx = new PrincipalContext(ContextType.Domain);
using (ctx)
{
Principal p = Principal.FindByIdentity(ctx, "ryandunn");
using (p)
{
var groups = p.GetGroups();
using (groups)
{
foreach (Principal group in groups)
{
Console.WriteLine(group.SamAccountName + "-" + group.DisplayName);
}
}
}
}
Console.ReadLine();
}
That's not too bad - in fact, it looks worse than it is because I am trying to make sure everything is wrapped in a 'using' statement where necessary. The equivalent code to do this would be many times more (using DsCrackNames or LDAP searches) and would yield far less information being returned (just the DN in most cases).
Over the next few weeks and months, I intend to dig more deeply into this namespace and put some samples up here for everyone. This is just a taste for now, but it should show you how powerful this namespace really is.
*Updated to fix CSS renderings in Google Reader
Thursday, June 28, 2007
I was checking on the book's website the other day and noticed that we broke the 54,000 mark for downloads of our sample code. That really surprised me. I just didn't think that there were that many people in the world working on these types of scenarios.
Now, I happen to know that we did not sell that many copies of the book, so this means a lot of people are downloading just the samples. For the record, that is perfectly fine and we encourage people to look at the samples. Admittedly, some of the samples might be head-scratchers without the book for context, but hey, its free and so is our time on the forums.
Joe and I log a lot of time here and in other forums helping people and we love to hear your feedback on what works and what doesn't. Give us a shout when samples don't make sense, or when a scenario seems to be overly painful. Some people have asked how they can repay us for our time (I have had offers for beer, wine, and lodging so far). A simple thanks is enough for both of us, however, and a perhaps a recommendation to others. If you really feel the need to contribute beyond nice words - simple, just buy the book!
Friday, June 01, 2007
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.
Thursday, April 05, 2007
If hot-LDAP-filter-action is your thing, but you were let down in my last post since it required SP2 and Longhorn, then this should get you all hot and bothered again: Hotfix for SP1.
I know, it's a hotfix - which means you have to contact Microsoft to get it. But if you want to take advantage of the new LDAP_MATCHING_RULE_IN_CHAIN without upgrading to SP2 or Longhorn, then this is it.
Tuesday, March 20, 2007
I was speaking with Eric Fleischman at the MVP summit this year and he told me about a neat feature you will find in Window Server 2003 SP2 and Longhorn server. It is a new type of matching rule ID filter that allows for transitive link value evaluation. This is one filter type that is incredibly useful and you will want to know about.
First some background: a matching rule ID filter is a special syntax filter that allows for an arbitrary search behavior as defined by the matching rule. Active Directory and ADAM only shipped with two matching rules until recently: LDAP_MATCHING_RULE_BIT_AND (1.2.840.113556.1.4.803) and LDAP_MATCHING_RULE_BIT_OR (1.2.840.113556.1.4.804). We commonly used these matching rules to check our bitwise flag values. For instance, here is the key portion of the filter that specifies an account is disabled:
(userAccountControl:1.2.840.113556.1.4.803:=2)
Notice that we have the attribute name we are searching on, the rule OID we want to use (the AND rule), and the value to check (in decimal). Pretty simple, right?
The new matching rule is called LDAP_MATCHING_RULE_IN_CHAIN and it has an OID of 1.2.840.113556.1.4.1941. This new rule allows us to search across all DN-syntax attributes recursively and evaluate the entire tree of relationships (hence the transitive name).
Evaluating Group Membership
Where we will typically see this used is in group membership evaluation. Specifically, it answers two questions that are constantly asked by developers: What groups are my user a member of? and What users are in this group?
It is really simple to use this filter, so here we go. The first question is, what groups is my user a member of?
(member:1.2.840.113556.1.4.1941:=CN=User1,OU=X,DC=domain,DC=com)
The next question, what users are in this group?
(memberOf:1.2.840.113556.1.4.1941:=CN=A Group,OU=Y,DC=domain,DC=com)
If we place base this search on the main partition and use a subtree search, it will return for us all the matches across the domain. However, if we scope the second search to a specific user object and use a base search, it is a quick and dirty way of telling us if the user is a member of the group. Hence, this would also work for a type of IsInRole() function:
public bool IsUserMember(DirectoryEntry user, string groupDN)
{
string filter = String.Format(
"(memberOf:1.2.840.113556.1.4.1941:={0})", groupDN);
DirectorySearcher ds = new DirectorySearcher(
user,
filter,
null,
SearchScope.Base);
return (ds.FindOne() != null);
}
Now, I want to also point out that this sort of code also makes me cringe a little bit thinking about the abuse that can occur... Remember, it is performing a search each and every time you want to check group membership. It is still a better idea to build the entire group membership of a user and store it in one of the IPrincipal classes and use the .IsInRole() functionality to keep network access to a minimum.
Creating an Org Chart
The other area where we will find this filter being pretty handy is when we want to find all the users that directly and indirectly report to a single person. This is the typical situation when building org charts or trying to find the users for a mailing list. Here is one such example:
public static void GetOrgChart(DirectoryEntry entry, string bossDN)
{
string filter = String.Format(
"(&(mail=*)(manager:1.2.840.113556.1.4.1941:={0}))",
bossDN);
DirectorySearcher ds = new DirectorySearcher(
entry,
filter
);
using (SearchResultCollection src = ds.FindAll())
{
foreach (SearchResult sr in src)
{
Console.WriteLine(sr.Properties["mail"][0]);
}
}
}
Performance
The next question we will want to answer for this new filter type is the performance relative to the code required to process this recursively. I will use the code I presented here to test the recursive case and compare it to a simple filter of:
(memberOf:1.2.840.113556.1.4.1941:=CN=BigNestedGroup,OU=X,DC=Y)
So, to cut to the chase, how does the new filter compare to recursively chasing DN link pairs yourself? Short answer... it doesn't. The code I wrote for the book and here on the blog blows it away by a factor of 10. I have to admit that before I ran the tests, I expected the new filter to run circles around my code and I was pretty shocked when the reverse was actually true. I tested this using 3 nested groups each with 100 members and running both the recursive search and the transitive search 100 times and averaging the results. To expand the lead group with 300 direct and indirect members took roughly half a second using a transitive filter (409ms) as compared to only (41ms) using the recursive search. This would have a big impact on server apps serving many of these searches, but would probably not be a huge factor in client side apps or where a smallish number of these searches are performed.
Summary
The transitive filter is a great new addition and can greatly simplify the code that you have to write. This new filter is not without pitfalls however. Make sure you are cognizant of the performance tradeoffs that are inherent in choosing this filter as it is considerably slower to use.
Friday, February 09, 2007
One of the neater abstractions we get using ASP.NET 2.0 is the ObjectDataSource. Essentially it allows us to specify our own objects or some arbitrary object graph as a datasource for databinding operations. Combined with the new GridView class it can be very powerful. Built in to both of these objects are methods for Selecting, Updating, Deleting, and Inserting. This allows you to plug-in your own code to manage each of these operations.
If we want to adapt this to support reading and updating (including adds and deletes) from AD or ADAM, we just need to put a few methods in place and hook them into the ObjectDataSource abstraction. The nice thing here is that it includes parameter support pretty easily. This allows us to add new values or update existing values. By specifying a data key (DataKeyNames) on the Gridview, it also allows us to uniquely index which object we are modifying.
I chose for this example to show a simple Select, Update, and Delete using a GridView and the ObjectDataSource. It only required me to configure 3 methods (one for each) and declaratively setup the parameters I would be using. I chose to use the 'objectGuid' for the key name since this is guaranteed to be unique and will always point me to the right object. However, I could also have used the DN as the key value in this case as well. If I had done that, it would have saved me a little bit of complication due to the need to rebind when using the GUID value for certain operations.
Keep in mind that this is just a simple sample of how this can be done and is not something that should be put into production as-is. The whole point of this is to show how the GridView gives us easy updating and reading of values, while the ObjectDataSource is used as our abstraction for operations on data. In the past, this would not have worked well because the DataGrid was so geared towards relational data like SQL. It would have been much more convoluted to get the same functionality. Being able to write only 3 methods and have working ASP.NET viewing and editing application is pretty neat.
You can download the sample here.
Wednesday, January 24, 2007
The task of figuring out your Active Directory NETBIOS domain name comes up now and again. This is the process of turning something like "DC=yourdomain,DC=com" into something like "YOURDOMAIN". This is important if you want to do things like prefix this name on the 'sAMAccountName' of a user for instance. There are three ways of doing this (perhaps more). I will cover each broadly.
Guess
Yes, that's right. You can pretty much guess what it is by parsing the DN of the defaultNamingContext. If you had something like "DC=yourdomain,DC=com", it seems a pretty good guess that the NETBIOS name is "YOURDOMAIN". However, this method of course is not foolproof. For whatever reason, your Active Directory admins might have decided to do something stupid - like say add a big dash (-) and some other random nonsense into the NETBIOS name. Who knows why they do this, but hey, they can and do (I suspect they hate their users). So, in this case, "DC=yourdomain,DC=com" might have a NETBIOS name of "YOURDOMAIN-CORP" for instance (it also means you have to type this every damn time you need to supply credentials as well in NT4 domain\user format).
DsCrackNames
You can simply use DsCrackNames to convert from DN format to NT4 format and it will work fine. Pass the DN (e.g. "DC=yourdomain,DC=com") and you will get back the NETBIOS name. This includes IADsNameTranslate if that is up your alley too. Since there is no easy way to use either from .NET unless you build your own, this might not be your first choice.
LDAP Query
Finally, we get to the last method. First, we connect to the RootDSE of the domain and inspect the following two properties: "configurationNamingContext" and "defaultNamingContext". Then we bind to the configuration partition (using the first of these properties) and perform the following search: (&(objectCategory=crossRef)(nCName={0})) where {0} is the value you just got from the RootDSE for the 'defaultNamingContext'. You should get one result. Now, just read back the 'nETBIOSName' attribute and there you have it.
If you are shipping software that needs to do this, obviously you should use one of the last two methods. Guessing probably will just not cut it for anything that is just not quick and dirty.
Monday, October 30, 2006
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:
- Create a connection to the LDAP directory (LdapConnection).
- Create a searching request operation (SearchRequest)
- Add a paging control to the SearchRequest to control paging
- Invoke the method asynchronously using LdapConnection.BeginSendRequest
- 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:
- Setup initial search and kick it off (steps 1–5 above), register a callback and pass my SearchRequest as state.
- Callback now picks up request and unwraps the SearchRequest (which also tells me which unique search I am after here) and calls LdapConnection.EndSendRequest()
- Pull the results from the resulting SearchResponse and fire the PageCompleted event – passing them as EventArgs
- 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.
- 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, August 04, 2006
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…
My co-author
Joe Kaplan is finally online and blogging now. I have asked him for more Wix content since he knows a bunch more than me on it. More LDAP blogging goodness to come I am sure…
Thursday, July 27, 2006
You learn something new everyday. I had been under the mistaken belief that the new ‘msDs-User-Account-Control-Computed’ attribute was a sexier and more accurate version of the older, non-constructed ‘us