In the previous section, we discussed the various LDAP syntaxes and showed how they map into the various data types. We also know that ADSI will map each LDAP attribute to an appropriate ADSI data type and COM data type. So, how does ADSI know the syntaxes of all of the LDAP attributes so that it knows the proper data type mappings? Given that LDAP schemas are extensible and can thus be completely unique, this obviously must be done dynamically.
To make this work, ADSI takes advantage of the fact that LDAP version 3 requires that each directory expose its schema in a special abstract form via a specific object in the directory. The abstract schema, an object of type subSchema, describes each attribute in the directory and its syntax (among many other things). The object's location is published in a DN-syntax attribute called subschemaSubentry, on the RootDSE object.
To create a schema mapping for a given directory, ADSI first reads RootDSE to find the abstract schema, and then reads the abstract schema to create a mapping in memory for that directory. From there, ADSI can create an appropriate ADSI or COM mapping for any attribute it reads or writes. The schema for each directory is read only once for a given process the first time that directory is accessed.
If you've ever looked at the aggregate schema object for an LDAP directory with a large schema, like Active Directory, you may have noticed that it is pretty large. There is a definite performance penalty for reading all of this data over the network each time a directory is accessed, especially in short-lived processes like scripts. To make things faster, ADSI will attempt to cache the schema locally on the filesystem so that the abstract schema can be read from disk.
Here is how it works. The first time ADSI encounters an LDAP version 3 directory, it will
On a subsequent visit to the same directory, ADSI will once again access the RootDSE object and find the subschemaSubentry object.
Troubleshooting ADSI Schema Cache
Normally, the ADSI schema cache just works behind the scenes and does what it is supposed to. However, there are several cases where it does not, and suddenly we lose the ability to read most attributes without weird exceptions. Some of the most frustrating problems we've seen developers suffer through are caused by schema caching. This is a topic that developers should understand well, since it tends to crop up with some regularity.
What Can Go Wrong?
Schema caching in ADSI imposes a number of preconditions that must be perfectly aligned for it to work correctly. The most obvious aspect is that we typically need an administrative-level account to write to HKLM in the registry or the Windows directory, yet this is something we obviously cannot be guaranteed of when our application runs. In fact, we can argue that in nearly all instances, we would not want this to be the case, especially in the context of an application servicing web browsers. In ASP.NET, the default settings for both IIS 5 and IIS 6 run the worker process as a low-privileged account, not an administrator.
The net result of reduced permissions is that the schema will simply not be cached and will be read directly from the directory for each process that loads ADSI against that LDAP directory. Luckily, this just makes things slower. It does not actually break anything.
However, what happens if we cannot read or interpret the schema at all? In this case, bad things are probably about to happen. If ADSI fails to get the schema for the current directory, it falls back to a built-in LDAP version 2 schema. If an attribute we happen to read or write is in this schema, then ADSI can map the LDAP data type to an ADSI and COM (and eventually .NET) data type and everything will work as we expect. However, if the attribute is not in the built-in schema, an exception will be thrown when trying to read or write it with the DirectoryEntry.
In .NET 1.x, the DirectorySearcher has the same problem, although the exception is different.
System.NotImplementedException "Handling of this ADSVALUE type is not yet implemented (type = 0xb)"
System.Runtime.InteropServices.COMException (error code 8000500c) "The Active Directory datatype cannot be converted to/from a native DS Datatype"
Things improve slightly in version 2.0 with DirectorySearcher, as any unknown data types will automatically be marshaled as a byte array (byte), so we should not see System.NotImplementedException anymore. We still have to contend with converting all the returned data from byte arrays back into strings and such, but at least we can read the data.
Addressing Schema Mapping Problems
ADSI can have schema-mapping problems for three primary reasons.
Security Related Schema Problems
Of the three errors, the first one is the only one that occurs with Active Directory and ADAM, so we will focus on that. Essentially, this problem tends to occur when the security context used to bind to the directory is not accepted by the server and the remote user ends up being authenticated as an anonymous user instead. Anonymous users do not have permission to read the subSchema object in either Active Directory or ADAM by default, so the LDAP version 2 schema is used.
This problem is especially significant in ASP.NET applications, as we should remember that the schema is cached only once per process.[a] In ASP.NET applications, multiple AppDomains are often loaded in the same process. When we find a situation where ASP.NET applications seem to have problems that are inconsistent, we need to make sure to check all of the applications in that process for any SDS or ADSI usage to make sure that none of them is binding anonymously. Otherwise, the first application to start up in that worker process will define the schema mapping for that particular directory for the life of the process. If that application happens to authenticate as an anonymous user, the schema will revert to LDAP version 2 and will cause problems for all applications running in the process.
The good news is that we can generally always find a way to correct this problem by fixing the security context. We discuss binding and security contexts in much greater detail in Chapters 3 and 8.
LDAP Compliance Problems
We probably can't do much to address LDAP compliance problems. For example, the directory may be an LDAP version 2 directory that does not support a RootDSE mechanism. It may also have a customized schema. In this case, no mechanism is available for ADSI to learn the schema, so it cannot map any data types for the custom attributes. The other version of this problem is that the directory has an incomplete LDAP version 3 implementation where there is no subschemaSubentry attribute on RootDSE.
In this case, our best bet is to use System.DirectoryServices.Protocols (SDS.P) in .NET 2.0. As we discussed in Chapter 2, this API is lower level and does not attempt to do any data-type conversion. As long as the directory is otherwise LDAP compliant, we should be able to get on with our work (at the expense of having to write more code to do the same things). If we do not have the option of going with .NET 2.0, then things get a little ugly.
Using the DirectoryEntry object, we can cast the NativeObject property to an IADsPropertyList object and use its GetPropertyItem method to get an IADsPropertyValue object. From there, we can attempt to convert the attribute value to whatever data type we choose. It is unpleasant and potentially slow, but it should work. In this situation, if we avoid using DirectorySearcher except to access very basic attributes like distinguishedName, then we might be able to cobble together a solution.
Schema Parsing Problems
These are rare, but they are also the most insidious to diagnose. Although we do not have any specific examples, it has been known to happen with some non-Microsoft directories that the abstract schema presented cannot be parsed by ADSI successfully, due to a bug in the ADSI implementation or in the format of the directory's subSchema data. In this case, ADSI will silently fail, leaving us with the default backup LDAP version 2 schema.
So, how can we tell if this is the problem? If the subSchema appears to be where it should and we can rule out security context issues blocking us from reading it, then this is a possibility.
The workarounds are similar to those suggested earlier. However, if we are interested in getting this fixed, this is the point where we should consider contacting Microsoft support. The Directory Services team is genuinely interested in correcting any flaws in ADSI and will want to hear about it. These bugs exist mainly because they are rarely communicated back to the product group. Don't be shy; they might even build you a hotfix.
[a] In more-recent versions of ADSI, if a connection using different credentials accesses a directory and sees that the schema cache for that directory is in the default mode, it will attempt to download the schema again using the new credentials.
Part I: Fundamentals
Introduction to LDAP and Active Directory
Introduction to .NET Directory Services Programming
Binding and CRUD Operations with DirectoryEntry
Searching with the DirectorySearcher
Advanced LDAP Searches
Reading and Writing LDAP Attributes
Active Directory and ADAM Schema
Security in Directory Services Programming
Introduction to the ActiveDirectory Namespace
Part II: Practical Applications
Part III: Appendixes
Appendix A. Three Approaches to COM Interop with ADSI
Appendix B. LDAP Tools for Programmers
Appendix C. Troubleshooting and Help