How to query the Active Directory Schema using LinqToLdap

When creating ClassMaps for earlier articles, I’ve needed info from the AD Schema.  Earlier, I thought, “Why not do this using LinqToLdap?”  So I did.  In this article, I show you what’s needed so you can do the same.  I build on earlier articles, Creating a directory objects assembly for LinqToLdap, in particular.

Overview

First, I added a couple of properties to Top.cs: adminDescription and adminDisplayName. I’d already created a new subclass of Top, TopX, to hold attributes for other directory object classes I haven’t blogged about, yet. I’m not keen on the name, so it may change. Feel free to offer alternatives.

Then, I created new directory object class files called ClassSchema.cs and AttributeSchema.cs with matching ClassMap files, ClassSchemaMap.cs and AttributeSchemaMap.cs.

Finally, I used them in a series of queries.  I’ve included all of the files below.  Here are some examples of queries I’ve run.

The first example retrieves a particular classSchema entry, user, and displays selected properties.

    static void Main(string[] args)
    {
        var config = new LdapConfiguration()
            .AddMapping(new ilminator.LinqToLdap.DirectoryObjects.ClassMaps.AttributeSchemaMap())
            .AddMapping(new ilminator.LinqToLdap.DirectoryObjects.ClassMaps.ClassSchemaMap());

        var factory = config.ConfigureFactory("dc1");    // Insert your DC name or domain DNS name here

        var context = new DirectoryContext();

        // ----- Start of section to replace -----

        var query = context.Query<ClassSchema>()
            .Where(c => c.lDAPDisplayName.Equals("user"));

        foreach (var item in query)
        {
            Console.WriteLine("lDAPDisplayName = {0}", item.lDAPDisplayName);
            DisplaySingleValuedAttribute(item.subClassOf, "subClassOf");
            DisplayMultiValuedAttribute(item.systemAuxiliaryClass, "systemAuxiliaryClass");
            DisplayMultiValuedAttribute(item.auxiliaryClass, "auxiliaryClass");
            DisplayMultiValuedAttribute(item.systemMustContain, "systemMustContain");
            DisplayMultiValuedAttribute(item.mustContain, "mustContain");
            DisplayMultiValuedAttribute(item.systemMayContain, "systemMayContain");
            DisplayMultiValuedAttribute(item.mayContain, "mayContain");
        }

        // ----- End of section to replace -----

        Console.WriteLine("\r\nPress a key to continue...");
        Console.ReadKey(true);
    }

    private static void DisplaySingleValuedAttribute(Object attribute, string attributeName)
    {
        if (attribute != null)
        {
            Console.WriteLine("{0,-30}: {1}", attributeName, attribute);
        }
    }

    private static void DisplayMultiValuedAttribute(Collection<String> attributeValues, string attributeName)
    {
        if (attributeValues != null)
        {
            var list = new List<String>(attributeValues);
            list.Sort();

            Console.WriteLine("{0}:", attributeName);
            foreach (var item in list)
            {
                Console.WriteLine("    {0}", item);
            }
        }
    }

The second example retrieves all classSchema entries which have mailRecipient as an auxiliaryClass or systemAuxiliaryClass and displays their lDAPDisplayNames.

    // ----- Start of section to replace -----

    var query = context.Query<ClassSchema>()
        .Where(c => (c.auxiliaryClass.Contains("mailRecipient") || c.systemAuxiliaryClass.Contains("mailRecipient")));

    foreach (var item in query)
    {
        Console.WriteLine(item.lDAPDisplayName);
    }
            
    // ----- End of section to replace -----

The third example retrieves all classSchema entries which have mail as an attribute displays their lDAPDisplayNames.

    // ----- Start of section to replace -----

    var query = context.Query<ClassSchema>()
        .Where(c => (c.mustContain.Contains("mail")
            || c.systemMustContain.Contains("mail")
            || c.mayContain.Contains("mail")
            || c.systemMayContain.Contains("mail")));

    foreach (var item in query)
    {
        Console.WriteLine(item.lDAPDisplayName);
    }
            
    // ----- End of section to replace -----

The fourth example retrieves and displays all of the mapped attributes of all attributeSchema entries.

    // ----- Start of section to replace -----

    DisplayAllAttributes(context);
            
    // ----- End of section to replace -----

and

    private static void DisplayAllAttributes(DirectoryContext context)
    {
        var query = context.Query<AttributeSchema>();

        var attributes = from a in query
                            select a;

        foreach (var attribute in attributes)
        {
            DisplaySchemaAttribute(attribute);
            Console.WriteLine();
        }
    }

    private static void DisplaySchemaAttribute(AttributeSchema attribute)
    {
        DisplaySingleValuedAttribute(attribute.lDAPDisplayName, "lDAPDisplayName");
        DisplaySingleValuedAttribute(attribute.commonName, "commonName");
        DisplaySingleValuedAttribute(attribute.adminDescription, "adminDescription");
        DisplaySingleValuedAttribute(attribute.adminDescription, "adminDescription");
        DisplaySingleValuedAttribute(attribute.attributeID, "attributeID");
        DisplaySingleValuedAttribute(attribute.attributeSyntax, "attributeSyntax");
        DisplaySingleValuedAttribute(attribute.classDisplayName, "classDisplayName");
        DisplayMultiValuedAttribute(attribute.description, "description");
        DisplaySingleValuedAttribute(attribute.displayName, "displayName");
        DisplaySingleValuedAttribute(attribute.distinguishedName, "distinguishedName");
        DisplaySingleValuedAttribute(attribute.isDefunct, "isDefunct");
        DisplaySingleValuedAttribute(attribute.isMemberOfPartialAttributeSet, "isMemberOfPartialAttributeSet");
        DisplaySingleValuedAttribute(attribute.isSingleValued, "isSingleValued");
        DisplaySingleValuedAttribute(attribute.linkID, "linkID");
        DisplaySingleValuedAttribute(attribute.name, "name");
        DisplaySingleValuedAttribute(attribute.objectGuid, "objectGuid");
        DisplaySingleValuedAttribute(attribute.oMObjectClass, "oMObjectClass");
        DisplaySingleValuedAttribute(attribute.oMSyntax, "oMSyntax");
        DisplaySingleValuedAttribute(attribute.rangeLower, "rangeLower");
        DisplaySingleValuedAttribute(attribute.rangeUpper, "rangeUpper");
        DisplaySingleValuedAttribute(attribute.searchFlags, "searchFlags");
        DisplaySingleValuedAttribute(attribute.systemOnly, "systemOnly");
    }

The fifth example retrieves and displays all of the mapped attributes of all classSchema entries.

    // ----- Start of section to replace -----

    DisplayAllClasses(context);
            
    // ----- End of section to replace -----

and


    private static void DisplayAllClasses(DirectoryContext context)
    {
        var schemaQuery = context.Query<ClassSchema>();

        var schemaclasses = from a in schemaQuery
                            select a;

        foreach (var schemaClass in schemaclasses)
        {
            DisplaySchemaClass(schemaClass);
            Console.WriteLine();
        }
    }

    private static void DisplaySchemaClass(ClassSchema schemaClass)
    {
        DisplaySingleValuedAttribute(schemaClass.lDAPDisplayName, "lDAPDisplayName");
        DisplaySingleValuedAttribute(schemaClass.commonName, "commonName");
        DisplaySingleValuedAttribute(schemaClass.adminDescription, "adminDescription");
        DisplaySingleValuedAttribute(schemaClass.adminDisplayName, "adminDisplayName");
        DisplayMultiValuedAttribute(schemaClass.auxiliaryClass, "auxiliaryClass");
        DisplaySingleValuedAttribute(schemaClass.classDisplayName, "classDisplayName");
        DisplaySingleValuedAttribute(schemaClass.defaultHidingValue, "defaultHidingValue");
        DisplaySingleValuedAttribute(schemaClass.defaultObjectCategory, "defaultObjectCategory");
        DisplayMultiValuedAttribute(schemaClass.description, "description");
        DisplaySingleValuedAttribute(schemaClass.displayName, "displayName");
        DisplaySingleValuedAttribute(schemaClass.distinguishedName, "distinguishedName");
        DisplaySingleValuedAttribute(schemaClass.governsID, "governsID");
        DisplaySingleValuedAttribute(schemaClass.isDefunct, "isDefunct");
        DisplayMultiValuedAttribute(schemaClass.mayContain, "mayContain");
        DisplayMultiValuedAttribute(schemaClass.mustContain, "mustContain");
        DisplaySingleValuedAttribute(schemaClass.name, "name");
        DisplaySingleValuedAttribute(schemaClass.objectClassCategory, "objectClassCategory");
        DisplaySingleValuedAttribute(schemaClass.objectGuid, "objectGuid");
        DisplayMultiValuedAttribute(schemaClass.possSuperiors, "possSuperiors");
        DisplaySingleValuedAttribute(schemaClass.rDNAttID, "rDNAttID");
        DisplaySingleValuedAttribute(schemaClass.schemaIDGUID, "schemaIDGUID");
        DisplaySingleValuedAttribute(schemaClass.showInAdvancedViewOnly, "showInAdvancedViewOnly");
        DisplaySingleValuedAttribute(schemaClass.subClassOf, "subClassOf");
        DisplayMultiValuedAttribute(schemaClass.systemAuxiliaryClass, "systemAuxiliaryCLass");
        DisplayMultiValuedAttribute(schemaClass.systemMayContain, "systemMayContain");
        DisplayMultiValuedAttribute(schemaClass.systemMustContain, "systemMustContain");
        DisplaySingleValuedAttribute(schemaClass.systemOnly, "systemOnly");
        DisplayMultiValuedAttribute(schemaClass.systemPossSuperiors, "systemPossSuperiors");
    }

Top and TopX

This is what Top looks like, now. Note that I’ve added a namespace, AbstractClasses, and moved Top into it. The ‘Abstract’ part refers to classes that are abstract in the project rather than in Active Directory. I’ve also added an abstract class, TopX. It contains commonName which is actually part of top but is optional. It’s absent from organizationalUnit so it doesn’t make sense to have it in Top. It also contains description, in it’s multi-valued incarnation. See the class summaries for more info.

using System;
using System.Collections.ObjectModel;
using LinqToLdap.Mapping;

namespace ilminator.LinqToLdap.DirectoryObjects.AbstractClasses
{
    /// <summary>
    /// Classes should not normally inherit Top but should instead choose one of
    /// existing subclasses: <see cref="TopX"/>, <see cref="SecPrinMailRecipEtc"/> 
    /// or <see cref="OrganizationalUnitObject"/> as these have additional 
    /// attributes common to other classes.
    /// </summary>
    public abstract class Top : DirectoryObjectBase
    {
        /// <summary>
        /// The description displayed on admin screens.
        /// [From: http://msdn.microsoft.com/en-us/library/ms675213.aspx]
        /// RangeLower: 0
        /// RangeUpper: 1024
        /// </summary>
        public String adminDescription { get; set; }
        /// <summary>
        /// The name to be displayed on admin screens.
        /// [From: http://msdn.microsoft.com/en-us/library/ms675214.aspx]
        /// RangeLower: 1
        /// RangeUpper: 256
        /// </summary>
        public String adminDisplayName { get; set; }

        // description cannot be implemented here because it has different uses in 
        // Active Directory.  For SAM-managed objects, it is used as if it were
        // defined as single-valued.  For other objects, it is used as defined, 
        // which is as a multi-valued object.  For this reason, I think it makes
        // sense to implement it differently for SAM- and non-SAM-managed objects.
        // See the definitions of SecPrinMailRecipEtc and OrganizationalUnit for
        // examples.
        // Ref: http://msdn.microsoft.com/en-us/library/windows/desktop/ms675492.aspx.

        private string _displayName;
        /// <summary>
        /// Display-Name.  The display name for an object. This is usually 
        /// the combination of the users first name, middle initial, and last name.
        /// [From: http://msdn.microsoft.com/en-us/library/windows/desktop/ms675514.aspx]
        /// </summary>
        public String displayName
        {
            get { return this._displayName; }
            set
            {
                if (value != this._displayName)
                {
                    this._displayName = value;
                    AttributeChanged<Top, String>(a => a.displayName);
                }
            }
        }

        /// <summary>
        /// Obj-Dist-Name.
        /// </summary>
        public String distinguishedName { get; set; }
        /// <summary>
        /// RDN.  The Relative Distinguished Name of an object.
        /// [From: http://msdn.microsoft.com/en-us/library/windows/desktop/ms678697.aspx]
        /// </summary>
        public String name { get; set; }
        /// <summary>
        /// Object-Guid.  The unique identifier for an object.
        /// [From: http://msdn.microsoft.com/en-us/library/windows/desktop/ms679021.aspx]
        /// </summary>
        public Guid objectGuid { get; set; }
        /// <summary>
        /// USN-Changed.  USN value assigned by the local directory for the 
        /// latest change, including creation.
        /// [From: http://msdn.microsoft.com/en-us/library/windows/desktop/ms680871.aspx]
        /// </summary>
        public Int64 uSNChanged { get; set; }
        /// <summary>
        /// USN-Created.  USN-Changed value assigned at object creation.
        /// [From: http://msdn.microsoft.com/en-us/library/windows/desktop/ms680874.aspx]
        /// </summary>
        public Int64 uSNCreated { get; set; }
        /// <summary>
        /// When-Changed.  The date when this object was last changed. This 
        /// value is not replicated and exists in the global catalog.
        /// [From: http://msdn.microsoft.com/en-us/library/windows/desktop/ms680921.aspx"]
        /// </summary>
        public DateTime whenChanged { get; set; }
        /// <summary>
        /// When-Created.  The date when this object was created. This value
        /// is replicated and is in the global catalog.
        /// [From: http://msdn.microsoft.com/en-us/library/windows/desktop/ms680924.aspx"]
        /// </summary>
        public DateTime whenCreated { get; set; }

        private string _wWWHomePage;
        /// <summary>
        /// WWW-HomePage.  The primary webpage.
        /// [From: http://msdn.microsoft.com/en-us/library/windows/desktop/ms680927.aspx]
        /// </summary>
        public String wWWHomePage
        {
            get { return this._wWWHomePage; }
            set
            {
                if (value != this._wWWHomePage)
                {
                    this._wWWHomePage = value;
                    AttributeChanged<Top, String>(a => a.wWWHomePage);
                }
            }
        }
    }
}

and TopX:

using System;
using System.Collections.ObjectModel;

namespace ilminator.LinqToLdap.DirectoryObjects.AbstractClasses
{
    /// <summary>
    /// Top, with additional attributes.  Non-SAM classes should
    /// normally use this class as their parent class.
    /// </summary>
    public abstract class TopX : Top
    {
        public String commonName { get; set; }  // Top (but optional)

        private Collection<string> _description;     // top (see note on Top)
        /// <summary>
        /// Description.  Contains the description to display for an object. In 
        /// Active Directory, this value is restricted as single-valued for 
        /// backward compatibility in some cases but is allowed to be multi-valued
        /// in others. See 'Remarks' on the follow link.
        /// [From: http://msdn.microsoft.com/en-us/library/windows/desktop/ms675492.aspx]
        /// </summary>
        public Collection<String> description
        {
            get { return this._description; }
            set
            {
                if (value != this._description)
                {
                    this._description = value;
                    AttributeChanged<TopX, Collection<String>>(a => a.description);
                }
            }
        }
    }
}

ClassSchema and ClassSchemaMap

First, ClassSchema. Note that I’ve created a namespace, SchemaClasses, to hold the directory object classes for attributes and classes.

using System;
using System.Collections.ObjectModel;

namespace ilminator.LinqToLdap.DirectoryObjects.SchemaClasses
{
    public class ClassSchema : ilminator.LinqToLdap.DirectoryObjects.AbstractClasses.TopX
    {
        public Collection<String> auxiliaryClass { get; set; }
        public String classDisplayName { get; set; }
        public Boolean defaultHidingValue { get; set; }
        public String defaultObjectCategory { get; set; }
        public String governsID { get; set; }
        public Nullable<Boolean> isDefunct { get; set; }
        public String lDAPDisplayName { get; set; }
        public Collection<String> mayContain { get; set; }
        public Collection<String> mustContain { get; set; }
        public Int32 objectClassCategory { get; set; }
        public Collection<String> possSuperiors { get; set; }
        public String rDNAttID { get; set; }
        public Guid schemaIDGUID { get; set; }
        public Boolean showInAdvancedViewOnly { get; set; }
        public String subClassOf { get; set; }
        public Collection<String> systemAuxiliaryClass { get; set; }
        public Collection<String> systemMayContain { get; set; }
        public Collection<String> systemMustContain { get; set; }
        public Boolean systemOnly { get; set; }
        public Collection<String> systemPossSuperiors { get; set; }
    }
}

then, ClassSchemaMap

using System;
using LinqToLdap.Mapping;

namespace ilminator.LinqToLdap.DirectoryObjects.ClassMaps
{
    public class ClassSchemaMap : ClassMap<ilminator.LinqToLdap.DirectoryObjects.SchemaClasses.ClassSchema>
    {
        public ClassSchemaMap()
        {
            NamingContext("CN=Schema,CN=Configuration,DC=big,DC=wooden,DC=badger");   //   Replace with your defaultNamingContext
            ObjectClass("classSchema");
            ObjectCategory("classSchema");

            // From ClassSchema
            Map(x => x.auxiliaryClass).StoreGenerated();
            Map(x => x.classDisplayName).StoreGenerated();
            Map(x => x.defaultHidingValue).StoreGenerated();
            Map(x => x.defaultObjectCategory).StoreGenerated();
            Map(x => x.governsID).StoreGenerated();
            Map(x => x.isDefunct).StoreGenerated();
            Map(x => x.lDAPDisplayName).StoreGenerated();
            Map(x => x.mayContain).StoreGenerated();
            Map(x => x.mustContain).StoreGenerated();
            Map(x => x.objectClassCategory).StoreGenerated();
            Map(x => x.possSuperiors).StoreGenerated();
            Map(x => x.rDNAttID).StoreGenerated();
            Map(x => x.schemaIDGUID).StoreGenerated();
            Map(x => x.showInAdvancedViewOnly).StoreGenerated();
            Map(x => x.subClassOf).StoreGenerated();
            Map(x => x.systemAuxiliaryClass).StoreGenerated();
            Map(x => x.systemMayContain).StoreGenerated();
            Map(x => x.systemMustContain).StoreGenerated();
            Map(x => x.systemOnly).StoreGenerated();
            Map(x => x.systemPossSuperiors).StoreGenerated();

            // From TopX
            Map(x => x.commonName).Named("cn").StoreGenerated();
            Map(x => x.description).StoreGenerated();

            // From Top
            DistinguishedName(x => x.distinguishedName);

            Map(x => x.adminDescription).StoreGenerated();
            Map(x => x.adminDisplayName).StoreGenerated();
            Map(x => x.displayName).StoreGenerated();
            Map(x => x.name).StoreGenerated();
            Map(x => x.objectGuid).StoreGenerated();
            Map(x => x.uSNChanged).StoreGenerated();
            Map(x => x.uSNCreated).StoreGenerated();
            Map(x => x.whenChanged).StoreGenerated();
            Map(x => x.whenCreated).StoreGenerated();
            Map(x => x.wWWHomePage).StoreGenerated();
        }
    }
}

AttributeSchema and AttributeSchemaMap

Now, AttributeSchema.

using System;

namespace ilminator.LinqToLdap.DirectoryObjects.SchemaClasses
{
    public class AttributeSchema : ilminator.LinqToLdap.DirectoryObjects.AbstractClasses.TopX
    {
        public AttributeSchema() { }

        public String attributeID { get; set; }
        public String attributeSyntax { get; set; }
        public String classDisplayName { get; set; }
        public Nullable<Boolean> isDefunct { get; set; }
        public Nullable<Boolean> isMemberOfPartialAttributeSet { get; set; }
        public Boolean isSingleValued { get; set; }
        public String lDAPDisplayName { get; set; }
        public Nullable<Int32> linkID { get; set; }
        public Byte[] oMObjectClass { get; set; }
        public Int32 oMSyntax { get; set; }
        public Nullable<Int32> rangeLower { get; set; }
        public Nullable<Int32> rangeUpper { get; set; }
        public Int32 searchFlags { get; set; }
        public Boolean systemOnly { get; set; }
    }
}

then, AttributeSchemaMap:

using System;
using LinqToLdap.Mapping;

namespace ilminator.LinqToLdap.DirectoryObjects.ClassMaps
{
    public class AttributeSchemaMap : ClassMap<ilminator.LinqToLdap.DirectoryObjects.SchemaClasses.AttributeSchema>
    {
        public AttributeSchemaMap()
        {
            NamingContext("CN=Schema,CN=Configuration,DC=big,DC=wooden,DC=badger");   //   Replace with your defaultNamingContext
            ObjectClass("attributeSchema");
            ObjectCategory("attributeSchema");

            // From AttributeSchema

            Map(x => x.attributeID).StoreGenerated();
            Map(x => x.attributeSyntax).StoreGenerated();
            Map(x => x.classDisplayName).StoreGenerated();
            Map(x => x.isDefunct).StoreGenerated();
            Map(x => x.isMemberOfPartialAttributeSet).StoreGenerated();
            Map(x => x.isSingleValued).StoreGenerated();
            Map(x => x.lDAPDisplayName).StoreGenerated();
            Map(x => x.linkID).StoreGenerated();
            Map(x => x.oMObjectClass).StoreGenerated();
            Map(x => x.oMSyntax).StoreGenerated();
            Map(x => x.rangeLower).StoreGenerated();
            Map(x => x.rangeUpper).StoreGenerated();
            Map(x => x.searchFlags).StoreGenerated();
            Map(x => x.systemOnly).StoreGenerated();

            // From TopX
            Map(x => x.commonName).Named("cn").StoreGenerated();
            Map(x => x.description).StoreGenerated();

            // From Top
            DistinguishedName(x => x.distinguishedName);

            Map(x => x.adminDescription).StoreGenerated();
            Map(x => x.adminDisplayName).StoreGenerated();
            Map(x => x.displayName).StoreGenerated();
            Map(x => x.name).StoreGenerated();
            Map(x => x.objectGuid).StoreGenerated();
            Map(x => x.uSNChanged).StoreGenerated();
            Map(x => x.uSNCreated).StoreGenerated();
            Map(x => x.whenChanged).StoreGenerated();
            Map(x => x.whenCreated).StoreGenerated();
            Map(x => x.wWWHomePage).StoreGenerated();
        }
    }
}

Note that the NamingContexts on both ClassMaps have been set to the forest schemaNamingContext.

Summary

It only took me a few minutes to set these up and now I can query the schema the same way I run other queries.  I hope you find it as useful as I have.

Retrieving move than 500 objects with LinqToLdap

By default, LinqToLdap sets the page size to be 500 objects, meaning standard queries will only return the first 500 objects.

This article builds on the article OrganizationalUnit CRUD and uses the same OrganizationalUnitObject and OrganizationalUnitObjectMap classes.

Before we start, it’s probably a good idea to make sure you’ve got a large number of OUs to play with. If you haven’t, you’ll have to adapt the code to work with objects you have got. Or wait until I write about users and groups. ADU&C will show all that there are: I suggest you go to the top of your domain and use the Find dialog to get a count. I had about 8,000.

So, what do we get if we don’t do anything special? Here’s the Program.cs to start with:

using System;
using System.Linq;
using LinqToLdap;

namespace BlogOuTest1
{
    class Program
    {
        static void Main(string[] args)
        {
            var config = new LdapConfiguration()
                .AddMapping(new OrganizationalUnitObjectMap());
            config.ConfigureFactory("dc1");

            var counter = 0;
            var context = new DirectoryContext();

            // ----- Start of section to replace -----
            var allOUsQuery = context.Query();

            var ous = from o in allOUsQuery
                      select o;

            foreach (var ou in ous)
            {
                counter++;
                Console.WriteLine("Ou: {0}",
                    ou.organizationalUnitName);
            }
            // ----- End of section to replace -----

            Console.WriteLine("Counter = {0}", counter);
            Console.WriteLine("\r\nPress a key to continue...");
            Console.ReadKey(true);
        }
    }
}

With this, I get a list of OUs and then

Counter = 500

Which was as expected because the default page size in LinqToLdap is 500. So, how do we get more?

Since the Active Directory default maximum page size, on the server, is 1000 we could increase the page size used by LinqToLdap. If you replace the config definition with the following, you’ll get 1000 objects returned:

    var config = new LdapConfiguration()
        .AddMapping(new OrganizationalUnitObjectMap())
        .MaxPageSizeIs(1000);

It’s better, but still not what we’re after. To get the rest, we’re going to have to get the server to page through the data. So, how do we do that in LinqToLdap? First, I’m going to revert to the original (LinqToLdap) default page size of 500 by removing the MaxPageSizeIs(1000) line.

Now, you have two choices. If you just want to retrieve all objects, you can use InPagesOf(n); if you want to process the objects as you retrieve them, you can use ToPage().

InPagesOf() – retrieving the objects in one pass

You can use a query like this to retrieve all of the objects in one pass:

using System;
using System.Collections.Generic;
using System.DirectoryServices.Protocols;
using System.Linq;
using LinqToLdap;
using LinqToLdap.Collections;

namespace BlogOuTest3
{
    class Program
    {
        static void Main(string[] args)
        {
            var config = new LdapConfiguration()
                .AddMapping(new OrganizationalUnitObjectMap());
            config.ConfigureFactory("dc1");

            var context = new DirectoryContext();

            // ----- Start of section to replace -----

            var ous = context.Query<OrganizationalUnitObject>()
                .Select(u => u.organizationalUnitName)
                .InPagesOf(10);

            var counter = 0;
            foreach (var ou in ous)
            {
                counter++;
                Console.WriteLine("Ou: {0}",ou);
            }

            // ----- End of section to replace -----

            Console.WriteLine("Counter = {0}", counter);

            Console.WriteLine("\r\nPress a key to continue...");
            Console.ReadKey(true);
        }
    }
}

I’ve chosen a silly page size for this one, so it’ll page even if you’ve only a handful of OUs. There is a scenario where this won’t work and it’s one that had me scratching my head for quite a while. So far, I’ve been chosing the root container (the domainDNS container, otherwise known as the defaultNamingContext) of my domain as the NamingContext on the ClassMaps, for example:

NamingContext("DC=big,DC=wooden,DC=badger");

But for searches to work against the root container, you need another search control:

SearchOptionsControl(SearchOption.DomainScope)

So the query statement should look like this:

var ous = context.Query<OrganizationalUnitObject>()
    .WithControls(new[] { new SearchOptionsControl(SearchOption.DomainScope) })
    .Select(u => u.organizationalUnitName)
    .InPagesOf(10);

I eventually found this here. It doesn’t say why and I still haven’t worked that out. I’ve run individual searches against every OU and container in the root container and they all work without the control, so it’s something to do with searching the root container itself. I’ll update if I ever sort it out.

ToPage() – processing objects in pages

You can use ToPage() in a query to process the objects in pages as you retrieve them. Here’s an example. First, I’ve added a method to the Program class:

   private static int ProcessPage(int counter, ILdapPage<string> page)
    {
        if (page.Count > 0)
        {
            foreach (var ou in page.ToList())
            {
                counter++;
                Console.WriteLine("Ou: {0}", ou);
            }
            System.Threading.Thread.Sleep(500);
        }

        return counter;
    }

All it does is display the results and increment a counter, then sleep for half a sec so you can see that it’s happening page by page.

Then, I’ve replaced the section between the comments, in the code sample above, with the following:

    // ----- Start of section to replace -----

    var counter = 0;

    ILdapPage<string> page = context.Query<OrganizationalUnitObject>()
        .WithControls(new[] { new SearchOptionsControl(SearchOption.DomainScope) })
        .Select(o => o.distinguishedName)
        .ToPage(10);

    counter = ProcessPage(counter, page);

    while (page.HasNextPage)
    {
        var pageSize = page.PageSize;
        var nextPage = page.NextPage;

        page = context.Query<OrganizationalUnitObject>()
            .WithControls(new[] { new SearchOptionsControl(SearchOption.DomainScope) })
            .Select(o => o.distinguishedName)
            .ToPage(pageSize, nextPage);

        counter = ProcessPage(counter, page);
    }

    // ----- End of section to replace -----

When you run the code you should see the results appear a page at a time.

Attribute Scope Queries using LinqToLdap

Attribute Scope Queries can be very useful – given a directory object, say a group, they allow you to search objects that are associated with it by distinguishedName and return their properties.

Using attribute scope queries, you can (for example):

  • return the mail properties of the members of a group;
  • return the sAMAccountName and telephoneNumber property of a user’s manager.
  • return the lastLogonTimestamp of the accounts listed in a user’s seeAlso attribute.

Here are some examples. They use the usual project definition:

This first example, shows how to extraact the  cn, sAMAccountName and email addresses (mail) of the members of a group. Replace Program.cs with this:

using System;
using System.Linq;
using LinqToLdap;

namespace AttributeScopeQuery1
{
    class Program
    {
        static void Main(string[] args)
        {
            var config = new LdapConfiguration();
            config.ConfigureFactory("dc1");    // Insert your DC name or domain DNS name here

            var context = new DirectoryContext();

            // ----- Start of section to replace -----

            var distinguishedName = "CN=Group1,OU=Test Groups,DC=big,DC=wooden,DC=badger";
            var response = context.Query(distinguishedName, System.DirectoryServices.Protocols.SearchScope.Base)
                .WithControls(new[] { new System.DirectoryServices.Protocols.AsqRequestControl("member") })
                .Where(u => Filter.Equal(u, "objectclass", "user"))
                .Select("cn", "sAMAccountName", "mail");

            foreach (var item in response)
            {
                var dirAtts = item as LinqToLdap.Collections.DirectoryAttributes;
                Console.WriteLine("DN: {0}", dirAtts.DistinguishedName);
                foreach (var dirAtt in dirAtts)
                {
                    Console.WriteLine("Key: {0:30} Value:{1}", dirAtt.Key, dirAtt.Value);
                }
                Console.WriteLine();
            }

            // ----- End of section to replace -----

            Console.WriteLine("\r\nPress a key to continue...");
            Console.ReadKey(true);
        }
    }
}

Each query returns a set of DirectoryAttributes collections. Each DirectoryAttributes collection is a set of KeyValuePairs<string, object>, the string being the attribute name and the object being the attribute value.

The second example repeats the above query but processes the results differently: an excuse to show how to gather the results from the set of DirectoryAttributes collections into a single set. Let’s say you need the three attributes for three separate purposes. You can still process it as a single query and use ToList() to run it once. You can then use queries on the results of the ToList() as in the ‘var emailAddresses =’ query below to pull out those values for processing. (I haven’t shown queries for cn and sAMAccountName but they’d be similar.)  You need an IgnoreCase StringComparison option if you tend to type the descriptions as they are in AD, such as sAMAccountName, so I include it here even though it’s not necessasry in this case.

    // ----- Start of section to replace -----

    var distinguishedName = "CN=Group1,OU=Test Groups,DC=big,DC=wooden,DC=badger";
    var response = context.Query(distinguishedName, System.DirectoryServices.Protocols.SearchScope.Base)
        .WithControls(new[] { new System.DirectoryServices.Protocols.AsqRequestControl("member") })
        .Where(u => Filter.Equal(u, "objectclass", "user"))
        .Select("cn", "sAMAccountName", "mail");

    var attributes = response.ToList();

    var emailAddresses = attributes.SelectMany(collection => collection
        .Where(kvp => kvp.Key.Equals("mail", StringComparison.CurrentCultureIgnoreCase))
        .Select(kvp => kvp.Value));

    foreach (var email in emailAddresses)
    {
        Console.WriteLine(email);
    }

    // ----- End of section to replace -----

The third example is another variation on the original. This time, only a single attribute, mail, is retrieved from the group members. They’re pulled into a single set of results in an additional query.

    // ----- Start of section to replace -----

    var distinguishedName = "CN=Group1,OU=Test Groups,DC=big,DC=wooden,DC=badger";
    var response = context.Query(distinguishedName, System.DirectoryServices.Protocols.SearchScope.Base)
        .WithControls(new[] { new System.DirectoryServices.Protocols.AsqRequestControl("member") })
        .Where(u => Filter.Equal(u, "objectclass", "user"))
        .Select("mail");

    var attributes = response.ToList();

    var emailAddresses = attributes.SelectMany(collection => collection.Select(kvp => kvp.Value));

    foreach (var email in emailAddresses)
    {
        Console.WriteLine(email);
    }

    // ----- End of section to replace -----

You could put the two queries together into a single statement:

    var emailAddresses = context.Query(distinguishedName, System.DirectoryServices.Protocols.SearchScope.Base)
        .WithControls(new[] { new System.DirectoryServices.Protocols.AsqRequestControl("member") })
        .Where(u => Filter.Equal(u, "objectclass", "user"))
        .Select("mail")
        .ToList()
        .SelectMany(collection => collection.Select(kvp => kvp.Value));

but it feels a bit messy to me, with the ToList() in the middle.

The final example shows how to query a user to get the telephoneNumber of the user’s manager.

    // ----- Start of section to replace -----

    var user = "CN=Test User 1,OU=Test Users,DC=big,DC=wooden,DC=badger";
    var telephoneNumber = context.Query(user, System.DirectoryServices.Protocols.SearchScope.Base)
        .WithControls(new[] { new System.DirectoryServices.Protocols.AsqRequestControl("manager") })
        .Where(u => Filter.Equal(u, "objectclass", "user"))
        .Select("telephoneNumber")
        .SingleOrDefault()
        .SingleOrDefault().Value.ToString();

    Console.WriteLine(telephoneNumber);

    // ----- End of section to replace -----

Further reading:

Introduction to System.DirectoryServices.Protocols (S.DS.P): Creating an Attribute Scoped Query (ASQ)

DirectorySearcher.AttributeScopeQuery Property

 

Creating a directory objects assembly for LinqToLdap

Introduction

This post was originally just going to be the first half of the next one but I thought a few people might find it useful on its own.  It describes creating a class library assembly containing the definitions of:

  • GroupObject
  • OrganizationalUnitObject
  • UserObject

and their ClassMaps.  It’s not meant to be prescriptive; this is just how I decided to create such an assembly and my thoughts on why I did a couple of things.

First, I’ll talk about the attribute type definitions in general, then, I’ll talk about some specific attribute type definitions and, finally, I’ll show you how to create the assembly.

Attribute type definitions

In this section I discuss the options available to you when mapping some LDAP attributes types to CLR types.

Integers and Large Integers/Intervals

For Integers, you have a choice of

  • Int32
  • Nullable<Int32>

For Intervals, you have a choice of:

  • Int64
  • Nullable<Int64>
  • DateTime
  • Nullable<DateTime>

Given the extra hassle of dealing with nullables, why would you do so?

When you create a user on a domain controller, Active Directory will set certain values for you. For example,

accountExpires = 9223372036854775807 [Large Integer/Interval]
badPasswordTime = 0 [Large Integer/Interval]
badPwdCount = 0 [Integer]
logonCount = 0 [Integer]
pwdLastSet = 0 [Large Integer/Interval]

If you check another domain controller you’ll find some of them don’t have values because AD doesn’t replicate some attributes:

accountExpires = 9223372036854775807
badPasswordTime = <not set>
badPwdCount = <not set>
logonCount = <not set>
pwdLastSet = 0

For the Integers, if you map them as Int32, the <not set> will be rendered as zero. If it’s important to you to know when they’re <not set> and when they’re actually zero, you need to map them as Nullable<Int32>.

Likewise with the Large Integer/Intervals, if you map them as Int64, the <not set> will be rendered as zero, so if you need to know when they’re <not set>, you need to map them as Nullable<Int64>.

If they are Intervals (such as the ones listed above) you also have the choice of mapping them as DateTime or Nullable<DateTime> (using .DateTimeFormat(null) in the ClassMap).

  • If you map them as DateTime, if the value is
    • <not set>, you’ll see “01/01/0001 00:00:00”;
    • zero you’ll see “01/01/1601 00:00:00”
  • If you map them as Nullable<DateTime> and the value is
    • <not set>, you’ll see null;
    • zero you’ll see “01/01/1601 00:00:00”.

So, with mapping to DateTime you don’t need Nullable to know whether the directory value was set but to me, at least, if feels cleaner to test for HasValue than = “01/01/0001 00:00:00”.

Multi-valued Unicode String attributes

You have a choice when mapping multi-valued Unicode String directory attributes.  You can, and generally should, map them as some form of Collection<String>.  But, if you’re only ever going store and retrieve a single value, you can map them as a simple String.  If you do so and there is more than one value in the attribute and you try to read it, you’ll see only one of the values.  If you try to update it, the update will replace all values in the attribute with the single value provided.

Specific attribute type definitions

So, now you’ve seen the options, I’ll discuss what I actually chose for specific attributes in my current model and why.  I’ll go class by class.

SecPrinMailRecipEtc

description

I’ve mapped description to String even though it’s described in the schema as a multi-valued attribute.  description is restricted to single values for SAM-managed classes (e.g. user and group) for backwards compatibility, because the attribute was so-defined in the SAM APIs which pre-date Active Directory.  You can read more, here.

UserObject

accountExpires

I’ve mapped accountExpires to Int64 because it always has a value and because its default value (0x7FFFFFFFFFFFFFFF) cannot be directly mapped to a valid DateTime using DateTime.FromFileTime(Int64).

badPasswordTime

I’ve mapped badPasswordTime to Nullable<DateTime> because it can be <not set> and because when I use it, it’ll be to know the date/time of the last bad password on this DC.

badPwdCount

I’ve mapped badPwdCount to Nullable<Int32> because it can be <not set> and because I’ve no reason not to do so.

c, co and countryCode

These three attributes are ‘linked’ in ADU&C, in that setting Country/region: sets all three values.  In the directory itself, they are not linked and can be set and cleared independently.  Setting c, will result in ADU&C displaying the country; setting either or both of the other two will not.

jpegPhoto

I’ve mapped jpegPhoto to Byte[].  You can convert the bytes to a bitmap using something like:

var bitmap2 = new Bitmap(new MemoryStream(user.jpegPhoto));

lastLogonTimestamp

I’ve mapped lastLogonTimestamp to Nullable<DateTime> because it can be <not set> and because when I use it it’ll be to know the approximate date/time of the last logon.

logonCount

I’ve mapped logonCount to Nullable<Int32> because it can be <not set> and because I’ve no reason not to do so.

pwdLastSet

I’ve mapped pwdLastSet to Int64 because it always has a value – it’s never <not set>.  I could’ve mapped it to DateTime and used .DateTimeFormat(null) but if I use this at all it’ll most likely be to see if the user needs to change their password at next logon.  To do this, I’ll be checking for pwdLastSet = 0 (and userAccountControl doesn’t contain UF_DONT_EXPIRE_PASSWD) so it’s easier to have it as an Int64.  On those occasions when I need the date/time I can always convert it using DateTime.FromFileTime(Int64).

thumbnailPhoto

I’ve mapped thumbnailPhoto to Byte[].  You can convert it to a bitmap using something like:

var bitmap = new Bitmap(new MemoryStream(user.thumbnailPhoto));

unicodePwd

I’ve mapped unicodePwd to a Byte[].  To set this value, use something like this:

/// <summary> 
/// Returns a unicode-encoded byte array based on the incoming password string.
/// </summary>
/// The password to turn into a byte array
public byte[] GetPasswordData(string password)
{
    var formattedPassword = String.Format("\"{0}\"", password);
    return Encoding.Unicode.GetBytes(formattedPassword);
}

I found this, here.  Note that you must be connected over SSL to set this value in Active Directory.

userAccountControl

I’ve mapped userAccountControl to Int32 with the expectation that I’ll cast to and from an enum defined with FlagsAttribute, called UserAccountControlFlags.  See the project files below for more details.

GroupObject

groupType

I’ve mapped groupType to an enum called GroupTypes (see below for definition).  Why have I not used Int32 and  FlagsAttribute on the enum?  Although the values are defined in powers of two and it’s possible to use bitwise operations on them, I’m more confident that I can defined all of the required combinations and make use of LinqToLdap’s .EnumStoredAsInt() method to simplify things.

OrganizationalUnitObject

description

I’ve mapped description as a Collection<String>.  See the explanation of description for UserObject for details.

Create the assembly

First, create a new Class Library project.  I called mine ilminator.LinqToLdap.DirectoryObjects because I like meaningful names for my projects, namespaces and assemblies and I tend to keep them all in line.

Now, add a reference to LinqToLdap.

Next, add a folder to the project, called ClassMaps.  To do this, right-click on the project name and select Add > New Folder.

Enter the name ClassMaps.

There are two reasons to do this: first, if you end up with a lot of directory object classes and ClassMaps, it’ll make them easier to find and manage; and second, by putting all of the ClassMaps in their own namespace, if you only add a using statement for the main namespace, IntelliSense won’t be cluttered with the ClassMap names.

Next, add a class file to the ClassMaps folder, called OrganizationalUnitObjectMap.cs.  Add another called GroupObjectMap.cs.  Add a third called UserObjectMap.cs.

Next, add class files to the main part of the project called:

  1. GroupObject.cs
  2. GroupTypes.cs
  3. OrganizationalUnitObject.cs
  4. SecPrinMailRecipEtc.cs
  5. Top.cs
  6. UserAccountControlFlags.cs
  7. UserObject.cs

So, why the extra files, Top and SecPrinMailRecipEtc, and why the odd name?

As you probably know Active Directory implements an inheritance model.  Unlike C#, AD supports multiple-inheritance through something called auxiliary classes.  For example, user inherits from mailRecipient, posixAccount, securityPrincipal and shadowAccount.  This is tricky to replicate in C#, so for this article I fudged it.  🙂

Everything that comes from top is in Top (except description – see the class file comments for the details).  Stuff that was common to group and user went in SecPrinMailRecipEtc.  If I were implementing other classes, I would probably have to be a bit cleverer than that.  cn which belongs in top (but is optional) went in SecPrinMailRecipEtc because organizationalUnit doesn’t have a cn attribute.  You’ve probably figured out why the odd name, by now.

Your project should now look something like this:

The next step is to add the code.

Top.cs

using System;
using System.Collections.ObjectModel;
using LinqToLdap.Mapping;

namespace ilminator.LinqToLdap.DirectoryObjects
{
    public abstract class Top : DirectoryObjectBase
    {
        // description cannot be implemented here because it has different uses in
        // Active Directory.  For SAM-managed objects, it is used as if it were
        // defined as single-valued.  For other objects, it is used as defined,
        // which is as a multi-valued object.  For this reason, I think it makes
        // sense to implement it differently for SAM- and non-SAM-managed objects.
        // See the definitions of SecPrinMailRecipEtc and OrganizationalUnit for
        // examples.
        // Ref: http://msdn.microsoft.com/en-us/library/windows/desktop/ms675492.aspx.

        private string _displayName;
        /// <summary>
        /// Display-Name. The display name for an object. This is usually
        /// the combination of the users first name, middle initial, and last name.
        /// [From: http://msdn.microsoft.com/en-us/library/windows/desktop/ms675514.aspx]
        /// </summary>
        public String displayName
        {
            get { return this._displayName; }
            set
            {
                if (value != this._displayName)
                {
                    this._displayName = value;
                    AttributeChanged(a => a.displayName);
                }
            }
        }

        /// <summary> 
        /// Obj-Dist-Name.
        /// </summary>
        public String distinguishedName { get; set; }
        /// <summary>
        /// RDN. The Relative Distinguished Name of an object.
        /// [From: http://msdn.microsoft.com/en-us/library/windows/desktop/ms678697.aspx]
        /// </summary>
        public String name { get; set; }
        /// <summary>
        /// Object-Guid. The unique identifier for an object.
        /// [From: http://msdn.microsoft.com/en-us/library/windows/desktop/ms679021.aspx]
        /// </summary>
        public Guid objectGuid { get; set; }
        /// <summary>
        /// USN-Changed. USN value assigned by the local directory for the
        /// latest change, including creation.
        /// [From: http://msdn.microsoft.com/en-us/library/windows/desktop/ms680871.aspx]
        /// </summary>
        public Int64 uSNChanged { get; set; }
        ///<summary>
        /// USN-Created. USN-Changed value assigned at object creation.
        /// [From: http://msdn.microsoft.com/en-us/library/windows/desktop/ms680874.aspx]
        /// </summary>
        public Int64 uSNCreated { get; set; }
        /// <summary>
        /// When-Changed. The date when this object was last changed. This
        /// value is not replicated and exists in the global catalog.
        /// [From: http://msdn.microsoft.com/en-us/library/windows/desktop/ms680921.aspx"]
        /// </summary>
        public DateTime whenChanged { get; set; }
        ///<summary> 
        /// When-Created. The date when this object was created. This value
        /// is replicated and is in the global catalog.
        /// [From: http://msdn.microsoft.com/en-us/library/windows/desktop/ms680924.aspx"]
        /// </summary>
        public DateTime whenCreated { get; set; }

        private string _wWWHomePage;
        /// <summary>
        /// WWW-HomePage. The primary webpage.
        /// [From: http://msdn.microsoft.com/en-us/library/windows/desktop/ms680927.aspx]
        /// </summary>
        public String wWWHomePage
        {
            get { return this._wWWHomePage; }
            set
            {
                if (value != this._wWWHomePage)
                {
                    this._wWWHomePage = value;
                    AttributeChanged(a => a.wWWHomePage);
                }
            }
        }
    }
}

SecPrinMailRecipEtc.cs

using System;

namespace ilminator.LinqToLdap.DirectoryObjects
{
    public abstract class SecPrinMailRecipEtc : Top
    {
        public String commonName { get; set; }  // Top (but optional)

        private string _description;
        /// <summary>
        /// Description. Contains the description to display for an object. In
        /// Active Directory, this value is restricted as single-valued for
        /// backward compatibility in some cases but is allowed to be multi-valued
        /// in others. See 'Remarks' on the follow link.
        /// [From: http://msdn.microsoft.com/en-us/library/windows/desktop/ms675492.aspx]
        /// </summary>
        public String description
        {
            get { return this._description; }
            set
            {
                if (value != this._description)
                {
                    this._description = value;
                    this.AttributeChanged(a => a.description);
                }
            }
        }

        private string _info;                   // mailRecipient
        /// <summary>
        /// A comment attribute, shown as 'Notes' in ADU&C for users and groups.
        /// </summary>
        public String info
        {
            get { return this._info; }
            set
            {
                if (value != this._info)
                {
                    this._info = value;
                    AttributeChanged(a => a.info);
                }
            }
        }

        private string _mail;                   // group
        /// <summary>
        /// E-mail-Address.
        /// </summary>
        public String mail
        {
            get { return this._mail; }
            set
            {
                if (value != this._mail)
                {
                    this._mail = value;
                    AttributeChanged(a => a.mail);
                }
            }
        }

        /// <summary>
        /// Object-Sid.
        /// </summary>
        public System.Security.Principal.SecurityIdentifier objectSid { get; set; } // securityPrincipal

        private string _sAMAccountName;          // securityPrincipal
        public String sAMAccountName
        {
            get { return this._sAMAccountName; }
            set
            {
                if (value != this._sAMAccountName)
                {
                    this._sAMAccountName = value;
                    AttributeChanged(a => a.sAMAccountName);
                }
            }
        }

        /// <summary>
        /// SAM-Account-Type. This attribute contains information about
        /// every account type object.
        /// [From: http://msdn.microsoft.com/en-us/library/windows/desktop/ms679637.aspx]
        /// </summary>
        public Int32 sAMAccountType { get; set;}  // securityPrincipal

        private string _telephoneNumber;        // mailRecipient
        public String telephoneNumber
        {
            get { return this._telephoneNumber; }
            set
            {
                if (value != this._telephoneNumber)
                {
                    this._telephoneNumber = value;
                    AttributeChanged(a => a.telephoneNumber);
                }
            }
        }
    }
}

GroupObject.cs

using System;
using System.Collections.ObjectModel;
using LinqToLdap.Mapping;

namespace ilminator.LinqToLdap.DirectoryObjects
{
    public class GroupObject : SecPrinMailRecipEtc
    {
        private GroupTypes _groupType;          // group
        /// <summary>
        /// Group-Type. Contains a set of flags that define the type and scope
        /// of a group object. For the possible values for this attribute,
        /// see Remarks in the following link.
        /// [From: http://msdn.microsoft.com/en-us/library/windows/desktop/ms675935.aspx]
        /// </summary>
        public GroupTypes groupType
        {
            get { return this._groupType; }
            set
            {
                if (value != this._groupType)
                {
                    this._groupType = value;
                    AttributeChanged(a => a.groupType);
                }
            }
        }

        private string _managedBy;              // group
        /// <summary> 
        /// The distinguished name of the user that is assigned to manage this object.
        /// </summary>
        public String managedBy
        {
            get { return this._managedBy; }
            set
            {
                if (value != this._managedBy)
                {
                    this._managedBy = value;
                    AttributeChanged(a => a.managedBy);
                }
            }
        }

        private Collection _member;     // group
        /// <summary>
        /// Member. The list of users that belong to the group.
        /// [From: http://msdn.microsoft.com/en-us/library/windows/desktop/ms677097.aspx]
        /// </summary>
        public Collection member
        {
            get { return this._member; }
            set
            {
                if (value != this._member)
                {
                    this._member = value;
                    AttributeChanged>(a => a.member);
                }
            }
        }
    }
}

OrganizationalUnitObject.cs

using System;
using LinqToLdap.Mapping;
using System.Collections.ObjectModel;

namespace ilminator.LinqToLdap.DirectoryObjects
{
    public class OrganizationalUnitObject : Top
    {
        /// <summary>
        /// Organizational-Unit-Name (ou). The name of the organizational unit.
        /// [From: http://msdn.microsoft.com/en-us/library/windows/desktop/ms679096.aspx]
        /// </summary>
        public String organizationalUnitName { get; set; }

        private Collection _description;     // top (see note on Top)
        /// <summary>
        /// Description. Contains the description to display for an object. In
        /// Active Directory, this value is restricted as single-valued for
        /// backward compatibility in some cases but is allowed to be multi-valued
        /// in others. See 'Remarks' on the follow link.
        /// [From: http://msdn.microsoft.com/en-us/library/windows/desktop/ms675492.aspx]
        /// </summary>
        public Collection description
        {
            get { return this._description; }
            set
            {
                if (value != this._description)
                {
                    this._description = value;
                    AttributeChanged>(a => a.description);
                }
            }
        }

        private string _managedBy;              // organizationalUnit
        /// <summary>
        /// The distinguished name of the user that is assigned to manage this object.
        /// </summary>
        public String managedBy
        {
            get { return this._managedBy; }
            set
            {
                if (value != this._managedBy)
                {
                    this._managedBy = value;
                    AttributeChanged(a => a.managedBy);
                }
            }
        }
    }
}

UserObject.cs

using System;
using System.Collections.ObjectModel;
using LinqToLdap.Mapping;

namespace ilminator.LinqToLdap.DirectoryObjects
{
    public class UserObject : SecPrinMailRecipEtc
    {
        public UserObject()
        {
            //this.accountExpires = 0x7FFFFFFFFFFFFFFF;
            this._userAccountControl = 546;
        }

        /// <summary>
        /// Returns a unicode-encoded byte array based on the incoming password string.
        /// Use this to convert your password value into the format required by
        /// <param name="password">The password to turn into a byte array</param>.
        /// </summary>
        ///The password to turn into a byte array
        public static Byte[] GetPasswordData(string password)
        {
            var formattedPassword = String.Format("\"{0}\"", password);
            return System.Text.Encoding.Unicode.GetBytes(formattedPassword);
        }

        #region ----- directory object properties and fields  -----

        private long _accountExpires;                      // organizationalPerson
        /// <summary>
        /// Account-Expires. The date when the account expires. This value
        /// represents the number of 100-nanosecond intervals since
        /// January 1, 1601 (UTC). A value of 0 or 0x7FFFFFFFFFFFFFFF
        /// (9223372036854775807) indicates that the account never expires.
        /// [From: http://msdn.microsoft.com/en-us/library/windows/desktop/ms675098.aspx]
        /// </summary>
        public Int64 accountExpires
        {
            get { return this._accountExpires; }
            set
            {
                if (value != this._accountExpires)
                {
                    this._accountExpires = value;
                    AttributeChanged(a => a.accountExpires);
                }
            }
        }

        /// <summary>
        /// Bad-Password-Time. The last time and date that an attempt to log on
        /// to this account was made with a password that is not valid. This value
        /// is stored as a large integer that represents the number of
        /// 100-nanosecond intervals since January 1, 1601 (UTC).
        /// A value of zero means that the last time a incorrect password was
        /// used is unknown. This attribute is not replicated and is maintained
        /// separately on each domain controller in the domain. To get an accurate
        /// value for the user's last bad password time in the domain, each domain
        /// controller in the domain must be queried. The largest value that is
        /// obtained represents the true bad password time.
        /// [From: http://msdn.microsoft.com/en-us/library/windows/desktop/ms675243.aspx]
        /// </summary>
        public Nullable badPasswordTime { get; set; }

        /// <summary>
        /// The number of times the user tried to log on to the account using an
        /// incorrect password. A value of 0 indicates that the value is unknown.
        /// This attribute is not replicated and is maintained separately on each
        /// domain controller in the domain. This attribute is reset on a specific
        /// domain controller when the user successfully logs onto that domain
        /// controller.
        /// [From: http://msdn.microsoft.com/en-us/library/windows/desktop/ms675244.aspx]
        /// </summary>
        public Nullable badPwdCount { get; set; }

        private string _c;                      // organizationalPerson
        /// <summary>
        /// Country-Name (e.g. GB). Set when 'Country/region' is set in ADU&C.
        /// </summary>
        public String c
        {
            get { return this._c; }
            set
            {
                if (value != this._c)
                {
                    this._c = value;
                    AttributeChanged(a => a.c);
                }
            }
        }

        private string _co;                // organizationalPerson
        /// <summary>
        /// Text-Country (e.g. United Kingdom). Set when 'Country/region' is set in ADU&C.
        /// </summary>
        public String co
        {
            get { return this._co; }
            set
            {
                if (value != this._co)
                {
                    this._co = value;
                    AttributeChanged(a => a.co);
                }
            }
        }

        private string _comment;                // organizationalPerson
        /// <summary>
        /// User-Comment.
        /// </summary>
        public String comment
        {
            get { return this._comment; }
            set
            {
                if (value != this._comment)
                {
                    this._comment = value;
                    AttributeChanged(a => a.comment);
                }
            }
        }

        private string _company;                // organizationalPerson
        /// <summary>
        /// Company.
        /// </summary>
        public String company
        {
            get { return this._company; }
            set
            {
                if (value != this._company)
                {
                    this._company = value;
                    AttributeChanged(a => a.company);
                }
            }
        }

        private int _countryCode;                // organizationalPerson
        /// <summary>
        /// Country-Code (e.g. 826).  Set when 'Country/region' is set in ADU&C.
        /// </summary>
        public Int32 countryCode
        {
            get { return this._countryCode; }
            set
            {
                if (value != this._countryCode)
                {
                    this._countryCode = value;
                    AttributeChanged(a => a.countryCode);
                }
            }
        }

        private string _department;                // organizationalPerson
        /// <summary>
        /// Department.
        /// </summary>
        public String department
        {
            get { return this._department; }
            set
            {
                if (value != this._department)
                {
                    this._department = value;
                    AttributeChanged(a => a.department);
                }
            }
        }

        private string _division;                // organizationalPerson
        /// <summary>
        /// Division.
        /// </summary>
        public String division
        {
            get { return this._division; }
            set
            {
                if (value != this._division)
                {
                    this._division = value;
                    AttributeChanged(a => a.division);
                }
            }
        }

        private string _employeeID;              // organizationalPerson
        /// <summary>
        /// Employee-ID.
        /// </summary>
        public String employeeID
        {
            get { return this._employeeID; }
            set
            {
                //TODO: Trap length > 16
                if (value != this._employeeID)
                {
                    this._employeeID = value;
                    AttributeChanged(a => a.employeeID);
                }
            }
        }

        private string _employeeNumber;                // user
        /// <summary>
        /// Employee-Number.
        /// </summary>
        public String employeeNumber
        {
            get { return this._employeeNumber; }
            set
            {
                if (value != this._employeeNumber)
                {
                    this._employeeNumber = value;
                    AttributeChanged(a => a.employeeNumber);
                }
            }
        }

        private string _employeeType;                // user
        /// <summary>
        /// Employee-Type.
        /// </summary>
        public String employeeType
        {
            get { return this._employeeType; }
            set
            {
                if (value != this._employeeType)
                {
                    this._employeeType = value;
                    AttributeChanged(a => a.employeeType);
                }
            }
        }

        private string _givenName;                // organizationalPerson & user
        /// <summary>
        /// Given-Name.
        /// </summary>
        public String givenName
        {
            get { return this._givenName; }
            set
            {
                if (value != this._givenName)
                {
                    this._givenName = value;
                    AttributeChanged(a => a.givenName);
                }
            }
        }

        private string _homeDirectory;          // posixAccount & user
        /// <summary>
        /// Home-Directory.
        /// </summary>
        public String homeDirectory
        {
            get { return this._homeDirectory; }
            set
            {
                if (value != this._homeDirectory)
                {
                    this._homeDirectory = value;
                    AttributeChanged(a => a.homeDirectory);
                }
            }
        }

        private string _homeDrive;                // user
        /// <summary>
        /// Home-Drive.
        /// </summary>
        public String homeDrive
        {
            get { return this._homeDrive; }
            set
            {
                if (value != this._homeDrive)
                {
                    this._homeDrive = value;
                    AttributeChanged(a => a.homeDrive);
                }
            }
        }

        private string _initials;                // organizationalPerson & user
        /// <summary>
        /// Initials.
        /// </summary>
        public String initials
        {
            get { return this._initials; }
            set
            {
                if (value != this._initials)
                {
                    this._initials = value;
                    AttributeChanged(a => a.initials);
                }
            }
        }

        private byte[] _jpegPhoto;                // user
        /// <summary>
        /// jpegPhoto. Used to store one or more images of a person using
        /// the JPEG File Interchange Format [JFIF].
        /// </summary>
        public Byte[] jpegPhoto
        {
            get { return this._jpegPhoto; }
            set
            {
                if (value != this._jpegPhoto)
                {
                    this._jpegPhoto = value;
                    AttributeChanged(a => a.jpegPhoto);
                }
            }
        }

        private string _l;                // organizationalPerson
        /// <summary>
        /// Locality-Name.
        /// </summary>
        public String l
        {
            get { return this._l; }
            set
            {
                if (value != this._l)
                {
                    this._l = value;
                    AttributeChanged(a => a.l);
                }
            }
        }

        /// <summary>
        /// Last-Logon-Timestamp. This is the time that the user last logged into
        /// the domain. Whenever a user logs on, the value of this attribute is
        /// read from the DC. If the value is older [ current_time - msDS-LogonTimeSyncInterval ],
        /// the value is updated. The initial update after the raise of the domain
        /// functional level is calculated as 14 days minus random percentage of 5 days.
        /// [From: http://msdn.microsoft.com/en-us/library/windows/desktop/ms676824.aspx]
        /// </summary>
        /// <seealso cref="http://blogs.technet.com/b/askds/archive/2009/04/15/the-lastlogontimestamp-attribute-what-it-was-designed-for-and-how-it-works.aspx"/>
        public Nullable lastLogonTimestamp { get; set; }   // user

        // lockoutTime

        /// <summary>
        /// Logon-Count. The number of times the account has successfully logged
        /// on. A value of 0 indicates that the value is unknown. This attribute
        /// is not replicated and is maintained on each domain controller in the
        /// domain. Due to compatibility with 16-bit versions of LAN Manager,
        /// the attribute has an upper limit of 65535. After this limit has been
        /// reached, you cannot use it as an indicator of user activity on this
        /// domain controller.
        /// [From: http://msdn.microsoft.com/en-us/library/windows/desktop/ms676845.aspx]
        /// </summary>
        public Nullable logonCount { get; set; }           // user

        private string _manager;        // organizationalPerson & user
        /// <summary>
        /// Manager. The distinguishedName of the manager object.
        /// </summary>
        public String manager
        {
            get { return this._manager; }
            set
            {
                if (value != this._manager)
                {
                    this._manager = value;
                    AttributeChanged(a => a.manager);
                }
            }
        }

        private string _mobile;        // organizationalPerson & user
        /// <summary>
        /// Phome-Mobile-Primary.
        /// </summary>
        public String mobile
        {
            get { return this._mobile; }
            set
            {
                if (value != this._mobile)
                {
                    this._mobile = value;
                    AttributeChanged(a => a.mobile);
                }
            }
        }

        private Collection _otherTelephone;
        /// <summary>
        /// Phone-Office-Other.
        /// </summary>
        public Collection otherTelephone
        {
            get { return this._otherTelephone; }
            set
            {
                if (value != this._otherTelephone)
                {
                    this._otherTelephone = value;
                    AttributeChanged>(a => a.otherTelephone);
                }
            }
        }

        /// <summary>
        /// Pwd-Last-Set. The date and time that the password for this account
        /// was last changed. This value is stored as a large integer that
        /// represents the number of 100 nanosecond intervals since
        /// January 1, 1601 (UTC). If this value is set to 0 and the
        /// <see cref="userAccountControl"/> attribute does not contain the 
        /// <see cref="UF_DONT_EXPIRE_PASSWD"/> flag, then the user must set the password at
        /// the next logon.
        /// [From: http://msdn.microsoft.com/en-us/library/windows/desktop/ms679430.aspx]
        /// </summary>
        public Int64 pwdLastSet { get; set; }

        private string _personalTitle;        // organizationalPerson
        /// <summary>
        /// Personal-Title. The title independent of job function, e.g. Mr or Ms.
        /// </summary>
        public String personalTitle
        {
            get { return this._personalTitle; }
            set
            {
                if (value != this._personalTitle)
                {
                    this._personalTitle = value;
                    AttributeChanged(a => a.personalTitle);
                }
            }
        }

        private string _physicalDeliveryOfficeName;        // organizationalPerson
        /// <summary>
        /// Physical-Delivery-Office-Name. Shown as 'Office' in ADU&C.
        /// </summary>
        public String physicalDeliveryOfficeName
        {
            get { return this._physicalDeliveryOfficeName; }
            set
            {
                if (value != this._physicalDeliveryOfficeName)
                {
                    this._physicalDeliveryOfficeName = value;
                    AttributeChanged(a => a.physicalDeliveryOfficeName);
                }
            }
        }

        private string _postalCode;        // organizationalPerson
        /// <summary>
        /// Postal-Code.
        /// </summary>
        public String postalCode
        {
            get { return this._postalCode; }
            set
            {
                if (value != this._postalCode)
                {
                    this._postalCode = value;
                    AttributeChanged(a => a.postalCode);
                }
            }
        }

        private string _postOfficeBox;        // organizationalPerson
        /// <summary>
        /// Post-Office-Box.
        /// </summary>
        public String postOfficeBox
        {
            get { return this._postOfficeBox; }
            set
            {
                if (value != this._postOfficeBox)
                {
                    this._postOfficeBox = value;
                    AttributeChanged(a => a.postOfficeBox);
                }
            }
        }

        private string _profilePath;        // user
        /// <summary>
        /// Profile-Path.
        /// </summary>
        public String profilePath
        {
            get { return this._profilePath; }
            set
            {
                if (value != this._profilePath)
                {
                    this._profilePath = value;
                    AttributeChanged(a => a.profilePath);
                }
            }
        }

        //pwdLastSet

        private string _scriptPath;        // user
        /// <summary>
        /// Script-Path.
        /// </summary>
        public String scriptPath
        {
            get { return this._scriptPath; }
            set
            {
                if (value != this._scriptPath)
                {
                    this._scriptPath = value;
                    AttributeChanged(a => a.scriptPath);
                }
            }
        }

        private Collection _seeAlso;            // person
        /// <summary>
        /// See-Also. List of distinguished names that are related to an object.
        /// [From: http://msdn.microsoft.com/en-us/library/windows/desktop/ms679769.aspx]
        /// </summary>
        public Collection seeAlso
        {
            get { return this._seeAlso; }
            set
            {
                if (value != this._seeAlso)
                {
                    this._seeAlso = value;
                    AttributeChanged>(a => a.seeAlso);
                }
            }
        }

        private string _sn;                 // person
        /// <summary>
        /// Surname.
        /// </summary>
        public String sn
        {
            get { return this._sn; }
            set
            {
                if (value != this._sn)
                {
                    this._sn = value;
                    AttributeChanged(a => a.sn);
                }
            }
        }

        private string _st;        // organizationalPerson
        /// <summary>
        /// State-Or-Province-Name
        /// </summary>
        public String st
        {
            get { return this._st; }
            set
            {
                if (value != this._st)
                {
                    this._st = value;
                    AttributeChanged(a => a.st);
                }
            }
        }

        private string _street;        // organizationalPerson
        /// <summary>
        /// Street-Address. [c.f. <see cref="streetAddress"/>, which is named Street.]
        /// </summary>
        public String street
        {
            get { return this._street; }
            set
            {
                if (value != this._street)
                {
                    this._street = value;
                    AttributeChanged(a => a.street);
                }
            }
        }

        private string _streetAddress;        // organizationalPerson
        /// <summary>
        /// Address. This is property displayed in ADU&C as 'Street'. 
        /// [c.f. <see cref="street"/>, which is named Street-Address.]
        /// </summary>
        public String streetAddress
        {
            get { return this._streetAddress; }
            set
            {
                if (value != this._streetAddress)
                {
                    this._streetAddress = value;
                    AttributeChanged(a => a.streetAddress);
                }
            }
        }

        private byte[] _thumbnailPhoto;         // organizationalPerson
        /// <summary>
        /// Picture.
        /// </summary>
        public Byte[] thumbnailPhoto
        {
            get { return this._thumbnailPhoto; }
            set
            {
                if (value != this._thumbnailPhoto)
                {
                    this._thumbnailPhoto = value;
                    AttributeChanged(a => a.thumbnailPhoto);
                }
            }
        }

        private string _title;                  // organizationalPerson
        /// <summary>
        /// Title.
        /// </summary>
        public String title
        {
            get { return this._title; }
            set
            {
                if (value != this._title)
                {
                    this._title = value;
                    AttributeChanged(a => a.title);
                }
            }
        }

        private string _uid;                    // user, shadowAccount and posixAccount
        /// <summary>
        /// A user ID. Usually used by UNIX for the login.
        /// </summary>
        public String uid
        {
            get { return this._uid; }
            set
            {
                if (value != this._uid)
                {
                    this._uid = value;
                    AttributeChanged(a => a.uid);
                }
            }
        }

        private byte[] _unicodePwd;             // user
        /// <summary>
        /// Unicode-Pwd. The ActiveDirectory password. It is write-only
        /// and can only be written over an SSL connection.
        /// </summary>
        public Byte[] unicodePwd
        {
            get { return this._unicodePwd; }
            set
            {
                if (value != this._unicodePwd)
                {
                    this._unicodePwd = value;
                    AttributeChanged(a => a.unicodePwd);
                }
            }
        }

        private int _userAccountControl;        // user
        /// <summary>
        /// User-Acccount-Control. Flags that control the behavior of the user account.
        /// See <see cref="UserAccountControlFlags"/>.
        /// [From: http://msdn.microsoft.com/en-us/library/windows/desktop/ms680832.aspx]
        /// </summary>
        public Int32 userAccountControl
        {
            get { return this._userAccountControl; }
            set
            {
                if (value != this._userAccountControl)
                {
                    this._userAccountControl = value;
                    AttributeChanged(a => a.userAccountControl);
                }
            }
        }

        private string _userPrincipalName;      // user
        /// <summary>
        /// User-Principal-Name.
        /// </summary>
        public String userPrincipalName
        {
            get { return this._userPrincipalName; }
            set
            {
                if (value != this._userPrincipalName)
                {
                    this._userPrincipalName = value;
                    AttributeChanged(a => a.userPrincipalName);
                }
            }
        }

        #endregion ----- directory object properties and fields -----
    }
}

GroupObjectMap.cs

using System;
using System.Linq;
using LinqToLdap.Mapping;

namespace ilminator.LinqToLdap.DirectoryObjects.ClassMaps
{
    public class GroupObjectMap : ClassMap
    {
        public GroupObjectMap()
        {
            NamingContext("DC=big,DC=wooden,DC=badger");   // Replace with your defaultNamingContext
            ObjectClass("group");
            ObjectCategory("group");

            Map(x => x.groupType).EnumStoredAsInt();
            Map(x => x.managedBy);
            Map(x => x.member);

            //From SecPrinMailRecipEtc
            Map(x => x.commonName).Named("cn").StoreGenerated();
            Map(x => x.description);
            Map(x => x.info);
            Map(x => x.mail);
            Map(x => x.objectSid).StoreGenerated();
            Map(x => x.sAMAccountName);
            Map(x => x.sAMAccountType).StoreGenerated();
            Map(x => x.telephoneNumber);

            // From Top
            DistinguishedName(x => x.distinguishedName);

            Map(x => x.displayName);
            Map(x => x.name).StoreGenerated();
            Map(x => x.objectGuid).StoreGenerated();
            Map(x => x.uSNChanged).StoreGenerated();
            Map(x => x.uSNCreated).StoreGenerated();
            Map(x => x.whenChanged).StoreGenerated();
            Map(x => x.whenCreated).StoreGenerated();
            Map(x => x.wWWHomePage);
        }
    }
}

OrganizationalUnitObjectMap.cs

using System;
using LinqToLdap.Mapping;

namespace ilminator.LinqToLdap.DirectoryObjects.ClassMaps
{
    public class OrganizationalUnitObjectMap : ClassMap
    {
        public OrganizationalUnitObjectMap()
        {
            NamingContext("DC=big,DC=wooden,DC=badger");   //   Replace with your defaultNamingContext
            ObjectClass("organizationalUnit");
            ObjectCategory("organizationalUnit");

            Map(x => x.organizationalUnitName).Named("ou").StoreGenerated();
            Map(x => x.description);
            Map(x => x.managedBy);

            // From Top
            DistinguishedName(x => x.distinguishedName);

            Map(x => x.displayName);
            Map(x => x.name).StoreGenerated();
            Map(x => x.objectGuid).StoreGenerated();
            Map(x => x.uSNChanged).StoreGenerated();
            Map(x => x.uSNCreated).StoreGenerated();
            Map(x => x.whenChanged).StoreGenerated();
            Map(x => x.whenCreated).StoreGenerated();
            Map(x => x.wWWHomePage);
        }
    }
}

UserObjectMap.cs

using System;
using System.Linq;
using LinqToLdap.Mapping;

namespace ilminator.LinqToLdap.DirectoryObjects.ClassMaps
{
    public class UserObjectMap : ClassMap
    {
        public UserObjectMap()
        {
            NamingContext("DC=big,DC=wooden,DC=badger");   // Replace with your defaultNamingContext
            ObjectClass("user");
            ObjectCategory("person");

            Map(x => x.accountExpires);
            Map(x => x.badPasswordTime).StoreGenerated().DateTimeFormat(null);
            Map(x => x.badPwdCount).StoreGenerated();
            Map(x => x.c);
            Map(x => x.co);
            Map(x => x.comment);
            Map(x => x.company);
            Map(x => x.countryCode);
            Map(x => x.department);
            Map(x => x.division);
            Map(x => x.employeeID);
            Map(x => x.employeeNumber);
            Map(x => x.employeeType);
            Map(x => x.givenName);
            Map(x => x.homeDirectory);
            Map(x => x.homeDrive);
            Map(x => x.initials);
            Map(x => x.jpegPhoto);
            Map(x => x.l);
            Map(x => x.lastLogonTimestamp).StoreGenerated().DateTimeFormat(null);
            Map(x => x.logonCount).StoreGenerated();
            Map(x => x.manager);
            Map(x => x.mobile);
            Map(x => x.otherTelephone);
            Map(x => x.personalTitle);
            Map(x => x.physicalDeliveryOfficeName);
            Map(x => x.postalCode);
            Map(x => x.postOfficeBox);
            Map(x => x.profilePath);
            Map(x => x.pwdLastSet).StoreGenerated().DateTimeFormat(null);
            Map(x => x.scriptPath);
            Map(x => x.seeAlso);
            Map(x => x.sn);
            Map(x => x.st);
            Map(x => x.street);
            Map(x => x.streetAddress);
            Map(x => x.thumbnailPhoto);
            Map(x => x.title);
            Map(x => x.uid);
            Map(x => x.unicodePwd);
            Map(x => x.userAccountControl);
            Map(x => x.userPrincipalName);

            //From SecPrinMailRecipEtc
            Map(x => x.commonName).Named("cn").StoreGenerated();
            Map(x => x.description);
            Map(x => x.info);
            Map(x => x.mail);
            Map(x => x.objectSid).StoreGenerated();
            Map(x => x.sAMAccountName);
            Map(x => x.sAMAccountType).StoreGenerated();
            Map(x => x.telephoneNumber);

            // From Top
            DistinguishedName(x => x.distinguishedName);

            Map(x => x.displayName);
            Map(x => x.name).StoreGenerated();
            Map(x => x.objectGuid).StoreGenerated();
            Map(x => x.uSNChanged).StoreGenerated();
            Map(x => x.uSNCreated).StoreGenerated();
            Map(x => x.whenChanged).StoreGenerated();
            Map(x => x.whenCreated).StoreGenerated();
            Map(x => x.wWWHomePage);
        }
    }
}

GroupTypes.cs

using System;

namespace ilminator.LinqToLdap.DirectoryObjects
{
    public enum GroupTypes
    {
        GlobalDistribution = 2,                     // 0x00000002
        DomainLocalDistribution = 4,                // 0x00000004
        UniversalDistribution = 8,                  // 0x00000008
        AzManBasic = 16,                            // 0x00000010
        AzManLdapQuery = 32,                        // 0x00000020
        GlobalSecurity = -2147483646,               // 0x80000002
        DomainLocalSecurity = -2147483644,          // 0x80000004
        BuiltinDomainLocalSecurity = -2147483643,   // 0x80000005
        UniversalSecurity = -2147483640             // 0x80000008
    }
}

UserAccountControlFlags.cs

using System;

namespace ilminator.LinqToLdap.DirectoryObjects
{
    [Flags]
    public enum UserAccountControlFlags
    {
        SCRIPT = 0x0001,                                // 1
        ACCOUNTDISABLE = 0x0002,                        // 2
        HOMEDIR_REQUIRED = 0x0008,                      // 8
        LOCKOUT = 0x0010,                               // 16
        PASSWD_NOTREQD = 0x0020,                        // 32
        PASSWD_CANT_CHANGE = 0x0040,                    // 64
        ENCRYPTED_TEXT_PWD_ALLOWED = 0x0080,            // 128
        TEMP_DUPLICATE_ACCOUNT = 0x0100,                // 256
        NORMAL_ACCOUNT = 0x0200,                        // 512
        INTERDOMAIN_TRUST_ACCOUNT = 0x0800,             // 2048
        WORKSTATION_TRUST_ACCOUNT = 0x1000,             // 4096
        SERVER_TRUST_ACCOUNT = 0x2000,                  // 8192
        DONT_EXPIRE_PASSWORD = 0x10000,                 // 65536
        MNS_LOGON_ACCOUNT = 0x20000,                    // 131072
        SMARTCARD_REQUIRED = 0x40000,                   // 262144
        TRUSTED_FOR_DELEGATION = 0x80000,               // 524288
        NOT_DELEGATED = 0x100000,                       // 1048576
        USE_DES_KEY_ONLY = 0x200000,                    // 2097152
        DONT_REQ_PREAUTH = 0x400000,                    // 4194304
        PASSWORD_EXPIRED = 0x800000,                    // 8388608
        TRUSTED_TO_AUTH_FOR_DELEGATION = 0x1000000      // 16777216
    }
}

Finally, build the solution, and you’re ready to go.

I’ve uploaded two versions of this, one with the LinqToLdap assembly files included and one without. Links are on the Downloads page.

Mapping directory syntaxes to .NET types

LinqToLdap will take care of the nitty-gritty of transmuting the directory data into CLR classes: all you have to do is define your directory object class.  This article explains which directory syntaxes map to which CLR classes and provides examples of each.  In the definition, I give the CLR type first and the C# type afterwards: the framework design guidelines recommend that you should use the  CLR type name to ensure cross-language interoperability.

Unicode String

attributeSyntax: 2.5.5.12
oMSyntax: 64

Example: displayName

Maps to: System.String, string

DistinguishedName

attributeSyntax: 2.5.5.1
oMSyntax: 127

Example: manager

Maps to: System.String, string

Integer

attributeSyntax: 2.5.5.9
oMSyntax: 2

Examples:  userAccountControl, logonCount

Maps to: System.Int32, int

Note: in version 2.0.2, <not set> is translated to 0 (zero).

Large Integer/Interval

attributeSyntax: 2.5.5.16
oMSyntax: 65

Examples:  uSNCreated, uSNChanged, pwdLastSet, lastLogonTimestamp

Maps to: System.Int64, long

Note: in version 2.0.2, <not set> is translated to 0 (zero).

Generalized Time

aka UTC Coded Time

attributeSyntax: 2.5.5.1z1
oMSyntax: 24

Examples:  whenCreated, whenChanged

Maps to: System.DateTime

Note: in version 2.0.2, <not set> is translated to 0 (zero).

Octet String

attributeSyntax: 2.5.5.10
oMSyntax: 4

Example:  objectGuid

Maps to: System.Guid

SID

attributeSyntax: 2.5.5.17
oMSyntax: 4

Example:  objectSid

Maps to: System.Security.Principal.SecurityIdentifier

Additional Notes

Sometimes, it’s necessary to know the purpose of the attribute to be able to interpret it.  For example, pwdLastSet is a Large Integer/Interval but it’s used to store a datetime (note the lack of PascalCasing, there) so LinqToLdap will translate it as an Int64 or DateTime, depending on how you map it:

Map(x => x.lastLogonTimestamp).StoreGenerated();

will map to an Int64, whereas,

Map(x => x.lastLogonTimestamp).DateTimeFormat(null).StoreGenerated();

will map to a DateTime.  Note that in version 2.0.2, <not set> is translated to 0 (zero) (which may be translated to “01/01/0001 00:00:00” if you’ve gone for DateTimeFormat(null).

Finally

If there are any I’ve missed, please let me know and I’ll add them.

How to define your directory object classes and ClassMaps

This article will show how to create a directory object class and associated ClassMap.

For the purposes of this article, I’m going to use the term ‘LDAP object’ to refer to an instance of user or organizationalUnit in the directory and ‘LDAP object class’ to refer to their definitions.  I’ll use ‘directory object class’ to refer to the class that represents the LDAP object in our program.

In my earlier article OrganinizationalUnit CRUD, I showed a simple POCO (which was our directory object class) and a ClassMap but pointed out that whilst they would work, they weren’t really the right way to do things.

This is what we had as the directory object class:

using System;
using LinqToLdap.Mapping;

namespace BlogOuTest1
{
    public class OrganizationalUnitObject
    {
        public Guid objectGuid { get; set; }
        public String distinguishedName { get; set; }
        public String organizationalUnitName { get; set; }
        public String description { get; set; }
        public DateTime whenCreated { get; set; }
        public DateTime whenChanged { get; set; }
    }
}

and this is what we had as the ClassMap:

using System;
using LinqToLdap.Mapping;

namespace BlogOuTest1
{
    public class OrganizationalUnitObjectMap : ClassMap
    {
        public OrganizationalUnitObjectMap()
        {
            NamingContext("DC=big,DC=wooden,DC=badger");    // Update this
            ObjectClass("organizationalUnit");
            ObjectCategory("organizationalUnit");
            DistinguishedName(x => x.distinguishedName);

            Map(x => x.organizationalUnitName).Named("ou").StoreGenerated();
            Map(x => x.description);
            Map(x => x.objectGuid).StoreGenerated();
            Map(x => x.whenChanged).StoreGenerated();
            Map(x => x.whenCreated).StoreGenerated();
        }
    }
}

So, what’s wrong with them?  And why did they work?  Actually, there’s nothing wrong with the OrganizationalUnitObjectMap, at the moment, but we need it to highlight what’s wrong with the OrganizationalUnitObject class.

First, some definitions:

NamingContext – this tells LinqToLdap what to use as the starting point for queries (searches).  It can be a domainContext as shown here or an OU if you want to restrict the searches.

ObjectClass – the directory objectClass of the object being represented by this class.  Yes, I know, too many ‘objects’ and ‘classes’ in that definition but that’s the problem with using an object-oriented language to write programs against a directory which uses OO terms and an OO inheritance model.

ObjectCategory – the directory objectCategory of the object being represented by this class.  If both objectClass and objectCategory are defined, objectCategory is used by LinqToLdap.

DistinguishedName – sets the POCO property holding the distinguishedName of the directory class.

Map – defines a property mapping and returns an IPropertyMapper, which is a fluent interface.  If the name of the property on the POCO is the same as the attribute name on the directory class, only the property name is required, for example:

Map(x => x.description);

If they don’t match, the Named() method allows you to specify the name in the directory, for example:

Map(x => x.organizationalUnitName).Named("ou");

The IPropertyMapper interface also contains .StoreGenerated() which tells LinqToLdap not to include that property in the set of modifications to be written back to the directory.  Since we can’t update the ou attribute directly, the previous line becomes:

Map(x => x.organizationalUnitName).Named("ou").StoreGenerated();

All properties not marked with StoreGenerated that aren’t the DistinguishedName are candidates to be updated.  In fact, in a class defined like ours all properties not marked with StoreGenerated that aren’t the DistinguishedName are updated. There is a way to change this behaviour, more of which later.  Meanwhile, back at the farm…

The IPropertyMapper interface contains other methods but we’ll leave them for a while because we’ve reached the point at which the problems with the above class definitions can be illuminated.  If you look at the OrganizationalUnitObjectMap above, you’ll see that there’s only one property that isn’t marked StoreGenerated and that is description.  So, the only property we’ll want to update is description so it doesn’t matter than every time we run Update() all non-StoreGenerated, non-DN properties are updated because we’ve only got one and it’s the one we want to update.  But if we add another property, say displayName, then it starts to go awry.  Every time we update displayName, we’ll write description back to the directory and vice-versa.  The odd unnecessary update to the directory isn’t a calamity but multiply it by lots of properties on lots of objects and it could be.  There are also circumstances where it won’t work, for instance, unicodePwd over a non-SSL connection.  So, how do we avoid it?

DirectoryObjectBase provides a mechanism to track changes to directory object class instances:

    public abstract class DirectoryObjectBase
    {
        public bool TrackChanges { get; internal set; }
        protected void AttributeChanged(string attribute);
        protected void AttributeChanged<T, TProperty>(Expression<Func<T, TProperty>> attribute);
        protected void ClearAllChanged();
        public IEnumerable<string> GetChangedAttributes()
        protected void RemoveChanged(string attribute);
        protected void RemoveChanged<T, TProperty>(Expression<Func<T, TProperty>> attribute);
    }

So, we derive our directory object class from DirectoryObjectBase (you can see why I used the term ‘directory object class’ now) and use AttributeChanged in our property setter.

I’ve created a new project, called BlogOuTest2 for this, so you can compare the differences (note the new Namespace).  To start quickly, I copied the OrganizationalUnitObject.cs and OrganizationalUnitObjectMap.cs from the BlogOuTest1 project folder to BlogOuTest2 project folder.  I did Add > Existing to include the files in the new project.  I opened both files and updated the namespace statements to reflect the new namespace.  I added a reference to LinqToLdap.  Then I copied the static void Main(string[] args) method from Program.cs in BlogOuTest1 and to replace the static void Main(string[] args) in Program.cs in BlogOuTest2.  To make sure all was well, I ran it before making any changes.

Then, I modified OrganizationalUnitObject as follows:

using System;
using LinqToLdap.Mapping;

namespace BlogOuTest2
{
    public class OrganizationalUnitObject : DirectoryObjectBase
    {
        public Guid objectGuid { get; set; }
        public String distinguishedName { get; set; }
        public String organizationalUnitName { get; set; }

        private string _description;
        public String description
        {
            get { return this._description; }
            set
            {
                if (value != this._description)
                {
                    this._description = value;
                    this.AttributeChanged<OrganizationalUnitObject, String>(a => a.description);
                }
            }
        }

        public DateTime whenCreated { get; set; }
        public DateTime whenChanged { get; set; }
    }
}

Now, when we submit our Update(), LinqToLdap will only update attributes that have been marked using one of the overloads of AttributeChanged.

So, now we know how to set up directory objects, we can look at how to map the different directory syntaxes, in the next article.

Restricting the scope of the query to a single OU with LinqToLdap

This article builds on OrganizationalUnit CRUD, providing more options when creating queries.

SearchScope

One thing you might want to do when searching is limit your search to a single organizationalUnit/container.  You can do this by specifying the SearchScope as a parameter to the query.

Using the project from OrganizationalUnit CRUD, you’ll need to add a reference to System.DirectoryServices.ProtocolsS.DS.P is the technology underlying LinqToLdap and you sometimes need to refer to S.DS.P objects to configure LinqToLdap.

The original version of Program.cs is:

using System;
using System.Linq;
using LinqToLdap;

namespace BlogOuTest1
{
    class Program
    {
        static void Main(string[] args)
        {
            var config = new LdapConfiguration()
                .AddMapping(new OrganizationalUnitObjectMap());

            config.ConfigureFactory("dc1");

            var counter = 0;
            var context = new DirectoryContext();

            // ----- Start of section to replace -----
            var ous = context.Query<OrganizationalUnitObject>();

            foreach (var ou in ous)
            {
                counter++;
                Console.WriteLine("Ou: {0}",
                    ou.organizationalUnitName);
            }
            // ----- End of section to replace -----

            Console.WriteLine("Counter = {0}", counter);
            Console.WriteLine("\r\nPress a key to continue...");
            Console.ReadKey(true);
        }
    }
}

with the usual OrganizationalUnitObject.cs and OrganizationalUnitObjectMap.cs files.

On my network, this produces a list of 500 OUs.  At the top level, I have only be twelve OUs and eleven other containers.  To view the twelve OUs, change as follows.

Add the word SearchScope between the parentheses of the line

var ous = context.Query<OrganizationalUnitObject>();

You’ll see a little blue line appear under the first ‘S’:

Some of you will know that this is part of IntelliSense and will give you the option of adding a Namespace or a fully-qualified class name.  Some of you won’t know that instead of hovering over the line and waiting for the menu icon , then hovering over that and waiting for the menu to appear, you can type Shift+Alt+F10 and bring the menu up immediately:

Select the first option, “using System.DirectoryServices.Protocols;” and you should see the following added to the bottom of your list of using statements at the top of the file:

using System.DirectoryServices.Protocols;

Now you can complete the line such that it looks like this:

var ous = context.Query<OrganizationalUnitObject>(SearchScope.OneLevel);

When I run this, I get the expected twelve OUs.

Unfortunately, it doesn’t appear to be possible to specify the source OU in this version of LinqToLdap, so you’re limited to the namingContext specified in the ClassMap (see MadHatter22’s second comment in this thread, at Dec 21 2011 at 2:58 AM).  That functionality is available in the current source code – I’ll blog about it in a future post.

I’ll address the other eleven containers in another article.